把動畫錄成對比 GIF
動畫的毛病和改進,靠一張截圖看不出來,靠嘴說使用者也無法判斷。要把它錄下來:同一段動畫,改之前、改之後(或者開關關、開)按同一條時間軸一幀一幀錄,並排放成 GIF,使用者一眼就能拍板。
這裡的核心只有一條:時間由我們推,不由真實時鐘走。 無頭瀏覽器畫得慢(軟體渲染時尤其慢),一秒可能只畫十幾幀;用真實時間錄,每次停的位置都不一樣,左右兩邊永遠對不上。所以錄的時候讓頁面裡的時間只在我們說「往前走 40 毫秒」時才走,然後截一張,再走,再截。這樣不管機器多慢,第 n 幀永遠是第 n × 40 毫秒,兩個變體逐幀對得上,同一個變體錄兩次也一模一樣(實測 75 幀全部逐位元組相同)。
1. 先想清楚:GIF 給人看,數給檢查用
GIF 適合驗收、對比、放進說明裡,不適合找毛病。實際碰過:慢慢舉手時膝蓋一直彎著 18° 撐了一秒,這是逐幀記角度查出來的,GIF 上看不出來。所以:
- 先逐幀記關鍵數值(角度、位置、高度、透明度、腳有沒有滑),開關前後各一遍,看數對不對;
- 數對了,再錄 GIF 給人拍板。
記數值的方法見 references/hook.md(頁面裡寫一個錄製函式,錄畫面的同時把每幀的數一起交回來)。
2. 選時鐘
| 時鐘 | 什麼時候用 | 頁面要不要改 |
|---|---|---|
shim(預設) | 幾乎都用它:CSS 過渡、CSS 動畫、element.animate()、rAF 寫的 JS 動畫、canvas、three.js、PixiJS、Live2D | 不用改。clock-shim.js 在頁面腳本之前注入,接管 performance.now、Date.now、requestAnimationFrame、setTimeout/setInterval,Math.random 用固定種子;CSS / WAAPI 動畫每幀停住、撥到同一時刻 |
hook | 要一邊錄一邊從模型裡讀數(逐幀數值),或者應用自己有一個能按固定步長推的迴圈、想繞開瀏覽器截圖 | 要在頁面上寫 window.__capture(...),見 references/hook.md |
不要用 Chrome 自帶的虛擬時間(Emulation.setVirtualTimePolicy):試過,rAF 拿到的時間和 performance.now 對不上(計數器出現負數),動畫也不按給的步長走。
假時鐘管不到的:Web Worker 裡的動畫、OffscreenCanvas 在 worker 裡畫的、<video>、new Date()(只接管了 Date.now)、crypto.getRandomValues。碰到了改用 hook,或者在測試頁裡把它們換掉。
3. 步驟
- 準備頁面和變體。 變體最好用 URL 參數切(
?v=off、?v=on),頁面讀參數決定開關;或者用--setup在開始錄前執行一段 JS 切換。只能在本機用的東西(別人做的模型、測試頁)放在不進 git 的地方,用完刪掉。 - 起頁面。 本地檔案直接用
file:///…;要打包的(Vite 等)起一個開發伺服器,埠挑空閒的(同一臺機器上別的對話可能佔著測試埠,別動人家的視窗)。Vite 第一次開啟新頁面會最佳化依賴、然後自己重新整理一次,錄到一半頁面被刷掉會報「window.xxx 是 undefined」:先用瀏覽器開啟一次預熱,或者報錯後重跑一次。 - 錄。
node <skill>/scripts/record.mjs --out frames --fps 25 --seconds 3 --clip "#stage" \ --ready "window.model != null" --setup "window.model.motion('TapBody', 0)" \ --variant "關=http://127.0.0.1:5173/lab.html?v=off" --variant "開=http://127.0.0.1:5173/lab.html"--clip只截那一塊(CSS 選擇器);不寫就截整個視窗(--width、--height定大小)。--ready:模型、貼圖是非同步讀的,寫一個「讀好了」的表達式。等的時候假時鐘不走:走了的話每次讀得快慢不同,開始錄的時刻和隨機數用到哪都不一樣,兩次錄的對不上(實測過)。讀的過程要靠setTimeout的頁面才加--ready-advance 1。--wait:讀好以後先走多少假時間(毫秒),讓入場動畫、彈簧停穩;--setup在它之後執行,執行完馬上開始錄。--setup裡可以await,回傳值會被丟掉(three.js 的物件太大,CDP 傳不回來)。- 腳本自己起無頭 Chrome(單獨的配置目錄),錄完自己關,別留著。
- 預設用顯示卡渲染(
--render gpu),開頭會打出「WebGL 渲染:…」。如果打出來的是 SwiftShader,說明沒用上顯示卡,CPU 會被吃滿好幾個核,錄 three.js / VRM 時使用者會看到 Chrome 一直佔 CPU。沒有顯示卡的機器,或者要和以前用軟體渲染錄的逐幀比較時,加--render cpu。 - 顯示卡和軟體渲染畫出來的邊緣不一樣,逐幀比較只在同一種渲染裡做。
- 先看對照圖,再出 GIF。
python <skill>/scripts/make_gif.py frames --variants 關,開 --fps 25 --width 300 --title "Hiyori · 摸頭" --sheet sheet.png --out compare.gif--sheet把幾幀(預設均勻取 4 幀,--pick 0,10,20指定)排成一張 PNG:自己先讀這張圖,確認動起來了、左右對得上、取景沒裁掉東西,再把 GIF 給使用者。GIF 裡所有幀共用一個調色板,不會一幀一幀閃色;標籤寫在畫面上方的白條裡,不壓在內容上。 - 交給使用者:GIF 用能發檔案的工具發過去(一次發幾張有代表性的,其餘說放在哪個資料夾);說清楚每張左邊、右邊是什麼,錄的是哪個版本,哪些沒驗(比如「用的是軟體渲染,邊緣比顯示卡渲染略粗」)。
4. 參數怎麼定
- 25 幀每秒、每張 2–4 秒;單格 300 像素寬。兩格並排的一張大約 0.6–3 MB(角色、漸層多的大一些)。
- 動作長度按動作本身來:
hook裡可以用片段長度加一點尾巴(停下以後的餘勢也要錄到)。 - 取景按包圍盒:三維角色用
Box3.setFromObject(scene)算到頭頂,只按頭骨算會把大頭盔、呆毛裁掉(實測過);腳下也留一點。 - 每個變體除了要比的那一處,其他都一樣:同一個模型、同一個相機、同一個動作、同樣的等待。隨機的部分假時鐘已經用固定種子,但頁面在不同變體裡如果多調了幾次
Math.random,後面的序列會錯開,要心裡有數。
5. 各種頁面的做法
- 網頁(CSS、WAAPI、rAF):頁面不用改。要錄互動(點開選單、懸停),在
--setup裡觸發(加 class、dispatchEvent)。實測:rAF 計數器、50 毫秒後才觸發的 CSS 過渡、CSS 關鍵幀動畫、element.animate()在假時鐘下都準,兩個緩動曲線的變體逐幀對得上。 - PixiJS / Live2D:頁面不用改,Pixi 的 ticker 用的是 rAF 和
performance.now,跟著假時鐘走;物理也按這個時間算。Live2D 的 Cubism 執行庫(live2dcubismcore.min.js)要用普通<script>放在模組前面先載入,引擎在被 import 時就檢查它,晚了直接報「Could not find Cubism runtime」。實測 Hiyori 的物理開、關兩個變體,兩次錄 75 幀逐位元組相同。 - three.js / VRM:頁面不用改,自己的 rAF 迴圈跟著假時鐘走(實測 VRM 桌寵類的頁面直接能錄,開關前後的差別都在)。要逐幀讀骨頭角度時改用
hook,見references/hook.md。 - 遊戲、別的 canvas 應用:同上。如果應用用
setTimeout驅動遊戲迴圈,也在假時鐘管轄裡。
6. 許可和隱私
- 別人做的模型、素材錄出來的 GIF,要看它的許可:禁止再分發的(如 VN3 的一些條款)只能自己看、給人看效果,不要放進公開倉庫或公開發布;允許使用但要署名的,公開時寫上作者。
- GIF 和幀放在不進 git 的地方(專案裡專門的本機資料夾,或臨時目錄),用完把測試頁刪掉。
7. 和別的 skill 的關係
- 靜態介面截圖、多視口、深色模式:
frontend-verification(它的shot.mjs是真實時間截圖;要錄動畫用這裡)。 - 做給觀眾看的宣傳片、帶配樂的 MP4:
motion-showreel。 - 角色動畫本身怎麼做得自然(帶動層、物理、動作片段):
live2d-vrm-animation。
8. 來源與維護
- 來源:Cirra(桌寵,RedCarpG/cirra-deskpet)2026-11-10 做 3D 人物的帶動層、胯和腿、頭髮甩動時,為了給使用者看開關前後的對比而摸索出來的;當時的做法在那個倉庫的
docs/vrm-motion.md§11–§12、docs/vrm-spring-force.md§8(「怎麼驗」)。經驗記在私人 repo 的lessons/animation-capture.md(三維角色那部分也在lessons/threejs-vrm-animation.md)。 - 源頭:RedCarpG/redcarpg-skills 的
skills/animation-capture/。~/.claude/skills/裡的是安裝出來的副本,不要直接改。 - 怎麼改:
- 在 redcarpg-skills 裡改,走 PR;
manifest.json版本加一,node scripts/validate.mjs通過後發版(taganimation-capture-v<版本>),再重裝本機副本。 - 改了腳本,在一個 CSS 頁面、一個 Live2D 或 three.js 頁面上各錄一次,確認兩個變體逐幀對得上;同一個變體錄兩次逐幀相同(用 Pillow 的
ImageChops.difference比)。 - 改了做法或坑,同步改 lessons 檔案;測試用例在
evals/。 - 下面加一行更新記錄。
- 在 redcarpg-skills 裡改,走 PR;
- 更新記錄:
- 2026-11-12:1.0.0,搬進 redcarpg-skills 公開發布。改掉本機絕對路徑,內容不變。
- 2026-11-10,Cirra:初版。
record.mjs(shim、hook 兩種時鐘)、clock-shim.js、make_gif.py。在網頁(CSS 過渡、CSS 動畫、WAAPI、rAF)、Live2D(Hiyori,物理開關)、three.js VRM(帶動層開關)上實測過。 - 2026-11-10,Cirra:測試時查出,頁面載入時開始的 CSS / WAAPI 動畫在假時鐘接住前已按真實時間跑了幾毫秒,兩個變體差 16.6 ms;定時器觸發的過渡要等這一步走完才被接住(50 ms 變 80 ms)。改成:第一次看到的動畫從 0 算,每次跑完定時器、rAF 和每步開始時都看一遍。修後實測:50 ms 觸發的 600 ms 過渡在 80 ms 時正好 8.0 px,兩個變體第 0 幀相同。兩個測試用例(網頁緩動對比、彈球擠壓變形)有、無 skill 都全過:這類任務模型本來會做,skill 省的是每次重寫錄製腳本的工夫,以及上面這些已經踩過的坑。
- 2026-11-10,Cirra:錄頭髮甩動(離心力、被拖著)的對比 GIF,Yumeka、千駄ヶ谷篠;三維角色用轉盤(把人放進一個能轉的 Group)和假時鐘裡的
setInterval驅動,頁面裡不寫錄製程式碼。 - 2026-11-10:按新的寫法規矩,「維護」一節改成「來源與維護」。
- 2026-11-10,Cirra:使用者發現錄 3D 時 Chrome 一直佔很多 CPU。
record.mjs改成預設用顯示卡渲染(--render gpu;--render cpu還是 SwiftShader),開頭打出用的是哪個渲染器;Chrome 退出前刪設定目錄會 EPERM,現在刪不掉就算了,不報錯。實測:RTX 3060 Ti 上一幀著色器負載從 154 毫秒降到 8 毫秒;顯示卡渲染錄兩次,10 幀逐位元組相同;顯示卡和軟體渲染逐幀不同(邊緣),不能混著比。