Nsenene season: a viral risograph animation, rebuilt in code and re-set in Kampala
The idea is Kevin Ngo's and the brief is a stranger's. What I brought was Uganda and a printer's model of ink: a riso separation does not overprint light on dark, it punches a hole. One index.html, 22 Ugandan scenes, an amadinda tuned to five equal steps, and a screenshot harness that caught every scene that did not read.
The idea is not mine. The risograph animation that started this — a whole film drawn in code on a canvas, ink by ink — is by Kevin Ngo (@kevin_t_ngo), and it went round r/ClaudeAI last week. The top comment under it was "what's the prompt?", and nobody had it. So somebody else wrote one from scratch and published it: a ~10,000-character brief, in that r/ClaudeAI thread, under the honest heading "It's a recreation of the idea, not the original."
That brief is what this was built against, and it deserves the credit for the shape of the thing: six acts, a centre dot that calls out in ripples and is answered, disc reveals and a dive, an archive of medallions that drains to one ink, a duotone reprise, a hand-lettered signature, the riso ink list with its screen angles, and — the part that mattered most — an instruction not to one-shot it.
What is mine is everything inside the frame. The film is Ugandan: nsenene in a lit drum, a shoebill in Mabamba's papyrus, matatus, boda stage, the rolex being rolled, mabati roofs in the rain, marabou storks on a streetlight, Kibuli's dome, an anthill at dusk. The soundtrack is an amadinda, not a pad. And the one genuinely technical idea below — how a riso separation actually works in a canvas compositor — is the thing I got wrong first, then got right, and it is the reason the film looks printed instead of muddy.
A riso separation is a hole, not a layer
Here is the whole problem in one picture. On the left, a nsenene (a grasshopper — the ones that come with the November rains and get fried with onions) drawn the obvious way: flood the ground in violet, then draw the body in yellow over it. On the right, the same scene after the fix.


Canvas layers composite with multiply, because that is what ink does: each
layer subtracts light. Violet times yellow is brown. That is not a bug in the
compositor, it is the compositor being correct — and it is exactly why a real
riso separation never overprints a light ink on a dark one. The bright shape
is not a layer on top. It is a hole in every layer underneath, which the
bright ink then fills. On a real machine the paper takes the yellow drum's ink
where no other drum laid any down.
So paint() takes a knock list, and before it draws anything it punches that
path out of the named inks with destination-out:
That comment is the second half of the lesson, and it cost an hour. Riso looks like riso partly because the drums are not perfectly aligned, so every ink in this film composites at its own 1–3px offset. Punch the hole at the ink's own coordinates and it lands up to three pixels away from the ink meant to fill it — and three pixels of bare paper around every shape reads as a white halo, not as print.


The fix is the printer's own: translate the knockout into the knocked layer's coordinate space, and trap — make the ink a hair wider than the hole it fills (1.6px here), so a small misalignment shows as overlap rather than as a gap. Overlap prints as a slightly darker seam. A gap prints as white, and white is the one thing a riso cannot print.
The method is: build it, screenshot it, and then actually look at it
The brief's best instruction was to not one-shot it, and the harness that enforces that is more of the work than it sounds:
It drives the Chrome already on this machine (npx playwright install is a
~150 MB download and I am on a mobile plan), seeks render(t), and writes a
PNG per frame. It exits non-zero on any page error and on any frame whose
contrast spread is under 6, so a silently blank canvas cannot pass as a
screenshot. --contact tiles the whole scene library into one sheet, which is
the only way to judge a batch: one muddy panel is obvious beside seven that
read.

Looking is what caught all of these, and none of them threw an error:
| scene | what the screenshot showed | the actual cause |
|---|---|---|
| gorilla | a circle with two orange rings | the head filled the disc, so crest, ears and shoulders were outside the clip — and the muzzle knocked indigo out of itself, printing a beard-shaped hole |
| jackal | lightning in four scraps above the disc | the moon was struck after the storm, so its knockout erased every bolt that crossed it |
| acacia | a mushroom, three times | a thick column under a domed mass IS a mushroom; an acacia forks low and its limbs reach the canopy's outer thirds |
| bats | no bats | dark bats over a violet flood over an orange flood — and a cave composed full-frame, entirely outside the disc clip |
| frogs | green smudges | three small frogs at thumbnail size; two large ones with knocked-out eyes read |
| archive | 8 of 22 medallions | a module-scope snapshot of a registry that two thirds of the scenes had not been added to yet |




The archive one is worth pausing on, because it is the most dangerous kind of bug:
No error. No blank frame. The archive drew 8 medallions beautifully, on the correct orbits, with the right spin — it was just missing most of the film. A module-scope snapshot of a lazily-populated registry fails silently and plausibly, which is the failure mode that survives review.

Africanizing it, and knowing when to cut

