Making-of · Direction 03
Der Maschinenraum.
So entstand diese Seite.
Eine statische One-Page-Site mit einer scrollgetriebenen Three.js-Szene — komplett offline, ohne Build-Step, ohne externe Requests. Konzipiert und gebaut von Fable (Claude) unter Regie von Alexander Schmidt.
1 · Konzept
Drei Markenworte haben jede Entscheidung gesteuert: ruhig, mächtig, im Betrieb. Daraus wurde ein Maschinenraum — aber ein warmer: kein Neon-Rechenzentrum, sondern Espresso-Dunkelheit mit Bernstein-Glut, eher Turbinenhalle bei Nacht als Sci-Fi-Labor.
Das Zentrum ist eine abstrakte Automationsmaschine: drei gekippte Node-Kränze um einen glühenden Kern, Datenpartikel strömen auf Spiralbahnen von außen hinein, Funken laufen von Node zu Node, kleine Teil-Aggregate rotieren auf eigenen Umlaufbahnen. Die Kamera fährt beim Scrollen durch sechs Zustände — jede Inhaltssektion bekommt ihre eigene Einstellung, ihre eigene Ring-Beleuchtung.
Bewusst verboten: Cyan, Blau-Neon, Lila-Gradients — die komplette KI-Klischee-Palette. Ebenso Glassmorphism, Gradient-Text und identische Kartenraster. Die Glut ist Akzent, nicht Tapete (60-30-10-Regel).
2 · Palette (OKLCH)
Alle Farben sind in OKLCH definiert, alle Neutralen warm getönt
(Hue ≈ 55–78), nirgends reines Schwarz oder Weiß. Die Hex-Werte existieren
nur für WebGL — getComputedStyle liefert
oklch()-Notation, die kein Hex-Parser versteht, deshalb sind
sie im Szenen-Code hart kodiert.
- Grund / bg
- oklch(0.17 0.015 60)
#140e09
- Nebel / fog
- oklch(0.14 0.012 55)
#0d0805
- Fläche / surface
- oklch(0.20 0.018 65)
#1c140d
- Linie / hairline
- oklch(0.34 0.025 70)
#40362a
- Text
- oklch(0.92 0.015 78)
#eae4da
- Text gedimmt
- oklch(0.70 0.02 72)
#a69d91
- Bernstein / amber
- oklch(0.75 0.14 70)
#e69c3a
- Bernstein hell
- oklch(0.82 0.13 78)
#f2b95a
- Bronze / deep
- oklch(0.62 0.13 60)
#be7125
- Glut / ember
- oklch(0.68 0.16 45)
#e67339
Verteilung: ~60 % Espresso-Grund, ~30 % getönte Flächen und Hairlines, ~10 % Bernstein — reserviert für Status-Punkt, Nummern, KPI-Werte, den Primär-CTA und die Maschine selbst. Aller DOM-Text erfüllt WCAG AA auf seinem jeweiligen Grund.
3 · Typografie
| Rolle | Schrift | Details |
|---|---|---|
| Display | Unbounded (variabel, 500–700) | Breit, technisch, aber rund genug für Wärme. Headlines, Nummern, KPI-Werte, Mail-Link. |
| Fließtext | Gantari (variabel, 400–700 + Italic 400) | Humanistische Grotesk — nüchtern ohne Kälte. Zeilenhöhe 1.75 statt 1.65: helle Schrift auf dunklem Grund braucht mehr Luft. |
- Selbst gehostet als
woff2(latin-Subset) — Google Fonts liefert bei modernem User-Agent pro Familie eine variable Datei für alle Gewichte; perfvar-Table verifiziert und alsfont-weight: 400 700-Range deklariert. - Typo-Skala mit Ratio ≈ 1.3, Headlines über
clamp(), Fließtext max. ~70 Zeichen pro Zeile.
4 · Die WebGL-Szene
Three.js 0.160 als selbst gehostetes ES-Modul (vendor-three.module.min.js), importiert ohne Bundler:
import * as THREE from './vendor-three.module.min.js';
Bauteile der Maschine
- Drei Node-Kränze (r = 2.4 / 3.5 / 4.6, je 16–28 Nodes) in unterschiedlich gekippten Gruppen, jede mit eigener Drehrichtung und -geschwindigkeit. Nodes sind
THREE.Pointsmit eigenem Shader: weicher Glut-Falloff übergl_PointCoord, per-Node-Flackern über ein Phasen-Attribut. - Kern: zwei gegenläufig rotierende Wireframe-Ikosaeder plus ein Billboard-Plane mit radialem Glow-Shader — der „atmet“ mit ~7 s Periode und zieht alle ~19 s in einer Lastspitze kurz hoch.
- Datenfluss: zehn CatmullRom-Spiralkurven von außen (r ≈ 9–11) in den Kern, vorab in je 160 Samples gebacken. 900 Partikel (mobil 280) wandern per CPU-Interpolation die Kurven entlang — Helligkeit und Farbe (Bronze → Glut) steigen zum Kern hin.
- Funken: Impuls-Systeme mit kurzem Schweif (6 Trail-Punkte) laufen entlang der Ring-Bögen von Node zu Node und über die Speichen in den Kern — unregelmäßig getaktet über zufällige Wartezeiten.
- Teil-Aggregate: sieben kleine Wireframe-Oktaeder und -Würfel auf eigenen geneigten Umlaufbahnen, jedes mit individueller Eigenrotation.
- Atmosphäre: 420 Staubpartikel im Raum, ein fast unsichtbares Bodenraster (
GridHelper, Opacity 0.13) undFogExp2in einem Ton dunkler als der Seitengrund — so versinkt die Maschine nach hinten im Schatten.
Scroll-Kamera
Sechs Keyframes (Hero, Leistungen, Referenzen, Ablauf, Haltung, Kontakt) definieren Kameraposition, Blickziel, Maschinen-Versatz, Ring-Helligkeiten und Kern-Intensität. Der Scroll-Fortschritt wird nie direkt angewendet, sondern pro Frame exponentiell nachgeführt — framerate-unabhängig, ohne Scroll-Jank:
camVal += (zielWert - camVal) * (1 - Math.exp(-dt * 4));
Zwischen den Keyframes wird mit Smoothstep geblendet; die Maus liefert zusätzlich einen leichten, ebenfalls geglätteten Parallax-Versatz. Beim Laden „zündet“ die Maschine: Ringe, Staub, Fluss und Funken rampen zeitversetzt von 0 auf 1 (Power-on-Choreografie).
Performance-Regeln (auf die harte Tour gelernt)
new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false })plusrenderer.setClearColor(0x140e09, 1)—alpha: truekostete in Tests 61 → 2 fps. Den Hintergrund-Hex hart kodieren, nie ausgetComputedStyleparsen (liefertoklch()).- Kickoff ausschließlich über
requestAnimationFrame(loop)— nieloop()synchron vor Ende der Deklarationen aufrufen (TDZ-Falle beiconst). - Alle Glut-Materialien:
transparent + AdditiveBlending + depthWrite: false, der Kern-Halo zusätzlichdepthTest: false— bewusst gesetzt, sonst werden Partikel weggecullt oder der Halo von Geometrie zerschnitten. devicePixelRatiobei 2 gekappt; Render-Loop pausiert beidocument.hidden.- Keine Allokationen im Frame: Kurven-Samples, Positions-Arrays und Vektoren werden einmal angelegt und wiederverwendet; nur
BufferAttribute.needsUpdatepro Frame. - Mobil reduzierte Stückzahlen (Partikel, Nodes, Funken, Satelliten) und größerer Kamera-Abstand.
prefers-reduced-motion: exakt ein statisches Frame, neu gerendert nur bei Scroll/Resize. Ohne WebGL: statisches SVG-Poster mit CSS-Glut.- DOM-Animationen ausschließlich über
transformundopacity(IntersectionObserver-Reveals, ease-out-quart).
5 · Replikation in 8 Schritten
- Palette zuerst: Grundfarbe als warmes Fast-Schwarz in OKLCH festlegen (L ≈ 0.17, Hue 55–65), einen einzigen Akzent-Hue wählen (hier 45–78) und die Hex-Zwillinge für WebGL notieren.
- Fonts selbst hosten: woff2 (latin) von der Google-Fonts-CSS-API laden,
fvarprüfen, als Variable-Font-Range deklarieren. Keine externen Requests im Endprodukt. - DOM komplett vor WebGL bauen: semantische Landmarken, Sektionen mit
data-cam-Markern, Panels mit halbtransparenten (nicht geblurrten) Flächen für AA-Kontrast über der Szene. - Renderer konservativ starten:
alpha: false, ClearColor = Seitengrund, DPR-Cap, Resize-Handler, try/catch mit Poster-Fallback. - Maschine schichtweise aufbauen: erst Ringe + Kern, dann Fluss, dann Funken und Satelliten. Jede Schicht einzeln testen, Stückzahlen zuletzt hochdrehen.
- Kamera-Keyframes je Sektion definieren und über geglätteten Scroll-Fortschritt blenden; Sektions-Offsets nach
document.fonts.readyneu vermessen. - Choreografie: Power-on beim Laden (zeitversetzte Rampen), Reveals per IntersectionObserver, Puls-Details (Atmen, Lastspitzen) als Sinus-Funktionen der Loop-Zeit.
- Härten: reduced-motion-Pfad,
document.hidden-Pause, Mobil-Stückzahlen, 390 px- und 1440 px-Viewports prüfen,overflow-x: clipgegen horizontales Scrollen.
6 · Bewusst weggelassen
- Cyan, Blau-Neon, Lila-Gradients — die KI-Klischee-Palette.
- Glassmorphism und
backdrop-filter: blur()— Kontrast kommt aus soliden getönten Flächen. - Gradient-Text, seitliche Akzentstreifen, identische Kartenraster, Hero-Metrik-Schablonen.
- Raster-Bilder und externe Requests jeder Art — alles ist Code, Geometrie oder SVG.
- Scroll-Hijacking: die Seite scrollt nativ, nur die Kamera interpretiert den Fortschritt.