深入详解 anime.js Timeline(时间轴)核心机制,系统剖析通过 offset 参数精准控制多动画串行等待、重叠交错与完全并行的实现方法,提供完整代码示例与生产避坑指南。
anime.js Timeline 详解:控制多动画顺序与并行执行
时间:2026-11-03
浏览:589
文章目录
setTimeout 或深层嵌套的 complete 回调函数去协调各元素的先后时序,代码很容易陷入时序漂移与难以维护的混乱状态。anime.js 提供的 Timeline(时间轴)机制通过单一虚拟时钟将分散的子动画纳管至统一轨道,利用简洁的 offset 参数即可实现精准的串行等待、重叠交错与同步并行。
一、单时钟编排:为何需要 Timeline
当我们要为一个卡片设计进场动效时,通常伴随着遮罩层淡入、卡片主体位移放大、内容文字逐行滑入这三个动作。直接使用多个独立的 anime() 实例会遇到一个明显问题:如果遮罩耗时 400ms,卡片就必须显式设置 delay: 400;若随后视觉要求将遮罩时长调整为 500ms,后续所有动画的 delay 都要人工重新计算累加。
此外,当用户在动画未播完时突然关闭弹窗,需要立即倒放(reverse)或直接暂停(pause)所有动作时,分散的动画实例无法实现协同同步,容易产生部分元素已停而另一部分仍在播放的画面撕裂。
anime.timeline() 从根本上解决了这一痛点。它构建了一个父级时间容器,内部子动画共享同一个全局时间进度游标。子动画的执行不再依赖系统时钟或硬编码的绝对延迟,而是挂载在时间轴的时间刻度上,支持链式组合与整体控制。
二、时间轴基础构建与链式调用
构建一个时间轴首先需要调用 anime.timeline() 创建实例,然后使用 .add() 方法按逻辑顺序挂载具体的动画描述对象。每一个 .add() 都会返回时间轴实例自身,因此可以采用链式调用。
// 引入并初始化时间轴容器
const tl = anime.timeline({
autoplay: true, // 创建后是否立即启动播放,默认为 true
loop: false, // 是否循环整个时间轴
direction: 'normal', // 播放方向:normal | reverse | alternate
defaults: { // 注入全局默认属性,后续所有子动画默认继承
duration: 600,
easing: 'easeOutQuad'
}
});
// 链式追加动画子项
tl.add({
targets: '.modal-backdrop',
opacity: [0, 1]
})
.add({
targets: '.modal-window',
translateY: [40, 0],
opacity: [0, 1]
})
.add({
targets: '.modal-content',
scale: [0.96, 1],
opacity: [0, 1]
});
在默认情况下,只要不提供第二个参数,.add() 就会采取严格的串行模式:后一个动画会精准等待前一个动画运行结束的那一毫秒才开始执行。这保证了基本逻辑的直观与可预测。
三、核心机制:Time Offsets 参数深度解析
实现动画顺序还是并行的关键,就在于 .add(parameters, [offset]) 的第二个可选参数 offset。通过改变 offset 的传入方式,我们可以自由操纵子动画在时间轨道上的起点位置。
1. 默认追加(省略 offset 或 '+=0')
若省略 offset,其语义等同于相对上一个动画结束时刻增加 0 毫秒。前序动画耗时 600ms,当前动画就在第 600ms 起跑;若前序动画时长被修改为 1000ms,当前动画自动后移至第 1000ms 触发,无需人工介入。
2. 相对正偏移('+=300')
表示在前一个动画完全结束之后,额外留出 300 毫秒的视觉空白或停顿期,再启动当前动画。这常用于节奏呼吸感的营造,避免元素紧接着闪现给用户带来仓促感。
3. 相对负偏移('-=400')—— 实现交错并行的核心
这是动效设计中最常用的手法。传入字符串 '-=400',意味着当前动画在前一个动画尚未播放完毕、还剩 400 毫秒时提前触发。此时,前一个动画的收尾帧与当前动画的起始帧在时间上形成重叠窗口,产生流畅自然的并行衔接效果。
4. 绝对时间戳数值(如 0 或 800)
若 offset 传入一个无符号的纯数字(如 0 或 1200),时间轴将其视为以整个时间轴起始时刻为基准的绝对毫秒刻度。
- 传入
0:即使它写在第三个.add()中,也会立刻与时间轴起点对齐,与第一个动画同时并行起跑。 - 传入
800:不论前面的动画执行了多长时间、添加了多少步,该动画都必定在时间轴行进到第 800ms 时精准启动。
5. 对齐符号('<' 与 '<+=100')
在 anime.js v3 中,还支持使用 '<' 符号将当前动画与上一个动画的开始时刻直接对齐。如果要让当前动画在上一个动画开始后的 100ms 启动,可以书写为 '<+=100',这为群组协同动效提供了极大便利。
'-=200'、'+=150')必须包裹在字符串引号中。如果误写成数字 -200,时间轴会因无法识别相对前缀而无法得到预期效果,甚至导致时序计算异常。
四、并行执行的两种实战形态与代码实现
在 UI 交互中,“并行执行”通常细分为两类业务形态:一种是多属性/多目标完全同拍起跑;另一种是层级交错的瀑布式(Cascade)微并行。
形态一:完全同拍并行(多元素独立缓动)
有时我们需要卡片在从下往上飞入的同时,背景色进行渐变,且文字逐渐放大。因为这三个动作适合的缓动曲线不同(位移适合 easeOutCubic,而透明度适合 linear),我们不能将其合并为一个动画,但又必须让它们同时起跑。
const heroTimeline = anime.timeline();
heroTimeline
// 动画 1:容器位移,耗时 800ms
.add({
targets: '.hero-card',
translateY: [60, 0],
duration: 800,
easing: 'easeOutBack'
})
// 动画 2:完全同拍并行(offset: 0),耗时 400ms,独立采用 linear 渐变
.add({
targets: '.hero-card',
opacity: [0, 1],
duration: 400,
easing: 'linear'
}, 0)
// 动画 3:背景光晕同拍并行(offset: 0),耗时 1000ms
.add({
targets: '.hero-glow',
scale: [0.8, 1.2],
opacity: [0, 0.6],
duration: 1000,
easing: 'easeOutQuad'
}, 0);
形态二:级联交错并行(Cascade Overlap)
在列表展开或菜单抽屉滑出时,如果等到菜单面板完全滑出之后文字才一条一条出现,会显得界面反应迟钝;但如果完全同时出现,又缺乏层次感。最佳体验是菜单滑到约 60% 时,第一批列表项就交错出现。
const menuTl = anime.timeline();
menuTl
// 菜单侧边栏滑出,持续 500ms
.add({
targets: '.drawer',
translateX: ['-100%', '0%'],
duration: 500,
easing: 'easeOutCubic'
})
// 提前 250ms(即侧边栏行进到一半时)列表项交错登场
.add({
targets: '.drawer-nav-item',
translateX: [-20, 0],
opacity: [0, 1],
delay: anime.stagger(60), // 列表子元素逐个微延迟
duration: 350,
easing: 'easeOutQuad'
}, '-=250');
五、属性继承与局部覆写规则
anime.timeline({ defaults }) 的继承机制是保持大型动效风格统一的重要保障。然而理解其作用边界十分关键:
- 可继承属性:
duration、delay、easing、round等补间计算参数。当子动画没有单独指定时,自动回退到defaults的值。 - 局部覆写:子动画对象中声明的同名属性具有最高优先级。例如默认
duration: 600,但某个装饰线动画显式传入duration: 200,则该子动画以 200ms 运行,不影响后续其他子动画。 - 时间轴专属属性(不被子项继承):
autoplay、loop、direction属于时间轴全局控制参数,子动画无法也不应单独声明这些字段。
六、全局播放控制与时间游标联动
时间轴的一个显著优势,在于把全部子动画收归为一个拥有统一播放控制 API 的对象。无论是正向播放、暂停还是倒放,均作用于整个时间轴全局。
| 控制方法 | 内部时钟行为 | 典型应用场景 |
|---|---|---|
tl.play() |
从当前游标位置沿既定方向持续推进时间 | 点击触发、显式播放 |
tl.pause() |
冻结当前游标位置,停止 requestAnimationFrame 调度 |
鼠标悬停、页面失焦、弹窗中断 |
tl.restart() |
将时间游标强制重置到 0ms 并重新开始推进 |
重置按钮、重新演示动效 |
tl.reverse() |
反转时间推进方向,从当前刻度向反方向递减 | 折叠面板收起、弹窗关闭过渡 |
tl.seek(targetMs) |
解除时间流逝,将游标瞬间跳转至指定毫秒数并重绘画面 | 滚动条驱动(Scroll-tied)、滑块拖拽预览 |
// 结合 HTML Range 滑块实时擦碰(Scrubbing)动画
const slider = document.querySelector('#progress-slider');
slider.addEventListener('input', () => {
// 将 0-100 的滑块百分比转换为时间轴对应的毫秒刻度
const targetTime = (slider.value / 100) * tl.duration;
tl.seek(targetTime);
});
七、实践中的常见陷阱与注意事项
在实际项目落地时,以下几点是开发者使用时间轴时最容易踩坑的细节:
1. 异步动态追加 .add() 导致总时长计算失真
时间轴在初始化执行链同步执行完后,就会立刻预计算出整个时间轴的 duration。如果在 fetch 或 setTimeout 等异步回调中动态调用 tl.add(),会破坏原有的时钟计算,导致 loop、reverse 或 complete 回调无法按预期触发。正确做法是将全部序列在同步阶段一次性声明完整。
2. CSS Transform 复合属性的相互冲撞
当多个子动画在重叠并行的时间段内同时作用于同一个 DOM 节点的 transform(例如动画 A 在改 translateX,并行动画 B 在改 scale),如果未合理隔离目标层级,后一个补间可能会覆盖前一个属性写入。对于复杂的同级并行,推荐拆分父子容器(外层做位移,内层做缩放),或者在同一个 .add() 中同时声明多个属性。
3. 回调函数的层级归属
anime.timeline({ complete: fn }) 里的 complete 是在整条时间轴的所有动画全部走完时才触发一次;而写在某个具体 .add({ complete: fn }) 里的回调,只代表该子阶段结束。务必根据业务需要将回调绑定在对应的层级上。
八、总结与时序控制速查
掌握 anime.js Timeline 的核心在于理解“单一虚拟时钟”与“相对时间偏移”两个概念:
- 需要严谨的分步交代时,省略 offset 或使用
'+=0',依托天然的串行队列; - 需要有机衔接、消除等待感时,利用
'-=xxx'提前交错重叠,让上一动效尾部与下一动效头部并行; - 需要多通道同拍爆发或绝对时间锚定时,使用绝对数值(如
0)或'<'符号实现多轨精确同步。
通过合理运用这些模式,即可摆脱混乱的定时器嵌套,以工程化、可维护的方式驾驭复杂生动的前端动画编排。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。