Skip to main content

Command Palette

Search for a command to run...

Claude Code Turns an Eight-Second Video Into 240 Replayable Frames

Updated
•13 min read•View as Markdown
Claude Code Turns an Eight-Second Video Into 240 Replayable Frames
E

Crafting seamless user experiences with a passion for headless CMS, Vercel deployments, and Cloudflare optimization. I'm a Full Stack Developer with expertise in building modern web applications that are blazing fast, secure, and scalable. Let's connect and discuss how I can help you elevate your next project!

Claude Code can build a seekable rendering pipeline that turns an eight-second animation into 240 independently reproducible frames. As of October 8, 2026, documentation from Anthropic, Remotion, and FFmpeg supports the underlying workflow. Claude Code is a coding agent that edits projects and runs tools; a browser draws the frames, and an encoder produces the video.

A request to move a headline one beat later sounds small. It becomes expensive when the animation depends on whatever happened during playback. A renderer that accepts an explicit time makes the change inspectable at the affected frames.

This tutorial builds an original demonstration, with four states and synthesized sound. The design inputs are 1280 by 720 pixels, 30 fps, eight seconds, and 48 kHz audio. Those numbers describe the example, rather than model performance.

Concept diagram of the original eight-second, 30 fps, 240-frame rendering workflow

Original example: eight seconds at 30 fps yields 240 frames. The diagram illustrates the workflow, rather than displaying captured output.

One shared specification controls the image and the sound

Create a new motion-example/ directory with Node.js and FFmpeg available. Playwright is a browser automation library; the example uses its element screenshot support to export Canvas frames. Canvas is the browser drawing interface. FFmpeg encodes the image sequence and muxes audio.

mkdir motion-example
cd motion-example
npm init -y
npm install --save-dev playwright
npx playwright install chromium
ffmpeg -version

On macOS with Homebrew, brew install ffmpeg supplies the encoder. Other environments can use packages linked from the official FFmpeg download page.

Save this as spec.js. Both the browser and the sound generator read the same file, so moving a cue changes one source of timing truth.

globalThis.motionSpec = Object.freeze({
  duration: 8, fps: 30, width: 1280, height: 720, bpm: 96,
  cues: [0, 1.875, 4.375, 6.875],
  labels: ['SEARCH', 'SORT', 'SELECT', 'READY']
});

At 96 BPM, one beat lasts 0.625 seconds. The chosen transitions occur at beats 0, 3, 7, and 11. These are declared design inputs calculated with 60 / 96; no commercial track was analyzed.

State Cue time First displayed frame Display time
Search 0 s 0 0 s
Sort 1.875 s 57 1.900 s
Select 4.375 s 132 4.400 s
Ready 6.875 s 207 6.900 s

Frame numbering starts at zero. A transition between frames appears on the next available frame, while its sound can begin at the exact cue. Here, the last three changes appear 25 ms after their cues. That is below one 30 fps frame interval, approximately 33.3 ms. For exact frame alignment, move the sound cues to the display times.

A time query replaces the accumulated animation clock

A seekable renderer computes the complete image from the requested time. Remotion, a React video framework, documents the same requirement: frames may render out of order. Its flicker prevention guide, checked October 8, 2026, explains why a frame cannot depend on prior playback.

