沿路径运动:取点与切线朝向
让一个物体沿任意曲线行进,不必解析这条曲线的方程。SVG 的 path 自带按弧长取点的能力:getTotalLength() 给出总长,getPointAtLength(d) 给出距起点
处的坐标。动画只需把进度
映射成距离
,逐帧取点。CSS 的 offset-path 底层做的是同一件事。
1 · 取点与朝向
要让物体朝向前进方向,在当前位置两侧各取一点,用 atan2 求这两点连线的角度即可——路径上足够近的两点,连线方向就是切线方向。
2 · 弧长参数化的意义
贝塞尔曲线的参数 与弧长不成正比。控制点稀疏的一段, 每走一小步对应的实际距离更长;密集处则更短。直接拿 驱动物体,位置仍在曲线上,但沿路速度会忽快忽慢,且这种快慢与选定的缓动毫无关系——它来自曲线的参数化,不来自动画。
getPointAtLength 以弧长为度量,「进度」就是已走过的真实长度比例。沿路速度这才与缓动曲线一致:选 linear 是匀速,选 easeInOut 是两端慢中间快,而不是叠加上一层曲线自身的畸变。
注 · 浏览器实现
getPointAtLength
也不是解析求解,而是把曲线细分成足够多的直线段后累加长度、再查表反解——本质是数值方法。这意味着它有精度上限,也意味着调用不便宜:本页每帧调三次(当前点加两侧各一点),路径复杂时这一项会成为逐帧开销的大头。真要压成本,可以在建立动画时预采样一张「弧长到坐标」的表,逐帧只做查表与线性插值。
切线角用差分近似而非解析导数,取的邻域是
像素。这个值不能太小——getPointAtLength 的数值误差会让两点几乎重合、角度抖动;也不能太大——拐弯半径小的地方,割线方向会明显偏离切线。
const L = road.getTotalLength();
new Animation({
keyframes: [0, 1],
duration: 2600,
ease: 'linear',
onUpdate: (g) => {
const d = g * L;
const p = road.getPointAtLength(d);
const a = road.getPointAtLength(d - 1);
const b = road.getPointAtLength(d + 1);
const deg = Math.atan2(b.y - a.y, b.x - a.x) * 180 / Math.PI;
mover.setAttribute('transform', `translate(${p.x} ${p.y}) rotate(${deg})`);
},
});
本页动的是物体的位置与姿态;让路径本身像被笔画出来,见 SVG 描边。沿路进度同样是一次插值,被缓动塑形后再换算成弧长。经典动画原则里的弧线是它的手工简化版——两个分量各走各的曲线,不必真有一条 path。