Claude Code 可用單幀渲染管線,把 8 秒動畫拆成 240 個可重播畫面。截至 2026 年 10 月 8 日,Anthropic、Remotion 與 FFmpeg 文件支持這套程式化製片方法。Claude Code 是能編輯專案並執行工具的 coding agent;影片仍由瀏覽器與編碼器產生。
片子交出去後,客戶要求標題晚一拍。若畫面依賴累加時間,改完就得猜整段會不會走樣。可尋址渲染讓程式直接回答一個問題:指定秒數應該長什麼樣子?
本文範例使用原創示意介面,不冒充真實產品。交付目標是 1280 × 720、30 fps、8 秒 MP4,另有 48 kHz 音效。這些是範例設定,並非模型效能數字。

本文原創範例共 240 幀。此圖解釋渲染流程,畫面縮圖為概念示意。
Claude Code 設定要明寫,畫質不能靠 effort 保證
Opus 5.5 是 Anthropic 模型,不是影片編碼器。依 2026 年 10 月 8 日核對的 Anthropic 模型設定文件,使用 Opus 5.5 需 Claude Code v2.1.280 以上。預設 effort 是 medium。
複雜分鏡可指定 xhigh,但提高推理投入不保證畫面更好。字級、時長與驗收條件仍要寫進專案。先確認 CLI 版本,再選擇帳號可用的模型。
claude --version
claude --model claude-opus-5-5 --effort xhigh
製片專案需要 Node.js、FFmpeg 和可截圖的瀏覽器。Playwright 是瀏覽器自動化函式庫;此範例以它輸出 Canvas 影格。FFmpeg 則負責編碼與音訊合成。下列指令在新的 motion-example/ 目錄執行。
mkdir motion-example
cd motion-example
npm init -y
npm install --save-dev playwright
npx playwright install chromium
ffmpeg -version
若用 macOS Homebrew,可先執行 brew install ffmpeg。其他環境使用 FFmpeg 官方下載頁提供的套件。
規格寫進檔案,畫面與音效共用拍點
動態設計需要可觀察的內容。品牌專案要提供真實 Logo、字體與產品畫面;參考影片則用來分析節奏與構圖。把觀察寫成 style-guide.md、鏡頭起止時間寫成 shotlist.md,再開始寫動畫。
給 Claude Code 的簡報可以如此具體:
製作 8 秒示意動畫,16:9、1280×720、30 fps。
視覺:深色背景、淺色標題、萊姆綠重點。
使用單一卡片,依拍點切換搜尋、排序、選取、就緒。
只使用本專案素材;示意介面必須標明,不捏造客戶數據。
先交 style-guide.md 和 shotlist.md,確認後才實作。
每一幀由 seek(t) 決定,不用時鐘或累加 delta。
交付原始碼、MP4、音效與重要拍點的接觸圖。
下面是 spec.js。瀏覽器與音效程式都讀取同一份設定,修改拍點只改這個檔案。
globalThis.motionSpec = Object.freeze({
duration: 8, fps: 30, width: 1280, height: 720, bpm: 96,
cues: [0, 1.875, 4.375, 6.875],
labels: ['搜尋', '排序', '選取', '就緒']
});
96 BPM 每拍長 0.625 秒。本文拍點分別是第 0、3、7、11 拍,皆由 60 / 96 推算。這是人為設定的節奏,沒有宣稱分析過任何商業歌曲。
| 狀態 | 設定時間 | 首個顯示影格 | 畫面時間 |
|---|---|---|---|
| 搜尋 | 0 秒 | 0 | 0 秒 |
| 排序 | 1.875 秒 | 57 | 1.900 秒 |
| 選取 | 4.375 秒 | 132 | 4.400 秒 |
| 就緒 | 6.875 秒 | 207 | 6.900 秒 |
影格編號從 0 開始。拍點落在兩幀之間時,畫面在下一幀切換。三次切換皆晚於聲音 25 ms,低於本例每幀約 33.3 ms 的間隔。要求逐幀對齊時,就把音訊拍點移到對應影格時間。

