Two short animated films in which every frame and every sound is computed in plain JavaScript on Canvas 2D. No images, fonts or audio samples are loaded: every dot of the halftone, every hatch line, every glyph and every note is placed by code, and any frame can be rendered on its own from its number.
FLIP THE COIN (29.5 s, finished). A cat’s tail knocks a coin off a kitchen table, and it falls through eleven unrelated worlds. The object in the centre never changes size or spin rate; every cut lands when it is edge-on, and the swap to the next object (moon, apple, ball, bottle cap, disc, ring, chip, tablet, eye) hides in that sliver.
HYDRO MASSACRE (41 s, part 1, nearly finished). Split screen: two raindrops, launched by a tin gutter and a poplar leaf, fly toward each other across a rainy city. The city reaches for them with spikes and never touches them; it can only kill their reflections in the glass.
Full clips with their synthesised sound: flip-the-coin.mp4, hydro-massacre.mp4. Stills: kitchen, reflection, glass trap.
The longer version, with measurements and the mistakes that led to each decision, is in docs/method.md. The shared engine is in lib/.
A scene never draws tone. It declares dens(x, y) -> 0..1, the amount of
ink at each point, derived from lighting, depth and material. A separate
rule turns the field into marks: AM halftone dots sized by area, engraving
lines, ordered or blue-noise dither, stipple, pen lines along a flow, or
flat cel bands. Changing one line (TONE.rule = 'hatch') restyles a film
without touching a scene. Hatch lines are isolines of a coordinate the
scene supplies, not strokes traced along a direction field, so they can
never converge into blots. FLIP THE COIN also has a riso mode (?v=2) that
separates each fill into per-ink coverage layers, then screens every ink
once at its own angle with a little misregistration.
HYDRO MASSACRE’s frames are a stage of depth cards traced per sample: a ray crosses the cards front to back, accumulates ink with coverage and refracts through drops. Each card is baked in its own coordinates and blurred by its own circle of confusion before printing, so the marks stay crisp while the image goes soft (blurring a halftone gives grey mush). Tone is stored premultiplied by coverage so dark objects in defocus do not grow halos. A focus pull bakes a card twice and blends the two bakes.
Open water is Gerstner waves (analytic height anywhere; the Jacobian is a free foam mask). Ripples are the wave equation on a grid with a nine-point Laplacian, because five points turn rings into squares. Liquid that changes shape is position-based fluids with constants calibrated on the first step. A single drop is pure optics: ray-ellipsoid intersection, Snell, Fresnel, total internal reflection in bubbles, Beer–Lambert, and floor caustics by pushing sun rays through it. Mirrors send the ray into a separate reflected world, which is how a drop’s double can die in the glass while the drop flies on.
Every shape is an array of points, so 3D is just a third coordinate and one
project(x, y, z). Vanishing points are not a setting; they appear from
yaw and pitch. Depth comes from marks rather than scale: the halftone pitch
stays fixed on the sheet, far things lose contrast and get fewer marks.
All randomness is seeded per purpose (makeR(key)), and every random draw
happens before any early continue, or one skipped mark reshuffles the
whole field every frame. HYDRO MASSACRE simulates its whole act once at load
at 1/960 s and renderFrame(f) only reads the record. So frames render in
any order, a film can be split across processes, and the comment clean-up
of this repository was verified by rendering reference frames and both
soundtracks before and after and comparing them byte for byte.
Contacts are fixed first and the paths are solved backwards. Two drops launched from different launchers arrived 3.6 m apart in height; since height difference grows linearly under equal gravity, one launch-speed correction closes it, found by re-running the flight with a secant update. Sets are placed where the drops will be on screen, and near misses are guaranteed by geometry (a bat pivots outside the frame at exactly its length plus the ball radius plus the gap).
tools/verify.py checks FLIP THE COIN’s invariants on finished frames:
cut grid, edge-on cuts, spin direction, byte-identical holds, no frame
throwing. For HYDRO MASSACRE, scenes register their objects every frame and
tools/act.py walks every frame of a range, flagging anything that appears
or vanishes mid-frame without entering past an edge or growing from zero,
jumps more than three times its own typical step, or has a spike with no
root on its pane. tools/pop.py asks the same question of the pixels,
for everything that is not registered: a compact region whose mean tone
changes far more in one frame than in its neighbours is a pop-in, while
lightning is told apart by changing the whole frame in one direction.
FLIP THE COIN’s score is a waveguide string into a body of band-pass resonators, modal bells and a comb-and-allpass room, calibrated by RMS and balanced with stems. Its harmony follows the colour chain: neighbouring chords share two tones and two voices hold through every cut, and the beat is the 12-frame cut grid. HYDRO MASSACRE’s heart beats on the frames where the on-screen ECG crosses the centre, and the rain thins in slow motion.
Open flip-the-coin/fall.html in a browser and it
plays, drawing every frame live. It is not real-time: in headless Chromium
on my machine a frame takes about 60 ms, so it runs near 15 fps against the
film’s 24, without sound. ?f=120 shows a single frame, ?s=2 renders at
2160×2160, ?v=2 switches to the riso separation.
hydro-massacre/hydro.html has no playback
loop. It spends about 20 s simulating the act on load and sets
window.READY; then renderFrame(400) in the console draws a frame in
2–5 s. docs/viewer.html wraps this in a frame slider
(it needs the files served over http, e.g. python -m http.server).
Rendering frames and video needs Python 3 with Playwright, Pillow and
imageio-ffmpeg (pip install playwright pillow imageio-ffmpeg, then
python -m playwright install chromium). Output goes to each film’s out/.
cd flip-the-coin
python tools/shoot.py one 120 # one frame
python tools/shoot.py video 708 # all frames and out/fall.mp4
python tools/sound.py # synthesise the score and mux it
python tools/verify.py # invariants over all 708 frames
cd ../hydro-massacre
python tools/film.py frame 400 # one frame (step=2, scale=2 for final quality)
python tools/film.py video # all 984 frames, split across processes
python tools/sound.py # synthesised score, muxed
python tools/act.py 440 520 # per-frame object bookkeeping
python tools/pop.py # pixel-level pop-in check over out/act1
python test/proof.py toneproof.html # proof sheets for the library
python test/measure.py ripples # numeric checks: ripples, fluid, settle
lib/ shared engine: core (noise, strokes, physics), tone, space,
water, stage, sound; README.md is the map
flip-the-coin/ fall.html driver, engine.js and kit.js, scene1–11.js,
transitions.js, press.js (riso mode), sound.js, tools/
hydro-massacre/ hydro.html driver, world.js and act1.js (simulation),
scene and set files, glass.js, flood.js, sound.js, tools/
test/ proof pages for lib and numeric measurements
docs/ method.md, the Pages index and a frame viewer (publish Pages
from the repository root; the index is at /docs/)
media/ preview clips, GIF loops and stills
lib/ was extracted from it
afterwards, and the film was not moved onto the shared code.hydro-massacre/sound.js.lib/ and delete its duplicate engine, using the
same byte-for-byte frame comparison as a safety net.act.py and pop.py in CI on a low-resolution render.MIT, see LICENSE.