弹簧:没有时长的动画
缓动的契约是「预先规定
毫秒走完一条固定曲线」。弹簧(spring)换了一套契约:给定刚度 stiffness、阻尼 damping、质量 mass 与初速度,让系统自己演化到静止。时长不再是输入,而是这组参数的结果——引擎反推出一个 settleTime 告诉调用方「这次大概要跑多久」。
数学上它是阻尼谐振子方程
的解析解。引擎不做数值积分,而是按阻尼比分情况代入闭式解,因此 value(t) 对任意
都是
求值,可以随意 seek,不必从头逐帧推进。
1 · 三个参数与三种形态
行为完全由一个无量纲数决定——阻尼比(damping ratio)
是欠阻尼:振荡着收敛,会过冲超过目标,弹性感即由此而来。 是临界阻尼:不振荡且最快归位。 是过阻尼:两个实指数叠加,缓慢爬向目标,看上去有点迟钝。
注 · 三个参数只有两个自由度。曲线完全由
与
确定,而把
、、
同倍放大等于给方程两边同乘一个常数,两者都不变。实跑对照过:(120, 12, 1) 与 (240, 24, 2) 在任意时刻的位移逐位相同,settleTime 也同为 996 ms。调 mass 而不动另外两个才会改变行为。
settleTime 的求法与 value 不同:位移有闭式解,稳定时刻却没有——引擎以 4 ms 步长从 0 扫到 10 s,取第一个「位移与速度都进入静止阈值」的时刻。所以它的分辨率是 4 ms,且有上限。滑块量程内就能撞到这个上限:stiffness = 20、damping = 60、mass = 4(,重度过阻尼)时 settleTime 报 10000 ms,但那一刻位移只走到 0.966,真正静止要到 20 s 之后。这类参数下读数是循环的天花板,不是物理结果。
2 · 承接初速度
固定曲线的缓动从静止出发,起点速度由曲线形状写死。弹簧不同:初速度是求解时的一个初始条件,可以是任意值,包括与目标方向相反。这就是手势松手后惯性的来源——把指针的瞬时速度直接交给弹簧,运动天然连续,看不出接管的痕迹。
警示 · springEasing 把弹簧化成一对 { easing, duration } 塞进通用动画管线,这一步会把初速度归零。一条可复用的
曲线携带不了一次性的发射速度——它被采样成纯函数的那一刻,初始条件就丢了。需要真正承接中途速度,得直接用 createSpring 并自行驱动,见中断与重定向。
import { createSpring, springEasing } from '@vega/anim';
// 物理求解: 给定参数, 算任意时刻的值 / 速度 / 是否静止
const s = createSpring({ stiffness: 120, damping: 12, mass: 1,
from: 0, to: 1, velocity: 0 });
s.value(120); // 120ms 时的位移
s.settleTime; // 自然稳定耗时 (毫秒) —— 不是给定的
// 化成 duration + easing, 即可塞进通用动画管线 (初速度在此丢失)
const { easing, duration } = springEasing({ stiffness: 120, damping: 12 });
同样没有内建时长、但连目标都没有的另一类物理,见惯性衰减;把标量弹簧推广到二维与链式跟随见向量弹簧;预设曲线那一套对照见缓动。