whiteboard-animation

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Whiteboard 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
stroke-dashoffset
from its full length down to 0 — the dash gap shrinks, exposing the stroke from start to end.
js
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 → end
Use
pathLength="1"
on the
<path>
to normalize: then
stroke-dasharray:1; stroke-dashoffset:1
draws any path regardless of real length, and a single eased value (0→1) is the draw progress.
linear
(or a very gentle ease) reads as a steady hand; avoid
ease-in-out
on long strokes — it makes the marker lurch.
路径通过将
stroke-dashoffset
从完整长度动画过渡到0来实现「自绘」效果——虚线间隙逐渐缩小,从起点到终点逐步显示描边。
js
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";       // 从起点绘制到终点
<path>
上使用
pathLength="1"
进行标准化:此时
stroke-dasharray:1; stroke-dashoffset:1
可绘制任意长度的路径,单个缓动值(0→1)即为绘制进度。
linear
(或非常平缓的缓动)会呈现出稳定的手绘效果;长描边避免使用
ease-in-out
,否则会让Marker手出现突兀的晃动。

The 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)
is the workhorse: it returns the on-path coordinate at the same progress driving
strokeDashoffset
, so the nib stays glued to the growing stroke. Lift the hand (fade out, jump to the next start) between separate strokes so it doesn't slide across blank board.
这个视觉效果的关键在于手始终精准位于当前绘制的端点。在每一帧根据当前绘制进度采样路径,并将手的图像放置在该位置;手素材的笔尖必须与路径点对齐(偏移图像,使其笔尖而非角落落在路径点上)。
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)
是核心函数:它返回与
strokeDashoffset
驱动进度一致的路径坐标,因此笔尖会始终贴合正在延伸的描边。在不同描边之间抬起手(淡出或跳转到下一个起点),避免手在空白白板上滑动。

Reveal 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.
StepMechanism
Draw a stroke
stroke-dashoffset
len→0, hand at
getPointAtLength(len*k)
Multi-stroke drawingstagger strokes; hand hops to each new stroke's start
Handwritingper-letter/word strokes drawn in reading order, hand following
Holdpause hand off-board after element completes (0.4–1.0s)
Next elementmove/fade hand to next start; begin its draw-on
Erase / clearwipe or reverse-draw to clear the board for the next section
按照真人绘制场景的顺序排列元素,即旁白的顺序:每个插画和手写块依次绘制完成并保持,然后开始下一个。在单个绘制内容内错开描边时机;每个元素绘制完成后停顿片刻,再移动手到下一个位置。一个绘制内容对应旁白的一个节拍。不要同时绘制两个内容——眼睛(和单只手)只能追踪一个端点。
步骤实现机制
绘制单条描边
stroke-dashoffset
从len→0,手位于
getPointAtLength(len*k)
多描边绘制内容错开描边时机;手跳转到每条新描边的起点
手写内容按阅读顺序绘制每个字母/单词的描边,手同步跟随
停顿元素完成后,手移到白板外停顿(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
    <path>
    elements should appear in the SVG in the order a hand would draw them — animate them in document order.
  • 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 (
    opacity
    or a clip-reveal) so it reads as "colored in after."
  • 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 (
NIB
above) and reuse it. Keep one hand asset for the whole piece for consistency. Mirror it (
scaleX(-1)
) 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."
一只手持Marker的PNG/SVG素材,笔尖位置已知。只需测量一次笔尖偏移量(即上述的
NIB
)并复用。整个作品使用同一手素材以保持一致性。仅在需要左手时进行镜像(
scaleX(-1)
)。为手添加微弱阴影以增加深度;手「离开」白板的停顿阶段隐藏阴影。

Erase / wipe transitions

擦除/过渡效果

Between sections, clear the board so it doesn't get cluttered:
  • Wipe: animate a
    clip-path
    (or a white rectangle) across the filled group to wipe it away, hand optionally "erasing."
  • Reverse draw-on: run
    strokeDashoffset
    0→len to un-draw, hand following backward.
  • 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
    (或白色矩形)动画,使其滑过并擦除内容,手可配合做出「擦除」动作。
  • 反向绘制:将
    strokeDashoffset
    从0→len进行动画,手反向跟随。
  • 淡入淡出+重绘:交叉淡出旧内容组,同时开始新内容组的绘制。
  • 保持统一的过渡规则(例如始终从左到右擦除),让过渡效果显得更刻意自然。

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.
  • prefers-reduced-motion
    : show the completed board (all strokes at offset 0, hand hidden) without the drawing animation.
  • 每条描边都是按绘制顺序排列的单条开放路径;没有内容突然完整出现。
  • 在整个描边过程中,手的笔尖始终精准位于绘制端点(无漂浮、滞后)。
  • 在不同描边/元素之间,手干净抬起;不在空白白板上滑动。
  • 元素按旁白顺序绘制,一次一个;每个元素完成后停顿再开始下一个。
  • 手写内容按阅读顺序绘制;填充内容在轮廓之后出现。
  • 擦除/过渡规则统一;白板从不杂乱。
  • 绘制时长与对应的旁白台词同步。
  • prefers-reduced-motion
    :显示完整的白板(所有描边的offset为0,手隐藏),不播放绘制动画。

Deliver & verify (standalone HTML)

交付与验证(独立HTML文件)

Packaged helper (
scripts/
):
scripts/seek-shot.sh anim.html 0 1.5 3
freezes the
?t=N
harness and screenshots each moment;
scripts/contact-sheet.sh sheet.png frame-*.png
tiles them for one-glance review. See
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
remotion still
— see explainer-video.)
Output contract:
  • One
    .html
    file: SVG inline (paths in draw order,
    pathLength="1"
    or measured length), the hand asset inline or as a data-URI, and the draw-on driver in one inline
    <script>
    .
  • All drawing on one master timeline (GSAP
    tl
    , or one rAF clock) so the whole sequence has a single playhead you can seek —
    strokeDashoffset
    AND the hand position both driven by the same progress value.
  • Include the seek harness below so any moment can be frozen for screenshots. Freezing at
    t
    shows exactly how much of the path is drawn — the verification leans on this.
