Apple 風格滾動動畫
效果是由滾動位置擦洗,而非由計時器觸發。向上滾動時會反向播放。無需任何程式庫。
三個核心概念
- 固定(Pin)。一個高的外層容器(例如
height: 300vh)包含一個position: sticky; top: 0; height: 100vh; overflow: hidden的子元素。該子元素會停留在畫面上,而外層容器則繼續滾動。外層容器的額外高度就是您的動畫預算。 - 進度。一個函數將滾動位置轉換為 0..1 之間的數字:
- 固定區段:
clamp(-rect.top / (rect.height - innerHeight)) - 元素進入視口:
clamp((innerHeight*a - rect.top) / (innerHeight*(a-b)))
- 固定區段:
- 根據數字驅動視覺效果。每幀設定
translate、rotate、scale、opacity(和borderRadius)。一個使用requestAnimationFrame節流的scroll監聽器會重新計算所有內容。
可直接套用的引擎:references/scroll-engine.js。
效果良好的動畫(選擇 3-5 個,不要全用)
| 效果 | 做法 |
|---|---|
| 組裝 | 項目從視口邊緣各處散落(隨機角度、旋轉、縮放 .35、透明度 .2)並漸變到其佈局位置。載入/調整大小時計算每個項目到其自然位置的偏移量,然後乘以 (1 - ease(p))。按索引交錯。 |
| 逐字發光 | 將每個單字包裹在 span 中。透明度 .18 → 1 作為 clamp(p * 1.25 * N - k)。 |
| 固定步驟器 | step = floor(p * N);分數 p*N - step 填充活動步驟內的進度條。點擊步驟應 scrollTo 該步驟的位置。 |
| 傾斜裝置模型 | 對於手機/裝置模型:perspective(1400px) rotateX((1-e)*22deg) translateY((1-e)*60px) scale(.84 + .16*e)。 |
| 卡片浮入 | translate: 0 (1-e)*80px; scale: .9+.1e; opacity: e,按欄位延遲(i % cols)。 |
| 展開 | 影片/圖片 scale .78 → 1 以及 borderRadius 44px → 28px。 |
在應用進度之前,使用 ease = t => 1 - (1-t)**3。
規則(從經驗中學到)
- 使用個別的 CSS 屬性
translate、rotate、scale進行擦洗,並將transform保留給滑鼠懸停。否則滑鼠懸停和滾動會爭奪同一個屬性。 - 不要在同一個元素上同時使用
.reveal淡入類別和擦洗的 transform。選擇其中一個。 - 將區段標題放在固定面板內部與內容一起。如果面板只包含內容並且居中顯示,高螢幕會出現大片空白區域。
- 保持滾動距離適中。對於 N 步驟的步驟器,約為
N * 55vh + 100vh;對於一次性效果,約為240-300vh。使用者會覺得 700vh 以上很累人。 - 限制固定區塊的內容以適應 100vh。在短螢幕(
max-height: 820px)和手機上,隱藏副標題和其他額外內容,並在步驟器中僅顯示當前活動步驟。 - 減少動畫:如果
prefers-reduced-motion: reduce,則在所有地方回傳進度1(最終狀態,無移動)。不要讓內容保持隱藏。 - Sticky 定位的上層元素不需要
overflow,除了body上的overflow-x: hidden(這是可以的)。避免在 sticky 元素和頁面之間的任何包裝器上使用overflow: hidden。 - 在調整視窗大小時重新計算(組裝偏移量依賴於佈局)。
- 僅對
translate/rotate/scale/opacity/border-radius進行動畫處理;避免使用會觸發佈局的屬性。 - 避免影片的開頭或結尾片段過短:如果擦洗
<video>播放,短片段可能顯示為空白。
典型頁面順序
英雄區(固定、組裝)→ 一個簡單陳述(無效果)→ 證明/影片(展開)→ 固定步驟器 → 裝置模型(傾斜裝置模型)→ 卡片網格(浮入)→ 行動呼籲。交替使用黑色和淺灰色區段以營造 Apple 風格;一個強調色。
測試
無頭 Chrome 的 --screenshot 無法滾動。請改為透過 DevTools 協議驅動真實的 Chrome:
chrome --headless=new --remote-debugging-port=9333 --user-data-dir=/tmp/prof about:blank &- 從 Node(v22+ 有全域
WebSocket):取得http://localhost:9333/json,開啟頁面的webSocketDebuggerUrl,發送Emulation.setDeviceMetricsOverride、Page.navigate,然後對於每個位置發送Runtime.evaluate("scrollTo(0,Y)"),等待約 700 毫秒,再發送Page.captureScreenshot。 - 從區段的
offsetTop和高度選擇 Y(例如每個固定區段的 0%、60%、100%)。 - 在桌面(1440x900)、大螢幕(2000x1300)和手機(390x800)上檢查,並確認
document.documentElement.scrollWidth === innerWidth(無水平溢出)。
交付清單
- 每個效果都由滾動進度驅動,可反向播放
- 減少動畫的備用方案
- 已檢查手機佈局,無水平滾動
- 固定區塊的滾動長度適中;高螢幕上無空白區域
- 在每個固定區塊的開始/中間/結束處拍攝螢幕截圖