How to Recreate the Liquid Glass Effect Using HTML Canvas
What is Refraction?
When light passes through a lens with rounded edges, such as a water droplet or a convex lens, the phenomenon of refraction causes the light rays to bend differently depending on where they enter the lens.
At the center, the surface is almost flat, so the rays are slightly deviated and the image appears quite sharp and accurate. However, towards the edges, the curvature increases: the light rays enter at a steeper angle relative to the normal (the line perpendicular to the surface) and are deviated more. This causes a stretching, bending, and non-uniform magnification effect on the underlying image.
💡 How to Reproduce It
To technically reproduce the refraction effect of a lens with rounded edges, a pixel remapping technique based on the distance from the edge is used.
Capture the Background: The underlying image is saved in a buffer (boardBufferData). This is used to read the original pixels that will then be "distorted" by the droplet.
Lens Shape: The shape of the lens is defined, for instance, a rectangle with rounded corners. Only the pixels inside this shape will be modified.
Calculate Distance from the Edge: For each pixel inside the lens, the code calculates how close it is to the edge. The closer the pixel is to the edge, the more intense the refraction effect will be.
Distortion Factor: A cubic easing (Math.pow(1 - norm, 3)) is used to provide a natural attenuation from the edge towards the center. This generates a more "physical" curvature similar to that observed in a real droplet.
Pixel Remapping: Each point on the lens is moved back towards the center proportionally, simulating the deviation of light rays. It is as if each pixel is "pulled" from the center based on the curvature.
Final Rendering: The distorted buffer is drawn on a secondary canvas and then projected onto the main canvas. Dynamic reflections and a Fresnel effect at the edges are added to simulate light bending.
A bit of code:
// Dimensions of the droplet
const W = /* width of the droplet */ 300;
const H = /* height of the droplet */ 150;
// Inner radius used to dampen the distortion (in pixels)
const R = /* radius */ Math.min(W, H) / 3;
// Distortion coefficient (e.g., –0.3 for a convex lens)
const k = /* lensStrength */ -0.3;
// Coordinates of the center of the droplet in the output buffer
const cx = W / 2;
const cy = H / 2;
// src: 2D pixel array of the background (buffer.getImageData)
const src = backgroundPixelArray;
// dst: 2D pixel array for the droplet (created with createImageData)
const dst = dropletPixelArray;
// Loop over each pixel of the droplet
for (let j = 0; j < H; j++) {
for (let i = 0; i < W; i++) {
// 1. Normalized distance from the edge (0 at the edge, 1 at the center)
let norm = Math.min(i, W - i, j, H - j) / R;
// 2. Distortion factor: greater at the edges, zero at the center
let f = 1 + k * Math.pow(1 - norm, 2);
// 3. "Refracted" coordinates on the background buffer
let sx = Math.round(cx + (i - cx) * f);
let sy = Math.round(cy + (j - cy) * f);
// 4. Copy the refracted pixel if within bounds
if (sx >= 0 && sx < canvasWidth && sy >= 0 && sy < canvasHeight) {
dst[j][i] = src[sy][sx];
}
}
}
// Finally, apply dst with putImageData on the canvas
In short, instead of using real light rays, the code simulates refraction by modifying the sampling coordinates of the pixels, precisely mimicking what happens to light passing through a curved transparent object.
🔍 Bonus 1 – HD Quality with Scaled Canvas
The internal canvas has a very high resolution but is displayed smaller in HTML. This trick allows the image to remain sharp even when distorted, avoiding the pixelated effect, especially on the edges of the drop.
⚙️ Bonus 2 – High Performance
The background is drawn only once in a buffer, so it doesn't need to be recalculated each time. Additionally, only the area of the drop is updated, reducing the workload and keeping the effect smooth and lightweight.
I hope you enjoyed reading the tutorial. If you have any questions or suggestions, feel free to reach out!