作者注:從提示詞、景深補償公式到 Playwright+ffmpeg 逐幀導出,完整拆解如何用 Claude Code 搭建一個“導出不掉幀”的 2.5D 視差動畫工作室。
很多人第一次用 AI 編程助手做動畫,瀏覽器裏預覽得很順,一導出視頻就出現卡頓、跳幀和時長對不上。問題往往出在架構上:一開始沒有把“導出”當成設計的一部分。本文圍繞 純函數 renderAt(t) + 逐幀 seek 截圖 這條主線,講清楚如何用 Claude Code 在一次會話裏搭出 2.5D 視差動畫工作室,並穩定導出 MP4。
這不是又一篇“讓 AI 幫你寫個動畫”的演示。本文會拆開景深補償公式的推導、一個很典型的偏移 bug,以及實時播放和逐幀導出的本質區別。
核心價值:讀完本文,你會拿到一套可複用的提示詞結構、一條可復現的導出管線,以及一份能遷移到其他程序化視頻項目的驗證清單。

Claude Code 2.5D 視差動畫工作室核心要點
參考資料中的實踐來自一篇 2026-11-09 發佈的文章。作者用 Claude Code 搭配 Opus 5.5,只用一次會話就做出一個單文件 HTML 工作室,外加一個 25 行左右的 Playwright 導出腳本。在作者的筆記本上,6 秒 720p 的視頻(180 幀)大約 20 秒導出完成。作者沒有給出硬件配置,所以這個耗時只能作爲量級參考。
| 要點 | 說明 | 價值 |
|---|---|---|
| 場景圖(Scene Graph) | 每個圖層有 z 深度、尺寸、位置和 HTML 內容 | 不用 3D 模型也能做出縱深感 |
| 時間軸(Timeline) | 相機與圖層的關鍵幀,每段配一條緩動曲線 | 鏡頭運動可編輯、可複用 |
| 確定性渲染器 | renderAt(t) 對任意時刻 t 輸出唯一畫面 |
導出結果與機器快慢無關 |
| 景深補償縮放 | 尺寸和偏移同時乘以 1 - z/perspective |
靜止時所見即所寫,運動時自帶視差 |
| 逐幀導出管線 | Playwright 按 i/fps seek 截圖,交給 ffmpeg 編碼 |
每臺機器輸出一致,不掉幀 |
2.5D 視差動畫的原理
2.5D 的做法是把扁平的 2D 素材放到 3D 空間的不同深度上,再讓一臺虛擬相機穿過它們。離相機近的圖層移動快,遠的移動慢,人眼會把這種速度差讀成縱深,這就是視差(parallax)。整個過程沒有建模、燈光,也沒有 3D 引擎,瀏覽器的 CSS perspective 和 preserve-3d 就夠用了。
“工作室”和“一次性動畫”的區別在於三個部件是否齊全:場景圖、時間軸和確定性渲染器。很多教程會跳過第三個,可它決定了導出的 MP4 會不會卡頓。所以在寫第一行提示詞時,就要把它定爲硬性要求。

