向量弹簧:二维联动与链式跟随
标量弹簧只解一个维度上的位移。要让一个点在平面上平滑跟随指针,把它推广成向量弹簧:x 与 y 各跑一个 createSpring,两条弹簧共享同一组 stiffness / damping / mass。
参数共享不是实现上的偷懒,而是这个推广成立的前提。两轴用同一个
与同一个
,收敛节奏才一致,合成出的轨迹是一条协调的曲线;若给 x、y 不同参数,物体会先在一个方向到位、再在另一个方向慢慢补齐,轨迹被拉成折线,看上去歪斜。
1 · 链式跟随
在向量弹簧之上做链式跟随(trailing)只需一句话:第
个节点的目标是第
个节点此刻的位置,头节点的目标是指针。每帧先把头节点 setTarget 到最新指针坐标,再从头到尾依次把前一节点的当前坐标交给后一节点。弹簧的滞后逐级累积,就得到果冻链般的拖尾波浪。
2 · setTarget 的接管语义
createSpring 求解一次就固定了初始条件,改目标只能重建。setTarget 因此做三件事:取当前时刻的位置与速度,以它们为初始条件建一条指向新目标的弹簧,把计时原点挪到此刻。位置与速度都被承接,所以指针连续移动时看不出每帧都在换弹簧。
注 · 这个 lab 每帧对每个节点重建两条弹簧,24 节点就是每秒约 2900 次 createSpring。之所以不心疼,是因为 createSpring 的主体只是算
、
与两三个系数,闭式解不需要预积分。真正贵的是它顺带扫出的 settleTime——那是一个 4 ms 步长、上限 10 s 的循环。链式跟随用不上 settleTime,这笔开销纯属浪费,但在这个规模下仍看不出掉帧。
import { createSpring } from '@vega/anim';
// 向量弹簧: x、y 各一个标量弹簧, 共享同一组参数
function vectorSpring(x0, y0, opts) {
let sx = createSpring({ ...opts, from: x0, to: x0 });
let sy = createSpring({ ...opts, from: y0, to: y0 });
let t0 = performance.now();
return {
setTarget(now, x, y) { // 接住当前位置 + 速度
const dt = now - t0;
sx = createSpring({ ...opts, from: sx.value(dt), to: x, velocity: sx.velocity(dt) });
sy = createSpring({ ...opts, from: sy.value(dt), to: y, velocity: sy.velocity(dt) });
t0 = now;
},
sample(now) { const dt = now - t0; return { x: sx.value(dt), y: sy.value(dt) }; },
};
}
// 链式跟随: 每帧头节点追指针, 其余节点追前一个节点的当前位置
function frame(now) {
nodes[0].setTarget(now, cursor.x, cursor.y);
for (let i = 1; i < nodes.length; i++) {
const prev = nodes[i - 1].sample(now); // to = 前一节点此刻坐标
nodes[i].setTarget(now, prev.x, prev.y);
}
for (const n of nodes) draw(n.sample(now));
requestAnimationFrame(frame);
}
单轴弹簧的三种物理形态见弹簧;本页「每帧重建并承接速度」的做法,是中断与重定向那套接管逻辑的连续版本;把指针换成真实手势的跟手与抛掷见手势。