Viruatios
最初在 3/17/2026
简要介绍 GSAP (GreenSock Animation Platform),从核心概念到进阶应用。
在CuLoo’s Homepage的开发中,当 CSS 动画或原生 Web Animations API 无法满足我的需求时,我选择求助于进阶的方案——GSAP (GreenSock Animation Platform)。
GSAP 是一个稳健、高性能且零依赖的 JavaScript 动画库。
可以把它理解成一个“动画引擎”:
它不只可以操作 DOM 元素,也能对几乎所有 JavaScript 对象的数值属性做补间(Tweening)运算。
ScrollTrigger 插件,可以轻松将动画进度与页面滚动条百分百绑定。总而言之,当你觉得“setTimeout 套娃太多,或者 CSS transition/keyframes 难以管理复杂节奏”时,就是 GSAP 发挥价值的时候。
GSAP 极易上手。只要在项目中引入 gsap (可通过 npm install gsap 或 CDN),就能调用它的核心补间方法:
gsap.to():从当前状态,动画正向补间到你指定的任意属性。这是最常用的一招。gsap.from():假定目前元素的状态是“终点”,你想让它从你指定的另一个状态反向回来。常用于进场/滑入动画。gsap.fromTo():完全无视元素的当前状态,强行控制起飞阶段与落地阶段的全部属性。gsap.set():零动画持续时间(即瞬发赋值)。如果你想瞬间改变某些属性且不想要动画,用它最方便!一组完整的示例,展现它们四个的经典用法和自带黑魔法:
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 眼中,动画本质上是一个插值过程。它只关心:起始值 -> 随时间/缓动变化 -> 结束值。这个抽象非常强大,因为它不限制目标类型。你可以动画 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),甚至作为一个子片段加入到另一个更长的时间轴里。
在制作网页动画时,应遵循以下思路:
progress,再用 Math.sin() 计算角度,通常比硬切多段更顺滑。GSAP 的能量远不只是位移渐变。以下是一些可能用上的进阶特性:
这是 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,
});
这是一种很实用的进阶技巧:先补间一个“代理对象”,再在 onUpdate 里把值映射到你真正想控制的内容。
const proxy = { value: 0 };
// GSAP 不直接改 DOM,而是先改这个内存对象
gsap.to(proxy, {
value: 100,
duration: 1,
onUpdate: () => {
// 每次更新时拿到最新值,再决定如何渲染
console.log(proxy.value);
},
});
当你需要让一组元素依次入场时,无须手写 for 循环叠加 delay,直接使用 stagger:
gsap.to(".list-item", {
y: 0,
opacity: 1,
stagger: 0.1, // 每个元素依次延迟 0.1s 执行入场
});
在 React/Vue/Astro 这类组件化项目里,页面切换或组件卸载时,建议清理动画实例,避免重复绑定或内存残留。
下面是一个简化思路:
const tween = gsap.to(".box", { x: 200, duration: 1 });
// 组件卸载时
tween.kill();
如果你在同一个元素上多次创建动画,也可以考虑使用 gsap.context() 管理作用域并统一 revert()。
CuLoo404Mascot 复盘。
我把这个动画拆成了 4 步:
> < 眼,再切回。Math.sin() 驱动 3D 摇摆:保证左右摆动是连续且无停顿的。这样拆分的好处是:每一步都能单独调试,出了问题也容易定位。
这一个迷你动效系统不是“一个补间”,而是“多个补间的协同表演”。
rotateX(),浏览器会出现“看起来总往一侧转”的表现。加入 perspective(...) 的视角后,正负角度的体积变化才更容易区分。to() 的接缝停顿:把角度变化交给 Math.sin() 连续计算,通常比“0 -> 正角 -> 0 -> 负角 -> 0”的硬切更顺滑。shakeMaxAngle、shakeDuration、changeDuration 这类值要集中定义,后续微调才不会牵一发动全身。我遇到一个非常典型的工程问题:
gsap.set() 才把不该显示的元素隐藏。结果就是:组件在首屏会闪一下(FOUC,Flash Of Unstyled Content)。
这个问题的关键结论是:gsap.set() 适合“运行时状态控制”,但不应该独自承担“首帧视觉基线”。
把初始状态前移到标记层或样式层
对默认不该出现的节点(例如 eye-cross、question-exclamation、anger-symbol),直接在 SVG 里写初始 opacity="0",让浏览器第一帧就画对。
增加初始化门控
在 mascot 根节点上增加 data-mascot-ready="false",并用 CSS 规则在“JS 已启用但脚本还没初始化完”这个窗口期先隐藏整只 mascot。
避免影响无 JS 场景
只在 html.js 条件下应用“预隐藏”规则。也就是先给 html 打一个 js 类,再启用这条 CSS。这样即使用户禁用了 JS,也不会永远看不到 mascot。
初始化完成后再 reveal
当关键节点采集、resetToBaseline 完成后,再做一次轻量 fromTo 淡入,最后把 data-mascot-ready 切到 true。
初始化流程必须幂等(idempotent)
在 Astro 路由切换场景中,脚本可能重复执行。要有 dataset 绑定标记(如 culooMascotBound)防止重复监听;即使重复触发,也只做安全 reveal,不重复挂事件。
失败路径要能“解锁显示”
如果关键节点缺失导致初始化失败,也应把 data-mascot-ready 置为 true,避免组件被一直隐藏。
一句话总结:动画系统不仅要关注“动起来以后”,还要设计“动起来之前的第一帧”。
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,
);
to/from/fromTo/set,理解补间。timeline 组织多段动画节奏。ScrollTrigger 和代理对象,处理复杂交互。GSAP 官方文档: https://gsap.com/docs/v3/