Claude Code 2.5D 工作室的環境與模型配置
參考資料推薦用 Opus 5.5 完成首次構建。Anthropic 官方的模型概覽把它定位爲“長時間運行的智能體編程與知識工作”模型,1M token 上下文,最大輸出 128K。官方定價爲每百萬輸入 / 輸出 token 4 / 20 美元,實際價格請以官方頁面爲準。
建議在啓動時鎖定具體模型 ID,避免項目中途別名切換導致行爲變化:
claude --model claude-opus-5-5 --effort high
Opus 5.5 默認使用 medium effort。首次構建時,agent 要一次寫對投影數學、關鍵幀引擎和導出管線,所以建議調到 high。之後只是改關鍵幀數值或顏色這類小修改,可以回到 medium,節省 token。
| 階段 | 推薦 effort | 典型任務 | 理由 |
|---|---|---|---|
| 首次構建 | high | 投影數學、關鍵幀引擎、導出腳本 | 多個子系統要一次對齊 |
| 迭代微調 | medium | 調關鍵幀、改配色、加圖層 | 改動範圍小,邏輯已穩定 |
| “描述鏡頭”接口 | low(起步) | 返回固定 schema 的 JSON 關鍵幀 | 任務短、輸出結構固定 |
用其他模型做同等構建效果如何、成本差多少,參考資料沒有給出對比數據。如果要換模型,建議用同一份提示詞各跑一遍,再用後文的靜幀驗證法逐項對比。
用 Claude Code 構建 2.5D 工作室的提示詞結構
一個寫得具體的提示詞,效果好過五個含糊的提示詞。參考資料裏的構建規格是一整段提示詞,下面是保留原意的版本:
Build a 2.5D motion studio as one file, studio.html, with no dependencies.
- Scene: 6 layers (sky, moon, hills, city skyline, a title card "Ship it.",
foreground foliage), each at its own z depth, using CSS perspective and preserve-3d.
- Camera: keyframes for x, y, z (dolly) and yaw (orbit around the card).
- Per-layer tracks: opacity, scale, y. Easings: linear, easeInOutCubic,
easeInOutSine, easeOutBack.
- A timeline bar with keyframe markers, a playhead, click to scrub, space to play or pause.
- renderAt(t) must be pure in t. Expose window.__seek(t) that resolves after
the frame paints, and a ?capture mode that hides the UI.
Then write export.mjs: Playwright opens ?capture, seeks every frame at 30 fps,
screenshots it and pipes PNGs to ffmpeg as H.264.
這段提示詞可以拆成四層:場景、相機、軌道與緩動、交互 UI,最後是導出契約。真正起決定作用的是最後兩行。它們要求 renderAt(t) 必須是關於 t 的純函數,要求暴露一個“畫完才返回”的 __seek,還要求提供隱藏 UI 的 ?capture 模式。這樣導出能力就成了架構約束,agent 寫代碼時會照着它來設計。
如果你要遷移到自己的項目,可以把這份結構當模板:前面寫清“畫什麼”,最後用兩三行寫死“怎麼被確定性地取幀”。
Claude Code 生成的 2.5D 核心代碼解析
整個工作室大約 150 行,關鍵是下面三段。
關鍵幀採樣與緩動
每條軌道都是一個按時間排序的關鍵幀數組。sample() 先找到 t 前後的兩個關鍵幀,用前一個關鍵幀的緩動函數算出進度,再對所有數值字段做插值:
function sample(keys, t) {
if (t <= keys[0].t) return keys[0];
for (let i = 0; i < keys.length - 1; i++) {
const a = keys[i], b = keys[i + 1];
if (t <= b.t) {
const p = (easings[a.ease] || easings.linear)((t - a.t) / (b.t - a.t));
const out = {};
for (const k in b)
if (typeof b[k] === 'number' && k !== 't') out[k] = a[k] + (b[k] - a[k]) * p;
return out;
}
}
return keys[keys.length - 1];
}
這個函數不讀當前時間、不依賴上一幀狀態,也不用隨機數。同一個 t 永遠得到同一組數值,renderAt(t) 的確定性就是從這裏來的。
景深補償:2.5D 視差的核心公式
這裏的思路是相機不動,讓整個世界反向運動。每個圖層除了在 z 軸上平移,還要乘一個補償縮放:
const PERSPECTIVE = 1200; // 必須與 #viewport 的 perspective 一致
world.style.transform =
`translate3d(${-cam.x}px, ${-cam.y}px, ${cam.z}px) rotateY(${-cam.yaw}deg)`;
const comp = 1 - L.z / PERSPECTIVE;
el.style.transform = `translate(-50%,-50%) ` +
`translate3d(${x * comp}px, ${y * comp}px, ${L.z}px) scale(${comp * s})`;
推導過程如下。CSS 透視距離爲 p 時,深度爲 z 的點(z 爲負表示遠離觀察者)投影到屏幕上,會被縮放 p / (p - z) 倍。取 p = 1200、z = -900,得到 1200 / 2100 ≈ 0.571,也就是圖層只剩原始尺寸的約 57%。
要抵消這個縮小,就乘以它的倒數 (p - z) / p = 1 - z/p = 1 + 900/1200 = 1.75。代碼裏的 comp 就是這個值。由於 0.571 × 1.75 = 1,相機靜止時,每個圖層都會以你設定的尺寸和位置出現。
視差是怎麼出現的?相機移動時,整個 world 平移了同樣的 dx,但每個圖層在屏幕上的位移仍然要乘以各自的 p/(p - z)。遠處圖層的係數小,所以移得少;近處圖層係數大,所以移得多。補償只修正了靜止狀態的構圖,不影響運動時的速度差,2.5D 的效果靠的就是這一點。
| 圖層 z | 投影縮放 p/(p−z) | 補償係數 comp | 相機平移時相對速度 |
|---|---|---|---|
| 0(標題卡平面) | 1.000 | 1.000 | 基準 |
| -300 | 0.800 | 1.250 | 較慢 |
| -900 | 0.571 | 1.750 | 明顯更慢 |
| -1800 | 0.400 | 2.500 | 接近靜止背景 |
(表中數值按 p = 1200 計算,-300 與 -1800 兩行爲示例推導。)
一個值得記住的 bug:只補償尺寸,沒補償偏移
第一版代碼只給圖層乘了 scale(comp),x/y 偏移沒有乘 comp。結果遠處的山丘偏移量被透視壓縮了,構圖整體錯位,山丘一路升高,蓋住了大約三分之二的天空。修復方法很簡單:x 和 y 也乘以 comp,因爲透視對位置和尺寸的縮放是同一個係數。
更值得注意的是這個 bug 是怎麼發現的。作者看了一幀渲染結果就發現了問題,讀代碼時並沒有看出來。所以在信任任何鏡頭運動之前,先讓 agent 渲染起點、中點、終點三張靜幀。

