网页动画:GSAP

Viruatios

最初在 3/17/2026

简要介绍 GSAP (GreenSock Animation Platform),从核心概念到进阶应用。

JavaScript

Web Development

Animation

GSAP

tutorial

Personal Journey

CuLoo404Mascot

在CuLoo’s Homepage的开发中,当 CSS 动画或原生 Web Animations API 无法满足我的需求时,我选择求助于进阶的方案——GSAP (GreenSock Animation Platform)

一、什么是 GSAP?它适合用在哪?

GSAP 是一个稳健、高性能且零依赖的 JavaScript 动画库。

可以把它理解成一个“动画引擎”:

它不只可以操作 DOM 元素,也能对几乎所有 JavaScript 对象的数值属性做补间(Tweening)运算。

适用场景:

  1. 复杂的网页叙事与特效:比如苹果官网那种丝滑的滚动视差效果、产品拆解动画。
  2. SVG 路径与图形动画:复杂形态变化、描边动画,或像我的 404 页面中吉祥物的动态 3D 旋转。
  3. 基于滚动的交互:配合官方著名的 ScrollTrigger 插件,可以轻松将动画进度与页面滚动条百分百绑定。
  4. WebGL / Canvas 结合:不操作 DOM 的情况下,使用 GSAP 去平滑插值 Three.js 或 Canvas 中的相机和多边形属性。

总而言之,当你觉得“setTimeout 套娃太多,或者 CSS transition/keyframes 难以管理复杂节奏”时,就是 GSAP 发挥价值的时候。


二、基础使用方法:快速入门

GSAP 极易上手。只要在项目中引入 gsap (可通过 npm install gsap 或 CDN),就能调用它的核心补间方法:

1. 核心四大方法

2. 基础示例

一组完整的示例,展现它们四个的经典用法和自带黑魔法:

import gsap from "gsap";

// 【示例 1:使用 gsap.to() 做交互过渡】
// 意图:让这个按钮向右移动并变成圆形,带回弹缓冲
gsap.to(".btn-submit", {
	x: 200,
	duration: 1.5,
	borderRadius: "50%",
	ease: "bounce.out", // 内置的回弹物理算法
});

// 【示例 2:使用 gsap.from() 做元素加载出场】
// 意图:假设卡片本来就在中心位置,现在让它“从”很远的下方、透明度为 0 的状态飘上来
gsap.from(".card-item", {
	y: 100,
	opacity: 0,
	duration: 1,
	ease: "power2.out",
});

// 【示例 3:使用 gsap.fromTo() 做绝对掌控】
// 意图:不关心元素当前变成了啥样,直接强行将背景色从红色过渡到蓝色,并缩放
gsap.fromTo(
	".loading-circle",
	{ backgroundColor: "#ff0000", scale: 0.5 }, // 起点状态(不填写 duration)
	{
		backgroundColor: "#0000ff",
		scale: 1.5,
		duration: 2,
		repeat: -1, // 定义无限重播
		yoyo: true, // 像溜溜球一样原路倒放
	},
);

// 【示例 4:使用 gsap.set() 瞬发赋值】
// 当你想要初始化某些不受控样式时直接上这个,免去了手写 CSS 的烦恼
gsap.set(".hidden-element", { display: "block", opacity: 1 });

提示:x, y 是 GSAP 对 CSS transform: translate() 的便捷写法。

桥接理解:to/from/fromTo/set 属于“补间层”,解决单段动画;timeline 属于“编排层”,负责把多段动画组织成完整节奏。


三、GSAP 的核心哲学与设计原则

在使用 GSAP 设计动画时,最实用的思路是:先写清楚“时间节奏”,再填充“视觉表现”。

1. 核心哲学:一切皆是“补间 (Tween)”与“时间轴 (Timeline)”

在 GSAP 眼中,动画本质上是一个插值过程。它只关心:起始值 -> 随时间/缓动变化 -> 结束值。这个抽象非常强大,因为它不限制目标类型。你可以动画 DOM、SVG,也可以动画一个普通对象。

更伟大的突破是它的 Timeline (时间轴) 概念:

const tl = gsap.timeline();
tl.to(".box1", { x: 100, duration: 1 })
	.to(".box2", { y: 50, duration: 0.5 }, "-=0.3") // 提前0.3秒插入
	.to(".box3", { rotation: 360 });

此时 tl 就相当于一个视频序列(Sequence),它可以被随时暂停 (Pause)、快进 (Play)、倒退 (Reverse),甚至作为一个子片段加入到另一个更长的时间轴里。

2. 动画设计的原则角度

在制作网页动画时,应遵循以下思路:


四、进阶用法

GSAP 的能量远不只是位移渐变。以下是一些可能用上的进阶特性:

1. ScrollTrigger (基于滚动的控制)

这是 GSAP 最常用的插件之一,可以把动画进度与某个元素的滚动位置精确绑定(即常说的 scrub)。

注意:在模块化项目中,你通常需要先注册插件。

import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";

gsap.registerPlugin(ScrollTrigger);

gsap.to(".hero-title", {
	scrollTrigger: {
		trigger: ".hero-section",
		start: "top center",
		end: "bottom top",
		scrub: true, // 开启擦洗,随着鼠标滚轮前后精密控制动画
	},
	y: 100,
	opacity: 0,
});

2. Proxy Animation (状态代理插值)

这是一种很实用的进阶技巧:先补间一个“代理对象”,再在 onUpdate 里把值映射到你真正想控制的内容。

