滾輪動畫(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 時銷毀觸發器。
Vanilla — 一個在 DOM 就緒後執行的 main.js;按區段使用 gsap.context() 分組。
4. 可避免 90% 錯誤的 ScrollTrigger 規則
- 將
scrollTrigger設定在時間軸或頂層 tween 上——絕不要設定在時間軸內部的巢狀 tween 上。 - 使用
scrub時搭配ease: "none"。 - 建立順序很重要:按頁面順序從上到下建立觸發器(或設定
refreshPriority),特別是在使用固定功能時。 - 固定一個包裝器,而不是你正在動畫化的元素。不要在設有
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(transform + opacity)進行動畫。避免對width/height/top/left以及大面積的filter: blur()進行動畫。 - 每個區段偏好使用一個時間軸,而不是數十個單獨的觸發器;對於大量相同的揭示效果,使用
ScrollTrigger.batch。 - 對於跟隨游標的效果,使用
gsap.quickTo。 - 刷洗場景內的大圖片:使用合適的
sizes屬性,並避免每一幀都縮放大型點陣圖。
7. 驗證
快速上下滾動整頁;縮放視窗;在頁面中間重新載入;導航離開再返回(SPA)——不應有跳動、不應有重複的固定元素、不應有主控台錯誤。在開啟減少動畫、375px 寬度以及啟用 CPU 節流的情況下進行測試。
常見效果配方:../motion-direction/references/patterns.md。