返回目录

从补间到叙事:GSAP 动画实战 · 公开阅读

第18章 GSAP高级插件概览

了解 SplitText、ScrambleText、DrawSVG 和 MorphSVG 等高级插件。

本章小节

18.1 GSAP 3.13 起的免费插件体系

从 GSAP 3.13(2025年4月发布)起,核心与全部插件都免费,包括商业使用。过去仅向 Club GSAP 会员提供的 SplitText、MorphSVG 等插件已纳入公开的 npm 包,不再需要会员订阅或私有仓库令牌。参见 官方发布说明。

免费使用不等于 MIT 许可;GSAP 使用自己的标准许可,具体以官方许可为准。

组成部分是否免费说明
gsap 核心免费to/from/fromTo/timeline 等核心API
ScrollTrigger免费滚动驱动动画
Observer免费统一手势处理
MotionPathPlugin免费路径运动动画
Flip免费布局过渡动画
ScrollToPlugin免费平滑滚动到指定位置
EaselPlugin免费EaselJS/CreateJS集成
PixiPlugin免费PixiJS集成
TextPlugin免费文本替换动画
SplitText免费将文本拆分为行、词、字符,支持逐字动画
ScrambleText免费文字乱码解码动画效果
DrawSVGPlugin免费SVG线条逐步描绘动画
MorphSVGPlugin免费SVG形状平滑变形动画
Physics2DPlugin免费2D物理模拟,支持重力、摩擦力等
PhysicsPropsPlugin免费物理属性模拟
CustomEase免费自定义缓动曲线
GSDevTools免费开发调试工具,时间轴可视化控制
InertiaPlugin免费拖拽松手后带惯性滑行

18.2 公开安装与按需导入

旧的 Simply GreenSock、Shockingly GreenSock、Business GreenSock 会员等级属于历史信息,不再作为当前安装或部署的前提。本书示例统一使用公开的 GSAP 3.13 包:

npm install gsap@3.13.0
import gsap from "gsap";
import { SplitText } from "gsap/SplitText";
import { MorphSVGPlugin } from "gsap/MorphSVGPlugin";

gsap.registerPlugin(SplitText, MorphSVGPlugin);

本章各示例假设核心与对应插件已加载并注册。只导入实际使用的插件,不需要加载整套工具。通过 <script> 引入时,应先加载核心,再加载同版本插件;路径均来自 gsap 包的 dist 目录。详见 官方安装文档。

18.3 SplitText

SplitText 将一段文字拆分成**行(lines)、词(words)、字符(chars)**三个层级,让每个文字碎片独立动画。

核心代码

<h1 class="split-title">从补间到叙事</h1>

<script>
const st = new SplitText(".split-title", {
  type: "lines,words,chars",  // 拆分方式
  linesClass: "line",         // 行容器的class
  wordsClass: "word",         // 词容器的class
  charsClass: "char",         // 字符容器的class
});

// 字符逐个飞入
gsap.from(st.chars, {
  opacity: 0,
  y: 60,
  rotation: 10,
  duration: 0.6,
  stagger: 0.03,
  ease: "back.out(1.7)",
});

// 清理(组件卸载时)
st.revert();
</script>

三种经典用法

const st = new SplitText(".title", { type: "chars,words" });

// 用法1:入场(in)—— 文字从不可见飞进来
gsap.from(st.chars, {
  opacity: 0,
  y: 80,
  rotationX: -90,
  stagger: 0.04,
  duration: 0.8,
  ease: "power4.out",
});

// 用法2:退场(out)—— 文字逐个消失
gsap.to(st.chars, {
  opacity: 0,
  y: -50,
  stagger: { from: "end", amount: 0.3 },
  duration: 0.4,
});

// 用法3:效果(effect)—— 不断变化的视觉
gsap.to(st.chars, {
  color: "#e74c3c",
  fontWeight: 700,
  stagger: { from: "center", amount: 0.5 },
  duration: 0.3,
  yoyo: true,
  repeat: 1,
});

