Nahtlose Übergabe: Loader-Element wird Teil des Layouts
Logo, Zähler, Ladebalken und Bild verschwinden nicht mit dem Loader, sondern reisen per Flip an ihren Platz im Layout: Die Seite baut sich um sie herum auf.
- Der Loader besteht aus Elementen, die nach dem Laden bleiben und zu Layoutbausteinen werden. Das Ladebild wächst zur Hero, der Zähler wird Kennzahl oder Seitenzahl, das Logo fliegt in die Navigation, der Ladebalken wird zur Trennlinie.
- Die Übergabe läuft per Flip, Clip-Path oder geteilten Koordinaten. Es gibt keinen Fade-to-Black und keinen harten Schnitt.
- Der restliche Inhalt baut sich um die übergebenen Elemente herum auf.
Der Loader ist der erste Akt der Seite und kein Wartezimmer. Das Auge folgt einem Objekt aus dem Loader direkt in die Seite. So entsteht Kontinuität statt Unterbrechung.
Wann einsetzen
Wenn ein Preloader ohnehin nötig ist, etwa für WebGL, Video oder viele Bilder. Und bei Portfolios, die ein Intro inszenieren.
Wann nicht
Wenn kein Loader nötig ist. Dann besser intro-ohne-wartezeit.
Varianten
- Ladebild wird zur Hero per Flip (Position und Größe)
- Ladebild wird zur Hero per clip-path: Fenster inset(…) → inset(0), Bild zoomt gegenläufig zurück
- Bildstapel wird Raster: die oberste Kachel wird Hero, die übrigen werden Vorschaubilder
- Logo fliegt in die Navigation (Flip)
- Zähler wird Seitennummer, Jahreszahl oder Kennzahl
- Mehrstufig: Loader, dann Intro-Screen, dann Layout
Umsetzung
Loader-Elemente sind dieselben DOM-Knoten wie die späteren Layout-Elemente, zum Beispiel das Hero-Bild. Beim Start hängt man sie in Plätze des Loaders um oder setzt eine Klasse wie .is-loading, die Fenstergröße und Zentrierung regelt. Nach dem Laden: const s = Flip.getState(els, { props: 'color' }); Knoten zurückhängen bzw. Klasse entfernen; Flip.from(s, { duration: 1, ease: 'expo.inOut' }). Text und Linien mit scale: true, Bilder ohne scale, sonst verzerrt object-fit. Das Loader-Panel gleichzeitig per clip-path inset(2.5rem) → inset(100% 0 0 0) wegziehen. Alternative ohne Umhängen: clip-path von einem kleinen Fenster auf inset(0) mit Gegen-Scale des Bildes. Fallstrick: Kein Element klonen oder ersetzen, sonst flackert der Bildwechsel. Die Übergabe erst starten, wenn Schriften und Bilder wirklich geladen sind (document.fonts.ready, img.decode()).
Robust umsetzen
Telefon. Funktioniert gut. Die Fenstergröße relativ zur kürzeren Bildschirmseite wählen. Ziele, die im einspaltigen Layout unter dem sichtbaren Bereich liegen, reisen aus dem Bild; das wichtigste Element (Hero-Bild) sollte oben landen.
Ohne Bewegung. Elemente stehen sofort in Endposition, kurzer Fade. Kein Zähler, kein Wischen.
Barrierefrei. Während des Loaders aria-busy="true" am main, danach entfernen. Den Fokus nicht verschieben. Das Loader-Panel ist aria-hidden; die Kennzahl bleibt als normaler Text im Layout lesbar.
Code
<!DOCTYPE html>
<!--
Nahtlose Übergabe: Loader-Element wird Teil des Layouts
Fundus · Preloader & Intro · https://cecodesign.de/lab/fundus/preloader-intro/loader-wird-layout/
Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Nahtlose Übergabe: Loader wird Layout · Fundus-Demo</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<style>
@font-face { font-family: 'Instrument Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-serif@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'Instrument Serif'; font-style: italic; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-serif@latest/latin-400-italic.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'Instrument Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-sans:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 400 700; font-display: swap; }
@font-face { font-family: 'Fragment Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/fragment-mono@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-display: swap; }
/* Mira Sand Fotografie, Berlin · Rezept „Bild zuerst“: neutrale Fläche, die keine Farbe ins Bild wirft,
kleine Typo, Serif nur für Namen und Serientitel, Zähler und Bildunterschrift in Mono */
:root {
--papier: #f4f4f2; --tinte: #141414; --leise: #6b6b69; --linie: rgba(20, 20, 20, .14); --platzhalter: #e6e6e6;
--film: #0e0e0e; --hell: #f2f2f2; --hell-leise: rgba(242, 242, 242, .52); --hell-linie: rgba(242, 242, 242, .16);
--kurve: cubic-bezier(0.87, 0, 0.13, 1); /* expo.inOut, Messwert */
--rand: 2.5rem; /* Loader-Panel inset(2.5rem), Messwert */
--pult: 58px;
--gx: clamp(20px, 3.2vw, 48px);
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--papier); color: var(--tinte); font-family: 'Instrument Sans', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
html:has(.buehne[data-zustand='laden']) { overflow: hidden; }
a { color: inherit; text-decoration: none; }
img { display: block; width: 100%; height: 100%; object-fit: cover; }
.mono { font: 400 11px/1.45 'Fragment Mono', ui-monospace, monospace; letter-spacing: .06em; text-transform: uppercase; color: var(--leise); font-variant-numeric: tabular-nums; }
.serif { font-family: 'Instrument Serif', Georgia, serif; font-weight: 400; }
.serif i, i.serif { font-style: italic; }
/* ── Bühne: die fertige Seite. Alle Übergabe-Elemente wohnen hier. ── */
.buehne { position: relative; height: calc(100svh - var(--pult)); min-height: 560px; display: grid; grid-template-rows: auto minmax(0, 1fr); }
.nav { display: grid; grid-template-columns: minmax(0, .9fr) minmax(0, 1.1fr); align-items: start; gap: 24px clamp(24px, 4vw, 64px); padding: 18px var(--gx) 16px; }
.marke { position: relative; display: inline-flex; align-items: baseline; gap: .3em; justify-self: start; font: 400 28px/1 'Instrument Serif', Georgia, serif; letter-spacing: -.01em; transform-origin: left top; white-space: nowrap; }
.marke sup { font: 400 .34em/1 'Fragment Mono', monospace; letter-spacing: .08em; text-transform: uppercase; vertical-align: .95em; opacity: .62; }
.nav__rechts { display: flex; justify-content: space-between; align-items: start; gap: 24px; }
.nav ul { display: grid; grid-template-columns: repeat(2, auto); gap: 2px clamp(28px, 4vw, 64px); list-style: none; padding: 0; font-size: 14px; line-height: 1.45; }
.nav ul a { color: var(--leise); transition: color 150ms; }
.nav ul a:hover, .nav ul a[aria-current] { color: var(--tinte); }
.anfrage { font-size: 14px; line-height: 1.45; text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 4px; text-decoration-color: var(--linie); transition: text-decoration-color 150ms; }
.anfrage:hover { text-decoration-color: currentColor; }
.menue { display: none; font-size: 14px; line-height: 1.45; }
/* Ladebalken = Trennlinie unter der Navigation */
.linie { padding: 0 var(--gx); }
.balken { position: relative; display: block; height: 1px; color: var(--linie); transform-origin: left center; }
.balken i { position: absolute; inset: 0; background: currentColor; transform-origin: left center; transform: scaleX(1); }
.hero { display: grid; grid-template-columns: minmax(0, .9fr) minmax(0, 1.1fr); grid-template-areas: 'text bild'; gap: clamp(24px, 4vw, 64px); padding: clamp(20px, 3.4vh, 36px) var(--gx) clamp(18px, 3vh, 30px); min-height: 0; }
.hero__text { grid-area: text; display: grid; grid-template-rows: auto minmax(0, 1fr) auto; gap: 20px; min-height: 0; }
.augenbraue { display: flex; gap: 4px 18px; flex-wrap: wrap; }
.kern { align-self: end; display: grid; gap: clamp(14px, 2.2vh, 24px); }
.titel { font: 400 clamp(52px, 6.2vw, 104px)/.92 'Instrument Serif', Georgia, serif; letter-spacing: -.02em; }
.titel i { font-style: italic; }
.maske { display: block; overflow: hidden; padding-bottom: .1em; margin-bottom: -.1em; }
.maske > span { display: block; }
.lead { margin-top: clamp(4px, 1vh, 12px); font-size: clamp(15px, 1.15vw, 17px); line-height: 1.55; max-width: 42ch; color: rgba(20, 20, 20, .76); }
.unten { display: flex; align-items: end; justify-content: space-between; gap: 24px; padding-top: 18px; border-top: 1px solid var(--linie); transition: border-color 1s linear; }
.buehne[data-zustand='laden'] .unten { border-color: transparent; transition: none; }
.bilanz { display: flex; align-items: end; gap: 14px; }
.zahl { position: relative; display: inline-block; font: 400 clamp(52px, 4.8vw, 80px)/.78 'Instrument Serif', Georgia, serif; letter-spacing: -.03em; font-variant-numeric: tabular-nums lining-nums; transform-origin: left bottom; }
.bilanz .mono { padding-bottom: 1px; }
.zuletzt { display: grid; gap: 8px; justify-items: end; }
.reihe { display: flex; gap: 6px; }
.ablage { width: clamp(52px, 5vw, 78px); aspect-ratio: 4 / 5; }
.hero__bild { grid-area: bild; display: grid; grid-template-rows: minmax(0, 1fr) auto; gap: 10px; min-height: 0; }
.fassung { min-height: 0; }
.unterzeile { display: flex; justify-content: space-between; gap: 6px 16px; flex-wrap: wrap; }
.unterzeile b { color: var(--tinte); font-weight: 400; margin-right: 1.4em; }
/* Bild-Kacheln: dieselben Knoten im Loader und im Layout; Platzhalter-Grau reserviert den Raum */
.kachel { position: relative; width: 100%; height: 100%; overflow: hidden; background: var(--platzhalter); }
.kachel img { transform-origin: 50% 50%; }
/* ── Loader-Panel: Dunkelkammer mit Plätzen, in die die Layout-Knoten beim Start umziehen ── */
.lader { display: none; position: absolute; z-index: 10; top: 0; left: 0; right: 0; height: min(100%, 100svh); background: var(--film); color: var(--hell);
clip-path: inset(var(--rand)); padding: calc(var(--rand) + clamp(18px, 2.4vw, 32px));
grid-template-columns: 1fr auto; grid-template-rows: auto minmax(0, 1fr) auto auto; grid-template-areas: 'marke info' 'fenster fenster' 'balken balken' 'zahl ort'; row-gap: clamp(14px, 2.4vh, 24px); }
.buehne[data-zustand='laden'] .lader, .buehne[data-zustand='uebergabe'] .lader { display: grid; }
/* bis Schriften und Bilder bereit sind: nur das leere Panel, keine Seite am Rand */
.buehne[data-wartet] > :not(.lader) { visibility: hidden; }
.lader .mono { color: var(--hell-leise); }
.p-marke { grid-area: marke; }
.p-info { grid-area: info; text-align: right; }
.p-fenster { grid-area: fenster; place-self: center; position: relative; width: calc(min(100vw, 100svh) * .32); aspect-ratio: 4 / 5; max-height: 100%; }
/* Schnittmarken wie auf dem Kontaktbogen; bleiben im Panel, wenn das Bild abreist */
.p-fenster::before, .p-fenster::after { content: ''; position: absolute; inset: -12px; pointer-events: none;
background: linear-gradient(var(--hell-leise) 0 0) 0 0 / 14px 1px no-repeat, linear-gradient(var(--hell-leise) 0 0) 0 0 / 1px 14px no-repeat,
linear-gradient(var(--hell-leise) 0 0) 100% 100% / 14px 1px no-repeat, linear-gradient(var(--hell-leise) 0 0) 100% 100% / 1px 14px no-repeat; }
.p-fenster::after { transform: scaleX(-1); }
.buehne[data-variante='clip'] .p-fenster::before, .buehne[data-variante='clip'] .p-fenster::after { display: none; }
.p-bild { position: absolute; left: calc(100% + 22px); bottom: -4px; white-space: nowrap; }
.buehne[data-variante='clip'] .p-bild { display: none; }
.p-balken { grid-area: balken; }
.p-zahl { grid-area: zahl; align-self: end; }
.p-ort { grid-area: ort; align-self: end; text-align: right; }
/* Aussehen der Knoten, solange sie im Loader stehen */
.p-marke .marke { font-size: clamp(44px, 6.8vw, 112px); color: var(--hell); }
.p-zahl .zahl { font-size: clamp(104px, 16vw, 248px); color: var(--hell); }
.p-balken { height: 1px; background: var(--hell-linie); }
.p-balken .balken { color: var(--hell); }
.p-fenster .kachel { position: absolute; inset: 0; background: #1b1b1b; }
/* Während Laden und Übergabe: Übergabe-Knoten über dem Panel, Rest der Seite unsichtbar */
.buehne[data-zustand='uebergabe'] .reist { z-index: 20; }
.buehne[data-variante='clip'][data-zustand='laden'] .fassung .kachel { z-index: 20; }
.buehne[data-zustand='laden'] .nach, .buehne[data-zustand='laden']:not([data-variante='stapel']) .ablage { opacity: 0; }
.buehne[data-zustand='laden'] .maske > span { visibility: hidden; }
/* ── Pult: Varianten und Werte ── */
.pult { height: var(--pult); display: flex; align-items: center; justify-content: space-between; gap: 10px 28px; white-space: nowrap; padding: 0 var(--gx); border-top: 1px solid var(--linie); }
.modi { display: flex; gap: 4px; flex-wrap: wrap; align-items: center; flex-shrink: 0; }
.modi > span { margin-right: 8px; }
.modi button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: var(--leise); background: none; border: 1px solid var(--linie); border-radius: 2px; height: 28px; padding: 0 11px; cursor: pointer; transition: background 150ms, color 150ms, border-color 150ms; }
.modi button:hover { color: var(--tinte); border-color: var(--leise); }
.modi button[aria-pressed='true'] { background: var(--tinte); color: var(--papier); border-color: var(--tinte); }
.modi .nochmal { border-color: transparent; }
.werte { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 2px 14px; font-size: 10px; min-width: 0; }
.werte b { color: var(--tinte); font-weight: 400; }
/* Reduzierte Bewegung: kein Loader, Seite steht, kurzer Fade */
.reduziert .lader { display: none !important; }
.reduziert .buehne { animation: einblenden 250ms linear both; }
@keyframes einblenden { from { opacity: 0; } }
.reduziert .modi button, .reduziert .nav ul a, .reduziert .anfrage { transition: none; }
/* Vorschau: nur Bühne und Umschalter */
.vorschau body { overflow: hidden; }
.vorschau .werte, .vorschau .anfrage { display: none; }
@media (max-width: 1240px) {
:root { --pult: 86px; }
.pult { flex-direction: column; align-items: flex-start; justify-content: center; gap: 8px; }
.werte { justify-content: flex-start; }
.werte span:nth-child(4) { display: none; }
html.vorschau { --pult: 58px; }
}
@media (max-width: 1080px) {
.nav ul { display: none; }
.nav__rechts { justify-content: flex-end; gap: 22px; }
.menue { display: inline; }
}
@media (max-width: 760px) {
:root { --rand: 1.25rem; }
.nav { grid-template-columns: 1fr auto; }
.buehne { height: auto; min-height: 0; }
.hero { grid-template-columns: 1fr; grid-template-areas: none; gap: 20px; padding-top: 22px; padding-bottom: 32px; }
.hero__text { display: contents; }
.augenbraue { order: 1; }
.kern { display: contents; }
.titel { order: 2; font-size: 19vw; }
.hero__bild { order: 3; grid-area: auto; }
.fassung { aspect-ratio: 4 / 5; max-height: 62svh; }
.lead { order: 4; }
.unten { order: 5; flex-wrap: wrap; }
.anfrage { display: none; }
.lader { grid-template-areas: 'marke marke' 'fenster fenster' 'balken balken' 'zahl ort'; }
.p-info, .p-bild { display: none; }
.p-marke .marke { font-size: 12vw; }
.p-fenster { width: calc(min(100vw, 100svh) * .52); }
.pult { height: auto; padding-top: 12px; padding-bottom: 14px; }
.werte { display: none; }
}
@media (hover: none) {
.modi button:hover { color: var(--leise); border-color: var(--linie); }
.modi button[aria-pressed='true']:hover { color: var(--papier); border-color: var(--tinte); }
.nav ul a:hover { color: var(--leise); }
.anfrage:hover { text-decoration-color: var(--linie); }
}
html, body { overflow-x: clip; } /* Kachel fliegt beim Intro kurz über den Rand */
</style>
</head>
<body>
<div class="buehne" data-zustand="laden" data-variante="flip" data-wartet>
<header>
<nav class="nav" aria-label="Hauptnavigation">
<a class="marke" href="#">Mira Sand<sup>Fotografie</sup></a>
<div class="nav__rechts">
<ul class="nach">
<li><a href="#" aria-current="page">Serien</a></li>
<li><a href="#">Porträts</a></li>
<li><a href="#">Aufträge</a></li>
<li><a href="#">Über mich</a></li>
</ul>
<span class="nach"><a class="anfrage" href="#">Auftrag anfragen</a><span class="menue">Menü</span></span>
</div>
</nav>
<div class="linie"><span class="balken" aria-hidden="true"><i></i></span></div>
</header>
<main class="hero" id="inhalt">
<div class="hero__text">
<p class="augenbraue mono nach"><span>Serie 07 · Lausitz</span><span>2023–2026</span></p>
<div class="kern">
<h1 class="titel"><span class="maske"><span>Nach der</span></span><span class="maske"><span><i>Kohle</i></span></span></h1>
<p class="lead nach">Ein Jahr lang habe ich Menschen in Boxberg, Weißwasser und Hoyerswerda besucht, in dem Jahr, in dem ihr Kraftwerksblock vom Netz ging. Mittelformat, Tageslicht, kein Studio.</p>
<div class="unten">
<div class="bilanz"><span class="zahl">100</span><p class="mono nach">Porträts<br>in der Serie</p></div>
<div class="zuletzt">
<p class="mono nach">Aus der Serie</p>
<div class="reihe">
<div class="ablage"><div class="kachel" data-rolle="karte" data-motiv="nacht"><img alt="Kühltürme bei Nacht, der Dampf von Natriumlicht orange beleuchtet" width="640" height="800"></div></div>
<div class="ablage"><div class="kachel" data-rolle="karte" data-motiv="halle"><img alt="Schwimmhalle von oben, eine Schwimmerin zwischen den Leinen" width="640" height="800"></div></div>
<div class="ablage"><div class="kachel" data-rolle="karte" data-motiv="spinde"><img alt="Umkleide mit grünen Spinden, ein weißer Helm auf der Bank" width="640" height="800"></div></div>
</div>
</div>
</div>
</div>
</div>
<figure class="hero__bild">
<div class="fassung"><div class="kachel" data-rolle="held" data-motiv="see"><img alt="Renate K. in orangefarbener Arbeitsjacke am Ufer des Restsees, im Dunst die Kühltürme" width="1500" height="1200"></div></div>
<figcaption class="unterzeile mono nach"><span><b>01 / 100</b>Renate K., Schichtleiterin</span><span>Mittelformat 6×7 · 2025</span></figcaption>
</figure>
</main>
<div class="lader" aria-hidden="true">
<div class="p-marke"></div>
<p class="p-info mono">Serie 07 · Nach der Kohle<br>Kontaktbogen wird geladen</p>
<div class="p-fenster"><span class="p-bild mono">Rolle 14 · Bild 7A</span></div>
<div class="p-balken"></div>
<div class="p-zahl"></div>
<p class="p-ort mono">Boxberg/O.L.<br>51° 25′ N · 14° 34′ O</p>
</div>
</div>
<div class="pult">
<div class="modi mono" role="group" aria-label="Übergabe">
<span>Übergabe</span>
<button type="button" data-v="flip" aria-pressed="true">Bild-Flip</button>
<button type="button" data-v="clip" aria-pressed="false">Clip-Fenster</button>
<button type="button" data-v="stapel" aria-pressed="false">Stapel → Raster</button>
<button type="button" class="nochmal" data-nochmal>↻ Nochmal</button>
</div>
<p class="werte mono" data-werte></p>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/Flip.min.js"></script>
<script>
/* Bildwelt: Die vier „Fotos“ der Serie werden einmal auf Canvas gemalt und als echte <img> geladen.
Auf einer echten Seite stehen hier die Fotodateien; der Loader wartet genauso auf img.decode(). */
const MOTIVE = (() => {
const zufall = s => () => { s = s + 0x6D2B79F5 | 0; let t = Math.imul(s ^ s >>> 15, 1 | s); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; };
const verlauf = (c, x0, y0, x1, y1, stopps) => { const g = c.createLinearGradient(x0, y0, x1, y1); stopps.forEach(([o, f]) => g.addColorStop(o, f)); return g; };
const rund = (c, x, y, r0, r1, stopps) => { const g = c.createRadialGradient(x, y, r0, x, y, r1); stopps.forEach(([o, f]) => g.addColorStop(o, f)); return g; };
const ellipse = (c, x, y, rx, ry, farbe, rot = 0) => { c.fillStyle = farbe; c.beginPath(); c.ellipse(x, y, rx, ry, rot, 0, Math.PI * 2); c.fill(); };
const unscharf = (c, px, f) => { c.save(); c.filter = `blur(${px}px)`; f(); c.restore(); };
const korn = (c, w, h, staerke, r) => {
const d = c.getImageData(0, 0, w, h), p = d.data;
for (let i = 0; i < p.length; i += 4) { const n = (r() - .5) * staerke; p[i] += n; p[i + 1] += n; p[i + 2] += n; }
c.putImageData(d, 0, 0);
};
// Kühlturm: Hyperboloid mit Taille bei 72 % der Höhe
const turm = (c, x, fuss, hoehe, breite, farbe) => {
const b = breite / 2, taille = b * .64, oben = b * .72, ty = fuss - hoehe, wy = fuss - hoehe * .72;
c.fillStyle = farbe; c.beginPath();
c.moveTo(x - b, fuss);
c.quadraticCurveTo(x - taille * .9, wy + hoehe * .1, x - taille, wy);
c.quadraticCurveTo(x - taille * 1.02, ty + hoehe * .1, x - oben, ty);
c.lineTo(x + oben, ty);
c.quadraticCurveTo(x + taille * 1.02, ty + hoehe * .1, x + taille, wy);
c.quadraticCurveTo(x + taille * .9, wy + hoehe * .1, x + b, fuss);
c.closePath(); c.fill();
};
const runderRahmen = (c, x, y, w, h, r, farbe) => { c.fillStyle = farbe; c.beginPath(); c.roundRect(x, y, w, h, r); c.fill(); };
// Hero: Porträt in der Landschaft. Bedeckter Himmel, Kühltürme im Dunst, Restsee, Sandterrasse, Figur in Arbeitsjacke.
function see(c, w, h, r) {
const hz = h * .44;
c.fillStyle = verlauf(c, 0, 0, 0, hz, [[0, '#a9b4b9'], [.55, '#cdd1ce'], [1, '#e4e2da']]); c.fillRect(0, 0, w, hz + 4);
unscharf(c, 46, () => { for (let i = 0; i < 10; i++) ellipse(c, r() * w, r() * hz * .8, w * (.12 + r() * .22), h * (.015 + r() * .03), i % 3 ? 'rgba(255,255,255,.32)' : 'rgba(108,120,128,.2)'); });
// Kraftwerk im Dunst, Dampf zieht nach rechts
[[.62, .17], [.7, .19]].forEach(([tx, th], j) => {
for (let i = 0; i < 9; i++) unscharf(c, 10 + i * 5, () => ellipse(c, w * (tx + i * .028 + j * .01), hz - h * (th + .02 + i * .018), w * (.025 + i * .016), h * (.02 + i * .009), `rgba(246,246,242,${.62 - i * .055})`));
});
unscharf(c, 1.2, () => {
turm(c, w * .62, hz + 2, h * .17, w * .09, verlauf(c, w * .575, 0, w * .665, 0, [[0, '#adb4b4'], [1, '#99a2a3']]));
turm(c, w * .7, hz + 2, h * .19, w * .1, verlauf(c, w * .65, 0, w * .75, 0, [[0, '#a8b0b0'], [1, '#929c9d']]));
c.fillStyle = '#a7aeae'; c.fillRect(w * .755, hz - h * .07, w * .07, h * .07 + 2); c.fillRect(w * .79, hz - h * .2, w * .009, h * .13);
});
// Baumkante am anderen Ufer
unscharf(c, 1.6, () => {
c.fillStyle = '#7c8684'; c.beginPath(); c.moveTo(0, hz + 6);
for (let x = 0; x <= w; x += 4) c.lineTo(x, hz - 6 - Math.abs(Math.sin(x / 23) * Math.sin(x / 61)) * 7 - r() * 2.5 - Math.sin(x / 140) * 3 - (x < w * .45 ? 5 : 0));
c.lineTo(w, hz + 8); c.closePath(); c.fill();
});
// Wasser
const ufer = h * .62;
c.fillStyle = verlauf(c, 0, hz, 0, ufer, [[0, '#b6bebc'], [.5, '#a3adac'], [1, '#909c9c']]); c.fillRect(0, hz + 6, w, h * .78 - hz);
unscharf(c, 3, () => { c.fillStyle = 'rgba(124,134,132,.45)'; c.fillRect(0, hz + 4, w, 9); });
unscharf(c, 6, () => { c.globalAlpha = .35; turm(c, w * .7, hz + 10, -h * .08, w * .1, '#8e9898'); c.globalAlpha = 1; });
for (let i = 0; i < 70; i++) { c.fillStyle = `rgba(255,255,255,${.04 + r() * .1})`; c.fillRect(r() * w, hz + 10 + r() * (ufer - hz - 14), w * (.02 + r() * .12), 1.5); }
// Sandterrasse mit Abbruchkanten
c.fillStyle = verlauf(c, 0, ufer - 20, 0, h, [[0, '#cfbe99'], [.45, '#bba27a'], [1, '#9d8662']]);
c.beginPath(); c.moveTo(0, ufer + 26); c.bezierCurveTo(w * .3, ufer - 6, w * .62, ufer + 4, w, ufer - 10); c.lineTo(w, h); c.lineTo(0, h); c.closePath(); c.fill();
unscharf(c, 4, () => { c.fillStyle = 'rgba(120,98,66,.35)'; c.beginPath(); c.moveTo(0, ufer + 26); c.bezierCurveTo(w * .3, ufer - 6, w * .62, ufer + 4, w, ufer - 10); c.lineTo(w, ufer + 2); c.bezierCurveTo(w * .62, ufer + 18, w * .3, ufer + 8, 0, ufer + 40); c.closePath(); c.fill(); });
c.strokeStyle = 'rgba(92,72,44,.14)'; c.lineWidth = 2;
for (let k = 0; k < 6; k++) { const y = ufer + 40 + k * h * .055; c.beginPath(); c.moveTo(-10, y + 30); for (let x = 0; x <= w + 20; x += 40) c.lineTo(x, y - x * .04 + Math.sin(x / 70 + k) * 5 + r() * 3); c.stroke(); }
unscharf(c, 3, () => { c.fillStyle = 'rgba(255,250,235,.25)'; c.fillRect(0, ufer + 6, w, 10); });
// Grasbüschel
c.lineCap = 'round';
for (let i = 0; i < 260; i++) {
const x = r() < .5 ? r() * w * .3 : w * .72 + r() * w * .28, y = h * .76 + r() * h * .24, l = 10 + r() * 26;
c.strokeStyle = r() < .5 ? 'rgba(84,90,58,.75)' : 'rgba(128,124,80,.7)'; c.lineWidth = 1.6 + r() * 1.4;
c.beginPath(); c.moveTo(x, y); c.quadraticCurveTo(x + (r() - .5) * 14, y - l * .6, x + (r() - .5) * 18, y - l); c.stroke();
}
// Figur: Renate K., frontal, klein im Bild
const H = h * .25, fx = w * .5, fy = h * .8, s = H / 100;
unscharf(c, 5, () => ellipse(c, fx + 6 * s, fy, 22 * s, 3.2 * s, 'rgba(70,52,30,.3)'));
runderRahmen(c, fx - 9 * s, fy - 46 * s, 8 * s, 45 * s, 2 * s, '#2b2d31');
runderRahmen(c, fx + 1 * s, fy - 46 * s, 8 * s, 45 * s, 2 * s, '#25272b');
runderRahmen(c, fx - 10.5 * s, fy - 3 * s, 10 * s, 4 * s, 1.5 * s, '#18191b');
runderRahmen(c, fx + .5 * s, fy - 3 * s, 10 * s, 4 * s, 1.5 * s, '#18191b');
// Arme
c.save(); c.translate(fx - 15 * s, fy - 82 * s); c.rotate(.07); runderRahmen(c, -3.5 * s, 0, 7 * s, 38 * s, 3.5 * s, '#d4612a'); runderRahmen(c, -3.5 * s, 22 * s, 7 * s, 2.6 * s, 0, '#e3e2da'); runderRahmen(c, -2.8 * s, 37 * s, 5.6 * s, 6 * s, 2.8 * s, '#b98b72'); c.restore();
c.save(); c.translate(fx + 15 * s, fy - 82 * s); c.rotate(-.07); runderRahmen(c, -3.5 * s, 0, 7 * s, 38 * s, 3.5 * s, '#c4572a'); runderRahmen(c, -3.5 * s, 22 * s, 7 * s, 2.6 * s, 0, '#d9d8d0'); runderRahmen(c, -2.8 * s, 37 * s, 5.6 * s, 6 * s, 2.8 * s, '#a97c64'); c.restore();
// Jacke
c.fillStyle = verlauf(c, fx - 15 * s, 0, fx + 15 * s, 0, [[0, '#e06a2e'], [.6, '#d45f28'], [1, '#b84f24']]);
c.beginPath(); c.moveTo(fx - 16 * s, fy - 80 * s); c.quadraticCurveTo(fx - 17 * s, fy - 85 * s, fx - 9 * s, fy - 86 * s); c.lineTo(fx + 9 * s, fy - 86 * s);
c.quadraticCurveTo(fx + 17 * s, fy - 85 * s, fx + 16 * s, fy - 80 * s); c.lineTo(fx + 13 * s, fy - 42 * s); c.lineTo(fx - 13 * s, fy - 42 * s); c.closePath(); c.fill();
c.fillStyle = '#e4e3dc'; c.fillRect(fx - 14.5 * s, fy - 56 * s, 29 * s, 3 * s); c.fillRect(fx - 15.5 * s, fy - 66 * s, 31 * s, 2.4 * s);
c.fillStyle = 'rgba(40,20,10,.35)'; c.fillRect(fx - .6 * s, fy - 85 * s, 1.2 * s, 43 * s);
// Hals, Kopf, graues Haar
runderRahmen(c, fx - 3.2 * s, fy - 91 * s, 6.4 * s, 6 * s, 2 * s, '#b58a70');
ellipse(c, fx, fy - 96.5 * s, 6.6 * s, 8 * s, '#c49b81');
c.fillStyle = 'rgba(90,55,40,.25)'; c.beginPath(); c.ellipse(fx + 2.4 * s, fy - 96 * s, 4 * s, 7.4 * s, 0, -Math.PI / 2, Math.PI / 2); c.fill();
// Strickmütze
c.fillStyle = '#2c3440'; c.beginPath(); c.ellipse(fx, fy - 99.5 * s, 7.2 * s, 6.6 * s, 0, Math.PI, 0); c.fill();
runderRahmen(c, fx - 7.4 * s, fy - 101 * s, 14.8 * s, 3.2 * s, 1.2 * s, '#36404d');
korn(c, w, h, 10, r);
}
// Nachtschicht: Kühltürme, Dampf im Natriumlicht, Warnleuchten
function nacht(c, w, h, r) {
const hz = h * .7;
c.fillStyle = verlauf(c, 0, 0, 0, hz, [[0, '#070b14'], [.55, '#18233a'], [.9, '#4a3c38'], [1, '#6b4a34']]); c.fillRect(0, 0, w, h);
c.fillStyle = rund(c, w * .5, hz, 10, w * .85, [[0, 'rgba(255,160,70,.55)'], [.4, 'rgba(255,130,60,.16)'], [1, 'rgba(255,130,60,0)']]); c.fillRect(0, 0, w, h);
unscharf(c, 26, () => {
for (let i = 0; i < 12; i++) {
const x = w * (.3 + r() * .5) + i * 6, y = hz - h * (.3 + r() * .3);
ellipse(c, x, y, w * (.12 + r() * .14), h * (.05 + r() * .05), i < 6 ? 'rgba(236,160,104,.42)' : 'rgba(150,150,166,.28)');
}
});
unscharf(c, 12, () => { ellipse(c, w * .44, hz - h * .33, w * .2, h * .08, 'rgba(255,190,130,.5)'); ellipse(c, w * .7, hz - h * .38, w * .16, h * .07, 'rgba(255,180,120,.4)'); });
turm(c, w * .38, hz + 4, h * .34, w * .4, '#10141c');
turm(c, w * .78, hz + 4, h * .3, w * .34, '#0d1118');
c.fillStyle = '#0b0e14'; c.fillRect(w * .02, hz - h * .1, w * .2, h * .1 + 4); c.fillRect(w * .09, hz - h * .42, w * .028, h * .34);
// Rand-Licht an den Turmkanten
c.fillStyle = 'rgba(255,170,90,.18)'; c.fillRect(w * .38 - w * .145, hz - h * .34, w * .29, 3); c.fillRect(w * .78 - w * .122, hz - h * .3, w * .245, 3);
// Lampen
const lampe = (x, y, rr, farbe, glanz) => { c.fillStyle = rund(c, x, y, 0, rr * 7, [[0, glanz], [1, 'rgba(0,0,0,0)']]); c.fillRect(x - rr * 7, y - rr * 7, rr * 14, rr * 14); ellipse(c, x, y, rr, rr, farbe); };
for (let i = 0; i < 16; i++) lampe(w * (.02 + r() * .96), hz - r() * h * .04 + 2, 1.6 + r() * 1.6, '#ffd79a', 'rgba(255,190,110,.5)');
lampe(w * .38, hz - h * .345, 2.6, '#ff5a44', 'rgba(255,70,50,.6)'); lampe(w * .78, hz - h * .305, 2.6, '#ff5a44', 'rgba(255,70,50,.6)'); lampe(w * .104, hz - h * .425, 2.2, '#ff5a44', 'rgba(255,70,50,.6)');
// Acker im Vordergrund, Zaun
c.fillStyle = verlauf(c, 0, hz, 0, h, [[0, '#120f0e'], [1, '#050506']]); c.fillRect(0, hz + 4, w, h - hz);
c.strokeStyle = 'rgba(255,190,120,.16)'; c.lineWidth = 1.5; c.beginPath(); c.moveTo(0, hz + h * .07); c.lineTo(w, hz + h * .055); c.stroke();
for (let x = 8; x < w; x += 46) { c.fillStyle = 'rgba(0,0,0,.9)'; c.fillRect(x, hz + h * .035, 3, h * .05); }
korn(c, w, h, 16, r);
}
// Schwimmhalle von oben: Leinen, Bahnmarkierung, Schwimmerin, Lichtnetz
function halle(c, w, h, r) {
c.fillStyle = verlauf(c, 0, 0, w, h, [[0, '#2f9aab'], [.5, '#3fb0b8'], [1, '#2a8ea4']]); c.fillRect(0, 0, w, h);
c.strokeStyle = 'rgba(255,255,255,.07)'; c.lineWidth = 1.5;
for (let x = 0; x < w; x += 32) { c.beginPath(); c.moveTo(x, 0); c.lineTo(x, h); c.stroke(); }
for (let y = 0; y < h; y += 32) { c.beginPath(); c.moveTo(0, y); c.lineTo(w, y); c.stroke(); }
[w * .17, w * .5, w * .83].forEach(x => { c.fillStyle = 'rgba(14,52,84,.55)'; c.fillRect(x - 9, h * .06, 18, h * .88); c.fillRect(x - 34, h * .06, 68, 14); c.fillRect(x - 34, h * .94 - 14, 68, 14); });
// Schwimmerin, Kraul, mittlere Bahn
const sx = w * .5, sy = h * .5;
unscharf(c, 10, () => { ellipse(c, sx, sy + h * .08, w * .1, h * .2, 'rgba(255,255,255,.28)'); ellipse(c, sx, sy - h * .13, w * .07, h * .04, 'rgba(255,255,255,.4)'); });
unscharf(c, 1.4, () => {
c.fillStyle = '#b58f80';
c.save(); c.translate(sx - w * .03, sy - h * .07); c.rotate(-.12); runderRahmen(c, -w * .016, -h * .19, w * .032, h * .19, w * .016, '#b9927f'); c.restore();
c.save(); c.translate(sx + w * .035, sy - h * .05); c.rotate(.5); runderRahmen(c, -w * .016, 0, w * .032, h * .15, w * .016, '#a8857a'); c.restore();
ellipse(c, sx, sy, w * .062, h * .1, '#1c2a3e');
runderRahmen(c, sx - w * .045, sy + h * .06, w * .038, h * .2, w * .019, '#b08a7c'); runderRahmen(c, sx + w * .007, sy + h * .06, w * .038, h * .2, w * .019, '#a68375');
ellipse(c, sx - w * .004, sy - h * .105, w * .042, h * .038, '#f1c84a');
});
// Lichtnetz auf dem Grund
c.strokeStyle = 'rgba(230,255,255,.16)'; c.lineWidth = 2;
for (let i = 0; i < 46; i++) { const y0 = r() * h, a = 8 + r() * 14; c.beginPath(); const f = 30 + r() * 10, g = 70 + r() * 30; for (let x = -20; x <= w + 20; x += 6) c.lineTo(x, y0 + Math.sin(x / f + i) * a + Math.sin(x / g) * a * .6); c.stroke(); }
// Leinen
[w * .335, w * .665].forEach(x => { for (let y = -6, k = 0; y < h + 10; y += 15, k++) { ellipse(c, x, y, 7.5, 7, k % 6 < 3 ? '#d4473a' : '#f1efe8'); ellipse(c, x - 2, y - 2, 2.5, 2.3, 'rgba(255,255,255,.35)'); } });
korn(c, w, h, 8, r);
}
// Umkleide: grüne Spinde, eine Tür offen mit Arbeitsjacke, weißer Helm auf der Bank
function spinde(c, w, h, r) {
const boden = h * .7;
c.fillStyle = '#5f7166'; c.fillRect(0, 0, w, boden);
const n = 4, bw = w / n;
for (let i = 0; i < n; i++) {
const x = i * bw;
if (i === 2) {
c.fillStyle = '#1e2420'; c.fillRect(x + 6, 10, bw - 12, boden - 20);
c.fillStyle = '#d0682f'; c.beginPath(); c.moveTo(x + bw * .25, h * .1); c.lineTo(x + bw * .75, h * .1); c.lineTo(x + bw * .82, h * .5); c.lineTo(x + bw * .18, h * .5); c.closePath(); c.fill();
c.fillStyle = '#e2e1d9'; c.fillRect(x + bw * .2, h * .38, bw * .6, 7);
c.fillStyle = 'rgba(0,0,0,.3)'; c.fillRect(x + bw * .49, h * .1, 3, h * .4);
c.fillStyle = '#7d9184'; c.beginPath(); c.moveTo(x + bw - 6, 10); c.lineTo(x + bw + bw * .32, -10); c.lineTo(x + bw + bw * .32, boden + 10); c.lineTo(x + bw - 6, boden - 10); c.closePath(); c.fill();
continue;
}
c.fillStyle = verlauf(c, x, 0, x + bw, 0, [[0, '#7e9286'], [.85, '#738779'], [1, '#56665c']]); c.fillRect(x + 6, 10, bw - 12, boden - 20);
c.fillStyle = 'rgba(20,30,24,.55)'; for (let k = 0; k < 6; k++) runderRahmen(c, x + bw * .22, 40 + k * 14, bw * .56, 5, 2.5, 'rgba(20,30,24,.55)');
runderRahmen(c, x + bw * .38, h * .26, bw * .24, 22, 2, '#e9e7dc');
c.fillStyle = '#3b453e'; c.font = `${Math.round(bw * .1)}px 'Fragment Mono', monospace`; c.textAlign = 'center'; c.fillText(String(41 + i), x + bw * .5, h * .26 + 16);
runderRahmen(c, x + bw * .78, h * .36, 7, 30, 3, '#2a322d');
}
c.fillStyle = rund(c, w * .3, -h * .1, 10, h * .9, [[0, 'rgba(255,250,230,.32)'], [1, 'rgba(255,250,230,0)']]); c.fillRect(0, 0, w, boden);
// Boden, Fliesen
c.fillStyle = verlauf(c, 0, boden, 0, h, [[0, '#8f918c'], [1, '#6c6e6a']]); c.fillRect(0, boden, w, h - boden);
c.strokeStyle = 'rgba(40,40,40,.18)'; c.lineWidth = 1.5;
for (let k = 1; k < 6; k++) { const y = boden + (h - boden) * (k / 6) ** 1.4; c.beginPath(); c.moveTo(0, y); c.lineTo(w, y); c.stroke(); }
// Bank mit Helm und Handschuhen
unscharf(c, 8, () => ellipse(c, w * .5, h * .86, w * .48, h * .02, 'rgba(0,0,0,.35)'));
c.fillStyle = '#3a3b39'; c.fillRect(w * .12, h * .8, 8, h * .06); c.fillRect(w * .86, h * .8, 8, h * .06);
c.fillStyle = verlauf(c, 0, h * .76, 0, h * .8, [[0, '#b48a5c'], [1, '#86643f']]); c.fillRect(w * .04, h * .76, w * .92, h * .045);
const hx = w * .38, hy = h * .76;
c.fillStyle = rund(c, hx - w * .04, hy - h * .07, 4, w * .2, [[0, '#ffffff'], [.6, '#e4e4de'], [1, '#a9aaa4']]);
c.beginPath(); c.ellipse(hx, hy, w * .14, h * .085, 0, Math.PI, 0); c.fill();
c.fillStyle = '#c9cac3'; c.beginPath(); c.ellipse(hx, hy - 2, w * .165, h * .012, 0, 0, Math.PI * 2); c.fill();
c.fillStyle = 'rgba(0,0,0,.08)'; c.fillRect(hx - 3, hy - h * .085, 6, h * .083);
runderRahmen(c, w * .64, hy - 16, w * .14, 16, 6, '#c9b25a'); runderRahmen(c, w * .66, hy - 26, w * .12, 12, 6, '#b39d4e');
c.fillStyle = 'rgba(0,0,0,.18)'; c.fillRect(0, 0, w, 10);
korn(c, w, h, 10, r);
}
const MALER = { see, nacht, halle, spinde };
// malt alle Motive in die vorhandenen <img> (Größe aus width/height); ergibt eine Promise
return () => Promise.all([...document.querySelectorAll('.kachel[data-motiv] img')].map((img, i) => new Promise(fertig => {
const w = +img.getAttribute('width'), h = +img.getAttribute('height');
const cv = document.createElement('canvas'); cv.width = w; cv.height = h;
const c = cv.getContext('2d', { willReadFrequently: true });
try { MALER[img.parentNode.dataset.motiv](c, w, h, zufall(7 + i * 31)); } catch (e) { /* Platzhalter-Grau bleibt */ }
cv.toBlob(b => { if (b) img.src = URL.createObjectURL(b); fertig(); }, 'image/jpeg', .9);
})));
})();
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const $ = s => document.querySelector(s), $$ = s => [...document.querySelectorAll(s)];
const buehne = $('.buehne'), lader = $('.lader'), main = $('#inhalt');
const marke = $('.marke'), zahl = $('.zahl'), balken = $('.balken'), fuellung = $('.balken i');
const held = $('[data-rolle="held"]'), heldBild = held.querySelector('img');
const karten = $$('[data-rolle="karte"]');
const zeilen = $$('.maske > span'), nach = $$('.nach');
const platz = { marke: $('.p-marke'), zahl: $('.p-zahl'), balken: $('.p-balken'), fenster: $('.p-fenster') };
const knoten = [marke, zahl, balken, held, ...karten];
const ablagen = $$('.ablage');
// Was sich um die übergebenen Elemente herum aufbaut (beim Stapel reisen die Kacheln selbst)
const nachher = () => variante === 'stapel' ? nach : [...nach.slice(0, -1), ...ablagen, nach[nach.length - 1]];
// Messwerte: expo.inOut; 1000 ms (Flip, Clip) bzw. 1800 ms (Stapel); Zähler 3000 ms in steps(14);
// Panel inset(2.5rem) → inset(100% 0 0 0); Text yPercent 105 → 0, 1000 ms power2.out, Versatz 150 ms
const V = {
flip: { dauer: 1.0, text: 'Bild-Flip' },
clip: { dauer: 1.0, text: 'Clip-Fenster' },
stapel: { dauer: 1.8, text: 'Stapel → Raster' },
};
const LADEN = 3.0, STUFEN = 14, KURVE = 'expo.inOut';
let variante = 'flip', tl = null, folge = null, vorfuehrung = false, bereit = false;
// Heimat jedes Knotens merken: Knoten werden umgehängt, nie geklont
const heimat = new Map(knoten.map(k => [k, [k.parentNode, k.nextSibling]]));
const heim = k => { const [eltern, danach] = heimat.get(k); eltern.insertBefore(k, danach); };
function werteZeigen() {
const d = V[variante].dauer * 1000;
const kern = variante === 'clip' ? 'clip-path → inset(0) · Bild 1,3 → 1' : variante === 'stapel' ? 'Flip · Versatz 80 ms' : 'Flip';
$('[data-werte]').innerHTML = reduziert()
? '<span>Reduziert: <b>kein Loader</b>, Seite steht, Fade <b>250 ms</b></span>'
: `<span><b>${KURVE}</b> · <b>${d} ms</b> · ${kern}</span><span>Panel <b>inset(2.5rem) → inset(100% 0 0 0)</b></span><span>Zähler <b>${LADEN * 1000} ms steps(${STUFEN})</b></span><span>Text <b>105 % · 1000 ms power2.out · 150 ms</b></span>`;
}
function aufraeumen() {
if (tl) tl.kill(); if (folge) folge.kill();
tl = folge = null;
const alle = [...knoten, heldBild, fuellung, lader, ...zeilen, ...nach, ...ablagen];
gsap.killTweensOf(alle);
knoten.forEach(k => { heim(k); k.classList.remove('reist'); });
gsap.set(alle, { clearProps: 'all' });
main.removeAttribute('aria-busy');
}
// Fenster für die Clip-Variante: so groß wie der Loader-Platz, mittig auf dem Hero-Bild
function fenster() {
const f = platz.fenster.getBoundingClientRect(), r = held.getBoundingClientRect();
const w = Math.min(f.width, r.width), h = Math.min(f.height, r.height);
return { x: (r.width - w) / 2, y: (r.height - h) / 2, h };
}
function neustart() {
if (!bereit) return;
delete buehne.dataset.wartet;
aufraeumen();
werteZeigen();
buehne.dataset.variante = variante;
if (reduziert()) { // Endposition sofort, nur kurzer Fade
buehne.dataset.zustand = 'fertig';
buehne.style.animation = 'none'; void buehne.offsetWidth; buehne.style.animation = '';
if (vorfuehrung) folge = gsap.delayedCall(4, naechste);
return;
}
scrollTo(0, 0);
main.setAttribute('aria-busy', 'true');
buehne.dataset.zustand = 'laden';
platz.marke.appendChild(marke);
platz.zahl.appendChild(zahl);
platz.balken.appendChild(balken);
if (variante === 'flip') platz.fenster.appendChild(held);
if (variante === 'stapel') [...karten].reverse().concat(held).forEach(k => platz.fenster.appendChild(k));
const stand = { v: 0 };
zahl.textContent = '0';
gsap.set(fuellung, { scaleX: 0 });
gsap.set(zeilen, { yPercent: 105 });
gsap.set(nachher(), { autoAlpha: 0, y: 14 });
tl = gsap.timeline();
tl.to(stand, { v: 100, duration: LADEN, ease: `steps(${STUFEN})`, onUpdate() { zahl.textContent = Math.round(stand.v); gsap.set(fuellung, { scaleX: stand.v / 100 }); } }, 0);
if (variante === 'flip') {
tl.fromTo(held, { clipPath: 'inset(100% 0% 0% 0%)' }, { clipPath: 'inset(0% 0% 0% 0%)', duration: .9, ease: KURVE }, .15)
.fromTo(heldBild, { scale: 1.18 }, { scale: 1, duration: LADEN, ease: 'none' }, .15);
} else if (variante === 'clip') {
const { y, x, h } = fenster();
gsap.set(heldBild, { scale: 1.3 });
tl.fromTo(held, { clipPath: `inset(${y + h}px ${x}px ${y}px ${x}px)` }, { clipPath: `inset(${y}px ${x}px ${y}px ${x}px)`, duration: .9, ease: KURVE }, .15);
} else {
const reihe = [...karten].reverse().concat(held);
gsap.set(reihe, { rotation: i => [-5, 3.5, -2, 0][i] });
reihe.forEach((k, i) => tl.fromTo(k, { clipPath: 'inset(100% 0% 0% 0%)' }, { clipPath: 'inset(0% 0% 0% 0%)', duration: .7, ease: KURVE }, .1 + i * (LADEN / 4)));
}
tl.call(uebergabe, null, LADEN + .25);
}
function uebergabe() {
const d = V[variante].dauer;
const text = [marke, zahl, balken];
const bilder = variante === 'flip' ? [held] : variante === 'stapel' ? [held, ...karten] : [];
const stand = Flip.getState([...text, ...bilder], { props: 'color' });
[...text, ...bilder].forEach(k => { heim(k); k.classList.add('reist'); });
if (variante === 'clip') held.classList.add('reist');
buehne.dataset.zustand = 'uebergabe';
if (variante === 'stapel') gsap.set(bilder, { clipPath: 'none', rotation: 0 });
const t = gsap.timeline({ onComplete: fertig });
t.add(Flip.from(stand, { targets: text, scale: true, duration: d, ease: KURVE }), 0);
if (bilder.length) t.add(Flip.from(stand, { targets: bilder, duration: d, ease: KURVE, stagger: variante === 'stapel' ? .08 : 0 }), 0);
if (variante === 'clip') t.to(held, { clipPath: 'inset(0px 0px 0px 0px)', duration: d, ease: KURVE }, 0)
.to(heldBild, { scale: 1, duration: d, ease: KURVE }, 0);
t.fromTo(lader, { clipPath: `inset(${getComputedStyle(lader).getPropertyValue('--rand')})` }, { clipPath: 'inset(100% 0rem 0rem 0rem)', duration: d, ease: KURVE }, 0);
t.to(zeilen, { yPercent: 0, duration: 1, ease: 'power2.out', stagger: .15 }, d * .55);
t.to(nachher(), { autoAlpha: 1, y: 0, duration: 1, ease: 'power2.out', stagger: .05 }, d * .7);
tl.add(t, tl.time());
}
function fertig() {
buehne.dataset.zustand = 'fertig';
main.removeAttribute('aria-busy');
knoten.forEach(k => k.classList.remove('reist'));
gsap.set([held, heldBild, lader, ...karten], { clearProps: 'clipPath,transform,scale' });
if (vorfuehrung) folge = gsap.delayedCall(2.4, naechste);
}
function setzen(v) {
variante = v;
$$('[data-v]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.v === v)));
}
function naechste() {
const reihe = Object.keys(V);
setzen(reihe[(reihe.indexOf(variante) + 1) % reihe.length]);
neustart();
}
$$('[data-v]').forEach(b => b.addEventListener('click', () => { vorfuehrung = false; setzen(b.dataset.v); neustart(); }));
$('[data-nochmal]').addEventListener('click', () => { vorfuehrung = false; neustart(); });
function vorfuehren() { vorfuehrung = true; gsap.globalTimeline.resume(); neustart(); }
function pause() { gsap.globalTimeline.pause(); }
function weiter() { gsap.globalTimeline.resume(); }
// Start erst, wenn Schriften und Bilder da sind: genau dafür gibt es den Loader
if (!window.gsap || !window.Flip) { buehne.dataset.zustand = 'fertig'; delete buehne.dataset.wartet; MOTIVE(); return; }
document.fonts.ready
.then(MOTIVE)
.then(() => Promise.all($$('.kachel img').map(i => i.decode().catch(() => {}))))
.then(() => {
gsap.registerPlugin(Flip);
bereit = true;
neustart();
});
werteZeigen();
})();
</script>
</body>
</html>