Motion graphics as code, timed to a voiceover
A starter kit for explainer animation with no After Effects: every scene a TypeScript function of time, word-level timings from forced alignment, a headless-Chrome renderer and a scripted sound mix. The kit’s full workflow guide, its demo prompt, and the 92-second video it renders, with sound. Built on the open-source pdoom-video engine, and checked against its code
An explainer video in which nothing is keyframed by hand. Each scene, called a plate, is a TypeScript file that draws every frame from the clock; word-level timings of a voiceover tell each plate when to move, so the picture stays locked to the voice; a script drives headless Chrome to render the frames and ffmpeg turns them into an MP4. Because it is all code, you change the animation by asking Claude to change the code. This is the kit’s whole workflow guide and the 92-second video it renders, sound included. The kit’s own files weren’t part of what was shared, but its engine is public, so the preview and render half works from the open-source pdoom-video today. The checks found the engine matches the guide closely, and fixed two example commands that point at the wrong moment.
A workflow guide for a starter kit called Motion as Code, and the video the kit produces: a 92.6-second, 1920x1080, 60 fps explainer whose opening line is “What if I told you you can create motion graphics like this without even opening After Effects?”. The kit is built on pdoom-video, an MIT-licensed engine for code-rendered video, which its author says he built with Claude Opus 5.5 in Claude Code. The kit’s own files (its nine plates, voiceover, sound effects and analysis scripts) weren’t included and don’t appear to be public; the engine underneath is, and the commands below are marked where they work on it as well.
What held up, on 9 October 2026
Checked against pdoom-video’s code and full public history (14 commits, 24 September to 5 October 2026), read rather than run; the video’s soundtrack and frames; and the uv, Playwright, ElevenLabs, Hugging Face and Anthropic pages. Claims about the kit’s own files can’t be checked, and say so.
One frame from each, in order
What the video says, plate by plate
The full voiceover, transcribed from the video. The guide’s own table abbreviates these lines.
Two of those lines want a footnote. “Without touching a timeline” means without a timeline editor: the edit still lives in a timeline, a code file called timeline.ts. And “prompt to code to render to video” leaves out the voiceover, the word alignment and the sound pass, which the guide adds back below.
What you are getting
The kit is a complete, working example of the prompt, code, render, video workflow. It produces the 92.6-second, 1920x1080, 60 fps explainer above.
Nothing in it is keyframed by hand. Each scene, called a plate, is a TypeScript file that draws every frame as a function of time. Word-level timings of the voiceover tell each plate when to animate, so the picture is locked to the voice.
Because it is all code, you can ask Claude to change the animation (a colour, a speed, a transition, a whole new plate) and re-render, instead of moving hundreds of keyframes.
The six stages
Stages 3 to 5 are the pdoom-video engine and work from its public repository. Stages 1, 2 and 6 are the kit’s additions for a spoken voiceover.
Voiceover
audio/voiceover.mp3, made with ElevenLabs.
Align
align_vo.py and audio_vo.py turn the voiceover into word-level timings and loudness data.
Scenes
scenes/*.ts, one file per plate, and timeline.ts, which says when each plays.
Preview
Vite, live in the browser at localhost:5173.
Render
render.ts drives headless Chrome to draw the frames and ffmpeg to encode them.
Sound
sfx_mix.py mixes the sound effects under the voice; ffmpeg puts the mix onto the picture.
The preview and the exported MP4 are drawn by the same code from the same time value, so what you scrub to is what renders. (The guide calls them identical; they aren’t quite, since the export averages motion-blur sub-frames and the preview doesn’t.) Re-timing works the same way: change the voiceover, re-run the alignment, and the plates find their words by content rather than by fixed times. Reload the page afterwards, since the timing data is read once at start-up. In pdoom-video, an anchor phrase in timeline.ts that is no longer in the script stops the whole preview from loading, with “lyric not found” shown in red, until you update the cut(...) phrase; the kit’s own cut() and lineOf weren’t available to check. Moves with a fixed length inside a plate keep that length.
What goes in the zip
Include these folders and files. Leave out out/ (the renders; the guide puts them at over 2 GB) and app/node_modules (rebuilt by the install step).
Paste it into your project
The guide says to unzip, then copy the contents into the folder you work in (your Claude Code project folder, or add the folder to the project’s files), and to keep the folder structure exactly as it is, because the scripts use relative paths.
One change worth making: unzip it into a folder of its own and start Claude Code there, rather than copying it over an existing project. The zip’s README.md and .claude/ would replace yours, and the .claude/ folder (most likely a preview configuration for the Claude Code desktop app) is worth reading before you use it, since project settings there can run commands.
Requirements
First run
From the project root. Open the address Vite prints, normally http://localhost:5173, and press space (or click the frame) to play; the voiceover plays with it. If a render later can’t find Chrome, see the troubleshooting table at the end.
The same preview, from pdoom-video
The engine’s repository has the same layout, the same preview and the same keys, with its own music video as the example: seventeen plates timed to a song instead of nine timed to a voice. Not in the guide.
Its documented render, from the repository root. The engine has moved on since the kit was built: current versions decide motion blur for each 32x32 tile of the frame and run two renderers at once.
1. Preview, live
The preview is the fast loop. Edit a scene file and the page hot-reloads; scrub back to the moment you changed. Add ?t=35 to the address to start at 35 s (it stays in the address, so each reload starts there too).
2. Check stills while editing
Renders single frames, here two moments in the code plate, so you can inspect a detail without rendering the whole video. Changed from the guide, whose example asks for 12.5 s, which is in the model plate: with --only the engine draws a placeholder there. The quotes around the times keep PowerShell from splitting the list; they’re harmless elsewhere.
3. Render the video
The kit’s shortcut, from its package.json; written to out/motion-as-code.mp4. It runs render.ts with adaptive motion blur (the guide says 4 to 12 sub-frames a frame; uncapped, this engine gives 12 or more, more where motion is fast, so either the kit caps it or the figure is off, which can’t be checked), a shutter of 0.5 and CRF 17. A flag the shortcut already sets, such as --samples, is ignored if you add it after bun run render, because render.ts reads only the first copy of a flag; one it doesn’t set, such as --scale 2, can simply be added. To change one it sets, run render.ts yourself with the shortcut’s other settings and its output name, so you keep the motion blur and the sound step below still finds the file, for example: bun scripts/render.ts video --samples 4 --shutter 0.5 --crf 17 --out ../out/motion-as-code.mp4 (check the kit’s package.json for its exact flags).
Useful flags
4. Add sound effects
There are 28 sound effects in audio/sfx and a cue sheet of about 500 cues in analysis/sfx_mix.py, derived from the same word timings. Effects duck up to 7 dB under the voice and the mix is normalised to -14 LUFS, the loudness Spotify plays at and YouTube is widely measured at. No re-render is needed: rebuild the mix, then copy the picture and add the audio.
To make an effect louder or quieter, change its db value in the cue sheet. To move one, change its time expression. This mixer is the kit’s own; the engine has none.
Rebuild the mix, from the project root: the render step above left you in app/, so cd .. first. The guide runs uv as python -m uv, which works only if uv was installed with pip into that Python; on a Mac that Python is usually python3, since macOS and Homebrew don’t put a bare python on the PATH. If uv was installed any other way, drop the python -m and type uv directly.
Then put the mix on the picture. mix.wav carries the voice as well as the effects, which is why the render’s own audio track is dropped. For a file you will host on the web, add -movflags +faststart. The backslash continues the line in macOS and Linux shells; in PowerShell, put it all on one line.
5. Swap the voiceover
Replace audio/voiceover.mp3
With your own read of the script.
Edit SCRIPT, and SPOKEN for numbers and acronyms, in analysis/align_vo.py
SPOKEN gives letters-only spellings for anything the alignment model can’t read, such as Opus 5.5, MP4 or 3D, because it only knows letters and apostrophes. The engine does the same thing in its pron.py.
Re-run the alignment and audio analysis
The commands are below. The plates find their words by content (the kit’s lineOf and wordOf helpers), so if you keep the same script, every animation re-times itself to the new read, as the guide puts it; strictly, that is every cut and word-synced move, since fixed-length moves inside a plate keep their length.
From the project root: the sound step left you in out/, so cd .. first. The python -m uv note above applies here too: on a Mac use python3 (a bare python usually isn’t on the PATH), or type uv directly if uv wasn’t installed with pip.
How the project fits together
Writing or changing a plate
A plate is a class that extends Scene (app/src/engine/scene.ts), and its file exports it as the default. The rule that matters: everything must be a deterministic function of time: no Math.random() or clock readings (seeded random numbers from the engine’s mulberry32 or hash are fine). The engine calls render(f, out) for every frame, and in the export for every motion-blur sub-frame too, out of order (the guide says once per frame), so a plate must never count its calls. f carries:
To add a plate, create app/src/scenes/yourplate.ts, then add one E('yourplate', 'yourplate', start, end) line to timeline.ts: the first argument is the plate’s id, the second its file name. Start times come from the script: in the kit, cut('This is Claude') cuts 0.18 s before that line’s first word. (The engine itself snaps cuts to a song’s beat instead.)
The example prompt shown inside the video, in the prompt plate, draws its token breakdowns from app/src/scenes/prompt-data.ts. Edit that file to change the words and the next-token candidates it displays. That panel, and the file’s format, come from the engine’s own prompt plate.
Copy this into Claude
After the folder is in place, open Claude Code in it and send this. It verifies the install, makes one visible edit, then walks you into using your own voiceover. Changed from the guide: the stills are at 92.2 s, once the slam has landed (it lands at about 91.9 s in the recording, whose cuts come about 0.2 s before the guide’s times, so 92.2 s is safe either way; the guide’s 89.5 s is about two seconds before the slam), and inside the added hold, where the edit actually shows, since a still at the slam alone looks the same before and after it; and the last rule covers any full render, not just 4K, so nothing renders in full without your say-so.
Ideas for your own follow-up prompts
The example prompt used in the video itself. You can paste it into Claude as a plain test of the idea; it asks for 15 seconds, so it isn’t the prompt that made the 92-second explainer.
Troubleshooting
Credits and licence
The visual language is the engine and style of pdoom-video by mexicat (Giacomo Magnanini). Its code is MIT-licensed, included in the kit as LICENSE.pdoom-engine; keep that file with the project. That covers the palette (ink, bone, signal orange), the spark motif, the graph-paper construction sheets and the post-processing (bloom, halation, grain). The fonts that ship with it keep their own licences: Archivo, IBM Plex Mono and Cormorant Garamond under the SIL Open Font License, and the single-stroke plotter fonts under OFL or public domain. (The guide puts the fonts under the MIT licence too.)
The guide says the nine plates themselves are new and were written for this voiceover; the prompt plate’s next-token panel and its prompt-data.ts format come from pdoom-video’s own prompt plate. The voiceover is by ElevenLabs, and the sound effects were generated with ElevenLabs Sound Effects v2. Check the terms of your own ElevenLabs plan before publishing a video that uses your own generated audio: on the free plan it can’t be used commercially and its title must credit elevenlabs.io or 11.ai. The alignment model, wav2vec2 (most likely Meta’s wav2vec2-base-960h, Apache-2.0), isn’t credited in the guide.
Built from the kit’s seven-page workflow guide and the 92-second video it renders, both kept in full, with the guide’s main corrections marked where they apply and summed up in the table at the top. The narration was transcribed from the video. Checked on 9 October 2026 by reading pdoom-video’s code at commit fa164b7 and its full public history, without running any of it; the kit predates the engine’s 5 October changes, so it is built on the version of 2 October or earlier. Commands that only exist in the kit say so.