GSAP 的 80/20 法則
開頭
在製作創意網站(Creative Website)時,我發現了其實真正會用到的技術沒有那麼多。只要掌握幾項核心功能,就能搞定市面上大部分的驚艷動畫,這就是網頁動態的 80/20 法則。在這篇文章中,我會介紹 GSAP Library 最常用到的 Core 功能:Timeline 與 Plugin 功能:ScrollTrigger。只要精通這些,就可以做出大部分的動畫效果,可以從 GSAP Showcase(gsap.com/showcase)看到大部分的網頁都有運用到 ScrollTrigger 功能,而 ScrollTrigger 和 Timeline 就是完美搭配。
不過,雖然這些工具看起來好學、很萬用,但實際踩進真實專案時,隱藏的暗坑可不少,以下我也整理出在用這些功能時我踩過的坑。
網頁動畫互動原理
網頁上絕大部分的酷炫互動,拆解起來就是一場數值轉換的設定而已!
我們將使用者的 Input(時間、滑鼠位置、滾輪移動速度)轉化為一個 0 到 1 的數值,然後把這個數值套用到元素的位移屬性上(如 transform、opacity、clip-path)。中間再透過 lerp、spring 或 easing curves 進行物理平滑化,畫面就會動得流暢、自然。
The Real Pattern
不論是滾動聯動還是滑鼠跟隨,所有頂級視覺都逃不出這個底層循環:
- Read Input: mouse, scroll, time
- Normalize it: map to a useful 0 to 1 range
- Apply easing: lerp, springs, easing curves
- Move stuff: transform, opacity, clip-path
這個原理可以透過底下介紹的 GSAP 功能得到實現,掌握之後就可以變出各種花樣。
Timeline 應用方式/場景
要理解 Timeline,得先認識 Tween,Tween 是 GSAP 中最小的動畫單元,源自傳統動畫的「In-between」(補間動畫),你只要宣告物件的「起點」或「終點」,中間的過渡數值,全由 GSAP 的 Ticker 自動補齊,它除了控制常見的 opacity、scale,更能調度進階的 clip-path(遮罩裁切)或 filter: blur(模糊濾鏡)。
而當多個 Tween 組合在一起,就進化成了 Timeline,過去要寫一連串的連續動畫,必須手動加上不同的 delay,只要中間微調 0.1 秒,後面幾十行程式碼就會全盤崩潰需要全部重調。Timeline 是一個自動排列容器,只要把 Tween 照順序放入 Timeline 中,就會一個個排序播放,透過 Timeline 的參數(如 "-=0.5" 提早進場、"<" 與上一個同時進場),也能打破生硬的線性播放,創造有呼吸感的自然視覺。
程式碼範例
const tl = gsap.timeline();
tl.to(".hero-title", { y: 0, opacity: 1, duration: 1 })
.to(".hero-bg-img", { scale: 1 }, "-=0.4") // 標題還沒跑完,背景圖就提早 0.4 秒開始放大
.from(".nav-item", { opacity: 0 }, "<"); // 導覽列跟「背景圖」完全在同一瞬間開始淡入避坑指南
1. 千萬不要在 GSAP 和 CSS 裡改同一個物件的屬性
當 GSAP 在每 16 毫秒用 Ticker 暴力修改物件的 style 時,你的 CSS transition 會試圖去攔截 GSAP 的數值,導致兩者在瀏覽器底層瘋狂打架,畫面看起來會像在抽搐一樣瘋狂抖動。只要該物件要交給 GSAP 控制,CSS 裡就絕對不能有該屬性的 transition,屬性需要錯開來寫,不能同時讓兩邊控制,或是讓 GSAP 在動畫完成之後 call onComplete,把主導權還給 CSS。
2. GSAP 動不了的屬性
top、left、margin、width、padding 被稱為佈局屬性(Layout Properties),如果用 GSAP 去改動,那網頁需要重新計算所有元素的相對位置(觸發 Reflow),會讓流暢的動畫變得卡頓,CPU 工作量瞬間暴增,要改位置的話用 x, y 也就是對應 CSS 裡的 transform: translate()。那些沒有中間數學連續值的屬性,例如從 display: none 切換到 block、position: absolute 切換到 fixed、width: auto 變到固定數值,對 GSAP 來說是物理無解的,要改用 opacity、autoAlpha 或進階插件處理。
ScrollTrigger Viewport 應用方式/場景
這是讓動畫與物件是否進入可視區域 Viewport 產生聯動,就像網頁上的感應門,人走過去門就打開,人離開門就關上。
想像你的網頁裡有兩個正在移動的容器:
- 螢幕視窗(Viewport):它是隨著你滾輪移動的視角,它身上帶有兩根偵測線 Viewport Start 和 Viewport End。
- 目標元件(Trigger Element):它是網頁上的元素,它身上也畫了兩根觸發線 Element Start 和 Element End。
所謂的觸發動畫,就是這場雙線交會的時刻,當視窗的偵測線滾動到網頁的某個位置,碰上元素的觸發線時,動畫就正式開演(Start);反之,當兩者的結束線相交時,這段表演就宣告結束(End)。
除了設定 start 和 end 的觸發點,toggleActions 可以來決定進入、離開、再次進入時動畫要播放還是反轉,能設定 4 條線不同的交會方式會 trigger 甚麼:進入(onEnter) 就是 Viewport Start 跟 Element Start 交疊、離開(onLeave) 是 Viewport End 跟 Element End 相交、再次進入(onEnterBack) 是 Viewport End 重新倒退碰到 Element End、再次離開(onLeaveBack) 是 Viewport Start 再次碰到 Element Start。如果對於兩個容器的線感到陌生不清楚,也可以把 markers 設為 true,就可以在網頁上看到容器的 viewport 的觸發線。
程式碼範例
gsap.from(".portfolio-card", {
y: 50,
scrollTrigger: {
trigger: ".portfolio-card",
start: "top 80%", // 當「元素的頂部 (top)」碰到「視窗的下方 80% (80%)」時觸發
end: "bottom 20%", // 當「元素的底部 (bottom)」碰到「視窗的上方 20% (20%)」時結束
markers: true, // 顯示除錯標籤!
}
});避坑指南
1. 元件的 Start 點設定比網頁頂端還高
對於網頁一開屏就要播放的 Hero Section 元素,不要用 ScrollTrigger 去觸發它!一律直接用一般的 gsap.timeline() 讓它網頁載入完直接開演;第二屏以下的元素,才交給 ScrollTrigger。
2. 手機版瀏覽器工具列縮放導致的畫面抽搐
手機瀏覽器在往下滑動時,上方的網址列或下方的工具列會自動收起,這會導致手機的 Viewport 高度(100vh)突然變大。因為視窗變高了,ScrollTrigger 為了維持精準度,會強迫在滾動中重新計算所有偵測線(Reflow),畫面因此產生瞬間位移和抽搐。透過全域設定來限制:ScrollTrigger.config({ ignoreMobileResize: true })
3. Lazy Load 導致動畫消失
現代網頁為了效能很常使用圖片懶載入(Lazy Loading)。但這會導致網頁滾動時,圖片加載完成的瞬間直接「蓋過」了 GSAP 動畫的初始幀,畫面上就會看到圖片生硬地閃現,沒有任何滑順轉場。第一屏的 Hero 圖片絕對不開 lazy load;第二屏底下的圖片,務必確保在圖片的 onload 事件觸發後,才將 GSAP 的 ScrollTrigger 動態掛載上去,確保動畫與加載順序完美銜接。
ScrollTrigger Scrub 應用方式/場景
如果說 Viewport 是網頁上的感應門,那麼 Scrub 就是你手裡的「遙控器」,它是基於進度綁定,你往下滑動多少,動畫就走多少,動畫出現的進度快慢由你掌控。
與 Viewport 的核心不同:Viewport 只是去測量兩條線何時相撞,撞到後動畫就自己照著預設的時間播完,使用者的手指就算停下來,動畫也不會停。而 Scrub 則是把使用者的滾輪位置,100% 綁定在動畫的時間軸進度上。
和 Scrub 最常搭配的好夥伴 pin,是 ScrollTrigger 用來釘住固定元素的屬性。當你往下捲動網頁時,預設所有元素都會跟著往上推並移出螢幕。但如果你設定了 pin: true,GSAP 會在底層自動動態將該元素加上 position: fixed,讓該元素像黏在螢幕上一樣,強迫使用者在滾動時留在原地,直到指定的滾動範圍結束後才解鎖。
程式碼範例
// 動畫本身的 duration 在 scrub 世界裡失效了
// 因為時間現在歸滾輪管!
gsap.to(".scrolling-car-parts", {
x: 200,
rotation: 180,
scale: 0.8,
ease: "none", // Scrub 動畫的 ease 一律設為 none
scrollTrigger: {
trigger: ".product-section",
start: "top top",
end: "+=1500", // 延長 1500px 的滾動距離來當作這段動畫的進度條
pin: true, // 釘住畫面,強迫看完這段秀才能繼續往下滾
scrub: 1.2, // 數字代表 1.2 秒的物理緩衝煞車感
}
});避坑指南
1. Scrub 結尾暴衝
當你的 ScrollTrigger 滾動區間(start 到 end 的距離)設定得太短(例如 100px),但動畫中元素位移的物理距離卻很長(例如 x: 500)。因為設定了平滑緩衝(scrub: 1),當網頁快要滾動到 end 終點時,GSAP 為了在邊界強行追上進度,會在結尾處產生一陣突兀的趕進度加速。這時要拉寬滾動範圍,並調高 scrub 的數值。
2. Pin 碰上 overflow
GSAP 的 pin: true 在底層是利用 CSS 的 position: fixed 來把元素鎖在螢幕上的,但在 CSS 規範中,只要父層容器設定了 overflow-x: clip 或 overflow: hidden,就會強行建立一個 Containing Block(包含塊)。這會沒收 position: fixed 的全螢幕鎖定能力,讓它被困在父層容器的骨架範圍內,導致畫面直接位移。這時建議放棄 GSAP 的 pin,改用網頁原生的 CSS Sticky + GSAP 純動畫。
總結
在 Creative Coding 的世界裡,真正的大師從來不是因為他背熟了 GSAP 官網所有的 API 字典,而是他能用核心工具,去編演出驚艷市場且體貼使用者的絕佳互動。
但我也發現無論是運用 Timeline 精準調度,還是用 ScrollTrigger 的邏輯去操控動畫,最重要的仍是對瀏覽器底層科學(Reflow、Containing Block、網絡下載速度差)有基本的理解與掌握,才能在這些基底上變化出更多的可能性。