DEV Community

Cover image for How I make motion videos with Claude Opus 5.5 (the full setup, prompts and numbers)
Arqam Waheed
Arqam Waheed Subscriber

Posted on AI-assisted

How I make motion videos with Claude Opus 5.5 (the full setup, prompts and numbers)

TL;DR. Opus 5.5 never touches a pixel. It writes one HTML file where every frame is a pure function of time, Playwright screenshots each frame, ffmpeg stitches them. Swap every adjective in your prompt for a number, ban the defaults by name, review stills before the full render. My 3D Slabwatch film rendered on a laptop RTX 2050 at about 0.3 s a frame.

I'm not a motion designer. I've never opened After Effects. Last week I still needed a product film for Slabwatch, so I set up a motion design agent in Claude Code on Opus 5.5 and made one.

It went through a flat 2D version that I honestly didn't like, then a full 3D rebuild with a modelled electricity meter, real lighting, depth of field, a mirror floor and a synthesised soundtrack, all rendered on my laptop's RTX 2050.

Every "one prompt" motion video on your feed skips the middle part. The second prompt, the review, the "frame 84 is late" note. This article is that middle part. The exact setup, the render rules, the prompt template I use now, the numbers that make motion look expensive, and the checks that keep it sharp. Then, because my way isn't the only way, every other route I found: Blender, video models, Remotion, Manim and the rest, and when each one beats mine.

What's inside

  • 01. Pixels: why Opus writes a program instead of a video
  • 02. Rig: the setup from zero
  • 03. Real assets: never let it draw your product
  • 04. Direction: replace every adjective with a number
  • 05. Numbers: easing, springs, durations, beats
  • 06. Bans: name the defaults you never want to see
  • 07. Film one: flat, and "not that amusing"
  • 08. Film two: 3D on a laptop GPU
  • 09. Sharpness: the four rules for readable 3D
  • 10. The loop: stills, score, fix, repeat
  • 11. Sound: scoring a film when the model can't hear
  • 12. Long pieces: the director's brief
  • 13. Ship: export for X, GIFs included
  • 14. Every other route: Blender, video models and the rest
  • The hype check, and the whole thing in one table

01. Pixels: Opus writes a program, not a video

Opus 5.5 outputs text. It never touches a pixel. Every motion clip you've seen from it is a program, almost always one HTML file, where every frame is a pure function of time. You give it a time t, it draws exactly that frame.

Then a headless browser (Playwright) opens the page, freezes it at frame 0, 1, 2 and so on, screenshots each one, and ffmpeg stitches the PNGs into an MP4.

That sounds boring but it's the whole trick, and it gives you three things:

  • The same input gives the same frame EVERY run. I checked on my rig: render one frame twice, the file hashes match.
  • An edit is one changed number and a re-render. You're not re-rolling a video model and praying.
  • Opus can look at its own frames. Render stills, it reads them like a storyboard, it fixes what's wrong.

For that to work the page has to follow a few rules. I call it the render contract and it gets pasted under every motion prompt. Each line exists because something breaks without it:

RENDER CONTRACT (paste under any motion prompt)
- One self-contained HTML file. Only the libraries I name, pinned to an exact version.
- No network requests except fonts and those pinned libraries.
- Every frame is a pure function of t (seconds). Expose window.renderAt(t).
- No Date.now, no setTimeout / setInterval, no CSS transitions,
  no requestAnimationFrame in render mode.
- Seeded noise only (mulberry32). Never Math.random.
- No state carried from one frame to the next.
- Wait for document.fonts.ready before frame 0.
- Loops: render frames 0 to N-1. Frame N-1 must flow into frame 0.
- PNG frames into the final encode. Never JPEG.
Enter fullscreen mode Exit fullscreen mode

Why each line matters, quickly:

  • A clock or a timer means frame 84 comes out different every run, so you can't fix it.
  • Math.random makes shapes jitter between frames.
  • Skip the fonts line and your first frames render in the fallback font. You'll see Times New Roman flash for a frame and not know why.
  • Render frame N of a loop and frame 0 shows twice, that's the little hiccup at the loop seam.

One thing Movez points out that surprised me: Opus defaults to this plain "one HTML file + Playwright + ffmpeg" route even when Remotion or HyperFrames are installed. If you want a framework, say it in the prompt.

My route is plain HTML, plus three.js when it's 3D. It's not the only one. Section 14 has the full table of every route (Remotion, HyperFrames, Blender, Manim, Lottie, Rive, video models and more), with licenses and when to pick each.

And where code loses: photoreal scenes, complex characters, organic movement. seeco makes this point well. Code wins on product films, UI motion, data stories, typography and launch explainers, which is basically everything a builder needs. For the rest, section 14 covers Blender and the video models, and how to mix them with code.

02. Rig: the setup from zero

No Claude Code? Start here. Florence's article has a no-install path. Open the Claude app, pick Opus 5.5, ask for the animation as one HTML file, and it plays in the preview panel. Then screen-record it (Cmd+Shift+5 on a Mac, the Snipping Tool's video mode on Windows). While you iterate, ask for a frame counter on screen and a space bar pause. It's not frame-exact, a screen recording can drop frames, but it's fine for a first post.

