Live-Effekte
Vier Effekte, die eine Seite in Bewegung halten, sobald der Inhalt auf dem Bildschirm ist:
- Aurora-Hero: Ein WebGL-Feld aus weichem Licht hinter dem Hero, das sich um den Mauszeiger biegt.
- Hologramm-Karten: Eine Karte neigt sich zum Cursor hin, hebt sich an und zeigt einen Glanz sowie einen schillernden Rand.
- Raster-Spotlight: Das Hintergrundraster der Seite leuchtet in einem weichen Kreis um den Cursor auf.
- Glitch- und Scramble-Text: Überschriften werden beim Eintreffen per RGB-Split verzerrt, und Link-Labels entschlüsseln sich beim Darüberfahren.
Der Maßstab ist „lebendig, niemals im Weg“. Die Nutzer sollen spüren, dass die Seite auf sie reagiert, aber nichts darf ruckeln, das Layout verschieben, Klicks stehlen oder Text vor einem Screen-Reader verstecken.
Live-Beispiel: https://www.lamanhtruong.com (über die Projektkarten fahren, über den Hero bewegen, über die Navigation fahren).
Was sich in assets/fx/ befindet
| Datei | Rolle |
|---|---|
ticker.ts | Eine gemeinsame requestAnimationFrame-Schleife, die schläft, wenn niemand Frames anfordert |
built.ts | Bereitschaftstor (isBuilt, buildActive), frozen(), deterministischer PRNG, Pointer-Media-Query |
aurora.ts | Der WebGL-Shader, sein Qualitätsskalierer und Lebenszyklus (createAurora) |
hero-shader.tsx | <HeroShader />, der React-Wrapper für die Aurora |
holo.ts | Neigung + Glanz + Schimmer auf Karten, Federphysik, eingefügte Überlagerung |
spotlight.ts | Raster-Spotlight, das dem Cursor folgt (nur transform) |
glitch.ts, glitch-copies.tsx | Glitch-Planer, <GlitchCopies /> und <GlitchHeading /> |
scramble.ts, scramble-text.tsx | Scramble-Ausführung und der <ScrambleText />-Markup-Hook |
fx-root.tsx | <FxRoot />: bindet Holo, Spotlight, Glitch und Scramble einmal ein |
fx.css | Alle Stile, thematisiert mit --fx-*-Custom-Properties |
Der Code benötigt nur React; keine Animationsbibliothek. Die Vorlagen sind typsicher unter striktem TypeScript gegen die React 18 und 19 Typen.
Installation
- Kopiere
assets/fx/in die App, z. B.src/components/fx/. Behalte die Dateien zusammen, da sie sich gegenseitig über relative Pfade importieren. - Binde es ein. Die Seite selbst kann eine statische Serverkomponente bleiben; nur
FxRootundHeroShadersind Client-Komponenten.
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"> {/* festes Basisraster; überspringe, wenn du dein eigenes hast */}
<main className="relative z-[1]"> {/* Inhalt liegt über Raster + Spotlight */}
<section className="relative min-h-[80vh]"> {/* Hero muss position: relative sein */}
<HeroShader /> {/* erstes Kind, hinter dem Hero-Inhalt */}
<h1>…</h1>
</section>
<GlitchHeading as="h2" className="text-5xl font-semibold">Ausgewählte Arbeiten</GlitchHeading>
<article data-holo className="rounded-2xl border p-6">
<a href="/work/x" className="inline-flex items-center gap-2">
<ScrambleText>Zur Fallstudie</ScrambleText> <ArrowIcon />
</a>
</article>
</main>
<FxRoot /> {/* einmal pro Seite, innerhalb des Raster-Wrappers */}
</div>
);
}
- Gestalte es: Bearbeite die
--fx-*-Variablen am Anfang vonfx.cssund übergibpalette/intensityan<HeroShader />(siehe „Theming“ unten). - Überprüfe mit den Playwright-Checks in
references/verification.md, bevor du es als erledigt betrachtest.
Schalte Teile mit Props aus: <FxRoot holo={false} spotlight={false} />. Richte die Neigung auf vorhandene Karten aus, ohne ihr Markup zu berühren: <FxRoot holo={{ selector: ".card", within: "main" }} />.
Regeln, die für eine flüssige Darstellung sorgen
Diese Regeln unterscheiden „Premium“ von „Ruckeln“. Jede existiert, weil ihr Verstoß ein sichtbares Problem verursacht hat.
-
Warte, bis der Inhalt bereit ist. Neige, verzerre oder scrambeln niemals etwas, während ein anderes Skript es noch enthüllt oder vermisst. Eine Transformation an einem Element während der Enthüllung korrumpiert die Messungen dessen, der es animiert, und das Ergebnis sieht aus wie ein Stottern.
built.tsimplementiert einen Vertrag: Solangehtml[data-build="on"]gesetzt ist, zählt ein Element erst, wenn sein[data-build-item]und[data-build-section][data-built]tragen. Ohne Build-Engine auf der Seite gilt alles sofort als erstellt. Jede Intro-Animation kann sich durch Setzen dieser Attribute anmelden. Details:references/gating-and-performance.md. -
Verwende eine Frame-Schleife und lass sie schlafen. Jeder Effekt abonniert
ticker.ts, anstatt selbstrequestAnimationFrameaufzurufen. Ein Abonnent gibtfalsezurück, wenn er sich beruhigt hat, und wenn niemand Frames benötigt, stoppt die Schleife. Eine Leerlaufseite kostet dann nichts, und die Effekte können sich nicht um Frames streiten. Glitch verwendet überhaupt keine Frames: Es sind CSS-Keyframes, die durch ein Attribut umgeschaltet werden. -
Animiere nur
transform,opacityundclip-path. Diese bleiben auf dem Compositor. Nichts hier ändert die Größe, Position im Fluss oder Schriftmetriken, sodass Layout-Shift von Haus aus gleich Null ist. Der Scramble behält den echten Text an Ort und Stelle (transparent) und zeichnet die Glyphen in einer Überlagerung, sodass selbst ein sich änderndes Label niemals die Breite ändert. -
Passe den Effekt an die Eingabe an. Neigung und das Folgespotlight existieren nur auf
(hover: hover) and (pointer: fine). Auf Touch wird das Neigen unter einem Finger als Fehler gelesen, daher erhält Touch eine kurze Einblendung am Berührungspunkt und eine selbsttreibende Shader-Linse stattdessen. -
Lasse Reacts DOM im Ruhezustand in Ruhe. Der Holo-Überlagerungs-
<span>und das Inline-transformwerden hinzugefügt, wenn eine Karte aktiviert wird, und entfernt, wenn sie wieder in den Ruhezustand zurückgekehrt ist. Im Ruhezustand ist das DOM genau das, was React gerendert hat, sodass Wiederholungen, Hydration und die eigenen:hover-Stile der Karte weiterhin funktionieren. Glitch-Ebenen und Scramble-Spannen werden im Markup geschrieben (<GlitchCopies>,<ScrambleText>) und nicht zur Laufzeit um React-eigene Knoten herum eingefügt; die Umstrukturierung von Reacts Baum von außen unterbricht die Rekonsiliation. -
Halte Text real. Glitch-Kopien sind nur Client-seitig und
aria-hidden, sodass das statische HTML, die Suche auf der Seite und Screen-Reader jede Überschrift einmal sehen. Die Scramble-Überlagerung istaria-hiddenund das echte Label bleibt während des gesamten Vorgangs der zugängliche Name. -
Räume auf und überlebe StrictMode. Jedes
setup*gibt eine Dispose-Funktion zurück, die Listener, Observer, Überlagerungen und Inline-Stile entfernt. Der Shader gibt seinen Puffer und sein Programm frei, ruft aber bewusst nichtWEBGL_lose_contextauf, da React StrictMode auf derselben Canvas einhängt, aushängt und wieder einhängt, und ein verlorener Kontext bleibt verloren. -
Budgetiere die GPU. Der Shader rendert mit reduzierter Auflösung unter einem Pixelbudget, zeichnet nur, während er sichtbar ist, begrenzt Zeichnungen auf ~80 pro Sekunde auf hochfrequenten Bildschirmen und senkt die Qualität einmal, wenn die Seite mit unter ~38 fps läuft. Er weigert sich, auf einem Software-Fallback zu starten (
failIfMajorPerformanceCaveat) und rendert nichts, anstatt etwas Langsames zu rendern.
Die Effekte
Aurora-Hero-Shader (aurora.ts, hero-shader.tsx)
Der Shader ist domain-verzerrtes Wertrauschen (fbm warped by fbm), gezeichnet als breites Leuchten plus dünne helle Stränge entlang der verzerrten Konturen. Der Zeiger wirkt wie eine Linse: Er vergrößert und verwirbelt die Strömung in der Nähe. Beim Betreten des Hero, beim Klicken oder bei schnellen Bewegungen fällt ein weicher Wellenring ab. Die Ausgabe ist premultiplied alpha und an jeder Kante ausgeblendet, sodass der Seitenhintergrund und ein eventuelles festes Raster durchscheinen und der Hero in den nächsten Abschnitt übergeht, anstatt in einer Box zu enden.
Lebenszyklus: Die Canvas blende über 1,8 s ein, sobald ihr Abschnitt erstellt ist. Sie rendert nur, während der Abschnitt auf dem Bildschirm ist (IntersectionObserver mit einem 80 px Rand) und der Tab sichtbar ist. Wenn WebGL fehlt, rendert sie nichts.
Knöpfe:
palette:{ veil, primary, secondary, accent }als Hex. Die Palette wird in den Shader kompiliert, daher erfordern Änderungen ein erneutes Mounten.intensity: multipliziert das Licht. 0,6 ist ruhig, 1,4 ist laut.- Textlesbarkeit: Der Shader neigt das Licht weg von der unteren linken Ecke, wo sich normalerweise der Hero-Text befindet. Wenn dein Text woanders liegt, ändere die Zeile
bias(siehe Referenz).
Lies references/aurora-shader.md, bevor du die Shader-Mathematik, die Geschwindigkeit oder den Qualitätsskalierer änderst.
Hologramm-Karten (holo.ts)
Delegiertes pointerover findet die Karte unter einem feinen Zeiger. Beim Aktivieren misst es die unveränderte Box der Karte, injiziert einen aria-hidden-Überlagerungs-<span> und führt eine kritisch gedämpfte Feder (sub-step bei 120 Hz, damit sie sich bei 60 oder 144 fps gleich verhält) in Richtung einer Neigung aus, die auf den Zeiger zielt. Der Rand unter dem Cursor kommt zum Betrachter. Beim Verlassen federt es mit einem weichen Überschwingen zurück und entfernt alles, sobald es sich beruhigt hat.
- Die Neigung skaliert mit der Kartengröße, 2,5° bis 7,5°: Große Karten neigen sich weniger, damit ihre entfernten Kanten nicht schwingen. Perspektive ist das 1,6-fache der größeren Seite. Die Anhebung beträgt 3 px.
- Die Überlagerung hat ein radiales Glanzlicht am Zeiger, eine schwache diagonale Farbwäsche und einen konischen Gradientenring, der auf den Rand maskiert ist (überall gedimmt, in der Nähe des Zeigers hell).
- Bei einer
overflow: hidden-Karte wird die 1 px große Border-Box abgeschnitten, daher wird der Ring knapp innerhalb gezeichnet (.fx-holo--in, automatisch erkannt). - Die Überlagerung sitzt bei
z-index: 3mitpointer-events: none, sodass sie über Steuerelemente in der Karte (einen Video-Umschalter, einen Link) malt, ohne Klicks zu blockieren. - Eine statisch positionierte Karte erhält
position: relativenur, während sie aktiv ist. - Opt-out:
data-holo="off"auf einer Karte oder einem beliebigen Vorfahren. Pausiere alles Neigen mithtml[data-fx-freeze], während jemand echte Rechtecke liest (ein Inspektor, ein Screenshot-Durchlauf). Der living-ui-swarm-Inspect-Umschalter wird automatisch erkannt.
Raster-Spotlight (spotlight.ts, div.fx-spot)
Ein festes 560 px großes Fenster, radial maskiert, enthält eine hellere Kopie des Basisrasters. Jeder Frame bewegt nur zwei Transformationen: Das Fenster folgt dem Cursor, und das Raster im Inneren wird um floor(x / cell) * cell - x zurückverschoben, sodass seine Linien genau auf dem Gitter des Basisrasters landen. Beide Raster müssen am Viewport-Ursprung verankert sein, und .fx-grid::before ist es. Der nachlaufende Lerp beruhigt sich und die Schleife schläft, wenn der Cursor anhält. Es versteckt sich bei Fensterunschärfe oder wenn der Zeiger das Fenster verlässt. Touch erhält eine 360 ms Einblendung am Berührungspunkt.
Rasterzelle, Abstand der feinen Linien und Fenstergröße stammen von --fx-grid-cell, --fx-grid-fine und --fx-spot-size (px-Werte), sodass CSS die einzige Quelle der Wahrheit bleibt.
Glitch-Überschriften (glitch.ts, <GlitchHeading> / <GlitchCopies>)
Markup: Ein .fx-glitch[data-fx-glitch]-Wrapper, der die echte Überschrift (.fx-glitch__base) und zwei reine Client-Kopien enthält. GlitchHeading schreibt alles; verwende GlitchCopies direkt, wenn die Überschrift bereits existiert und andere Attribute trägt.
- Eingang: 560 ms, wenn eine erstellte Überschrift zu mindestens 50 % in Sicht ist. Dieselbe Überschrift wird innerhalb von 2,5 s nicht wiederholt.
- Mikro-Glitch: 260 ms auf einer zufälligen sichtbaren Überschrift alle 8–14 s.
- Die Zufälligkeit ist gesät, sodass der Rhythmus bei jedem Besuch und in Tests gleich ist.
- Es glitcht immer nur eine Überschrift gleichzeitig; Eingänge reihen sich hintereinander.
- Implementiert als reine CSS-Keyframes, die durch
[data-glitch]umgeschaltet werden: Ausschnitt mitclip-path: inset(), Versatz mittranslate3dund Farbaufteilung mittext-shadow. --fx-giskaliert die Versätze pro Durchlauf.
Scramble-Text (scramble.ts, <ScrambleText>)
Umschließe nur das Label, niemals das Icon: <a><Icon/> <ScrambleText>{label}</ScrambleText></a>.
- Auslöser: Hover (nicht Touch) oder Tastatur-
:focus-visibleauf dem umschließendena,buttonoder[data-fx-scr-host]. - Dauer: 360–520 ms, abhängig von der Länge.
- Auflösung: Zeichen lösen von links nach rechts mit einem kleinen Zittern auf. Der ungelöste Rest wird mit ~18 Hz neu gezeichnet, nicht jeden Frame, sodass es wie Dekodierung und nicht wie Rauschen wirkt.
- Glyphen-Pools: groß-/kleinschreibungsabhängig, und Satzzeichen wie
@ . / : -bleiben erhalten, sodass E-Mails und Pfade während des Scrambles lesbar bleiben. - Bereinigung: Die Überlagerung wird immer geleert, einschließlich eines Timer-Fallbacks für Hintergrund-Tabs, bei denen rAF stoppt.
Verwende inline für einfache Inline-Links (behält die Unterstreichung). Verwende quiet bei ausgefüllten Buttons, zusammen mit --fx-scr-tail: currentColor auf dem Button.
Theming
| Variable | Verwendet von | Standard |
|---|---|---|
--fx-accent | Glitch-Ebene A, Scramble-Glyphen, Spotlight-Leuchten, Holo-Schatten | #5eead4 |
--fx-hot | Glitch-Ebene B | #f472b6 |
--fx-sheen-1..5 | Holo-Ring, in der Reihenfolge um den Kreis | Teal, Blau, Violett, Pink, Bernstein |
--fx-grid-line, --fx-grid-fine-line | Basisraster-Linien | niedriges Alpha-Schiefer |
--fx-grid-cell, --fx-grid-fine | Rasterabstand (px) | 96px, 24px |
--fx-spot-size | Spotlight-Fenster (px) | 560px |
--fx-mono | Scramble-Glyphen-Schriftart | System-Mono |
--fx-scr-tail | Glyphenfarben-Override pro Element | nicht gesetzt |
Farben mischen sich über color-mix(in srgb, …), das in allen aktuellen Browsern unterstützt wird. Der Shader nimmt seine eigene Palette: <HeroShader palette={{ accent: "#22d3ee", secondary: "#f0abfc" }} intensity={0.8} />.
Wähle Shader-Farben aus derselben Familie wie die Seitenakzentfarbe. Der Shader ist eine Stimmung, keine zweite Markenfarbe; wenn ein Erstbesucher den Shader vor der Überschrift bemerkt, ist er zu laut.
Schichtung
| Schicht | z-Index | Hinweise |
|---|---|---|
Basisraster (.fx-grid::before, fixed) | 0 | Am Viewport-Ursprung verankert |
Spotlight (div.fx-spot, fixed) | 0 | Nach dem Raster in der Baumreihenfolge, malt also darüber |
Inhalt (main, footer) | 1 | Muss mit z-index: 1 positioniert sein |
Hero-Shader (canvas.fx-shader) | auto | Absolut innerhalb des Hero, erstes Kind, späterer Inhalt malt darüber |
| Holo-Überlagerung | 3 innerhalb ihrer Karte | pointer-events: none |
| Fixer Header, etwaige Build-Animation-Überlagerungs-Canvas | über dem Inhalt | Nicht betroffen |
Wenn das Spotlight über deinem Text erscheint, fehlt dem Inhalts-Wrapper position: relative; z-index: 1.
Bewegungspräferenzen
Auf lamanhtruong.com hat sich der Eigentümer dafür entschieden, jeden Effekt für alle abzuspielen, einschließlich Besucher mit prefers-reduced-motion, da die Bewegung der Kern des Portfolios ist. Das ist eine legitime Produktentscheidung, aber treffe sie bewusst. Um die Einstellung stattdessen zu respektieren:
<FxRoot motion="reduce">: Überschreibt den CSS-Wert, sodass die Effekte auf allen Geräten deaktiviert werden. Setztmediaauf(prefers-reduced-motion: no-preference).<FxRoot motion="allow">(Standard): Zeigt Effekte unabhängig von der Browsereinstellung an.
Referenzen
| Datei | Enthält |
|---|---|
references/verification.md | Playwright-Checks: Holo-Neigung, Spotlight-Follow, Glitch-Zeitsteuerung, Scramble-Auflösung, Accessibility-Prüfung, GPU-Budget |
references/gating-and-performance.md | Bereitschaftstor-Details: Was macht built.ts, wie verzögern Intro-Animationen die Effekte, wie testet man mit einem Build-Engine-Emulator |
references/aurora-shader.md | Shader-Interna: Die Rauschfunktion, die Linsenverzerrung, der Qualitätsskalierer, wie man das bias ändert, um den Text an anderen Stellen zu lesen, wie man den Shader in einer Nicht-React-Umgebung ausführt |