Vanilla MotionVanilla Motion

Ripple Reveal WebGL

motionwebglglslanimation

Built with WebGL, GLSL, CSS Custom Properties, Vanilla JS, Tailwind CSS v4

WebGL canvas replaces CSS mask entirely. A fragment shader crossfades two slide textures behind a radial mask while Gaussian-enveloped sine rings physically distort pixels at the wave front — true refractive warping, not a visual overlay.

Ripple reveal WebGL

Circle expands from a configurable origin, distorting image pixels at the wave edge like light refracting through moving water.

MobileDesktop
Open Demo

Top-left origin

Set --origin-x: 0%; --origin-y: 0% on the container — the same shader handles any origin.

MobileDesktop
Open Demo

How it works

A fullscreen <canvas> element replaces all slide markup. One GLSL fragment shader does everything in a single pass:

  1. Compute aspect-correct distance from each pixel to the wave origin
  2. Apply Gaussian-enveloped sine ring distortion near the wave front
  3. Sample both slide textures at the displaced UV coordinates
  4. Mix prev/curr via a smoothstep radial mask

Ring distortion

float radius = uT * uRingSpeed * 2.0;
float x      = dist - radius;                        // signed dist from wave front

// leading edge: tight Gaussian bell; trailing wake: exponential decay (--decay)
float band = x > 0.0
  ? exp(-pow(x / uRingWidth, 2.0))
  : exp(x / max(uDecay * uRingWidth, 0.0001));

float freq  = uRingCount * 6.28318 / max(uRingLength, 0.001);
float rings = sin(x * freq);                         // oscillation in the band
float disp  = rings * band * uAmplitude * fade;

vec2 dir    = normalize(uv - origin + 0.0001);
vec2 dispUv = uv + dir * disp;                       // push pixels radially

x = dist - radius is negative ahead of the wave, zero at the front, positive behind. The Gaussian bell (band) ensures only pixels close to the wave front get displaced — the effect stays confined to the ring, not the whole image.

The trailing wake (x < 0) uses a different envelope: exponential decay scaled by --decay. At --decay: 0 the exponential collapses to zero instantly, giving a clean leading edge only. At --decay: 3 (default) the distortion fades out slowly behind the wave like a comet tail.

Radial mask crossfade

float maxDist     = sqrt(aspect * aspect + 1.0) * 0.75;
float maskRadius  = uT * maxDist;
float halfFeather = uFeather * maxDist * 0.5;
float maskAlpha   = smoothstep(maskRadius + halfFeather, maskRadius - halfFeather, dist);
gl_FragColor = mix(prevColor, currColor, maskAlpha);

smoothstep produces the soft feather zone. maxDist * 0.75 covers the farthest corner from any origin position.

CSS vars → WebGL uniforms

private readConfig() {
  const cs = getComputedStyle(this.container)
  return {
    originX:   parseCssPct(cs.getPropertyValue('--origin-x'),      0.5),
    amplitude: parseCssNum(cs.getPropertyValue('--distort-scale'), 40) / 1000,
    ringSpeed: parseCssNum(cs.getPropertyValue('--ring-speed'),     1),
    decay:     parseCssNum(cs.getPropertyValue('--decay'),          0),
    // ...
  }
}

CSS vars on the container still drive all config — same API as a CSS-only component. readConfig() runs every frame so live CSS edits take effect without restart.

Texture management

All slide images preload into WebGL textures before the first frame:

await Promise.all(this.srcs.map((src, i) => this.loadImage(this.textures[i], src)))

gl.UNPACK_FLIP_Y_WEBGL flips Y on upload so UV (0,0) maps to the image top-left (matching CSS convention).

Ken Burns zoom

UV scaling in the shader — no CSS transform needed:

vec2 zoomUv(vec2 uv, float zoom, vec2 origin) {
  return (uv - origin) / (1.0 + zoom) + origin;
}

uKenBurnsCurr goes 0→0.1 during the transition; uKenBurnsPrev stays fixed at 0.1 (outgoing slide is already fully zoomed). Both run from their own independent clocks so the outgoing slide doesn’t stutter when a new transition begins.

Accessibility

if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
  new RippleSlideshow(el).start()
}

Skip start() entirely — don’t just reduce duration. The Ken Burns zoom alone can trigger vestibular symptoms at full speed.

More tutorials

Ripple Reveal

Ripple Reveal

motioncsscss-maskanimation
Venetian Blind

Venetian Blind

motioncsscss-maskanimation
CSS Tab Shape

CSS Tab Shape

css-maskcss-clip-pathshapestabs