Lenis + GSAP:讓網站慢下來的捲動與動畫配方
- 動畫
- 前端
這個站的動態設計只有一個原則:慢。捲動要有慣性,元素進場要從容,緩動曲線拉得很長。技術上是兩個庫的組合:Lenis 負責捲動,GSAP 負責動畫。
Lenis:把捲動的 lerp 調低
import Lenis from 'lenis';
if (!matchMedia('(prefers-reduced-motion: reduce)').matches) {
const lenis = new Lenis({ autoRaf: true, lerp: 0.08 });
(window as Window & { lenis?: Lenis }).lenis = lenis;
}
lerp 是每一幀往目標捲動位置靠近的比例,預設約 0.1,我調到 0.08 讓捲動更綿長。autoRaf: true 讓 Lenis 自己接管 requestAnimationFrame,不用手寫迴圈。掛在 window 上是為了讓各頁的動畫腳本拿得到同一個實例。
注意第一行:使用者偏好減少動態時,根本不建立 Lenis,還給瀏覽器原生捲動。
跟 ScrollTrigger 同步
GSAP 的 ScrollTrigger 需要知道「現在捲到哪」,而 Lenis 改寫了捲動行為,兩者必須接起來,不然觸發點會飄:
const lenis = (window as Window & { lenis?: Lenis }).lenis;
lenis?.on('scroll', ScrollTrigger.update);
一行就好:Lenis 每次更新捲動位置,就通知 ScrollTrigger 重算。
進場動畫:data-reveal
頁面上要進場的元素只掛一個 data-reveal 屬性,腳本統一處理:
gsap.utils.toArray<HTMLElement>('[data-reveal]').forEach((el) => {
gsap.from(el, {
y: 28,
opacity: 0,
duration: 1.1,
ease: 'lux',
scrollTrigger: { trigger: el, start: 'top 88%' },
});
});
lux 是用 CustomEase 註冊的自訂曲線,跟 CSS 裡的 --ease-lux 是同一條(cubic-bezier(0.12, 0.23, 0.17, 0.99))——前段快、尾巴拖很長,慢優雅的關鍵就在這條曲線。CSS transition 和 GSAP 動畫共用同一條 ease,整個站的動態才會像同一個人做的。
View Transitions 下的清理
Astro 的 <ClientRouter /> 換頁不會重新整理,舊頁面的 ScrollTrigger 如果不清掉,會留著一堆指向已卸載 DOM 的觸發器。做法是把所有動畫包進 gsap.context(),在 astro:before-swap 時 ctx.revert() 一次撤乾淨。動畫寫得再漂亮,不會收拾自己就是記憶體洩漏製造機。