Seek harness — freeze an exact moment for screenshots.
?t=N
seeks the master timeline to
N
seconds and pauses, so a screenshot lands on a still, deterministic frame mid-draw.
html
<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:
  1. Open the file at moments across the timeline — start, a frame mid-draw, end:
    …/scribe.html?t=0
    ,
    ?t=<dur/2>
    ,
    ?t=<dur>
    . Read
    tl.duration()
    from the console for the end time. Pick at least one
    t
    in the middle of a stroke — that is where the hand-on-tip illusion is proven or broken.
  2. Headless-screenshot each frozen frame:
    bash
    npx playwright screenshot --wait-for-timeout=500 "file://$PWD/scribe.html?t=1.2" frame-mid.png
  3. INSPECT each still — check fidelity: at a mid-stroke
    t
    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
    ?t=<dur>
    every 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.
  4. Iterate: adjust
    NIB
    , stroke order, durations, and re-screenshot until each frozen frame looks like a hand mid-draw.
Before you finish:
  1. Opens standalone in a browser — no console errors, no missing CDN/assets.
  2. One master timeline;
    ?t=N
    freezes correctly and
    strokeDashoffset
    + hand position share one progress value.
  3. Screenshotted at start / mid-stroke / end — hand nib on the tip throughout, strokes complete cleanly at the end, draw order correct.
  4. Hand lifts between strokes; no sliding across blank board.
  5. prefers-reduced-motion
    shows the completed board (all strokes drawn, hand hidden) without the drawing animation.
