GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

HTML5 Canvas Animation: 9 Warp & Droplet Effects

HTML5 canvas animation bends pixels — a droplet swelling the text under it, a black hole pulling the screen in — no retouching, no 3D engine.

Auto-plays · click a tile to jump to its section · all nine in one zip

The order is not a ranking by flash; it is the order in which the distortion is built. First pixels are pulled out and laid back down as refraction (01), then the illusion of depth appears (02, 03, 04), then whole-screen feedback flows (05, 06), and finally light and fog dress the entire scene (07, 08, 09). The canvas particle engine that scatters grains and the physics interaction piece covering springs and gravity are this article's neighbors.

01Water droplet lens

A hero piece where the background is painted once and only the pixels under the droplet are pulled out and re-laid by a spherical refraction formula, like a drop rolling across a product photo.

Water droplet lens
Open in a new tab

01 · Water droplet lens — Measured area 32.968% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

getImageDataputImageDatacreateRadialGradient

Magnification is just moving the sample coordinates

  for (var y = 0; y < sh; y++) {
    for (var x = 0; x < sw; x++) {
      var i = (y * sw + x) * 4;
      var dx = x - ox, dy = y - oy;
      var dist = Math.sqrt(dx * dx + dy * dy);
      var rn = dist / pr;
      var j = i;
      if (rn < 1 && dist > 0) {
        var inc = Math.asin(rn);                 // angle of incidence
        var ref = Math.asin(rn / IOR);           // angle of refraction (Snell)
        var shift = depth * Math.tan(inc - ref); // how far inward we read from
        var k = (dist - shift) / dist;
        var mx = Math.round(ox + dx * k), my = Math.round(oy + dy * k);
        if (mx < 0) mx = 0; else if (mx >= sw) mx = sw - 1;
        if (my < 0) my = 0; else if (my >= sh) my = sh - 1;
        j = (my * sw + mx) * 4;
      }

What gets drawn stays untouched; only the sample point moves toward the droplet center, so the grid and letters swell under the lens. Snell's law is solved with the refractive index of water, 1.34, and dragging the droplet makes it glide to your hand (measured: 14,273 alpha pixels changed during the drag).

02Rising 3D orb field

Forty orbs each carry a z value, and a single division by the focal length creates perspective — a background drifting up behind a pricing section.

Rising 3D orb field
Open in a new tab

02 · Rising 3D orb field — Measured area 31.135% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

createRadialGradientctx.filterglobalAlpha

Perspective is a single division

  // Painter's algorithm — far to near by descending z. The only way to settle
  // overlap order in 2D, where there is no depth buffer.
  order.sort(function (a, b) { return b.z - a.z; });

  var blurNow = -1;
  for (var i = 0; i < COUNT; i++) {
    var o = order[i];
    var s = focal / (focal + o.z);                 // perspective projection — this one line is all of it
    var sx = W * 0.5 + (o.x * spanX * 0.5 - camX) * s;
    var sy = H * 0.5 + (o.y * spanY * 0.5 - camY) * s;
    var r = o.r * s;
    if (r < 0.8) continue;
    // Blur snaps to whole pixels — since the orbs are z-sorted, ctx.filter
    // only needs swapping about five times per frame.
    var blur = Math.round((o.z - Z_NEAR) / (Z_FAR - Z_NEAR) * MAX_BLUR * 1.2);

Multiplying coordinates and radius by the scale s draws identically sized orbs small when far and large when near. Distant orbs get a ctx.filter blur and low alpha, and moving the pointer tilts the camera slightly so the space itself appears to lean.

03Noise shader in 2D

Four layers of value noise are written pixel by pixel into a small 160x100 buffer that is then scaled up — a drifting cloud background to fill the gap between sections.

Noise shader in 2D
Open in a new tab

03 · Noise shader in 2D — Measured area 44.889% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

createImageDataimageSmoothingEnableddrawImage

Noise is stacked at double frequency

// fbm — the same noise stacked at double frequency and half amplitude.
// The layer count is the fineness of the grain.
function fbm(x, y, z, oct) {
  var sum = 0, amp = 0.5, norm = 0, f = 1;
  for (var i = 0; i < oct; i++) {
    sum += vnoise(x * f, y * f, z * f) * amp;
    norm += amp;
    amp *= 0.5;
    f *= 2;
  }
  return sum / norm;
}

This mimics a fragment shader loop but drops the resolution to 160x100, cutting the per-frame pixel math to about a hundredth of the screen. Time feeds in as the third argument so the clouds drift, and dropping the layers from 4 to 2 makes the grain chunky (measured: slider at 2 layers, 69,182 alpha pixels changed).

04Wobbling 3D text

The phrase is drawn once onto an offscreen canvas, then copied back one scanline at a time, each line pushed sideways by a sine wave — a rippling headline for an event page.

Wobbling 3D text
Open in a new tab

04 · Wobbling 3D text — Measured area 13.245% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

fillTextmeasureTextdrawImage

Each line is pushed by a sine phase

function pass(img, drop, t) {
  for (var y = 0; y < H; y += ROW) {
    var sx = 7 * Math.sin(0.055 * y + 2.4 * t) + 3 * Math.sin(0.021 * y - 1.5 * t + 1.4);
    var w = 1 + 0.05 * Math.sin(0.026 * y - 1.9 * t + 0.7);
    ctx.drawImage(img, 0, y * dpr, img.width, ROW * dpr, (W - W * w) * 0.5 + sx, y + drop, W * w, ROW);
  }
}

Two sines run as counter-marching waves, so a phase difference builds between lines and the letters ripple like a flag. A dark copy laid underneath gives the letters thickness, and typing your own phrase lets measureText re-center it (measured: 14,779 alpha pixels changed on phrase swap).

05Black hole tunnel

One feedback zoom — redrawing the previous frame slightly enlarged about the center — produces a tunnel that pulls inward forever, right before a page transition.

Black hole tunnel
Open in a new tab

05 · Black hole tunnel — Measured area 52.193% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

drawImageglobalCompositeOperationcreateRadialGradient

One feedback zoom is the whole tunnel

function frame(now) {
  var w = cv.width, h = cv.height;
  // 1. copy the previous frame onto an aux canvas
  actx.clearRect(0, 0, w, h);
  actx.drawImage(cv, 0, 0);
  // 2. enlarge about the center and lay it back — this single drawImage is the tunnel
  var dw = w * zoom, dh = h * zoom;
  ctx.globalCompositeOperation = 'source-over';
  ctx.drawImage(aux, 0, 0, w, h, (w - dw) * 0.5, (h - dh) * 0.5, dw, dh);
  // 3. add fresh specks — the lighter composite brightens where they overlap
  ctx.globalCompositeOperation = 'lighter';
  for (var i = 0; i < SEEDS; i++) speck();
  // 4. horizon and photon sphere
  horizon(now);
  raf = requestAnimationFrame(frame);
}

Enlarging the previous frame by 1.06 every pass pushes the specks outward, and the tunnel becomes infinitely deep. Lowering the zoom to 1.02 visibly slows the pull (measured: pixels changed in the same 0.4s fell from 63,768 to 57,853).

06Galaxy swirl

Six hundred stars get a radius and a starting angle once; every position after that comes from a formula — a calm background for a dark-mode dashboard.

Galaxy swirl
Open in a new tab

06 · Galaxy swirl — Measured area 30.838% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

globalCompositeOperationdestination-outMath.cos

Positions are computed, not stored

function draw(t) {
  // tails — never fully erased, just coated with low alpha
  ctx.globalCompositeOperation = 'destination-out';
  ctx.fillStyle = 'rgba(0, 0, 0, ' + FADE + ')';
  ctx.fillRect(0, 0, W, H);
  // stars and the hub glow go on with lighter, which brightens on overlap
  ctx.globalCompositeOperation = 'lighter';
  for (var i = 0; i < COUNT; i++) {
    var st = stars[i];
    var R = st.r * rmax + 8;                       // floor radius 8 keeps them from all piling into the center
    var a = st.a0 + t * K / (Math.pow(R, 0.8) + 20);
    var ex = Math.cos(a) * R;
    var ey = Math.sin(a) * R * FLAT;
    // pointer term — the whole field leans toward (px, py) by inf (density center borrows that spot)
    var x = cx + ex * cosT - ey * sinT + (px - cx) * inf;
    var y = cy + ex * sinT + ey * cosT + (py - cy) * inf;

Angular speed falls off toward the outside, so the inside spins fast and the rim slowly, and differential rotation stacks into spiral arms on its own. No array stores velocity or lifetime, so the same clock time always draws the same picture, while the low-alpha destination-out eraser leaves the trails behind.

07Fluid ripple surface

Two height grids take turns solving a wave equation, and the height difference between neighboring cells fetches pixels from the picture underneath — a banner water surface that answers your touch.

Fluid ripple surface
Open in a new tab

07 · Fluid ripple surface — Measured area 57.639% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

Float32ArraygetImageDataputImageData

Twice the neighbor average, minus the previous step

// Wave equation — twice the average of the four neighbors minus the previous
// value gives the next height. Solved by swapping two buffers.
function sim(steps) {
  for (var s = 0; s < steps; s++) {
    for (var y = 1; y < ROWS - 1; y++) {
      var row = y * COLS;
      for (var x = 1; x < COLS - 1; x++) {
        var i = row + x;
        prev[i] = ((curr[i - 1] + curr[i + 1] + curr[i - COLS] + curr[i + COLS]) * 0.5 - prev[i]) * DAMP;
      }
    }
    swap = prev; prev = curr; curr = swap;
  }
}
    for (var x = 0; x < RW; x++) {
      var cx = Math.round(x * kx);
      var cl = Math.max(0, cx - 1), cr = Math.min(COLS - 1, cx + 1);
      var hL = curr[rc + cl], hR = curr[rc + cr];
      var hU = curr[ru + cx], hD = curr[rd + cx];
      var sxp = x + (hL - hR) * REFR;

The wave itself is physics, but what makes it visible is refraction — the left-right and up-down height differences become offsets that pull in source pixels. Press or sweep the board and a cosine dip dents the water, spreads into rings and bounces back off the walls (measured: 79,762 alpha pixels changed during a drag).

08Scattered light rays

The silhouette layer is stacked twelve times, each copy scaled up a little around the light source, and the lighter composite turns the gaps into a fan of rays over a forest or window photo.

Scattered light rays
Open in a new tab

08 · Scattered light rays — Measured area 46.398% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

globalCompositeOperationlightercreateLinearGradient

Rays grow where the stack brightens

  ctx.globalAlpha = 1;
  ctx.drawImage(scene, 0, 0, W, H);
  // 2. twelve copies growing 1.03x each about the light — the lighter composite
  // grows the rays where copies overlap
  var s = 1;
  for (var i = 1; i <= STACK; i++) {
    s *= GROW;
    ctx.globalCompositeOperation = 'lighter';
    ctx.globalAlpha = 0.55 * (1 - i / (STACK + 1));
    ctx.drawImage(veil, lx * (1 - s), ly * (1 - s), W * s, H * s);
  }

Overlaying the dark veil twelve times, each copy drifting away from the light, accumulates brightness only in the gaps between trees. The light source eases toward your pointer (measured: 77,145 alpha pixels changed), and the arrow keys move it too.

09Depth fog scene

Six ridge layers stand in order of distance and fog color is mixed in exponentially with distance — a quiet, distant background for a travel landing page.

Depth fog scene
Open in a new tab

09 · Depth fog scene — Measured area 1.434% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

createLinearGradientfillRectglobalAlpha

Contrast melts away exponentially

  // ridges — back to front. Fog color mixes by distance: t = exp(-distance/density)
  for (var i = 0; i < LAYERS; i++) {
    var d = LAYERS - i;              // distance — the front layer is 1
    var t = Math.exp(-d / density);  // the contrast this layer keeps
    var off = (sec * meta[i].speed) % W;
    ctx.fillStyle = css3(mix3(SKY1C, INK, t));
    ctx.save();
    ctx.translate(-off, 0);
    ctx.fill(paths[i]);
    ctx.translate(W, 0);
    ctx.fill(paths[i]);

Exponential blending quickly melts distant layers into the sky color, so six ridges of the same ink read as a stepped panorama. Each layer drifts sideways at its own speed, doubling the depth cue, and raising the density from x2.4 to x5.0 thickens the fog until only the nearest ridge survives (measured: 46,650 alpha pixels changed).

How do html5 canvas animation examples bend real pixels?

The way html5 canvas 2d produces distortion is not drawing but re-reading. Take pixels already painted with getImageData, move only the sample coordinates and put them back (01), pull them by the height difference of their neighbors (07), or scale up the previous frame (05) — the same picture becomes a lens, a ripple or a tunnel. Changing only the paint order and color, as in the painter's algorithm and the exponential mix (02, 09), belongs to the same family.

All nine take their clock from the time requestAnimationFrame hands over; only 02 measures the gap between frames, and it clamps that gap at 0.05 seconds. In measurement the average 60-frame gap held at 16.4 to 16.6 milliseconds — pinned to 60fps — with every one of the 23 gap frames moving, and the accumulated area ranging from 1.4 percent on 09 to 57.6 percent on 07. Three functions of the html5 canvas api — read, write, composite — are the whole of the droplet and the black hole.

Where it breaks — glass turns into a gray ball

The first cut of the droplet read as a translucent gray ball, not a lens. Computing the refraction with the water thickness at half the radius made the magnification too weak: the background showed through almost unchanged, and the rim shadow layered on top became the protagonist. Only after raising the thickness to 0.9 of the radius did the grid visibly bend, and the source notes the boundary too: past 1.0 the sample coordinates near the rim go negative and the image flips.

The measurement side had its own trap. The refraction check was first run on the blue channel, which was wrong — the blue grid and the cream background differ by only 5 on that channel, so a perfectly bent grid read as an empty screen. Re-measured on the red channel, the grid spacing inside the circle comes out at about 1.3 times the outside. The nine finished effects, sources and measurements are all in the password zip below the article. The zip password is w2k393bp, and the download buttons of all three locale editions share that one password.

Three variants and accessibility

Name Changed value What it feels like
Feedback zoom 05 scale 1.06 to 1.02 The pull slows and the tunnel grows longer
Noise layers 03 octaves 4 to 2 Clouds turn chunky and coarse-grained
Color $subject-blue to $subject-mint Cold blue becomes fresh mint

Accessibility follows one rule across all nine boards. For users with prefers-reduced-motion: reduce, the rAF loop never starts; each board draws a single still frame at its most readable moment — the droplet resting on the background (01), the finished tunnel (05), the birds hanging in the sky (09) — and each still frame measured a non-zero alpha-pixel count. Six boards (01, 02, 03, 06, 07, 08) carry tabindex and role: Enter and Space start a ripple on 07, and the arrow keys move the droplet, camera, noise pan or light. The cursor on 08 is a thin crosshair that never blocks the scene.

FAQ

Is this html5 canvas animation tutorial for beginners or for advanced coders?

The core of each effect is one short formula, so the entry point is gentle: a single division for perspective (02) and one drawImage for the tunnel (05). The heavier parts — the wave grid (07) and the noise buffer (03) — are self-contained loops you can paste first and tune later.

Why canvas instead of CSS animations here?

Because real pixel values have to be read. CSS transforms move whole elements, but swelling the text under a droplet (01) or refracting the picture under a ripple (07) is pixel-level relocation that CSS cannot express. For simple movement and fades, CSS remains the better choice for battery and performance.

Do these slow down on phones?

Keep the computed pixel count below the screen size. The noise is calculated at 160x100 and scaled up (03), the water simulation solves on a 96x47 grid and renders at three times that (07), and the tunnel draws only one enlargement and a few specks per frame, which makes it the lightest of the nine.

Further reading

Enter the archive password

The password is inside this article. You will find it as you read.