逐行动画

const st = new SplitText(".paragraph", { type: "lines" });

gsap.from(st.lines, {
  opacity: 0,
  x: 30,
  duration: 0.5,
  stagger: 0.1,
  scrollTrigger: {
    trigger: ".paragraph",
    start: "top 80%",
  },
});

把这一节跑一遍:SplitText 字符入场

真实插件按 Unicode 文本拆分。先保留可访问的原文名称,再对子元素做有限时长 Tween;清理时恢复原文。逐行场景与逐字场景的测量时机不同。

实验:SplitText 字符入场

GSAP 3.13 / 实验

亲手调整参数,观察动画差异。点击开启后再播放,不会自动启动动画。

观察与验证:观察中英混排与 emoji,重置后确认没有越来越多的包装层。需要可复制或被辅助技术读取的正文,避免无意义的拆字。

18.4 ScrambleText

文字从随机乱码字符逐渐解码为正确内容的动画效果。适合科技感、黑客风、解谜类场景。

核心代码

<h1 id="decrypt">解码中...</h1>

<script>
const tl = gsap.timeline();

tl.to("#decrypt", {
  scrambleText: {
    text: "从补间到叙事",
    chars: "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",  // 乱码字符集
    revealDelay: 0.5,       // 开始揭示前的整体延迟(秒)
    speed: 0.3,             // 乱码更新速度系数,不是秒
    newClass: "revealed",   // 揭示后的字符添加的class
    tweenLength: false,      // 是否动画文本长度
  },
  duration: 2,
});
</script>

参数详解

scrambleText: {
  text: "目标文字",                    // 必需:最终显示的文字
  chars: "!@#$%^&*()_+-=[]{}|;:",    // 乱码阶段使用的随机字符集
  revealDelay: 0,       // 开始揭示前的整体延迟
  speed: 0.3,           // 乱码字符滚动的速度
  newClass: "fresh",    // 揭示后添加到span上的class
  oldClass: "scrambled",// 乱码中的字符的class
  tweenLength: false,   // true时会动画文字长度变化
  rightToLeft: false,   // 从右向左揭示
  delimiter: "",        // 分隔符
}

把这一节跑一遍:ScrambleText 文字解码

revealDelay 是开始揭示前的整体延迟;speed 是乱码更新系数,不是秒。tweenLength 影响长度变化,不是字间距。中间字符随机,不能要求每次重播完全一致。

实验:ScrambleText 文字解码

GSAP 3.13 / 实验

亲手调整参数,观察动画差异。点击开启后再播放,不会自动启动动画。

观察与验证:暂停并定位不同进度,检查最终文字与参数。核心信息不要只依赖解码动画才能获得;这里原文也出现在正文。

18.5 DrawSVGPlugin

让 SVG 线条从 0% 到 100% 逐渐描出。适合签名动画、Logo 描边效果。

核心代码

<svg viewBox="0 0 800 200">
  <path id="signature"
    d="M50,100 C150,30 250,170 350,100 S550,30 650,100 S700,150 750,80"
    fill="none" stroke="#3498db" stroke-width="4"/>
</svg>

<script>
gsap.registerPlugin(DrawSVGPlugin);

gsap.from("#signature", {
  drawSVG: 0,            // 从0%开始描绘
  duration: 3,
  ease: "power2.inOut",
});

// 或直接使用 to
gsap.to("#signature", {
  drawSVG: "100%",       // 描绘到100%
  duration: 3,
  ease: "power2.inOut",
});
</script>

高级:分段描绘——不是从头画到尾

// 从20%描绘到80%
gsap.fromTo("#path",
  { drawSVG: "20% 20%" },    // 起点20%,终点也在20%(线条不可见)
  { drawSVG: "20% 80%", duration: 2 }  // 起点20%,终点80%
);

