How it works
01 Eased scroll velocity
Instead of a scroll event listener, window.scrollY is sampled directly inside
the RAF loop each frame. The raw delta is clamped to ±200 px (preventing a spike if the
canvas was off-screen) then fed into an exponential ease: vel += (raw - vel) * 0.15. The result is normalised to −1..1 by dividing by
40 px. When you stop scrolling, raw becomes 0 every frame and the velocity decays
to rest on its own. No separate timeout or reset needed.
02 Shear and smear
The fragment shader offsets each pixel's UV.y by a function of its UV.x position and the
current velocity: shear = v × (uv.x − 0.5) × 0.65. Columns left of centre
shift one way, columns right shift the other. The image leans diagonally into the scroll
direction. A second term stretches the y-axis slightly
(y = uv.y × (1 + |v|×0.1) − |v|×0.05), adding a smear that makes fast
scrolls feel more physical.
03 Chromatic split
The red, green, and blue channels are sampled at slightly different UV offsets proportional to velocity. Red leads in the scroll direction; blue lags behind; green stays at the base UV. At rest all three channels overlap and the image is clean. The split grows with speed and collapses back to zero as velocity eases out.
// 1. Eased velocity — sampled every RAF frame, not on scroll events const raw = Math.max(-200, Math.min(200, scrollY - lastScrollY)); lastScrollY = scrollY; vel += (raw - vel) * 0.15; // exponential ease toward raw const v = Math.max(-1, Math.min(1, vel / 40)); // normalise to −1..1 // 2. Shear + smear — GLSL: offset y by column position × velocity float shear = v * (uv.x - 0.5) * 0.65; // lean proportional to x float y = uv.y * (1.0 + abs(v)*0.10) - abs(v)*0.05; // y-stretch // 3. Chromatic split — R/G/B at different y offsets vec2 uvG = vec2(uv.x, y + shear); vec2 uvR = vec2(uv.x, y + shear + v * 0.038); // red leads vec2 uvB = vec2(uv.x, y + shear - v * 0.038); // blue lags gl_FragColor = vec4( texture2D(u_image, uvR).r, texture2D(u_image, uvG).g, texture2D(u_image, uvB).b, 1.0);
A Svelte action, <canvas use:scrollShear>, driving a single WebGL
fragment pass. Texture managed outside glQuad, velocity tracked in the RAF loop.
DPR-aware, pauses off-screen, accessible under reduced-motion.