算法与数据结构 / 动画引擎原理 · 从插值到播放控制 / 关键帧与编排:途经点、时间轴、交错 待审核 11 / 47
keyframes · timeline · stagger

关键帧与编排:途经点、时间轴、交错

一段动画最小的形态是一对 fromto。把它往三个方向推广,就得到编排所需的全部要素:值可以是一串而不止两个(关键帧),动画可以是多段而不止一段(时间线),元素可以是一组而不止一个(交错)。三者互不重叠,组合起来足以描述绝大多数界面动效。

1 · 关键帧与 times

keyframes: [0, 0.9, 0.3, 1] 给出四个途经值,引擎在相邻两值之间分段插值。真正让关键帧好用的是 times:它另给一串 [0,1][0,1] 上的位置,指定每个值出现在整段进度的哪一处。

值与位置就此解耦。同一串途经值配不同的 times,节奏完全不同:把 times 写成 [0, 0.2, 0.8, 1],中间那段占了 60% 的时间却只从 0.9 挪到 0.3,看上去像是在慢悠悠地折返;而首尾两段各只有 20% 的时间要走完 0.9 的位移,快得多。

图 1-1 · 途经 0 → 0.9 → 0.3 → 1 四个值、位置为 0 / 0.2 / 0.8 / 1 的关键帧动画,右侧折线是值随进度的曲线。

2 · 交错

stagger(each, { from }) 返回一个 (index,total)delay(\text{index}, \text{total}) \to \text{delay} 的函数,不产生动画,只批量算延迟。延迟正比于该元素到发起点的距离:delay=ratiomaxDistanceeach\text{delay} = \text{ratio} \cdot \text{maxDistance} \cdot \text{each},其中 ratio 是归一化距离,可再过一层 ease 改变疏密。

图 2-1 · 同一段「跳一下」动画配递增延迟。可调 each 与发起点 from,观察波从首、尾或中心发起。

注 · from: 'center' 在偶数个元素时发起点落在 (total1)/2(\text{total}-1)/2 这个半整数上,中间两个元素到它的距离相同、延迟相同,波看上去是从两个格子之间发出的。奇数个则有一个明确的波心。图 2-1 用 9 个点正是为此。

3 · 时间线

timeline 把多段动画排到一条轴上,每段的 at 决定它的起点:省略表示接在上一段结束处,'<' 表示与上一段同时起,'+=100' 表示上一段结束后再等 100 ms,也可以直接给绝对毫秒数。排布在建立时一次算定,entries 里每段都带上算好的 startend

关键在于时间线本身也是一个动画,它的「值」就是全局时间。因此 play / pause / seek / reverse 这些能力不需要为编排单独实现一遍——把主时钟的值分发给各段,各段用 time - start 取自己的局部时间即可。

图 3-1 · 三段动画排在一条轴上:B 在 A 结束后晚 100 ms 起,C 与 B 同时起。下方泳道是引擎算出的布局,红线是播放头。
import { Animation, timeline, stagger, animate } from '@vega/anim';

// 关键帧 + times: 一串途经值, times 指定各值出现的进度
new Animation({
  keyframes: [0, 0.9, 0.3, 1],
  times:     [0, 0.2, 0.8, 1], // 0.2→0.8 占 60% 时间, 只缓行一小段
  duration: 2600, ease: 'linear',
});

// 交错: stagger 返回 (index, total) → delay
const delayFor = stagger(80, { from: 'center' });
dots.forEach((d, i) =>
  animate(d, { y: [0, -26, 0] }, { duration: 520, delay: delayFor(i, N) }));

// 时间线: 各段排在一条轴上, at 控制相对位置
timeline([
  { animation: a },
  { animation: b, at: '+=100' }, // A 结束后晚 100ms 起
  { animation: c, at: '<' },     // 与 B 同时起
]);

每个途经值之间怎么插出来见插值,每段的快慢由缓动塑形。序列、运行时依赖与二维交错见编排进阶;整条时间线如何被一个可反向的游标驱动见播放控制