跳到主要内容

路径动画与描边

路径上的「动画」其实是三类不同机制:用 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", ...),便于暂停和命中。

mpathhref 指向同文档 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 若在别的 &lt;g> 变换下,要先把点乘到同一空间,或让 target 成为 path 的兄弟。不要把 getPointAtLength 的结果当 client 坐标去对 overlay。

切线:取 tt + ε 两点做 atan2ε 在端点处要回退,否则退化。

getTotalLength() / getPointAtLength() 在超长、含弧的 path 上不便宜。长度缓存;动画中只取样。d 变了必须使缓存失效。

5. dasharray 不是补间框架

你想做的不要用 dash 去改用
节点从 A 移到 B假虚线「走」过去transform / WAAPI
形状 A morph 成 B对两套 dash 插值换 path / 自研插值 / Canvas
颜色、透明度dashfill / 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

权威资料

核对日期:2026-08-26