从补间到叙事:GSAP 动画实战 · 公开阅读
第7章:Control Methods
学习播放、暂停、反向、进度定位和动画生命周期控制。
本章小节
7.1 Tween 的控制方法
GSAP 为每个动画提供了完整的播放控制方法,就像视频遥控器上的播放、暂停、快进、停止键:
const tween = gsap.to(".box", { x: 500, duration: 2 });
tween.play(); // 播放
tween.pause(); // 暂停
tween.resume(); // 从暂停位置恢复
tween.restart(); // 从头开始
tween.reverse(); // 反向播放
tween.kill(); // 销毁方法详解
| 方法 | 行为 | 关键差异 |
|---|---|---|
play() | 从当前位置正向播放 | 将播放方向设为正向 |
pause() | 在当前位置暂停 | 保留进度 |
resume() | 从暂停位置继续 | 保持原有播放方向 |
restart() | 跳到起始位置重新播放 | 等于 seek(0) + play() |
reverse() | 反向播放 | 如果已播完,从终点开始倒退 |
kill() | 停止并从父时间线移除 tween | 保留当前视觉状态;后续需要动画时可重建 |
play 与 resume 的区别——一个隐藏的设计细节
const tween = gsap.to(".box", { x: 300, duration: 2, paused: true });
// 场景一:从未播放过
tween.play(); // 从 0% 开始播放
// 场景二:播到一半暂停
tween.pause(); // 停在 50%
tween.resume(); // 保持原方向继续;原来正向时从 50% 向终点前进
tween.play(); // 从当前位置正向播放play() 与 resume() 的关键区别是播放方向:play() 设置为正向,resume() 保留原方向。原来处于正向时,两者看起来相同;反向播放暂停后,两者的结果就不同。
两者都不会自动把已完成的动画移回起点。需要重新播放完整动画时,使用 restart()。
const tween = gsap.to(".box", { x: 300, duration: 1 });
// 动画已完成后...
tween.restart(); // 移回起点并重新播放
// 反向播放途中暂停后...
tween.reverse().pause();
tween.resume(); // 继续反向播放
tween.pause().play(); // 从当前位置改为正向播放reverse()——"倒带键"的行为
const tween = gsap.to(".box", { x: 300, duration: 1 });
// 播到一半按倒带
setTimeout(() => {
tween.reverse(); // 从 50% 开始向 0% 倒退
}, 500);
// 如果已经播完了
tween.reverse(); // 从 100% 开始向 0% 倒退7.2 Timeline 的控制方法
Tween 和 Timeline 共享 Animation 的播放控制方法;seek、progress、timeScale 在两者上都可使用。Timeline 还提供标签和子动画编排能力:
const tl = gsap.timeline();
tl.to(".a", { x: 100 })
.to(".b", { y: 200 }, "-=0.5")
.to(".c", { rotation: 360 });
// Timeline 独有
tl.seek(1.5); // 跳到 1.5 秒位置
tl.progress(0.5); // 跳到 50% 进度
tl.timeScale(2); // 2 倍速播放
tl.totalDuration(); // 获取总时长
tl.time(); // 获取当前时间seek(time)——时间轴跳转(快进/快退键)
const tl = gsap.timeline();
tl.to(".box", { x: 200, duration: 1 }) // 0 ~ 1s
.to(".box", { y: 200, duration: 1 }) // 1 ~ 2s
.to(".box", { rotation: 360, duration: 1 }); // 2 ~ 3s
tl.seek(1.5); // 跳到 1.5 秒——box 的 x 已到位,y 走了一半progress(0~1)——百分比控制
tl.progress(0); // 起始位置
tl.progress(0.5); // 50% 位置
tl.progress(1); // 终点位置
// 获取当前进度
const currentProgress = tl.progress(); // 返回 0~1| 方法 | 参数 | 使用场景 |
|---|---|---|
seek(t) | 秒(绝对时间) | 你知道具体秒数 |
progress(p) | 0~1(百分比) | 你想控制百分比 |
timeScale(n) | 倍率 | 你想加速/减速/倒放 |
timeScale(n)——倍速播放开关
tl.timeScale(2); // 2 倍速
tl.timeScale(0.5); // 半速
tl.timeScale(-1); // 倒放!注意是负值
tl.timeScale(0); // 暂停timeScale(1) 为正常速度,大于 1 加速,小于 1 减速,负数则反向播放。
totalDuration()——获取总时长
const tl = gsap.timeline();
tl.to(".a", { x: 100, duration: 1 })
.to(".b", { y: 100, duration: 0.5 }, "-=0.3") // 重叠 0.3s
.to(".c", { rotation: 360, duration: 1.5 });
console.log(tl.totalDuration()); // 输出总时长(考虑重叠后的实际时长)
console.log(tl.duration()); // 同上,不包含 repeat
console.log(tl.totalDuration()); // 包含 repeat把这一节跑一遍:播放方向与时间控制
seek(1) 的单位是秒,progress(0.5) 是单轮比例。resume 保留正反方向;play 明确向前播放。reverse 从当前播放头往回走,播放头在 0 时不会凭空倒放完整动画。
实验:播放方向与时间控制
GSAP 3.13 / 实验亲手调整参数,观察动画差异。点击开启后再播放,不会自动启动动画。
观察与验证:先定位到 50%,逆放、暂停、继续,再正放。观察 reversed 与 time;比较 seek(1) 和 progress(0.5),这条 4 秒动画的两次定位不同。
7.3 yield 与自动回收
GSAP 没有名为 yield 的动画回收 API。动画完成后,保存在变量里的 Tween 仍可以 restart();不需要 paused:true 或 context 来“防止引用失效”。JavaScript 的垃圾回收取决于对象是否仍被引用。
const tween = gsap.to(box, { x: 100, duration: 1 });
// 即使已经播放完成,仍能重播:
button.addEventListener('click', () => tween.restart());kill() 停止并移除动画,当前视觉状态通常保留;revert() 停止并恢复动画记录的原始样式。组件卸载时,用 gsap.context() 收集动画,再 ctx.revert()。自己添加的 DOM 监听、定时器、ResizeObserver 和 WebGL 资源需要另行清理。
const ctx = gsap.context(() => {
gsap.from('.box', { x: 100 });
}, container);
// 卸载时:
ctx.revert();保留引用的已完成动画、真正仍在播放的动画以及泄漏的事件监听是三种不同情况。先找出谁还在持有资源,再选择清理方式。
把这一节跑一遍:作用域与清理
用同名类同时创建 A/B 动画,再只还原 A。作用域限定选择器,context 记录副作用;context 数量或 Tween 数量不是帧率指标。完成后的引用仍可能存在,必须区分“完成”与“清理”。
实验:作用域与清理
GSAP 3.13 / 实验亲手调整参数,观察动画差异。点击开启后再播放,不会自动启动动画。
观察与验证:先创建 A、B,等待完成,再还原 A。检查 A 回到初始样式、B 保留自身状态;重复创建 A 时先 revert,计数应保持 1。
7.4 overwrite 模式——动画"打架"了怎么办
当同一个元素的同一个属性同时被两个 tween 操作时,就"打架"了。overwrite 决定"谁赢"。
gsap.to(".box", { x: 300, duration: 2 });
setTimeout(() => {
// 1 秒后才创建——和第一个 tween 的 x 属性冲突
gsap.to(".box", { x: 0, duration: 1, overwrite: true });
}, 1000);overwrite 三种模式
| 模式 | 行为 |
|---|---|
overwrite: false(默认) | 多个 tween 可以写同一属性,通常由后写入的值覆盖,并非数值相加 |
overwrite: true | 新 tween 创建时,终止相同目标上的已有 tween |
overwrite: "auto" | 新 tween 首次渲染时,只终止其他活动 tween 的冲突属性 |
实操对比
// 模拟场景:快速点击按钮触发动画
function handleClick() {
// 问题:如果用户快速点击 10 次,怎么处理?
// 方案一:overwrite: true —— 每次点击中断旧动画
gsap.to(".box", { x: "+=50", duration: 0.5, overwrite: true });
// 方案二:手动管理——更可控
let tween;
function handleClick() {
if (tween) tween.kill();
tween = gsap.to(".box", { x: "+=50", duration: 0.5 });
}
}7.5 全局控制
gsap.globalTimeline
所有 tween 默认挂载在 globalTimeline 上,操作它可以影响整个动画系统:
// 一拉闸,所有动画暂停
gsap.globalTimeline.pause();
// 合上闸,所有动画恢复
gsap.globalTimeline.resume();
// 全局二倍速
gsap.globalTimeline.timeScale(2);使用全局时间线暂停与恢复
GSAP 3 没有 gsap.pauseAll() 或 gsap.resumeAll() 方法。使用 globalTimeline,或只控制当前模块的时间线:
// 全局暂停
gsap.globalTimeline.pause();
// 全局恢复
gsap.globalTimeline.resume();
// 只控制某个模块时,调用该模块时间线的 pause()/resume()ticker——帧调度器
// GSAP 内部的核心帧调度器
gsap.ticker.fps(30); // 限制到 30fps(省电模式)
gsap.ticker.fps(240); // 恢复 GSAP 默认目标上限;实际频率仍受浏览器影响
gsap.ticker.add(myCallback); // 每帧执行
gsap.ticker.remove(myCallback); // 移除7.6 实战示例
示例一:交互式动画播放器
// HTML
// <div class="box"></div>
// <button id="play">播放</button>
// <button id="pause">暂停</button>
// <button id="reverse">倒放</button>
// <button id="restart">重播</button>
// <input type="range" id="progress" min="0" max="100" value="0">
const tween = gsap.to(".box", {
x: 500,
rotation: 360,
duration: 3,
ease: "power2.inOut",
paused: true,
});
document.querySelector("#play").addEventListener("click", () => tween.play());
document.querySelector("#pause").addEventListener("click", () => tween.pause());
document.querySelector("#reverse").addEventListener("click", () => tween.reverse());
document.querySelector("#restart").addEventListener("click", () => tween.restart());
// 进度条联动
const slider = document.querySelector("#progress");
tween.eventCallback("onUpdate", () => {
slider.value = tween.progress() * 100;
});
slider.addEventListener("input", (e) => {
tween.progress(e.target.value / 100);
});示例二:Timeline 导航控制器
const tl = gsap.timeline({ paused: true });
tl.from(".nav", { y: -100, opacity: 0, duration: 0.5 })
.from(".hero", { scale: 0.8, opacity: 0 }, "-=0.2")
.from(".features .card", { y: 50, opacity: 0, stagger: 0.15 }, "-=0.3")
.from(".cta", { y: 30, opacity: 0 });
// 用按钮控制进度
document.querySelector("#prev").addEventListener("click", () => {
const current = tl.progress();
tl.progress(Math.max(0, current - 0.25));
});
document.querySelector("#next").addEventListener("click", () => {
const current = tl.progress();
tl.progress(Math.min(1, current + 0.25));
});示例三:悬停加速/慢动作
const tl = gsap.timeline({ repeat: -1 });
tl.to(".gear", { rotation: 360, duration: 6, ease: "none" });
// 鼠标悬停——齿轮加速
document.querySelector(".gear").addEventListener("mouseenter", () => {
gsap.to(tl, { timeScale: 3, duration: 0.3 });
});
// 鼠标离开——恢复
document.querySelector(".gear").addEventListener("mouseleave", () => {
gsap.to(tl, { timeScale: 1, duration: 0.3 });
});示例四:全局暂停——弹窗场景
function openModal() {
gsap.to(gsap.globalTimeline, {
timeScale: 0,
duration: 0.3,
});
gsap.from(".modal", { scale: 0.8, opacity: 0, duration: 0.4 });
}
function closeModal() {
gsap.to(".modal", { scale: 0.8, opacity: 0, duration: 0.3, onComplete: () => {
document.querySelector(".modal").style.display = "none";
gsap.to(gsap.globalTimeline, {
timeScale: 1,
duration: 0.3,
overwrite: true,
});
}});
}7.7 常见陷阱
陷阱一:kill 后动画"回不来了"
现象:调了 kill(),之后想重新播放但完全无效。
原因:kill() 会停止并从父时间线移除动画,但不会清除 JavaScript 变量或自动还原样式。要恢复原样式使用 revert();要执行新的过渡可重建动画。
const tween = gsap.to(".box", { x: 300, duration: 2 });
tween.kill();
// 以下全部无效!
tween.play(); // 无效
tween.restart(); // 无效
tween.seek(0); // 无效正确做法:只是想"暂停一下"的话,用 pause();只有确定这个动画再也不会用到了,才用 kill()。
// 临停
tween.pause();
// 重新创建
function recreate() {
if (tween) tween.kill();
tween = gsap.to(".box", { x: 300, duration: 2 });
}陷阱二:timeScale 负值带来的意外
现象:设置了 timeScale(-1) 后动画行为不符合预期。
原因:timeScale 为负时时间"倒流",但 repeat 和 yoyo 的组合可能产生反直觉的结果。
const tl = gsap.timeline({ repeat: -1 });
tl.to(".box", { rotation: 360, duration: 4, ease: "none" });
tl.timeScale(-1); // 倒放——rotation 从 360 倒回到 0
// 由于 repeat: -1 生效,它会无限循环倒放注意:当 tween 处于 timeScale(-1) 并且播放到 0% 时,它会"完成"(反向完成)。对于设置了 repeat: -1 的 tween,这会导致始终反向循环。
陷阱三:全局暂停影响 ScrollTrigger
现象:暂停全局时间线后,其他模块的动画也停了,但滚动监听仍可能更新进度。
原因:全局时间线控制范围较大,而 ScrollTrigger 的监听、刷新与滚动进度更新不是一个“全局暂停”开关所能统一关闭的。
// 暂停全局时间线;不等于关闭 ScrollTrigger 的滚动监听与刷新
gsap.globalTimeline.pause();
// 更好的做法:只暂停自己创建的动画
const tl = gsap.timeline();
// ...构建时间线
tl.pause(); // 只暂停这一个如需临时停止某个滚动触发器,应使用该实例的 disable(),恢复时使用 enable()。注意 disable() 默认会还原 pin 等布局变化,应根据场景选择参数。
陷阱四:progress 设置后动画"瞬移"
现象:设置 tl.progress(0.8) 后元素直接跳到 80% 位置——没有过渡,看着像 bug。
原因:progress() 是即时跳转,不是"播放到那个位置"。如果你想要平滑过渡到某个进度,需要用一个 tween 来驱动:
// 错误:直接跳——画面突然切换
tl.progress(targetProgress);
// 正确:用 tween 平滑过渡过去
gsap.to(tl, {
progress: targetProgress,
duration: 0.5,
ease: "power2.inOut",
overwrite: true,
});陷阱五:overwrite 误杀回调
现象:设置了 overwrite: true 后旧动画被杀了,但旧动画的 onComplete 也没了。
原因:中断后的动画没有正常到达终点,因此不能依赖旧动画的 onComplete 执行清理。中断处理可使用 onInterrupt,而资源清理应在组件生命周期中显式执行。
const old = gsap.to(".box", {
x: 500,
duration: 3,
onComplete: () => console.log("到达 500"),
});
setTimeout(() => {
gsap.to(".box", {
x: 0,
duration: 1,
overwrite: true,
});
// old 被 kill,"到达 500" 永远不会打印
}, 1000);