108

Slit-Scan Stretch

£10 or the whole Vault, £99

A full-bleed photograph read through a moving slit, like a 1970s photo-finish camera: sweep across it and the picture smears into long streaks that trail the pointer's path, fringed with colour, then retract into the slit and settle. three.js and one shader; GSAP core runs the media branching, the load sweep and the release.

advanced
5 more details
webgl-shaderchromatic-aberrationpointer-trackingvelocity-reactivetouch-drag

This demo reads better at your own screen size than in the frame below:

About this effect

A full-bleed photograph rendered like the finish-line cameras of the 1970s, which never took a picture at all: they read one thin slit, over and over, and let the film travel past it. Here the slit follows the pointer, and every column of the picture is captured at a different moment. The further a column sits from the slit, the older the pointer position it was sampled at, so a sweep across the frame drags the photograph out behind you into long horizontal streaks that follow the path of your hand, up and down as well as across.

Read the full effect overview

Red, green and blue are read at slightly different delays, so the streaks are fringed with a fine spectrum rather than a flat smear. The side the pointer is heading toward gets a short bow wave, the side it has left gets the long trail, and reversing direction rolls one into the other.

Let go and nothing snaps. The delays collapse on an eased curve, the streaks pull back into the slit, and the clean photograph returns. On touch a finger drags the slit, and between touches it sways on its own so a phone never shows a dead picture. Without JavaScript, without WebGL or under reduced motion the photograph is simply shown.

Slit-Scan Stretch - GSAP animation effect preview

What's included

9 items
  • Time-delayed column sampling from a history buffer: each column of the frame reads the photograph displaced by where the pointer was N frames ago, with the delay growing outward from the slit
  • Held columns, not folds: where the drag would fold the picture back on itself the slit's own column is frozen instead, which is what makes a real slit-scan streak rather than a rubbery warp
  • Follows the path: the history carries the vertical position as well, so the trail bends with the stroke as well as stretching along it
  • A bow wave ahead, a long trail behind: the delay weighting follows the direction of travel and rolls over smoothly when you reverse
  • Chromatic separation as a secondary channel: red, green and blue each read at their own delay, so the fringe grows with the stretch and vanishes with it
  • An eased release: the delay envelope collapses on a GSAP ease and the streaks retract toward the slit instead of snapping to the clean frame
  • A load sweep: the slit crosses the photograph once on load so it arrives already smeared, then settles; any input takes over from wherever it has got to
  • Touch drags the slit and a gentle autonomous sway keeps a phone alive between touches; arrow keys sweep it from the keyboard
  • Frame-rate independent, renders only while on screen and only while something is moving, and disposes every texture, the renderer and its WebGL context on teardown

Perfect for

5 use cases
  • Photography and architecture portfolios where one image carries the page
  • Editorial and magazine covers that want a single, memorable hover
  • Film, music and event landing pages with a retro photo-finish or darkroom feel
  • Case-study openers that turn a hero photograph into the first interaction
  • Award-entry pages that want a mechanism nobody has seen on a photo before

How it works

3 sections

The photograph is one full-bleed plane under an orthographic camera, drawn by a fragment shader. The shader knows nothing about the pointer. Each frame the script keeps the slit position (the pointer, smoothed on a time-based exponential ease) and a ring of its recent positions at a fixed 60Hz step, three seconds of path. From that ring it builds a one-dimensional warp table with one texel per column, per colour channel: for every column it works out a delay from the column's distance to the slit, looks up where the pointer was that long ago, and displaces the column by how far the pointer has travelled since.

The table is then made monotonic, working outward from the slit: no column may advance slower than a small floor. Where the raw drag would fold the picture back, the slit's own column is held instead, and a short blur rounds the join. That is the streak. The shader reads the table (float texels, read nearest and blended by hand), samples the photograph once per colour channel, folds any coordinate that lands beyond the picture back into it, adds film grain that shows only where columns are held, and draws a one-pixel slit that inverts what lies beneath it.

The ring advances only while the slit is travelling and freezes when it stops. A GSAP-driven envelope from 1 to 0 scales every delay, so on release the same frozen path is read at shorter and shorter delays: each streak slides toward the slit and the picture clears from the outside in. GSAP owns that envelope, the load sweep, the media branching and the teardown; the render loop owns the pointer, the ring, the table and every uniform.

Difficulty Advanced
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates

Lighthouse, as measured

Google Lighthouse on this effect's demo, from the latest scan. A measurement of the demo as shipped, not a promise for your page.

Accessibility
100
Best practices
100

