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

**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](https://s4.tenten.co/learning/content/images/2026/10/linkedin-infographic-1-11.png)

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.

```bash
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.

```javascript
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](https://www.remotion.dev/docs/flickering), 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.

```html
<!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](https://playwright.dev/docs/screenshots), 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.

```javascript
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](https://ffmpeg.org/ffmpeg.html), 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.

```javascript
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);
```

```bash
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](https://librosa.org/doc/main/api/generated/librosa.beat.beat_track.html), checked October 8, 2026, supports time values in seconds. The optional `beats.py` requires `music.wav` and a separate Python environment.

```bash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install librosa numpy
```

```python
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](https://ffmpeg.org/ffmpeg-filters.html#loudnorm), 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:

```javascript
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.

```bash
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](https://s4.tenten.co/learning/content/images/2026/10/linkedin-infographic-2-11.png)

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](https://code.claude.com/docs/en/model-config), 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:

```bash
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:

```text
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

- [Model and effort configuration](https://code.claude.com/docs/en/model-config)
- [Element and page screenshots](https://playwright.dev/docs/screenshots)
- [Preventing frame flicker](https://www.remotion.dev/docs/flickering)
- [Frame rates and image sequence encoding](https://ffmpeg.org/ffmpeg.html)
- [Beat tracking and time units](https://librosa.org/doc/main/api/generated/librosa.beat.beat_track.html)

- [Loudness normalization parameters](https://ffmpeg.org/ffmpeg-filters.html#loudnorm)

#### 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 |

