Communitygithub.com

wiggle-that-diagram

將靜態的「運作原理」圖解轉換為三種風格的循環動畫,並帶有字幕,適合放入 HTML 簡報或報告中。

wiggle-that-diagram 是什麼?

這個技能讓你能將一張靜態的機制圖解(如檔案流程、規則檢查或審核流程)製作成短小、帶有字幕、會循環播放的動畫,並提供三種截然不同的風格供選擇:A) 乾淨的 3Blue1Brown 風格圖解影片(Huashu 引擎);B) 黑板風格的書寫影片(Huashu y3 引擎);C) 使用虛擬相機的即時 SVG 動畫(GSAP 驅動)。支援的平台包括 Claude Code、Cursor、Codex 以及其他相容的 AI 代理。這個技能的核心方法已在實際客戶專案中驗證,其工作流程從撰寫字幕腳本開始,然後分別為三種風格進行建置,最後將產出的檔案嵌入到 HTML 的 iframe srcdoc 中。它也內建了節奏控制機制,確保每個節拍有足夠的閱讀時間,並針對手機與瀏覽器相容性進行了測試,確保在所有主要瀏覽器中都能正常運作。

相容平台✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/wan-huiyan/wiggle-that-diagram/tree/HEAD/plugins/wiggle-that-diagram/skills/wiggle-that-diagram

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

wiggle-that-diagram:三種風格的解說動畫

將一張靜態的「運作原理」圖解,轉變為一段簡短、帶有字幕且會循環播放的動畫,並內嵌在 HTML 簡報或報告中,提供三種格式讓讀者選擇。此方法已在實際客戶的簡報中驗證(請參閱「實例」)。它使用 huashu-art-motion 作為分鏡方向指引(鏡頭規則、六種常見失敗模式),並在風格 A 與 B 中作為渲染引擎。

使用時機與不適合的情況

適合使用時機:

  • 簡報或 HTML 報告中包含機制圖解(如檔案流向、規則檢查、人員審批),且讀者應能 逐步看到 這些過程,並搭配可閱讀的文字說明;
  • 使用者希望獲得真正不同格式的選項,而不只是同一種動畫的三種配色;
  • 先前的動畫「太快而難以閱讀」或需要加上字幕。

不適合用於:單一的微互動(請使用 animate)、有旁白的影片(請直接使用 huashu-art-motion),或數值圖表(請使用 dataviz)。

三種風格(差異在格式,而非色調)

A · 圖解短片B · 黑板風格C · 即時 SVG
畫面Huashu 引擎場景,t1_3b1b 語法,渲染為無文字的 H.264 影片Huashu y3_whiteboard 語法,深色黑板,鋼筆繪製與手寫字體 (Kalam),方形影片由虛擬相機拍攝的內嵌 SVG 世界(viewBox 由 GSAP 3.12.5 驅動)
畫面上的文字根據導出的相機軌跡,將 HTML 標籤疊加在影片上(永遠清晰)直接繪製在影片中(必須夠大,以便在手機上閱讀)真實的 SVG 文字(清晰,可根據寬度調整相機視角進行重排)
每部影片大小(實例)360–820 KB(預算 900 KB)847–886 KB(預算 900 KB)21–34 KB(預算 40 KB)
風格感受乾淨的 3Blue1Brown 風格圖解有人在黑板前講解精美的產品動畫
主要風險場景變更若未重新渲染,標籤可能會偏移檔案大小預算緊繃;手機上標籤字級需為 12–14 px相機計算繁瑣;GSAP 變形可能有怪異行為

三種風格共享一份字幕腳本、相同的節拍與相同的嵌入契約,因此風格切換器能以三種方式呈現相同的故事。

完整工作流程