2.5D 視差動畫逐幀確定性導出 MP4
實時播放與逐幀導出的本質差異
實時預覽用 requestAnimationFrame,機器一忙就會丟幀,這在預覽階段可以接受。導出不能丟幀。如果用錄屏,錄到的是“真實時間裏屏幕剛好顯示的東西”,慢一幀就會變成重複幀或缺幀。
| 維度 | 實時播放(rAF / 錄屏) | 逐幀 seek 導出 |
|---|---|---|
| 時間來源 | 系統時鐘 | 幀序號 i / fps |
| 機器繁忙時 | 掉幀、重複幀 | 只是變慢,畫面不變 |
| 跨機器一致性 | 不一致 | 每臺機器輸出相同 |
| 幀數 | 不可控 | 嚴格等於 duration × fps |
| 適用場景 | 編輯預覽、拖動時間軸 | 最終成片、CI 自動出片 |
極簡導出腳本
導出器的時間由幀序號驅動。每幀先 seek,等畫面繪製完成再截圖,然後把 PNG 寫進 ffmpeg 的標準輸入:
import { chromium } from 'playwright';
import { spawn } from 'node:child_process';
import { pathToFileURL } from 'node:url';
const out = process.argv[2] || 'out.mp4';
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
await page.goto(pathToFileURL('studio.html').href + '?capture');
const { duration, fps } = await page.evaluate(() => window.__scene);
const ffmpeg = spawn('ffmpeg', ['-y', '-f', 'image2pipe', '-framerate', String(fps),
'-i', '-', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-crf', '18', out],
{ stdio: ['pipe', 'ignore', 'inherit'] });
const frames = Math.round(duration * fps);
for (let i = 0; i < frames; i++) {
await page.evaluate(t => window.__seek(t), i / fps);
ffmpeg.stdin.write(await page.screenshot({ type: 'png' }));
}
ffmpeg.stdin.end();
await new Promise(r => ffmpeg.on('close', r));
await browser.close();
有幾個細節需要注意。window.__seek(t) 要等兩個動畫幀之後才 resolve,這樣截圖拿到的一定是新的 transform。image2pipe 讓 ffmpeg 從 stdin 讀取 PNG 序列。yuv420p 保證生成的文件能在瀏覽器中正常播放。如果 Playwright 還沒裝瀏覽器,先運行一次 npx playwright install chromium。
查看 __seek 與靜幀驗證的參考實現(示意代碼)
// studio.html 內:確定性 seek,畫完兩幀後再返回
window.__seek = (t) => new Promise(resolve => {
renderAt(t); // 純函數:只依賴 t
requestAnimationFrame(() =>
requestAnimationFrame(resolve)); // 確保樣式已提交併繪製
});
// stills.mjs:渲染起點 / 中點 / 終點三張靜幀,先看圖再信任鏡頭
import { chromium } from 'playwright';
import { pathToFileURL } from 'node:url';
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
await page.goto(pathToFileURL('studio.html').href + '?capture');
const { duration } = await page.evaluate(() => window.__scene);
for (const [name, t] of [['start', 0], ['mid', duration / 2], ['end', duration - 1e-3]]) {
await page.evaluate(t => window.__seek(t), t);
await page.screenshot({ path: `still-${name}.png` });
}
await browser.close();
以上兩段是按參考資料思路寫的示意實現,具體以 agent 在你項目裏生成的代碼爲準。
作者的實測結果:180 幀、1280×720、30 fps、時長 6.0 秒,輸出 MP4 約 420 KB,總耗時約 20 秒。畫面中城市塔樓的位移小於深色前景山丘,前景山丘最終完全移出畫面,這正是視差效果。

爲 2.5D 工作室加一個“描述鏡頭”功能
接下來可以讓編劇輸入一句鏡頭描述,由模型返回關鍵幀 JSON。參考資料中的做法是直接調用 Anthropic Messages API:
curl https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2026-10-21" \
-H "content-type: application/json" \
-d '{
"model": "claude-opus-5-5",
"max_tokens": 4096,
"output_config": { "effort": "low" },
"system": "Return only JSON: {\"camera\": [{\"t\",\"x\",\"y\",\"z\",\"yaw\",\"ease\"}], \"tracks\": {}}. Times in seconds, 0 to 6.",
"messages": [{ "role": "user", "content": "Slow push-in, slight orbit right, title fades up in the first second." }]
}'
這裏有兩個容易踩的坑。第一,effort 要寫在 output_config.effort 裏。第二,Opus 5.5 始終使用自適應思考,傳 thinking: {"type": "disabled"} 會返回 400,而且思考 token 也計入 max_tokens。max_tokens 設得太低,JSON 會在數組中途被截斷,stop_reason 爲 "max_tokens"。
截斷的或夾雜多餘文字的回覆,會在渲染時讓 sample() 出錯,所以這個調用最好配一個契約測試。參考資料用 Apidog CLI 讓同一個 Claude Code 會話創建並運行測試,斷言狀態碼 200、stop_reason 不是 "max_tokens"、返回文本能解析爲 JSON,且 camera 數組的 t 從 0 遞增到 6。用其他 API 測試工具或自己寫的斷言腳本也能達到同樣目的。修改 system prompt 或 effort 後要重跑一遍,因爲兩者都會改變返回內容。
常見問題 FAQ
Q1:2.5D 視差動畫和真正的 3D 動畫有什麼區別?
2.5D 把扁平的 2D 素材放在不同深度上,再用移動的相機拍攝,靠速度差產生縱深感。它不需要 3D 模型、燈光和 3D 引擎,用瀏覽器 CSS 就能實現,代價是相機不能大角度繞到圖層背後。
Q2:爲什麼不直接錄屏,而要逐幀 seek 截圖?
錄屏跟隨真實時間,任何一幀慢了都會變成丟幀或重複幀。按 `i / fps` seek 後截圖,每一幀的內容只由幀序號決定,所以在任何機器上的輸出都一樣。
Q3:補償係數 1.75 是怎麼算出來的?
透視距離 1200、深度 -900 時,投影縮放爲 1200/2100 ≈ 0.571。補償取倒數 `1 – z/p = 1 + 900/1200 = 1.75`,兩者相乘等於 1,靜止時圖層就回到設計尺寸和位置。
Q4:首次構建爲什麼建議用 Opus 5.5 和 high effort?
首次構建要在一次會話裏同時寫好投影數學、關鍵幀引擎和導出管線。參考資料中,一次提示詞加一輪修復就得到了可用的工作室。後續的小修改用 medium 或更低就夠了。換成其他模型的效果目前沒有公開對比數據,需要自己測試。
Q5:沒有 API Key 能用這個工作室嗎?
可以。工作室本身和 MP4 導出都在本地運行,不需要 Key。只有“描述鏡頭”功能要調用 Messages API,需要你自己的 Anthropic API Key。用 Claude Code 構建時消耗的 token,則按你所用賬號的計費方式結算。
Q6:這套方法有哪些已知限制?
逐幀截圖 PNG 的速度受分辨率和頁面複雜度影響,參考資料裏 20 秒的耗時只是單檯筆記本的單次結果。CSS 3D 對大角度 yaw 和半透明疊層的渲染可能因瀏覽器版本而有差異。模型 ID、API 版本頭和 effort 參數都有時效性,使用前請覈對官方文檔。
Q7:如何把“確定性渲染 + 靜幀驗證”遷移到其他程序化視頻項目?
可以按下面的清單逐項檢查:
– 渲染函數只接收 t,不讀系統時鐘、不依賴上一幀狀態、不用未固定種子的隨機數
– 提供一個“繪製完成才返回”的 seek 接口
– 提供隱藏 UI 的 capture 模式和固定視口尺寸
– 導出幀數嚴格等於 `round(duration × fps)`
– 改動鏡頭後先渲染起點、中點、終點三張靜幀,確認無誤再導出全片
– 對模型生成的 JSON 關鍵幀加契約測試:可解析、時間單調遞增、未截斷
Claude Code 2.5D 視差動畫核心要點 Key Takeaways
- 導出是設計的一部分:在提示詞最後幾行寫死純函數 renderAt(t) 和 seek 契約
- 景深補償公式:comp = 1 − z/perspective,尺寸和 x/y 偏移都要乘
- 視差從哪裏來:補償只修正靜止構圖,運動時各層仍按深度呈現不同速度
- 先看靜幀再信鏡頭:起點、中點、終點三張圖能發現讀代碼看不出的 bug
- 按幀序號驅動時間:Playwright seek 加截圖,ffmpeg image2pipe 編碼成 H.264
- effort 分階段調整:首建用 high,微調用 medium,結構化 JSON 接口從 low 起步
- 模型輸出要加契約測試:防止 max_tokens 截斷導致關鍵幀解析失敗
總結
用 Claude Code 搭建 2.5D 視差動畫工作室,核心有三點:
- 架構:場景圖、時間軸、確定性渲染器三件套,第三件決定成片質量
- 數學:透視補償係數
1 - z/p同時作用於尺寸和偏移,靜止時構圖準確,運動時自帶視差 - 管線:按
i/fps逐幀 seek、截圖後交給 ffmpeg 編碼,輸出不受機器性能影響
建議先照着本文的提示詞結構跑通一次 6 秒樣片,養成“改鏡頭先看三張靜幀”的習慣。之後再加圖層、接入“描述鏡頭”接口,或者把同一套確定性思路遷移到 Remotion 等其他程序化視頻方案。
📚 參考資料
- How to Build a 2.5D Motion Studio With Opus 5.5:本文實踐來源
- 鏈接:
apidog.com/blog/build-2-5d-motion-studio-opus-5-5 -
說明: 包含完整提示詞、工作室代碼、導出腳本與“描述鏡頭”接口示例
-
Anthropic 模型與 effort 文檔:模型定位、上下文、定價與 effort 參數說明
- 鏈接:
docs.anthropic.com -
說明: 覈對 claude-opus-5-5 的模型 ID、API 版本頭和 output_config 用法
-
ffmpeg 格式文檔:image2pipe 等輸入格式說明
- 鏈接:
ffmpeg.org/ffmpeg-formats.html -
說明: 理解 stdin 讀取 PNG 序列與 H.264 編碼參數
-
Playwright 文檔:瀏覽器自動化與截圖 API
- 鏈接:
playwright.dev - 說明: page.evaluate、page.screenshot 與瀏覽器安裝命令
