从补间到叙事:GSAP 动画实战 · 公开阅读
第2章 核心概念
理解 Tween、Timeline 和 Easing 三个核心概念。
本章小节
GSAP 看似简单,但三根支柱撑起了整个体系:Tween(补间动画)、Timeline(时间线)、Easing(缓动函数)。这三个术语听起来唬人,背后的概念其实非常直觉。
2.1 Tween:补间动画
2.1.1 什么是"补间"
"补间"(Tween,即 In-Between)这个词听起来很学术,但道理简单。回想一下你小时候翻动画本的经验——你只需要画第一页和最后一页,中间的那些过渡画面自动就"补"上了。
GSAP 的"补间"也是这个意思:你告诉它起始状态和终止状态,中间的每一帧它自动帮你算出来。
以手动实现来对比看看:
// 手动:你需要在每一帧自己写
let progress = 0;
const targetX = 300;
function animate() {
progress += 0.016; // 约 60fps
box.style.left = (progress * targetX) + 'px';
if (progress < 1) requestAnimationFrame(animate);
}
animate();
// GSAP:一句话搞定
gsap.to(box, { x: 300, duration: 1 });GSAP 在内部做了同样的事,但它的插值引擎比你手写的版本精确得多、高效得多。
2.1.2 GSAP 如何计算中间帧
把时间进度和属性进度分开,公式才不会混淆:
const p = elapsed / duration; // 时间进度,通常限制到 0…1
const q = ease(p); // 属性进度,back/elastic 可超出 0…1
const x = startX + (endX - startX) * q;GSAP 的 power2.out 是三次减速曲线,q = 1 - (1-p)^3。从 0 到 200、时长 2 秒时:
| t / 秒 | p | q | x / px |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 0.5 | 0.25 | 0.578125 | 115.625 |
| 1 | 0.5 | 0.875 | 175 |
| 1.5 | 0.75 | 0.984375 | 196.875 |
| 2 | 1 | 1 | 200 |
以前将 0.5 秒算成 44% 的结果对应二次曲线,不能套在 power2.out 上。时间已过一半时,位移已完成 87.5%;两种“进度”不是同一件事。
把这一节跑一遍:时间进度与属性进度
把两条轨道定位到同一个时间进度。none 的属性进度等于 p,power2.out 则是 1-(1-p)³。代码、曲线与读数都使用实际 gsap.parseEase,避免示意图与运行结果不一致。
实验:时间进度与属性进度
GSAP 3.13 / 实验亲手调整参数,观察动画差异。点击开启后再播放,不会自动启动动画。
观察与验证:用默认 power2.out 定位到 50%;应看到 q=0.875,而线性为 0.5。把终点距离乘以 q,即可解释当下的位置。
2.1.3 Tween 能驱动的不仅是 CSS
GSAP 能做动画的对象,远不止 DOM 元素的 CSS 属性。任何 JavaScript 对象的数值属性,它都能驱动:
// 驱动 Canvas 绘制
const ctx = canvas.getContext('2d');
const state = { radius: 0, color: [0, 0, 255] };
gsap.to(state, {
radius: 100,
duration: 2,
onUpdate: () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(150, 150, state.radius, 0, Math.PI * 2);
ctx.fill();
}
});
// 驱动 Three.js 场景
gsap.to(cube.position, { x: 5, duration: 1 });
gsap.to(cube.rotation, { y: Math.PI * 2, duration: 2 });GSAP 是属性动画引擎,不是 DOM 动画引擎。这个认知决定了你能用它做到什么程度——它动的是"数值",不是"元素",这个数值最终用在哪里,你说了算。
把这一节跑一遍:普通对象补间
补间只计算属性,渲染是另一层职责。此实验用普通对象展示这个分工,后面的图表和 Three.js 延续同一思路。
实验:普通对象补间
GSAP 3.13 / 实验亲手调整参数,观察动画差异。点击开启后再播放,不会自动启动动画。
观察与验证:观察原始浮点值与整数显示:若要显示金额,可在显示层使用 toFixed(2),不要每帧把补间对象本身改回整数。
2.2 Timeline:时间线
2.2.1 从"散装动画"到"编排系统"
如果你做过这样的噩梦——产品经理让你做一个"Logo 先出来,然后标题淡入,接着副标题滑入,最后按钮放大"的入场动画,你可能会这样写:
// ❌ 散装动画——维护起来是噩梦
gsap.to('.logo', { opacity: 1, duration: 0.5 });
gsap.to('.title', { opacity: 1, duration: 0.5, delay: 0.5 });
gsap.to('.subtitle', { opacity: 1, duration: 0.5, delay: 1.0 });
gsap.to('.cta', { scale: 1.1, duration: 0.3, delay: 1.5 });看起来能跑,但问题来了:如果你想把 logo 的 duration 从 0.5 改为 0.8,后面所有的 delay 都要重新算一遍。改一个数字,牵一发而动全身。
2.2.2 Timeline 方案
// ✅ Timeline——编排逻辑清晰如乐谱
const tl = gsap.timeline();
tl.to('.logo', { opacity: 1, duration: 0.5 })
.to('.title', { opacity: 1, duration: 0.5 })
.to('.subtitle', { opacity: 1, duration: 0.5 })
.to('.cta', { scale: 1.1, duration: 0.3 });用 Timeline 之后,每个 .to() 自动衔接前一个动画的结束——前一个人完事了下一个人自动跟上。在源代码修改 duration 并重建这条串行时间线后,后续段重新按末端插入;直接修改已创建 Tween 的 duration 并不会自动重排所有现有段。这才是真正的编排,而不是"散装动画 + delay 计算"。
2.2.3 Timeline 的更多能力
Timeline 不只是串行,它还能并行、加标签、全局控制:
const tl = gsap.timeline({ defaults: { duration: 0.6, ease: 'power2.out' } });
// 并行:position 参数为 "<" 表示与前一个动画同时开始
tl.to('.left', { x: 200 })
.to('.right', { x: -200 }, '<'); // 同时开始
// 延时并行
tl.to('.top', { y: 100 })
.to('.bottom', { y: -100 }, '<0.3'); // 比上一个晚 0.3 秒开始
// 标签
tl.to('.step1', { opacity: 1 })
.addLabel('stage2')
.to('.step2', { opacity: 1 }, 'stage2+=0.5'); // stage2 之后 0.5 秒
// 全局控制
tl.play();
tl.pause();
tl.reverse();
tl.timeScale(2); // 2 倍速播放Timeline 的本质是动画的时间容器,它把一个个 Tween 组织成有结构的时间序列。Tween 是一个个独立的任务,Timeline 负责安排它们的时序。
2.3 Easing:缓动函数
2.3.1 物理直觉
你有没有注意到,现实世界中几乎没有东西是匀速运动的?开车起步不是瞬间 100 码,而是慢慢加速;刹车也不是瞬间停下,而是滑行减速。把一个球向上抛,它的速度不断减小直到顶点,然后加速下落。
缓动函数就是给动画赋予这种物理直觉的东西。没有它,动画生硬、机械、毫无生气。
// 没有缓动——机械、生硬
gsap.to('.ball', { y: -200, duration: 1, ease: 'none' });
// 有缓动——像真实抛球
gsap.to('.ball', { y: -200, duration: 1, ease: 'power2.out' });2.3.2 GSAP 缓动分类
GSAP 内置了丰富的缓动函数,每种都有自己的"性格":
| 类型 | 命名规则 | 运动特征 | 适用场景 |
|---|---|---|---|
power1 ~ power4 | powerN.in/out/inOut | 渐进加速或减速 | 通用场景,默认首选 |
back | back.in/out/inOut | 过冲后回弹 | 弹窗、强调动画 |
elastic | elastic.in/out/inOut | 弹簧式振荡 | 趣味交互 |
bounce | bounce.in/out/inOut | 弹跳 | 下落、碰撞效果 |
circ | circ.in/out/inOut | 圆弧形加速/减速 | 比 power 更柔和的过渡 |
expo | expo.in/out/inOut | 快速启动或急停(比 power4 更极致) | 快速淡入淡出 |
sine | sine.in/out/inOut | 轻柔如呼吸 | 柔和动画、呼吸灯效果 |
steps(n) | steps(12) | 逐格跳动 | 逐帧动画、打字效果 |
rough | rough({ template: ... }) | 带随机抖动 | 手绘风格 |
2.3.3 三态对比
每个缓动函数都有三种形态:.in、.out、.inOut。它们就是在表达加速和减速分别放在动画的哪一端:
// 同是 power2,三种形态完全不同
gsap.to('.in', { x: 300, duration: 1.5, ease: 'power2.in' }); // 慢→快
gsap.to('.out', { x: 300, duration: 1.5, ease: 'power2.out' }); // 快→慢(最常用)
gsap.to('.io', { x: 300, duration: 1.5, ease: 'power2.inOut' }); // 慢→快→慢power2.in: 开头缓慢,逐渐加速 → 适合元素离开屏幕
power2.out: 开头快速,逐渐减速 → 适合元素进入屏幕(最自然)
power2.inOut: 两端慢中间快 → 适合需要对称感的动画.out 开头快结尾慢——入场时可先尝试这种方向。.in 反过来,开头慢结尾快——适合让东西离开屏幕。
把这一节跑一遍:in / out / inOut
相同 p 分别送入 .in、.out、.inOut,就得到不同 q。快慢来自曲线斜率,而不是距离或 duration 改变。曲线图显示当前家族的 out 分支,读数给出三分支的实际值。
实验:in / out / inOut
GSAP 3.13 / 实验亲手调整参数,观察动画差异。点击开启后再播放,不会自动启动动画。
观察与验证:默认 power2,在 50% 处检查 in=0.125、out=0.875、inOut=0.5。随后试 sine,体会相同中点与不同加减速过程。
2.3.4 自定义贝塞尔曲线
如果内置缓动都不满意,可以用贝塞尔曲线做精确控制:
// 从公开的 gsap 包加载 CustomEase 后注册
gsap.registerPlugin(CustomEase);
CustomEase.create('customCurve', '0.17,0.67,0.83,0.67');
gsap.to('.custom', {
x: 300,
duration: 1.5,
ease: 'customCurve'
});在 cubic-bezier.com 上可视化调参后,将四个数值交给 CustomEase.create(),再引用注册的名称。CSS 的 cubic-bezier(...) 字符串不能直接作为 GSAP 的内置 ease 名称。
2.4 三件套的协作关系
把这三个概念放在一起看,可以用交响乐团的类比来理解它们的协作关系。
Tween 是单个乐器。 它定义了"什么在动"和"怎么动"——就像小提琴拉一个小节的音符。一个 Tween 管一件事,比如"这个方块从左边移到右边"。
Timeline 是交响乐团总谱。 它定义了"谁在什么时候动"——小提琴先起,然后是长笛,鼓在第三拍加入。Timeline 里可以装几十个 Tween,就像总谱上有几十个声部。指挥家一抬手,所有人按谱子来。
Easing 是演奏技巧。 同一个音符,用连奏(power2.out)和断奏(steps(5))弹出完全不同的感觉。Easing 决定了动画的"性格"——是优雅地滑过来,还是俏皮地弹两下。
// 这个比喻落实到代码
const symphony = gsap.timeline({ defaults: { duration: 0.8 } });
symphony
// 小提琴起(第一小提琴声部)
.to('.violin', { opacity: 1, y: -20, ease: 'power2.out' })
// 长笛加入(同时开始,但用更柔和的缓动)
.to('.flute', { opacity: 0.8, y: -10, ease: 'sine.out' }, '<')
// 定音鼓(比上一拍晚 0.3 秒,用弹跳缓动)
.to('.timpani', { scale: 1.2, ease: 'bounce.out' }, '<0.3')
// 休止
.to({}, { duration: 0.5 }) // 空 Tween 产生停顿
// 全体强奏
.to('.all', { scale: 1.5, ease: 'power4.out' });2.5 动画对象结构:三角关系
一个 GSAP Tween 由三要素构成:
gsap.to(target, { vars })
│ │
│ └── 配置对象:duration、x、ease、onComplete 等
└── 目标:CSS 选择器 / DOM 元素 / 任意 JS 对象 / 数组// target 的四种形式
gsap.to('.box', { x: 100 }); // CSS 选择器
gsap.to(document.querySelector('.box'), { x: 100 }); // DOM 元素
gsap.to({ val: 0 }, { val: 100 }); // JS 对象
gsap.to(['.box1', '.box2', '.box3'], { x: 100 }); // 数组(批量)
// vars 的关键字段结构
const vars = {
// === 动画目标值 ===
x: 100, y: 50, // transform 简写
rotation: 360, scale: 1.5, // transform 简写
opacity: 0, backgroundColor: '#ff0000', // CSS 属性
val: 999, // 任意属性
// === 时间控制 ===
duration: 1.5, delay: 0.3, // 持续时间、延迟
repeat: 2, repeatDelay: 1, // 重复次数、重复间隔
yoyo: true, // 往返(需要 repeat)
yoyoEase: 'power2.out', // 往返时的缓动
// === 缓动 ===
ease: 'power2.out',
// === 回调 ===
onStart: () => console.log('开始'),
onUpdate: () => console.log('每一帧'),
onComplete: () => console.log('完成'),
onRepeat: () => console.log('重复'),
onInterrupt: () => console.log('被中断'),
// === 控制 ===
paused: true, // 创建后不动,等待 play()
overwrite: 'auto', // 冲突动画覆盖策略
immediateRender: true, // from() 动画是否立即渲染初始状态
stagger: 0.1, // 批量目标的间隔时间
};这么多参数里,日常最常用的就那么几个:duration、ease、delay、stagger。其他的用到再查就行。
2.6 常见陷阱
陷阱 1:target 选择器没命中任何元素
// 类名写错时默认会有 GSAP target not found 警告
gsap.to('.cardd', { x: 100 }); // 没有任何 .cardd 元素
// ✅ 先确认元素存在
const cards = document.querySelectorAll('.card');
if (cards.length) {
gsap.to(cards, { x: 100 });
}GSAP 默认开启 nullTargetWarn,空目标会产生控制台警告,通常不会抛出中断脚本的异常。先检查选择器、执行时机和作用域,再检查参数。
陷阱 2:vars 属性拼写错误
// ❌ 拼错了,但 GSAP 不会警告
gsap.to('.box', { duratoin: 2, x: 100 }); // duratoin 无效
// 动画会以默认 duration=0.5 运行
// ❌ 还有这些常见误写
// opacity → opaticy / opaicity
// rotation → rotaion / roatation
// backgroundColor → backgorundColor没有拼写检查,没有类型提示。这种错误最坑——不会报错,但动画就是"不太对劲"。建议使用 TypeScript 或编辑器补全减少此类问题。
陷阱 3:duration 单位混淆
如果你之前用过 CSS 或 jQuery,可能会习惯毫秒为单位。但在 GSAP 里,请记住:
// ❌ GSAP 的 duration 单位是秒,不是毫秒
gsap.to('.box', { x: 100, duration: 2000 }); // 2000 秒!不是 2 秒
// ✅ 正确
gsap.to('.box', { x: 100, duration: 2 }); // 2 秒GSAP 所有时间参数(duration、delay、repeatDelay 等)单位都是秒。写 2 就是 2 秒,写 2000 就是半个多小时。
陷阱 4:混淆"动画"和"过渡"
// ❌ 错误认知:以为 GSAP 是 CSS transition 的替代
// GSAP 做的是"声明式动画",不是"状态过渡"
// GSAP 正确的使用方式:
// 在 JS 事件中主动创建动画,而不是依赖 CSS 状态变化
button.addEventListener('click', () => {
gsap.to('.modal', { scale: 1, opacity: 1, duration: 0.3 });
});CSS transition 是"状态变了,浏览器帮你过渡一下",而 GSAP 是"你主动告诉它要做什么动画"。思维方式不同——GSAP 更像是导演喊"Action",而不是等待状态变化被动触发。
插件前提:RoughEase、SlowMo、ExpoScaleEase 来自
gsap/EasePack,先导入并gsap.registerPlugin(RoughEase, SlowMo, ExpoScaleEase)。CustomEase 也需单独注册。