形状形变:path 的 d 插值
两条 path 之间的平滑形变,本质仍是插值——只不过插的不是一个数,而是 d 属性里的一长串坐标。前提很硬:两条 path 的命令结构必须相同,命令类型与数量一一对应。满足这一点后,d 就退化成一个等长数字数组,与数字插值完全同构,只是维度从
1 变成几十。
1 · 结构对齐时的逐位插值
本页第一组的四个形状刻意都写成「1 个 M 加 8 个 C」,数组长度恒为 50,顶点天然一一对应,任意两者之间都能直接形变。圆用八段三次贝塞尔逼近,控制点长度取经典的 ;方、星、心则先给出八个顶点,再把每条边按三等分点转成 C 段——视觉上仍是折线,但命令结构与圆一致。
2 · 结构不同时的重采样
真实图标很少天生对齐:三角形 3 段直线、十字 12 段、blob 几段曲线,数组长度不同便无法逐位插值。通用做法是重采样——用 getTotalLength() 与 getPointAtLength() 把每个形状按弧长均匀采成同样多的点,任意 path 都化成等长点集。
代价是曲线被离散成折线:采样点越多越平滑,越少越棱角分明。这也是为什么这一组的 blob 在低采样数下看起来像多边形。
3 · 旋转对齐
采样点的编号从各自路径的起点开始,而两个形状的起点在几何上未必对应。直接逐点插值会让整个形状在形变途中拧转一圈。做法是把目标点集循环移位,取与源点集逐点平方距离之和最小的那个位移:
注 · 这是一个 的暴力搜索: 时是 4096 次距离计算,每次形变开始时跑一遍。之所以没优化,是因为它只在建立动画时算一次,逐帧路径上完全不涉及。真要提速,这个式子展开后是一个循环互相关,可以用 FFT 降到 ——但那要写三倍的代码去省一次 4096 次浮点乘法,在这个规模上不划算。
警示 · 循环移位只解决「起点错位」,解决不了走向相反。若两个形状的采样方向一个顺时针一个逆时针,任何移位都对不齐,形变会整体翻面。稳妥的做法是先按有向面积判断绕向、必要时把其中一组点反转,再做移位对齐。本页的四个形状绕向一致,所以省掉了这一步。更复杂的拓扑(孔洞、一个形状分裂成多个)则要拆分子路径并配对,flubber 正是做这件事的库。
const lerpArr = (a, b, g) => a.map((v, i) => v + (b[i] - v) * g);
const toD = (arr) => {
let s = `M ${arr[0]} ${arr[1]}`;
for (let i = 2; i < arr.length; i += 6)
s += ` C ${arr.slice(i, i + 6).join(' ')}`;
return s;
};
new Animation({
keyframes: [0, 1],
duration: 900,
ease: 'easeInOut',
onUpdate: (g) => {
const d = lerpArr(fromArr, toArr, g);
path.setAttribute('d', toD(d));
},
});
只想画出单条路径本身、或让物体沿路径行进,见 SVG 描边与沿路径运动。形变把整段 d 当作高维向量来插,与基础插值同源,区别只在于需要一个「path 插值器」——逐位混合数字再拼回
d。