Scroll Animation (GSAP + ScrollTrigger + Lenis)
如果已安装 GreenSock 的官方技能(gsap-core、gsap-scrolltrigger、gsap-react、gsap-plugins),请使用它们获取 API 详情;本技能涵盖项目设置、集成以及导致实际站点出错的常见错误。
1. 安装
npm i gsap lenis
npm i @gsap/react # React / Next.js 专用
所有 GSAP 插件(ScrollTrigger、SplitText、Flip、ScrollSmoother、MorphSVG、DrawSVG…)都包含在公共 gsap 包中,并且可以免费用于商业项目。从 gsap/<PluginName> 导入并调用 gsap.registerPlugin(...) 一次即可。
2. 单一设置模块
创建 lib/motion/setup.(js|ts)(或 animations/),在客户端仅导入一次:
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { SplitText } from "gsap/SplitText";
import Lenis from "lenis";
import "lenis/dist/lenis.css";
gsap.registerPlugin(ScrollTrigger, SplitText);
export function initSmoothScroll() {
if (matchMedia("(prefers-reduced-motion: reduce)").matches) return null;
const lenis = new Lenis();
lenis.on("scroll", ScrollTrigger.update);
const tick = (time) => lenis.raf(time * 1000); // GSAP 时间单位为秒,Lenis 需要毫秒
gsap.ticker.add(tick);
gsap.ticker.lagSmoothing(0);
return () => { gsap.ticker.remove(tick); lenis.destroy(); };
}
请仅使用 Lenis 或 GSAP ScrollSmoother,不要同时使用。
3. 框架集成
React / Next.js(App Router) — 使用 GSAP 的文件需要添加 "use client"。始终使用 useGSAP(自动清理,严格模式下不会重复执行):
"use client";
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(useGSAP, ScrollTrigger);
export function Features() {
const root = useRef<HTMLElement>(null);
useGSAP(() => {
const mm = gsap.matchMedia();
mm.add("(prefers-reduced-motion: no-preference)", () => {
gsap.from(".feature", {
autoAlpha: 0, y: 32, stagger: 0.08, ease: "power3.out",
scrollTrigger: { trigger: root.current, start: "top 75%" },
});
});
}, { scope: root }); // 选择器限定在该组件范围内
return <section ref={root}>…</section>;
}
- 创建动画的事件处理函数:使用
useGSAP中的contextSafe包裹。 - 平滑滚动提供者:在根布局中创建一个客户端组件,在 effect 中调用
initSmoothScroll()(或使用lenis/react中的ReactLenis,设置autoRaf: false并结合 GSAP ticker)。 - Next.js 中的路由变化:滚动到顶部(
lenis.scrollTo(0, { immediate: true })),并在新页面挂载后调用ScrollTrigger.refresh()。
Vue / Nuxt — 在 onMounted 中使用 gsap.context(() => {…}, rootEl) 创建,在 onUnmounted 中调用 ctx.revert()。Nuxt:使用客户端专属插件(plugins/gsap.client.ts)。
Astro / 静态多页面 — 在布局中使用 <script> 标签;配合 View Transitions 时,在 astro:page-load 事件中重新初始化,在 astro:before-swap 事件中销毁触发器。
纯 JS — 创建一个在 DOM 就绪后执行的 main.js;使用 gsap.context() 按区域分组。
4. ScrollTrigger 规则(可避免 90% 的 Bug)
- 将
scrollTrigger放在时间线或顶级 tween 上—切勿放在嵌套在时间线内的 tween 上。 - 使用
scrub时请同时使用ease: "none"。 - 创建顺序很重要:按页面从上到下的顺序创建触发器(或设置
refreshPriority),特别是使用固定(pin)功能时。 - 固定一个容器,而不是你动画化的元素。不要在具有
transform属性的父元素内部固定。除非有特殊意图,否则避免使用pinSpacing: false。 - 对于依赖视口大小的属性,使用函数值(
x: () => …)并加上invalidateOnRefresh: true。 - 在图片、字体或动态内容改变布局后,调用
ScrollTrigger.refresh()(window.addEventListener("load", …),document.fonts.ready)。 - 使用
markers: true进行调试—上线前请移除。 - 初始隐藏状态:使用
gsap.from/fromTo或通过 JS 设置autoAlpha,这样即使没有 JS 内容也是可见的。 - 对于不应重复播放的揭示效果,使用
once: true(大多数情况都适用)。
5. 响应式与减少动画偏好
在每个场景外包裹 gsap.matchMedia():
const mm = gsap.matchMedia();
mm.add({
isDesktop: "(min-width: 1024px)",
isMobile: "(max-width: 1023px)",
motionOK: "(prefers-reduced-motion: no-preference)",
}, ({ conditions: c }) => {
if (!c.motionOK) return; // 静态布局,无动画
if (c.isDesktop) { /* 固定 / 水平滚动场景 */ }
if (c.isMobile) { /* 仅简单淡入上移效果 */ }
});
matchMedia 在条件变化时自动恢复—无需手动清理。
6. 性能
- 动画化
x/y/scale/rotation/autoAlpha(变换和透明度)。避免动画化width/height/top/left以及在大面积上使用高消耗的filter: blur()。 - 优先为每个区域使用一个时间线,而不是使用数十个独立的触发器;对于大量相同的揭示效果,使用
ScrollTrigger.batch。 - 使用
gsap.quickTo实现指针跟随效果。 - 在擦洗场景中的大图:使用合适的
sizes属性,并避免每帧缩放巨大的位图。
7. 验证
快速上下滚动整个页面;调整窗口大小;在页面中间重新加载;导航离开后再返回(SPA)— 无跳跃、无重复固定区域、无控制台错误。在启用减少动画偏好、375px 宽度以及开启 CPU 节流的情况下进行测试。
常见效果配方:../motion-direction/references/patterns.md。