← all writing
2026-08-10 · 5 min read

An animated illustration of yourself, from one photo

The character on my homepage waves, then goes back to work when you scroll. One photo in, a folder of JPEGs out — about an hour, and roughly 40 credits. Here's the recipe, including the four things that cost me the most time.

The illustration at the top of this site waves at you twice, then rests. Scroll down and she puts her hand on the keyboard and starts typing. Scroll back up and she waves again. The 404 page has her turning the laptop around to show you the error.

None of it is video. It's a handful of JPEGs and about 120 lines of code, built from one photograph of me in an afternoon.

Where the idea came from

Three people, stitched together.

Ahmed Dahbi's portrait follows your cursor across nine photo poses, with three-frame tweens between them. His site is where I understood that a flipbook of stills beats video for anything interactive — video can't respond to your mouse.

Aditya Sur built a pre-loader and a 404 out of 100+ SVG frames, and dropped the frame rate on purpose so it reads hand-drawn. That single decision is what separates "made" from "generated."

oil-oil open-sourced the pipeline: generate motion with AI video, extract the frames, delete the bad ones, map them to an input. That third step is the one everybody skips.

Get one good still first

Everything is built from a single base illustration, so don't move on until you like it. Four rounds taught me what to ask for:

Ask for solid shapes, not outlines. "Line art" gets you a colouring book — thin, uniform, lifeless. Say rich, solid filled shapes, varied line weight.

Ban grey explicitly. "Every pixel is either pure black or pure white, plus one accent colour." Left alone, everything drifts to mid-grey and turns muddy, and your one accent colour stops being special.

Simplify the face or it goes creepy. Large round eyes, tiny nose, small smile, and say no realistic facial detail. A semi-realistic face at illustration scale lands squarely in the uncanny valley. My first pass genuinely looked frightening.

One accent colour, not two. The brown coffee mug works precisely because it's alone on the page.

Make every other pose from that image

This is the part that makes it hold together.

Every subsequent pose — the typing one, the 404 one — is generated with the first illustration as the reference, never the photo again. The prompt is always the same shape:

Recreate the EXACT same illustration with only ONE change: [the new pose]. Everything else identical — same hair, same shirt, same desk, same mug position, same line weight, same framing.

Go back to the photo and you get a different person with different hair.

Animate between two poses you already approved

Now the motion. Use a video model with both a start image and an end image. It only has to interpolate between two drawings you've already accepted, so it can't invent a new style halfway through.

Two traps here, and they pull in opposite directions.

Start and end set to the same image, and it barely moves. I wanted a looping wave, so I pinned both ends to the same frame. Got 5% motion. Obvious in hindsight — the cheapest way to begin and end identically is to stay still.

Drop the end image and it moves, but colourises. Brown skin, grey laptop, the whole black-and-white palette gone.

What actually works is both at once: keep the end image, and write forceful motion language, and add an explicit "do NOT add colour — skin, shirt, laptop all stay pure white with black outlines." That combination gets you movement and holds the style.

For a loop where the two ends genuinely differ, don't fight the model — ping-pong the frames in code instead. Play forward, then backward. Any clip closes seamlessly that way.

Pull the frames out

ffmpeg -i clip.mp4 \
  -vf "select='lt(n\,60)*not(mod(n\,3))',scale=400:-2,colorlevels=rimax=0.96:gimax=0.96:bimax=0.96" \
  -vsync 0 -q:v 4 frames/f_%02d.jpg

Two things matter in that command.

colorlevels is not optional. The video encode leaves your background at rgb(250,250,250), not white. On a white page that's a faint grey square, and you will see it. This clips anything above 96% to pure 255.

Take fewer frames than you think. Twenty frames at 55ms each is about 18fps. Choppy on purpose. Smooth looks like video; choppy looks drawn.

Then open every frame and throw away the bad ones. Faces wobble, lines thicken, hands melt. Skipping this is the difference between a drawing and AI slop.

Check the motion with numbers

At 180px you cannot eyeball whether something is actually moving. So measure it: sum the ink in the region that's supposed to move, across every frame, and look at the spread.

Under about 5% variation means it's static — regenerate. This caught two completely dead clips I'd otherwise have shipped.

The same trick catches silent colourisation. Sample the face region and check max(r,g,b) − min(r,g,b). Above ~8 and the model has quietly coloured in your black-and-white drawing while you weren't looking.

Playing it back

A few things that aren't obvious:

Draw to a <canvas> rather than swapping <img src> — swapping a large image every tick gives you a visible decode hitch.

Pace it with requestAnimationFrame and a time accumulator, not setInterval. setInterval drifts, and a background tab throttles it to roughly 1Hz — I measured 4 ticks in 3 seconds instead of 54 — which strands your character mid-gesture.

Put the first frame in the HTML as a real <img> with the canvas transparent on top. The illustration is then in the markup, visible before JavaScript runs, and degrades to a still under prefers-reduced-motion.

And stop drawing when it's off-screen.

Pick the trigger carefully

Scroll direction as a state toggle is the good one: scroll down, she goes back to work; scroll up, she waves. Direction only picks a target pose. The page never captures or slows your scroll.

Play-once-on-arrival suits the 404 and about-page beats. They're moments, not loops.

And do not scroll-scrub a narrative. I built that first: a hero film where scroll position drove the playhead. It was strictly worse than letting a video play — 2.9MB instead of 1.1MB, autoplay removed, and the viewer doing manual labour to see something that took twenty seconds on its own. If the input is "move forward through a story," a <video> wins. Frames earn their cost only when the input is something video can't respond to.

The one rule

A greeting is finite.

My first wave looped forever, and it read as frantic — I counted eight direction changes per cycle when I traced the hand position. Real people wave twice and stop.

So she waves twice, then rests. The typing loops, because that's what continuous work actually looks like. Waving doesn't.