動態效果
四個讓頁面內容顯示後持續保持動感的特效:
- 極光英雄區: 一個 WebGL 的柔和光場,位於英雄區後方,並會隨游標彎曲。
- 全息投影卡片: 卡片會朝游標方向傾斜、微微上提,並產生眩光與虹彩邊緣。
- 網格聚光燈: 頁面背景網格會在游標周圍形成一個柔和的亮圈。
- 故障與解碼文字: 標題在出現時會產生 RGB 分離效果,連結標籤則會在懸停時解碼。
標準是「生動,但絕不礙事」。使用者應感受到頁面對他們的操作有所回應,但任何效果都不能造成卡頓、版面位移、阻擋點擊,或對螢幕閱讀器隱藏文字。
即時範例:https://www.lamanhtruong.com(將滑鼠懸停在專案卡片上、在英雄區移動、懸停導航列)。
assets/fx/ 內容
| 檔案 | 角色 |
|---|---|
ticker.ts | 一個共用的 requestAnimationFrame 迴圈,在無人要求影格時進入休眠 |
built.ts | 就緒閘門(isBuilt, buildActive)、frozen()、種子化 PRNG、指標媒體查詢 |
aurora.ts | WebGL 著色器、其品質縮放器與生命週期(createAurora) |
hero-shader.tsx | <HeroShader />,極光效果的 React 包裝器 |
holo.ts | 卡片傾斜 + 眩光 + 光澤效果、彈簧物理、注入覆蓋層 |
spotlight.ts | 跟隨游標的網格聚光燈(僅 transform) |
glitch.ts, glitch-copies.tsx | 故障排程器、<GlitchCopies /> 與 <GlitchHeading /> |
scramble.ts, scramble-text.tsx | 解碼執行器與 <ScrambleText /> 標記鉤子 |
fx-root.tsx | <FxRoot />:一次掛載全息投影、聚光燈、故障與解碼效果 |
fx.css | 所有樣式,使用 --fx-* 自訂屬性設定主題 |
此程式碼僅需 React,無需動畫函式庫。這些模板在嚴格的 TypeScript 下可通過 React 18 與 19 類型的型別檢查。
安裝
- 將
assets/fx/複製到應用程式中,例如src/components/fx/。請將檔案放在一起,因為它們會透過相對路徑互相引用。 - 掛載它。頁面本身可以保持為靜態伺服器元件;只有
FxRoot和HeroShader是用戶端元件。
import { FxRoot } from "@/components/fx/fx-root";
import { HeroShader } from "@/components/fx/hero-shader";
import { GlitchHeading } from "@/components/fx/glitch-copies";
import { ScrambleText } from "@/components/fx/scramble-text";
export default function Page() {
return (
<div className="fx-grid"> {/* 固定的基礎網格;若已有自己的網格則可跳過 */}
<main className="relative z-[1]"> {/* 內容位於網格與聚光燈之上 */}
<section className="relative min-h-[80vh]"> {/* 英雄區必須是 position: relative */}
<HeroShader /> {/* 第一個子元素,位於英雄區內容後方 */}
<h1>…</h1>
</section>
<GlitchHeading as="h2" className="text-5xl font-semibold">精選作品</GlitchHeading>
<article data-holo className="rounded-2xl border p-6">
<a href="/work/x" className="inline-flex items-center gap-2">
<ScrambleText>閱讀案例研究</ScrambleText> <ArrowIcon />
</a>
</article>
</main>
<FxRoot /> {/* 每頁一次,位於網格包裝器內部 */}
</div>
);
}
- 設定主題:編輯
fx.css頂部的--fx-*變數,並將palette/intensity傳遞給<HeroShader />(請參閱下方「設定主題」章節)。 - 在完成前,使用
references/verification.md中的 Playwright 檢查進行驗證。
透過 props 關閉特定效果:<FxRoot holo={false} spotlight={false} />。將傾斜效果指向現有卡片,無需修改其標記:<FxRoot holo={{ selector: ".card", within: "main" }} />。
保持流暢的規則
這些規則區分了「高級感」與「卡頓感」。每一條規則的存在,都是因為違反它曾導致明顯的問題。
-
等待內容準備就緒。 永遠不要在另一個腳本仍在揭示或測量元素時,對其施加傾斜、故障或解碼效果。在元素揭示過程中對其施加 transform 會破壞正在為其製作動畫的腳本的測量結果,導致看起來像卡頓。
built.ts實現了一個約定:當html[data-build="on"]被設定時,一個元素僅在它的[data-build-item]和[data-build-section]帶有[data-built]屬性質時才算就緒。如果頁面上沒有建置引擎,所有元素都會立即被視為已建置。任何入場動畫都可以透過設定這些屬性來選擇加入。詳細資訊請參閱references/gating-and-performance.md。 -
使用單一影格迴圈,並讓它休眠。 每個效果都訂閱
ticker.ts,而不是自行呼叫requestAnimationFrame。當訂閱者回傳false表示已穩定時,且無人需要影格時,迴圈就會停止。這樣一來,閒置頁面就不會消耗任何資源,且效果之間不會爭搶影格。故障效果完全不使用影格:它是由屬性切換的 CSS keyframes。 -
僅對
transform、opacity和clip-path進行動畫。 這些屬性會停留在合成器層。這裡沒有任何效果會改變大小、在流程中的位置或字型度量,因此從根本上保證了零版面位移。解碼效果將真實文字保持在原位(透明),並在覆蓋層中繪製字形,因此即使標籤在變化,其寬度也永遠不會改變。 -
根據輸入條件匹配效果。 傾斜效果和跟隨式聚光燈僅在
(hover: hover) and (pointer: fine)條件下存在。在觸控裝置上,手指下的傾斜效果會被視為錯誤,因此觸控裝置會在觸碰點獲得一個短暫的淡入效果,以及一個自漂移的著色器鏡頭。 -
在靜止時不要碰觸 React 的 DOM。 全息投影覆蓋層
<span>和行內transform是在卡片啟動時添加,並在卡片恢復靜止時移除。在靜止時,DOM 完全符合 React 的渲染結果,因此重新渲染、hydration 以及卡片自身的:hover樣式都能正常運作。故障效果層與解碼效果的<span>是直接在標記中撰寫(<GlitchCopies>、<ScrambleText>),而不是在執行時從外部插入到 React 擁有的節點周圍;從外部重組 React 的樹結構會破壞協調機制。 -
保持文字的真實性。 故障效果複製層僅限於用戶端且帶有
aria-hidden屬性,因此靜態 HTML、頁面內查找功能以及螢幕閱讀器只會看到每個標題一次。解碼效果覆蓋層帶有aria-hidden屬性,且真實標籤在整個過程中都是可存取名稱。 -
進行清理,並在 StrictMode 下正常運作。 每個
setup*都會回傳一個 dispose 函式,用於移除監聽器、觀察器、覆蓋層和行內樣式。著色器會釋放其緩衝區和程式,但刻意不呼叫WEBGL_lose_context,因為 React StrictMode 會在同一個 canvas 上進行掛載、卸載和重新掛載,而遺失的 context 將永遠保持遺失狀態。 -
為 GPU 做預算規劃。 著色器在像素預算下以降低的解析度渲染,僅在可見時繪製,在高更新率螢幕上將繪製次數限制在每秒約 80 次,並在頁面執行速度低於約 38 fps 時自動降低一檔品質。它在使用軟體備援方案時(
failIfMajorPerformanceCaveat)拒絕啟動,寧可什麼都不渲染,也不渲染緩慢的內容。
效果
極光英雄區著色器(aurora.ts、hero-shader.tsx)
著色器是經域扭曲的數值雜訊(由 fbm 扭曲的 fbm),繪製成寬廣的輝光加上沿著扭曲輪廓的細亮條紋。指標充當透鏡:它會放大並旋轉附近的流體。進入英雄區、點擊或快速移動時會產生一個柔和漣漪環。輸出為預乘 alpha,並在每個邊緣淡出,以便頁面背景和任何固定網格能夠透出,且英雄區能與下一個區塊融合,而不是以一個盒子形狀結束。
生命週期:在其區塊建置完成後,canvas 會在 1.8 秒內淡入。它僅在區塊位於螢幕上(使用 IntersectionObserver,邊距 80 px)且分頁可見時才進行渲染。如果缺少 WebGL,則不渲染任何內容。
調節參數:
palette:{ veil, primary, secondary, accent }十六進位色碼。調色板會被編譯到著色器中,因此變更需要重新掛載。intensity:乘以光線強度。0.6 為平靜,1.4 為強烈。- 文字可讀性:著色器會將光線偏向遠離左下角的位置,英雄區文字通常位於此處。如果您的文字在其他位置,請更改
bias行(請參閱參考文件)。
在更改著色器數學運算、速度或品質縮放器之前,請閱讀 references/aurora-shader.md。
全息投影卡片(holo.ts)
委派的 pointerover 事件會找到精細指標下的卡片。啟動時,它會測量卡片未經轉換的邊界框,注入一個帶有 aria-hidden 屬性的覆蓋層 <span>,並執行一個臨界阻尼彈簧(以 120 Hz 進行子步進,使其在 60 或 144 fps 下表現一致),朝向指向指標的傾斜角度。游標下方的邊緣會朝向觀看者靠近。離開時,它會以輕微的 overshoot 彈回,並在穩定後移除所有內容。
- 傾斜角度會根據卡片大小縮放,範圍為 2.5° 至 7.5°:大卡片傾斜角度較小,以免其遠端擺動。透視距離為較長邊的 1.6 倍。上提量為 3 px。
- 覆蓋層在指標位置具有徑向眩光、微弱的對角線漸層色調,以及一個遮罩到邊框的圓錐漸層環(周圍暗色,指標附近明亮)。
- 在
overflow: hidden的卡片上,1 px 的邊框盒會被裁切,因此環會被繪製在內部(.fx-holo--in,自動偵測)。 - 覆蓋層位於
z-index: 3,並設定pointer-events: none,因此它會在卡片內部的控制項(例如影片切換開關、連結)上方繪製,而不會阻擋點擊。 - 一個
position: static的卡片僅在啟動時才會獲得position: relative屬性。 - 選擇退出:在卡片或其任何祖先元素上設定
data-holo="off"。在html[data-fx-freeze]存在時暫停所有傾斜效果,以便讀取真實的矩形資訊(例如檢查器、螢幕截圖階段)。living-ui-swarm 檢查切換功能會自動偵測。
網格聚光燈(spotlight.ts、div.fx-spot)
一個 560 px 的固定視窗,帶有徑向遮罩,包含一個較亮的基礎網格副本。每個影格僅移動兩個 transform:視窗跟隨游標,並且內部的網格會向後移動 floor(x / cell) * cell - x,使其線條精確對齊基礎網格的格點。兩個網格都必須錨定在視口原點,而 .fx-grid::before 正是如此。當游標停止時,追蹤 lerp 會穩定下來,迴圈進入休眠。當視窗失去焦點或指標離開視窗時,它會隱藏。觸控裝置會在觸碰點獲得一個 360 ms 的淡入效果。
網格單元格、細線間距和視窗大小來自 --fx-grid-cell、--fx-grid-fine 和 --fx-spot-size(px 值),因此 CSS 保持為唯一的真實來源。
故障標題(glitch.ts、<GlitchHeading> / <GlitchCopies>)
標記:一個包含真實標題(.fx-glitch__base)和兩個僅限用戶端副本的 .fx-glitch[data-fx-glitch] 包裝器。GlitchHeading 會撰寫所有這些內容;當標題已存在且帶有其他屬性時,請直接使用 GlitchCopies。
- 入場效果: 當一個已建置的標題至少有 50% 進入視野時,持續 560 ms。相同的標題在 2.5 秒內不會重複播放。
- 微型故障: 每 8-14 秒對一個隨機的可見標題觸發一次,持續 260 ms。
- 隨機性是種子化的,因此每次訪問和在測試中的節奏都是相同的。
- 一次只有一個標題會產生故障效果;入場效果會彼此排隊。
- 透過由
[data-glitch]切換的純 CSS keyframes 實現:使用clip-path: inset()切片、使用translate3d偏移,並使用text-shadow分割顏色。 --fx-gin會根據每次執行的情況縮放偏移量。
解碼文字(scramble.ts、<ScrambleText>)
僅包圍標籤,而非圖示:<a><Icon/> <ScrambleText>{label}</ScrambleText></a>。
- 觸發條件: 對包含
a、button或[data-fx-scr-host]的元素進行懸停(非觸控)或鍵盤:focus-visible。 - 持續時間: 360-520 ms,取決於文字長度。
- 解析過程: 字元從左到右解析,帶有些微抖動。未解析的尾部以約 18 Hz 而非每個影格重新繪製,使其看起來像解碼過程,而非雜訊。
- 字形池: 區分大小寫,且
@ . / : -等標點符號保持不變,因此電子郵件和路徑在解碼過程中仍可閱讀。 - 清理: 覆蓋層始終會被清空,包括為背景分頁(此處 rAF 會停止)提供的計時器備援方案。
對純行內連結使用 inline(保留底線)。對填滿的按鈕使用 quiet,同時在按鈕上設定 --fx-scr-tail: currentColor。
設定主題
| 變數 | 使用者 | 預設值 |
|---|---|---|
--fx-accent | 故障效果層 A、解碼字形、聚光燈輝光、全息投影陰影 | #5eead4 |
--fx-hot | 故障效果層 B | #f472b6 |
--fx-sheen-1..5 | 全息投影環,依序圍繞圓圈 | 青綠色、藍色、紫色、粉紅色、琥珀色 |
--fx-grid-line, --fx-grid-fine-line | 基礎網格線 | 低透明度石板色 |
--fx-grid-cell, --fx-grid-fine | 網格間距(px) | 96px, 24px |
--fx-spot-size | 聚光燈視窗(px) | 560px |
--fx-mono | 解碼字形字型 | 系統等寬字型 |
--fx-scr-tail | 每個元素的字形顏色覆蓋 | 未設定 |
顏色透過 color-mix(in srgb, …) 混合,所有現代瀏覽器均支援。著色器使用自己的調色板:<HeroShader palette={{ accent: "#22d3ee", secondary: "#f0abfc" }} intensity={0.8} />。
從與頁面強調色相同的色系中選擇著色器顏色。著色器是一種氛圍,而非第二個品牌顏色;如果初次訪問的使用者在看到標題之前就注意到著色器,那就表示它太過強烈了。
圖層
| 圖層 | z-index | 備註 |
|---|---|---|
基礎網格(.fx-grid::before,固定) | 0 | 錨定在視口原點 |
聚光燈(div.fx-spot,固定) | 0 | 在樹狀順序中位於網格之後,因此它繪製在網格之上 |
內容(main,footer) | 1 | 必須使用 position: relative; z-index: 1 定位 |
英雄區著色器(canvas.fx-shader) | auto | 在英雄區內部絕對定位且為第一個子元素,以便後續內容繪製在其之上 |
| 全息投影覆蓋層 | 3(在其卡片內部) | pointer-events: none |
| 固定頁首、任何建置動畫覆蓋層 canvas | 高於內容 | 不受影響 |
如果聚光燈顯示在您的文字之上,表示內容包裝器缺少 position: relative; z-index: 1。
動畫偏好設定
在 lamanhtruong.com 上,站長選擇為所有訪客(包括設定 prefers-reduced-motion 的訪客)播放所有效果,因為在其作品集網站上,動畫本身就是重點。這是一個合法的產品決策,但請有意識地做出這個決定。若要尊重該設定:
<FxRoot motion="reduced" />會關閉所有assets/fx/的效果。這是最簡單的選擇。- 或者,使用
mediaprop:<FxRoot media={{ prefersReducedMotion: "reduce" }} />僅在prefers-reduced-motion: reduce時關閉效果,而"no-preference"則會關閉所有效果。 - 對於受控的體驗,請使用
motion="controlled"並自行管理html[data-fx-paused]屬性。
請勿在 prefers-reduced-motion: reduce 時強制開啟效果,除非您已確認使用者是為了體驗效果而特意造訪。
驗證
在 references/verification.md 中為每個效果提供了 Playwright 檢查。在完成實施之前執行它們,以避免將來出現回歸問題。
配備觸控板的 MacBook 可能會同時觸發懸停和觸控設備路徑,這是混淆的常見來源。references/input-ambiguity.md 解釋了邊緣情況以及 FxRoot 如何處理它們。
參考文件
references/ 中的文件擴展了此處的摘要:
| 文件 | 涵蓋內容 |
|---|---|
gating-and-performance.md | 建置閘門的工作原理、為何 built.ts 檢查 data-build、著色器品質縮放器決策樹 |
aurora-shader.md | 著色器數學原理(雜訊、域扭曲、合成)以及調整速度或筆觸寬度的起點 |
input-ambiguity.md | 在混合輸入(觸控板 + 滑鼠 + 觸控)下,設備類型的 holo.ts 和 spotlight.ts 的模糊性;代理程式應遵循的決策樹 |
verification.md | Playwright 測試:每個效果的通過條件、元素狀態、數值容差、可視性檢查 |
閱讀它們以了解邊界情況下的行爲;在更改效果邏輯之前請先閱讀它們。
與代理程式的協定
- 除非要求使用
spotlight.{{direction}}的變體,否則絕不更改assets/fx/中的檔案。這些效果是經過硬體調整的,引入邊界情況會削弱它們。 - 當要求「極光」、「液體」(或其他動態背景)、「3D/傾斜/全息投影卡片」、「跟隨游標/聚光燈」、「故障/解碼文字」或「懸停效果」時,無論使用者如何描述,都使用此技能。當使用者要求「活潑」、「高級感」、「驚豔」或「哇」的時候,也使用它。使用者也許不了解這些術語;請將其翻譯到這個技能中。
- 第一步始終是將
assets/fx/複製到適當的位置(src/components/fx/或類似位置)。在整合之前,檢查 Next.js 配置以確保 canvas 不會被伺服器端渲染。 - 完成複製後,提供一個簡短的整合計劃,然後編寫程式碼。始終使用
<FxRoot />、<HeroShader />、<GlitchHeading />和<ScrambleText />的方式,與安裝步驟中的範例相同。 - 如果頁面缺少網格,請說明如何添加網格,或建議
className="fx-grid"包裝器。 - 在主題化顏色時,從一個突出的強調色(通常來自按鈕、連結或徽章)開始,並使用工具將其轉換為
--fx-accent。提取一個次要顏色(顏色較深、飽和度較低的版本)作為--fx-hot。如果品牌顏色是中性色,請選擇一種鮮豔的青色/紫色/粉色/琥珀色組合來使用。 - 當使用
color-mix()時,如果背景顏色與預期不同,解釋如何進行調整。 - 在標記
data-holo卡片和設定它們的 CSS 時,提供簡短的程式碼範例。 - 如果該頁面有或將要有
next/font,請確認<FxRoot />位於字型載入器內部,或者在初始渲染後重新掛載整個內容,以便字型度量在傾斜效果測量之前是最終的。 - 在
verification.md上執行 Playwright 檢查,並在整合完成時預覽頁面。