中断与重定向:带速度接管
真实交互里,一段动画几乎总是还没播完就被新目标打断:列表滚到一半又被甩一下、卡片归位途中又被拖走。
幼稚的做法是丢弃旧动画、从当前位置重新开始一段——位置虽连续,速度却被清零,画面上是一个生硬的顿挫。正确的做法是读出打断时刻的当前速度,让新的弹簧带着这个初速度继续演化。
1 · 速度曲线上的跳变
位置曲线在两种策略下都连续,看不出差别;分野全在速度曲线:接管时它平滑过渡,不接管时它竖直跳到 0 再重新爬升。
2 · 为什么弹簧做得到
一条 duration 加 easing 的曲线把初速度写死成起点的切线斜率,无法在中途接一个任意值。弹簧是解析解,createSpring 的 velocity 参数就是初始条件里的初速度;打断时用
spring.velocity(t) 读出当前速度,作为新弹簧的初速度即可。
注 · 接管之后运动可能先朝远离新目标的方向继续走一小段——旧速度足够大时,弹簧要先把这份动量耗掉才能掉头。这看上去像是「没有立刻响应」,实际正是物理正确的表现;真实世界里甩出去的东西也不会因为改了主意就瞬间转向。想要更快掉头,该调的是 stiffness 与
damping,不是砍掉速度。
警示 · 接管的代价是失去可 seek 性。每次 retarget 都把历史写进了新弹簧的初始条件,sampleAt 与实际播放过的画面就此分家——播放控制与文字变形两页各从另一侧记过同一件事。可 seek
与可无缝打断在这套引擎里是一对需要取舍的性质。
import { createSpring } from '@vega/anim';
function retarget(to) {
const el = now() - t0;
const pos = spring.value(el); // 当前所在
const vel = spring.velocity(el); // 当前速度 (关键)
spring = createSpring({
stiffness, damping,
from: pos, // 位置永远连续
to,
velocity: handoff ? vel : 0, // 接管 = 带着旧速度继续
});
t0 = now();
}
本页的初速度来自上一段动画;若来自输入设备(拖拽松手的甩动速度),就是高级交互的拖拽惯性。弹簧接住速度的物理本身见弹簧;把同一套接管做成声明式接口见飞行中重定向。