高级交互:拖拽惯性、滚动驱动、FLIP
前面的要素组合起来就能支撑三类常见的交互动画。它们都不引入新机制,只是把输入接到动画值上,或把播放头接到别的来源上。
1 · 拖拽与惯性
松手瞬间用最近一小段时间窗的位移估出速度 ,作为弹簧的初速度接管,于是顺势抛出并在边界回弹。
注 · 速度估计的时间窗长度是一处隐含取舍。窗口太短,最后一两个 pointer 事件的抖动会被放大成一个荒谬的初速度;太长则跟不上急停——用户明明已经停住手指,松开却仍飞出去。这也是「甩一下再停住」这个手势在很多实现里表现不一致的原因。
2 · 滚动驱动
进度不再由时间流动,而是由 scrollTop 在区间内归一化得到——滚动条就是动画的播放头。这一步之所以成立,正因为播放头可被任意 seek 而无需重放历史。
3 · FLIP
直接对 width 做动画会逐帧重排。FLIP 换个次序:先量变更前位置(First),让 DOM 直接跳到终态并量新位置(Last),用一个 transform 把它反推回看起来没变(Invert),再把 transform 动画回 identity(Play)。整个过渡只动 transform,不触发重排。
警示 · Invert 那一步必须同步写上,不能等到动画的首帧。Animation 的第一次 onUpdate 落在下一个 requestAnimationFrame 里,而布局变更已经在当前任务内生效——中间隔着一次绘制,终态会先闪现一帧。本页的 lab 因此在建立
Animation 之前就把 invertTransform(delta, 1) 直接写进 style.transform。
被点的方块除了尺寸还要换颜色,而 class 切换会让背景色立刻跳到目标值。做法是在 mutate 之后立刻把 backgroundColor 写回旧色,再由 onUpdate 用 mixColor 逐帧推进,onComplete 时清空内联样式交还给 CSS。
import { drag, onScroll, invert, invertTransform, Animation } from '@vega/anim';
// 拖拽惯性: 松手用估出的速度做弹簧初速度, 越界回弹
drag(box, { axis: 'x', min: -120, max: 120, stiffness: 500, damping: 32 });
// 滚动驱动: 播放头 = scrollTop 在 [start, end] 内归一化
onScroll((p) => { el.style.transform = `rotate(${p * 360}deg)`; },
{ container: scroller, start: 0, end });
// FLIP: First → Last → Invert → Play, 全程只动 transform
const first = el.getBoundingClientRect();
mutateLayout(); // 直接跳到终态
const d = invert(first, el.getBoundingClientRect());
el.style.transform = invertTransform(d, 1); // 反推回原位 (须同步)
new Animation({ keyframes: [1, 0], duration: 420, ease: 'easeOut',
onUpdate: (f) => (el.style.transform = invertTransform(d, f)) });
拖拽惯性的物理见弹簧与惯性衰减;真正的中途速度接管见中断与重定向;归一化映射见插值的 interpolate(input, output)。为什么只动 transform 就不重排,见合成器。