エージェント群構築アニメーション
固定キャンバスオーバーレイに、小さなカーソル群が描画され、各セクションがスクロールでビューに入ると「構築」します。コンテンツは常に実際のサーバーレンダリングされたHTMLです。エンジンはアイテムを数秒間非表示にし、それぞれに短い振り付けを再生するだけです。
ライブ例: https://www.lamanhtruong.com。そのソースは[portfolio-anh@a5d1651](https://github.com/anhlamtruong/portfolio-anh/tree/a5d165123922ea5c7fd645c4d0701554b1e4df8f)に固定されています。`assets/swarm/`には、再利用のためにクリーンアップされた同じエンジンが含まれています。
使用すべきタイミングと、使用すべきでないタイミング
ページ自体が提案である場合に使用します。ポートフォリオ、ローンチまたはプロダクトストーリーページ、ケーススタディ、「AIについて」のページなど。ユーザーは一度訪れ、上から下までスクロールし、構築プロセスが何かを伝えます(「この人はエージェントと一緒に出荷する」)。
ダッシュボード、ドキュメント、設定、チェックアウト、長文の読み物など、毎日使用するものにはスキップしてください。構築はセクションごとに数秒かかり、再訪問ユーザーがそれを二度見る理由はありません。そのため、エンジンはタブごとに完了した構築を記憶し(sessionStorage)、リロード時に再再生しません。
コスト:
- 約5,000行のTypeScript。構築レイヤーはハイドレーション後に
next/dynamicで読み込まれるため、ファーストペイントはそれを待ちません。スムーススクロールとパララックス部分(Lenis、GSAP、ScrollTrigger)はページと一緒に読み込まれます。4つのliving-uiスキルすべてを備えた新しいNext.js 15アプリでは、ルートJSは約67kB(初回読み込み170kB)でした。 - ライブログが埋まる間の小さなレイアウトシフトスコア(テストページで約0.03、「良好」のライン0.1を大幅に下回る):新しいログ行は固定高さのボックス内で移動します。ページの残りの部分は決して移動しません。
- 1つの追加固定キャンバスと1つの
gsap.tickerループ。何も動いていないときはループは停止します。 - GSAP 3.13+(無料、ScrollTriggerを含む)、
@gsap/react、Lenis。
1段落での仕組み
<head>内の小さなインラインスクリプトが、ファーストペイントの前にhtml[data-build="on"]を設定し、CSSはまだ構築されていないすべての[data-build-item]をopacity: 0で非表示にします。opacity(displayやvisibilityではなく)を使用することで、レイアウトシフトが発生せず、アクセシビリティツリーからも削除されません。ハイドレーション後、エンジンはページを[data-build-section]ごとのタスクグラフにスキャンし、各セクションにScrollTriggerを設定し、ビューポートに入ると plan → build → review を実行します:
- Plan: プランナーがガイドを描き、タスクを割り振ります。
- Build: ワーカーが距離、役割、クリティカルパスに基づいてタスクを主張し、各タスクが1つのGSAPタイムラインを再生します。
- Review: レビュアーがセクションの輪郭を描き、チェックリストにチェックを入れてスタンプを押します。
完了したアイテムとセクションには[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がハイドレートする前に<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> ); } -
エンジンを1回マウントする、使用するページで、コンテンツの後に:
// 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()を呼び出さないでください。代わりにクライアント側でそれをフェッチすれば、ページはプリレンダリングされて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を編集します。キャンバスはCSS変数を毎フレーム安価に読み取れないため、パレットはTypeScriptに存在します。
ページをマークアップする
すべてはサーバーレンダリングされたマークアップ上のデータ属性によって駆動されます。デフォルトと電話動作を含む完全なリファレンスはreferences/dom-contract.mdにあります。ヒーロー、カードグリッド、リスト、画像またはビデオ、メトリクスストリップ、フィナーレのコピーペーストレシピはreferences/recipes.mdにあります。
| 属性 | 対象 | 意味 |
|---|---|---|
data-build-section="name" | セクションラッパー | 独自の計画、クルー、レビューを持つ1つのビルド単位 |
data-build-start="load" | セクション | スクロールではなくブート直後に構築、より大きなヒーローバジェットを使用 |
data-build-item + data-build="frame|type|reveal|image|dup" | 要素 | その種類の1つのタスク |
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|bottom" | image | 画像がドラッグインされるエッジ |
data-build-keep / ="mobile" | 任意のアイテム | 非表示にしない(mobileの場合は電話のみ)、それでも注釈は付けられる |
data-build-label、-order、-role | 任意のアイテム | タグテキスト、順序ヒント、担当する役割 |
data-swarm-spawn="role" | SVG <g> in the hero | ブート時にクルーが飛び出すメッシュノード(≥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]。
動作を維持するルール:
- セクションサイズ。 セクションあたり約1ビューポートのアイテム。長いページを分割:タイムラインのジョブごとに1セクション、大きなグリッドのカードごとに1セクション、セクションヘッダーに別のセクション。小さなセクションはバジェット内で完了し、意図的に見えます。巨大なセクションはキャッチアップ速度にフォールバックします。
- ネスト。 別のアイテム内にネストされたアイテムは、その親を待ちます。カードを
frameとしてマークし、その中にタイトル、テキスト、チップを配置します。 - Type。
typeは短いテキスト(約120文字未満)にのみ使用します。長いテキストは自動的にrevealにフォールバックします。タイピングはグリフボックスを一度読み取るため、ハイドレーション後に変更されるReact管理テキスト(カウンター、ビュー数)はframeにすべきであり、typeではありません。 - LCP。 電話では
data-build-keep="mobile"でメイン見出しを表示したままにします。電話ではヒーロー見出しがLargest Contentful Paintであり、非表示にするとCore Web Vitalsにコストがかかります。デスクトップでは、完全なフォントスワップ構築が行われます。 - ライブログ。
data-swarm-logコンテナは、実際のフォールバック行と固定高さ(.swarm-log)でサーバーレンダリングされなければなりません。エンジンはそれらをライブ行に交換し、Skipまたはアンマウント時に元に戻します。
最小限のヒーロー:
<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()はキャッチアップ係数を滑らかに、最大1.7倍まで0.7秒かけてランプアップします。 - すべてのトゥイーンは、完全なキャッチアップ時でも少なくとも120ミリ秒の長さを保ち、タイピングステップは少なくとも16ミリ秒を保ちます。何もポップしません。
- より速くしたい場合は、カーソルを速くしてください(
palette.tsのspeed)。アニメーションを短く切り詰めないでください。
- opacity、clip-path、filter、transformのみをアニメーション化する。 レイアウト書き込みはありません。フォントスワップドラフトは、フロー外のレイヤー内のクローンであり、スケールタスクは最後にインライントランスフォームを削除します。
- 1つのクロック。 シミュレーションとキャンバス描画は、単一の
gsap.tickerコールバックで実行されます。React状態は、構築の開始または停止時にのみ変更され、フレームごとには変更されません。エージェントが画面上にいない場合、tickerは停止します。 - 事前に測定する。 フォールドの下約2ビューポートまでのセクションは、
requestIdleCallbackスライスで測定されます。セクションの開始がスクロールフレームで大きなレイアウト読み取りバーストを引き起こすことはありません。 - 読者を尊重する。
- 未構築セクション内の何かにフォーカスすると、そのセクションがすぐに完了するため、キーボードユーザーは待つ必要がありません。
- 構築中のリサイズは、実行中のセクションを完了します(測定された矩形が古くなるため)。
- 非表示のタブはすべてを一時停止します。
- HUDは常にSkip、Replay、Inspectを提供します。
- 電話は少なく。 各プロファイルには、独自のバジェット、クルー、詳細があります(
palette.tsのdetectProfile())。- 電話:8エージェント、セクションあたり約2.2秒、ページあたり1つのフォントスワップ、セクションあたり1つのカラーピック、リストあたり1つのdupスイープ、インスペクターパネルなし、スケールタスクなし、デプロイヤーなし。
- タブレット:11エージェント。
- デスクトップ:15エージェント、セクションあたり約3.9秒、ヒーローは5.25秒。
「より速く」、「より落ち着いた」、「より多くのエージェント」のためのノブ:references/tuning.md。
モーション軽減
エンジンは、OSがモーション軽減を要求している人を含む、すべての訪問者に対して再生されます。サイト所有者は、構築プロセスがページのポイントであると判断し、HUDは常にSkipを提供するため、これは盲目的にコピーするデフォルトではなく、意図的な選択です。代わりに設定を尊重するには、これら3つの変更をすべて行います:
// 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を報告します。これが、ここでのデフォルトが「オン」である理由の1つです。
他のエフェクトとの連携
-
[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>がないためです。 -
装飾的なパララックスは、装飾のみを動かす場合、どこでも問題ありません。エンジンが測定するものに決して配置しないでください。
自分だけのものにする
ここのエンジンはオリジナルの実装であり、それから構築するものにも同じ基準が適用されます。サイトに触発された場合は、そのサイトを認識可能にするものを書き留め、それらの正確なものから遠ざけてください。借用する…
[翻訳のために切り詰められました]