本例聲音拍點與下一個影格相差 25 ms。圖為概念示意,時間軸未按等距比例繪製。
seek(t) 直接重繪,彈簧不用逐幀累加
Canvas 是瀏覽器繪圖介面。以下 index.html 每次呼叫 seek(t),都清除畫布並重算整個畫面。文字、卡片位置與波形僅依指定時間改變。
Remotion 是 React 影片框架。其截至 2026 年 10 月 8 日的防閃爍指南指出,渲染不一定按影格順序進行。本文沿用這個約束:跳到任意時間,結果不應依賴前一幀。
<!doctype html>
<html lang="zh-TW">
<meta charset="utf-8">
<title>可尋址動態範例</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('定位・製作・重播', 76, 125);
ctx.fillStyle = '#9dabb9'; ctx.font = '24px monospace';
ctx.fillText(`影格 ${Math.floor(t * spec.fps + 1e-9)} / 時間 ${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>
settle() 使用臨界阻尼階躍響應。起點位置與速度固定,響應是 1 − (1 + r t)e^(−r t)。這個公式只描述臨界阻尼,不是所有阻尼比例的通用解。
卡片在狀態切換時重新進場,屬於刻意切鏡。若要連續變形,應把各次目標改變疊加在同一條解析曲線上。別靠每幀更新速度與位置,否則跳時間可能產生不同軌跡。
範例未使用隨機值。需要粒子或材質噪聲時,將種子寫進規格,並從種子與影格編號推導結果。字體、瀏覽器版本與外部素材也要固定;可重播只在已鎖定的環境內成立。
240 張 PNG 編成 MP4,失敗就讓指令報錯
影格輸出要顯式設定解析度、縮放比與時間。依 2026 年 10 月 8 日核對的 Playwright 截圖文件,可對單一元素截圖。以下 render.mjs 等字體載入,再擷取 Canvas。
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 });
}
執行 node render.mjs 後會產生 out/silent.mp4。程式讀取 FFmpeg 結束狀態,失敗時拋出錯誤;暫存影格在 finally 清理。避免把不存在的 MP4 當成成功交付。
於 2026 年 10 月 8 日核對的 FFmpeg 文件區分輸入 -framerate 與輸出 -r。本例對 PNG 序列指定 30 fps,輸出 240 幀。取樣時間是 i / 30,最後一幀為第 239 幀,約 7.967 秒。
240 幀除以 30 fps 等於 8 秒。最後一張取樣不用落在 8 秒整,否則會多出一幀。
音效從同一時間軸生成,重拍另行確認
音訊時間可比影格更細。以下 sfx.mjs 從 spec.js 讀取拍點,合成單聲道 PCM WAV。每個短音效有 3 ms 起音,降低突然跳變造成的爆音。
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
若加入音樂,先確認使用權,再取得拍點。librosa 是 Python 音訊分析函式庫;其節拍追蹤文件在 2026 年 10 月 8 日提供 units='time' 輸出秒數。可用獨立虛擬環境執行這段 beats.py。
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)
detected-beats.json 是節拍候選,不是分鏡指令。每四個節拍抽一次,還缺少小節起點與拍號確認,不能直接叫作重拍。聽出正確起點後,再把選定秒數填回 spec.js。
−14 LUFS 是可能的交付設定,並非通用規範。FFmpeg 響度濾鏡文件允許指定目標值。使用 loudnorm 時,保留量測與驗收結果。若轉場落在拍點之外,事後加音樂不會改動轉場時間。
驗收比對單幀,循環要另外設計接點
可重播測試要先畫一次 A,再畫 B,最後回到 A。只比對同一瀏覽器、字體與素材環境中的畫布像素。瀏覽器主控台可以執行:
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('同一時間必須產生相同像素');
if (a === b) throw new Error('不同時間應產生不同畫面');
整個 MP4 雜湊會受封裝或編碼設定影響,不能取代這個測試。本文範例在 2026 年 10 月 8 日通過 A→B→A 畫布比對;重新載入後,倒序重繪 240 幀也全數吻合原始影格。實際編碼檔確認為 240 幀、30 fps、8 秒。
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
抽出代表性影格做 contact sheet,也就是接觸圖。檢查手機尺寸下的文字、轉場前後畫面與邊界。給 agent 的修補要求應指明秒數與缺陷,限定本輪最嚴重的三處,不要重寫整個視覺方向。
循環動畫另有條件。本例是有頭尾的示範,沒有承諾無縫循環。t % duration 只會重設時間;接點仍須核對物件位置、速度、文字狀態與音訊尾音。

A→B→A 的示意畫面用來解釋驗收順序。本文另以實際 Canvas 輸出驗證像素,沒有把插畫當成測試截圖。
工作室的價值在可修改交付,規格能存成 Skill
橫式、直式與方形影片應共用時間軸,分別重排版面。把 16:9 直接裁成 9:16,容易截掉文字與產品操作。原始碼帶來的價值,是可重新配置版面與局部修改。
商業交付把修改輪數、原始碼、音樂授權及各比例輸出寫清楚。報價應列明各項交付與修改範圍。此範例沒有客戶收入或觀看成效證據,不提供收益承諾。
將簡報、風格、分鏡、渲染命令和驗收條件整理成 SKILL.md,下次就能重用流程。下一支影片可沿用規格與驗收條件,替換簡報與素材。
FAQ
Claude Code 如何把動畫拆成可重播影格?
Claude Code 可撰寫 seek(t),讓每個時間點獨立決定畫面。本文 8 秒範例在 30 fps 下輸出 240 幀,驗收採 A→B→A 像素比對。結果只適用於已固定的瀏覽器、字體與素材環境。
Claude Code 製片一定要用 Playwright 嗎?
Claude Code 製片不限定 Playwright。Remotion 使用 React 元件與影格時間;其官方文件同樣要求畫面能獨立渲染。已有 React 元件庫的團隊,可選擇 Remotion,保留相同驗收約束。
Claude Code 選 Opus 5.5 可以永久設定 max 嗎?
Opus 5.5 的 max 可用於目前工作階段。Anthropic 文件說明,透過 CLAUDE_CODE_EFFORT_LEVEL 環境變數設定時才另有持續套用方式。提高 effort 增加推理投入,沒有影片品質保證。
Claude Code 的影片專案可以用自有字體嗎?
Claude Code 專案可使用已有使用權的字體檔。Playwright 渲染應先等待 document.fonts.ready;字體載入不完整會改變排版。品牌字體授權條件需另向權利人確認,截圖文件不授予字體權利。
Sources
- Anthropic:Claude Code 模型與 effort 設定
- Microsoft Playwright:元素與頁面截圖
- Remotion:避免影格閃爍
- FFmpeg:影格率與序列編碼
- librosa:節拍追蹤與時間單位
- FFmpeg:響度正規化參數
Author Insight
只要客戶要求多比例原生排版,原始碼就應列入交付。保留時間軸與版面程式,才能重新排版;成品 MP4 做不到;是否使用 Opus 5.5 反而是可以替換的製作選項。
術語表
| 術語 | 本文用法 |
|---|---|
| 可尋址渲染 | 直接指定時間,無須先播放之前所有影格 |
| 確定性 | 相同輸入與固定環境得到相同畫面 |
| 臨界阻尼 | 不過衝的特定彈簧響應,本例採固定起始條件 |
| 拍點 | 供畫面和聲音共用的秒數位置 |
| Contact sheet | 用多張關鍵影格組成的接觸圖 |
