代理人群體建構動畫
一個固定的 Canvas 圖層會繪製出一個小型游標團隊,當頁面的每個區塊滾動進入視野時,這些游標會「建構」該區塊。內容是真實的、伺服器端渲染的 HTML。引擎僅將項目隱藏幾秒鐘,並為每個項目播放一段簡短的編排動畫。
線上範例:https://www.lamanhtruong.com。其原始碼位於 portfolio-anh@a5d1651。
assets/swarm/ 包含相同的引擎,已清理乾淨以便重複使用。
何時使用,何時不使用
在頁面本身作為宣傳重點時使用:作品集、發布或產品故事頁面、案例研究、「關於我們的 AI」頁面。人們會訪問一次,從上到下滾動,而建構過程告訴他們一些事情(例如「這個人使用代理群體來發布產品」)。
跳過它來用於人們每天使用的東西:儀表板、文件、設定、結帳、長篇閱讀。建構過程每個區塊需要幾秒鐘,回訪用戶沒有理由觀看兩次。這就是為什麼引擎會記住每個分頁已完成建構(透過 sessionStorage)並且在重新載入時絕不重播。
它帶來的成本:
- 大約 5,000 行 TypeScript。建構層使用
next/dynamic在 hydration 之後載入,因此首次繪製從不等待它;平滑滾動和視差組件(Lenis、GSAP、ScrollTrigger)會與頁面一起載入。一個全新的 Next.js 15 應用程式,包含所有四個 living-ui 技能,其路由 JS 約為 67 kB(首次載入 170 kB)。 - 在即時日誌填入時,會產生一個小的佈局偏移分數(在測試頁面中約為 0.03,遠低於 0.1 的「良好」門檻):新的日誌行在其固定高度的框內移動。頁面的其餘部分從不偏移。
- 一個額外的固定 Canvas 和一個
gsap.ticker迴圈。當沒有東西在移動時,迴圈會停止。 - GSAP 3.13+(免費,包含 ScrollTrigger)、
@gsap/react、Lenis。
用一段話解釋其運作原理
<head> 中的一個小型內聯腳本在首次繪製之前將 html[data-build="on"] 設定為啟用,然後 CSS 透過 opacity: 0 隱藏所有未建構的 [data-build-item]。使用 opacity 而非 display 或 visibility 意味著沒有佈局偏移,也不會從無障礙樹中移除任何元素。Hydration 之後,引擎掃描頁面,為每個 [data-build-section] 建立一個任務圖,為每個區塊設定一個 ScrollTrigger,當某個區塊進入視口時,依次執行規劃 → 建構 → 審閱:
- 規劃: 規劃者繪製引導線並分派任務。
- 建構: 工作者根據距離、角色和關鍵路徑認領任務,每個任務播放一個 GSAP 時間軸。
- 審閱: 審閱者勾勒區塊輪廓,勾選清單並蓋上印章。
完成的項目和區塊會獲得 [data-built] 屬性。如果引擎從未啟動,標頭腳本會在 3 秒後移除該標誌,以免任何內容保持隱藏。
架構、資料流程以及如何擴展:references/architecture.md。
安裝 (Next.js App Router)
-
依賴項。 已在 Next.js 15.1 + React 18.3(實際範例)和 Next.js 15.5 + React 19.1 + Tailwind 4(全新的
create-next-app)、TypeScript 5.8–5.9、[email protected]–3.15、@gsap/[email protected]、[email protected]上測試通過:npm i gsap@^3.13 @gsap/react@^2.1 lenis@^1.3 -
複製引擎。 將
assets/swarm/複製到src/components/swarm/。內部的導入是相對路徑,因此任何資料夾都可以使用。 -
將標誌腳本 添加到根佈局的
<head>。它在 React hydrating 之前為<html>設定一個屬性,因此請為<html>添加suppressHydrationWarning:// app/layout.tsx import { BuildFlagScript } from "@/components/swarm/build-flag-script"; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en" suppressHydrationWarning> <head> <BuildFlagScript /> </head> <body>{children}</body> </html> ); } -
掛載引擎一次,在使用它的頁面上,內容之後:
// app/page.tsx (一個伺服器組件;保持靜態) import "@/components/swarm/swarm.css"; import { SwarmRoot } from "@/components/swarm/swarm-root"; export default function Page() { return ( <> <main>{/* 使用 data-build-* 標記的區塊 */}</main> <SwarmRoot /> </> ); }SwarmRoot也會掛載由gsap.ticker驅動的 Lenis 平滑滾動。如果應用程式已經運行了 Lenis,請從swarm-root.tsx中刪除<SmoothScroll />。build-layer.tsx透過useLenis()讀取實例,這需要頁面上某處有一個rootReactLenis 實例(<ReactLenis root />,如同smooth-scroll.tsx所掛載的)。它不一定是父組件。 -
保持路由靜態。 引擎僅裝飾已存在的 HTML。不要在頁面的佈局樹中調用
cookies()、headers()或auth()來讀取 session。而是在客戶端獲取這些資料,這樣頁面可以預渲染並從 CDN 提供服務。在沒有掛載SwarmRoot的頁面上,標誌腳本是無害的:沒有元素帶有data-build-item,並且故障安全機制會清除該標誌。 -
設定主題。 設定 CSS 變數(
--swarm-font-mono、--swarm-accent、--swarm-text、--swarm-muted、--swarm-dim、--swarm-line、--swarm-neutral-text|fill|stroke),然後編輯palette.ts中的COLORS和ROLE_COLOR。Canvas 無法在每一幀上廉價地讀取 CSS 變數,因此其調色板存在於 TS 中。
標記頁面
一切均由伺服器渲染標記上的 data 屬性驅動。完整的參考資料,包含預設值和手機行為,位於 references/dom-contract.md。用於 hero、卡片網格、列表、圖片或影片、指標條和結局的可複製範本,位於 references/recipes.md。
| 屬性 | 套用在 | 意義 |
|---|---|---|
data-build-section="name" | 區塊包裝器 | 一個建構單元,有自己的規劃、團隊和審閱 |
data-build-start="load" | 區塊 | 在啟動後立即建構,而非等待滾動,並享有更大的 hero 預算 |
data-build-item + `data-build="frame | type | reveal |
data-build-list | 列表容器 | 其第一個項目是主體 (frame);dup 兄弟元素從它複製 |
data-build-grid | 容器或區塊本身 | 佈局代理會先繪製網格並框出每個單元格 |
data-build-font / -weight / -scale | type 項目 | 先使用等寬草稿字型,然後進行字體切換;權重掃描;從小型開始,然後調整大小和移動 |
| `data-build-color="text | fill | stroke"` |
data-build-tune | frame | 開啟圓角和內距檢查器 |
| `data-build-from="left | right | top |
data-build-keep / ="mobile" | 任何項目 | 永不隱藏(手機版僅用 mobile),但仍會被註解 |
data-build-label、-order、-role | 任何項目 | 標籤文字、排序提示、由哪個角色負責 |
data-swarm-spawn="role" | Hero 中的 SVG <g> | 團隊在啟動時彈出的網格節點(≥1024 px) |
data-swarm-log | 容器 | 伺服器渲染的備用行會被替換為即時日誌 |
data-build-finale + data-deploy-target + data-deploy-text | 最後一個區塊及其 CTA | 部署者在最後一次審閱後「發布」頁面 |
?build=off / ?build=force | URL | 對此視圖停用 / 即使此分頁已觀看過也強制重播 |
引擎會設定:html[data-build="on"] 在活躍時,[data-built] 在完成的項目和區塊上,[data-lit] 在啟動期間的網格節點上,以及 [data-deployed] 在結局目標上。
保持其運作的法則:
- 區塊大小。 每個區塊約一個視口的項目。分割長頁面:時間軸中的每個工作一個區塊,大型網格中的每個卡片一個區塊,區塊標題一個單獨的區塊。小型區塊在其預算內完成,看起來有目的性;大型區塊會回退到追趕速度。
- 巢狀結構。 巢狀在另一個項目內部的項目會等待其父項目。將卡片標記為
frame,並將其標題、文字和晶片標記在內部。 - 類型。 僅對短文字(少於約 120 個字元)使用
type。較長的文字會自動回退到reveal。打字會讀取字形框一次,因此 hydration 後變化的 React 管理文字(計數器、觀看次數)應該是frame,而不是type。 - LCP。 在手機上使用
data-build-keep="mobile"保持主標題可見。在手機上,hero 標題是 Largest Contentful Paint,隱藏它會損害您的 Core Web Vitals。桌面版仍可獲得完整的字體切換建構。 - 即時日誌。
data-swarm-log容器必須是伺服器渲染的,帶有真實的備用行和固定高度 (.swarm-log)。引擎將它們替換為即時行,並在跳過或卸載時恢復它們。
最小 hero 範例:
<section data-build-section="hero" data-build-start="load">
<p data-build-item data-build="type">your eyebrow line</p>
<h1
data-build-item data-build="type" data-build-font data-build-weight
data-build-keep="mobile" data-build-order="-1" data-build-role="coder"
>
Your Name <span className="serif">(Accent)</span>
</h1>
<div data-build-item data-build="image" data-build-from="right" className="avatar">
<img src="/me.jpg" alt="Portrait" />
</div>
{/* next/image 也可以(它會渲染一個引擎可讀取的內部 <img>)。 */}
<div data-build-list className="buttons">
<a data-build-item data-build="frame" href="#">GitHub</a>
<a data-build-item data-build="dup" href="#">LinkedIn</a>
<a data-build-item data-build="dup" data-build-color="fill" href="#">Resume</a>
</div>
</section>
平滑度法則
這些法則來自觀察真實的建構過程。每一個都修復了一個可見的問題。
- 永遠不要在截止時間強制完成。 早期版本會將所有正在運行的時間軸跳到 5 倍速,並在每個區塊的截止時間彈出未認領的項目。人們在每個區塊結束時會看到一個卡頓。現在預算是規劃目標。
estimate()會縮放任務持續時間,以使計畫符合預算。- 當一個區塊延遲時,
pace()會平穩地增加追趕因子,最大在 0.7 秒內達到 1.7 倍。 - 即使在完全追趕的情況下,每個補間動畫至少保持 120 毫秒長,打字步驟至少保持 16 毫秒。不會有彈出效果。
- 如果您需要更快,請讓游標更快(
palette.ts中的speed)。不要縮短動畫。
- 僅對 opacity、clip-path、filter 和 transform 進行動畫處理。 沒有佈局寫入。字體切換草稿是脫離流程圖層中的克隆,並且縮放任務會在結束時移除其內聯的 transform。
- 一個時鐘。 模擬和 Canvas 繪製在單一的
gsap.ticker回調中運行。React 狀態僅在建構開始或停止時更改,而不是每幀更改。當沒有代理在螢幕上時,ticker 會停止。 - 提前測量。 在折疊線下方約兩個視口範圍內的區塊,會在
requestIdleCallback片段中進行測量。啟動的區塊絕不會在滾動幀上引起大的佈局讀取爆發。 - 尊重閱讀者。
- 聚焦未建構區塊內的任何內容,會立即完成該區塊,這樣鍵盤用戶就不會等待。
- 建構過程中的大小調整事件會完成正在運行的區塊,因為它們測量的矩形已過時。
- 隱藏的分頁會暫停所有內容。
- HUD 始終提供跳過、重播和檢查功能。
- 手機獲得較少內容。 每個設定檔都有自己的預算、團隊和細節(
palette.ts中的detectProfile())。- 手機:8 個代理,每個區塊約 2.2 秒,每頁一個字體切換,每個區塊一個顏色選取,每個列表一個 dup 掃描,無檢查器面板,無縮放任務,無部署器。
- 平板:11 個代理。
- 桌面:15 個代理,每個區塊約 3.9 秒,hero 約 5.25 秒。
用於「更快」、「更平靜」和「更多代理」的旋鈕:references/tuning.md。
減少動畫
引擎會為每位訪客播放動畫,包括那些在作業系統中要求減少動畫的訪客。網站所有者認為建構過程是頁面的重點,並且 HUD 始終提供跳過功能,因此這是一個刻意的選擇,而不是一個可以盲目複製的預設值。若要改為尊重此設定,請進行以下所有三項更改:
// build-flag-script.tsx: 在 CODE 內部,d.setAttribute('data-build','on') 之前
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
// motion.ts
export function offByUrl(): boolean {
return OFF_RE.test(window.location.search) ||
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}
// smooth-scroll.tsx
respectReducedMotion: true,
兩種情況都測試:某些瀏覽器(以及某些自動化設定)即使沒有人選擇,也會報告 reduce,這就是為什麼此處的預設值是「啟用」。
與其他效果協作
-
以
[data-built]為門檻。 引擎在建構時會讀取元素矩形並切換 opacity 和 clip-path。傾斜、故障、亂碼、內容視差或任何其他轉換項目的效果,如果過早運行,都會破壞這些測量。僅當項目及其區塊帶有[data-built]時,或當html[data-build]不是"on"時,才啟動此類效果。一個現成的輔助函式:export function isBuilt(el: Element): boolean { if (document.documentElement.getAttribute("data-build") !== "on") return true; const item = el.closest("[data-build-item]"); if (item && !item.hasAttribute("data-built")) return false; const section = el.closest("[data-build-section]"); return !section || section.hasAttribute("data-built"); } -
影片。 一個包裝了
<video poster>的image項目,會使用其海報幀作為縮圖拖入。為包裝器提供一個data-build-label,例如clip.mp4,因為沒有<img>可以讀取檔案名稱。 -
裝飾性視差 可以在任何僅移動裝飾元素的地方正常使用。切勿將其放在引擎要測量的內容上。
讓它成為你自己的
這裡的引擎是一個原創實作,同樣的標準適用於您從它建構的任何東西。如果某個網站啟發了您,請記錄下什麼使該網站具有辨識度,並遠離這些確切的東西。借鑑
…
[為翻譯而截斷]
