白板動畫 是做什麼的?
VideoScribe / Doodly 風格:一隻手在板上逐筆畫出插圖和手寫文字,同時搭配旁白解說。透過動畫化的 SVG 路徑 (它們會自行繪製) 加上一個追蹤繪製筆尖的標記手來建構,然後依旁白順序安排元素,使板子逐漸填滿內容。
使用時機
- 「速寫」/塗鴉解說視訊,一隻手在白色板子上繪製藝術品和文字。
- 將素描或標誌繪製的過程即時逐線條動畫化。
- 手寫文字自動書寫出現,並與畫外音同步。
- 將靜態插圖轉變為逐步揭示的導覽體驗。
核心機制:SVG 筆觸繪製
路徑透過將 stroke-dashoffset 從其完整長度動畫化到 0 來「自行繪製」 — 虛線間距縮小,從起點到終點逐漸顯露筆觸。
const path = document.querySelector("#stroke");
const len = path.getTotalLength();
path.style.strokeDasharray = len; // 一個與整個路徑等長的虛線
path.style.strokeDashoffset = len; // 完全推出視野 = 不可見
path.getBoundingClientRect(); // 強制 reflow,使過渡生效
path.style.transition = "stroke-dashoffset 1.2s linear";
path.style.strokeDashoffset = "0"; // 從起點 → 終點繪製
在 <path> 上使用 pathLength="1" 來標準化:這樣 stroke-dasharray:1; stroke-dashoffset:1 就能繪製任何長度的路徑,且單一的緩動值 (0→1) 即為繪製進度。linear (或非常輕微的緩動) 看起來像穩定的手繪;避免在長筆觸上使用 ease-in-out — 它會讓標記顯得突兀。
標記手跟隨筆尖
這個幻覺成敗的關鍵在於手是否精確地位於正在繪製的點上。在每個影格根據當前繪製進度對路徑取樣,並將手部圖像放置在該處;手部資源的筆尖必須對齊路徑點 (偏移圖像,使其尖端 (而非角落) 落在該點上)。
const hand = document.querySelector("#hand"); // <img>/<g>,尖端在左上角的「筆尖」
const NIB = { x: 6, y: 4 }; // 手部藝術中筆尖的偏移量
function drawOn(path, hand, dur = 1200) {
const len = path.getTotalLength(), t0 = performance.now();
path.style.strokeDasharray = len;
(function frame(now) {
const k = Math.min(1, (now - t0) / dur); // 0→1 進度
path.style.strokeDashoffset = len * (1 - k); // 從起點暴露到筆尖
const p = path.getPointAtLength(len * k); // SVG 座標中的當前筆尖位置
hand.setAttribute("transform", `translate(${p.x - NIB.x} ${p.y - NIB.y})`);
if (k < 1) requestAnimationFrame(frame);
})(t0);
}
getPointAtLength(len * k) 是關鍵:它返回與驅動 strokeDashoffset 相同的進度在路徑上的座標,因此筆尖能緊貼生長的筆觸。在分開的筆觸之間提起手 (淡出,跳到下一個起點),這樣它就不會在空白板子上滑動。
揭示順序 — 板子逐漸填滿
按照旁白順序,以人類繪製場景的方式排列元素:每個插圖和每個手寫區塊依序繪製、保持,然後下一個開始。在同一個繪圖中交錯筆觸;在每個元素完成後稍作停頓,再移動手部。一個繪圖 = 旁白的一個節拍。不要同時繪製兩個東西 — 眼睛 (以及單隻手) 一次只能跟隨一個筆尖。
| 步驟 | 機制 |
|---|---|
| 繪製一個筆觸 | stroke-dashoffset len→0,手位於 getPointAtLength(len*k) |
| 多筆觸繪圖 | 交錯筆觸;手跳躍到每個新筆觸的起點 |
| 手寫文字 | 按閱讀順序繪製每個字母/單詞的筆觸,手跟隨其後 |
| 保持 | 在元素完成後將手移出板外暫停 (0.4–1.0 秒) |
| 下一個元素 | 移動/淡入手到下一個起點;開始其繪製 |
| 擦除 / 清除 | 抹除或反向繪製以清除板面,為下一部分做準備 |
將藝術品轉換為可繪製的路徑
繪製效果需要按繪製順序排列的單筆觸、開放式路徑 — 而不是填充的複合形狀。
- 將線條藝術創作或描繪為開放式筆觸 (Illustrator/Inkscape:輪廓線,非填充)。填充的色塊沒有可以動畫化的「筆觸」;將填充轉換為中心線筆觸,或繪製輪廓路徑並在輪廓完成後填充它。
- 順序很重要:
<path>元素在 SVG 中的出現順序應如同人手繪製它們的順序 — 按文件順序對它們進行動畫處理。 - 長而連續的線條看起來最自然;僅在真實的筆會提起的地方中斷。
- 對於純色區域:先繪製輪廓筆觸,然後在它後面淡入/放大填充 (
opacity或裁切揭示效果),使其看起來像是「之後才著色的」。 - 手寫文字:使用筆觸/手寫字體,將字形轉換為路徑,然後按筆順繪製每個字形的路徑。或者偽造它:遮罩單詞並讓一個揭示效果從左到右掃過,讓手跟隨遮罩邊緣。
標記手部資源
一個拿著名牌的 PNG/SVG 圖像,筆尖位於已知點。測量一次筆尖偏移量 (NIB 如上) 並重複使用它。為了保持一致,在整個作品中使用同一個手部資源。僅在需要左手時才鏡像它 (scaleX(-1))。在手下添加一個淡淡的陰影以增加深度;在手「離開」的保持期間隱藏它。
擦除 / 抹除轉場
在各個部分之間,清除板面使其不顯得雜亂:
- 抹除:在已填充的群組上動畫化一個
clip-path(或一個白色矩形) 以將其抹除,手可選擇性地「擦拭」。 - 反向繪製:執行
strokeDashoffset0→len 來取消繪製,手跟隨其後倒退。 - 淡出 + 重繪:交叉淡出舊群組,並開始下一個群組的繪製。
保持一個清晰的語法 (例如總是從左到右抹除),這樣轉場會感覺有設計感。
與旁白同步節奏
繪製時間應跟隨口說臺詞,而不是固定計時器。根據 VO 約 2.3 字/秒的速度估算;將每個繪圖的總繪製時間設定為它所說明的臺詞長度,再加上一個短暫的保持。VO 提到的標籤應在該詞被說出時恰好完成繪製。對於強調性的臺詞,放慢手的速度;對於次要的連接筆觸,加快速度。千萬不要讓手在完成的板子上閒置而旁白繼續 — 將其移開或開始下一個元素。
輸出檢查清單
- 每個筆觸都是按繪製順序排列的單一開放路徑;沒有任何東西是突然完整出現的。
- 在每個筆觸的過程中,手部的筆尖精確地位於繪製尖端上 (不飄移,不延遲)。
- 在分開的筆觸/元素之間,手部乾淨俐落地提起;不會在空白板子上滑動。
- 元素按旁白順序一個接一個繪製;每個在開始下一個之前會保持一段時間。
- 手寫文字按閱讀順序繪製;填充在輪廓之後出現。
- 擦除/抹除語法一致;板子永遠不會過於雜亂。
- 繪製時機與所說明的 VO 臺詞同步。
prefers-reduced-motion:顯示完成的板子 (所有筆觸的偏移量為 0,手部隱藏),不播放繪製動畫。
交付與驗證 (獨立 HTML)
打包的輔助工具 (
scripts/):scripts/seek-shot.sh anim.html 0 1.5 3凍結?t=N框架並截取每個時刻的螢幕截圖;scripts/contact-sheet.sh sheet.png frame-*.png將它們拼貼在一起,以便一覽式審查。詳見scripts/README.md。
對於一個自包含的白板場景 (繪製插圖、手寫文字、一個簡短的速寫節拍),可交付成果是一個可直接在瀏覽器中開啟的 HTML 檔案 — 無需建置步驟,無需渲染管線。對於網頁動畫來說,單一檔案是合適的層級;當一個檔案就能完成工作時,不要使用打包工具。(對於帶有配音 VO 的完整敘述性速寫視訊,請將其建置為 Remotion 組合並透過 remotion still 驗證 — 請參閱解說視訊技能。)
輸出契約:
- 一個
.html檔案:內嵌 SVG (路徑按繪製順序排列,使用pathLength="1"或測量長度),手部資源內嵌或使用 data-URI,以及一個內嵌<script>的繪製驅動程式。 - 所有繪製都在一個主時間軸上 (GSAP
tl,或一個 rAF 時鐘),因此整個序列有一個單一的播放頭可供定位 —strokeDashoffset和手部位置都由同一個進度值驅動。 - 包含下面的定位框架,以便可以凍結任何時刻進行螢幕截圖。在
t處凍結會顯示路徑已繪製了多少 — 驗證依賴於此。
定位框架 — 凍結精確時刻以進行螢幕截圖。 ?t=N 將主時間軸定位到 N 秒並暫停,因此螢幕截圖會落在繪製中一個靜止、確定的影格上。
<script>
// ... 將您的主時間軸建置為 `tl` (繪製呼叫放置於其上) ...
const t = new URLSearchParams(location.search).get("t");
if (t !== null) { tl.pause(); tl.seek(parseFloat(t)); } // 在 t 秒處凍結
// 無 ?t → 正常播放
window.__ready = true; // 為無頭等待準備的信號
console.log("duration", tl.duration());
</script>
驗證迴圈 — 渲染 → 凍結 → 截圖 → 檢查:
- 在時間軸的各個時刻開啟檔案 — 開始、繪製中的影格、結束:
…/scribe.html?t=0,?t=<dur/2>,?t=<dur>。從控制台讀取tl.duration()以取得結束時間。在筆觸繪製過程中至少選擇一個t— 正是在這裡,手在筆尖上的幻覺才能被證明或打破。 - 對每個凍結的影格進行無頭截圖:
npx playwright screenshot --wait-for-timeout=500 "file://$PWD/scribe.html?t=1.2" frame-mid.png - 檢查每張靜止圖 — 檢查保真度:在筆觸繪製中間的
t時,手部筆尖精確地位於已繪製部分的先端尖端上 (不超前、不落後、不飄浮);路徑僅繪製到尖端為止;在?t=<dur>時,每個筆觸都完整乾淨地完成,沒有間隙、沒有超調、沒有剩餘的虛線。檢查瑕疵:手在筆觸之間於空白板子上滑動、筆尖偏移錯誤 (尖端飄離線條)、筆觸繪製順序錯誤、填充在其輪廓之前出現、被裁切/超出畫布的繪圖、字體加載前的 FOUC。 - 迭代:調整
NIB、筆觸順序、持續時間,並重新截圖,直到每個凍結的影格看起來都像是一隻正在繪製過程中的手。
在您完成之前:
- 可在瀏覽器中獨立開啟 — 無控制台錯誤,無缺少的 CDN/資源。
- 一個主時間軸;
?t=N正確凍結,且strokeDashoffset+ 手部位置共享一個進度值。 - 在開始 / 筆觸中間 / 結束處截圖 — 手部筆尖在整個過程中位於尖端上,筆觸在結束時完整乾淨地完成,繪製順序正確。
- 手在筆觸之間提起;不會在空白板子上滑動。
prefers-reduced-motion顯示完成的板子 (所有筆觸已繪製,手部隱藏),不播放繪製動畫。
對於有旁白、影格確定的輸出,將相同的繪製機制 (使用 useCurrentFrame()/fps 驅動 strokeDashoffset 和手部變換) 移植到 Remotion 組合中,並渲染為 MP4/GIF — 請參閱解說視訊技能以了解渲染靜止影格 → 編碼的循環。
參考檔案
references/draw-on-recipes.md— 可執行的繪製機制:getTotalLength/getPointAtLength手跟隨路徑、GSAPDrawSVGPlugin和MotionPathPlugin變體、多筆觸交錯、逐字形手寫、輪廓後填充、擦除/抹除和反向繪製轉場,以及 Remotion 影格驅動移植。references/whiteboard-pipeline.md— 將藝術品轉換為可繪製的單筆觸 SVG、按繪製順序創作、手寫/筆觸字體、標記手部資源和筆尖校準、將繪製時間與 VO 臺詞同步,以及從分鏡到板子的建置順序。