Bewegung als CSS-Funktion
Die Laden-Partitur einer Boulderhalle als reines CSS: drei gerechnete linear()-Federn als Tokens, H1-Zeilen und Kurskacheln gestaffelt über sibling-index() mit Obergrenze aus sibling-count(), @function als Experiment.
Beim Übersetzen einer Rezept-Partitur in tokens.css: Hero mit gestaffelten H1-Zeilen, kleine Elemente federn, Rest steht.
Die Bewegungs-Partitur eines Rezepts wird reines CSS. Federn und Aufprall stehen als linear()-Kurven in tokens.css, gerechnet einmal aus Steifigkeit und Dämpfung. Die Staffelung kommt aus sibling-index() und sibling-count(), mit einer Obergrenze für die Gesamtdauer. Ein @function-Experiment fasst die Staffelregel in eine eigene Funktion, mit festen Variablen als Rückfall. Kein JavaScript bewegt etwas.
Federn wirken lebendig, solange sie sparsam und überall gleich sind. Als Token in tokens.css bekommt jede Seite dieselbe Feder, und der Browser rechnet sie auf dem Compositor. Staffelung aus der Position im DOM passt sich von selbst an, wenn der Kunde eine Kachel ergänzt, und die Obergrenze verhindert, dass zwölf Kacheln eine Sekunde warten.
Wann einsetzen
Beim Übersetzen einer Rezept-Partitur in tokens.css, vor allem bei verspielten Rezepten (Freizeit, Fitness, Kurs, D2C), wo kleine Elemente federn dürfen. Für Listen und Raster, die nacheinander erscheinen. Für Häkchen, Zähler, Abzeichen.
Wann nicht
Federn nie auf Flächen, Fotos oder Deckkraft. Keine Staffelung für Inhalte, die man sofort lesen muss (Öffnungszeiten, Preise, freie Plätze). @function und if() nicht als einzige Quelle: ohne Chromium gibt es sie nicht.
Varianten
- Partitur: Laden-Partitur eines Rezepts als CSS, H1-Zeilen gestaffelt, Zähler und Häkchen federn
- Federn: drei linear()-Kurven (weich, federnd, Aufprall) mit Kurvenbild und Wirkung im Vergleich
- Staffel: sibling-index() mit Obergrenze per sibling-count(), linear, von der Mitte, gedeckelt
Umsetzung
- Federn einmal offline rechnen (gedämpfte Schwingung x(t) = 1 − e^(−ζωt)·(cos(ω_d t) + ζω/ω_d·sin(ω_d t)), 40 bis 60 Stützpunkte) und als
--ease-feder:linear(…)in tokens.css schreiben, mit Dauer, bei der die Schwingung unter 0,1 % ist. Aufprall als zweite Kurve aus Parabelbögen. - Rückfall für alte Browser:
@supportsnot (transition-timing-function:linear(0, 1)) {--ease-feder:cubic-bezier(0.34, 1.56, 0.64, 1)}. - Partitur: Jede Rolle bekommt ihr Token (
--dauer-laden-1,--dauer-laden-1-versatz,--ease-ein…). - Staffeln:
animation-delay: calc(var(--ab)+ (sibling-index() - 1) *var(--versatz)). Obergrenze: Versatz =min(var(--versatz), var(--budget) / sibling-count()). Von der Mitte: abs(sibling-index() - (sibling-count() + 1) / 2). - Rückfall ohne sibling-index():
@supportsnot (z-index: sibling-index()) setzt:nth-child(1…12){--i: 0…11 } aus einer Liste. - Experiment:
@function--staffel(--i<number>,--n<number>) returns <time> { result:min(var(--versatz), var(--budget) / var(--n))*var(--i); } nur hinter@supportsat-rule(@function) bzw. als zusätzliche Deklaration nach der festen. - Reduziert: Tokens im
@media(prefers-reduced-motion) auf Überblenden 150 ms und Versatz 0 umstellen.
Robust umsetzen
Telefon. Gleich. Staffelung mit Obergrenze ist auf dem Telefon wichtiger, weil dort mehr Kacheln untereinander stehen und sonst die letzten zu spät kommen.
Ohne Bewegung. Tokens schalten um: Federn werden zur Überblendung 150 ms ohne Überschwingen, Versatz 0. Die Kurvenbilder bleiben als Abbildung stehen.
Barrierefrei. Nur Bewegung, keine Inhalte hängen davon ab: Alles ist ohne Animation sofort im DOM und lesbar. Keine Bewegung länger als 1 s. Das Kurvenbild hat eine Textbeschreibung (Spitze, Dauer).
Code
<!DOCTYPE html>
<!--
Bewegung als CSS-Funktion
Fundus · Animationsstile · https://cecodesign.de/lab/fundus/animationsstile/bewegung-als-css-funktion/
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>Bewegung als CSS-Funktion · 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: 'Anybody'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/anybody:vf@latest/latin-wdth-normal.woff2') format('woff'); font-weight: 100 900; font-stretch: 50% 150%; font-display: swap; }
@font-face { font-family: 'TikTok Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/tiktok-sans:vf@latest/latin-wdth-normal.woff2') format('woff2-variations'); font-weight: 300 900; font-stretch: 75% 150%; 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; }
/* ============================================================
tokens.css-Ausschnitt: Partitur des Rezepts freizeit-erlebnis-ticket (Stand 04.11.2026) als reines CSS
============================================================ */
:root {
--grund: #F4F3EF; --text: #17161A; --matte: #E6E3DA; --dunkel: #19181D; --signal: #AE1650; --leise: #5B595F; --linie: #D3D0C7;
--signal-hell: #F5B8CE; /* Magenta hell, nur auf Dunkel (8,9 : 1) */
/* Kurven: einmal offline aus Feder-Parametern gerechnet, hier nur noch Token */
--ease-weich: linear(0, 0.005 1.5%, 0.02 3%, 0.046 4.8%, 0.085 6.8%, 0.169 10.2%, 0.376 18%, 0.465 21.5%, 0.553 25.2%, 0.63 29%, 0.693 32.5%, 0.748 36%, 0.797 39.8%, 0.838 43.5%, 0.88 48.2%, 0.913 53.2%, 0.94 58.8%, 0.961 64.8%, 0.977 71.5%, 0.987 79.2%, 1); /* k 140, c 22,4: ζ 0,95, 600 ms */
--ease-feder: linear(0, 0.009 1.2%, 0.033 2.5%, 0.079 4%, 0.139 5.5%, 0.199 6.8%, 0.277 8.2%, 0.575 13.5%, 0.696 15.8%, 0.817 18.2%, 0.911 20.5%, 0.988 22.8%, 1.049 25%, 1.093 27.3%, 1.123 29.5%, 1.138 31.2%, 1.145 33.2%, 1.146 35.2%, 1.14 37.2%, 1.118 41%, 1.049 49.5%, 1.019 53.8%, 0.996 58.5%, 0.983 63.2%, 0.979 71.2%, 0.998 89%, 1); /* k 180, c 14: ζ 0,52, Spitze 1,146, 800 ms */
--ease-aufprall: linear(0, 0.004 2.7%, 0.016 5.3%, 0.035 8%, 0.062 10.7%, 0.097 13.3%, 0.14 16%, 0.191 18.7%, 0.249 21.3%, 0.316 24%, 0.39 26.7%, 0.472 29.3%, 0.561 32%, 0.659 34.7%, 0.764 37.3%, 0.877 40%, 1 42.7%, 0.952 45.3%, 0.916 47.7%, 0.89 49.7%, 0.866 51.8%, 0.848 54%, 0.835 56.2%, 0.827 58.3%, 0.824 60.7%, 0.826 62.8%, 0.834 65%, 0.847 67.2%, 0.866 69.5%, 0.89 71.7%, 0.919 73.8%, 0.95 75.8%, 1 78.7%, 0.977 82.3%, 0.971 84.2%, 0.969 86.2%, 0.976 89.8%, 0.986 91.7%, 1 93.7%, 0.995 96.8%, 1); /* Fall + 3 Sprünge, Rückprall 0,42, 900 ms */
--dauer-weich: 600ms; --dauer-feder: 800ms; --dauer-aufprall: 900ms;
--ui-ease-ein: cubic-bezier(0.16, 1, 0.3, 1);
--ui-ease-aus: cubic-bezier(0.7, 0, 0.84, 0);
/* Partitur-Rollen (Rezept): Laden 1 = H1-Zeilen aus Masken ab 0/40/80 ms, je 450 ms */
--dauer-laden-1-ab: 0ms;
--dauer-laden-1: 450ms;
--dauer-laden-1-versatz: 40ms;
--dauer-zaehler: 400ms; /* Zähler, Häkchen: federn */
--dauer-klein: 150ms; /* Hover, Farbe */
/* Staffel mit Obergrenze: Versatz 50 ms, aber alle zusammen höchstens 300 ms */
--staffel-versatz: 50ms;
--staffel-budget: 300ms;
--staffel-dauer: 500ms;
}
/* Alte Browser ohne linear(): Überschwinger als cubic-bezier */
@supports not (transition-timing-function: linear(0, 1)) {
:root { --ease-feder: cubic-bezier(0.34, 1.56, 0.64, 1); --ease-aufprall: cubic-bezier(0.34, 1.56, 0.64, 1); --ease-weich: cubic-bezier(0.16, 1, 0.3, 1); }
}
/* Reduziert: dieselben Token, andere Werte. Kein Überschwingen, nur Überblenden 150 ms, kein Versatz */
@media (prefers-reduced-motion: reduce) {
:root { --ease-feder: linear; --ease-aufprall: linear; --ease-weich: linear; --dauer-laden-1: 150ms; --dauer-laden-1-versatz: 0ms; --dauer-zaehler: 150ms; --staffel-versatz: 0ms; --dauer-feder: 150ms; --dauer-aufprall: 150ms; --dauer-weich: 150ms; }
}
:root.reduziert { --ease-feder: linear; --ease-aufprall: linear; --ease-weich: linear; --dauer-laden-1: 150ms; --dauer-laden-1-versatz: 0ms; --dauer-zaehler: 150ms; --staffel-versatz: 0ms; --dauer-feder: 150ms; --dauer-aufprall: 150ms; --dauer-weich: 150ms; }
/* Experiment: Staffelregel als eigene Funktion (nur Chromium ab 139). Andere Browser verwerfen die zweite Deklaration. */
@function --staffel(--i <number>, --n <number>) returns <time> {
result: calc(min(var(--staffel-versatz), var(--staffel-budget) / var(--n)) * var(--i));
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--text); font: 400 1.0625rem/1.55 'TikTok Sans', system-ui, sans-serif; font-stretch: 100%; }
body { min-height: 100svh; }
:focus-visible { outline: 2px solid var(--text); outline-offset: 3px; }
button { font: inherit; color: inherit; }
.mono { font: 500 11px/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }
code, .code { font: 400 12px/1.5 'Geist Mono', ui-monospace, monospace; }
.unsichtbar { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.knopf { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 44px; padding: 0 18px; border-radius: 8px; border: 0; background: var(--signal); color: #fff; font-weight: 700; cursor: pointer; transition: background-color var(--dauer-klein) linear; }
.knopf:hover { background: #8E1141; }
.knopf.leer { background: transparent; color: var(--text); box-shadow: inset 0 0 0 1.5px var(--text); }
.knopf.leer:hover { background: var(--matte); }
.knopf svg { width: 16px; height: 16px; }
/* ============ Variante partitur ============ */
.hero { background: var(--dunkel); color: #fff; padding: 18px clamp(18px, 4vw, 56px) clamp(28px, 4vw, 48px); display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); gap: clamp(20px, 4vw, 56px); align-items: end; }
.hero .leiste { grid-column: 1 / -1; display: flex; align-items: center; justify-content: space-between; gap: 16px; padding-bottom: clamp(20px, 4vw, 44px); border-bottom: 1px solid rgba(255, 255, 255, .14); }
.hero .wortmarke { font: 850 1.25rem/1 'Anybody', sans-serif; font-stretch: 70%; letter-spacing: .02em; text-transform: uppercase; }
.hero .wortmarke span { color: var(--signal-hell); }
.hero .leiste p { font-size: .9rem; color: rgba(255, 255, 255, .78); }
.h1-maske { font: 850 clamp(2.75rem, 1.2rem + 6.4vw, 6.5rem)/0.92 'Anybody', sans-serif; font-stretch: 70%; letter-spacing: -.02em; text-transform: uppercase; }
.h1-maske .z { display: block; overflow: clip; } /* Maske ist die Zeile selbst, ohne feste Höhe: bricht sie um (Zoom, Textabstand), bleibt alles sichtbar */
.h1-maske .z > span { display: block; }
.h1-maske .z:nth-child(2) > span { color: var(--signal-hell); }
/* Der Kern: Versatz aus der Position, nicht aus JS.
--zi ist registriert, damit sibling-index() an der Zeile (.z) ausgewertet wird und nicht am inneren span. */
@property --zi { syntax: '<number>'; inherits: true; initial-value: 0; }
@supports (z-index: sibling-index()) { .h1-maske .z { --zi: calc(sibling-index() - 1); } }
@supports not (z-index: sibling-index()) { .h1-maske .z:nth-child(2) { --zi: 1; } .h1-maske .z:nth-child(3) { --zi: 2; } }
.spielt .h1-maske .z > span { animation: aus-maske var(--dauer-laden-1) var(--ui-ease-ein) both; animation-delay: calc(var(--dauer-laden-1-ab) + var(--zi) * var(--dauer-laden-1-versatz)); }
@keyframes aus-maske { from { transform: translateY(100%); } }
.reduziert .spielt .h1-maske .z > span, .spielt.ruhig .h1-maske .z > span { animation-name: nur-ein; }
@keyframes nur-ein { from { opacity: 0; } }
.buchung { background: #24232A; border-radius: 20px; padding: 18px; display: grid; gap: 14px; }
.buchung h2 { font: 800 1.2rem/1.1 'Anybody', sans-serif; font-stretch: 80%; text-transform: uppercase; letter-spacing: .01em; }
.slots { display: flex; flex-wrap: wrap; gap: 8px; }
.slots label { position: relative; }
.slots input { position: absolute; opacity: 0; inset: 0; cursor: pointer; }
.slots span { display: grid; gap: 2px; min-width: 72px; padding: 8px 10px; border-radius: 8px; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .28); font-variant-numeric: tabular-nums; }
.slots span b { font-size: 1.05rem; font-weight: 600; }
.slots span small { font-size: .78rem; color: rgba(255, 255, 255, .78); }
.slots input:checked + span { background: #fff; color: var(--text); box-shadow: none; }
.slots input:checked + span small { color: var(--leise); }
.slots input:focus-visible + span { outline: 2px solid #fff; outline-offset: 2px; }
.slots input:disabled + span { opacity: .5; text-decoration: line-through; }
.zeile { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.zaehler { display: flex; align-items: center; gap: 4px; background: rgba(255, 255, 255, .08); border-radius: 999px; padding: 4px; }
.zaehler button { width: 40px; height: 40px; border-radius: 50%; border: 0; background: rgba(255, 255, 255, .12); color: #fff; cursor: pointer; font-size: 1.2rem; line-height: 1; }
.zaehler button:hover { background: rgba(255, 255, 255, .22); }
.zaehler output { min-width: 3.2ch; text-align: center; font: 700 1.3rem/1 'TikTok Sans', sans-serif; font-variant-numeric: tabular-nums; display: inline-block; }
.zaehler output.pop { animation: pop var(--dauer-zaehler) var(--ease-feder); }
@keyframes pop { from { transform: scale(.6); } }
.haken { display: inline-flex; align-items: center; gap: 10px; cursor: pointer; font-size: .95rem; }
.haken input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.haken .box { width: 24px; height: 24px; border-radius: 6px; box-shadow: inset 0 0 0 1.5px rgba(255, 255, 255, .7); display: grid; place-items: center; }
.haken svg { width: 16px; height: 16px; transform: scale(0); transition: transform 120ms var(--ui-ease-aus); }
.haken input:checked + .box { background: var(--signal-hell); box-shadow: none; }
.haken input:checked + .box svg { transform: scale(1); transition: transform var(--dauer-zaehler) var(--ease-feder); }
.haken input:focus-visible + .box { outline: 2px solid #fff; outline-offset: 2px; }
.buchung .knopf { background: var(--signal-hell); color: var(--text); }
.buchung .knopf:hover { background: #fff; }
.partitur { padding: clamp(24px, 4vw, 48px) clamp(18px, 4vw, 56px) 96px; display: grid; gap: 18px; max-width: 1240px; margin: 0 auto; }
.partitur .kopfzeile { display: flex; align-items: end; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.partitur h2 { font: 850 clamp(1.6rem, 1.2rem + 1.6vw, 2.4rem)/1 'Anybody', sans-serif; font-stretch: 80%; text-transform: uppercase; }
.partitur p.leise { color: var(--leise); max-width: 60ch; }
.spur { position: relative; display: grid; grid-template-columns: minmax(0, 13rem) minmax(0, 1fr); border-top: 1px solid var(--linie); }
.spur:last-child { border-bottom: 1px solid var(--linie); }
.spur .wer { padding: 10px 12px 10px 0; font-size: .92rem; }
.spur .wer small { display: block; color: var(--leise); font: 400 11px/1.4 'Geist Mono', monospace; }
.spur .bahn { position: relative; min-height: 52px; background-image: repeating-linear-gradient(90deg, transparent 0 calc(100% / 6 - 1px), var(--linie) calc(100% / 6 - 1px) calc(100% / 6)); }
.spur .balken { position: absolute; top: 14px; height: 24px; border-radius: 6px; background: var(--text); color: #fff; font: 500 10.5px/24px 'Geist Mono', monospace; padding: 0 6px; white-space: nowrap; overflow: hidden; }
.spur .balken.feder { background: var(--signal); }
.spur .balken.steht { background: transparent; color: var(--leise); box-shadow: inset 0 0 0 1px var(--linie); left: 0; right: 0; }
.lineal { display: grid; grid-template-columns: minmax(0, 13rem) minmax(0, 1fr); font: 400 10.5px/1 'Geist Mono', monospace; color: var(--leise); }
.lineal .skala { display: flex; justify-content: space-between; }
.kopfstrich { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--signal); left: 0; opacity: 0; pointer-events: none; }
.spielt-strich .kopfstrich { opacity: 1; animation: strich 600ms linear both; }
@keyframes strich { from { left: 0; } to { left: 100%; } }
.reduziert .spielt-strich .kopfstrich { animation: none; left: 100%; }
@media (prefers-reduced-motion: reduce) { .spielt-strich .kopfstrich { animation: none; left: 100%; } }
/* ============ Variante federn ============ */
.federn { padding: clamp(28px, 5vw, 64px) clamp(18px, 4vw, 56px) 96px; max-width: 1240px; margin: 0 auto; display: grid; gap: 24px; }
.federn .kopfzeile { display: flex; justify-content: space-between; align-items: end; gap: 16px; flex-wrap: wrap; }
.federn h1, .staffel h1 { font: 850 clamp(2.2rem, 1.2rem + 4vw, 4.6rem)/0.92 'Anybody', sans-serif; font-stretch: 70%; text-transform: uppercase; letter-spacing: -.01em; max-width: 14ch; }
.drei { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }
.kurve { background: #fff; border-radius: 20px; padding: 18px; display: grid; gap: 12px; align-content: start; }
.kurve h2 { font: 800 1.25rem/1 'Anybody', sans-serif; font-stretch: 80%; text-transform: uppercase; display: flex; justify-content: space-between; align-items: baseline; gap: 8px; }
.kurve h2 small { font: 500 11px/1 'Geist Mono', monospace; letter-spacing: .06em; color: var(--leise); text-transform: none; }
.plot { position: relative; aspect-ratio: 16 / 10; background: var(--grund); border-radius: 12px; overflow: hidden; }
.plot svg { position: absolute; inset: 0; width: 100%; height: 100%; }
/* Punkt läuft die Kurve ab: außen Zeit (linear), innen Weg (die Kurve) */
.plot .x { position: absolute; left: 8%; top: 0; bottom: 0; width: 84%; }
.plot .x i { position: absolute; left: 0; bottom: 20%; width: 14px; height: 14px; margin: 0 0 -7px -7px; border-radius: 50%; background: var(--signal); }
.plot .x { --hoehe: 0; }
.spielt .plot .x i { animation: px var(--d) linear both, py var(--d) var(--e) both; }
@keyframes px { to { left: 100%; } }
@keyframes py { to { transform: translateY(calc(-1 * var(--h))); } }
.kurve .wirkung { height: 86px; border-radius: 12px; background: var(--matte); position: relative; overflow: hidden; display: grid; place-items: center; }
.kurve .wirkung .ding { width: 54px; height: 54px; border-radius: 14px; background: var(--text); color: #fff; display: grid; place-items: center; font: 700 1.3rem/1 'TikTok Sans', sans-serif; }
.spielt .w-weich .ding { animation: rein-x var(--dauer-weich) var(--ease-weich) both; }
.spielt .w-feder .ding { animation: rein-s var(--dauer-feder) var(--ease-feder) both; background: var(--signal); }
.spielt .w-aufprall .ding { animation: fall var(--dauer-aufprall) var(--ease-aufprall) both; border-radius: 46% 54% 42% 58% / 52% 44% 56% 48%; background: #E8A33D; color: var(--text); }
.w-aufprall::after { content: ''; position: absolute; left: 20%; right: 20%; bottom: 10px; height: 2px; background: var(--leise); border-radius: 2px; }
.w-aufprall .ding { align-self: end; margin-bottom: 12px; }
@keyframes rein-x { from { transform: translateX(-140px); } }
@keyframes rein-s { from { transform: scale(0); } }
@keyframes fall { from { transform: translateY(-110px); } }
.kurve dl { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 4px 12px; font-size: .88rem; }
.kurve dt { color: var(--leise); }
.kurve dd { font-variant-numeric: tabular-nums; }
.kurve .code { background: var(--grund); border-radius: 10px; padding: 10px 12px; overflow-wrap: anywhere; color: var(--leise); max-height: 4.6em; overflow: hidden; }
.kurve .code b { color: var(--text); font-weight: 500; }
.regel { background: var(--dunkel); color: #fff; border-radius: 20px; padding: 18px 20px; display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 14px; align-items: center; }
.regel b { font: 850 2rem/1 'Anybody', sans-serif; font-stretch: 70%; color: var(--signal-hell); }
/* ============ Variante staffel ============ */
.staffel { padding: clamp(28px, 5vw, 64px) clamp(18px, 4vw, 56px) 96px; max-width: 1240px; margin: 0 auto; display: grid; gap: 22px; }
.staffel .kopfzeile { display: flex; justify-content: space-between; align-items: end; gap: 16px; flex-wrap: wrap; }
.wahl { display: flex; flex-wrap: wrap; gap: 10px 18px; align-items: center; background: #fff; border-radius: 16px; padding: 12px 14px; }
.segment { display: flex; background: var(--matte); border-radius: 10px; padding: 3px; }
.segment button { border: 0; background: transparent; padding: 0 14px; min-height: 40px; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: .92rem; }
.segment button[aria-pressed="true"] { background: var(--text); color: #fff; }
.wahl label { display: flex; align-items: center; gap: 10px; font-size: .92rem; }
.wahl input[type=range] { accent-color: var(--signal); width: 140px; }
.wahl output { font-variant-numeric: tabular-nums; font-weight: 700; min-width: 2ch; }
.befund { font: 400 12px/1.5 'Geist Mono', monospace; color: var(--leise); margin-left: auto; }
.befund b { color: var(--text); font-weight: 500; }
.raster { list-style: none; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(min(150px, 100%), 1fr)); gap: 12px; }
.raster li { background: #fff; border-radius: 14px; padding: 14px; display: grid; gap: 6px; box-shadow: inset 0 0 0 1px var(--linie); }
.raster li b { font: 800 1.35rem/1 'Anybody', sans-serif; font-stretch: 80%; font-variant-numeric: tabular-nums; }
.raster li span { font-size: .9rem; }
.raster li small { color: var(--leise); font-size: .8rem; }
.raster li .frei { justify-self: start; font-size: .78rem; font-weight: 600; background: var(--matte); border-radius: 999px; padding: 2px 8px; }
/* Der Kern: Index und Anzahl aus dem DOM, Obergrenze per min() */
.raster > li { --n: var(--anzahl, 12); }
@supports (z-index: sibling-index()) {
.raster > li { --i: calc(sibling-index() - 1); --n: sibling-count(); --mitte: max(sibling-index() - (sibling-count() + 1) / 2, (sibling-count() + 1) / 2 - sibling-index()); }
}
/* Rückfall ohne sibling-index(): Index per :nth-child, Anzahl per :has(), alles CSS */
@supports not (z-index: sibling-index()) {
.raster > li:nth-child(1) { --i: 0; } .raster > li:nth-child(2) { --i: 1; } .raster > li:nth-child(3) { --i: 2; } .raster > li:nth-child(4) { --i: 3; } .raster > li:nth-child(5) { --i: 4; } .raster > li:nth-child(6) { --i: 5; } .raster > li:nth-child(7) { --i: 6; } .raster > li:nth-child(8) { --i: 7; } .raster > li:nth-child(9) { --i: 8; } .raster > li:nth-child(10) { --i: 9; } .raster > li:nth-child(11) { --i: 10; } .raster > li:nth-child(12) { --i: 11; } .raster > li:nth-child(13) { --i: 12; } .raster > li:nth-child(14) { --i: 13; } .raster > li:nth-child(15) { --i: 14; } .raster > li:nth-child(16) { --i: 15; } .raster > li:nth-child(17) { --i: 16; } .raster > li:nth-child(18) { --i: 17; } .raster > li:nth-child(19) { --i: 18; } .raster > li:nth-child(20) { --i: 19; } .raster > li:nth-child(21) { --i: 20; } .raster > li:nth-child(22) { --i: 21; } .raster > li:nth-child(23) { --i: 22; } .raster > li:nth-child(24) { --i: 23; }
.raster:has(> li:nth-child(4):last-child) > li { --n: 4; } .raster:has(> li:nth-child(5):last-child) > li { --n: 5; } .raster:has(> li:nth-child(6):last-child) > li { --n: 6; } .raster:has(> li:nth-child(7):last-child) > li { --n: 7; } .raster:has(> li:nth-child(8):last-child) > li { --n: 8; } .raster:has(> li:nth-child(9):last-child) > li { --n: 9; } .raster:has(> li:nth-child(10):last-child) > li { --n: 10; } .raster:has(> li:nth-child(11):last-child) > li { --n: 11; } .raster:has(> li:nth-child(12):last-child) > li { --n: 12; } .raster:has(> li:nth-child(13):last-child) > li { --n: 13; } .raster:has(> li:nth-child(14):last-child) > li { --n: 14; } .raster:has(> li:nth-child(15):last-child) > li { --n: 15; } .raster:has(> li:nth-child(16):last-child) > li { --n: 16; } .raster:has(> li:nth-child(17):last-child) > li { --n: 17; } .raster:has(> li:nth-child(18):last-child) > li { --n: 18; } .raster:has(> li:nth-child(19):last-child) > li { --n: 19; } .raster:has(> li:nth-child(20):last-child) > li { --n: 20; } .raster:has(> li:nth-child(21):last-child) > li { --n: 21; } .raster:has(> li:nth-child(22):last-child) > li { --n: 22; } .raster:has(> li:nth-child(23):last-child) > li { --n: 23; } .raster:has(> li:nth-child(24):last-child) > li { --n: 24; }
.raster > li { --mitte: max(var(--i) - (var(--n) - 1) / 2, (var(--n) - 1) / 2 - var(--i)); }
}
.spielt .raster > li { animation: kachel var(--staffel-dauer) var(--ui-ease-ein) both; }
.spielt .raster[data-art="linear"] > li { animation-delay: calc(var(--i) * var(--staffel-versatz)); }
.spielt .raster[data-art="mitte"] > li { animation-delay: calc(var(--mitte, var(--i)) * var(--staffel-versatz)); }
.spielt .raster[data-art="gedeckelt"] > li {
animation-delay: calc(min(var(--staffel-versatz), var(--staffel-budget) / var(--n)) * var(--i));
animation-delay: --staffel(var(--i), var(--n)); /* Experiment @function, sonst verworfen */
}
@keyframes kachel { from { opacity: 0; transform: translateY(16px); } }
.reduziert .spielt .raster > li { animation-name: nur-ein; animation-duration: 150ms; animation-delay: 0ms !important; }
/* Varianten */
html[data-v="partitur"] .w-federn, html[data-v="partitur"] .w-staffel { display: none; }
html[data-v="federn"] .w-partitur, html[data-v="federn"] .w-staffel { display: none; }
html[data-v="staffel"] .w-partitur, html[data-v="staffel"] .w-federn { display: none; }
/* Pult */
.pult { position: fixed; right: 16px; bottom: 16px; z-index: 20; display: flex; flex-wrap: wrap; align-items: center; gap: 10px; background: rgba(25, 24, 29, .95); color: #fff; padding: 8px 10px 8px 14px; border-radius: 10px; box-shadow: 0 10px 28px rgba(0, 0, 0, .25); max-width: calc(100vw - 32px); }
.pult .mono { font-size: 10px; color: rgba(255, 255, 255, .7); }
.pult .gruppe { display: flex; flex-wrap: wrap; background: rgba(255, 255, 255, .08); border-radius: 6px; padding: 2px; }
.pult button { background: none; border: 0; color: rgba(255, 255, 255, .8); font: 500 10.5px/1 'Geist Mono', monospace; letter-spacing: .06em; text-transform: uppercase; padding: 8px 10px; border-radius: 4px; cursor: pointer; }
.pult button[aria-pressed="true"] { background: #fff; color: var(--text); }
.pult button:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.pult .werte { flex-basis: 100%; font: 400 10.5px/1.55 'Geist Mono', monospace; color: rgba(255, 255, 255, .72); max-width: 48ch; }
.pult .werte b { color: #fff; font-weight: 500; }
.vorschau body { overflow: hidden; }
.vorschau .partitur .leise, .vorschau .federn .regel { display: none; }
@media (max-width: 960px) {
.drei { grid-template-columns: minmax(0, 1fr); }
.kurve { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
.kurve h2, .kurve .code, .kurve dl { grid-column: 1 / -1; }
.kurve .wirkung { height: auto; min-height: 86px; }
}
@media (max-width: 760px) {
.hero { grid-template-columns: minmax(0, 1fr); }
.spur, .lineal { grid-template-columns: minmax(0, 7.5rem) minmax(0, 1fr); }
.spur .wer small { display: none; }
.pult .werte { display: none; }
.befund { margin-left: 0; flex-basis: 100%; }
}
@media (max-width: 560px) {
.kurve { grid-template-columns: minmax(0, 1fr); }
.hero .leiste p { display: none; }
}
@media (max-width: 360px) { .slots span { min-width: 64px; } .wahl input[type=range] { width: 100px; } }
@media (forced-colors: active) {
button, [role=button], [role=tab], [role=switch], input, select, textarea { border: 1px solid ButtonText !important; }
.spur .balken { forced-color-adjust: none; }
}
</style>
</head>
<body>
<!-- ============ partitur ============ -->
<div class="w-partitur" data-buehne="partitur">
<header class="hero">
<div class="leiste"><span class="wortmarke">Griff<span>werk</span></span><p>Boulderhalle Wolfsburg · heute bis 23 Uhr</p></div>
<h1 class="h1-maske" aria-label="Bouldern ab 15 Uhr, spontan"><span class="z" aria-hidden="true"><span>Bouldern</span></span><span class="z" aria-hidden="true"><span>ab 15 Uhr,</span></span><span class="z" aria-hidden="true"><span>spontan.</span></span></h1>
<form class="buchung" data-buchung>
<h2>Freie Zeitfenster heute</h2>
<div class="slots" role="radiogroup" aria-label="Zeitfenster">
<label><input type="radio" name="slot" value="15"><span><b>15:00</b><small>14 frei</small></span></label>
<label><input type="radio" name="slot" value="17" checked><span><b>17:00</b><small>6 frei</small></span></label>
<label><input type="radio" name="slot" value="19"><span><b>19:00</b><small>9 frei</small></span></label>
<label><input type="radio" name="slot" value="21" disabled><span><b>21:00</b><small>voll</small></span></label>
</div>
<div class="zeile">
<div class="zaehler" role="group" aria-label="Personen"><button type="button" aria-label="Eine Person weniger" data-minus>−</button><output aria-live="polite" data-personen>2</output><button type="button" aria-label="Eine Person mehr" data-plus>+</button></div>
<label class="haken"><input type="checkbox" data-schuhe><span class="box" aria-hidden="true"><svg viewBox="0 0 16 16"><path d="M3.5 8.5l3 3 6-7" fill="none" stroke="#17161A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>Schuhe leihen</label>
</div>
<button class="knopf" type="submit">Zeitfenster buchen</button>
<p class="unsichtbar" aria-live="polite" data-buchung-ansage></p>
</form>
</header>
<section class="partitur" aria-labelledby="p-titel">
<div class="kopfzeile">
<div><span class="mono" style="color:var(--signal)">Partitur · Laden</span><h2 id="p-titel">Was sich beim Laden bewegt</h2></div>
<button class="knopf leer" type="button" data-nochmal><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3 8a5 5 0 1 0 1.5-3.5M3 2.5V5h2.5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>Noch einmal laden</button>
</div>
<p class="leise">Die Balken lesen ihre Werte aus denselben Tokens wie die Überschrift oben. Ändert man --dauer-laden-1-versatz in tokens.css, ändern sich beide.</p>
<div class="lineal" aria-hidden="true"><span></span><span class="skala"><span>0</span><span>100</span><span>200</span><span>300</span><span>400</span><span>500</span><span>600 ms</span></span></div>
<div class="spuren" data-spuren role="list"></div>
</section>
</div>
<!-- ============ federn ============ -->
<section class="federn w-federn" aria-labelledby="f-titel" data-buehne="federn">
<div class="kopfzeile">
<div><span class="mono" style="color:var(--signal)">tokens.css · linear()</span><h1 id="f-titel">Drei Federn, ein Token je Rolle</h1></div>
<button class="knopf" type="button" data-abspielen><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3.5v9l7-4.5z" fill="currentColor"/></svg>Abspielen</button>
</div>
<div class="drei">
<article class="kurve" data-kurve="weich" style="--d: var(--dauer-weich); --e: var(--ease-weich)">
<h2>Weich <small>--ease-weich</small></h2>
<div class="plot" role="img" aria-label="Kurve ohne Überschwingen, erreicht das Ziel nach 600 Millisekunden"><svg viewBox="0 0 100 75" preserveAspectRatio="none" aria-hidden="true"></svg><div class="x"><i></i></div></div>
<div class="wirkung w-weich" aria-hidden="true"><span class="ding">→</span></div>
<dl><dt>für</dt><dd>Panels, Leisten, alles Flächige</dd><dt>Feder</dt><dd>k 140 · c 22,4 · ζ 0,95</dd><dt>Spitze</dt><dd>1,00 · 600 ms</dd></dl>
<p class="code" data-code></p>
</article>
<article class="kurve" data-kurve="feder" style="--d: var(--dauer-feder); --e: var(--ease-feder)">
<h2>Feder <small>--ease-feder</small></h2>
<div class="plot" role="img" aria-label="Federkurve, schwingt auf 1,15 über das Ziel und pendelt sich nach 800 Millisekunden ein"><svg viewBox="0 0 100 75" preserveAspectRatio="none" aria-hidden="true"></svg><div class="x"><i></i></div></div>
<div class="wirkung w-feder" aria-hidden="true"><span class="ding">3</span></div>
<dl><dt>für</dt><dd>Zähler, Häkchen, Abzeichen</dd><dt>Feder</dt><dd>k 180 · c 14 · ζ 0,52</dd><dt>Spitze</dt><dd>1,146 · 800 ms</dd></dl>
<p class="code" data-code></p>
</article>
<article class="kurve" data-kurve="aufprall" style="--d: var(--dauer-aufprall); --e: var(--ease-aufprall)">
<h2>Aufprall <small>--ease-aufprall</small></h2>
<div class="plot" role="img" aria-label="Aufprallkurve, landet und springt dreimal kleiner werdend zurück, 900 Millisekunden"><svg viewBox="0 0 100 75" preserveAspectRatio="none" aria-hidden="true"></svg><div class="x"><i></i></div></div>
<div class="wirkung w-aufprall" aria-hidden="true"><span class="ding"></span></div>
<dl><dt>für</dt><dd>ein Griff, ein Sticker, sparsam</dd><dt>Sprünge</dt><dd>3 · Rückprall 0,42</dd><dt>Tiefster</dt><dd>0,82 · 900 ms</dd></dl>
<p class="code" data-code></p>
</article>
</div>
<div class="regel"><b>1</b><p>Budget aus dem Rezept: höchstens eine federnde Bewegung zugleich. Federn nie auf Flächen, Fotos oder Deckkraft.</p></div>
</section>
<!-- ============ staffel ============ -->
<section class="staffel w-staffel" aria-labelledby="s-titel" data-buehne="staffel">
<div class="kopfzeile">
<div><span class="mono" style="color:var(--signal)">sibling-index() · sibling-count()</span><h1 id="s-titel">Kurse heute, ohne eine Zeile JS gestaffelt</h1></div>
<button class="knopf" type="button" data-abspielen-s><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 3.5v9l7-4.5z" fill="currentColor"/></svg>Abspielen</button>
</div>
<div class="wahl">
<div class="segment" role="group" aria-label="Staffelung"><button type="button" data-art="linear" aria-pressed="false">linear</button><button type="button" data-art="mitte" aria-pressed="false">von der Mitte</button><button type="button" data-art="gedeckelt" aria-pressed="true">gedeckelt</button></div>
<label>Kacheln <input type="range" min="4" max="24" value="12" data-anzahl><output data-anzahl-zahl>12</output></label>
<p class="befund" data-befund></p>
</div>
<ul class="raster" data-art="gedeckelt" data-raster aria-label="Kurse heute"></ul>
</section>
<div class="pult">
<span class="mono">Variante</span>
<div class="gruppe" role="group" aria-label="Variante">
<button type="button" data-v="partitur" aria-pressed="true">Partitur</button>
<button type="button" data-v="federn" aria-pressed="false">Federn</button>
<button type="button" data-v="staffel" aria-pressed="false">Staffel</button>
</div>
<p class="werte" data-werte></p>
</div>
<script>
/* JS nur für Bedienung, Abspielen und die Anzeige. Bewegt wird ausschließlich in CSS. */
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const $ = (s, r = document) => r.querySelector(s);
const $$ = (s, r = document) => [...r.querySelectorAll(s)];
const token = n => getComputedStyle(html).getPropertyValue(n).trim();
const ms = n => { const t = token(n); return t.endsWith('ms') ? parseFloat(t) : parseFloat(t) * 1000; };
const KANN_INDEX = CSS.supports('z-index', 'sibling-index()');
const KANN_LINEAR = CSS.supports('transition-timing-function', 'linear(0, 1)');
// @function erkennen: Probe-Element mit einer Funktion, die 7ms liefert
let KANN_FUNKTION = false;
try { const s = document.createElement('style'); s.textContent = '@function --probe() returns <time> { result: 7ms; } #fx-probe { transition-duration: --probe(); }'; document.head.appendChild(s); const p = document.createElement('i'); p.id = 'fx-probe'; document.body.appendChild(p); KANN_FUNKTION = getComputedStyle(p).transitionDuration === '0.007s'; p.remove(); s.remove(); } catch (_) {}
function abspielen(el, klasse = 'spielt') { el.classList.remove(klasse); void el.offsetWidth; el.classList.add(klasse); }
// ================= partitur =================
const buehneP = $('[data-buehne=partitur]'), spuren = $('[data-spuren]');
function zeichnePartitur() {
const ab = ms('--dauer-laden-1-ab'), d = ms('--dauer-laden-1'), vs = ms('--dauer-laden-1-versatz'), z = ms('--dauer-zaehler');
const SKALA = 600, pct = x => (x / SKALA * 100).toFixed(2) + '%';
const zeilen = [
{ wer: 'Kopf und Leiste', t: '', art: 'steht' },
{ wer: 'H1 Zeile 1', t: '--dauer-laden-1-ab', ab, d },
{ wer: 'H1 Zeile 2', t: '+ 1 × --dauer-laden-1-versatz', ab: ab + vs, d },
{ wer: 'H1 Zeile 3', t: '+ 2 × --dauer-laden-1-versatz', ab: ab + 2 * vs, d },
{ wer: 'Freie Zeitfenster', t: 'Rezept: ohne Animation', art: 'steht' },
{ wer: 'Zähler, Häkchen', t: 'bei Eingabe · --ease-feder', ab: 0, d: z, feder: true }
];
spuren.innerHTML = '';
zeilen.forEach(zl => {
const s = document.createElement('div'); s.className = 'spur'; s.setAttribute('role', 'listitem');
const text = zl.art === 'steht' ? 'steht' : `${zl.ab} → ${zl.ab + zl.d} ms`;
s.innerHTML = `<div class="wer">${zl.wer}<small>${zl.t}</small></div><div class="bahn"><span class="balken ${zl.art || ''} ${zl.feder ? 'feder' : ''}"></span><i class="kopfstrich" aria-hidden="true"></i></div>`;
const b = s.querySelector('.balken'); b.textContent = text;
if (zl.art !== 'steht') { b.style.left = pct(zl.ab); b.style.width = `max(${pct(zl.d)}, 4px)`; }
spuren.appendChild(s);
});
}
function ladenAbspielen() { abspielen(buehneP); abspielen(spuren, 'spielt-strich'); }
$('[data-nochmal]').addEventListener('click', ladenAbspielen);
const personen = $('[data-personen]');
function setzePersonen(n) { n = Math.max(1, Math.min(8, n)); if (String(n) === personen.textContent) return; personen.textContent = n; abspielen(personen, 'pop'); }
$('[data-plus]').addEventListener('click', () => setzePersonen(+personen.textContent + 1));
$('[data-minus]').addEventListener('click', () => setzePersonen(+personen.textContent - 1));
$('[data-buchung]').addEventListener('submit', e => { e.preventDefault(); const s = $('input[name=slot]:checked'); $('[data-buchung-ansage]').textContent = `${personen.textContent} Personen um ${s ? s.value : '?'}:00 vorgemerkt. In der echten Seite geht es zur Kasse.`; });
// ================= federn =================
const buehneF = $('[data-buehne=federn]');
function punkteAus(lin) {
// linear(0, 0.5 10%, …, 1) → [[x, y]]
const teile = lin.replace(/^linear\(|\)$/g, '').split(',').map(s => s.trim().split(/\s+/));
const pts = teile.map(([y, x]) => [x ? parseFloat(x) / 100 : null, parseFloat(y)]);
pts[0][0] = 0; pts[pts.length - 1][0] = 1;
for (let i = 1; i < pts.length - 1; i++) if (pts[i][0] === null) pts[i][0] = (pts[i - 1][0] + pts[i + 1][0]) / 2;
return pts;
}
$$('[data-kurve]').forEach(k => {
const name = '--ease-' + k.dataset.kurve, lin = token(name);
const svg = k.querySelector('svg'), plot = k.querySelector('.plot');
// y-Bereich −0,25 … 1,3 auf 75 Einheiten; Ziel-Linie bei 1
const Y = y => (75 - (y + 0.25) / 1.55 * 75).toFixed(2), X = x => (8 + x * 84).toFixed(2);
const code = k.querySelector('[data-code]');
if (!lin.startsWith('linear(')) { svg.innerHTML = ''; code.innerHTML = `<b>${name}:</b> ${lin}`; return; }
const pts = punkteAus(lin);
svg.innerHTML = `<line x1="8" x2="92" y1="${Y(1)}" y2="${Y(1)}" stroke="#D3D0C7" stroke-width=".4" stroke-dasharray="1.5 1.5" vector-effect="non-scaling-stroke"/><line x1="8" x2="92" y1="${Y(0)}" y2="${Y(0)}" stroke="#D3D0C7" stroke-width=".4" vector-effect="non-scaling-stroke"/><polyline fill="none" stroke="#17161A" stroke-width="2" stroke-linejoin="round" vector-effect="non-scaling-stroke" points="${pts.map(([x, y]) => X(x) + ',' + Y(y)).join(' ')}"/>`;
// Punkt: Höhe von 0 bis 1 in Pixeln der Fläche
const setzeH = () => { const h = plot.clientHeight; plot.style.setProperty('--h', ((1) / 1.55 * h).toFixed(1) + 'px'); plot.querySelector('.x i').style.bottom = ((0.25) / 1.55 * 100).toFixed(2) + '%'; };
setzeH(); new ResizeObserver(setzeH).observe(plot);
code.innerHTML = `<b>${name}:</b> ${lin.slice(0, 120)} …`;
});
$('[data-abspielen]').addEventListener('click', () => abspielen(buehneF));
// ================= staffel =================
const buehneS = $('[data-buehne=staffel]'), raster = $('[data-raster]'), anzahl = $('[data-anzahl]');
const KURSE = [['16:00', 'Kinder-Bouldern', '6–10 Jahre'], ['16:30', 'Technik Basis', 'Einsteiger'], ['17:00', 'Offene Halle', 'alle'], ['17:30', 'Bouldern 50+', 'ruhig'], ['18:00', 'Technik Überhang', 'Fortgeschr.'], ['18:00', 'Yoga für Kletternde', 'Matte da'], ['18:30', 'Schnupperkurs', 'mit Schuhen'], ['19:00', 'Frauen-Abend', 'offen'], ['19:00', 'Routenbau zuschauen', 'Wand 3'], ['19:30', 'Kraft und Finger', 'Fortgeschr.'], ['20:00', 'Offene Halle', 'alle'], ['20:30', 'Afterwork', 'Bar offen']];
function baueRaster(n) {
raster.innerHTML = '';
for (let i = 0; i < n; i++) { const k = KURSE[i % KURSE.length], li = document.createElement('li'); li.innerHTML = `<b>${k[0]}</b><span>${k[1]}</span><small>${k[2]}</small>`; raster.appendChild(li); }
raster.style.setProperty('--anzahl', n);
}
function befund() {
const n = +anzahl.value, art = raster.dataset.art, vs = ms('--staffel-versatz'), bud = ms('--staffel-budget');
const letzte = art === 'linear' ? (n - 1) * vs : art === 'mitte' ? Math.abs(1 - (n + 1) / 2) * vs : Math.min(vs, bud / n) * (n - 1);
$('[data-befund]').innerHTML = `letzte Kachel startet nach <b>${Math.round(letzte)} ms</b> · sibling-index(): <b>${KANN_INDEX ? 'ja' : 'nein, :nth-child'}</b> · @function: <b>${KANN_FUNKTION ? 'ja' : 'nein, fest'}</b>`;
}
$$('[data-art]', buehneS).forEach(b => b.matches('button') && b.addEventListener('click', () => { raster.dataset.art = b.dataset.art; $$('.segment button').forEach(x => x.setAttribute('aria-pressed', String(x === b))); befund(); abspielen(buehneS); }));
anzahl.addEventListener('input', () => { $('[data-anzahl-zahl]').textContent = anzahl.value; baueRaster(+anzahl.value); befund(); abspielen(buehneS); });
$('[data-abspielen-s]').addEventListener('click', () => abspielen(buehneS));
baueRaster(12);
// ================= Varianten =================
const WERTE = {
partitur: 'H1: --dauer-laden-1 450 ms · --ui-ease-ein (0.16,1,0.3,1) · Versatz (sibling-index() − 1) × 40 ms · Zähler und Häkchen --ease-feder 400 ms · Zeitfenster stehen',
federn: 'weich 600 ms ζ 0,95 · feder 800 ms ζ 0,52 Spitze 1,146 · aufprall 900 ms 3 Sprünge · Punkt: X linear, Y mit der Kurve',
staffel: 'Versatz 50 ms · Budget 300 ms · min(50ms, 300ms / sibling-count()) × (sibling-index() − 1) · 500 ms (0.16,1,0.3,1)'
};
const IDS = Object.keys(WERTE); let v = IDS[0];
function variante(neu) {
v = IDS.includes(neu) ? neu : IDS[0]; html.dataset.v = v;
$$('button[data-v]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.v === v)));
$('[data-werte]').innerHTML = WERTE[v] + ` · linear(): <b>${KANN_LINEAR ? 'ja' : 'Ersatz'}</b>`;
if (v === 'partitur') { zeichnePartitur(); ladenAbspielen(); }
if (v === 'federn') abspielen(buehneF);
if (v === 'staffel') { befund(); abspielen(buehneS); }
const q = new URLSearchParams(location.search);
if (q.get('v') !== v) { q.set('v', v); history.replaceState(null, '', '?' + q.toString().replace(/=(&|$)/g, '$1')); }
}
$$('button[data-v]').forEach(b => b.addEventListener('click', () => { pause(); variante(b.dataset.v); }));
variante(new URLSearchParams(location.search).get('v'));
// ================= Vorführung =================
let zeit = [], laeuft = false;
const nach = (ms, f) => zeit.push(setTimeout(f, ms));
function runde() {
if (!laeuft) return;
if (v === 'partitur') { ladenAbspielen(); nach(1100, () => setzePersonen(3)); nach(1800, () => { const c = $('[data-schuhe]'); c.checked = !c.checked; }); nach(2600, () => setzePersonen(2)); nach(3600, runde); }
else if (v === 'federn') { abspielen(buehneF); nach(2200, runde); }
else {
const arten = ['gedeckelt', 'linear', 'mitte']; const i = (arten.indexOf(raster.dataset.art) + 1) % arten.length;
const b = $(`.segment [data-art=${arten[i]}]`); b.click(); nach(2200, runde);
}
}
function neustart() { zeit.forEach(clearTimeout); zeit = []; variante(v); }
function vorfuehren() { zeit.forEach(clearTimeout); zeit = []; laeuft = true; html.classList.add('vorfuehrung'); runde(); }
function pause() { laeuft = false; zeit.forEach(clearTimeout); zeit = []; html.classList.remove('vorfuehrung'); }
function weiter() { if (!laeuft) { laeuft = true; html.classList.add('vorfuehrung'); runde(); } }
})();
</script>
</body>
</html>