A slit-scan browser camera for iPhone, with slitscanning beyond vertical and horizontal — a line, a burst, and a force field.
This is a prototype and a design project. It is not a product, there is no app store listing, and it will probably never have one. It exists because I love photography, and because slit-scan images sit between photography and video and allow you to show space in constricted ways.
An ordinary photograph samples the whole frame at one instant. A slit-scan samples a thin slice of the frame, over and over, and lays the slices out in order. One axis of the resulting picture is no longer space — it is time.
The consequence is strange and specific: a single still image can hold hundreds of different moments simultaneously, stitched into something that looks continuous but never existed.
Slit-scan keeps being reinvented, usually by accident, and always beautifully.
Photo-finish cameras, from the 1930s onward, drew a moving strip of film past a narrow slit. The horizontal axis of those negatives is time, which is exactly why they settle a race: two horses that were never side by side appear side by side, ordered by when they crossed the line.
Jacques Henri Lartigue photographed a car at the 1912 Grand Prix de l'Automobile Club de France with a focal-plane shutter travelling vertically while the car moved horizontally. The wheel leans into an oval; the spectators lean the other way. The distortion is the picture.
Marey and Muybridge had already been taking time apart into frames a few decades earlier. Slit-scan is the same impulse run continuously instead of in steps — no frames, just a smear.
2001: A Space Odyssey built its stargate corridor on a slit-scan rig: artwork, a slit, and a camera crawling along a track, one long exposure per frame.
And your phone does it already, badly and by accident. A CMOS sensor reads out row by row rather than all at once, so the bottom of the frame is a few milliseconds younger than the top. Rolling shutter is an uncontrolled slit-scan. This app is the same effect with the controls put back in.
Every slit here is the same idea: time is a scalar field over the frame, and the slit is the contour where that field equals now. Only the distance function changes, which is why very different-looking effects share one engine.
| Swipe | Burst | Strip |
| Shape | The field | Reads as |
|---|---|---|
| Swipe | distance along a normal — a straight line, at any angle | Diagonal time. The classic slit-scan, freed from vertical/horizontal. |
| Burst | distance from a centre — a ring | A sunburst. Anything moving smears along rays, because neighbouring radii are neighbouring moments. |
| Strip | a fixed slit at the frame's edge | A photo-finish camera. The picture grows sideways without bound, one column per frame. |
A fourth shape, Field — a live band down the middle with time peeling away from it in both directions — is built and works, but is currently not offered in the mode bar. Its code is intact; one line of markup brings it back.
![]() Burst · sunburst |
![]() Field · retired from the bar, code intact |
![]() Swipe · angled, anti-aliased |
Every image above came out of the app itself, scanning the built-in demo source — no camera, no post-processing.
Swipe the picture to choose the slit, rather than aiming it by hand:
| Shape | swipe | does |
|---|---|---|
| Swipe | up / down / left / right | the slit enters from behind the direction you dragged |
| Burst | up / down | expands outward from the centre, or contracts inward |
| Strip | — | the slit is fixed at the edge; there is nothing to aim |
Slice thickness is a scrub dial. Press the pill at the left edge and drag: a ruler runs up over half the screen, 1px at the pill to 60px at the top. Release commits, a tap resets to 1px. Thicker slices mean fewer, chunkier bands, each from a single instant — the traverse rate is unchanged, so a pass still takes the same time whatever the setting.
One press is one exposure. Tap the shutter and the slit runs its full traverse — edge to edge for Swipe, centre to rim for Burst, band to edge for Field — and the picture is captured automatically the instant it finishes. The shutter ring is the progress meter. A second tap cuts the exposure short and captures where it stands.
Every capture is archived automatically with a unique name — no dialog, no tap, straight back to a live feed so you can shoot again immediately. The roll — top bar, left of Settings — holds the session's captures as thumbnails. Open it, deselect anything you don't want, and Save hands the rest to the share sheet in one gesture, so twenty captures cost one tap rather than twenty.
Anti-aliasing (Settings) swaps the renderer for a WebGL engine that keeps a short rolling history of frames and samples it with sub-pixel and sub-frame interpolation, which is what removes the stair-stepping a slit-scan normally has along fast-moving edges. It costs real GPU memory, shown live in Settings, and is capped so it can't be pushed past what an iPhone can hold.
Capture size follows the device by default — the true screen resolution, not a fixed export size — with a 2× option for when the source can support it.
Sources: rear camera, front camera, or a built-in synthetic demo so the app does something before it asks for permissions.
Two builds live side by side:
v2.html the current app — SPLIIIT, all three shapes, anti-aliasing, the roll
index.html the original prototype (Sweep / Radial / Strip, the darkroom UI)
No build step, no dependencies. Serve the folder:
python3 -m http.server 3468The camera needs an https:// origin. Safari only exposes
navigator.mediaDevices on a secure context — over plain http:// to a LAN
address it does not exist at all, and the app tells you so rather than failing
on tap. localhost counts as secure; a LAN IP does not. The demo source works
anywhere.
Served over HTTPS, open it in Safari on the phone and use Share → Add to Home
Screen. It then launches fullscreen, and the service worker keeps it working
with no network and no server. Pages are cached under their own URL, so v2.html
and index.html update independently on the next online launch.
- One time field, three shapes.
stepFor()dispatches tostepSwipe/stepBurst/stepField, but all three paint withclip()+drawImage()— never per-pixel — so an angled or curved slit costs the same as a straight one, and each frame paints the whole span since the last tick rather than a slit at one position, which is what keeps fast motion gap-free. - The anti-aliasing engine is WebGL2. A
TEXTURE_3Dring buffer holds the last N downscaled frames; every tick, one fragment shader pass re-renders the whole image by computing each pixel's time and sampling the history at that depth, with hardware bilinear filtering for the spatial axis and an explicit blended tap for the temporal one. History resolution is decoupled from output resolution, so a modest buffer reads as soft rather than blocky. - The lime line is the slit itself, not a decoration — drawn at full strength in every state so there is always a visible reference for where the scan is reading, on its own canvas so it never bakes into a capture.
- The roll is IndexedDB, not the photo library. Safari requires a live user
gesture for both
navigator.shareand downloads, and a pass takes several seconds — well past that window — so nothing can write to Photos unattended. The roll is what makes one gesture cover a whole session instead of one per capture. - Capture follows the device. Buffers accumulate at the viewport's own aspect and true pixel density, guarded against iOS's canvas area ceiling, rather than a fixed export size that would crop a portrait photo into a landscape frame.
window.__slit2.burn(n, dt) steps n frames of synthetic time, which is how
every shape, gesture and capture path here gets tested without depending on
requestAnimationFrame or a real camera.
See DESIGN.md for the design rationale, and docs/ for the working specs written before each feature was built.
Honest about what this is:
- The on-device camera path is the least-tested part, on both builds. The geometry, the gestures, and every shape/anti-aliasing combination are verified by stepping synthetic time; a real camera on real glass has not been pointed at either build.
- Stills only. No video export.
- Settings do not persist between launches; the roll does, in IndexedDB, up to 40 captures or 250 MB, oldest evicted first.
- One theme, deliberately dark. A camera that flips to white would wreck your night vision and your judgement of the image.
- Tested in the browsers available during development. Not audited across devices.
v2.html current app
index.html original prototype
sw.js offline shell, shared by both builds
manifest.webmanifest home-screen install
icon-*.png generated, not drawn
logo.svg the SPLIIIT wordmark, inlined into v2.html
mode-*.svg the three mode drawings — masked in-app, so they
take the UI's colour rather than their own
blank-roll.svg the roll's empty state
tools/make-icons.mjs regenerates the icons — node tools/make-icons.mjs
examples/ stills from the app, used in this README
docs/ working specs, written before each feature was built
Concept, design and code by Bruno Silva — Ditongo, a design studio in Lisbon — built together with Claude.
MIT. Take it apart.


