# Slit-Scan Stretch

> 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.

Canonical: https://gsapvault.com/effects/slit-scan-stretch
Live demo: https://gsapvault.com/demos/slit-scan-stretch/index.html

| Property | Value |
|----------|-------|
| Type | effect |
| Tier | paid |
| Price | £10 |
| Difficulty | advanced |
| Plugins | Core GSAP only |
| Techniques | webgl-shader, chromatic-aberration, pointer-tracking, velocity-reactive, touch-drag |
| Uses Lenis | No |

## Lighthouse, as measured

Google Lighthouse on the demo, 28 September 2026. A measurement of the demo as shipped, not a promise for your page.

| Category | Score |
|----------|-------|
| Accessibility | 100 |
| Best practices | 100 |

No performance score is published: it depends on the deployment (server compression, caching, CDN, connection and device) rather than on the code, so measure it where it will live.

## Overview

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.

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.

## Features

- 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

## 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

## Vibe-Code Ready Setup

This effect includes `START-HERE-AI.md`, a product-specific copy-paste setup prompt for Cursor, Claude Code, ChatGPT, GitHub Copilot, Windsurf, and other coding assistants. It tells the assistant to inspect the existing stack, integrate the supplied files, preserve the design, scope selectors, retain accessibility and responsive behaviour, add framework-appropriate GSAP cleanup, and report what it tested.

[How AI-assisted setup works](https://gsapvault.com/vibe-coding)

## How It Works

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.

## Documentation

How this effect works and how it goes into a page. The reference you use once you own the files (worked examples, events, the programmatic API, the class list) ships with the download.

### 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.

## What You Get

- `index.html`: working demo page
- `assets/script.js`: commented, readable source
- `assets/style.css`: effect styles
- `README.md`: full documentation with examples and framework integration notes
- `START-HERE-AI.md`: product-specific copy-paste prompt for AI-assisted setup
- `LICENSE.txt`: standard license terms
- Lifetime updates: re-download anytime from your library

## Get the Code

This is a premium effect. The standard license costs £10 one-time and covers unlimited personal and commercial projects with no attribution required for our code. Bundled third-party assets retain their own licences and attribution requirements. The only restrictions: no redistribution of the code itself and no competing effect libraries.

- [Buy Slit-Scan Stretch](https://gsapvault.com/effects/slit-scan-stretch)
- [The Vault (£99 one-time, best value): every collection in the Vault library, plus future items added to those collections](https://gsapvault.com/effects)

## Judge the Code Quality First

These related effects are free with complete source published, written to the same production standard (cleanup functions, reduced-motion support, framework-agnostic):

- [Hover Underline](https://gsapvault.com/effects/hover-underline.md): Four material link underlines (an exit-through line, marker sweep, hand-drawn wave, and an endlessly travelling wave) with coordinated type and active-index responses.
- [3D Card Flip Gallery](https://gsapvault.com/effects/3d-card-flip.md): Tactile GSAP 3D cards with deep perspective, reactive edge lighting and shifting shadows. Flip on hover, keyboard focus or tap, with grouped auto-close.
- [Tailwind Component Remixer](https://gsapvault.com/effects/tailwind-class-playground.md): Generate fresh Tailwind component recipes with procedural SVG artwork, coordinated colour palettes and animated GSAP layout transitions.

---

From [GSAP Vault](https://gsapvault.com): production-ready GSAP animation effects. Full catalog for agents: https://gsapvault.com/llms-full.txt