const proxy = { value: 0 };
// GSAP 不直接改 DOM,而是先改这个内存对象
gsap.to(proxy, {
	value: 100,
	duration: 1,
	onUpdate: () => {
		// 每次更新时拿到最新值,再决定如何渲染
		console.log(proxy.value);
	},
});

3. 交错序列 (Staggers)

当你需要让一组元素依次入场时,无须手写 for 循环叠加 delay,直接使用 stagger

gsap.to(".list-item", {
	y: 0,
	opacity: 1,
	stagger: 0.1, // 每个元素依次延迟 0.1s 执行入场
});

4. 工程实践:在框架里正确清理动画

在 React/Vue/Astro 这类组件化项目里,页面切换或组件卸载时,建议清理动画实例,避免重复绑定或内存残留。

下面是一个简化思路:

const tween = gsap.to(".box", { x: 200, duration: 1 });

// 组件卸载时
tween.kill();

如果你在同一个元素上多次创建动画,也可以考虑使用 gsap.context() 管理作用域并统一 revert()


五、Personal Journey:CuLoo404Mascot 是怎么做出来的

CuLoo404Mascot 复盘。

1. 制作过程回顾

我把这个动画拆成了 4 步:

  1. 先画静态 SVG 结构:六边形主体、眼睛、嘴巴、问号感叹号、前后轨道。
  2. 再拆动画层级:把“身体轮廓”与“面部表情”分组,避免互相干扰。
  3. 用 GSAP timeline 编排表情变化:正常眼切换到 > < 眼,再切回。
  4. 用代理对象 + Math.sin() 驱动 3D 摇摆:保证左右摆动是连续且无停顿的。

这样拆分的好处是:每一步都能单独调试,出了问题也容易定位。

2. 设计

这一个迷你动效系统不是“一个补间”,而是“多个补间的协同表演”。

3. 最需要注意的细节

  1. SVG 3D 旋转的投影问题:如果只写 rotateX(),浏览器会出现“看起来总往一侧转”的表现。加入 perspective(...) 的视角后,正负角度的体积变化才更容易区分。
  2. 避免多段 to() 的接缝停顿:把角度变化交给 Math.sin() 连续计算,通常比“0 -> 正角 -> 0 -> 负角 -> 0”的硬切更顺滑。
  3. 时间参数统一管理:像 shakeMaxAngleshakeDurationchangeDuration 这类值要集中定义,后续微调才不会牵一发动全身。
  4. 避免重复绑定事件:如果组件会重复挂载,记得在销毁时清理监听与动画实例。
  5. 主次动作分离:摇摆是主动作,眼神和标点是点缀动作。不要让所有元素同时做大幅变化,否则会造成视觉噪声。
  6. 尽可能依赖GSAP:原生的CSS动画会因为浏览器等各种原因出现不稳定的表现,GSAP的补间引擎能帮助实现平滑过渡,减少意外。

4. 具体来说说一次实现过程中的经验教训:重视首帧

我遇到一个非常典型的工程问题:

结果就是:组件在首屏会闪一下(FOUC,Flash Of Unstyled Content)。

这个问题的关键结论是:gsap.set() 适合“运行时状态控制”,但不应该独自承担“首帧视觉基线”

我最终采用的方案

  1. 把初始状态前移到标记层或样式层 对默认不该出现的节点(例如 eye-crossquestion-exclamationanger-symbol),直接在 SVG 里写初始 opacity="0",让浏览器第一帧就画对。

  2. 增加初始化门控 在 mascot 根节点上增加 data-mascot-ready="false",并用 CSS 规则在“JS 已启用但脚本还没初始化完”这个窗口期先隐藏整只 mascot。

  3. 避免影响无 JS 场景 只在 html.js 条件下应用“预隐藏”规则。也就是先给 html 打一个 js 类,再启用这条 CSS。这样即使用户禁用了 JS,也不会永远看不到 mascot。

  4. 初始化完成后再 reveal 当关键节点采集、resetToBaseline 完成后,再做一次轻量 fromTo 淡入,最后把 data-mascot-ready 切到 true

  5. 初始化流程必须幂等(idempotent) 在 Astro 路由切换场景中,脚本可能重复执行。要有 dataset 绑定标记(如 culooMascotBound)防止重复监听;即使重复触发,也只做安全 reveal,不重复挂事件。

  6. 失败路径要能“解锁显示” 如果关键节点缺失导致初始化失败,也应把 data-mascot-ready 置为 true,避免组件被一直隐藏。

一句话总结:动画系统不仅要关注“动起来以后”,还要设计“动起来之前的第一帧”。

5. 一段核心思路伪代码

const state = { progress: 0, angle: 0 };

// 主时间轴:统一编排角色动作
const tl = gsap.timeline({ paused: true });

// 连续摇摆:用 progress 驱动正弦角度
tl.to(
	state,
	{
		progress: 1,
		duration: shakeDuration * 4,
		ease: "none",
		onUpdate: () => {
			state.angle = Math.sin(state.progress * Math.PI * 2) * shakeMaxAngle;
			updateTransform(state.angle);
		},
	},
	0,
);

// 表情变化:与摇摆并行插入
tl.to(normalEyes, { opacity: 0, duration: 0.2 }, 0).to(
	crossEyes,
	{ opacity: 1, duration: 0.2 },
	0,
);

六、结语

  1. 先熟练 to/from/fromTo/set,理解补间。
  2. 再用 timeline 组织多段动画节奏。
  3. 最后引入 ScrollTrigger 和代理对象,处理复杂交互。

GSAP 官方文档: https://gsap.com/docs/v3/