每個步驟結束時都有一個檢查點,用來確認該步驟已完成。

  1. 找出機制與一個具體範例。 閱讀主機頁面(僅限讀取)。選定要展示的時機,以及在每個節拍中貫穿的一個真實範例(一個資料列、一筆記錄、一個案例,並帶有實際數值)。列出不可發明的內容(ID、數字、圖像)。→ 檢查點:你能用「這個物件做了這個動作」的句型,搭配一個名詞和一個動詞,來說明每個節拍。

  2. 先撰寫字幕腳本。 每個節拍一句字幕,每部影片 3–6 個節拍,放在 beats.json 中(格式:scripts/common/beats.example.json)。字幕在所有風格中完全相同。然後運行節奏檢查工具,並閱讀其印出的底線:

    python3 ~/.claude/skills/wiggle-that-diagram/scripts/common/pace_check.py beats.json
    

    → 檢查點:每個節拍都通過,且使用者已同意由此產生的循環長度(或縮短字幕)。切勿獨立於字幕設定循環目標;請參閱「節奏控制」一節。

  3. 撰寫簡報。 將 references/brief-template.md 填入 SPEC.md:引用的回饋、代幣、節奏規則、字幕腳本、節拍、嵌入契約(含完整程式碼片段)、預算、交付物。→ 檢查點:一個沒有其他上下文的代理,可以僅根據此檔案進行建置。

  4. 方向性回合(可選,當使用者尚未看過三種風格時)。 三個代理平行作業,每個針對一種風格,為同一部影片的同一個時機建立原型(約 8–12 秒),並在 1280 與 390 px 寬度下產出靜態畫面。將這些靜態畫面並排展示;由使用者選擇其中一種,或保留全部三種。→ 檢查點:使用者的選擇已白紙黑字記錄下來。

  5. 建置回合。 每個風格一個代理(各自繼續其原型),所有該風格的影片僅寫入 <work>/<style>/final/。簡報文件:references/agent-briefs.md。在啟動前,告知使用者代理的數量與層級。→ 檢查點:每個代理都回傳檔案、靜態畫面、NOTES.md 與一份誠實的未解決問題清單。

  6. 建置食譜(每個風格的代理實際做了什麼;腳本位於 scripts/style-a|b|c/): 兩部中性影片會以全部三種風格發布,每部都包含 beats.json 加上 style-a/、style-b/、style-c/:examples/pizza/(5 個節拍,簡單)與 examples/expenses/(6 個節拍:卡片、顏色編碼的原因、指向第二與第三面板的箭頭)。從你的風格資料夾開始。

    • A. 執行 cp -R ~/.claude/skills/huashu-art-motion/scripts/engine <A>/engine 與 touch <A>/engine/favicon.ico。加入 engine/ways/way_make.js(來自 scripts/style-a/),執行 beats_js.py beats.json --engine engine --var <VAR>(字幕與時間軸使用共享腳本,絕不手動複製),建立場景檔案 engine/ways/<film>.js,該檔案呼叫 WAY.make({poster, beats, camBase, cam, labels}) 並註冊 SCENES['<film>'],以及 engine/eras_<film>.js,設定 FILM_SIZE=[1280,720]、PUNCH=0(否則引擎會在過渡時加入 0.03 的推近效果)、SCENE_LIBS=['ways/way_make.js','ways/<film>_beats.js','ways/<film>.js']、SCENE_DIR='ways'、ERAS=[{id:'<film>',dur:1}]。請勿在畫布上繪製任何文字。繪圖輔助工具(圓角方框、點、線、淡出、沿路徑移動)會針對每部影片分別撰寫;examples/pizza/style-a/pizza.js 提供了一組可直接複製的小型範例。接著執行 render_way.py <film> --scale 0.75 --crf 30 → build.py <film> --meta meta.json → pace_check.py work/<film>_data.json → test_embed.py final/<film>.html:<verdict beat>(或一次全部執行:make.sh)。每次修改場景後,都需重新渲染並在移動中點執行 midmove.py:標籤必須精準定位在其對應物件上。
    • B. 以同樣方式複製引擎(加上空的 favicon.ico:在真實 Chrome 中,其 404 錯誤會被視為頁面錯誤,導致 render.py 在寫入完整影片後回傳 1 並退出),並修補 render.py 為 chromium.launch(channel='chrome', …)。將 kit.js 放入 engine/demos/_hb/,撰寫 engine/demos/<film>/board.js(使用 HB.make(),將事件錨定到 BEATS[k].t0 + offset)與 engine/demos/<film>/eras.js(FILM_SIZE=[1080,1080]、PUNCH=0、Kalam 字型、SCENE_LIBS=['demos/<film>/beats.js','demos/_hb/kit.js','demos/<film>/board.js'])。接著執行 build_beats.py beats.json --engine engine → uv run --with playwright python engine/render.py --film demos/<film> --fps 30 --crf 12 --out work/<film>_master.mp4 → build_html.py <film> --beats beats.json(720²、24 fps、能符合預算的最低 CRF)→ test_contract.py final/<film>.html --beats beats.json。每部影片結尾需有一個向後拉遠到整個黑板的鏡頭、至少 1 秒的停頓,然後一個橡皮擦擦拭效果;空的黑板是循環重新開始的地方。beats.json 中最後一個節拍的 still 參數由所有風格共享,且必須在擦拭開始前結束,否則海報畫面與減少動態模式的影格會顯示半擦拭的黑板。會移動的物體(如滑板車、盒子)需使用 K.custom 項目,設定 handed: false,其位置為時間的函數;為每個物體設定結束時間,否則它在完成任務後仍會留在黑板上。
    • C. 從 examples/pizza/style-c/ 開始(SVG、時間軸、主題)。撰寫 src/<film>.svg(一個 <svg id="scene" class="scene"> 包含一個 <g id="world">;為每個 <text> 或其群組加上 data-b="<beat>" 屬性,從 1 開始,設定為相機 首次 顯示該文字的節拍)與 src/<film>.js(相機影格 FR / ORDER,每個影格包含一個 w 矩形(寬螢幕)與一個 t 矩形(窄螢幕);使用 R(selector, beat, offset) 進行揭示、camTo(i, beat, offset) 進行移動、draw() 進行線條繪製)。在 JS 中,beat 是 以 0 為基底(B[0] 代表節拍 1)。R() 僅用於文字,且 offset 需 ≥ 2.5 秒:每次呼叫 R() 都會被記錄為一次文字揭示,而字幕本身在 offset 0 時也算作文字,因此在無文字的形狀上或 offset 0 時使用 R() 會導致間距檢查失敗。與字幕同時出現的形狀、移動與文字,應直接使用 tl.to(…, T(beat, offset))。將任何擁有自己的 transform 屬性的元素包裹在父層 <g> 中,並對子元素進行動畫。接著執行 build.py beats.json --src src --out final [--css theme.css] → test_contract.py final/<film>.html --stills <scratch>/stills → widthcheck.py。執行時期檔案(shell.html、shell.css、head.js、tail.js)保持原樣直接使用;若要與主機頁面配色相符,請將新的 :root 變數(如 --panel、--ink、--muted、--line、--gold、--cap-ink、--on-gold 等)放入主題檔案,並以 --css 參數傳入。若要為 README 或聊天產生影片的 GIF 圖片:gif.py final/<film>.html --beat 4 --from 1.9 --to 8.6 --out film.gif(或使用 --still <s> 產生 PNG)。
  7. 在展示給任何人之前進行檢查(所有三個建置流程都跳過了最後三步;請務必執行):

    • 節奏檢查通過;契約測試在真實 Chrome 的 1100 與 360 px 寬度下全部通過;檔案大小在預算內;
    • 查看每個節拍在兩種寬度下的聯絡表(scripts/common/contact_sheet.py);
    • 對 A/B 風格的 mp4 檔案執行 huashu 的 film_gate.py(必要時配合 ffprobe 橋接程式),紅色讀數需按照 traps.md 中的說明解讀;
    • 由一位未參與建置的獨立審查者以正常速度觀看每個循環,並撰寫發現報告;
    • 在真實的主機頁面中測試(即 references/embed-contract.md 的父端),並在真實手機與 Safari 瀏覽器上測試(風格 A/B 的降級方案:當 H.264 無法播放時,顯示海報畫面加上編號字幕)。
  8. 交付。 將三種風格的成果並排展示(使用靜態畫面或切換頁面),附上循環長度、檔案大小、畫面上發明瞭哪些元素(供使用者確認),以及哪些項目未被檢查。

