惯性衰减:没有目标的滑行
弹簧有一个明确的目标
to,系统被它吸引,欠阻尼时还会绕着它振荡。惯性衰减(decay)恰好相反:它没有目标,只有一个初速度和摩擦。物体顺着方向滑行、速度指数衰减、自己减速到停,落在哪里完全由初速度决定。松手后的动量滚动、惯性列表、地图甩动,底层都是这个模型。
位移的闭式解是
是振幅(amplitude),也就是总滑行距离;
是 timeConstant,越大滑得越久。落点是初速度的结果而非输入——这是 decay 与弹簧最根本的差别。
1 · 位置与速度两条曲线
settleTime 由「与落点之差小于 restDelta」反解得到
,是少见的有闭式解的稳定时刻——弹簧就只能步进扫出来。默认 power = 0.8、、restDelta = 0.5、
时
,代入得 2916.6 ms,与引擎返回值逐位相符。
2 · power 与 τ 不是两个独立的旋钮
power 与
看上去分管「滑多远」与「滑多久」,但它们同时决定了运动的起始速度。对
求导得
于是
与滑块上标的
只在 power 恰好等于
(换算成秒)时相等。默认参数下
,实际起始速度是标称值的 2.29 倍:
时物体真正以 5943 px/s 起步。
警示 · 本页原先照抄模型文档写的是
。对着 decay.ts 实跑才发现这一条只在
时成立:velocity() 返回的是 value() 的严格导数(数值微分逐位对得上),而 value() 的振幅来自
,两者衔接处并没有约束
。把 power 调到 0.35 再看,起始速度才回到 2600。这不是实现的 bug——Popmotion 的 inertia 也是这么定义的,power 与 velocity 本就是一对经验参数——但把它写成「速度按
衰减」会误导调参。
3 · 带惯性的吸附
modifyTarget 在算出理论落点之后、开始运动之前插一手:把落点对齐到最近的刻度,再反推振幅使
恰好停在修正后的位置。甩动的手感全部保留(起始速度仍由初速度决定),落点却精确——轮播与滚动选择器正是这么做的。
import { createDecay } from '@vega/anim';
// 没有目标值, 只给初速度 + 摩擦参数; 落点是 power·velocity 的结果
const d = createDecay({
velocity: 2600, // px/s, 由手势松手时的瞬时速度提供
power: 0.8, // 落点放大: amplitude = power · velocity
timeConstant: 350, // 位移衰减的时间常数 (ms), 越大滑越久
from: 0,
// 可选: 把理论落点对齐到最近刻度 → 带惯性的吸附
// modifyTarget: (ideal) => Math.round(ideal / 500) * 500,
});
d.target; // 理论落点 (t→∞), 与初速度成正比
d.settleTime; // 减速到停的有效时长 (ms)
// rAF 驱动: 每帧用经过的毫秒数采样位置
const t0 = performance.now();
function tick() {
const pos = d.value(performance.now() - t0);
ball.style.transform = `translateX(${pos}px)`;
if (!d.done(performance.now() - t0)) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
decay 最常见的入口是手势:松手时把指针的瞬时速度交给它,见手势速度与拖拽与惯性。越界后被弹回边界的那一档,见橡皮筋。