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-swapctx.revert() 一次撤乾淨。動畫寫得再漂亮,不會收拾自己就是記憶體洩漏製造機。