Building with AI
Make a motion graphics video in code
A 12-second video with no editing app and no timeline. One function draws any frame, a browser renders them, and the AI reviews its own frames.
- Kind
- Guide
- Effort
- Weekend Project
- Level
- Intermediate
What you'll need
- A Windows, Mac, or Linux computer with Node 22 or newer
- ffmpeg, the free command-line video tool
- Claude Code, or another coding agent that can run commands and look at images
The video below has no editing app behind it. No timeline, no keyframes dragged by hand. It’s a JavaScript file that can draw any moment of the film on request. A headless browser asked it for 360 frames, and ffmpeg stitched them into an MP4. The music was composed in code too.
I had Claude Code, running Opus 5.5, build the whole setup on my Windows PC and make this film with it. This guide walks through that setup. Once it exists, a new film starts from a shot list instead of a blank page.
One function, every frame
Everything rests on one rule: the film is a function of time. You hand it a canvas and a number of seconds, and it paints that exact frame. It keeps no state between calls, uses no timers, and never calls Math.random. Ask for 4.25 seconds twice and you get the same picture twice.
That rule buys three things. Two renders of the same code match. Any single frame can be pulled out and looked at, by you or by the AI. And changing one shot means editing a few lines and rendering again, not redoing everything after it.
Below is the same film, drawn live in your browser by the same code that rendered the video. Drag the slider. Every position calls the film’s draw function once.
Frame by frame
Live: the film's own code, drawn in your browser
Or start with one prompt
If you’d rather see where Claude takes it, open a fresh Claude Code session in an empty folder and paste the prompt below. Swap in your own subject and length. Twelve seconds is a good first film. It asks for the same studio this guide builds, in stages, and stops for your OK before the film gets made. The steps after it explain what each stage is doing, and the starter kit at the end skips the building entirely.
I want to make short motion graphics videos entirely in code, with no video editor. Build a small studio in this folder, then make a first film with it. Work in the stages below and stop where I ask you to.
The core idea: a film is a JavaScript function, draw(ctx, t), that paints the frame at t seconds onto a 1920x1080 canvas and keeps nothing between calls. No timers, no CSS animation, and no Math.random (use a seeded hash when something should look random). Any frame must be drawable on its own.
Stage 1, setup. Check for Node 22 or newer and ffmpeg, and tell me how to install anything missing on my system. Install Playwright with Chromium. Write a CLAUDE.md of house rules every film follows: the rendering rules above; one sans font and one mono font; one accent color; never a title centered on a gradient, a fade-in on every element, glows, particle bursts, or a border around the frame; a new event on screen at least every few seconds; text that reads at phone size; and no number on screen that isn't true.
Stage 2, tools. Write:
- render.mjs: serve this folder on a local port, open the film page in headless Chromium, call window.seek(t) for each frame, and pipe the frames to ffmpeg as an H.264 MP4. Paint several sub-frames per frame and average them for motion blur. Add --stills (one frame per beat, tiled into a contact sheet) and --at <seconds> (one frame).
- lib/motion.js: a closed-form damped spring, a helper for a value that moves to a new target more than once (one spring per move, added together), and a smoothstep ramp.
- music.mjs: an original soundtrack written as a WAV on the film's beat grid. No samples or tracks that would need a license.
- finish.mjs: join picture and sound, bring loudness to about -14 LUFS (measure first, then correct), and make review sheets from the finished MP4: a contact sheet, a phone-size sheet, and a strip of frames around a moment I name.
Stage 3, plan. Use 120 BPM unless the subject calls for something else. Propose a [LENGTH]-second film about [YOUR SUBJECT]. Give me a beat-by-beat shot list: what's on screen, what arrives or leaves, and where the soundtrack hits. Then stop and wait for my OK.
Stage 4, build and review. Build the film, render the stills, and open the contact sheet yourself. Mark each check pass or fail, with a timestamp for every fail: something happens in the first second, every label reads at phone size, nothing is clipped at the edge, no beat is empty, motion uses springs rather than fixed easing curves, and every number is true. Fix the fails and look again. Keep a short log of what each round found.
Stage 5, finish. Render the full video, write the soundtrack, finish it, and review the sheets from the real video too, since some problems only show in motion. Fix what you find. Then show me the final MP4 and the log.
It’s a long prompt on purpose. Each stage gives Claude something it can check before moving on, and the stop after the shot list is where you steer.
What it made for me
I ran the prompt in an empty folder and gave it the AI chip market as the subject. It stopped at the shot list, and I let it run longer than 12 seconds so the figures had time to be read.
Then I told it to use numbers from company filings only. It swapped out one figure but missed another: a milestone date that came from news coverage, while the end card said every figure came from company reports. A review in a separate Claude session caught it. The fix was a new rule in its own check script that fails the build on any number without a filing behind it.
That review also suggested seven changes. Claude measured each one against the film before touching anything, made the ones that held up, and pushed back on two. Four versions in, this is the film.
Build the studio
-
Install the tools. On Windows,
winget install OpenJS.NodeJS.LTS Gyan.FFmpeg. On a Mac,brew install node ffmpeg. Then make a folder, and inside it runnpm install playwrightandnpx playwright install chromium. Playwright drives a headless Chrome, which is the thing that paints your frames. -
Write the house rules. At the start of every session, Claude Code loads the
CLAUDE.mdfile in the folder it’s working in. Rules written there apply to every film without repeating them. The list of looks I never want matters most. Left alone, an AI reaches for the same few moves every time, and people have learned to spot them:## Look - Never: a title centered over a gradient, a fade-in on every element, labels parked in the corners, a border around the frame, glow effects, particle bursts. - Geist for type, Geist Mono for code. Deep teal is the only accent. - No beat goes by with nothing happening. - Judge text on phone.png. If it can't be read at 360 pixels wide, it's too small. - A number on screen is either true or it's gone.The full file also covers the render rules (nothing tied to the clock or to chance), sound, and the review checklist in step 6. It’s in the download at the end.
-
Get a shot list before any code. A film that hits on the beat feels made on purpose. Pick a tempo first. At 120 beats per minute, a beat is half a second, and 12 seconds is 24 beats. Then ask for the plan, and read it before you say go:
Plan a 12-second, 1920x1080 film about [SUBJECT] at 120 BPM. Number the beats 0 to 23. For each one, say what is on screen, what arrives or leaves, and whether the soundtrack hits there. Follow CLAUDE.md. Show me the list and don't write code until I say so. -
Build the film as one function. The film file exports
draw(ctx, t). A small HTML page gives it a canvas and exposeswindow.seek(t)for the renderer. The renderer loads that page, callsseekonce per frame, and pipes each picture into ffmpeg. Browsers block JavaScript modules on a page opened straight from disk, so the renderer runs a tiny local web server for it. -
Give the motion some weight. An easing curve takes the same shape whether a box moves an inch or across the screen, and the eye reads that as mechanical. A spring acts like something with mass. It picks up speed, swings a little past the mark, and comes back. The scrubber above shows the two side by side at about 9 seconds.
The harder case is a value that moves more than once, like a cursor visiting two buttons. Restarting the spring at each move makes it jerk. Instead, each move adds its own spring, carrying the difference between the old target and the new one, on top of whatever the earlier springs are still doing. Any frame can still be drawn on its own, because each spring only needs
t:// stops: [[time, value], ...] export function retarget(t, stops, stiffness = 170, damping = 26) { return stops.reduce( (value, [at, to], i) => i === 0 ? to : value + (to - stops[i - 1][1]) * spring(t - at, stiffness, damping), 0, ); } -
Make it look at its own frames. Claude Code can read images, so it can check what it rendered instead of trusting the code it wrote. Every fix in the table below was found by looking at frames. Render one still per beat, tile them into a contact sheet, and hand it a checklist rather than asking whether it looks good:
Open out/frame-by-frame/contact.png. Review it as if someone else made it and you have to sign off before it ships. Pass or fail each check, with a timestamp for every fail: something happens in the first second, every label reads at phone size, nothing is clipped at the edge, no beat is empty, every number on screen is true. Fix the fails, render the stills again, and show me the new sheet. -
Render, add sound, and review the real video. For motion blur, the full render draws each frame 8 times, a fraction of a frame apart, and averages them. That makes this 12-second film 2,880 browser paints. It took 40 seconds on my PC. The soundtrack is generated from the film’s own beat grid, so every hit lands where the picture expects it. The last step joins picture and sound at about -14 LUFS, the loudness most video sites expect, and makes review sheets from the finished MP4. Review those too.
What the review rounds caught
Four rounds. Rounds 1 and 2 looked at stills, rounds 3 and 4 at the rendered video. Each one found something.
| Round | What was wrong | Fix |
|---|---|---|
| 1 | Two dead beats with nothing happening. Thumbnails clipped at the edge. Labels unreadable at phone size | Moved scenes a beat earlier, bigger labels |
| 2 | A bar turned muddy gray halfway through a color change | Changed the color path |
| 3 | Fast moves showed four ghost copies instead of blur. Loudness at -15.3 LUFS | Twice the blur steps, measure loudness first |
| 4 | Two lines of headline type set too tight | Opened up the spacing |
Round 3 is the one to remember. The stills looked fine, because a still can’t show motion blur. The ghosting only showed up in a strip of frames pulled from the real video.
Round 3 also caught something I care about more. The progress bar’s label counted “frame 013 of 360,” and the number wasn’t tied to anything. It was decoration dressed as data. Now it says “Rendering 360 frames,” which is true for this film. An AI will add a believable fake number to make a shot look busy. Check every number on screen.
Where it falls short
The music is simple: a bass line, chords, and ticks on the beat. Good enough to carry 12 seconds, not a score. The loudness landed at -14.4 LUFS against a target of -14, because the tool won’t push the peaks any louder. The film uses no screenshots or video, so it doesn’t show the harder job of animating a real product’s interface. That’s the next thing I’d try.
Take it further with a voiceover
Music carries a 12-second film. A longer one usually needs a narrator. I made this 83-second video for Kindred Tales with Claude Code, on the same idea of a film drawn from code:
Claude Code wrote the script, recorded it one line at a time with an ElevenLabs voice, and let the length of each recording set how long its scene runs. Re-record a line and the film re-times itself. The scenes are built with Remotion, a framework that turns React components into video, instead of the plain canvas above.
The step I’d steal first: Claude can’t hear. Every voice take was transcribed back to text with faster-whisper, a free speech-to-text model, and checked word by word. That caught stutters, repeated phrases, and once the narrator reading its own stage direction aloud. The transcript’s word timings also told each scene when to bring in its headline, so the words on screen land as they’re spoken. The whole build is in How I made a narrated product video in code.
Keep the ElevenLabs key in a .env file that Git ignores, and give Claude Code the variable name, not the key. A key typed into a prompt stays in your chat history.
Get the starter
The download has the studio as I used it: the house rules, the renderer, the soundtrack and finishing scripts, the motion helpers, and this film as a starting point.
Motion studio starter ZIP, 16 KB. Node scripts, the house rules, and the example filmExploring AI for your business?
I’m happy to compare notes. Tell me what you’re trying to do and where it’s stuck.
Key takeaways
- A video can be a function. draw(ctx, t) paints the frame at t seconds and remembers nothing.
- Write down the looks you never want, in a rules file the agent loads every session.
- Review the rendered video, not only the stills. Some problems only show in motion.
- Never show a number as real that isn't. A decorative frame counter counts.