No performance score, on purpose. That figure depends on how you deploy: your server's compression and caching, your CDN, the connection and the device doing the test, none of which the code controls. The same page can score very differently on two consecutive runs, so measure it where it will live.

Paid effect

Purchase to unlock the code.

Buying Slit-Scan Stretch opens the HTML, CSS and JavaScript source, the full documentation, an AI setup prompt for your editor, and every update we ship to it. Standard license: unlimited personal and commercial projects. Every paid product is tested for desktop and mobile layouts, reduced-motion handling, animation cleanup and keyboard controls wherever there is something to operate.

£10 Standard license, unlimited projects

Browse free effects

Or the whole library: the Vault, £99, one payment.

Documentation

Quick Start

1. Add to your HTML <head>:

Code snippet omitted: it ships with the download.

The inline script is not optional. The photograph in the <figure> is the fallback for no JavaScript, no WebGL and reduced motion, and without this it paints for the half second it takes three.js to arrive, then gets swapped for the canvas. The probe runs before first paint, stamps html.gl when a canvas is coming, and the stylesheet hides the figure under that class. No JavaScript, no class, the photograph shows; the effect removes the class again if it cannot build a renderer after all.

2. Add the markup anywhere in your <body>:

Code snippet omitted: it ships with the download.

3. Add before the closing </body> tag:

Code snippet omitted: it ships with the download.

The script reads the <img> inside the figure, drops a canvas in front of it, and hides the figure once the canvas is actually there. The photograph is used as the texture directly: there is no second request.

Why the import map rather than a plain <script src> for three.js: three ships as ES modules only, and its old UMD build logs a deprecation warning on every page load. The shim above imports it as a module, puts it on window, and then loads assets/script.js as an ordinary script, so the effect itself stays a plain file you can drop into any build (or none).

Already using three.js as a module? Then skip the shim, and just make sure window.THREE is set before assets/script.js runs:

Code snippet omitted: it ships with the download.


Using It With Your Own Design

What the effect needs from your markup: one element with data-slitscan, and inside it an element with data-slitscan-source that contains the <img>. That is all the script reads. The <img> is the photograph, its alt becomes the accessible name of the canvas, and its width and height are only for layout before the image decodes; the shader cover-fits any aspect ratio, so a portrait photo and a panorama both fill the container without distortion.

What is only the demo's styling: the dark ground, the prompt line and its scrim, the Mona Sans import and the .showcase-* shell. Delete all of it. The effect is the .slitscan block; put it in a hero, a card, a full page or a gallery cell.

CSS the effect depends on:

  • The container must have a real size. The canvas is sized from the container's clientWidth/clientHeight, so .slitscan needs a height from somewhere: it is position: absolute; inset: 0 in the demo. Give it a height of 0 and you get a canvas of 0.
  • The .slitscan__source figure must stay in the document. It is the fallback for no JavaScript, no WebGL and reduced motion, and it is only hidden by .slitscan.is-live, a class the script adds after a canvas exists. Never pre-hide it under .has-js.
  • .gl .slitscan__source { visibility: hidden } is what stops the fallback flashing before the canvas arrives. It depends on the head probe above.
  • touch-action: pan-y on .slitscan. A finger dragging sideways drives the slit, while vertical drags still scroll the page from on top of the photograph.
  • overflow: hidden on the container keeps the canvas inside its box.
  • The root is given tabindex="0" and a label while the canvas is live (and restored on teardown), so the arrow keys work. Keep a visible :focus-visible style on it.

Choosing Your Images

This effect has a real content requirement, and ignoring it is the main way to make it look broken. The smear runs horizontally, so it only shows on a photograph that has something for it to smear: vertical edges with tonal contrast. So:

  • Strong vertical structure works; horizontal structure does not. Tree trunks, columns, building edges, poles, a standing figure, a doorway, a row of lamp posts. A calm horizon, a flat sky or a sea reads as a grey blur under a horizontal drag, because a horizontal streak of a horizontal line is the same line.
  • Tonal contrast between the verticals and their ground. Dark trunks against fog, a pale facade against a deep sky. The streak is the slit's own column held across the frame, so that column should contain something worth holding. Mid-grey on mid-grey shows nothing.
  • Leave room around the subject. The picture is dragged sideways by up to a third of its width and any coordinate beyond the frame is folded back into the photograph, so a subject hard against an edge gets mirrored copies of itself. A little air on both sides is enough.
  • Arrive ungraded. The shader gives the frame a gentle S-curve; a photograph that is already heavily graded goes muddy under it.
  • Size for the largest screen, not the viewport. 2048px wide is enough for a full-bleed plane on a 4K display; beyond that the texture upload is paid on every visitor's first frame for nothing.