For a narrated, frame-deterministic export, port the same draw-on (drive
strokeDashoffset
and the hand transform from
useCurrentFrame()/fps
) into a Remotion composition and render to MP4/GIF — see the explainer-video skill for the render-stills → encode loop.
打包工具
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合成项目,并通过
remotion still
进行验证——详见explainer-video技能。)
输出规范:
  • 单个
    .html
    文件:内联SVG(按绘制顺序排列的路径,带有
    pathLength="1"
    或已测量长度)、内联或转为data-URI的手素材,以及内联在
    <script>
    中的绘制驱动代码。
  • 所有绘制基于单个主时间轴(GSAP
    tl
    或一个rAF时钟),因此整个序列有一个可跳转的播放头——
    strokeDashoffset
    和手的位置均由同一个进度值驱动。
  • 包含以下跳转机制,以便冻结任意时刻进行截图。在
    t
    时刻冻结可准确显示路径已绘制的部分——验证过程依赖此功能。
跳转机制——冻结精确时刻以截图
?t=N
会将主时间轴跳转到
N
秒并暂停,因此截图会定格在绘制过程中的某个确定帧。
html
<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>
验证流程——渲染→冻结→截图→检查:
  1. 在时间轴的不同时刻打开文件——开始、绘制中、结束:
    …/scribe.html?t=0
    ,
    ?t=<dur/2>
    ,
    ?t=<dur>
    。从控制台读取
    tl.duration()
    获取结束时间。至少选择一个描边绘制中
    t
    值——这是验证手与端点贴合效果的关键。
  2. 无头模式截取每个冻结帧的截图:
    bash
    npx playwright screenshot --wait-for-timeout=500 "file://$PWD/scribe.html?t=1.2" frame-mid.png
  3. 检查每张截图——验证保真度:在描边绘制中的
    t
    时刻,手的笔尖精准位于已绘制部分的前端(不超前、不滞后、不漂浮);路径仅绘制到端点处;在
    ?t=<dur>
    时刻,所有描边完整绘制完成,无间隙、无过绘制、无残留虚线。检查瑕疵:描边之间手在空白白板上滑动、笔尖偏移错误(笔尖脱离线条)、描边绘制顺序错误、填充内容在轮廓之前出现、绘制内容被裁剪/超出画布、字体加载前出现FOUC(无样式内容闪烁)。
  4. 迭代调整:修改
    NIB
    、描边顺序、时长,重新截图直到每个冻结帧都呈现出手正在绘制的效果。
完成前检查:
  1. 可在浏览器中独立打开——无控制台错误,无缺失的CDN/素材。
  2. 单个主时间轴;
    ?t=N
    可正确冻结,且
    strokeDashoffset
    与手的位置共享同一个进度值。
  3. 在开始/描边绘制中/结束时刻均已截图——全程手的笔尖贴合端点,结束时所有描边完整,绘制顺序正确。
  4. 描边之间手抬起;不在空白白板上滑动。
  5. prefers-reduced-motion
    模式下显示完整白板(所有描边已绘制,手隐藏),不播放绘制动画。
如需导出带旁白、帧确定的视频,可将相同的描边绘制逻辑(通过
useCurrentFrame()/fps
驱动
strokeDashoffset
和手的变换)移植到Remotion合成项目中,渲染为MP4/GIF格式——详见explainer-video技能的渲染截图→编码流程。

Reference files

参考文件

  • references/draw-on-recipes.md
    — runnable draw-on mechanics:
    getTotalLength
    /
    getPointAtLength
    hand-follows-path, GSAP
    DrawSVGPlugin
    and
    MotionPathPlugin
    variants, multi-stroke staggering, per-glyph handwriting, fill-after-outline, erase/wipe and reverse-draw transitions, and the Remotion frame-driven port.
  • references/whiteboard-pipeline.md
    — 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/draw-on-recipes.md
    ——可运行的描边绘制机制:
    getTotalLength
    /
    getPointAtLength
    手跟随路径、GSAP
    DrawSVGPlugin
    MotionPathPlugin
    变体、多描边错开时机、逐字形手写、轮廓后填充、擦除/反向绘制过渡,以及Remotion帧驱动移植方案。
  • references/whiteboard-pipeline.md
    ——将素材转换为可绘制的单条描边SVG、绘制顺序创作、手写/描边字体、Marker手素材与笔尖校准、绘制时长与旁白同步、从分镜到白板的制作流程。