// 描绘 + 擦除效果
const tl = gsap.timeline({ repeat: -1, yoyo: true });
tl.fromTo("#path",
  { drawSVG: 0 },
  { drawSVG: "100%", duration: 2, ease: "power2.inOut" }
);
tl.to("#path", { drawSVG: "0% 100%", duration: 1, ease: "power2.in" });

把这一节跑一遍:DrawSVG 描绘范围

DrawSVG 改变 stroke 的可见范围,不改变路径几何。20% 20% 为零长度,20% 80% 为中间 60% 的路径;百分比参考路径总长而非 SVG 宽度。

实验:DrawSVG 描绘范围

GSAP 3.13 / 实验

亲手调整参数,观察动画差异。点击开启后再播放,不会自动启动动画。

观察与验证:切换整条与局部范围,查看可见段长度。对比第17章:DrawSVG 是线的呈现,MotionPath 是物体的位置。

18.6 MorphSVGPlugin

将 SVG 路径从形状 A 平滑变形为形状 B。

核心代码

<svg viewBox="0 0 300 300">
  <path id="shape" d="M150,50 L250,150 L150,250 L50,150 Z" fill="#3498db"/>
</svg>
<button onclick="morph()">变换形状</button>

<script>
gsap.registerPlugin(MorphSVGPlugin);

function morph() {
  // 菱形 → 圆形
  gsap.to("#shape", {
    duration: 1.5,
    morphSVG: "M150,20 C230,20 280,70 280,150 C280,230 230,280 150,280 C70,280 20,230 20,150 C20,70 70,20 150,20 Z",
    ease: "power2.inOut",
  });
}
</script>

在多个形状之间来回切换

// 在多个路径之间变形
const shapes = [
  "#star",      // 星形
  "#hexagon",   // 六边形
  "#circle",    // 圆形
  "#square",    // 方形
];

let index = 0;
document.querySelector("button").addEventListener("click", () => {
  index = (index + 1) % shapes.length;
  gsap.to("#shape", {
    duration: 1,
    morphSVG: shapes[index],
    ease: "power2.inOut",
  });
});

// findShapeIndex 辅助
// shapeIndex:"auto" 让插件自动匹配;视觉辅助使用独立的 findShapeIndex 工具,非 MorphSVGPlugin 静态方法。

关键参数

morphSVG: {
  shape: "#targetShape",    // 目标形状的选择器或路径字符串
  shapeIndex: 2,            // 调整两条路径的起始点匹配
  type: "linear",           // 默认 linear;可选 rotational
  map: "size",              // 控制点映射方式:"size" | "position" | "complexity"
  origin: "50% 50%",        // 变形中心点
  precompile: "log",        // 输出可预编译的数据,或传入预编译路径数组
}

把这一节跑一遍:MorphSVG 路径变形

MorphSVG 处理路径匹配与插值。shapeIndex 调整两条路径起始点的匹配,不是目标数组的索引。两端形状的点数可不同,但匹配方式影响变形中途是否扭曲。

实验:MorphSVG 路径变形

GSAP 3.13 / 实验

亲手调整参数,观察动画差异。点击开启后再播放,不会自动启动动画。

观察与验证:停在 50% 观察中间轮廓,再回到两端。需要设计更复杂形状时先检查路径方向、闭合和起始点匹配。

18.7 Physics2DPlugin

让元素像真实物体一样受重力、摩擦力、加速度影响。设置初始速度和角度即可模拟抛物线运动。

核心代码

<div class="physics-ball">⚽</div>

<script>
gsap.registerPlugin(Physics2DPlugin);

gsap.to(".physics-ball", {
  duration: 2,
  physics2D: {
    velocity: 300,        // 初始速度(扔出去的力度)
    angle: -45,           // 发射角度(度)
    gravity: 400,         // 重力加速度(px/s²)
    acceleration: 0,      // 自身加速度
    accelerationAngle: 0, // 加速度方向
    friction: 0.2,        // 摩擦系数(0-1,越大停得越快)
  },
  ease: "none",           // 物理动画必须用none
});
</script>

弹跳效果

