从补间到叙事:GSAP 动画实战 · 公开阅读
第8章:Easing 缓动
通过并排动画比较匀速、缓出、回弹和弹性曲线。
本章小节
8.1 缓动是什么
匀速动画缺少加速和减速的变化,看起来机械而生硬。缓动函数通过控制动画在不同时间点的速率变化,让运动产生自然的物理感。
GSAP 不依赖 CSS 的 transition-timing-function,它有自己的缓动引擎,内置 20+ 种缓动函数。
// 显式设置 none;GSAP Tween 默认 ease 为 power1.out
gsap.to(".box", { x: 400, duration: 2 });
// 有缓动——活过来了
gsap.to(".box", { x: 400, duration: 2, ease: "power3.out" });8.2 内置缓动全集
Power 系列——五挡"风速"
Power 系列是 GSAP 的主力缓动,从 Power0 到 Power4,曲线陡峭程度递增:
| 缓动 | 数学本质 | 加速程度 |
|---|---|---|
power0 (即 none) | y = x | 匀速,无加速度 |
power1 (即 quad) | y = x² | 轻微加速 |
power2 (即 cubic) | y = x³ | 中等加速 |
power3 (即 quart) | y = x⁴ | 强烈加速 |
power4 (即 quint) | y = x⁵ | 极速爆发 |
// 力度对比
gsap.to(".box1", { x: 400, duration: 1.5, ease: "power1.out" }); // 轻
gsap.to(".box2", { x: 400, duration: 1.5, ease: "power2.out" }); // 中
gsap.to(".box3", { x: 400, duration: 1.5, ease: "power3.out" }); // 强
gsap.to(".box4", { x: 400, duration: 1.5, ease: "power4.out" }); // 暴力特效缓动
除了 Power 系列,GSAP 还提供了一批特效缓动:
| 缓动 | 行为特征 | 典型用途 |
|---|---|---|
back | 越过目标值后回弹 | 按钮回弹、元素弹入 |
bounce | 多次弹跳后稳定 | 下落效果、通知提示 |
circ | 圆弧形加减速 | 圆形展开、进度环 |
elastic | 阻尼弹簧振动 | 弹性拉伸、趣味交互 |
expo | 指数级加速/减速 | 视差滚动、消失效果 |
sine | 正弦波柔和过渡 | 微妙过渡、呼吸动画 |
stepped | 分步跳跃,无插值 | 逐帧动画、计数效果 |
rough | 叠加随机噪声 | 手绘风格、笔触模拟 |
slowMo | S 形:两端快中间慢 | 特殊节奏效果 |
把这一节跑一遍:缓动与超调对比
线性、选定曲线、Back、Elastic 共享时长和位移,差异来自 q 的变化。Back/Elastic 可以暂时越过终点,不能把曲线参数直接理解为超调百分比。
实验:缓动与超调对比
GSAP 3.13 / 实验亲手调整参数,观察动画差异。点击开启后再播放,不会自动启动动画。
观察与验证:定位到 60%–80% 看超调,再回到 100% 看最终值。边界严格的组件应预留空间或选不超调的曲线。
8.3 缓动方向:In / Out / InOut
每个内置缓动都有三种方向变体:
ease: "power2.in" // 缓慢起步 → 加速冲刺(起跑用)
ease: "power2.out" // 快速起步 → 优雅减速到位(最常用!)
ease: "power2.inOut" // 缓慢起步 → 中间加速 → 减速停下(循环用)行为对比
// 三种变体的视觉差异
// in——起步慢,加速冲线
gsap.to(".box-in", { x: 400, duration: 2, ease: "power3.in" });
// out——起步快,优雅减速到位(90% 场景用这个)
gsap.to(".box-out", { x: 400, duration: 2, ease: "power3.out" });
// inOut——慢-快-慢,平滑完整
gsap.to(".box-inout", { x: 400, duration: 2, ease: "power3.inOut" });把这一节跑一遍:in / out / inOut
图中每个点由 parseEase 实际计算。横轴是归一化时间,纵轴是属性进度;速度对应曲线斜率,图上的高低并不是速度本身。
实验:in / out / inOut
GSAP 3.13 / 实验亲手调整参数,观察动画差异。点击开启后再播放,不会自动启动动画。
观察与验证:默认 power2 定位到 25%,计算 out=1-0.75³=0.578125;与实时读数核对,再观察 inOut 在前半程的表现。
选型原则
| 场景 | 推荐 | 为什么 |
|---|---|---|
| 元素入场(从不可见到可见) | .out | 快速出来,然后优雅停稳——符合直觉 |
| 元素出场(从可见到不可见) | .in | 慢慢开始,最后加速消失——有"离去感" |
| 循环动画(如脉冲、呼吸) | .inOut | 头尾都平滑,无线循环不会突兀 |
| 滚动驱动的动画 | .none 或 .out | 跟上手指就行,不需要花哨 |
back.out 的超调参数
back.out 接受一个参数控制超调量——即动画越过目标值的幅度:
ease: "back.out(1.7)" // 默认超调量
ease: "back.out(3)" // 大幅超调
ease: "back.out(0.5)" // 轻微超调,接近 power2.out
// 对比
gsap.to(".b1", { y: -50, duration: 0.6, ease: "back.out(1.2)" }); // 轻微回弹
gsap.to(".b2", { y: -50, duration: 0.6, ease: "back.out(1.7)" }); // 标准回弹
gsap.to(".b3", { y: -50, duration: 0.6, ease: "back.out(4)" }); // 剧烈回弹elastic.out——弹簧参数
弹簧振子缓动有两个参数:振幅(弹多远)和周期(弹多快):
ease: "elastic.out(1, 0.3)" // (振幅, 周期)
// 振幅默认 1,周期默认 0.3
// 振幅越大越"弹",周期越小震动越频繁
gsap.to(".box", {
y: 200,
duration: 2,
ease: "elastic.out(1.5, 0.5)", // 大幅度慢震
});
gsap.to(".box", {
y: 200,
duration: 2,
ease: "elastic.out(1, 0.2)", // 周期更短,振幅参数不小于1
});stepped——逐帧跳跃
ease: "steps(5)" // 5 步跳跃
ease: "steps(12)" // 12 步——模拟逐帧动画8.4 自定义缓动——你自己画速度曲线
GSAP 的免费插件 CustomEase 让你用贝塞尔曲线自己画缓动——就像在一张画布上描一条"速度-时间"曲线:
// 从公开的 gsap 包导入插件后注册
gsap.registerPlugin(CustomEase);
// 从 cubic-bezier 值创建
CustomEase.create("myEase", "0.47, 0, 0.745, 0.715");
// 使用
gsap.to(".box", {
x: 400,
duration: 2,
ease: "myEase",
});SVG 路径缓动
CustomEase 支持用 SVG Path 语法定义缓动曲线——这意味着你可以在可视化工具里拖动曲线,直接导出代码:
CustomEase.create("custom1", "M0,0 C0.5,0 0.5,1 1,1"); // 平滑 S 形
CustomEase.create("custom2", "M0,0 C0.2,1.2 0.8,-0.2 1,1"); // 夸张的回弹
CustomEase.create("custom3", "M0,0 C0,1.5 0.5,1.5 1,1"); // 大超调曲线格式:M0,0(起点)→ Cx1,y1 x2,y2 1,1(三次贝塞尔,终点必须是 1,1)
建议先在 Ease Visualizer 中调试
GSAP 官网的 Ease Visualizer 可以拖动曲线实时预览效果,调好后将生成的代码复制回来即可。
把这一节跑一遍:自定义贝塞尔缓动
贝塞尔控制点影响时间到属性进度的映射。允许 y 超出 0…1 产生超调;时间轴必须保持单调。四数形式是两个控制点,不是四个经过的动画位置。
实验:自定义贝塞尔缓动
GSAP 3.13 / 实验亲手调整参数,观察动画差异。点击开启后再播放,不会自动启动动画。
观察与验证:切到 0.2,1.4,0.6,1,读采样峰值,再观察方块超调。比较三种曲线时保持 duration 不变。
8.5 roughEase——手写字的微微颤抖
roughEase 在缓动曲线中注入随机噪声,模拟手绘线条的不规则运动:
gsap.to(".hand-drawn", {
x: 300,
duration: 2,
ease: "rough({ template: power1.out, strength: 1, points: 20, taper: 'out', randomize: true, clamp: true })"
});roughEase 参数
| 参数 | 默认值 | 描述 |
|---|---|---|
template | "none" | 底层基础缓动(在它的基础上加噪声) |
strength | 1 | 抖动强度——越大越"抖" |
points | 20 | 每秒采样点数——越高抖动越细密 |
taper | "none" | 抖动衰减:"in" / "out" / "both" |
randomize | true | 每次播放随机种子不同——每次都不一样 |
clamp | false | 是否限制在 0~1 范围 |
实战——打字机效果
gsap.from(".typewriter span", {
opacity: 0,
duration: 0.05,
stagger: 0.03,
ease: "rough({ strength: 0.3, points: 5 })",
});实战——手绘风描边
gsap.from(".sketch-line", {
drawSVG: "0%",
duration: 3,
ease: "rough({ template: power1.inOut, strength: 0.5, points: 30, clamp: true })",
});8.6 expoScale——指数缩放
expoScale 是专门为缩放动画设计的。普通 expo 缓动在 scale: 0 时有个数学问题——指数函数理论上永远到不了 0:
// 不使用 expoScale——scale 从 0 开始会有问题
gsap.from(".box", { scale: 0, duration: 1, ease: "expo.out" }); // scale 0 可能永远到不了
// 使用 expoScale——干净利落
gsap.from(".box", {
scale: 0,
duration: 1,
ease: "expoScale(1, 5, power2.out)", // (start, end, ease)
});参数:expoScale(startValue, endValue, ease)——startValue 是起始刻度,endValue 是结束刻度,ease 是基础缓动。
8.7 实战示例
示例一:弹性按钮——带按压反馈
const btn = document.querySelector(".btn");
btn.addEventListener("mouseenter", () => {
gsap.to(btn, {
scale: 1.15,
duration: 0.3,
ease: "back.out(2.5)",
});
});
btn.addEventListener("mouseleave", () => {
gsap.to(btn, {
scale: 1,
duration: 0.3,
ease: "power2.out",
});
});
btn.addEventListener("mousedown", () => {
gsap.to(btn, {
scale: 0.95,
duration: 0.1,
ease: "power2.in",
});
});
btn.addEventListener("mouseup", () => {
gsap.to(btn, {
scale: 1.15,
duration: 0.2,
ease: "elastic.out(1, 0.3)",
});
});示例二:缓动对比预览器
// 一个简单的缓动预览工具
const eases = [
"power1.out", "power2.out", "power3.out", "power4.out",
"back.out(1.7)", "elastic.out(1, 0.3)", "bounce.out",
"circ.out", "expo.out", "sine.out",
];
eases.forEach((ease, i) => {
const dot = document.createElement("div");
dot.className = "ease-dot";
dot.style.top = `${i * 40 + 20}px`;
document.querySelector(".ease-demo").appendChild(dot);
gsap.to(dot, {
x: 500,
duration: 2,
ease,
repeat: -1,
yoyo: true,
repeatDelay: 0.5,
delay: i * 0.1,
});
});示例三:6 种缓动并排对比
// 并排对比 6 种常用缓动
const demoBoxes = document.querySelectorAll(".demo-box");
const easingList = [
"none",
"power2.out",
"power3.out",
"back.out(1.7)",
"elastic.out(1, 0.3)",
"bounce.out",
];
demoBoxes.forEach((box, i) => {
gsap.to(box, {
x: 450,
duration: 2.5,
ease: easingList[i],
repeat: -1,
yoyo: true,
repeatDelay: 1,
delay: i * 0.1,
});
});示例四:slowMo——"慢门"效果
// slowMo 创造"中间暂停"的效果
gsap.to(".car", {
x: 600,
duration: 3,
ease: "slow(0.2, 0.8, false)",
// 参数: (起始比例, 结束比例, 是否平滑衔接)
});
// slow(0.1, 0.9, false)
// 开始 10%:加速
// 中间 80%:线性匀速(慢门区)
// 最后 10%:减速8.8 常见陷阱
陷阱一:Elastic / Back 弹过头了
back.out(1.7) 的 1.7 不是 70% 的超调,elastic.out(1,0.3) 的 1 也不是超出 100%。这些是曲线参数,实际超调需计算曲线峰值。
const ease = gsap.parseEase('back.out(1.7)');
const peak = Math.max(...Array.from({ length:1001 }, (_,i) => ease(i/1000)));
console.log(peak); // 约 1.10
console.log(250 * peak); // 0→250 的运动,峰值约 275pxGSAP 的 elastic 参数会把小于 1 的振幅提升到 1,因此把 1 改成 0.3 不能直接得到“30% 超调”。需要严格控制边界时,改用不超调的缓动,缩短位移或预留空间;overflow:hidden 仅裁剪内容,不会改变运动路径。
陷阱二:自定义缓动路径格式错误
现象:CustomEase.create() 报错或缓动效果诡异。
原因:贝塞尔曲线的控制点有限制——x 值必须在 0~1 之间(时间不能倒退),但 y 值可以超出这个范围(这就是为什么可以有超调效果)。
// 错误:x1 超出 0~1 范围
CustomEase.create("bad", "M0,0 C1.2,0 0.5,1 1,1");
// 正确:x 值在 0~1,y 值可以任意
CustomEase.create("good", "M0,0 C0.5,1.5 0.8,-0.3 1,1");
// x1=0.5 ✓ y1=1.5 ✓(超调)
// x2=0.8 ✓ y2=-0.3 ✓(undershoot)陷阱三:Timeline 的 ease 不是子 tween 的 ease
在 timeline 顶层写 ease 不会设置子 Tween 的缓动,也不负责子动画的排列节奏。使用 defaults.ease 统一子 Tween,或在每个 Tween 上指定:
const tl = gsap.timeline({ defaults: { ease: 'power3.out', duration: 1 } });
tl.to(boxA, { x: 300 }).to(boxB, { x: 300, ease:'none' });如果要让时间线的播放头本身加减速,可另建一个 Tween 驱动 tl.progress() 或使用 tweenTo()。播放头缓动和子 Tween 缓动叠加后会影响运动速度,需要分别理解。
陷阱四:step 缓动配合短 duration——闪瞎了眼
现象:ease: "steps(5)" 配合短 duration 导致动画一闪而过,根本看不清。
// duration=0.3, steps=10 → 每步只有 30ms,几乎没有可感知的停顿
gsap.to(".counter", { x: 200, duration: 0.3, ease: "steps(10)" });
// 更好的搭配
gsap.to(".counter", { x: 200, duration: 2, ease: "steps(20)" });
// 每步 100ms——人力能感知的范围陷阱五:写错的 ease 名称不报错
现象:写了 ease: "myCustomEase" 但没注册,JS 不报错但缓动看起来不对。
原因:GSAP 对不存在的 ease 名称会静默回退到默认值,不会抛出错误。
// 这不会报错!
gsap.to(".box", { x: 300, ease: "bounce.inOut" });
// bounce 没有 .inOut 变体,静默回退正确写法:
// bounce 只有 .out 和 .in
gsap.to(".box", { x: 300, ease: "bounce.out" });插件前提:RoughEase、SlowMo、ExpoScaleEase 来自
gsap/EasePack,先导入并gsap.registerPlugin(RoughEase, SlowMo, ExpoScaleEase)。CustomEase 也需单独注册。