workshop private

← all creations

Wheel of Life

sprites · created 2026-09-10

64×44 pixel zoetrope, drawn in cutaway, with three loops and a demo where twelve slits against eleven drawings makes the wagon wheel roll backwards.

pixel-artcanvasphysics

A zoetrope. William Horner’s daedalum of 1834, sold in America from 1867 under the trade name this borrows: a slotted drum, a paper strip pasted round the inside, a crank. Spin it, look through the slits, and the drawings move.

Drawn as a cutaway, the way a patent figure would: the near wall survives only at the two edges — where you can still see the slits go past — and the middle is sawn out so the strip is legible. That is a liberty, and a deliberate one. With the wall intact you would see the strip as a dozen one-pixel slivers, which is the honest picture and the useless one. The third loop pays it back.

The strip’s subject is a rolling wagon wheel, four spokes, one of them painted red so you can tell which way it is going. Not decoration: the wagon-wheel effect and the zoetrope are the same phenomenon, and this device is the first one a person could hold in their hands.

Three loops:

The demo

Crank it and the drum turns from a continuous angle — the twenty-two exported frames are samples of exactly this, not a separate path. The panel on the right is the part worth reading: what you actually see through a slit, the strip unwrapped, a drawing and a half of it, centred on whatever is opposite your eye. With the slits in place it holds the frame the last flash exposed. With them off it is the strip’s continuous position, smeared over the ~55 ms an eye integrates.

Then the control that makes it a piece rather than a toy: the strip is swappable for one with eleven or thirteen drawings on it, on the same twelve-slit drum.

The drum has S slits and turns at f revolutions per second, so a slit crosses your eye S·f times a second — that is the shutter. Between two flashes the drum turns one slit, 1/S of a revolution, and the strip carries n drawings round that revolution, so it advances n/S drawings per flash. Everything falls out of that one ratio:

which is the identity drift = (n − S)·f drawing-widths per second, exported as driftPerSecond and checked in validateGrids rather than asserted here. Wind the crank down instead and a third thing happens: under about 10 flashes a second the eye stops filling in the dark between them, and the demo stops filling it in too — you see the shutter rather than the motion.

Three knobs, three ways for a sampler to lie to you, and all of them are mechanical.

Reuse

Render at integer scales with nearest-neighbor filtering or the edges smear (differently, and worse). spin plays at 12 fps, crank at 6, smear at 12 (ANIMATIONS carries all three).

Import the module rather than the PNGs if you want the drum to do anything. paintScene(ctx, spec, opts) takes { phi, n, mode, crank } directly, so the rotation is a parameter and the strip length is too. The maths is exported separately and has no drawing in it:

Gotchas

Source

No .aseprite — the canonical source is source/wheel-of-life.mjs: a framework-free ES module. The only character map in it is the stand, because a turned wooden foot is hand-drawn work and geometry can’t help; everything round or moving — rim ellipses, slits, the strip’s wrap, the wheels themselves — is painted procedurally over it. Edit and regenerate everything with:

node source/render.mjs

(needs site/node_modules installed — the script resolves Playwright from there.) That one command rewrites export/, media/ frames, thumb.png, and the demo’s bundled copy of the module, so nothing drifts. The demo screenshots in media/ come from node scripts/screenshot-demo.mjs, which also fails on any page error — SHOT_W=900 SHOT_H=480 renders it at the site’s embed size, which is the one that catches a caption running off the canvas.

Port notes

paintScene is grid-walking and trigonometry with no DOM dependency beyond a 2D context — same shape as the claw’s and inner-groove’s — so a DragonRuby port translates directly: keep the stand as a string grid, map cells to args.outputs.solids, or just consume zoetrope-sheet.png as a spritesheet with source_x = frame * 64. The sampling functions are pure arithmetic and port as-is.