For the real thing: I run everything in Claude Code on Linux, but this works the same on a Mac. You need Node, ffmpeg and a headless Chromium.

# one folder for every piece you make
mkdir motion-studio && cd motion-studio
npm init -y
npm i -D playwright
npx playwright install chromium
# ffmpeg: sudo apt install ffmpeg (Linux) or brew install ffmpeg (Mac)
claude   # then /model and pick Opus 5.5
Enter fullscreen mode Exit fullscreen mode

Then a renderer. Mine is scripts/render.mjs, Claude wrote it, and the core is about this big:

// node render.mjs anim.html 6 30   (file, seconds, fps)
import { chromium } from 'playwright';
import { pathToFileURL } from 'node:url';
import { mkdirSync } from 'node:fs';

const [file = 'anim.html', secs = '6', fps = '30'] = process.argv.slice(2);
const FPS = Number(fps), N = Math.round(Number(secs) * FPS); // frames 0..N-1
mkdirSync('out/frames', { recursive: true });

const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1920, height: 1080 } });
await page.goto(pathToFileURL(file).href, { waitUntil: 'networkidle' });
await page.evaluate(() => document.fonts.ready);

for (let i = 0; i < N; i++) {
  await page.evaluate((t) => {
    if (window.renderAt) window.renderAt(t);
    document.getAnimations().forEach((a) => { a.pause(); a.currentTime = t * 1000; });
  }, i / FPS);
  await page.screenshot({ path: `out/frames/${String(i).padStart(4, '0')}.png` });
}
await browser.close();
Enter fullscreen mode Exit fullscreen mode

Then frames to video:

ffmpeg -framerate 30 -i out/frames/%04d.png -c:v libx264 -profile:v high \
  -pix_fmt yuv420p -crf 18 -movflags +faststart out/anim.mp4
Enter fullscreen mode Exit fullscreen mode

Every frame is exact because every PNG is the page frozen at one precise t, no matter how slow your machine is. A screen recording drops frames, this doesn't.

Next to the renderer I keep two more scripts:

  • scripts/qa.sh: makes a contact sheet, a frame strip, a phone-size test and a loop check from any MP4 (the ffmpeg lines are in section 10)
  • scripts/beats.py: librosa beat detection, so cuts can land on the music. In the smoke test it found 120 BPM on a 120 BPM click track, so it at least knows what a beat is

And a CLAUDE.md in the studio folder that holds the render contract, the ban list and the QA rules. Every time something goes wrong, the fix goes in there as a rule, so the next piece doesn't hit it again. Honestly that file ends up mattering more than any single prompt.

Each piece gets its own folder: pieces/<name>/ with the brief, style guide, shot list, assets, src, reviews and out.

From then on you barely type commands. You tell Claude Code "render this, open frames 0, 30 and 60, tell me what you see before you change anything" and it runs the loop itself.

03. Real assets: never let it draw your product

Movez and seeco both say this and it's the rule I'd tattoo on the agent: never let the model invent your product screens or numbers. A fake UI looks fine for about two seconds, until someone opens the real app and nothing matches.

What I did for Slabwatch: the agent didn't redraw my UI at all. It loaded the site's actual HTML and CSS in an iframe inside the film, with live numbers. Same fonts, same layout, same everything, because it IS the page.

If you're doing a brand or launch reel, the instructions that work (mostly from Movez and seeco):

ASSETS
- Here is the URL: <your site>. Use the actual product screens, logo and assets.
- Capture them with Playwright into ./assets and list them before you start.
- Never redraw the UI from imagination.
- If a real asset is missing, stop and ask me. Never draw a convincing fake.
- Use my real numbers only. Never invent metrics.
Enter fullscreen mode Exit fullscreen mode

Same for references. Naming a style beats describing it. If you give it a frame from a film you like, say what to take (palette, type, grain, shot length) and what NOT to take (the subject). Movez's line for this is basically "take the grammar, never the content".

04. Direction: replace every adjective with a number

This is the single most useful idea in all of this, and it's from Florence.

Florence ran the same 3D product shot twice. The vague prompt was three adjectives ("cool", "premium", "modern"). It came back as navy space, a glowing halo ring, bloom, particles, a spinning product and a tagline nobody asked for. That's the default look, and adjectives are where the default lives.

So you swap every adjective for a number:

You'd say Say this instead
"premium" materials: roughness 0.38, clearcoat 0.35; ease-out-quint, no overshoot on text
"cool lighting" key, fill and rim lights, each with a hex color and an intensity
"modern camera" fov 30, never parked, one push-in on ease-out-expo at 2.0 s
"snappy" durations in frames and a spring damping ratio
"make it pop" scale 1.0 to 1.08 to 1.0 over 6 frames, peak on the beat
"it drags" cut each hold by one beat
"too busy" one hero move, everything else 150 ms later at 60% of the travel

Here's the template I use now. It's Florence's structure with a couple of lines from Movez and seeco. Fill every line before you hit enter:

GOAL:    one sentence. What the viewer feels or does after watching.
OUTPUT:  engine + size. e.g. "A three.js scene in one HTML file, 1920x1080"
TIME:    seconds x fps = frames. Loop: yes / no.
LOOK:    hex palette, ONE accent color, typeface / weight / size / tracking.
MOTION:  cubic-beziers for enter / move / exit, springs as damping ratios,
         durations in frames.
