# Ripple Reveal WebGL > Slideshow transition using WebGL with radial mask crossfade and Gaussian sine ring distortion for true pixel-level wave refraction at the reveal edge Live: https://vanilla-motion.com/ripple-reveal-webgl/ · Tuning app: https://vanilla-motion.com/ripple-reveal-webgl/app/ · Code reference: https://vanilla-motion.com/ripple-reveal-webgl/skill.md · Params: https://vanilla-motion.com/ripple-reveal-webgl/params.json 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. ## Top-left origin Set `originX: 0, originY: 0` (or `data-origin-x="0" data-origin-y="0"`) — the same shader handles any origin. > ▶ [Live demo](https://vanilla-motion.com/ripple-reveal-webgl/embed/top-left/) · [open in the tuning app](https://vanilla-motion.com/ripple-reveal-webgl/app/) ## How it works A fullscreen `` 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 ```glsl 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 ```glsl 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. ### Params → WebGL uniforms ```ts // params.ts: one definition, also the data-* attributes (data-ring-speed) and the panel const { originX, originY, feather, ringCount, ringWidth, ringLength, ringSpeed, decay } = this.p const amplitude = this.p.distortScale / 1000 ``` Every value lives in `params.ts` and reaches the shader as a uniform. The values are read every frame, so `setParams({ ringSpeed: 2 })` takes effect without a restart. ## Texture management All slide images preload into WebGL textures before the first frame: ```ts 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: ```glsl 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 ```ts if (!window.matchMedia('(prefers-reduced-motion: reduce)').matches) { mount(el, { images }) } ``` Skip `mount()` entirely — don't just reduce duration. The Ken Burns zoom alone can trigger vestibular symptoms at full speed.