Build-Animation für Agenten-Schwarm
Ein festes Canvas-Overlay zeichnet ein kleines Team von Cursorn, das jeden Abschnitt einer Seite "baut", wenn er in den Viewport scrollt. Der Inhalt ist durchgehend echtes, serverseitig gerendertes HTML. Die Engine versteckt Elemente nur für wenige Sekunden und spielt für jedes eine kurze Choreographie ab.
Live-Beispiel: https://www.lamanhtruong.com. Der Quellcode ist unter
portfolio-anh@a5d1651 festgeschrieben.
assets/swarm/ enthält dieselbe Engine, bereinigt für die Wiederverwendung.
Wann verwenden – und wann nicht
Verwende es, wenn die Seite selbst die Botschaft ist: ein Portfolio, eine Launch- oder Produktstory-Seite, eine Fallstudie, eine "Über unsere KI"-Seite. Leute besuchen sie einmal, scrollen von oben nach unten, und der Build erzählt ihnen etwas ("Diese Person liefert mit Agenten aus").
Überspringe es für alles, was Leute täglich nutzen: Dashboards, Dokumentation, Einstellungen, Checkout, langes Lesen. Der Build kostet ein paar Sekunden pro Abschnitt, und ein wiederkehrender Benutzer hat keinen Grund, ihn zweimal anzusehen. Deshalb merkt sich die Engine einen abgeschlossenen Build pro Tab (sessionStorage) und spielt ihn beim Neuladen nie wieder ab.
Was es kostet:
- Etwa 5.000 Zeilen TypeScript. Die Build-Ebene wird mit
next/dynamicnach der Hydration geladen, sodass der First Paint nie darauf wartet; die Smooth-Scroll- und Parallax-Teile (Lenis, GSAP, ScrollTrigger) werden mit der Seite geladen. Eine frische Next.js 15 App mit allen vier Living-UI-Skills kam auf etwa 67 kB Route-JS (170 kB Erstladevorgang). - Ein kleiner Layout-Shift-Score, während das Live-Log gefüllt wird (etwa 0,03 in einer Testseite, deutlich unter der "guten" 0,1-Linie): neue Logzeilen bewegen sich in ihrer Box mit fester Höhe. Der Rest der Seite verschiebt sich nie.
- Eine zusätzliche feste Canvas und eine
gsap.ticker-Schleife. Die Schleife stoppt, wenn sich nichts bewegt. - GSAP 3.13+ (kostenlos, inklusive ScrollTrigger),
@gsap/react, Lenis.
Wie es in einem Absatz funktioniert
Ein winziges Inline-Script im <head> setzt html[data-build="on"] vor dem ersten Paint, und CSS
versteckt dann jedes ungebaute [data-build-item] mit opacity: 0. Opazität anstelle von Display
oder Sichtbarkeit bedeutet keinen Layout-Shift und nichts wird aus dem Accessibility-Tree entfernt.
Nach der Hydratation durchsucht die Engine die Seite nach einem Task-Graphen pro
[data-build-section], bewaffnet einen ScrollTrigger für jeden Abschnitt, und wenn einer in den
Viewport eintritt, läuft Plan → Bau → Review:
- Plan: Ein Planer zeichnet Guides und verteilt Aufgaben.
- Bau: Arbeiter beanspruchen Aufgaben nach Entfernung, Rolle und kritischem Pfad, und jede Aufgabe spielt eine GSAP-Timeline ab.
- Review: Ein Reviewer umrandet den Abschnitt, hakt eine Checkliste ab und stempelt ihn ab.
Fertige Items und Abschnitte erhalten [data-built]. Falls die Engine nie startet, entfernt das
Head-Script das Flag nach 3 s, sodass nichts versteckt bleibt.
Architektur, Datenfluss und wie man sie erweitert: references/architecture.md.
Installation (Next.js App Router)
-
Abhängigkeiten. Getestet mit Next.js 15.1 + React 18.3 (das ausgearbeitete Beispiel) und Next.js 15.5 + React 19.1 + Tailwind 4 (ein frisches
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 -
Engine kopieren. Kopiere
assets/swarm/nachsrc/components/swarm/. Die Imports darin sind relativ, also funktioniert jeder Ordner. -
Flag-Script hinzufügen in den
<head>des Root-Layouts. Es setzt ein Attribut auf<html>, bevor React hydriert, also fügesuppressHydrationWarningzu<html>hinzu:// 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> ); } -
Engine einmal mounten, auf den Seiten, die sie verwenden, nach dem Inhalt:
// app/page.tsx (eine Server-Komponente; bleibt statisch) import "@/components/swarm/swarm.css"; import { SwarmRoot } from "@/components/swarm/swarm-root"; export default function Page() { return ( <> <main>{/* Abschnitte mit data-build-* Attributen */}</main> <SwarmRoot /> </> ); }SwarmRootmountet auch Lenis Smooth Scrolling, gesteuert vongsap.ticker. Falls die App bereits Lenis verwendet, lösche<SmoothScroll />ausswarm-root.tsx.build-layer.tsxliest die Instanz durchuseLenis(), was einerootReactLenis-Instanz irgendwo auf der Seite benötigt (<ReactLenis root />, wie vonsmooth-scroll.tsxgemountet). Sie muss kein Eltern-Element sein. -
Route statisch halten. Die Engine dekoriert nur HTML, das bereits existiert. Rufe
cookies(),headers()oderauth()im Layout-Tree der Seite nicht auf, nur um eine Session zu lesen. Hole das stattdessen auf dem Client, und die Seite kann vorgerendert und vom CDN ausgeliefert werden. Auf Seiten, dieSwarmRootnicht mounten, ist das Flag-Script harmlos: kein Element trägtdata-build-item, und die Sicherheitsfunktion löscht das Flag. -
Thema festlegen. Setze CSS-Variablen (
--swarm-font-mono,--swarm-accent,--swarm-text,--swarm-muted,--swarm-dim,--swarm-line,--swarm-neutral-text|fill|stroke) und bearbeite dannCOLORSundROLE_COLORinpalette.ts. Die Canvas kann CSS-Variablen nicht günstig pro Frame lesen, daher lebt ihre Palette in TS.
Seite auszeichnen
Alles wird durch Datenattribute auf serverseitig gerendertem Markup gesteuert. Die vollständige
Referenz mit Vorgaben und Telefonverhalten findest du in references/dom-contract.md.
Copy-Paste-Rezepte für Hero, Card-Grid, Liste, Bild oder Video, Metriken-Strip und Finale
findest du in references/recipes.md.
| Attribut | Auf | Bedeutung |
|---|---|---|
data-build-section="name" | Section-Wrapper | Eine Build-Einheit mit eigenem Plan, Team und Review |
data-build-start="load" | Abschnitt | Direkt nach dem Booten bauen statt beim Scrollen, mit dem größeren Hero-Budget |
data-build-item + data-build="frame|type|reveal|image|dup" | Element | Eine Aufgabe dieser Art |
data-build-list | Listen-Container | Sein erstes Item ist die Master-Vorlage (frame); dup-Geschwister werden davon kopiert |
data-build-grid | Container oder der Abschnitt | Ein Layout-Agent zeichnet das Grid und boxt zuerst jede Zelle |
data-build-font / -weight / -scale | type-Items | Mono-Entwurf, dann Schriftartentausch; Gewichtsdurchlauf; klein starten, dann Größe ändern und bewegen |
data-build-color="text|fill|stroke" | beliebiges Item | Farbrad wählt die echte Farbe, nachdem das Item erscheint |
data-build-tune | frame | Öffnet den Radius- und Padding-Inspektor |
data-build-from="left|right|top|bottom" | image | Kante, von der das Bild hereingezogen wird |
data-build-keep / ="mobile" | beliebiges Item | Nie versteckt (nur Telefone mit mobile), trotzdem annotiert |
data-build-label, -order, -role | beliebiges Item | Tag-Text, Reihenfolgehinweis, welche Rolle es übernimmt |
data-swarm-spawn="role" | SVG <g> im Hero | Mesh-Knoten, aus denen das Team beim Booten hervorkommt (≥1024 px) |
data-swarm-log | Container | Serverseitig gerenderte Fallback-Zeilen werden durch ein Live-Log ersetzt |
data-build-finale + data-deploy-target + data-deploy-text | letzter Abschnitt, sein CTA | Deployer "verschifft" die Seite nach dem letzten Review |
?build=off / ?build=force | URL | Für diese Ansicht deaktivieren / erneut abspielen, selbst wenn dieser Tab es bereits gesehen hat |
Die Engine setzt: html[data-build="on"] während der Aktivität, [data-built] auf
fertigen Items und Abschnitten, [data-lit] auf Mesh-Knoten während des Bootens und
[data-deployed] auf dem Finale-Ziel.
Regeln, die dafür sorgen, dass es funktioniert:
- Abschnittsgröße. Etwa ein Viewport voller Items pro Abschnitt. Teile lange Seiten auf: ein Abschnitt pro Job in einer Zeitleiste, einer pro Karte in einem großen Grid, ein separater Abschnitt für einen Abschnitts-Header. Kleine Abschnitte werden innerhalb ihres Budgets fertig und wirken bewusst; riesige fallen auf die Aufholgeschwindigkeit zurück.
- Verschachtelung. Ein Item, das in einem anderen Item verschachtelt ist, wartet auf sein
Elternelement. Markiere die Karte als
frameund ihren Titel, Text und Chips darin. - Type. Verwende
typenur für kurzen Text (unter etwa 120 Zeichen). Längerer Text fällt automatisch aufrevealzurück. Tippen liest Glyphen-Boxen einmal, daher sollten React-verwaltete Texte, die sich nach der Hydratation ändern (Zähler, Aufrufzahlen), einframeund keintypesein. - LCP. Halte die Hauptüberschrift auf Telefonen mit
data-build-keep="mobile"sichtbar. Auf Telefonen ist die Hero-Überschrift der Largest Contentful Paint, und sie zu verstecken kostet Core Web Vitals. Desktop erhält weiterhin den vollen Schriftartentausch-Build. - Live-Log. Der
data-swarm-log-Container muss serverseitig mit echten Fallback-Zeilen und einer festen Höhe (.swarm-log) gerendert werden. Die Engine tauscht sie gegen Live-Zeilen aus und setzt sie bei Überspringen oder Unmount zurück.
Minimales 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 funktioniert auch (es rendert ein inneres <img>, das die Engine liest). */}
<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>
Glätte-Regeln
Diese stammen aus der Beobachtung echter Builds. Jede behob ein sichtbares Problem.
- Niemals zu einer Deadline zwangsbeenden. Eine frühe Version beschleunigte jede laufende
Timeline auf 5× Geschwindigkeit und setzte die nicht beanspruchten Items zur Deadline jedes
Abschnitts ein. Leute sahen das als Ruckler am Ende jedes Abschnitts. Budgets sind jetzt
Planungsziele.
estimate()skaliert Aufgabenlaufzeiten, sodass der Plan passt.- Wenn ein Abschnitt zu spät dran ist, erhöht
pace()einen Aufholfaktor sanft auf maximal 1,7× über 0,7 s. - Jeder Tween bleibt mindestens 120 ms lang, selbst bei vollem Aufholen, und Tippschritte bleiben mindestens 16 ms. Nichts springt ein.
- Wenn du es schneller brauchst, mache die Cursor schneller (
speedinpalette.ts). Schneide keine Animationen ab.
- Nur Opazität, Clip-Path, Filter und Transform animieren. Keine Layout-Schreibvorgänge. Schriftartentausch-Entwürfe sind Klone in einer Out-of-Flow-Schicht, und die Skalierungsaufgabe entfernt am Ende ihre inline-transform.
- Eine Uhr. Die Simulation und die Canvas-Zeichnung laufen in einem einzigen
gsap.ticker-Callback. React-Status ändert sich nur, wenn der Build startet oder stoppt, nie pro Frame. Der Ticker stoppt, wenn kein Agent auf dem Bildschirm ist. - Vorausmessen. Abschnitte bis zu etwa zwei Viewports unter dem Falz werden in
requestIdleCallback-Slots gemessen. Ein startender Abschnitt verursacht nie einen großen Layout-Read-Burst im Scroll-Frame. - Den Leser respektieren.
- Fokussieren auf etwas in einem ungebauten Abschnitt beendet diesen Abschnitt sofort, sodass Tastaturbenutzer nie warten.
- Eine Größenänderung während des Builds beendet die laufenden Abschnitte, weil ihre gemessenen Rechtecke veraltet sind.
- Ein verborgener Tab pausiert alles.
- Das HUD bietet immer Überspringen, Wiederholen und Inspizieren.
- Telefone bekommen weniger. Jedes Profil hat sein eigenes Budget, Team und Detail
(
detectProfile()inpalette.ts).- Telefone: 8 Agenten, etwa 2,2 s pro Abschnitt, ein Schriftartentausch pro Seite, eine Farbauswahl pro Abschnitt, ein Dup-Durchlauf pro Liste, keine Inspektor-Panels, keine Skalierungsaufgabe, kein Deployer.
- Tablets: 11 Agenten.
- Desktop: 15 Agenten, etwa 3,9 s pro Abschnitt, 5,25 s für das Hero.
Regler für "schneller", "ruhiger" und "mehr Agenten": references/tuning.md.
Reduzierte Bewegung
Die Engine spielt für jeden Besucher, auch für diejenigen, deren Betriebssystem reduzierte Bewegung anfordert. Der Seitenbesitzer hat entschieden, dass der Build der Kern der Seite ist, und das HUD bietet immer Überspringen, daher ist dies eine bewusste Entscheidung, keine blind zu kopierende Vorgabe. Um die Einstellung stattdessen zu respektieren, nimm alle drei folgenden Änderungen vor:
// build-flag-script.tsx: innerhalb von CODE, vor 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,
Teste beide Wege: Einige Browser (und einige Automatisierungsumgebungen) melden
reduce, auch wenn niemand es gewählt hat, was einer der Gründe ist, warum die Vorgabe hier
"ein" ist.
Zusammenarbeit mit anderen Effekten
-
Tore mit
[data-built]. Die Engine liest Element-Rechtecke und schaltet Opazität und Clip-Path während des Builds um. Kipp-, Glitch-, Scramble-, Parallax- oder andere Effekte, die ein Item transformieren, werden diese Messungen korrumpieren, wenn sie früh laufen. Starte solche Effekte nur, wenn das Item und sein Abschnitt[data-built]tragen, oder wennhtml[data-build]nicht"on"ist. Ein fertiger Helfer: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"); } -
Videos. Ein
image-Item, das ein<video poster>umschließt, wird mit seinem Poster-Frame als Vorschaubild hereingezogen. Gib dem Wrapper eindata-build-labelwieclip.mp4, da es kein<img>gibt, aus dem ein Dateiname gelesen werden kann. -
Dekoratives Parallax ist überall in Ordnung, wo es nur Dekoration bewegt. Setze es niemals auf etwas, das die Engine misst.
Mach es zu deinem Eigenen
Die Engine hier ist eine originelle Implementierung, und der gleiche Standard gilt für alles, was du daraus baust. Wenn eine Website dich inspiriert hat, schreibe auf, was diese Website wiedererkennbar macht, und halte dich von genau diesen Dingen fern. Ausleihen ist nicht Kopieren: Behalten...
[... abgeschnitten für Übersetzung]
