返回目录

从补间到叙事: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叠加随机噪声手绘风格、笔触模拟
slowMoS 形:两端快中间慢特殊节奏效果

把这一节跑一遍:缓动与超调对比

线性、选定曲线、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"底层基础缓动(在它的基础上加噪声)
strength1抖动强度——越大越"抖"
points20每秒采样点数——越高抖动越细密
taper"none"抖动衰减:"in" / "out" / "both"
randomizetrue每次播放随机种子不同——每次都不一样
clampfalse是否限制在 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 的运动,峰值约 275px

GSAP 的 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 也需单独注册。