fragsplainer Slit Scanner
A Shader, Explained

Slit Scanner

A camera recorded one column at a time, until the screen's width becomes a timeline.

A normal photograph captures one instant across all of space. A slit scanner does the opposite: it captures one thin column of space, over and over, and lays each capture down beside the last. The picture you end up with is mostly made of the past — its left edge is older than its right. Anything that holds still draws as smooth horizontal streaks; anything that moves smears into a wave. This one looks through your webcam if you let it — hit Enable camera on the render panel. Otherwise a simple bobbing shape stands in, which scans just as well.


The trick needs a feedback buffer — a place to keep the columns we’ve already recorded while a new one is being written. Just one moving part decides everything: the scan head, a vertical line that creeps across the frame and wraps around. Its scan speed 1.00 is how many columns it covers per frame.

Pass · Buffer A — the recording

The buffer is almost entirely lazy. Every frame it copies its previous self straight through — each column keeps exactly what it already held. The only exception is the narrow band the scan head is passing over right now, where the old value is thrown away and replaced with the live subject. So the moment the head moves on, that column is frozen forever at the instant it was scanned.

Freezing one column at a time → technical explainer

iChannel1 is Buffer A reading itself from last frame — the memory. Sampling it at the same uv and writing it back is a perfect copy; the mix only diverges where write is 1, inside the scan band.

vec3 frozen = texture(iChannel1, uv).rgb;   // last frame, this column
vec3 now    = camera(uv);                    // the live subject
float write = step(abs(uv.x - scanline), band);
fragColor   = vec4(mix(frozen, now, write), 1.0);

The band is sized to be at least as wide as the head travels in one frame — max(1.5*px, 0.5*speed*px + px) — so cranking the speed up never tears gaps between the columns it stamps.

Where the camera comes from (and the fallback)

Both passes sample the subject through one helper. When the camera is granted, the engine binds the live video to iChannel0 and reports its size in iChannelResolution[0]; when it isn’t, that size reads 0 and we draw a moving stand-in instead — so the shader is identical whether or not a camera exists.

bool camLive() { return iChannelResolution[0].x > 0.5; }
vec3 camera(vec2 uv) {
    return camLive() ? texture(iChannel0, uv).rgb : fallback(uv);
}
Pass · Image — the viewfinder

If we only ever showed the frozen buffer, you’d never see what’s about to be recorded. So the display pass keeps the recorded past to the left of the head untouched, and to the right it fades the live subject back in — strong at the head, dissolving into the recording as you look further ahead. The width of that dissolve is the feather live feather 0.20.

The live reveal and the bright seam
float d = abs(uv.x - scanline);
if (uv.x > scanline) col = mix(col, live, smoothstep(feather, 0.02, d));
col += seam * smoothstep(px * 1.5, 0.0, d);   // the scan-head line

The last line adds a thin highlight right at the head — the seam where now becomes then. Turn its seam glow 0.60 down to nothing for a seamless record, or up for a hard scanning line you can watch travel.

Slow the scan right down and hold still: you’ll watch yourself get painted, strip by strip, into a portrait that’s a few seconds wide. Wave an arm across the frame and it stretches into the long diagonal that gives slit-scan photographs their melting, time-warped signature.

↗ Shadertoy
1 // Buffer A — the canvas the scan paints onto. Each column keeps whatever was last
2 // written to it (iChannel1 = this buffer, last frame), except the thin band the
3 // scan head is over right now, which is overwritten with the live subject. As the
4 // head moves on, every column stays frozen at the instant it was scanned — so the
5 // X axis becomes a record of time.
6
7 void mainImage(out vec4 fragColor, in vec2 fragCoord) {
8 vec2 uv = fragCoord / R;
9 float px = 1.0 / R.x;
10
11
12 vec3 frozen = texture(iChannel1, uv).rgb; // this column, as last left
13 vec3 now = camera(uv); // the live subject, this frame
14 // a write band at least as wide as the head moves per frame, so even fast
15 // scans leave no gaps between the columns they stamp
16 float band = max(1.5 * px, 0.5 * 1.00 * px + px);
17 float write = step(abs(uv.x - scanline), band);
18 fragColor = vec4(mix(frozen, now, write), 1.0);
19
20 }
21