PUMPKIN 萬聖節:用 GSAP ScrollTrigger 與 three.js 讓捲動變成一段旅程
2024年10月30日

為什麼做這個專案
萬聖節前想做一個「只有一頁、但一路往下滑都有驚喜」的網頁。主題定為 Season of Fire & Gold:黑底、火金色漸層、飄散的餘燼,讓捲動本身變成一段短短的旅程。
整個專案只有一個 index.html,不用任何建置工具,三個函式庫都從 CDN 載入:
| 項目 | 使用技術 |
|---|---|
| 捲動動畫 | GSAP 3.12 + ScrollTrigger |
| 3D 場景 | three.js r128 |
| 字體 | Cinzel、Cinzel Decorative |
| 部署 | GitHub Pages |
一路往下滑會看到什麼
- 開場:大大的 PUMP / KIN 漸層標題,背景是 2,200 顆會隨時間與捲動緩慢旋轉的粒子
- The Golden Ones:三張傾斜的 3D 卡片,滑進畫面時依序登場,滑鼠移上去會跟著傾斜,移開時再「彈」回原位
- 莫比烏斯環:整頁最核心的 3D 場景,下面詳細說明
- CARVED / IN / FIRE:文字從左右兩側滑入的視差段落
- 結尾: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 的內建材質,也能做出有層次的視覺效果。