主機端:一個投影片內含全部九個動畫與一個切換器(scripts/deck/)

實際簡報的做法,以及過程中遇到的問題:

  1. 嵌入,而非連結。 每部影片以 <script type="text/plain" id="anim-{style}-{way}"> 的形式放入簡報,並將 </script 轉義為 <\/script、<!-- 轉義為 <\!--;JavaScript 僅在該投影片被顯示時(一個 iframe,按需建置)將內容解碼並寫入 iframe.srcdoc。embed_anims.py 會在 <!--ANIM-DATA-START--> / <!--ANIM-DATA-END--> 標記之間以冪等方式執行此操作,因此重新建置後再次執行只會替換資料。簡報大小從約 0.3 MB 增加到 4.5 MB。
  2. 一個投影片,兩個分段按鈕群組(方式、風格;使用 aria-pressed 屬性),並在每個「方式」的詳細資訊面板中加入一個「觀看運作過程」連結,該連結會設定方式、關閉面板並跳轉到該投影片。
  3. 播放/暫停跟隨投影片切換: go() 會分派一個 deck:slide 事件;切換器在其對應的投影片成為當前投影片時(以及在 iframe 載入完成且為當前投影片時)發送 anim-play 訊息,否則發送 anim-pause。
  4. 覆蓋 iframe 的手勢操作: 簡報的滾輪/按鍵/滑動手勢處理器被重構為 onWheel(dy,mode)、onKey(key)、onSwipe(dy,dx),由簡報本身的監聽器以及 iframe 轉發的訊息共同呼叫(僅接受 e.source === frame.contentWindow 的訊息)。howtest2.py 證明每次手勢操作只會切換一個投影片。
  5. 適應單一筆電螢幕:先縮窄內部版面,最後才縮放。 在 1280×800 解析度下,影片全寬時高度為 740–840 px,導致字幕落到螢幕下方。解決方案(embed_anims.py 中的 howFit 函式):srcdoc iframe 為同源,因此簡報可以直接測量 contentDocument.body 的高度。它從方框寬度開始,向下搜尋到 740 px(二元搜尋,每次調整寬度後等待約 90 ms,因為影片可能需要在 resize 事件後用 JavaScript 重新計算版面,期間隱藏 iframe),並保留能放入螢幕的最大寬度;只有在 740 px 仍然太高時,才設定 frame.style.zoom。縮窄版面優於縮放,因為字幕可以保持原有字級,而圖片會縮小。過程中遇到的陷阱:(a) 使用 width:100% 進行縮放會使內部版面變寬(1126 → 1876 px),並導致搜尋循環到最小值,因此始終將 frame.style.width 固定為 px 單位;(b) documentElement.scrollHeight 從不回報小於 iframe 當前高度的值,因此無法用於驅動縮小(應測量 body,並先將 iframe 設為足夠高);(c) 在寬度變更的同一個 tick 中進行測量,會讀到過時的 JavaScript 版面資訊。請要求影片在寬螢幕下採用並排版面(方形圖片),並在 780 px 寬度下總高度 ≤ 580 px。手機(< 900 px)無法適應;投影片將可滾動,並使用精簡的頁首。
  6. 測試: howtest.py(切換、深層連結、方向鍵)、howtest2.py(所有 9 個動畫在真實 Chrome 中載入並播放,透過 iframe 的滾輪/按鍵/滑動手勢確實只移動一個投影片),以及簡報自身的一次手勢一個投影片檢查。任何硬編碼了投影片編號的簡報測試,在插入投影片後都會失效:請改用 id 查詢投影片。