The brief ships a scene library — wolves under aurora, a rotary telephone, a Ferris wheel, an umbrella pine. I started by adding eight Kampala scenes to the middle of it, which produced exactly the thing I did not want: a Japanese-ish film with a Uganda section. The instruction was Africanize it, so the whole vocabulary went: 22 scenes, all Ugandan, and the library above is the result.
Two of them are worth the paragraphs.
Matoke failed three times and got cut. Fingers drawn as ellipses read as leaves. Drawn as thin arcs they merged into two vertical columns and the bunch read as a fir tree. Leaves radiating from one point read as a palm fan. Three attempts, three different wrong plants.


The matatu read first try, and it carries a riso joke I only get to make because the compositor is honest: its white body is bare paper. White is the one colour a riso cannot print, so the van is a hole in every ink — the same primitive as the grasshopper, used as a subject rather than as a fix.
Acacia is the one that taught me the difference between a decorative fix and a structural one. Three attempts of fiddling with the canopy edge — scallops, gaps, a thinner trunk — and it stayed a mushroom, because a single thick column meeting a domed mass in the middle is a mushroom whatever you do to the outline. The fix was the tree's actual structure: fork low, push the limbs out to the canopy's outer thirds, flatten the top, scallop the underside, and put the sun behind it.


Two more places the obvious thing is wrong
A duotone is a luminance ramp, not two overlays. Act V reprints twelve
scenes in pink and indigo only. The obvious order — multiply pink over the
scene, screen indigo under it — printed a flat magenta wash with the picture
gone. What works is to grayscale the plate, destination-over the dark ink
beneath it, and only then multiply the light one:

Paper must be built once. Grain, mottling and ink-starved voids regenerated
per frame make the paper boil — it is the single most obvious tell that
something is fake, and it is invisible in a still. Build them into bitmaps at
boot and never touch them again. Same rule as the seeded PRNG: render(t) is a
pure function of time, and Math.random() may never appear inside it.

The soundtrack is an amadinda, and it is not in any key
The brief asks for a warm analog pad and an arpeggio at 120 BPM. A pad is a twelve-tone object, and I had just spent five hours making the pictures Ugandan. So the soundtrack is a log xylophone instead.
Buganda's amadinda and akadinda are tuned to an approximately equal pentatonic: five equal steps to the octave, 240 cents each. That is not a mode of anything in twelve-tone equal temperament — the notes land between the piano's keys and stay there. Twelve lines of code:
Two parts play it the way two players do: okutema (the starter) and okwawula (the divider), a rung apart and offset by half a step, so each falls into the other's gaps and the pattern that emerges belongs to neither hand. Every ripple in the film puts a ping on the ladder; the scheduling is anchored to the animation clock, never wall time, and re-anchors whenever it drifts more than 0.15 s.
A soundtrack is the one thing a screenshot cannot check, which is why the
harness grew an --audio mode: it clicks the real Sound button (WebAudio needs
a genuine gesture), then asserts that no AudioContext existed before the click,
that it is running after, and that at least one ping and one arpeggio note
actually got queued. Scheduling a note into the past is silent, and silence
looks exactly like success.

What it cost
| measure | value | how it was counted |
|---|---|---|
| wall clock | ~7 h, one session | first commit 15:41, last 18:27, plus engine work before the first commit |
| output tokens | 532,807 | sum of output_tokens over the session log from the brief's timestamp |
| cache reads | 80.96 M | cache_read_input_tokens over the same window |
| cache writes | 1.16 M | cache_creation_input_tokens over the same window |
| the artifact | 105 KB / 2,007 lines | one index.html, no dependencies of any kind |
The token number is large and slightly misleading in an interesting way: the cache reads dominate by two orders of magnitude, because the loop is "re-read a 2,000-line file, change forty lines, screenshot, look" a few hundred times over. Cheap tokens, many times.
Credits, plainly
- The original animation is by Kevin Ngo (@kevin_t_ngo), also made with Claude Code. Nothing here is derived from his code, which was never published — only from the fact that he proved the idea was possible.
- The brief is from the r/ClaudeAI writeup "I recreated the viral riso animation with Claude Code + Opus 5", written from scratch by its author after nobody could produce the original prompt. Its six-act structure, call-and-response conceit, centre dot, transition vocabulary, riso ink list and screen angles, and its staged "do not one-shot this" process are all theirs and are used as given.
- Mine, for the avoidance of doubt: the registration-aware knockout and trapping model, the 22-scene Ugandan library, the amadinda tuning and the okutema/okwawula interlock in place of the brief's pad, the screenshot-and- look harness, and every bug listed above.
- The riso ink hexes and screen angles are the standard Riso drum colours, which are a printer's fact rather than anybody's invention.
The page carries the same credit under its own controls, and the source file carries it in a header comment, so it travels with the artifact and not only with this post.
