算法与数据结构 / 动画引擎原理 · 从插值到播放控制 / 中断与重定向:带速度接管 待审核 25 / 47
interrupt · velocity handoff · redirect

中断与重定向:带速度接管

真实交互里,一段动画几乎总是还没播完就被新目标打断:列表滚到一半又被甩一下、卡片归位途中又被拖走。

幼稚的做法是丢弃旧动画、从当前位置重新开始一段——位置虽连续,速度却被清零,画面上是一个生硬的顿挫。正确的做法是读出打断时刻的当前速度,让新的弹簧带着这个初速度继续演化。

1 · 速度曲线上的跳变

位置曲线在两种策略下都连续,看不出差别;分野全在速度曲线:接管时它平滑过渡,不接管时它竖直跳到 0 再重新爬升。

图 1-1 · 位置与速度随时间,红虚线标出每次中断的时刻。可关掉接管开关,对比同一次中断处速度曲线是平滑过渡还是竖直跳变。

2 · 为什么弹簧做得到

一条 durationeasing 的曲线把初速度写死成起点的切线斜率,无法在中途接一个任意值。弹簧是解析解createSpringvelocity 参数就是初始条件里的初速度;打断时用 spring.velocity(t) 读出当前速度,作为新弹簧的初速度即可。

新弹簧=createSpring(from=x(t), to=新目标, velocity=x˙(t))\text{新弹簧} = \text{createSpring}\big(\text{from} = x(t),\ \text{to} = \text{新目标},\ \text{velocity} = \dot{x}(t)\big)

注 · 接管之后运动可能先朝远离新目标的方向继续走一小段——旧速度足够大时,弹簧要先把这份动量耗掉才能掉头。这看上去像是「没有立刻响应」,实际正是物理正确的表现;真实世界里甩出去的东西也不会因为改了主意就瞬间转向。想要更快掉头,该调的是 stiffnessdamping,不是砍掉速度。

警示 · 接管的代价是失去可 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();
}

本页的初速度来自上一段动画;若来自输入设备(拖拽松手的甩动速度),就是高级交互的拖拽惯性。弹簧接住速度的物理本身见弹簧;把同一套接管做成声明式接口见飞行中重定向