程式碼渲染動畫
概念(Addy Osmani, LinkedIn 2026-09-24):一個編碼模型讀取原始素材(文章、規格書),並編寫出能繪製每一幀畫面及其音效的 JavaScript 程式碼。最終產出是一個程式,因此任何時刻都可以被編輯、重新計時、重新渲染或重新調整格式。Osmani 未公開其提示或工具。此技能的設計來自開源的 riso-windowseat 專案(MIT 授權,僅供參考,請勿複製其說明):https://github.com/sevenevesai/riso-windowseat 。同時也借鑒了 HeyGen HyperFrames (https://hyperframes.heygen.com/guides/pipeline) 和 Remotion 的設計理念。
基本規則:以至少 4 個採樣/秒的頻率逐幀審查,絕不宣稱通過未執行的檢查,並精準修復缺陷。
合約(一切仰賴於此)
每個作品都是一個獨立的 index.html,其中包含一個能讀取 URL 中 ?w=&h= 參數的 <canvas>,並必須公開以下 API:
window.__anim = {
duration, fps, width, height, canvas,
ready, // 在字型/素材載入完畢且 seek(0) 繪製完成前為 false
seek(t), // 同步繪製時間點 t 的準確幀
renderAudio(), // Promise<base64 WAV>,48 kHz 立體聲,長度正好為 duration
marks, // 可見事件(彈出、切換)發生的時間點,音效必須對齊這些時間點
shots, // [{id,start,end}] - 驗證時會檢查邊界
};
不變條件:
seek(t)是 t 的純函數。螢幕上的每個數值都由 t 計算得出:prog(t,a,b)、緩動函數、pop(t,at)。幀之間不得有狀態殘留,不得使用requestAnimationFrame計時,不得使用Date,不得進行物理模擬模擬除非從 0 重新計算或使用閉合形式解。- 渲染過程中絕不能呼叫
Math.random()。請改用rngFor('stable-key')(基於 mulberry32 的種子亂數),並在載入時使用固定的鍵來創建隨機佈局。更改鍵會改變輸出,因此在獲得批准後應凍結鍵。 - 一個單一的時間軸物件(
TL)包含所有場景範圍和事件時間點。畫面與配樂都讀取同一個時間軸物件。這就是同步的運作原理:音效在與畫面事件相同的數值時間點上被排程。 - 在
ready=true之前等待字型載入完成(document.fonts.load('700 40px "DejaVu Sans"'))。否則,最初的幾幀會使用後備字型渲染。使用fc-list : family(Linux/macOS)或C:\Windows\Fonts(Windows;Segoe UI、Bahnschrift、Consolas 和 Georgia 始終存在,DejaVu 則非如此)檢查已安裝的字型。如果設計需要特定字型,請將其嵌入為 base64 的 @font-face 並以同樣方式載入。 - 作品中不得有網路請求。任何素材都必須以 data URI 的形式嵌入。
可編輯元素(用於影片修復)
review/film-fix.html 允許審閱者直接在畫面上移動和縮放元素,並將修改結果寫回 index.html。若希望啟用此功能(範例作品已啟用,請從那裡複製),需在作品中添加:
const TUNE = /*TUNE*/{}/*END-TUNE*/;寫在同一行,以格式和元素 ID 作為鍵:{"16x9":{"title":{"dx":0,"dy":0,"s":1}}}。它會為每種格式分別設定鍵值,因為每種比例都有各自的佈局。dx 和 dy 以畫布像素為單位。該工具只會改寫兩個標記之間的文字。- 圍繞每個主要元素使用
E(id, [x,y,w,h], draw)。它會查找TUNE[fmt][id],然後保存狀態,根據 dx/dy 平移,圍繞盒子中心縮放 s,呼叫draw()並恢復狀態。它還會將{id, box}推入__anim.elements,其中 box 是轉換後的畫布像素座標(來自ctx.getTransform(),因此相機移動也會被考慮在內)。seek(t)在每幀開始時會清空此列表。 __anim.elements、__anim.format(16x9、9x16、4x5、1x1)和__anim.tune(getter/setter,用於操作即時的 TUNE 物件,供工具即時預覽使用)。- 規則:ID 在整部影片中必須唯一;沒有隨機性;當 TUNE 為空時,
E不套用任何轉換,因此畫面能保持位元組相同。可透過在添加E前後計算幀的雜湊值來證明,然後執行verify.mjs。
不包含 E 的影片仍可在 film-fix 中運作:其「微調」模式會框選一個區域,並將拖曳或縮放動作轉換為供 AI 使用的數值修正。
專案設定
需要 Node 18+、ffmpeg(libx264、aac、blackdetect、freezedetect、ebur128)和本機的 Chrome/Chromium。工具鏈會在 Windows、macOS 和 Linux 的常見安裝位置尋找 Chrome;可設定 CHROME 環境變數來覆蓋。qa.sh 需要 bash(Windows 上為 Git Bash),並會選取系統字型用於顯示時間碼;可設定 QA_FONT 環境變數來覆蓋。無需全域安裝任何軟體;puppeteer-core 是唯一的 npm 依賴。
SKILL_DIR=<此技能資料夾路徑>
mkdir -p anim && cp -r "$SKILL_DIR/harness" anim/harness
cd anim/harness && npm install && cd ..
mkdir -p works/<名稱>
cp "$SKILL_DIR/example/browser-10s/index.html" works/<名稱>/index.html # 從可用的範例開始
# 所有工具鏈指令都在 anim/ 目錄下執行;輸出會放在 out/ 目錄
工具鏈指令
| 指令 | 功能 | 成功訊號 |
|---|---|---|
node harness/verify.mjs works/<n>/index.html --format 16x9 | 以採樣點和場景邊界為基礎,進行前向、後向和冷啟動跳轉,然後比較像素雜湊值 | verify: OK,退出碼 0 |
node harness/shoot.mjs works/<n>/index.html --format 9x16 --times 1.5,3.4 或 --range 3:4:0.1 --out out/stills | 在精確時間點以原生 Canvas 格式輸出 PNG | 手動檢查 |
node harness/audio.mjs works/<n>/index.html --twice | 僅渲染配樂(秒為單位),檢查準確長度和執行結果的確定性 | 無 FAIL。Chrome Web Audio 每次執行可能相差 1 LSB;容差為 1 |
node harness/render.mjs works/<n>/index.html --format 16x9 --fps 30 --out out/<n>-16x9.mp4 | 逐幀呼叫 seek 並將 PNG 傳送給 ffmpeg(libx264 crf18、yuv420p、+faststart),然後將 renderAudio 的結果混入 AAC | 顯示幀數和時間。頁面錯誤時退出碼為 2。使用 --from/--to 或 --silent 可渲染不包含音訊的範圍 |
node harness/lint.mjs works/<n>/index.html --all-formats | 對頁面本身進行品質把關(16:9 + 9:16;若指定 --format 則只檢查一種):文字大小、安全區域、文字重疊、靜態拉伸、空白幀、場景列表、音效對齊事件時間點、螢幕上的文字數量 | lint: OK (0 FAIL),退出碼 0 |
node harness/review.mjs works/<n>/index.html --format 16x9 --out out/review-16x9,然後 --check out/review-16x9/REVIEW.md | 每個場景輸出一張聯絡表(第一幀、6 張均分幀和最後一幀,並燒錄時間碼)、lint JSON 以及包含每個場景固定問題的 REVIEW.md。--check 在遇到空白或 TODO 答案、評分低於 4、lint 失敗、場景章節被刪除或影片自聯絡表生成後已被修改時,判定為失敗 | review check: OK,退出碼 0 |
bash harness/qa.sh out/<n>-16x9.mp4 4 | ffprobe、blackdetect、freezedetect、EBU R128 音量、音訊/影片長度、以 N 採樣/秒的速度生成帶有燒錄時間碼的聯絡表 | 參見下方 QA 章節 |
格式:16x9 1920x1080、9x16 1080x1920、4x5 1080x1350、1x1 1080x1080。幀預設以 JPEG q95 的品質傳送給 ffmpeg,在 1080p 下約為 16 fps,因此一個 40 秒的作品每種格式約需 60-75 秒。--png 品質無損,但速度約慢 4 倍。
長時間渲染:AI 的 shell 呼叫可能會超時(通常為 120 秒),並終止其子程序。請增加呼叫的超時時間,或將完整渲染以分離方式執行並輪詢日誌(以下為 Linux/macOS 範例;Windows 上請使用背景 shell 或 Start-Process):
setsid nohup bash -c 'node harness/render.mjs works/<n>/index.html --format 16x9 --out out/<n>-16x9.mp4; echo RENDER_DONE' > out/render.log 2>&1 < /dev/null & disown
tr '\r' '\n' < out/render.log | tail -2
同時,應將撰寫作品的 heredoc 與執行作品的指令分開,以避免超時導致檔案遺失。
完成標準
確切的指令在 AGENTS.md(與其他編碼工具共享的單一來源)中:每種格式皆需驗證、audio --twice、lint --all-formats 且無 FAIL、執行 review.mjs 接著 review.mjs --check 通過、渲染、QA。絕不能在閘門檢查失敗時聲稱完成,也絕不能為了通過檢查而削弱閘門或編輯工具鏈的閾值。
工作流程
- 簡報。 包含原始素材的內容、目標受眾、長度、格式、是否包含旁白。從原始文章中提煉關鍵論點。每個場景都必須展示正在被講述的內容。
故事優先於正確性。 工具鏈只能確保影片技術上的正確,無法讓它變得好看。在製作故事板之前,請回答簡報中的四個問題:
- 角色是誰?他們在每個場景中的感受是什麼?
- 哪個日常物品可以代表每個抽象概念?(原始範例使用了帶有火漆封印的信件、沙盒和一個由房屋組成的小鎮。)
- 什麼樣的持久性 HUD 能將場景串聯在一起?通常是一個章節轉盤和一個輸入的標題。
- 最後一幀如何與第一幀形成迴圈? 一部不具備以上任何元素的影片可以通過所有閘門檢查,但仍然會被觀眾滑過。
- 先有 STORYBOARD.md 再有程式碼。 每個場景一行:時間範圍 | 螢幕內容 | 動作 | 音效提示。列出每個事件時間點(這些將構成
TL和marks)。每隔幾秒規劃一個切換或新的動作,避免靜態拉伸。規劃轉場為推入或重疊,絕不能過渡到空白幀。除非獲得批准,否則不得以淡出至黑色結束。 - 時間軸模組,然後是每個場景的純 t 函數
sceneN(t),最後是一個合成器,負責繪製背景、活躍的場景以及轉場效果。 - 先處理最難的幀。 構建內容最密集的時刻,在每種格式下
shoot並檢查、修復。然後再繼續開發其他部分。 - verify 在每種格式下執行,然後 audio --twice,最後 render 每種格式。
- QA(見下方),精準修復缺陷,僅重新渲染需要的部分,重新執行 QA。
- 交付 將 MP4 作為附件,並附上 QA 結果與已知的弱點。
包含旁白時
先處理 TTS,然後取得逐詞時間戳記(使用任何能返回逐詞時間的工具轉錄 WAV 檔案),再根據這些詞語設定 TL。場景、彈出效果和卡拉 OK 式字幕高亮都對齊詞語時間點。規則:使用方框顯示卡拉 OK 式字幕,只能有一個字幕軌,說話時對應的數字要顯示在螢幕上,並透過聽覺確認專有名詞。將旁白 WAV 嵌入為 data URI,並在 renderAudio() 內部進行混音(使用 ctx.decodeAudioData 解碼,並在時間點 0 處排程),或者使用 ffmpeg 進行混流,並在旁白下方降低配樂音量。確保整個影片中旁白的音量一致。
從單一時間軸產生多種格式(非裁剪)
- 使用相同的
TL、相同的場景,但採用不同的佈局。計算portrait = H > W、S = min(W,H)/1080(文字與形狀的縮放比例)、M = 0.06*min(W,H)(安全邊距)。根據長寬比進行佈局:16:9 中的行在 9:16 中變成列,樹狀圖的展開範圍變窄,依此類推。 - 文字:使用自動調整大小以符合容器(
measureText迴圈,針對W-2M),確保文字永遠不會被邊緣裁切。字型大小來自S。在直向模式中,使用更大的字型並填滿高度。 - 不要將「直向」視為單一情況。4:5 (1080x1350) 比 9:16 矮得多。如果將範例的 9:16 直列佈局直接套用到 4:5,會導致像素網格和標語被擠到畫面底部並相互重疊。應根據長寬比進行分支處理(
H/W:約 0.56、1.25、1.78),或根據可用高度進行佈局,並對你將交付的每種格式都執行shoot。 - 在檢查邊距時,記得考慮彈出效果的過衝(easeOutBack 約 1.1 倍)和「熱門」放大效果。使用
shoot驗證每種格式下內容最密集的畫面。
程序化音效(OfflineAudioContext)
new OfflineAudioContext(2, 48000*duration, 48000),建立音訊圖,await ctx.startRendering(),編碼為 16-bit PCM WAV,傳回 base64(參見範例中的wavB64)。- 來自範例的模式:每個場景有一個背景和弦(失諧的正弦波/三角波對,緩慢起音,在切換處重疊)、在每個
marks事件上有一個撥弦聲(三角波 + 2 個泛音,5 毫秒起音,指數衰減)、基於種子亂數的打字噪音爆破點擊聲、用於撞擊效果的音高下降重擊聲,以及主音軌上的 DynamicsCompressor。 - 噪音來自種子亂數產生器,絕不使用 Math.random。
- 音量:目標是集成式響度約 -14 LUFS,峰值低於 -1 dBFS,以符合社交媒體平台的規範。原文測量結果為 -14.3 LUFS;內附範例較為安靜,為 -17.2 LUFS,峰值 -1.4。可在頁面中透過主音量增益進行設定,使其保留在程式碼中。
- 如需位元精確的音訊,可在純 JS 中將樣本合成為 Float32Array(使用閉合形式的包絡函數對多個振盪器進行加總),而非使用 Web Audio 節點。
- Tone.js 的
Tone.Offline(https://tonejs.github.io/docs/14.9.17/functions/Offline.html) 是一個選項,適用於需要更豐富音樂 API 的情況。由於作品無法存取網路,因此必須內聯嵌入。
QA(審查是我們自己的責任,而非觀眾的)
自動化閘門檢查,全部基於最終的 MP4 檔案:
- 每種格式的 verify 都必須通過。頁面錯誤即視為失敗。
- ffprobe:檢查是否為預期尺寸,30/1 fps,nb_frames = 持續時間 x fps,音訊持續時間 = 影片持續時間。
- blackdetect:不得有任何黑色畫面(除非有經批准的戲劇性黑色節拍)。freezedetect(> = 2 秒靜止):不得有任何靜止畫面。
- ebur128:集成式響度和真實峰值必須在目標範圍內。不同段落之間需保持一致。
Lint(品質把關)
node harness/lint.mjs works/<n>/index.html --all-formats [--json out/lint.json] 每 0.1 秒(--step)對作品執行 seek,並從外部包裝 fillText/strokeText,因此它可以在任何遵循合約的作品上執行,無需修改。以下情況會被判定為 FAIL:
- text-size:靜止文字小於 min(W,H) 的 2.6%(在 1080p 下為 28 px);小於 3.3%(36 px)則為 WARN。被縮小至 0.5 倍以下的文字視為總覽圖,僅發出警告。
- safe-area:靜止文字距離邊緣小於 4%;4-6% 則為 WARN。
- text-overlap:兩個不同的可見字串在超過 0.2 秒的時間內重疊範圍超過較小方框的 15%。短暫交錯則為 WARN。
- static-stretch:超過 3 秒沒有任何可見變化(2 秒則為 WARN)。
- empty-frame:任何接近均勻的幀,包括第一幀和最後一幀。
- shots-coverage:shots 陣列中存在間隙或重疊,或某個場景持續時間少於 1 秒。
- audio-sync:少於 70% 的 marks 在 ±40 毫秒內有對應的聲音起始點(通過標準為 90%);峰值超過 -1 dBFS 則為 WARN。
- density:直向模式螢幕上超過 18 個詞,橫向模式超過 30 個詞(僅為 WARN)。
在你要交付的每種格式中,lint 都必須以 0 FAIL 通過,作品才算完成。lint 只會偵測繪製在作品自身 canvas 上的 fillText/strokeText 文字,因此形狀碰撞和繪製到離屏 canvas 的文字仍需透過視覺審查來確認。example/browser-10s 在三種格式中皆通過檢查;example/browsers-40s 在此閘門建立之前製作,因此無法通過(標籤尺寸為 17-28 px,其中一個標籤在 9:16 格式中被裁切,文字)
…
[為翻譯而截斷]