Test at the extreme, not at rest: sweep the full width quickly, reverse mid-stroke, then stop.


Options

Every option is a data-* attribute on the data-slitscan element. A value of 0 is honoured where it makes sense (it is read as a number, not as "unset").

Attribute Values Default Description
data-strength 0 to 2 1 How hard the picture is dragged per unit of pointer travel. 0 keeps the slit but no smear
data-reach seconds, 0.1 to 1 0.5 The delay at the far side of the frame. Longer reach means longer trails and a bigger ripple
data-split 0 to 2 1 Colour separation between the red, green and blue delays. 0 is a monochrome smear
data-ease 0.02 to 1 0.14 How quickly the slit chases the pointer, as a fraction per 60Hz frame. Lower is heavier and glassier
data-settle seconds 0.9 How long the release takes: the streaks retract into the slit over this time
data-intro 1, 0 1 0 turns off the load sweep, so the photograph appears clean
data-drift 1, 0 1 0 turns off the idle sway on touch screens
data-grain 0 to 2 1 Film grain and the scanline grain that shows in held columns. 0 is clean

Accessibility

  • Reduced motion: the WebGL scene never starts. The <figure> stays exactly as it is, a full-bleed photograph with its alt text. If the preference changes while the page is open the effect tears down or rebuilds without a reload.
  • Without JavaScript, or without WebGL: the same figure, for the same reason. One fallback, three failure modes, and it is never hidden until a canvas has actually been created.
  • Keyboard: focus the photograph and press the left and right arrow keys. Each press moves the slit by a fixed step and the picture does the animating; nothing plays that the user has to wait out. The focus ring is a real outline.
  • Touch: pointer events on every device. Dragging sideways moves the slit; dragging vertically still scrolls the page. Between touches the slit sways on its own so the picture is never dead on a phone (data-drift="0" turns that off).
  • Screen readers: the canvas takes the photograph's alt text as its label, plus a short hint about the keys. The photograph is the content; the smear is decoration and never carries information.
  • Photosensitivity: there is no flashing. Motion is driven by the visitor and eases out; the load sweep is one slow pass and can be switched off.

Dependencies

Dependency Version Required
three.js 0.180.0 Yes, as an ES module (see Quick Start)
GSAP core 3.15.0 (3.12+) Yes: matchMedia branching, teardown, the load sweep and the release envelope

No GSAP plugins. GSAP owns exactly this: the matchMedia branch (fine pointer, touch, reduced motion), the teardown via revert(), and the played values, which are the load sweep, the 0 to 1 delay envelope that stretches the picture on movement and collapses it on release, and the hand-over to and from the touch sway. The render loop owns everything continuous: the pointer chase, the history buffer, the warp table and every uniform. The loop runs on gsap.ticker so a tweened value is always current when the frame draws, and GSAP is not the animation engine of the smear itself: the shader is.

Everything the effect uses (WebGLRenderer, Scene, OrthographicCamera, PlaneGeometry, ShaderMaterial, DataTexture, Texture, TextureLoader) is long-stable three.js API, so pinning to a different version is a one-line change in the import map.


Browser Support

Anything with WebGL 2, which is every current browser. Support is probed before three.js is asked for a renderer, deliberately: three logs its own failure to the console, as errors, several times over, before it throws, so catching the exception would hide nothing and a visitor with WebGL disabled would get a console full of red on a page that had quietly fallen back. Probed first, that visitor simply gets the photograph and a clean console.

The warp table is a float texture read with nearest filtering, which WebGL 2 supports without extensions. Import maps are supported everywhere current. In a browser old enough to lack them the module never runs, window.THREE is never set, and the photograph is again what shows.

Performance

One draw call: a single quad, three photograph reads and four table reads per pixel, plus a 384 by 2 float texture uploaded while something is moving. Once the delays have collapsed and nothing is moving the loop stops drawing altogether, and it does not run at all while the effect is off screen (an IntersectionObserver gates it). Pixel ratio is capped at 2 (1.5 on touch devices) and the canvas is resized from a ResizeObserver on the container.

To buy back frames on low-end hardware, lower the pixel-ratio cap first (the Math.min(window.devicePixelRatio || 1, ...) line in buildEffect): the fragment pass scales with pixels, and it is the only real cost. Then data-grain="0" drops two hash evaluations per pixel.

Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.