SHOTS:   frame ranges, one hero move per shot, and the handoff
         (cut / match cut / mask wipe). Default to the cut.
MODEL / CAMERA (3D only): what the object is, fov, the one camera move.
IDENTITY LOCK: what may never change (logo proportions, mascot shape).
ASSETS:  real logo, real UI screens, real numbers. If something is
         missing, stop and ask.
BANNED:  see section 06.
QA:      stills at frames [..], a contact sheet, my OK, then the full render.
Enter fullscreen mode Exit fullscreen mode

Write GOAL first. If you can't say in one sentence what the viewer should feel, Opus can't animate it, and no easing curve saves it. Then TIME, because every other number hangs off the frame count.

The QA line matters too. It makes Opus show you stills BEFORE it renders the whole clip, which is where all the real work happens (section 10).

For a short product film, seeco's default story shape is a good starting SHOTS line:

0-2 s    HOOK      one thing that stops the scroll
2-5 s    PROBLEM   shown on screen, not told
5-9 s    REVEAL    the product, through a real interaction
9-13 s   PROOF     one screen or one number
13-16 s  CLOSE     logo + a CTA people can actually read
Enter fullscreen mode Exit fullscreen mode

Every beat needs an entry state, an exit state and a reason. No reason, cut it.

For Slabwatch my hook was literally the problem: an LCD ticking past 200 units and a Rs 32,000 bill. If you're in Pakistan you already know why that's scary, the 200-unit slab is where your bill jumps.

05. Numbers: easing, springs, durations, beats

Motion designers talk in curves and frames, so give Opus curves and frames. Screenshot this section, it's the one I go back to most. Almost all of it is from Florence's cheat sheet.

Easing

Name cubic-bezier Use it for
ease-out-expo 0.16, 1, 0.3, 1 text and logo reveals
ease-out-quint 0.22, 1, 0.36, 1 premium entrances
ease-in-out-quart 0.76, 0, 0.24, 1 moves and camera pushes
ease-in-expo 0.7, 0, 0.84, 0 fast exits
ease-out-back 0.34, 1.56, 0.64, 1 a playful pop with overshoot

Rules: enter with ease-out, exit with ease-in, and make exits 40 to 50% shorter than entrances (400 ms in, 200 ms out). Linear only for rotation, progress bars and the phase of a loop. Linear easing on a move is the fastest way to make a render look like a render.

Springs

Every library names its spring knobs differently. One number translates between all of them, the damping ratio:

// damping ratio from any library's numbers. 1 = no bounce
const dampingRatio = (stiffness, damping, mass = 1) =>
  damping / (2 * Math.sqrt(stiffness * mass));
Enter fullscreen mode Exit fullscreen mode
  • 1.0 never bounces
  • 0.7 to 0.85 feels premium
  • 0.4 to 0.6 is playful
  • under 0.3 is a cartoon

Remotion's default spring works out to 0.5, react-spring's default to 1.0. Position and size may bounce, opacity and color NEVER do. Tiny overshoot on UI, none on type.

Ask for springs as a closed-form function of t (no simulation), so frame 84 can render on its own without computing frames 0 to 83. That keeps the render contract happy. Both Movez and Florence have the code for this, you can just tell Opus "write the spring as a pure function of t".

seeco adds a good rule on top: one spring personality per kind of object. Small UI snaps, panels settle, the camera glides almost invisibly, headlines land hard and hold, mascots bounce on purpose. If everything overshoots, nothing feels precise.

Durations and stagger

UI feedback        100-200 ms
UI moves           200-500 ms
hero reveals       600-900 ms
letters            1 frame apart
words              60-120 ms apart
list items         40-80 ms apart
next element starts when the previous one is 30-50% done
Enter fullscreen mode Exit fullscreen mode

For video, say it in frames. At 30 fps a frame is about 33 ms, so a 400 ms move is 12 frames.

Hierarchy

One hero move per shot. Secondary moves travel 50 to 70% as far and start 100 to 200 ms later. Ambient motion stays under 5% of the hero's travel on a slow sine. Something new every 2 to 4 s, and the hook has to land in the first 2 s.

Holds

On-screen text stays up at least 0.3 s per word plus 0.5 s. A two-word title needs 1.1 s. Keep a slow 2% push going during the hold so the frame doesn't feel dead while people read.

Beat grid

Frames per beat = fps x 60 / BPM. At 120 BPM and 30 fps that's 15 frames. Compute each beat from time and round each one separately, because adding a rounded number again and again drifts off the music. Cut on the beat, change the layout on the bar (every 4 beats). Changing everything on every beat wears people out.

Two accessibility numbers

Text at 4.5:1 contrast on every frame, including over moving backgrounds. Never more than 3 flashes a second. And for phone size, push key poses 10 to 30% further than feels right, the feed shrinks everything.

The trick that ties this together: put all of it in one motion.js tokens file (EASE, durations, stagger) and tell Opus "use only these tokens, no inline numbers". Then the whole film moves like one thing.

06. Bans: name the defaults you never want to see

