PUMPKIN 萬聖節:用 GSAP ScrollTrigger 與 three.js 讓捲動變成一段旅程

2024年10月30日

PUMPKIN 首頁

為什麼做這個專案

萬聖節前想做一個「只有一頁、但一路往下滑都有驚喜」的網頁。主題定為 Season of Fire & Gold:黑底、火金色漸層、飄散的餘燼,讓捲動本身變成一段短短的旅程。

整個專案只有一個 index.html,不用任何建置工具,三個函式庫都從 CDN 載入:

項目使用技術
捲動動畫GSAP 3.12 + ScrollTrigger
3D 場景three.js r128
字體Cinzel、Cinzel Decorative
部署GitHub Pages

一路往下滑會看到什麼

  1. 開場:大大的 PUMP / KIN 漸層標題,背景是 2,200 顆會隨時間與捲動緩慢旋轉的粒子
  2. The Golden Ones:三張傾斜的 3D 卡片,滑進畫面時依序登場,滑鼠移上去會跟著傾斜,移開時再「彈」回原位
  3. 莫比烏斯環:整頁最核心的 3D 場景,下面詳細說明
  4. CARVED / IN / FIRE:文字從左右兩側滑入的視差段落
  5. 結尾:Happy Halloween,以及持續從畫面底部升起的火花

另外還做了自訂游標:一個圓環加上一個延遲跟隨的外圈,用 gsap.ticker 每一幀更新位置。


三張卡片,其實是同一支影片

三張卡片「Golden Lantern」、「Ember Spirit」、「Midnight Glow」看起來是三顆不同的南瓜,其實播的是同一支 pumpkin.mp4,只是用 CSS 的 hue-rotate 轉成不同色調。

滑鼠傾斜的效果,是把游標在卡片上的位置換算成 rotateX、rotateY,交給 GSAP 補間;滑鼠離開時改用 elastic.out,卡片就會有一點彈性地回正。


主角:會隨捲動旋轉的莫比烏斯環

這一段是 400vh 高的 sticky 區塊:畫面固定不動,捲動距離全部拿來驅動 3D 場景。

1. 先畫出 ∞ 形的中心線

莫比烏斯環的骨架是一條「雙紐線」(lemniscate),也就是 ∞ 的形狀。但純平面的 ∞ 在中間交叉處會讓管子自己穿過自己,所以額外加了一點 z 方向的起伏:

function lemni(t) {
  const s2 = Math.sin(t) * Math.sin(t);
  const d  = 1 + s2;
  const x  =  a * Math.cos(t) / d;
  const y  =  a * Math.sin(t) * Math.cos(t) / d;
  const z  =  0.6 * Math.sin(t); // 加一點深度,避免交叉處互相穿插
  return new THREE.Vector3(x, y, z);
}

2. 沿著中心線轉半圈

莫比烏斯環的特性是「只有一個面」:沿著環走一圈,會回到起點的背面。做法是在繞管子一圈的角度上,再額外加上一個隨位置增加、總共 π 的相位:

// s 掃過 [0, 2π],再額外加上 π 的相位,形成半圈扭轉
const s = (j / VSEG) * Math.PI * 2 + uNorm * Math.PI;

管子每一段的方向用平行移動(parallel transport)計算,避免法線在轉彎處突然翻轉,表面才不會出現破面。

環上還有一顆沿著軌道移動的光球、1,200 顆貼著表面的粒子,以及 40 個漂浮的數學符號,例如 eⁱπ+1=0、∇×E。

3. 把捲動進度接到 3D 場景

ScrollTrigger 只負責回報「目前捲到這一段的百分之幾」,真正的旋轉在 three.js 的渲染迴圈裡計算:

ScrollTrigger.create({
  trigger: '#spin-section',
  start: 'top top', end: 'bottom bottom',
  scrub: 1.2,
  onUpdate: self => { scrollProgress = self.progress; }
});
 
// render() 裡
group.rotation.y = t * 0.18 + p * Math.PI * 3;

scrub: 1.2 讓旋轉稍微落後捲動一點點,動起來比較有重量感。整段捲完,環會轉一圈半,文字也隨著進度依序出現:One Surface / No Boundary、方程式,最後是 χ = 0(莫比烏斯環的歐拉示性數)。


小結

這個專案最好玩的地方,是讓捲動取代播放鍵:使用者滑多快,故事就走多快。技術上沒有用到自訂 shader 或建置工具,只靠 GSAP 和 three.js 的內建材質,也能做出有層次的視覺效果。

🌐 線上 Demo | 💻 GitHub