whiteboard-animation
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWhiteboard Animation
白板动画
The VideoScribe / Doodly look: a hand draws illustrations and handwriting onto a board, stroke by stroke, while narration explains. Build it from animated SVG paths that draw themselves plus a marker hand that tracks the drawing tip, then sequence elements so the board fills up in narration order.
VideoScribe / Doodly风格效果:一只手逐笔在白板上绘制插画和手写内容,同时旁白进行讲解。通过可自绘的SVG动画路径加上追踪绘制端点的Marker手来实现,然后按照旁白顺序排列元素,让白板逐步填充内容。
When to use
使用场景
- A "scribe" / doodle explainer where a hand draws art and text onto a whiteboard.
- Animating a sketch or logo being drawn live, line by line.
- Handwriting that writes itself in, synced to a voiceover.
- Turning a static illustration into a guided, stroke-by-stroke reveal.
- 「手写/涂鸦风格讲解视频」:手在白板上绘制图形和文字
- 动画展示草图或标志的逐笔绘制过程
- 与旁白同步的自动手写效果
- 将静态插画转换为分步引导的逐笔展示效果
Core mechanic: SVG stroke draw-on
核心机制:SVG描边绘制
A path "draws itself" by animating from its full length down to 0 — the dash gap shrinks, exposing the stroke from start to end.
stroke-dashoffsetjs
const path = document.querySelector("#stroke");
const len = path.getTotalLength();
path.style.strokeDasharray = len; // one dash as long as the whole path
path.style.strokeDashoffset = len; // pushed fully out of view = invisible
path.getBoundingClientRect(); // force reflow before transitioning
path.style.transition = "stroke-dashoffset 1.2s linear";
path.style.strokeDashoffset = "0"; // draws start → endUse on the to normalize: then draws any path regardless of real length, and a single eased value (0→1) is the draw progress. (or a very gentle ease) reads as a steady hand; avoid on long strokes — it makes the marker lurch.
pathLength="1"<path>stroke-dasharray:1; stroke-dashoffset:1linearease-in-out路径通过将从完整长度动画过渡到0来实现「自绘」效果——虚线间隙逐渐缩小,从起点到终点逐步显示描边。
stroke-dashoffsetjs
const path = document.querySelector("#stroke");
const len = path.getTotalLength();
path.style.strokeDasharray = len; // 虚线长度等于整条路径
path.style.strokeDashoffset = len; // 完全移出视野 → 不可见
path.getBoundingClientRect(); // 过渡前强制重排
path.style.transition = "stroke-dashoffset 1.2s linear";
path.style.strokeDashoffset = "0"; // 从起点绘制到终点在上使用进行标准化:此时可绘制任意长度的路径,单个缓动值(0→1)即为绘制进度。(或非常平缓的缓动)会呈现出稳定的手绘效果;长描边避免使用,否则会让Marker手出现突兀的晃动。
<path>pathLength="1"stroke-dasharray:1; stroke-dashoffset:1linearease-in-outThe marker hand follows the tip
Marker手跟随绘制端点
The illusion lives or dies on the hand sitting at the exact point being drawn. Sample the path at the current draw progress and place the hand image there each frame; the pen-nib of the hand asset must align to the path point (offset the image so its tip, not its corner, lands on the point).
js
const hand = document.querySelector("#hand"); // <img>/<g>, tip at its top-left "nib"
const NIB = { x: 6, y: 4 }; // nib offset inside the hand art
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 progress
path.style.strokeDashoffset = len * (1 - k); // expose start→tip
const p = path.getPointAtLength(len * k); // current tip in SVG coords
hand.setAttribute("transform", `translate(${p.x - NIB.x} ${p.y - NIB.y})`);
if (k < 1) requestAnimationFrame(frame);
})(t0);
}getPointAtLength(len * k)strokeDashoffset这个视觉效果的关键在于手始终精准位于当前绘制的端点。在每一帧根据当前绘制进度采样路径,并将手的图像放置在该位置;手素材的笔尖必须与路径点对齐(偏移图像,使其笔尖而非角落落在路径点上)。
js
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)strokeDashoffsetReveal order — the board fills up
展示顺序——白板逐步填充
Sequence elements the way a person would draw the scene, in narration order: each illustration and each handwriting block draws in, holds, then the next begins. Stagger strokes within one drawing; hold a beat after each element lands before moving the hand on. One drawing = one beat of the VO. Don't draw two things at once — the eye (and the single hand) can only follow one tip.
| Step | Mechanism |
|---|---|
| Draw a stroke | |
| Multi-stroke drawing | stagger strokes; hand hops to each new stroke's start |
| Handwriting | per-letter/word strokes drawn in reading order, hand following |
| Hold | pause hand off-board after element completes (0.4–1.0s) |
| Next element | move/fade hand to next start; begin its draw-on |
| Erase / clear | wipe or reverse-draw to clear the board for the next section |
按照真人绘制场景的顺序排列元素,即旁白的顺序:每个插画和手写块依次绘制完成并保持,然后开始下一个。在单个绘制内容内错开描边时机;每个元素绘制完成后停顿片刻,再移动手到下一个位置。一个绘制内容对应旁白的一个节拍。不要同时绘制两个内容——眼睛(和单只手)只能追踪一个端点。
| 步骤 | 实现机制 |
|---|---|
| 绘制单条描边 | |
| 多描边绘制内容 | 错开描边时机;手跳转到每条新描边的起点 |
| 手写内容 | 按阅读顺序绘制每个字母/单词的描边,手同步跟随 |
| 停顿 | 元素完成后,手移到白板外停顿(0.4–1.0秒) |
| 下一个元素 | 移动/淡出手到下一个起点;开始绘制 |
| 擦除/清空 | 使用擦除或反向绘制效果清空白板,以便展示下一部分 |
Converting art to drawable paths
将素材转换为可绘制路径
Draw-on needs single-stroke, open paths in draw order — not filled compound shapes.
- Author or trace line art as open strokes (Illustrator/Inkscape: outline, not fill). A filled blob has no "stroke" to animate; convert fills to a centerline stroke or draw an outline path and fill it after the outline completes.
- Order matters: the elements should appear in the SVG in the order a hand would draw them — animate them in document order.
<path> - Long continuous lines look most natural; break only where a real pen would lift.
- For solid color regions: draw the outline stroke first, then fade/grow the fill in behind it (or a clip-reveal) so it reads as "colored in after."
opacity - Handwriting: use a stroke/handwriting font, convert glyphs to paths, then draw each glyph's path in stroke order. Or fake it: mask the word and sweep a reveal left-to-right with the hand riding the mask edge.
描边绘制需要按绘制顺序排列的单条开放路径——而非填充的复合图形。
- 将线稿创作或描摹为开放描边(Illustrator/Inkscape:使用轮廓,而非填充)。填充的图形没有可动画的「描边」;需将填充转换为中心线描边,或先绘制轮廓路径,完成后再填充内部。
- 顺序很重要:SVG中的元素应按真人绘制的顺序排列——按文档顺序进行动画。
<path> - 长连续线条看起来最自然;仅在真实画笔会抬起的位置断开。
- 对于纯色区域:先绘制轮廓描边,然后在其后方淡入/填充(通过或裁剪展示),使其呈现为「先勾线再填色」的效果。
opacity - 手写内容:使用手写字体,将字形转换为路径,然后按描边顺序绘制每个字形的路径。或模拟效果:为文字添加遮罩,从左到右扫过遮罩,让手跟随遮罩边缘移动。
Marker hand asset
Marker手素材
A PNG/SVG of a hand holding a marker, with the nib at a known point. Measure the nib offset once ( above) and reuse it. Keep one hand asset for the whole piece for consistency. Mirror it () only if you need a left hand. Add a faint shadow under the hand for depth; drop it during holds when the hand is "off."
NIBscaleX(-1)一只手持Marker的PNG/SVG素材,笔尖位置已知。只需测量一次笔尖偏移量(即上述的)并复用。整个作品使用同一手素材以保持一致性。仅在需要左手时进行镜像()。为手添加微弱阴影以增加深度;手「离开」白板的停顿阶段隐藏阴影。
NIBscaleX(-1)Erase / wipe transitions
擦除/过渡效果
Between sections, clear the board so it doesn't get cluttered:
- Wipe: animate a (or a white rectangle) across the filled group to wipe it away, hand optionally "erasing."
clip-path - Reverse draw-on: run 0→len to un-draw, hand following backward.
strokeDashoffset - Fade + redraw: cross-fade the old group out and start the next group's draw-on.
Keep one clear grammar (always wipe left-to-right, say) so transitions feel deliberate.
在不同章节之间清空白板,避免内容杂乱:
- 擦除:为已填充的组添加(或白色矩形)动画,使其滑过并擦除内容,手可配合做出「擦除」动作。
clip-path - 反向绘制:将从0→len进行动画,手反向跟随。
strokeDashoffset - 淡入淡出+重绘:交叉淡出旧内容组,同时开始新内容组的绘制。
- 保持统一的过渡规则(例如始终从左到右擦除),让过渡效果显得更刻意自然。
Pacing to narration
与旁白同步节奏
Draw-on duration should track the spoken line, not a fixed timer. Estimate from VO at ~2.3 words/sec; set each drawing's total draw time to roughly the length of the line it illustrates, plus a short hold. A label that the VO names should finish drawing right as the word is spoken. Slow the hand for emphasis lines; speed minor connective strokes. Never let the hand idle on a finished board while narration continues — move it off or start the next element.
绘制时长应跟随旁白台词,而非固定计时器。根据旁白语速估算(约2.3词/秒);将每个绘制内容的总时长设置为其对应的台词长度,再加上短暂停顿。旁白提到的标签应在说出该单词时刚好绘制完成。强调性台词放慢手速;次要连接性描边加快速度。旁白继续时,不要让手停在已完成的白板上——将手移开或开始下一个元素的绘制。
Output checklist
输出检查清单
- Every stroke is a single open path in draw order; nothing pops in fully formed.
- The hand nib sits exactly on the draw tip throughout each stroke (no float, no lag).
- Hand lifts cleanly between separate strokes/elements; no sliding across blank board.
- Elements draw in narration order, one at a time; each holds before the next.
- Handwriting draws in reading order; fills appear after their outline.
- Erase/wipe grammar is consistent; the board never over-clutters.
- Draw timing tracks the VO line it illustrates.
- : show the completed board (all strokes at offset 0, hand hidden) without the drawing animation.
prefers-reduced-motion
- 每条描边都是按绘制顺序排列的单条开放路径;没有内容突然完整出现。
- 在整个描边过程中,手的笔尖始终精准位于绘制端点(无漂浮、滞后)。
- 在不同描边/元素之间,手干净抬起;不在空白白板上滑动。
- 元素按旁白顺序绘制,一次一个;每个元素完成后停顿再开始下一个。
- 手写内容按阅读顺序绘制;填充内容在轮廓之后出现。
- 擦除/过渡规则统一;白板从不杂乱。
- 绘制时长与对应的旁白台词同步。
- :显示完整的白板(所有描边的offset为0,手隐藏),不播放绘制动画。
prefers-reduced-motion
Deliver & verify (standalone HTML)
交付与验证(独立HTML文件)
Packaged helper ():scripts/freezes thescripts/seek-shot.sh anim.html 0 1.5 3harness and screenshots each moment;?t=Ntiles them for one-glance review. Seescripts/contact-sheet.sh sheet.png frame-*.png.scripts/README.md
For a self-contained whiteboard scene (draw-on illustration, handwriting, a short scribe beat) the deliverable is one HTML file that opens directly in a browser — no build step, no render pipeline. A single file is the right tier for web motion; don't reach for a bundler when one file does the job. (For a full narrated scribe video with baked VO, build it as a Remotion composition and verify via — see explainer-video.)
remotion stillOutput contract:
- One file: SVG inline (paths in draw order,
.htmlor measured length), the hand asset inline or as a data-URI, and the draw-on driver in one inlinepathLength="1".<script> - All drawing on one master timeline (GSAP , or one rAF clock) so the whole sequence has a single playhead you can seek —
tlAND the hand position both driven by the same progress value.strokeDashoffset - Include the seek harness below so any moment can be frozen for screenshots. Freezing at shows exactly how much of the path is drawn — the verification leans on this.
t
Seek harness — freeze an exact moment for screenshots. seeks the master timeline to seconds and pauses, so a screenshot lands on a still, deterministic frame mid-draw.
?t=NNhtml
<script>
// ... build your master timeline as `tl` (drawOn calls placed on it) ...
const t = new URLSearchParams(location.search).get("t");
if (t !== null) { tl.pause(); tl.seek(parseFloat(t)); } // frozen at t seconds
// no ?t → plays normally
window.__ready = true; // ready signal for headless wait
console.log("duration", tl.duration());
</script>Verify loop — render → freeze → screenshot → check:
- Open the file at moments across the timeline — start, a frame mid-draw, end:
,
…/scribe.html?t=0,?t=<dur/2>. Read?t=<dur>from the console for the end time. Pick at least onetl.duration()in the middle of a stroke — that is where the hand-on-tip illusion is proven or broken.t - Headless-screenshot each frozen frame:
bash
npx playwright screenshot --wait-for-timeout=500 "file://$PWD/scribe.html?t=1.2" frame-mid.png - INSPECT each still — check fidelity: at a mid-stroke the hand nib sits exactly on the leading tip of the drawn portion (not ahead, not behind, not floating); the path is drawn only up to the tip; at
tevery stroke completes cleanly with no gaps, no overshoot, no leftover dash. Check artifacts: hand sliding across blank board between strokes, nib offset wrong (tip floats off the line), strokes drawing out of order, fills appearing before their outline, clipped/off-canvas drawings, FOUC before fonts load.?t=<dur> - Iterate: adjust , stroke order, durations, and re-screenshot until each frozen frame looks like a hand mid-draw.
NIB
Before you finish:
- Opens standalone in a browser — no console errors, no missing CDN/assets.
- One master timeline; freezes correctly and
?t=N+ hand position share one progress value.strokeDashoffset - Screenshotted at start / mid-stroke / end — hand nib on the tip throughout, strokes complete cleanly at the end, draw order correct.
- Hand lifts between strokes; no sliding across blank board.
- shows the completed board (all strokes drawn, hand hidden) without the drawing animation.
prefers-reduced-motion
For a narrated, frame-deterministic export, port the same draw-on (drive and the hand transform from ) into a Remotion composition and render to MP4/GIF — see the explainer-video skill for the render-stills → encode loop.
strokeDashoffsetuseCurrentFrame()/fps打包工具(目录):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文件——无需构建步骤,无需渲染流水线。单个文件是Web动效的最佳交付形式;单个文件能完成的工作,无需使用打包工具。(如需包含旁白的完整手写讲解视频,可将其构建为Remotion合成项目,并通过进行验证——详见explainer-video技能。)
remotion still输出规范:
- 单个文件:内联SVG(按绘制顺序排列的路径,带有
.html或已测量长度)、内联或转为data-URI的手素材,以及内联在pathLength="1"中的绘制驱动代码。<script> - 所有绘制基于单个主时间轴(GSAP 或一个rAF时钟),因此整个序列有一个可跳转的播放头——
tl和手的位置均由同一个进度值驱动。strokeDashoffset - 包含以下跳转机制,以便冻结任意时刻进行截图。在时刻冻结可准确显示路径已绘制的部分——验证过程依赖此功能。
t
跳转机制——冻结精确时刻以截图。会将主时间轴跳转到秒并暂停,因此截图会定格在绘制过程中的某个确定帧。
?t=NNhtml
<script>
// ... 将主时间轴构建为`tl`(drawOn调用添加到时间轴上) ...
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 - 无头模式截取每个冻结帧的截图:
bash
npx playwright screenshot --wait-for-timeout=500 "file://$PWD/scribe.html?t=1.2" frame-mid.png - 检查每张截图——验证保真度:在描边绘制中的时刻,手的笔尖精准位于已绘制部分的前端(不超前、不滞后、不漂浮);路径仅绘制到端点处;在
t时刻,所有描边完整绘制完成,无间隙、无过绘制、无残留虚线。检查瑕疵:描边之间手在空白白板上滑动、笔尖偏移错误(笔尖脱离线条)、描边绘制顺序错误、填充内容在轮廓之前出现、绘制内容被裁剪/超出画布、字体加载前出现FOUC(无样式内容闪烁)。?t=<dur> - 迭代调整:修改、描边顺序、时长,重新截图直到每个冻结帧都呈现出手正在绘制的效果。
NIB
完成前检查:
- 可在浏览器中独立打开——无控制台错误,无缺失的CDN/素材。
- 单个主时间轴;可正确冻结,且
?t=N与手的位置共享同一个进度值。strokeDashoffset - 在开始/描边绘制中/结束时刻均已截图——全程手的笔尖贴合端点,结束时所有描边完整,绘制顺序正确。
- 描边之间手抬起;不在空白白板上滑动。
- 模式下显示完整白板(所有描边已绘制,手隐藏),不播放绘制动画。
prefers-reduced-motion
如需导出带旁白、帧确定的视频,可将相同的描边绘制逻辑(通过驱动和手的变换)移植到Remotion合成项目中,渲染为MP4/GIF格式——详见explainer-video技能的渲染截图→编码流程。
useCurrentFrame()/fpsstrokeDashoffsetReference files
参考文件
- — runnable draw-on mechanics:
references/draw-on-recipes.md/getTotalLengthhand-follows-path, GSAPgetPointAtLengthandDrawSVGPluginvariants, multi-stroke staggering, per-glyph handwriting, fill-after-outline, erase/wipe and reverse-draw transitions, and the Remotion frame-driven port.MotionPathPlugin - — turning art into drawable single-stroke SVGs, draw-order authoring, handwriting/stroke fonts, the marker hand asset and nib calibration, pacing draw time to the VO line, and the storyboard-to-board build sequence.
references/whiteboard-pipeline.md
- ——可运行的描边绘制机制:
references/draw-on-recipes.md/getTotalLength手跟随路径、GSAPgetPointAtLength和DrawSVGPlugin变体、多描边错开时机、逐字形手写、轮廓后填充、擦除/反向绘制过渡,以及Remotion帧驱动移植方案。MotionPathPlugin - ——将素材转换为可绘制的单条描边SVG、绘制顺序创作、手写/描边字体、Marker手素材与笔尖校准、绘制时长与旁白同步、从分镜到白板的制作流程。
references/whiteboard-pipeline.md