Intro ohne Wartezeit (LCP-sicheres Eröffnungsballett)
Der Hero steht ab dem ersten Frame, Zeilen steigen aus Masken und die Navigation taucht nach: ein inszenierter Auftritt in 1,2 Sekunden, ganz ohne Ladebildschirm.
Kein blockierender Ladebildschirm: Das HTML liefert die Hero-Inhalte sofort sichtbar aus. Die Eröffnungsanimation läuft per CSS-@keyframes ab dem ersten Rendern, nicht erst nach JavaScript oder window.load. Headline-Zeilen steigen aus Masken, das Hero-Bild öffnet sich auf Wunsch per clip-path, die Navigation taucht nach. Das LCP-Element (Hero-Bild oder H1) wird nie mit opacity: 0 oder display: none versteckt. Es bewegt sich höchstens per Transform oder Clip, oder es steht von Anfang an und nur Nebenelemente animieren. Bilder, die später gebraucht werden, laden während des Intros im Hintergrund.
Die Seite wirkt inszeniert, aber niemand wartet. Der Inhalt ist sofort lesbar, die Bewegung ist Zugabe und kein Vorhang. Weil das LCP-Element sichtbar bleibt, misst der Browser einen frühen Largest Contentful Paint. Laut Shopify verbesserte das Entfernen von Fade-ins am LCP-Element den LCP in einzelnen Fällen um 3 bis 6 Sekunden.
Wann einsetzen
Als Standard für jede Seite, die ein Intro möchte. Besonders bei Seiten, die über Suchmaschinen gefunden werden oder verkaufen sollen: Portfolio, Shop, Agentur, Produktseite.
Wann nicht
Wenn vor der ersten Interaktion wirklich schwere Assets nötig sind, etwa eine 3D-Szene oder ein Video. Dann besser eine Ladeszene mit echtem Fortschritt (webgl-ladeszene-fortschritt).
Varianten
- Reines CSS-Keyframe-Intro, das mit dem ersten Paint startet
- LCP-Element steht sofort, nur Nebenelemente animieren
- Clip-Path-Reveal statt Opacity-Fade für das Hero-Bild, ab dem ersten Frame minimal sichtbar
- Start aus pagereveal: Inhalte nur in Browsern verstecken, die das Event unterstützen
Umsetzung
Zeilen: .maske > span { display: block; animation: steigen .9s cubic-bezier(.22, 1, .36, 1) both; animation-delay: calc(var(--i) * 80ms) } mit @keyframes steigen { from { transform: translateY(110%) } }. Die Masken-Wrapper (overflow: hidden) stehen im HTML statt per JS-Split, so läuft alles ohne JavaScript. Bild-Reveal: @keyframes oeffnen { from { clip-path: inset(0 0 98% 0) } } mit 1,1 s cubic-bezier(.76, 0, .24, 1). Das Bild ist so ab dem ersten Frame minimal sichtbar. Wie Chrome Clip-Animationen für den LCP bewertet, ist nicht dokumentiert. Deshalb mit Lighthouse oder dem Performance-Panel der DevTools gegenprüfen. Das LCP-Bild bekommt fetchpriority="high" und kein loading="lazy". Fallstrick Webfonts: font-display: swap mit einer Ersatzschrift, deren Metrik per size-adjust und ascent-override angeglichen ist, sonst springt die Headline.
Robust umsetzen
Telefon. Gleicher Ablauf, nur kürzer: Gesamtdauer etwa 800 ms.
Ohne Bewegung. Die Animation entfällt, alles steht sofort. Guter Diagnosetest nach Shopify: LCP mit und ohne reduzierte Bewegung vergleichen. Ist der Wert ohne Animation deutlich besser, bremst das Intro.
Barrierefrei. Alle Inhalte stehen sofort im DOM und sind lesbar. Kein Overlay, kein Fokus-Trap. Die Masken-Wrapper ändern die Lesereihenfolge nicht.
Code
<!DOCTYPE html>
<!--
Intro ohne Wartezeit (LCP-sicheres Eröffnungsballett)
Fundus · Preloader & Intro · https://cecodesign.de/lab/fundus/preloader-intro/intro-ohne-wartezeit/
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>Intro ohne Wartezeit · 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 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: '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: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
/* Ersatzschrift mit angeglichener Metrik: kein Sprung, wenn Instrument Sans per swap nachkommt
(gemessen gegen Arial/Liberation Sans, Schnitt 400) */
@font-face { font-family: 'Instrument Sans Ersatz'; src: local('Arial'), local('Liberation Sans'), local('Helvetica'); size-adjust: 102.6%; ascent-override: 94.5%; descent-override: 24.4%; line-gap-override: 0%; }
:root {
--papier: #ebe7df; --tinte: #161513; --leise: rgba(22, 21, 19, .66); --linie: rgba(22, 21, 19, .14);
--gruen: #2e6b57; --rot: #b8492a;
--text-kurve: cubic-bezier(0.22, 1, 0.36, 1); /* power4.out, Messwert Text */
--clip-kurve: cubic-bezier(0.76, 0, 0.24, 1); /* Messwert Clip */
--tempo: 1; /* Telefon: 0,65 → Gesamt etwa 800 ms */
--start: 0ms; /* nur die Gegenprobe verschiebt den Start */
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--papier); color: var(--tinte); font-family: 'Instrument Sans', 'Instrument Sans Ersatz', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; }
a { color: inherit; text-decoration: none; }
.mono { font: 500 11px/1.4 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
.serif { font-family: 'Instrument Serif', Georgia, serif; font-style: italic; font-weight: 400; letter-spacing: -.02em; }
.seite { min-height: 100svh; display: grid; grid-template-rows: auto 1fr auto; }
/* Navigation */
.nav { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 22px clamp(20px, 3.4vw, 48px); border-bottom: 1px solid var(--linie); }
.marke { font-weight: 600; font-size: 17px; letter-spacing: -.02em; display: flex; align-items: center; gap: 10px; }
.marke i { width: 10px; height: 10px; border-radius: 50%; background: var(--tinte); }
.nav ul { display: flex; gap: clamp(18px, 2.6vw, 40px); list-style: none; padding: 0; font-size: 15px; }
.nav ul a { position: relative; }
.nav ul a[aria-current] { color: var(--tinte); }
.nav ul a[aria-current]::after { content: ''; position: absolute; left: 0; right: 0; bottom: -4px; height: 1px; background: currentColor; }
.anfrage { font-size: 14px; border: 1px solid var(--tinte); border-radius: 999px; padding: 9px 16px; transition: background 150ms, color 150ms; }
.anfrage:hover { background: var(--tinte); color: var(--papier); }
.menue { display: none; }
/* Hero */
.hero { display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); gap: clamp(28px, 4vw, 72px); align-items: end; padding: clamp(28px, 5vh, 64px) clamp(20px, 3.4vw, 48px) clamp(24px, 4vh, 48px); }
.hero__text { display: grid; gap: clamp(18px, 2.6vh, 30px); align-self: stretch; align-content: end; }
.titel { font-size: clamp(54px, 8.4vw, 136px); font-weight: 560; letter-spacing: -.048em; line-height: .92; }
.titel .serif { font-size: 1.08em; letter-spacing: -.035em; }
.maske { display: block; overflow: hidden; padding-bottom: .06em; margin-bottom: -.06em; }
.maske > span { display: block; }
.lead { font-size: clamp(16px, 1.3vw, 19px); line-height: 1.5; max-width: 38ch; color: rgba(22, 21, 19, .78); }
.aktion { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.cta { display: inline-flex; align-items: center; gap: 12px; font-size: 15px; font-weight: 500; background: var(--tinte); color: var(--papier); border-radius: 999px; padding: 14px 22px; }
.cta span { transition: transform 150ms; }
.cta:hover span { transform: translateX(3px); }
.hero__bild { display: grid; gap: 12px; justify-self: end; width: 100%; max-width: 520px; }
.rahmen { width: 100%; height: clamp(320px, calc(100svh - 400px), 640px); overflow: hidden; background: #d9d4ca; }
.rahmen img { display: block; width: 100%; height: 100%; object-fit: cover; }
.unterzeile { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.unterzeile b { color: var(--tinte); font-weight: 500; }
.naechstes { display: flex; align-items: center; gap: 12px; padding-top: 12px; border-top: 1px solid var(--linie); font-size: 14px; }
.naechstes img { width: 56px; height: 38px; object-fit: cover; background: #d9d4ca; }
.naechstes span:last-child { margin-left: auto; }
/* ── Das Eröffnungsballett: reines CSS, startet mit dem ersten Paint ── */
@keyframes steigen { from { transform: translateY(110%); } }
@keyframes auftauchen { from { opacity: 0; transform: translateY(12px); } }
@keyframes oeffnen { from { clip-path: inset(0 0 98% 0); } to { clip-path: inset(0 0 0 0); } }
@keyframes entzoomen { from { transform: scale(1.08); } }
@keyframes einblenden { from { opacity: 0; } }
/* Zeilen steigen aus Masken: 900 ms, 80 ms Versatz, translateY 110 % */
.maske > span { animation: steigen calc(900ms * var(--tempo)) var(--text-kurve) both;
animation-delay: calc(var(--start) + var(--i) * 80ms * var(--tempo)); }
/* Nebenelemente tauchen nach: 600 ms, gestaffelt ab 200 ms */
.nach { animation: auftauchen calc(600ms * var(--tempo)) var(--text-kurve) both;
animation-delay: calc(var(--start) + var(--d) * var(--tempo)); }
/* Variante „LCP steht“: das Hero-Bild hat KEINE Animation */
/* Variante „Clip-Reveal“: Bild öffnet per clip-path, ab dem ersten Frame 2 % sichtbar */
[data-variante='clip'] .rahmen { animation: oeffnen calc(1100ms * var(--tempo)) var(--clip-kurve) both; }
[data-variante='clip'] .rahmen img { animation: entzoomen calc(1100ms * var(--tempo)) var(--text-kurve) both; }
/* Gegenprobe (so nicht): Ladebildschirm, Inhalt bis dahin unsichtbar */
[data-variante='gegenprobe'] { --start: 2100ms; }
[data-variante='gegenprobe'] .rahmen { animation: einblenden 300ms linear var(--start) both; }
.lader { display: none; position: fixed; inset: 0; z-index: 5; background: var(--tinte); color: var(--papier); padding: clamp(20px, 3.4vw, 48px); grid-template-rows: auto 1fr auto; }
[data-variante='gegenprobe'] .lader { display: grid; animation: lader-weg 700ms var(--clip-kurve) 1800ms both; }
@keyframes lader-weg { from { clip-path: inset(0 0 0 0); } to { clip-path: inset(0 0 100% 0); } }
.lader .mono { color: rgba(235, 231, 223, .55); }
.lader__zahl { align-self: end; font-size: clamp(96px, 18vw, 260px); font-weight: 500; letter-spacing: -.06em; line-height: .8; font-variant-numeric: tabular-nums; }
@property --n { syntax: '<integer>'; inherits: false; initial-value: 0; }
@keyframes zaehlen { to { --n: 100; } }
.lader__zahl { animation: zaehlen 1800ms cubic-bezier(0.65, 0, 0.35, 1) both; counter-reset: n var(--n); }
.lader__zahl::before { content: counter(n); }
.lader__fuss { display: flex; justify-content: space-between; gap: 12px; }
/* Neustart: alle Animationen kurz aus, dann wieder an */
.halt *, .halt .lader { animation: none !important; }
/* ── Pult: Varianten, Zeitachse, Werte ── */
.pult { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 14px 32px; align-items: center; padding: 16px clamp(20px, 3.4vw, 48px) 18px; border-top: 1px solid var(--linie); background: rgba(22, 21, 19, .03); }
.modi { display: flex; gap: 4px; flex-wrap: wrap; }
.modi button { font: inherit; letter-spacing: inherit; text-transform: inherit; color: var(--leise); background: none; border: 1px solid var(--linie); border-radius: 999px; height: 30px; padding: 0 12px; 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 button[data-v='gegenprobe'] { border-style: dashed; }
.modi button[data-v='gegenprobe'][aria-pressed='true'] { background: var(--rot); border-color: var(--rot); }
.modi .nochmal { border-color: transparent; }
.achse { position: relative; height: 34px; }
.achse__spur { position: absolute; left: 0; right: 0; top: 14px; height: 6px; background: var(--linie); border-radius: 3px; overflow: hidden; }
.achse__balken { position: absolute; top: 0; bottom: 0; background: var(--tinte); }
.achse__balken.lade { background: repeating-linear-gradient(-45deg, var(--rot) 0 4px, rgba(184, 73, 42, .45) 4px 8px); }
.achse__marke { position: absolute; top: 0; bottom: 0; width: 0; }
.achse__marke::before { content: ''; position: absolute; top: 9px; left: -1px; width: 2px; height: 16px; background: var(--gruen); }
.achse__marke span { position: absolute; bottom: -2px; left: 6px; white-space: nowrap; font: 600 10px/1 'Geist Mono', monospace; letter-spacing: .1em; color: var(--gruen); }
.achse__marke.spaet::before { background: var(--rot); }
.achse__marke.spaet span { color: var(--rot); }
.achse__kopf { position: absolute; top: 6px; width: 1px; height: 22px; background: var(--tinte); opacity: .5; left: 0; }
.achse__skala { position: absolute; left: 0; right: 0; top: 0; display: flex; justify-content: space-between; font: 500 9px/1 'Geist Mono', monospace; letter-spacing: .08em; color: var(--leise); }
.werte { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 4px 18px; font-size: 10px; }
.werte b { color: var(--tinte); font-weight: 500; }
/* Reduzierte Bewegung: alles steht ab dem ersten Frame */
.reduziert *, .reduziert .lader { animation: none !important; transition: none !important; }
.reduziert .lader { display: none !important; }
.reduziert .achse__kopf { display: none; }
/* Vorschau: nur Bühne und Pult */
.vorschau body { overflow: hidden; }
.vorschau .naechstes, .vorschau .werte, .vorschau .anfrage { display: none; }
.vorschau .rahmen { height: calc(100svh - 280px); }
@media (max-width: 1100px) {
.nav ul { display: none; }
.menue { display: inline; }
}
@media (max-width: 760px) {
:root { --tempo: .65; }
.hero { grid-template-columns: 1fr; align-items: start; padding-top: 32px; }
.titel { font-size: 16vw; }
.hero__bild { justify-self: stretch; max-width: none; }
.rahmen { aspect-ratio: 3 / 2; height: auto; }
.anfrage { display: none; }
.pult { grid-template-columns: 1fr; }
.naechstes { display: none; }
}
@media (hover: none) {
/* Kein Hover nötig: das Intro läuft beim Laden. Tippen auf „Nochmal“ spielt es erneut. */
.modi button { height: 36px; }
}
@media (forced-colors: active) {
button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; }
}
</style>
</head>
<body data-variante="steht">
<div class="seite">
<header class="nav">
<a class="marke nach" style="--d: 200ms" href="#"><i aria-hidden="true"></i>Studio Feldhaus</a>
<nav aria-label="Hauptnavigation" class="nach" style="--d: 260ms">
<ul><li><a href="#" aria-current="page">Arbeiten</a></li><li><a href="#">Studio</a></li><li><a href="#">Journal</a></li><li><a href="#">Kontakt</a></li></ul>
</nav>
<a class="anfrage nach" style="--d: 320ms" href="#">Projekt anfragen</a>
<span class="menue mono nach" style="--d: 320ms">Menü</span>
</header>
<main class="hero">
<div class="hero__text">
<p class="mono maske"><span style="--i: 0">Büro für Marken, Bücher und Räume · Freiburg</span></p>
<h1 class="titel">
<span class="maske"><span style="--i: 1">Gestaltung,</span></span>
<span class="maske"><span style="--i: 2">die sofort</span></span>
<span class="maske"><span style="--i: 3" class="serif">da ist.</span></span>
</h1>
<p class="lead nach" style="--d: 360ms">Wir entwickeln Erscheinungsbilder, Magazine und Leitsysteme für Häuser, die etwas zu sagen haben. Seit 2011, aus Freiburg für den Südwesten.</p>
<div class="aktion nach" style="--d: 440ms">
<a class="cta" href="#">Ausgewählte Arbeiten <span aria-hidden="true">→</span></a>
<span class="mono">48 Projekte · 12 Branchen</span>
</div>
</div>
<figure class="hero__bild">
<div class="rahmen">
<picture>
<source media="(max-width: 760px)" srcset="bilder/magazin-doppelseite.jpg" width="1200" height="800">
<img src="bilder/magazin-doppelseite-hoch.jpg" width="800" height="1000" alt="Werkstattheft No. 04, aufgeschlagenes Magazin mit der Reportage „Die Ruhe der Dinge“" fetchpriority="high" decoding="async">
</picture>
</div>
<figcaption class="mono unterzeile nach" style="--d: 520ms"><span><b>Werkstattheft No. 04</b> · Editorial</span><span>2025</span></figcaption>
<a class="naechstes nach" style="--d: 600ms" href="#"><img data-spaeter="bilder/marke-krume.jpg" alt="" width="56" height="38"><span><span class="mono">Als Nächstes</span><br>Krume, Backstube</span><span aria-hidden="true">→</span></a>
</figure>
</main>
<section class="pult" aria-label="Demo-Steuerung">
<div class="modi mono" role="group" aria-label="Variante">
<button type="button" data-v="steht" aria-pressed="true">LCP steht</button>
<button type="button" data-v="clip" aria-pressed="false">Clip-Reveal</button>
<button type="button" data-v="gegenprobe" aria-pressed="false" title="So nicht: klassischer Ladebildschirm zum Vergleich">Gegenprobe: Preloader</button>
<button type="button" class="nochmal" data-nochmal aria-label="Intro erneut abspielen">↻ Nochmal</button>
</div>
<div class="achse" data-achse role="img" aria-label="Zeitachse von 0 bis 3,5 Sekunden mit Intro-Dauer und Zeitpunkt des Largest Contentful Paint">
<div class="achse__skala" aria-hidden="true"><span>0 s</span><span>0,5</span><span>1</span><span>1,5</span><span>2</span><span>2,5</span><span>3</span><span>3,5 s</span></div>
<div class="achse__spur"><div class="achse__balken lade" data-lade></div><div class="achse__balken" data-intro></div></div>
<div class="achse__marke" data-lcp><span>LCP</span></div>
<div class="achse__kopf" data-kopf></div>
</div>
<p class="werte mono" data-werte aria-live="polite"></p>
</section>
</div>
<div class="lader" aria-hidden="true">
<p class="mono">Studio Feldhaus</p>
<p class="lader__zahl"></p>
<div class="lader__fuss mono"><span>Wird geladen</span><span>Bitte warten</span></div>
</div>
<script>
(() => {
const html = document.documentElement, body = document.body;
const reduziert = () => html.classList.contains('reduziert');
const SKALA = 3500; // Zeitachse 0–3,5 s
const knoepfe = [...document.querySelectorAll('[data-v]')];
const el = s => document.querySelector(s);
const achse = { lade: el('[data-lade]'), intro: el('[data-intro]'), lcp: el('[data-lcp]'), kopf: el('[data-kopf]') };
const werte = el('[data-werte]');
let variante = 'steht', lcpGemessen = null;
// Projekt 02 lädt im Hintergrund, während das Intro läuft (blockiert nichts)
const spaeter = el('[data-spaeter]');
(window.requestIdleCallback || setTimeout)(() => { spaeter.src = spaeter.dataset.spaeter; });
// Echter LCP dieser Seite beim ersten Laden (Chromium)
try {
new PerformanceObserver(l => { const e = l.getEntries().at(-1); if (e) { lcpGemessen = Math.round(e.startTime); zeigeWerte(); } })
.observe({ type: 'largest-contentful-paint', buffered: true });
} catch (_) {}
const tempo = () => parseFloat(getComputedStyle(html).getPropertyValue('--tempo')) || 1;
const ms = n => Math.round(n).toLocaleString('de-DE') + ' ms';
// Zeitplan je Variante (in ms), passend zu den CSS-Werten
function plan(v) {
const t = tempo();
const intro = 1200 * t; // letzte Zeile 240 + 900, letztes Nebenelement 600 + 600
if (v === 'gegenprobe' && !reduziert()) return { lade: [0, 2500], intro: [2100, 2100 + intro], lcp: 2400 };
if (reduziert()) return { lade: null, intro: [0, 0], lcp: 0 };
return { lade: null, intro: [0, intro], lcp: 0 };
}
function zeichneAchse() {
const p = plan(variante), pc = x => (x / SKALA * 100) + '%';
achse.lade.style.display = p.lade ? '' : 'none';
if (p.lade) { achse.lade.style.left = pc(p.lade[0]); achse.lade.style.width = pc(p.lade[1] - p.lade[0]); }
achse.intro.style.left = pc(p.intro[0]); achse.intro.style.width = pc(p.intro[1] - p.intro[0]);
achse.lcp.style.left = pc(p.lcp);
achse.lcp.classList.toggle('spaet', p.lcp > 0);
achse.lcp.querySelector('span').textContent = p.lcp ? 'LCP ≈ 2,4 s' : 'LCP im ersten Frame';
}
function zeigeWerte() {
const t = tempo(), r = reduziert();
const gem = lcpGemessen != null ? `<span>Diese Seite beim Laden: <b>LCP ${ms(lcpGemessen)}</b></span>` : '';
if (r) { werte.innerHTML = `<span><b>Reduzierte Bewegung:</b> keine Animation, alles steht ab dem ersten Frame</span>${gem}`; return; }
const text = `<span>Zeilen <b>translateY(110%) → 0</b> · <b>${ms(900 * t)}</b> · Versatz <b>${ms(80 * t)}</b> · <b>cubic-bezier(0.22, 1, 0.36, 1)</b></span>`;
const neben = `<span>Nebenelemente <b>opacity 0 → 1, 12 px</b> · <b>${ms(600 * t)}</b> ab <b>${ms(200 * t)}</b></span>`;
const teile = {
steht: `<span>Hero-Bild (LCP) <b>ohne Animation</b></span>${text}${neben}<span>Gesamt <b>${ms(1200 * t)}</b></span>`,
clip: `<span>Hero-Bild <b>clip-path inset(0 0 98% 0) → 0</b> · <b>${ms(1100 * t)}</b> · <b>cubic-bezier(0.76, 0, 0.24, 1)</b> · Bild <b>scale 1,08 → 1</b></span>${text}<span>Gesamt <b>${ms(1200 * t)}</b></span>`,
gegenprobe: `<span>So nicht: Ladebildschirm <b>1.800 ms</b> + Wischen <b>700 ms</b></span><span>Inhalt <b>opacity 0</b> bis <b>2.100 ms</b>, Bild blendet <b>300 ms</b> ein</span><span>LCP rutscht auf <b>≈ 2.400 ms</b></span>`
};
werte.innerHTML = teile[variante] + gem;
}
// Abspielkopf der Zeitachse
let kopfUhr = null, t0 = 0;
function kopfLaufen() {
cancelAnimationFrame(kopfUhr);
if (reduziert()) return;
t0 = performance.now();
const ende = plan(variante).intro[1] + 200;
const schritt = jetzt => {
const t = jetzt - t0;
achse.kopf.style.left = (Math.min(t, ende) / SKALA * 100) + '%';
if (t < ende) kopfUhr = requestAnimationFrame(schritt);
};
kopfUhr = requestAnimationFrame(schritt);
}
function setzen(v) {
variante = v;
body.dataset.variante = v;
knoepfe.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.v === v)));
zeichneAchse(); zeigeWerte();
}
// CSS-Animationen neu starten: kurz abschalten, Layout erzwingen, wieder an
function abspielen() {
body.classList.add('halt');
void body.offsetWidth;
body.classList.remove('halt');
kopfLaufen();
}
function neustart() { pause(); abspielen(); }
knoepfe.forEach(b => b.addEventListener('click', () => { pause(); setzen(b.dataset.v); abspielen(); }));
el('[data-nochmal]').addEventListener('click', () => { pause(); abspielen(); });
addEventListener('resize', () => { zeichneAchse(); zeigeWerte(); });
// Vorführung: alle drei Varianten nacheinander, in Schleife
const FOLGE = [['steht', 3400], ['clip', 3400], ['gegenprobe', 4800]];
let schleife = null, schritt = 0;
function vorfuehren() {
pause();
const [v, dauer] = FOLGE[schritt % FOLGE.length];
schritt++;
setzen(v); abspielen();
schleife = setTimeout(vorfuehren, dauer);
}
function pause() { clearTimeout(schleife); schleife = null; }
function weiter() { if (!schleife) vorfuehren(); }
setzen('steht');
kopfLaufen();
})();
</script>
</body>
</html>