節奏控制(使用者對初版的主要抱怨)

初版原型為 8–13 秒。讀者評價:太快,無法閱讀正在發生的事,而且需要字幕。重建時使用的規則:

  • 每個節拍持續時間 ≥ max(4.5 秒, (字幕字數 + 新圖片字數) / 3 + 1.5 秒)。標準計數由 scripts/common/pace_check.py 執行(風格 C 的 build.py 也以相同方式從 SVG 計數)。風格 A 的 pace_check.py 會檢查已建置影片的真實標籤時間,其差異在於:如果一個詞在後續節拍中被新的標籤重複使用,則會再次計數;
  • 新的視覺元素只能在前一段文字出現後 ≥ 2.5 秒才出現;相機在閱讀文字期間保持靜止;
  • 節拍之間的緩慢相機移動(1.4–2.0 秒,正弦緩動),每部影片最多一個快速強調動作,用於結論時;
  • 在循環開始前,最後一個影格需停留 ≥ 1 秒。

向使用者說明的後果:每個節拍約 20 個字的字幕,會使 5–6 個節拍的影片長度達到 40–80 秒。在實際案例中,簡報的 18–34 秒目標是不可能達成的(僅字幕就需要 31.5–51.7 秒);三個代理都選擇採用了閱讀規則,建置了 35–82 秒的循環,但沒有人事先與使用者確認。要縮短循環,就必須縮短字幕,且只需在 beats.json 中修改一次,即可套用到所有風格。

嵌入契約、大小與手機(摘要;完整內容請參閱 references/embed-contract.md)

  • 每部影片為一個自包含的 HTML 檔案,由主機載入到 <iframe srcdoc> 中。包含畫面 + 字幕條 + 步驟軌道 + 播放/暫停按鈕,無其他內容。媒體以 base64 內嵌;
  • 所有三個風格的影片都遵守相同的嵌入契約,因此切換器可以在不重新載入頁面的情況下動態替換 iframe 的內容。
  • 手機版(< 900 px 寬度)不採用適應性縮放,而是讓投影片可滾動,並使用精簡的頁首,因為無法在這麼小的螢幕上同時舒適地顯示圖片與字幕。
  • 測試涵蓋了所有主要瀏覽器(Chrome、Safari、Firefox),並確保在 H.264 無法播放時(例如在某些 Safari 版本中),風格 A/B 會優雅地降級為顯示海報畫面與編號字幕。

相關技能與參考資料

  • huashu-art-motion:提供分鏡方向、相機規則與渲染引擎。
  • animate:用於單一的微互動動畫。
  • dataviz:用於數值圖表與資料視覺化。
  • 實際案例:examples/pizza/ 與 examples/expenses/ 展示了完整的建置流程與輸出範例。
  • 簡報文件模板與嵌入契約:位於 references/ 資料夾。

貢獻與回饋

歡迎提出問題或改進建議。請開啟一個 Issue 或提交 Pull Request 到本技能的儲存庫。


這個技能是為了讓技術簡報與報告中的機制圖解更具互動性與可理解性而設計的。

相關技能