Physics2D 计算带速度、重力和摩擦的轨迹,但不提供碰撞检测,也不支持 physics2D.onUpdate(velocity,angle) 返回新的速度来反弹。不要照抄不存在的接口。

如果需要碰撞,应在独立的物理更新循环中保存位置与速度,做边界判断后反转相应速度分量,或使用物理引擎。只需表现一次落地回弹时,可用 Timeline 编排下降与缩放/回弹,但那是设计的视觉效果,不是 Physics2D 的碰撞模拟。

// 有限时长抛物线,超出容器可以被裁剪;它不会自动反弹。
gsap.to(ball, { duration:2, ease:'none', physics2D:{ velocity:200, angle:-60, gravity:300 } });

把这一节跑一遍:Physics2D 抛物线

每个粒子使用初速度、角度和重力。无摩擦时 y 随时间包含二次项,速度持续变化;ease:none 保持物理时间映射。实验根据预测的轨迹范围等比例缩放世界坐标到舞台,粒子的显示大小保持不变。这个视图变换不会改变物理速度、重力或时间;裁剪和视图缩放都不是碰撞。

实验:Physics2D 抛物线

GSAP 3.13 / 实验

亲手调整参数,观察动画差异。点击开启后再播放,不会自动启动动画。

观察与验证:把重力改为 0 看直线运动,再增大重力看抛物线。暂停读取 t,思考 y≈vy×t+0.5×g×t²;实际插件负责求值。

18.8 CustomEase

自定义缓动曲线,通过贝塞尔路径精确控制加速/减速/弹跳/回弹节奏。

核心代码

gsap.registerPlugin(CustomEase);

// 方法1:可视化创建
CustomEase.create("myBounce", "M0,0 C0.5,0 0.5,1 1,1");
// 路径格式:M(startX,startY) C(cp1X,cp1Y, cp2X,cp2Y, endX,endY)

// 方法2:从GreenSock可视化工具复制
// 访问 https://gsap.com/docs/v3/Eases/CustomEase/
CustomEase.create("elasticCustom", "M0,0,C0.2,0,0.2,0.8,0.5,1,0.8,0.8,0.8,0,1,0");

// 使用
gsap.to(".box", {
  x: 300,
  duration: 2,
  ease: "myBounce",
});

使用GreenSock可视化缓动编辑器

访问 GreenSock Ease Visualizer:在线拖拽曲线控制点,画好了复制生成的字符串就行。

// 强烈弹性——超出目标很多再弹回来
CustomEase.create("superElastic", "M0,0 C0.2,0.1 0.25,1 0.25,1 C0.25,1 0.3,0.3 0.3,0.3 C0.3,0.3 0.35,1 0.35,1 C0.4,0.4 0.45,0.1 0.5,0 C0.6,-0.1 0.7,0.2 0.8,0.5 C0.9,0.8 0.95,0.6 1,1");

gsap.from(".item", {
  opacity: 0,
  y: 100,
  duration: 1.5,
  stagger: 0.1,
  ease: "superElastic",
});

把这一节跑一遍:自定义贝塞尔缓动

这个实验复用第8章真实 CustomEase 模块。缓动是 p 到 q 的函数,可独立用于对象、DOM 和 WebGL;导入注册的责任属于应用代码,不属于自由执行的 MDX 字符串。

实验:自定义贝塞尔缓动

GSAP 3.13 / 实验

亲手调整参数,观察动画差异。点击开启后再播放,不会自动启动动画。

观察与验证:用同一曲线驱动不同属性,先确保曲线端点与时间单调,再考虑超调是否适合 opacity 等有物理边界的属性。

18.9 GSDevTools

在页面底部添加时间轴控制器,支持拖拽、暂停、快进、慢放动画,调试时不需反复刷新浏览器。

// 仅在开发环境加载
if (process.env.NODE_ENV === "development") {
  GSDevTools.create();
}

