编排进阶:序列、依赖、二维交错
多段动画凑在一起,问题从「一段怎么动」变成「彼此何时动」。三种手法各有适用面,差别不在效果而在布局什么时候确定:序列在建立时一次算定,依赖式串接推迟到每段跑完才决定,二维交错则把空间关系折算成时间关系。
1 · 序列
timeline 的 at 省略时,每段从上一段结束处起步,于是 A、B、C 首尾相接。这与「三段同时播放」的区别是结构性的:同时播放把各段并排在轴的同一起点,总时长取最长一段;序列的总时长是各段之和,任意时刻只有一段在推进。两者都由一个主时钟分发全局时间,差别只在每段的
start 落在哪里。
2 · 依赖式串接
序列要求各段时长在排布时已知。若下一段的参数或时长得等上一段跑完才确定——比如要根据接口返回的数据决定动多远——就没法预先排布,改用事件式串接:给每段一个 onComplete,在回调里启动后继段。
代价是失去了整体控制。序列拿到的是一个主动画,seek 与 reverse 免费继承;依赖链只是一串各自独立的动画,没有全局时间轴可言,也就无从 seek。取舍很清楚:布局的确定性换运行时的灵活性。
注 · 衔接停顿写在后继段的 delay 上,而不是回调里包一层 setTimeout,为的是让停顿也归这段动画自己管。图 2-1 的「重置」按钮对每段调 cancel() 就能全部停住,包括那段还停在 delay 里、一帧都没渲染过的——若停顿另起一个定时器,就得额外记住 timer
id 并单独清掉,漏一个就会在重置之后突然冒出一段动画。
3 · 二维交错
一维 stagger 的延迟取决于元素到发起点的索引距离。推广到网格不需要新 API:把每格
先折算成一条空间轴上的一个位置,再把这个位置当作 index 交给同一个 stagger。
- 按行取 ,按列取 ,对角取
- 从中心扩散取到中心格的棋盘距离
换轴即换波形,引擎本身一行不改。这也解释了为什么 stagger 的签名是
而不是「元素列表」——它压根不关心 index 是不是数组下标。
警示 · 折算后的轴值会碰撞:从中心扩散时,同一圈上的多个格子映射到同一个轴值,因而共享同一个延迟——这正是「一圈一圈扩散」的观感来源,而不是缺陷。但也意味着格子数与延迟档数不再相等:5 × 5 的中心扩散只有 5 档延迟(棋盘距离 0 到 4),最大 delay 是
而非
。按行按列同理。调 each 时若按格子数估算总时长,会高估好几倍。
import { Animation, stagger, animate } from '@vega/anim';
// 二维交错: 把网格每格映射到一条空间轴, 再交给 stagger
const axisFor = (r, c) => Math.abs(r - cr) + Math.abs(c - cc); // 到中心的棋盘距离
const delayFor = stagger(70, { from: 'first' });
cells.forEach((cell, i) => {
const axis = axisFor(Math.floor(i / COLS), i % COLS);
animate(cell, { opacity: [0, 1], scale: [0.4, 1] }, {
duration: 420,
delay: delayFor(axis, maxAxis + 1), // axis 当作 index
ease: 'easeOut',
});
});
// 依赖串接: 用 onComplete 触发下一段, delay 作为衔接停顿
new Animation({
keyframes: [0, 1], duration: 700, delay,
onComplete: () => runSegment(idx + 1),
});
基础的 timeline 与一维 stagger 见关键帧与编排;整条编排如何被一个可暂停、可反向的游标驱动见播放控制。