Save this as index.html. Each call clears the canvas and recalculates every element. The sample UI is illustrative and represents no customer product.

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Seekable motion example</title>
<style>body{margin:0;background:#101216}canvas{display:block}</style>
<canvas id="scene" width="1280" height="720"></canvas>
<script src="./spec.js"></script>
<script>
const canvas = document.querySelector('#scene');
const ctx = canvas.getContext('2d');
const spec = window.motionSpec;
const settle = (age, rate = 10) => age <= 0
  ? 0 : 1 - (1 + rate * age) * Math.exp(-rate * age);

window.seek = (seconds) => {
  if (!Number.isFinite(seconds)) throw new TypeError('finite time required');
  const t = Math.max(0, Math.min(seconds, spec.duration));
  const phase = spec.cues.reduce((p, cue, i) => t >= cue ? i : p, 0);
  const age = t - spec.cues[phase];
  const p = settle(age);
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = '#101216'; ctx.fillRect(0, 0, 1280, 720);
  ctx.fillStyle = '#f5f0e8'; ctx.font = 'bold 66px sans-serif';
  ctx.fillText('SEEK / BUILD / REPLAY', 76, 125);
  ctx.fillStyle = '#9dabb9'; ctx.font = '24px monospace';
  ctx.fillText(`FRAME ${Math.floor(t * spec.fps + 1e-9)} / TIME ${t.toFixed(3)}s`, 80, 178);
  const x = 80 + 42 * (1 - p);
  ctx.fillStyle = '#202833'; ctx.fillRect(x, 245, 1120, 285);
  ctx.fillStyle = '#c7ff6b'; ctx.font = 'bold 48px sans-serif';
  ctx.fillText(spec.labels[phase], x + 38, 315);
  for (let i = 0; i < 12; i++) {
    const wave = Math.sin(t * Math.PI * 2 / 2.5 + i * 0.55);
    ctx.fillStyle = i <= phase * 3 ? '#c7ff6b' : '#526274';
    ctx.fillRect(x + 38 + i * 84, 365, 56, 56 + 24 * wave);
  }
  ctx.fillStyle = '#526274'; ctx.fillRect(80, 620, 1120, 6);
  ctx.fillStyle = '#c7ff6b'; ctx.fillRect(80, 620, 1120 * t / spec.duration, 6);
};
window.seek(0);
</script>
</html>

The settle() function implements a critically damped step response with fixed starting position and velocity. Its response is 1 - (1 + r*t)*exp(-r*t). It is a specific solution, rather than a general formula for every damping ratio.

The card intentionally restarts its entrance at each state cut. A continuous morph needs a continuous target trajectory, or an analytical sum of target changes. Updating velocity with an accumulated delta makes arbitrary seeking harder to reproduce.

This example has no random inputs. For procedural textures, derive randomness from a stored seed and frame number. Lock fonts, assets, and the browser version too. Reproducibility here means matching images within that fixed environment; it is not a promise of identical pixels across operating systems.

Frame export fails loudly when encoding fails

Playwright supports screenshots of individual elements in its screenshot documentation, checked October 8, 2026. The render.mjs exporter waits for fonts, takes exactly 240 samples, and passes the image sequence to FFmpeg. Save the exporter in the project directory.

import { chromium } from 'playwright';
import { mkdtemp, mkdir, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { spawnSync } from 'node:child_process';

const frames = await mkdtemp(join(tmpdir(), 'motion-frames-'));
let browser;
try {
  await mkdir('out', { recursive: true });
  browser = await chromium.launch();
  const page = await browser.newPage({
    viewport: { width: 1280, height: 720 }, deviceScaleFactor: 1
  });
  await page.goto(pathToFileURL(resolve('index.html')).href);
  await page.evaluate(() => document.fonts.ready.then(() => true));
  const spec = await page.evaluate(() => window.motionSpec);
  const count = spec.duration * spec.fps;
  if (!Number.isInteger(count)) throw new Error('whole frame count required');
  for (let i = 0; i < count; i++) {
    await page.evaluate(t => window.seek(t), i / spec.fps);
    await page.locator('#scene').screenshot({
      path: join(frames, `frame-${String(i).padStart(4, '0')}.png`)
    });
  }
  const result = spawnSync('ffmpeg', [
    '-y', '-framerate', String(spec.fps), '-i', join(frames, 'frame-%04d.png'),
    '-frames:v', String(count), '-c:v', 'libx264', '-crf', '18',
    '-pix_fmt', 'yuv420p', '-movflags', '+faststart', 'out/silent.mp4'
  ], { stdio: 'inherit' });
  if (result.error) throw result.error;
  if (result.status !== 0) throw new Error(`ffmpeg exited ${result.status}`);
} finally {
  if (browser) await browser.close();
  await rm(frames, { recursive: true, force: true });
}

Run node render.mjs to create out/silent.mp4. The script checks FFmpeg's exit status and cleans only its own temporary frame directory. An encoding error propagates instead of leaving a success message beside a missing file.

The command-line documentation, checked October 8, 2026, distinguishes input -framerate from output -r. This example declares the input image rate explicitly. Frame 239 is sampled at approximately 7.967 seconds; 240 frames at 30 fps still produce an eight-second video.

Adding a sample at exactly eight seconds would create an extra frame. Keep the sample count and encoded duration separate in your reasoning.

Deterministic sound cues are enough for the first render

The sfx.mjs sound generator reads cue times from spec.js, matching the image timeline. The generator writes mono PCM WAV; save the code as sfx.mjs. A three-millisecond attack reduces a sudden amplitude jump at each onset.

import './spec.js';
import { writeFileSync, mkdirSync } from 'node:fs';
const { cues, duration } = globalThis.motionSpec;
const sampleRate = 48000, count = sampleRate * duration;
if (!Array.isArray(cues) || !cues.every(t =>
  Number.isFinite(t) && t >= 0 && t < duration)) throw new Error('invalid cues');
const samples = new Float64Array(count);
for (const cue of cues) {
  const start = Math.round(cue * sampleRate);
  for (let n = 0; n < sampleRate * 0.12 && start + n < count; n++) {
    const t = n / sampleRate;
    samples[start + n] += 0.2 * Math.sin(2 * Math.PI * 880 * t)
      * Math.exp(-45 * t) * Math.min(1, t / 0.003);
  }
}
const wav = Buffer.alloc(44 + count * 2);
wav.write('RIFF', 0); wav.writeUInt32LE(wav.length - 8, 4);
wav.write('WAVEfmt ', 8); wav.writeUInt32LE(16, 16);
wav.writeUInt16LE(1, 20); wav.writeUInt16LE(1, 22);
wav.writeUInt32LE(sampleRate, 24); wav.writeUInt32LE(sampleRate * 2, 28);
wav.writeUInt16LE(2, 32); wav.writeUInt16LE(16, 34);
wav.write('data', 36); wav.writeUInt32LE(count * 2, 40);
for (let i = 0; i < count; i++) {
  wav.writeInt16LE(Math.round(Math.max(-1, Math.min(1, samples[i])) * 32767), 44 + i * 2);
}
mkdirSync('out', { recursive: true });
writeFileSync('out/sfx.wav', wav);
node sfx.mjs
ffmpeg -y -i out/silent.mp4 -i out/sfx.wav -map 0:v:0 -map 1:a:0 -c:v copy -c:a aac -t 8 -movflags +faststart out/final.mp4

For a licensed music track, librosa can estimate beat positions. It is a Python audio analysis library; its beat tracking reference, checked October 8, 2026, supports time values in seconds. The optional beats.py requires music.wav and a separate Python environment.

python3 -m venv .venv
source .venv/bin/activate
python -m pip install librosa numpy
import json
import numpy as np
import librosa

y, sr = librosa.load('music.wav', sr=None, mono=True)
tempo, beat_times = librosa.beat.beat_track(y=y, sr=sr, units='time')
value = float(np.asarray(tempo).reshape(-1)[0])
with open('detected-beats.json', 'w') as f:
    json.dump({'bpm': value, 'beats_s': beat_times.tolist()}, f, indent=2)

Treat detected-beats.json as candidate timing data. Selecting every fourth detected beat does not establish the bar's first beat or confirm a time signature. Listen for the correct phase, then put selected cues back into spec.js.

Loudness targets also belong in the delivery specification. A setting of -14 LUFS is a possible brief, not a universal requirement for every platform. FFmpeg documents adjustable targets in its loudness normalization reference, checked October 8, 2026. Adding music after animation is finished does not move a transition that misses the intended beat.

Verify the image after a jump, then inspect the encoded file

A useful repeatability test draws A, jumps to B, and returns to A. Compare the canvas pixels in the same browser, font, and asset environment. Run this in the browser console:

const scene = document.querySelector('#scene');
seek(4.375); const a = scene.toDataURL();
seek(1.0);   const b = scene.toDataURL();
seek(4.375); const again = scene.toDataURL();
if (a !== again) throw new Error('the same time must reproduce the same pixels');
if (a === b) throw new Error('different times should produce different images');

An MP4 file hash tests the encoded container, which can change with encoding or metadata. It does not prove arbitrary-frame repeatability. This example passed the A-B-A canvas test on October 8, 2026. After navigation, all 240 frames were also redrawn in reverse order and matched their saved images. The encoded output was verified as 240 frames, 30 fps, and eight seconds.

ffprobe -v error -count_frames -show_entries stream=codec_name,width,height,r_frame_rate,nb_read_frames,sample_rate -show_entries format=duration -of json out/final.mp4

Export representative frames as a contact sheet, a grid of still images. Include frames immediately before and after state changes. Review text at phone size and look for unsafe margins. Ask the agent to fix the three most consequential defects with specific timestamps, rather than restarting the entire visual direction.

This demonstration has a beginning and an end. For a seamless loop, matching t % duration is insufficient: the seam must preserve position, velocity, text state, and audio decay. Test the seam as its own requirement.

A-B-A return test in a fixed environment, with a partial loop-seam reminder

The matching frames illustrate the A-B-A test. The loop reminder highlights position, velocity and audio; text state also needs checking, as described above.

The production brief outlasts the choice of model

Opus 5.5 is an Anthropic model, not the rendering engine. According to the model configuration reference, checked October 8, 2026, Opus 5.5 requires Claude Code v2.1.280 or later and defaults to medium effort. A complex shot specification can use an explicit setting:

claude --version
claude --model claude-opus-5-5 --effort xhigh

Higher reasoning effort is not a quality guarantee. Give the agent a concrete brief before it writes animation code:

Make an eight-second illustrative animation at 1280x720, 30 fps.
Use a dark background, light headline and lime accent.
Keep one card with search, sort, select and ready states.
Use only project assets; label illustrative UI and invent no customer metrics.
Deliver style-guide.md and shotlist.md for approval before implementation.
Compute each frame with seek(t), without a wall clock or accumulated delta.
Deliver source, MP4, audio and a contact sheet at the important cues.

For branded work, supply actual logos, licensed fonts, and product screenshots. Analyze references for timing and composition; record those observations in style-guide.md and shot timings in shotlist.md. A style reference supplies visual grammar, not ownership of someone else's assets.

Native landscape, portrait, and square exports should share a timeline but use separate layouts. Scope revision rounds, source delivery, music rights, and format variants in the production agreement. This example supplies no customer revenue or audience-growth evidence.

Package the brief, render commands, and acceptance criteria in SKILL.md. The reusable asset is the production specification and its evidence, even if a later project selects another coding model.

FAQ

How does Claude Code make video frames independently reproducible?

Claude Code can write a renderer that computes each frame from an explicit time. This example outputs 240 frames for eight seconds at 30 fps and checks pixel equality after an A-B-A jump. Fonts, assets, and the browser environment must remain fixed.

Does Claude Code video production require Playwright?

Claude Code video production can use other renderers. Remotion uses React components and frame time; its documentation also requires independent frame rendering. Teams with an existing React component library can keep that rendering contract with Remotion.

Can Claude Code persist max effort for Opus 5.5?

Opus 5.5 can use max for the current session. Anthropic documents a separate persistent route through the CLAUDE_CODE_EFFORT_LEVEL environment variable. More reasoning effort does not guarantee better video.

Can a Claude Code video project use custom fonts?

A Claude Code video project can load font files whose use is authorized. Wait for document.fonts.ready before capturing frames, because incomplete font loading changes layout. Screenshot documentation grants no rights to the font itself.

Sources

Author Insight

Source delivery should be contractual when a client needs native format variants. A shared timeline can support new layouts; an exported MP4 cannot recover that layout logic. The coding model is a replaceable production choice.

Terminology

Term Meaning in this example
Seekable rendering Drawing a requested time without playing every earlier frame
Determinism Matching images from the same inputs in a fixed environment
Critical damping A specific spring response with fixed starting conditions
Cue A time in seconds shared by picture and sound
Contact sheet A grid of representative still frames

More from this blog

T

Tenten - AI / ML Development

269 posts

🚀 Revolutionize your business with AI! 🤖 Trusted by tech giants since 2013, we're your go-to LLM experts. From startups to corporations, we bring ideas to life with custom AI solutions