路径动画与描边
路径上的「动画」其实是三类不同机制:用 stroke-dashoffset 揭示描边、用 animateMotion 把节点钉在 path 上、用 getPointAtLength 在 JS 里取样。它们都不是通用补间框架,不要拿 dasharray 去模拟位移、颜色、形变。
vector-effect 生产只用 non-scaling-stroke;其它值在 SVG 2 仍 at-risk,见 差异清单。
1. 适用 / 不适用
适用: 进度环、路线揭示、沿轨迹的标记、仪表指针沿弧。path 条数可控,长度可缓存。
不适用: 把任意场景图做成「dasharray 时间轴」;用 dash 补间去 morph d;每帧对未缓存的超长 path 调 getTotalLength()。
2. stroke-dashoffset 揭示
原理:stroke-dasharray 设成 path 全长,stroke-dashoffset 从全长收到 0,看起来像笔画长出来。这是 描边本身 在动,fill 不会跟着长出来。
export function cachePathLength(path: SVGGeometryElement): number {
const existing = path.getAttribute("pathLength");
if (existing) {
const n = Number(existing);
if (Number.isFinite(n) && n > 0) return n;
}
const len = path.getTotalLength();
path.setAttribute("pathLength", String(len));
return len;
}
export function setStrokeProgress(
path: SVGGeometryElement,
t: number,
): void {
const len = cachePathLength(path);
const p = Math.min(1, Math.max(0, t));
path.style.strokeDasharray = `${len}`;
path.style.strokeDashoffset = `${len * (1 - p)}`;
}
pathLength 把「作者单位长度」钉死,避免字体 / 近似弧在引擎间 getTotalLength() 对不齐。进度环用弧 path + 这个函数,不要每帧重建 d。
自包含文件里同一效果用 SMIL 动 stroke-dashoffset,见 SMIL。应用内用 WAAPI 动该 CSS 属性即可。
闭合 path 的 dash 衔接、linecap 圆头会在 0/1 附近「多出一截」。进度类 UI 用 butt,并夹紧 t 不要到视觉溢出。
3. animateMotion:无 JS 跟点
<path id="rail" d="M10 50 C 40 10, 80 90, 110 50" fill="none" stroke="#ccc"/>
<circle r="4" fill="crimson">
<animateMotion dur="2s" repeatCount="indefinite" rotate="auto">
<mpath href="#rail"/>
</animateMotion>
</circle>
rotate="auto" 让物体切线转向。这是自包含 .svg 的强项:<img> 里也能跑。应用 UI 里同等需求用下一节的取样 + setAttribute("transform", ...),便于暂停和命中。
mpath 的 href 指向同文档 path。不要从用户文档里留下可指向外部的 motion 轨道。
4. getPointAtLength 跟点
export function pointAt(
path: SVGGeometryElement,
t: number,
): DOMPoint {
const len = cachePathLength(path);
const p = Math.min(1, Math.max(0, t));
return path.getPointAtLength(len * p);
}
export function attachToPath(
target: SVGGraphicsElement,
path: SVGGeometryElement,
t: number,
): void {
const pt = pointAt(path, t);
target.setAttribute("transform", `translate(${pt.x} ${pt.y})`);
}
点在 该 path 的局部用户空间。target 若在别的 <g> 变换下,要先把点乘到同一空间,或让 target 成为 path 的兄弟。不要把 getPointAtLength 的结果当 client 坐标去对 overlay。
切线:取 t 与 t + ε 两点做 atan2。ε 在端点处要回退,否则退化。
getTotalLength() / getPointAtLength() 在超长、含弧的 path 上不便宜。长度缓存;动画中只取样。d 变了必须使缓存失效。
5. dasharray 不是补间框架
| 你想做的 | 不要用 dash 去 | 改用 |
|---|---|---|
| 节点从 A 移到 B | 假虚线「走」过去 | transform / WAAPI |
| 形状 A morph 成 B | 对两套 dash 插值 | 换 path / 自研插值 / Canvas |
| 颜色、透明度 | dash | fill / opacity |
| 相机缩放 | 改 dash 冒充远近 | 改 viewBox |
non-scaling-stroke 让线宽不跟 CTM 放大。相机缩放时描边要保持 1 CSS 像素,用它;不要靠每帧改 stroke-width。不要用 non-scaling-size 等 at-risk 值。
6. 失败形态
| 症状 | 原因 |
|---|---|
| 揭示到不了 100% 或过头 | 长度和 pathLength 不一致,或 round linecap |
| 跟点的标记漂在旁边 | 取样空间和 target 的 CTM 不是同一局部空间 |
| 一缩放线宽爆炸 | 没 non-scaling-stroke,或用 CSS scale 当相机 |
| 滚动进度卡主线程 | 每帧 getTotalLength() 还重建 d |
<img> 里跟点不动 | 把跟点写成了 JS,没有 animateMotion |
权威资料
- SVG 2 — Paths,getTotalLength / getPointAtLength
- SVG 1.1 — animateMotion
- vector-effect(仅 non-scaling-stroke 生产可用)
- MDN — stroke-dashoffset
核对日期:2026-08-26