A generic "avoid the AI look" doesn't work. It just swaps one default for another. What works is banning the defaults BY NAME. This is the list I start from, merged from Movez and Florence:

BANNED
- centered title on a gradient
- everything fading in at once
- fade-and-slide-up on every element
- centered-everything layouts
- purple or blue gradients
- glow (incl. on UI), glow orbs, bloom, neon, lens flares
- decorative particles, particle bursts
- corner labels, frame borders
- generic spinning turntables
- text that moves while it's being read
- dead time
Enter fullscreen mode Exit fullscreen mode

Then keep it alive. After every first render, look at what defaults it used anyway, and add them to the list. My list is longer than this now, and it gets longer every piece.

07. Film one: flat, and "not that amusing"

OK, here's what actually happened when I used all of this.

v1 of the Slabwatch film was 60 seconds, 1920x1080, 30 fps, all flat 2D. A seekable HTML page rendered frame by frame with Playwright. The beats:

  • hook: the meter's LCD crossing 200 units and the Rs 32,000 bill
  • the rates
  • day 19 of usage vs bill day 30
  • the site's boot self-test as the reveal
  • the REAL Slabwatch page in an iframe, live numbers
  • the message unpacked, how it decides
  • logo lockup

Sound was a fallback. The Openverse API timed out, so the agent built a calm sound kit with earshot (more on that in section 11) and a synthesised pad in C, mixed to -14 LUFS.

On X I said it one shotted my demo video. And the first render did come out in one go. It just wasn't the one I ended up using.

My honest verdict on v1: it was flat and not that amusing. Everything was correct and nothing made you want to keep watching.

The lesson I gave my agent from this: for a product film, aim high by default. Real 3D, camera moves, light, depth. And show me the style in stills before rendering anything long. So I told it to go all out and make it 3D.

08. Film two: 3D on a laptop GPU

v2 was three.js 0.186 scenes rendered in headless Chromium on my laptop's RTX 2050. The scenes:

  • a modelled meter with a 7-segment LCD
  • rate columns and 30 day tiles
  • the boot self-test with a 3D mark
  • the real Slabwatch page on a floating slab over a mirror floor
  • a 3D forecast chart with the 200-unit limit as a red glass plane
  • three decision slabs
  • an extruded logo lockup

PBR lighting, depth of field, grain. The agent checked stills over 4 rounds before it rendered the full film.

Things that will cost you time if nobody tells you:

Headless Chromium renders on the CPU by default. It uses SwiftShader, which is software rendering and MUCH slower. These flags put it on your actual GPU through Vulkan:

const browser = await chromium.launch({
  args: ['--use-angle=vulkan', '--enable-gpu', '--ignore-gpu-blocklist'],
});
Enter fullscreen mode Exit fullscreen mode

With them, my RTX 2050 rendered about 0.3 s per frame at 1080p with depth of field and shadows. That's a laptop GPU, so you don't need a 4090 for this.

ES modules need a local http server. file:// won't load them, and three.js examples are all modules now. Something like npx http-server and point the renderer at localhost.

Pin your three.js version and know what changed. PCFSoftShadowMap is gone in three 0.186, so older examples (and older model knowledge) break on it. Florence hit a similar trap in r160 where TextGeometry reads height and ignores depth. Pin the version, name the right parameter in the prompt, and the trap is gone.

Motion blur comes from subframes. A real camera's shutter stays open while things move, a frame from renderAt(t) is frozen at one instant, so fast moves look like they strobe. The fix Florence's 3D pieces and Movez's renderer both use: render 4 subframes per frame and blend them. Spread them over half a frame, which is how a 180-degree film shutter records blur. In the renderer:

// 4 subframes per output frame, spread over half a frame (180° shutter)
const SUB = 4, SHUTTER = 0.5;
let n = 0;
for (let i = 0; i < N; i++) {
  for (let k = 0; k < SUB; k++) {
    const t = (i + (k / SUB) * SHUTTER) / FPS;
    await page.evaluate((t) => window.renderAt(t), t);
    await page.screenshot({ path: `out/frames/${String(n++).padStart(5, '0')}.png` });
  }
}
Enter fullscreen mode Exit fullscreen mode

Then blend each group of 4 into one frame:

ffmpeg -framerate 120 -i out/frames/%05d.png \
  -vf "tmix=frames=4,select='eq(mod(n\,4)\,3)',setpts=N/30/TB" -r 30 \
  -c:v libx264 -pix_fmt yuv420p -crf 16 out/anim.mp4
Enter fullscreen mode Exit fullscreen mode

It costs 4x the render time, so turn it on for the final, not while iterating. Anything that isn't moving stays exactly as sharp, since 4 identical frames blend into the same frame.

Sound for v2 was earshot again, this time with generated risers, whooshes and sub hits over a synth bed, mixed to -14.6 LUFS. Earshot's critique scored the mix 19 out of 21, "launch-grade".

09. Sharpness: the four rules for readable 3D

3D gives you depth of field, grain and close-ups, and every one of those eats text if you let it. A product film lives or dies on whether people can read the screen and the end card. So these four go in the render contract BEFORE the first frame:

1. Text and UI never go through depth of field or any blur. Draw them on their own layer after the DOF pass. Otherwise the bokeh drags ink out of the letters and you get grey halos, and the end card is the one frame everybody actually reads.

2. At least 1 texel per output pixel at the closest shot. If the camera pushes into your UI, capture the page at 2x or more (3x for real close-ups). You can't zoom into pixels that aren't there.

3. Grain at .02 or less, PNG frames all the way to the encoder. JPEG frames before x264 means every frame gets compressed twice, and heavy grain makes it worse. Encode the final at crf 18 or lower.

4. Look at full-size frames before you call it done. Pull the closest shot and the end card at full size and crop the text at 200%. A contact sheet won't show soft text and neither will a self-score. Only a full-size frame does. My agent's QA script also measures it: a sharpness score per text box (the strongest edges divided by the text's contrast), 0.50 or higher to pass, 0.60+ for big type. It's not done until that passes, whatever any self-score says.

Here's the block I paste:

SHARPNESS
- Text and UI on their own layer, drawn AFTER depth of field / any blur.
- Screens and textures captured at >= 1 texel per output pixel at the
  closest framing (2x+, 3x for close-ups).
- Grain <= .02. Chromatic aberration ~.0025. Max blur ~.006.
- PNG frames only, final encode crf <= 18 (16 by default, 14 for detailed 3D).
- Before "done": full-size closest shot + end card, crop text at 200%.
Enter fullscreen mode Exit fullscreen mode

Two web-side extras from the sources: never put will-change on anything the camera scales (it rasterizes once and stays blurry), and snap resting text to whole pixels or render at 2x and scale down.

10. The loop: stills, score, fix, repeat

The first render is a draft. Every source says it and it matched what happened to me. The loop that finishes a film:

  1. Render stills at named frames and a contact sheet. Not the full video.
  2. Score them like a harsh motion director, not a proud author (that's Movez's line, and it's the right mindset).
  3. Fix the 3 worst things, with frame numbers.
  4. Repeat 2 to 4 rounds. Then render the full thing.
  5. Run the sharpness check from section 09 on full-size frames.

The scorecard I use, from Movez, each 1 to 10, keep going until everything is 8+:

SCORE EACH 1-10, THEN FIX THE 3 WORST WITH FRAME NUMBERS
- hook in the first 2 s
- readability at 360 px wide (phone)
- motion quality
- variety
- composition
- brand accuracy (real assets, right colors)
- sound sync
Enter fullscreen mode Exit fullscreen mode

And the hunt list, things to look for on every pass: text overlapping during swaps, sliding where it should ease, corner labels and borders sneaking back in, centered-on-gradient, blurry scaled text, dead beats, a stutter at the loop seam.

Measure instead of eyeballing where you can. Florence caught a glint flooding 178,864 pixels and an anticipation pose landing ONE frame late, just by measuring. You can ask Opus for an onion skin (frames stacked at low opacity, even spacing means robotic motion) or a plot of position per frame (sharp corners mean dead starts and stops). It can draw both and read both.

Your notes should point at frames, not feelings. "The punch at frame 84 peaks late, move it 2 frames earlier" works. "Make it better" doesn't.

When a fix doesn't hold, render frames N-1 and N+1 side by side, have it list the differences, and fix only that line.

These ffmpeg one-liners do most of my reviewing:

# contact sheet: 2 frames a second, tiled
ffmpeg -i out/final.mp4 -vf "fps=2,scale=270:-1,tile=6x5" -frames:v 1 out/contact.png

# strip around a fast action (starting at 4.1 s)
ffmpeg -ss 4.1 -i out/final.mp4 -vf "scale=320:-1,tile=12x1" -frames:v 1 out/strip.png

# phone test: does it read at 360 px wide?
ffmpeg -i out/final.mp4 -vf "fps=1,scale=360:-1,tile=5x3" -frames:v 1 out/phone.png

# loop check: play it 3 times and watch the seam
ffmpeg -stream_loop 2 -i out/anim.mp4 -c copy out/loop_check.mp4
Enter fullscreen mode Exit fullscreen mode

Freeze any frame by URL (from Florence). Put this before </body> and anim.html?t=2.5 shows exactly that frame, frozen. Opus can screenshot it without rendering anything else, which makes every round of the loop cheap:

<script>
  // anim.html?t=2.5 -> render that exact frame and freeze everything
  const q = new URLSearchParams(location.search).get('t');
  if (q !== null) addEventListener('load', async () => {
    await document.fonts.ready;
    const t = Number(q);
    if (window.renderAt) window.renderAt(t);
    document.getAnimations().forEach((a) => { a.pause(); a.currentTime = t * 1000; });
  });
</script>
Enter fullscreen mode Exit fullscreen mode

Loops that actually loop. "Render 0 to N-1" fixes the double frame, but the motion still has to come back to where it started. Drive everything from one phase, and only use whole cycles:

const N = 180;                        // loop length in frames
const phase = (frame % N) / N;        // 0..1, wraps exactly
const angle = phase * Math.PI * 2;    // whole turns only, never 1.5
// organic drift that comes back: walk seeded noise around a circle
const drift = noise2D(Math.cos(angle) * 0.8, Math.sin(angle) * 0.8);
Enter fullscreen mode Exit fullscreen mode

A rotation of 2 full turns loops, 1.5 turns jumps. For organic wobble, sampling noise around a circle means it ends exactly where it began. And make frame 0 a finished composition, because it's what people see while a GIF loads.

One more trick while iterating: ask for a playhead and frame labels on screen, pinned to frames. A move that runs late shows up as a label that lights before its move. Remove them for the final.

And every fix that works goes into the studio's CLAUDE.md as a rule, so the next film starts with it.

11. Sound: scoring a film when the model can't hear

Claude can't hear. So every "this sound fits" needs a number behind it, and that's what earshot by Leon Lin is for. It's an open source tool ("ears for coding agents") that ranks and describes sounds, measures loudness, generates sound effects in code and critiques a full mix against professional launch films. That's where my 19/21 came from. It only judges the audio, so it tells you nothing about the picture.

The order my agent sources sound in:

  1. Openverse (CC0, public domain or CC BY only)
  2. Real recorded SFX through earshot's real-sound skill (Freesound, Mixkit)
  3. earshot generates it in code (it ships a bank of 252 effects and a launch preset)

Rules that held up:

  • The picture has to work muted first. Most feeds autoplay without sound.
  • Each hit lands one frame AFTER its picture, never before. Light reaches you before sound in real life too.
  • Hits sit 3 to 6 dB over the music bed. Master to -14 LUFS.
  • Check the phone band (200 Hz to 6 kHz). Florence's first mix had 80% of its energy under 60 Hz, which a phone speaker just doesn't play.
  • Limit the WAV to about -2.2 dBTP so the AAC-encoded MP4 stays under -1 dBTP.
  • Use fewer sounds than you think. About 5 designed effects per 10 s. Cursor moves and hard cuts get nothing. One click voice per film. The reveal is the music's drop. The logo gets a short near-silence, then one soft note in key.
  • Blindfold test (seeco): listen with your eyes closed. If the audio has no shape on its own, it's not finished.

Voice. One of the examples in Movez's article gave a mascot a voice with ElevenLabs. If you do that, the key lives in .env and the prompt only names the variable. Never paste a real key into a prompt, you'll screenshot that prompt for a post one day. HyperFrames also has built-in TTS if you don't want another account.

VOICE
- The ElevenLabs key is in .env as ELEVENLABS_API_KEY. Read it from there,
  never print it, never write it into a file. .env stays in .gitignore.
- Generate the voice-over first, then time every beat to the words.
Enter fullscreen mode Exit fullscreen mode

Licenses matter if you post this. Fine: CC0, public domain, CC BY 2.0 to 4.0, Mixkit free. Not fine: anything NC, ND or SA. And read the actual item page, the license field in an API is a lead, not proof. CC BY credit goes in the post.

Muxing it all, with loudness normalisation in the same pass:

ffmpeg -i out/anim.mp4 -i music.wav -i sfx.wav \
  -filter_complex "[1:a][2:a]amix=inputs=2:duration=first,loudnorm=I=-14:TP=-1:LRA=11[a]" \
  -map 0:v -map "[a]" -c:v copy -c:a aac -b:a 192k -shortest \
  -movflags +faststart out/final.mp4
Enter fullscreen mode Exit fullscreen mode

12. Long pieces: the director's brief

For anything longer than a short loop, the agent works for hours, and you don't want it polishing 900 wrong frames. So you put gates in, places where it stops and waits for your OK. This structure is from seeco and Movez:

GATES (stop and wait for my OK at each)
1  references + assets approved
2  style guide + shot list approved
3  stills per shot, critiqued
4  animatic at 960x540 with placeholder audio
5  full pass, polish, sound
6  formats, loop check, poster, README

DELIVER
final.mp4, loop_check.mp4, poster.png, contact.png, README
Enter fullscreen mode Exit fullscreen mode

The brief itself: the film in one line, references, tools and budget, an identity lock for any character or logo, a beat sheet with timestamps, text-on-screen rules, then the gates above and the critique loop.

If you split chapters across subagents, have it write an ANIMATION_GUIDE.md first so they all code in one style.

13. Ship: export for X

# X video: H.264 High, yuv420p, AAC, up to 60 fps
ffmpeg -framerate 30 -i out/frames/%04d.png -c:v libx264 -profile:v high \
  -pix_fmt yuv420p -crf 18 -movflags +faststart out/anim.mp4
Enter fullscreen mode Exit fullscreen mode

Sizes: 1920x1080 (16:9), 1080x1080 (1:1), 1080x1350 (4:5), 1080x1920 (9:16). Write scenes against a layout function and render every format from one timeline, then reframe per format, never crop. Vertical gets bigger type and fewer elements.

Without Premium, X caps video at 140 s.

GIFs. X allows 15 MB, up to 1280x1080, and caps a GIF at 350 frames (about 14 s at 25 fps). Gradients band in GIFs, so avoid them there. The palette command for a clean one:

ffmpeg -i out/anim.mp4 -vf "fps=25,scale=1200:-1:flags=lanczos,split[a][b];[a]palettegen=stats_mode=diff[p];[b][p]paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle" -loop 0 out/anim.gif
Enter fullscreen mode Exit fullscreen mode

The timing trap: GIF frame delays are in hundredths of a second. 25 fps (4/100 s) and 20 fps (5/100 s) are exact. 24 fps isn't, every frame gets 40 ms and the loop runs 4% fast. If you need 24, the delays have to go five frames at 40 ms, then one at 50 ms. Easier to export the GIF at 25.

14. Every other route: Blender, video models and the rest

The three articles I learned from barely touch Blender. It shows up once, as a caption in Florence's article: Shikhar's 3D browser world (1.75M views) was built "from Blender assets, generated images and Three.js", on Opus 5.5 medium, third iteration. No Blender how-to anywhere, and nothing on Manim, Motion Canvas, Lottie, Rive, After Effects scripting or Veo either. So I looked into everything else too.

My way (HTML and three.js, rendered frame by frame) is one route. Here's every route I found and when to pick it. Versions and prices are as of 2026-10-26, they change fast, so check before you commit.

You're making Use License / cost
One idea under 10 s, product film, UI motion, type, data, launch explainer HTML + Playwright + ffmpeg (my rig), or HyperFrames free; HyperFrames is Apache 2.0
A templated series, data-driven videos, a React team Remotion free up to 3 employees, then $25/seat/month (Creators) or $0.01/render with a $100/month minimum (Automators)
3D in the browser, abstract or product shots three.js MIT, free
Photoreal 3D, real light, physics, rigged characters Blender (Claude writes Python, Blender renders with no window) free, GPL; your renders are yours
Math, equations, diagrams Manim Community MIT, free
Vector explainers synced to a voice-over Motion Canvas (or its fork Revideo) MIT, free
Motion inside an app: icons, loaders Lottie (dotLottie) MIT runtimes
Interactive, state-driven app motion Rive editor free with a splash screen on exports, $9/seat to remove it; runtimes MIT
Motion inside a live website Motion or GSAP free, GSAP plugins included
You already live in After Effects AE + ExtendScript + aerender Adobe subscription
People, photoreal scenes, organic movement a video model (Veo 3.1 / Gemini Omni, Kling 4.0, Seedance 2.5, Runway Gen-4.5) paid credits, check the commercial terms
Hand-drawn, storybook style the claude-animation skill (node canvas) free

Blender

Blender does the stuff three.js fakes: real path-traced light (Cycles), physics sims (cloth, fluid, rigid body), rigged characters and geometry nodes. Current version is Blender 5.2 LTS, released 2026-07-14 and supported until July 2028.

There are two ways to get Claude driving it.

Way 1, scripts. This is the one I'd use, because it's the same mindset as the render contract. Claude writes a Python (bpy) file that builds the whole scene, Blender runs it with no window, then renders the frames. Re-runnable, so an edit is still a changed number and a re-render.

# build the scene from Claude's script and save it
blender -b -P build_scene.py

# render frames 1-240 with Cycles on the GPU, as PNGs
blender -b scene.blend -E CYCLES -o //out/frame_#### -F PNG -s 1 -e 240 -a -- --cycles-device OPTIX
Enter fullscreen mode Exit fullscreen mode

The gotcha, straight from the Blender manual: arguments run in the order you write them. -a (or -f 10 for one frame) goes LAST, after the output path and format, or your settings get ignored. Use CUDA instead of OPTIX on older NVIDIA cards, HIP on AMD, METAL on a Mac. blender -E help lists the engines.

Way 2, MCP (Claude drives a live Blender). There's an official one from Blender Lab, the Blender Foundation's experimental arm: add-on v1.0.3, needs Blender 5.1+. Its own page warns it "will execute LLM generated code in Blender without any guards" and recommends a VM, so take that seriously. The popular community one is ahujasid/blender-mcp (MIT, around 32k GitHub stars). It's a socket add-on that gives Claude viewport screenshots, runs Python, and pulls assets from Poly Haven, Sketchfab and 3D generators.

Engines. Cycles is the path tracer, accurate and slow, and it renders fine with no display. EEVEE is real-time and fast but needs a GPU context. Headless on Linux works through EGL and 5.2 added gpu.init() for GPU in background mode, but test it on your own machine first. Workbench is for quick previews.

The version trap, same lesson as three.js. Blender 5.0 renamed EEVEE's engine id from BLENDER_EEVEE_NEXT back to BLENDER_EEVEE, so older scripts (and older model knowledge) break on it. Put the Blender version in the prompt.

What it costs you. Cycles frames take seconds to minutes each, nowhere near the ~0.3 s my three.js frames took. A 4 GB laptop GPU like mine limits how big a scene can get. And Claude can't see the viewport in Way 1, so the loop from section 10 still applies: render stills, contact sheet, fix, then the full render.

Assets. Poly Haven is the safe default (CC0 models, textures and HDRIs). Sketchfab licenses vary per model, so read each one before it goes in a film you post.

Hybrids are the strongest move. Model and light it in Blender, export glTF (built in), and load it in three.js with GLTFLoader. That's literally how Shikhar's piece was made. Or render Blender plates as backgrounds and put the UI and text on top in HTML, so text never goes through Blender's blur at all (that's rule 1 from section 09 again).

Where it fits: if the shot needs real materials, physics or a character, Blender. If it's UI, type or a product screen, stay in HTML.

Video models

seeco says code loses on photoreal scenes, characters and organic motion. Here's what to use instead:

  • Google: Veo 3.1, and Gemini Omni Flash (announced at I/O on 2026-05-19, in Google Flow and the Gemini app, SynthID watermark, clips around 10 s)
  • Kling 4.0: global launch in October 2026, native 30-second clips with stereo audio
  • Seedance 2.5 (ByteDance, through Dreamina): what Donald used for the 2.1M-view Claude Pop video
  • Runway Gen-4.5, and Wan (Alibaba) if you want open weights

Don't go looking for Sora. OpenAI shut the Sora app on 2026-04-26 and the API on 2026-09-24.

What you give up compared to code: you can't seek, you re-roll instead of editing, text and UI come out wrong, and you can't hit a beat on an exact frame. Check each tool's commercial terms before you use it for a paid launch.

The pattern that gets you both, from Movez and seeco, is generate-then-trace. Seedance renders the base shots for characters and physics, then Opus redraws the whole film in JavaScript on top of them, so the viewer only ever sees the code layer. Or keep it simpler: generated footage as background plates, with code-drawn type over it.

The rest, quickly

Remotion. v4 (4.0.538 as of 2026-10-22). Videos are React components, it has official agent skills and mature cloud rendering on Lambda. License in the table above. Remember Movez's point from section 01: Opus won't use it unless you say so.

HyperFrames (by HeyGen). Plain HTML and GSAP, no build step. It renders by seeking each frame in headless Chrome and encoding with FFmpeg, locally or on AWS Lambda. Apache 2.0, "no per-render fees or commercial-use thresholds". Needs Node 22+ and FFmpeg. It's basically my rig as a framework, and it's the default in my agent's studio now.

Manim Community. Python, the 3Blue1Brown-style engine, v0.22.0 (2026-10-08). Best for anything with equations or graphs.

manim -pqh scene.py SceneName   # render the scene in high quality and preview it
Enter fullscreen mode Exit fullscreen mode

Motion Canvas. TypeScript generators with a live editor, made for explainers synced to a voice-over. The catch: the last tagged release (v3.17.2) was December 2024 and development has slowed. Revideo is an MIT fork that adds rendering from code or an API.

Lottie. Vector animation stored as JSON, usually exported from After Effects with Bodymovin, or written by code. Use it for motion inside apps. dotLottie is the maintained runtime (lottie-web hasn't moved since 2025-09).

Rive. Interactive animations with state machines, for apps and games. Pricing's in the table.

After Effects. If you already use it, Claude can write ExtendScript (.jsx) and render with aerender, AE's command-line renderer. There are community AE MCP servers but none are official and I haven't vetted any, so I'm not naming one.

claude-animation skill (from Movez's setup). Hand-drawn, storybook-style films drawn in node canvas, with synthesised sound.

The rule across all of it: pick the route by the shot, mix routes when one film needs two, and keep text in HTML wherever you can.

The hype check

All three source articles say it in different ways, and it matches what happened to me: "one prompt" is mostly a myth.

seeco's numbers are the clearest. The best "one-prompt" films started from 9,500 to 19,000 character briefs and ran for hours. One watercolor short took 163 model calls and about 7 hours. Florence's own pieces took 4 to 15 follow-ups and around 2 hours each, plus 45 to 90 minutes for sound. The biggest piece in Florence's article, Vox's black-hole lab, took 5 h 28 min end to end, about $90 at API prices. /fast is up to 2.5x faster but double the price ($8/$40 instead of $4/$20 per million tokens, Florence's numbers).

On effort settings the sources disagree. Florence keeps Opus 5.5 on medium for everything and only raises it when a from-scratch piece misses twice. Movez and seeco go xhigh for new films and max when the first 3 seconds carry a launch. What I do: medium for fixes, xhigh for a new piece.

The sources also sell things (a Substack, a "sell this as a service" angle). The techniques hold up. The business stuff is anecdotes, skip it.

The whole thing in one table

Step What you do The one rule
Pixels Opus writes a program, Playwright renders, ffmpeg encodes every frame is a pure function of t
Rig renderer + QA scripts + a studio CLAUDE.md every fix becomes a rule in CLAUDE.md
Assets load the real site, real logo, real numbers never let it draw your product
Direction fill the template, GOAL first, then TIME swap every adjective for a number
Numbers easing, springs, durations from one tokens file position may bounce, opacity never
Bans start from the list, extend after every render ban defaults by name
3D GPU flags, local server, pinned version aim high, show style in stills first
Sharpness text after DOF, 2x+ textures, PNG, low grain check full-size frames, crop at 200%
Loop stills, score, fix the 3 worst, 2 to 4 rounds notes point at frames, not feelings
Sound earshot, measured, licensed picture works muted, hits 1 frame late
Ship H.264 yuv420p, one timeline per format, palette GIF at 25 fps reframe, never crop
Alternatives Blender for real 3D/physics, Manim for math, Remotion for templates, video models for organic shots pick by the shot, mix routes, keep text in HTML

If you try this, send me what you make, I wanna see it.

Top comments (0)