// 绑定到特定动画
const tl = gsap.timeline();
tl.to(".box", { x: 200, duration: 1 });
tl.to(".box", { y: 200, duration: 1 });

GSDevTools.create({ animation: tl });

18.10 InertiaPlugin

InertiaPlugin 根据属性初速度和阻力计算自然减速。属性名是第一层:x 下放 velocity/min/max/end;resistance 和 duration 放在 inertia 配置中。

gsap.registerPlugin(InertiaPlugin);
gsap.to(slider, {
  inertia: {
    x: { velocity:200, min:0, max:500, end:gsap.utils.snap(20) },
    resistance:500,
    duration:{ min:0.5, max:3, overshoot:0 }
  }
});

end 可接受吸附函数;这里把预计终点吸附到 20px 网格,不存在 endSnap 参数。使用 velocity:'auto' 前先 InertiaPlugin.track(slider,'x'),在释放时创建惯性 Tween,并在卸载时 untrack。Observer 的实时输入速度与手工指定初速度要区分。

演示使用指定速度,便于重复验证边界;实际拖拽结合 Draggable/Observer 获取输入,并只在释放时启动惯性。

把这一节跑一遍:Inertia 速度与边界

初速度给 x,阻力设置在 inertia 层,时长由插件按规则决定。此处使用显式速度便于重复实验;真实拖拽可以先 track 获取自动速度,释放后再启动 Tween。

实验:Inertia 速度与边界

GSAP 3.13 / 实验

亲手调整参数,观察动画差异。点击开启后再播放,不会自动启动动画。

观察与验证:设正负速度比较方向;从中间开始,终点应被限制在 0 到容器最大位移之间。不要在 drag 过程中同时让另一个 Tween 写 x。

18.11 常见陷阱

陷阱1:继续使用旧会员包或混用插件版本

# 核心与全部插件均来自公开的 gsap 包
npm install gsap@3.13.0

旧项目应按 官方安装说明迁移到公开包。CDN 示例也要保持核心与插件版本一致,例如 gsap@3.13.0/dist/gsap.min.js 与 gsap@3.13.0/dist/SplitText.min.js。SplitText 3.13 已移除旧的 position: "absolute" 选项,不应继续照抄旧版本参数。

陷阱2:SplitText中文需要特殊处理

// 使用真实文本;仅拆字符时改善中英混排中的不自然断词
const st = new SplitText(".chinese-title", {
  type: "chars",
  smartWrap: true,
});

SplitText 3.13 改善了 Unicode、组合 emoji 和多语言处理,不能简单描述为 charAt 遍历。应检查实际字体、标点和换行结果,并保留原文本需要的空格;涉及拆行时,结合 autoSplit 与 onSplit 处理字体加载和宽度变化。参见 SplitText 官方文档。

陷阱3:MorphSVG路径点数不一致

两个形状的控制点数量差距过大时,变形动画会看起来很诡异。解决方案:在矢量工具中统一路径点数,或用 shapeIndex:"auto" 和开发期可视化辅助检查匹配。

陷阱4:DrawSVG的百分比参考

drawSVG 的百分比是基于路径总长度,不是 viewBox。如果路径有多个子路径,行为可能不符合预期——合并为单个路径即可。

陷阱5:把Physics2D位置误认为由ease驱动

// 注意:物理位置不会变成 power2.out 曲线
gsap.to(".ball", {
  physics2D: { velocity: 300, angle: -45, gravity: 400 },
  ease: "power2.out",  // Physics2D 的位置按时间计算;该缓动仍可影响同一 Tween 的其他属性
});

// 只做物理位置时,建议明确 ease: "none"
gsap.to(".ball", {
  physics2D: { velocity: 300, angle: -45, gravity: 400 },
  ease: "none",
});

Physics2D 使用 Tween 的时间计算位置,不使用缓动后的比例。写 ease:"none" 有助于明确物理时间映射;如果同一个 Tween 还包含 opacity 等属性,它们仍会受到 ease 影响,必要时拆成独立 Tween。