Bild-Scheiben in der Tiefe
Kampagnen-Hero eines Ladens für Duft, Leinen und Keramik: drei Scheiben aus dem Produktbild heben sich unscharf ab, werden scharf, zoomen stärker als das Bild und verschieben sich mit Zeiger und Scroll, während das Bild dahinter zurücktritt.
Produkt- oder Kampagnenbild mit einem klaren Objekt: Streifen schneiden durch Flakon, Fassade oder Figur.
Aus einem Hero-Bild werden zwei bis vier Streifen oder Rechtecke herausgeschnitten. Sie zeigen genau den Bildteil, der dahinter liegt, heben sich aber leicht ab: Sie zoomen etwas stärker als das Bild, werfen einen Schatten und versetzen sich bei Zeigerbewegung oder beim Scrollen. So entsteht Tiefe aus einem einzigen Foto.
Das Auge liest den Versatz an den Schnittkanten als Abstand. Die Scheiben folgen dem Raster der Seite oder des Motivs (Lamellen, Geschosse, Fenster) und machen das Raster selbst zum Bild. Es braucht kein 3D-Modell und kein zweites Motiv.
Wann einsetzen
Hero mit einem starken Foto, das klare Linien hat: Architektur, Innenraum, Mode-Kampagne, Boutique-Hotel als ruhige Alternative zum Ken-Burns-Hero, Kreativstudio.
Wann nicht
Werke von Fotograf:innen (verändert die Komposition fremder Bilder). Bilder mit Gesichtern oder Schrift an den Schnittkanten. Produktfotos im Shop. Zusammen mit mehrschicht-parallax oder zoom-parallax auf demselben Bild. Wenn das Motiv keine Linien hat, an denen die Schnitte sitzen können.
Varianten
- Senkrecht: drei bis vier Streifen wie Lamellen oder Fassadenachsen
- Waagerecht: Bänder wie Geschosse oder Stufen
- Rechtecke: zwei bis drei freie Ausschnitte nach dem Seitenraster (Slicey-Grundform)
- Auslöser: Zeiger (Desktop), Scroll (Hero verlässt den Bildschirm), einmal beim Laden
Umsetzung
Bühne mit container-type: size, darin das Bild als <img> (object-fit: cover). Jede Scheibe ist ein Rahmen mit overflow: hidden an Position x, y, b, h in Prozent der Bühne; darin dasselbe Bild in voller Bühnengröße (100cqw × 100cqh), um −x und −y verschoben. Damit zeigt die Scheibe deckungsgleich den Bildteil dahinter. Tiefe je Scheibe (0 bis 1) steuert scale 1 + Tiefe × (0,1 + Scrollhub × 0,07), Versatz Zeiger × Tiefe × 30 px und einen weichen Schatten. Beim Laden heben sich die Scheiben in 1,1 s (110 ms versetzt) und werden dabei von 7 px Unschärfe scharf; das Bild dahinter wird gleichzeitig dunkler und etwas entsättigt (brightness .68, saturate .75), damit die Scheiben im Licht stehen. Zeiger: pointermove setzt --zx, --zy (−1 bis 1), weich nachgezogen (Lerp 0,08). Scroll: @property --hub per animation-timeline: view() von 0 auf 1, solange der Hero aus dem Bild scrollt; ohne Scroll-Timeline setzt ein Skript --hub.
Robust umsetzen
Telefon. Ohne Zeiger nur Scroll und Anheben beim Laden. Weniger Scheiben (die äußere entfällt unter 760 px), Hochformat-Bild.
Ohne Bewegung. Scheiben stehen still in ruhender Tiefe, Schatten bleibt; kein Zeiger- und kein Scrollversatz.
Barrierefrei. Die Scheiben sind Dekoration (aria-hidden, Hintergrundbild ohne Alt), das eigentliche <img> trägt den Alt-Text. Kein Autoplay: Bewegung nur einmal beim Laden (gut 1 s) und als Antwort auf Zeiger und Scroll, deshalb kein Pause-Knopf nötig. Text liegt nie auf den Scheiben.
Code
<!DOCTYPE html>
<!--
Bild-Scheiben in der Tiefe
Fundus · Hero & Section-Layouts · https://cecodesign.de/lab/fundus/hero-layouts/bild-scheiben-tiefe/
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>Bild-Scheiben in der Tiefe · 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: 'Zalando Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/zalando-sans:vf@latest/latin-wdth-normal.woff2') format('woff2-variations'); font-weight: 200 900; font-stretch: 75% 125%; 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; }
/* Welt: Haff, Duft, Leinen und Keramik aus Ueckermünde · Rezept mode-beauty-kollektion
Warmes Off-White, eine Grotesk (Zalando Sans, breit, nicht fett), Formel in Fragment Mono. */
@property --heben { syntax: '<number>'; inherits: false; initial-value: 0; }
@property --licht { syntax: '<number>'; inherits: false; initial-value: 0; }
@property --hub { syntax: '<number>'; inherits: true; initial-value: 0; }
:root {
--grund: #f6f3ee; --flaeche: #ebe7e0; --linie: #cecdc6; --text: #121010; --leise: #6b645b;
--fokus: #121010;
--kurve: cubic-bezier(0.25, 1, 0.5, 1); /* Kampagnenbild (Toteme-Karussell) */
--ui: 250ms;
--heben-dauer: 1100ms; --stagger: 110ms;
--kopf: 68px;
--rand: clamp(16px, 2.6vw, 40px);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--text); font-family: 'Zalando Sans', system-ui, sans-serif; font-weight: 400; font-stretch: 100%; -webkit-font-smoothing: antialiased; }
a { color: inherit; text-underline-offset: .3em; text-decoration-thickness: 1px; }
:focus-visible { outline: 2px solid var(--fokus); outline-offset: 3px; }
.mono { font: 400 .75rem/1.4 'Fragment Mono', ui-monospace, monospace; letter-spacing: .06em; text-transform: uppercase; font-variant-numeric: tabular-nums; }
/* ---------- Kopf ---------- */
.kopf { height: var(--kopf); display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: 16px; padding: 0 var(--rand); }
.marke { justify-self: center; grid-column: 2; font-stretch: 125%; font-weight: 500; font-size: 1.375rem; letter-spacing: .02em; text-decoration: none; min-height: 44px; display: inline-flex; align-items: center; }
.kopf nav { grid-column: 1; grid-row: 1; }
.kopf ul { display: flex; gap: clamp(14px, 2vw, 28px); list-style: none; }
.kopf ul a { text-decoration: none; font-size: .9375rem; padding: 12px 0; }
.kopf ul a:hover, .kopf ul a[aria-current] { text-decoration: underline; }
.kopf__rechts { grid-column: 3; justify-self: end; display: flex; gap: 18px; align-items: center; font-size: .9375rem; }
.kopf__rechts a { text-decoration: none; padding: 12px 0; }
.kopf__rechts a:hover { text-decoration: underline; }
.menue { display: none; font: inherit; background: none; border: 0; min-height: 44px; padding: 0 4px; color: inherit; cursor: pointer; }
/* ---------- Bühne: Kampagnenbild in voller Breite + Scheiben ---------- */
.buehne {
position: relative; container-type: size; overflow: hidden; background: #1f2a6b;
height: max(380px, calc(100svh - var(--kopf) - 30px - 132px));
--zx: 0; --zy: 0;
--bewegung: translate(calc(var(--zx) * -8px), calc(var(--zy) * -6px)) scale(calc(1.03 + var(--hub) * .03));
transition: --licht var(--heben-dauer) var(--kurve);
}
.buehne.gehoben { --licht: 1; }
.buehne > img, .scheibe > span { transform: var(--bewegung); }
/* Bild dahinter tritt zurück: dunkler und leicht entsättigt, die Scheiben bleiben im vollen Licht */
.buehne > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; filter: brightness(calc(1 - var(--licht) * .32)) saturate(calc(1 - var(--licht) * .25)); }
@supports (animation-timeline: view()) {
.buehne { animation: hub linear both; animation-timeline: view(); animation-range: exit 0% exit 100%; }
body[data-simuliert] .buehne { animation: none; }
}
@keyframes hub { to { --hub: 1; } }
.scheibe {
--t: calc(var(--heben) * var(--tiefe));
position: absolute; left: calc(var(--sx) * 1%); top: calc(var(--sy) * 1%); width: calc(var(--sb) * 1%); height: calc(var(--sh) * 1%);
overflow: hidden;
transform: translate(calc(var(--zx) * var(--t) * 30px), calc(var(--zy) * var(--t) * 20px)) scale(calc(1 + var(--t) * (.1 + var(--hub) * .07)));
box-shadow: 0 calc(var(--t) * 34px) calc(var(--t) * 70px) rgba(8, 10, 30, calc(var(--t) * .5)), 0 calc(var(--t) * 6px) calc(var(--t) * 14px) rgba(8, 10, 30, calc(var(--t) * .25));
filter: blur(calc((1 - var(--heben)) * 7px)); /* Slicey-Prinzip: unscharf von, scharf bis */
transition: --heben var(--heben-dauer) var(--kurve) calc(var(--j) * var(--stagger));
will-change: transform;
}
.gehoben .scheibe { --heben: 1; }
.sofort, .sofort .scheibe { transition: none !important; }
/* Inneres: dasselbe Bild in Bühnengröße, um −x/−y versetzt → deckungsgleich mit dem Bild dahinter */
.scheibe > span { position: absolute; left: calc(var(--sx) * -1cqw); top: calc(var(--sy) * -1cqh); width: 100cqw; height: 100cqh; background: var(--bild) center / cover no-repeat; transform-origin: 50cqw 50cqh; }
.scheibe::after { content: ''; position: absolute; inset: 0; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, calc(var(--t) * .55)); pointer-events: none; }
/* Marken über dem Bild: was die Scheibe zeigt (Duftnote, Ton, Stück) */
.achsen { position: relative; height: 30px; }
.achsen span { position: absolute; bottom: 7px; color: var(--leise); translate: -50% 0; white-space: nowrap; }
.achsen span::after { content: ''; position: absolute; left: 50%; top: calc(100% + 2px); width: 1px; height: 6px; background: currentColor; }
/* ---------- Titelzeile: Name, Formel, Preis (wie Typology) ---------- */
.titelzeile { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); gap: 12px 32px; align-items: end; padding: 22px var(--rand) 0; }
.titelzeile h1 { font-weight: 400; font-stretch: 125%; font-size: clamp(2rem, 1.2rem + 2.6vw, 3.75rem); line-height: 1.04; letter-spacing: -.01em; }
.titelzeile h1 i { font-style: normal; font-stretch: 100%; font-weight: 300; font-size: .62em; color: var(--leise); white-space: nowrap; }
.titelzeile .rechts { display: flex; justify-content: space-between; align-items: end; gap: 12px 20px; flex-wrap: wrap; padding-bottom: 6px; }
.titelzeile .rechts p { max-width: 36ch; color: var(--text); }
.titelzeile .rechts a { font-weight: 500; white-space: nowrap; min-height: 44px; display: inline-flex; align-items: center; gap: 10px; padding: 0 22px; border-radius: 999px; background: var(--text); color: var(--grund); text-decoration: none; transition: background-color var(--ui) var(--kurve); }
.titelzeile .rechts a:hover { background: #3a3431; }
/* ---------- Danach ---------- */
.haus { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: 32px; padding: clamp(64px, 12vh, 128px) var(--rand); }
.haus h2 { font-weight: 400; font-stretch: 125%; font-size: 1.75rem; letter-spacing: -.01em; }
.haus p { font-size: 1.0625rem; line-height: 1.5; max-width: 52ch; }
.haus p + p { margin-top: 1em; }
.sortiment { list-style: none; margin-top: 36px; border-top: 1px solid var(--linie); max-width: 64ch; }
.sortiment li { display: flex; justify-content: space-between; gap: 16px; padding: 14px 0; border-bottom: 1px solid var(--linie); flex-wrap: wrap; }
.sortiment a { text-decoration: none; } .sortiment a:hover { text-decoration: underline; }
.sortiment .mono { color: var(--leise); }
.fuss { background: var(--text); color: #f6f3ee; display: grid; grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr)); gap: 24px; padding: 48px var(--rand); font-size: .9375rem; line-height: 1.5; }
.fuss .mono { color: #c9c2b8; }
/* ---------- Pult der Demo ---------- */
.pult { position: fixed; z-index: 10; right: 12px; top: calc(var(--kopf) + 38px); display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; max-width: calc(100vw - 24px); padding: 8px 10px; background: rgba(18, 16, 16, .92); color: #f6f3ee; border-radius: 10px; font: 400 .75rem/1.3 'Fragment Mono', ui-monospace, monospace; }
.pult button { font: inherit; color: inherit; background: none; border: 1px solid rgba(246, 243, 238, .32); border-radius: 999px; min-height: 30px; padding: 0 12px; cursor: pointer; }
.pult button[aria-pressed=true] { background: #f6f3ee; color: var(--text); }
.pult__werte { color: #d3ccc2; font-variant-numeric: tabular-nums; }
/* ---------- Telefon ---------- */
@media (max-width: 760px) {
.kopf { position: relative; grid-template-columns: auto minmax(0, 1fr) auto; }
.marke { grid-column: 1; justify-self: start; }
.kopf nav { grid-column: 2; }
.kopf ul { display: none; }
.kopf.offen ul { display: grid; gap: 0; position: absolute; z-index: 5; left: 0; right: 0; top: var(--kopf); background: var(--grund); padding: 8px var(--rand) 20px; border-bottom: 1px solid var(--linie); }
.kopf.offen ul a { display: block; padding: 12px 0; font-size: 1.25rem; }
.kopf__rechts .nur-breit { display: none; }
.menue { display: inline-flex; align-items: center; }
.buehne { height: max(360px, 62svh); }
.scheibe[data-aussen] { display: none; }
.titelzeile { grid-template-columns: minmax(0, 1fr); }
.haus { grid-template-columns: minmax(0, 1fr); }
.achsen span { font-size: .6875rem; letter-spacing: .03em; }
.achsen span[data-aussen] { display: none; }
}
@media (max-width: 760px) and (hover: none) { .achsen { display: none; } }
.vorschau body { overflow: hidden; }
.vorschau .pult { display: none; }
/* Reduzierte Bewegung: Scheiben stehen in ruhender Tiefe, kein Zeiger, kein Scroll */
:root.reduziert .scheibe { --heben: 1; transition: none; }
:root.reduziert .buehne { --licht: 1; --zx: 0 !important; --zy: 0 !important; animation: none; transition: none; }
:root.reduziert * { transition-duration: .01ms !important; }
@media (hover: none) { .titelzeile .rechts a { transition: none; } }
@media (forced-colors: active) {
button, [role=button], [role=tab] { border: 1px solid ButtonText !important; }
.titelzeile .rechts a { border: 1px solid LinkText; }
.pult button[aria-pressed=true] { outline: 2px solid Highlight; }
}
</style>
</head>
<body data-v="senkrecht">
<header class="kopf">
<nav aria-label="Hauptnavigation">
<ul id="hauptmenue">
<li><a href="#start" aria-current="page">Duft</a></li>
<li><a href="#haus">Leinen</a></li>
<li><a href="#haus">Keramik</a></li>
<li><a href="#haus">Laden</a></li>
</ul>
</nav>
<a class="marke" href="#start" aria-label="Haff, Startseite">HAFF</a>
<div class="kopf__rechts">
<a class="nur-breit" href="#haus">Suche</a>
<a href="#haus">Korb (0)</a>
<button class="menue" type="button" aria-expanded="false" aria-controls="hauptmenue">Menü</button>
</div>
</header>
<main id="start">
<div class="achsen mono" aria-hidden="true" data-achsen></div>
<figure class="buehne" data-buehne>
<img data-bild src="bilder/flakon.jpg" alt="" width="1200" height="800" fetchpriority="high">
</figure>
<div class="titelzeile">
<h1 data-titel>Strandgut <i>Eau de Parfum</i></h1>
<div class="rechts">
<p class="mono" data-unterschrift>Labdanum 12 % · Iris 4 % · Treibholz · 50 ml · 128 €</p>
<a href="#haus" data-cta>Zum Duft</a>
</div>
</div>
<section class="haus" id="haus" aria-labelledby="h-titel">
<h2 id="h-titel">Der Laden am Hafen</h2>
<div>
<p>Wir mischen Düfte, lassen Leinen weben und drehen Keramik, alles in kleinen Auflagen. Der Laden liegt in einem alten Netzschuppen am Ueckermünder Hafen, geöffnet Mittwoch bis Samstag.</p>
<p>Jeder Duft trägt seine Formel auf dem Etikett. Leinen und Keramik sind Einzelstücke, kleine Abweichungen gehören dazu.</p>
<ul class="sortiment" aria-label="Aus dem Sortiment">
<li><a href="#start">Strandgut, Eau de Parfum</a><span class="mono">50 ml · 128 €</span></li>
<li><a href="#start">Tischtuch, gewaschenes Leinen</a><span class="mono">fünf Töne · ab 38 €</span></li>
<li><a href="#start">Flaschenvase, Steinzeug</a><span class="mono">Einzelstück · 64 €</span></li>
<li><a href="#start">Probenset, drei Düfte</a><span class="mono">3 × 2 ml · 18 €</span></li>
</ul>
</div>
</section>
</main>
<footer class="fuss" id="kontakt">
<p>Haff · Duft, Leinen, Keramik<br>Am Hafen 7, 17373 Ueckermünde</p>
<p>Mi–Sa 11–18 Uhr<br>laden@haff.example</p>
<p class="mono">Versand in drei Werktagen<br>Rücksendung 30 Tage kostenlos</p>
</footer>
<div class="pult" role="group" aria-label="Variante der Demo">
<span>Schnitt</span>
<button type="button" data-variante="senkrecht" aria-pressed="true">Senkrecht</button>
<button type="button" data-variante="waagerecht" aria-pressed="false">Waagerecht</button>
<button type="button" data-variante="rechtecke" aria-pressed="false">Rechtecke</button>
<span class="pult__werte" data-werte></span>
</div>
<script>
(() => {
const html = document.documentElement, body = document.body;
const reduziert = () => html.classList.contains('reduziert');
const buehne = document.querySelector('[data-buehne]');
const bild = document.querySelector('[data-bild]');
const achsen = document.querySelector('[data-achsen]');
/* Schnitte in Prozent der Bühne: x, y, Breite, Höhe, Tiefe (0–1), aussen (entfällt unter 760 px), Marke über dem Bild */
const VARIANTEN = {
senkrecht: {
bild: 'flakon', alt: 'Rechteckiger Glasflakon mit bernsteinfarbener Füllung und schwarzer Kappe auf einem runden Steinsockel vor kobaltblauem Grund, daneben eine kleine Kugel',
titel: 'Strandgut <i>Eau de Parfum</i>', unterschrift: 'Labdanum 12 % · Iris 4 % · Treibholz · 50 ml · 128 €', cta: 'Zum Duft',
werte: '3 Lamellen · Tiefe .7 / 1 / .55 · scale bis 1,17',
scheiben: [[8.33, 10, 8.33, 78, .7, 1, 'Kopfnote'], [25, 4, 12.5, 90, 1, 0, 'Herznote'], [66.66, 16, 8.33, 74, .55, 0, 'Basisnote']]
},
waagerecht: {
bild: 'stoff-stapel', alt: 'Stapel gefalteter Leinentücher in Pflaume, Altrosa, Ocker und Creme auf einem Eichentisch',
titel: 'Leinen <i>gewaschen</i>', unterschrift: 'Tischwäsche aus Leinen · fünf Töne · 140 × 240 cm · ab 38 €', cta: 'Zum Leinen',
werte: '3 Bänder · Tiefe .55 / 1 / .75 · scale bis 1,17',
scheiben: [[16.66, 26, 66.66, 10, .55, 1], [8.33, 45, 83.33, 11, 1], [25, 62, 58.33, 9, .75]]
},
rechtecke: {
bild: 'keramik-stillleben', alt: 'Helle Flaschenvase, schwarzes Kugelgefäß, ockerfarbener Becher und flache Schalen auf Gipssockeln vor terrakottafarbener Wand',
titel: 'Keramik <i>vom Haff</i>', unterschrift: 'Steinzeug, in Ueckermünde gedreht · Einzelstücke · ab 24 €', cta: 'Zur Keramik',
werte: '3 Rechtecke · Tiefe 1 / .6 / .8 · scale bis 1,17',
scheiben: [[29.16, 14, 16.66, 44, 1, 0, 'Flaschenvase'], [53.5, 58, 7.5, 18, .6, 1, 'Becher'], [75, 81, 16.66, 17, .8, 0, 'Schale']]
}
};
let v = 'senkrecht';
function setzeVariante(neu) {
if (!VARIANTEN[neu]) neu = 'senkrecht';
v = neu; body.dataset.v = v;
const c = VARIANTEN[v];
const quelle = 'bilder/' + c.bild + (matchMedia('(max-width: 760px)').matches ? '-hoch' : '') + '.jpg';
bild.src = quelle; bild.alt = c.alt;
document.querySelector('[data-titel]').innerHTML = c.titel;
document.querySelector('[data-unterschrift]').textContent = c.unterschrift;
document.querySelector('[data-cta]').textContent = c.cta;
document.querySelector('[data-werte]').textContent = c.werte;
buehne.querySelectorAll('.scheibe').forEach(s => s.remove());
achsen.textContent = '';
c.scheiben.forEach(([x, y, b, h, tiefe, aussen, marke], j) => {
const s = document.createElement('div');
s.className = 'scheibe'; s.setAttribute('aria-hidden', 'true');
if (aussen) s.dataset.aussen = '';
s.style.cssText = `--sx:${x};--sy:${y};--sb:${b};--sh:${h};--tiefe:${tiefe};--j:${j};--bild:url('${quelle}')`;
s.appendChild(document.createElement('span'));
buehne.appendChild(s);
if (marke) { const m = document.createElement('span'); m.textContent = marke; m.style.left = (x + b / 2) + '%'; if (aussen) m.dataset.aussen = ''; achsen.appendChild(m); }
});
document.querySelectorAll('[data-variante]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.variante === v)));
const q = new URLSearchParams(location.search); q.set('v', v);
history.replaceState(null, '', location.pathname + '?' + q.toString().replace(/=(?=&|$)/g, '') + location.hash);
heben();
}
document.querySelectorAll('[data-variante]').forEach(b => b.addEventListener('click', () => { pause(); setzeVariante(b.dataset.variante); }));
/* Anheben: einmal beim Laden (1100 ms, 110 ms versetzt), dabei werden die Scheiben scharf */
function heben() {
buehne.classList.add('sofort'); buehne.classList.remove('gehoben'); void buehne.offsetWidth; buehne.classList.remove('sofort');
if (reduziert()) { buehne.classList.add('gehoben'); return; }
const los = () => requestAnimationFrame(() => requestAnimationFrame(() => buehne.classList.add('gehoben')));
if (bild.complete && bild.naturalWidth) los(); else bild.addEventListener('load', los, { once: true });
}
/* Zeiger: weich nachgezogen (Lerp 0,08 je Frame), nur mit echtem Hover */
let zx = 0, zy = 0, ziel = { x: 0, y: 0 }, laeuft = false;
function setzeZeiger(x, y) { buehne.style.setProperty('--zx', x.toFixed(4)); buehne.style.setProperty('--zy', y.toFixed(4)); }
function nachziehen() {
zx += (ziel.x - zx) * .08; zy += (ziel.y - zy) * .08;
setzeZeiger(zx, zy);
if (Math.abs(ziel.x - zx) + Math.abs(ziel.y - zy) > .001) requestAnimationFrame(nachziehen); else laeuft = false;
}
function anstossen() { if (!laeuft) { laeuft = true; requestAnimationFrame(nachziehen); } }
if (matchMedia('(hover: hover) and (pointer: fine)').matches) {
buehne.addEventListener('pointermove', e => {
if (reduziert() || uhr) return;
const r = buehne.getBoundingClientRect();
ziel.x = ((e.clientX - r.left) / r.width) * 2 - 1; ziel.y = ((e.clientY - r.top) / r.height) * 2 - 1;
anstossen();
});
buehne.addEventListener('pointerleave', () => { ziel.x = 0; ziel.y = 0; anstossen(); });
}
/* Scroll-Hub ohne Scroll-Timeline: Skript setzt --hub */
if (!CSS.supports('animation-timeline: view()')) {
const hub = () => { if (reduziert() || uhr) return; const r = buehne.getBoundingClientRect(); buehne.style.setProperty('--hub', Math.min(1, Math.max(0, -r.top / r.height)).toFixed(4)); };
addEventListener('scroll', () => requestAnimationFrame(hub), { passive: true });
}
setzeVariante(new URLSearchParams(location.search).get('v'));
/* Menü auf dem Telefon */
const kopf = document.querySelector('.kopf'), menue = document.querySelector('.menue');
menue.addEventListener('click', () => { const auf = !kopf.classList.contains('offen'); kopf.classList.toggle('offen', auf); menue.setAttribute('aria-expanded', String(auf)); menue.textContent = auf ? 'Schließen' : 'Menü'; });
kopf.addEventListener('keydown', e => { if (e.key === 'Escape' && kopf.classList.contains('offen')) { menue.click(); menue.focus(); } });
kopf.querySelectorAll('ul a').forEach(a => a.addEventListener('click', () => { if (kopf.classList.contains('offen')) menue.click(); }));
/* ---------- Vorführung: Anheben, gedachter Zeiger kreist, Scroll-Hub schwillt ---------- */
let uhr = null, t0 = 0;
const RUNDE = 8000;
function vorfuehren() {
pause(); scrollTo(0, 0);
body.dataset.simuliert = '';
t0 = performance.now(); let runde = -1;
const schritt = t => {
const z = t - t0, n = Math.floor(z / RUNDE), q = (z % RUNDE) / RUNDE;
if (n !== runde) { runde = n; heben(); }
if (!reduziert()) {
const a = Math.max(0, Math.min(1, (q - .12) / .1)) * Math.max(0, Math.min(1, (.95 - q) / .08));
const w = q * Math.PI * 2 * 1.5;
zx = Math.sin(w) * .95 * a; zy = Math.cos(w * .8) * .7 * a;
setzeZeiger(zx, zy);
buehne.style.setProperty('--hub', (Math.max(0, Math.sin(Math.max(0, q - .55) / .4 * Math.PI)) * .9).toFixed(4));
}
uhr = requestAnimationFrame(schritt);
};
uhr = requestAnimationFrame(schritt);
}
function pause() {
cancelAnimationFrame(uhr); uhr = null;
if ('simuliert' in body.dataset) { delete body.dataset.simuliert; buehne.style.removeProperty('--hub'); }
}
function neustart() { pause(); zx = zy = 0; ziel = { x: 0, y: 0 }; setzeZeiger(0, 0); scrollTo(0, 0); heben(); }
})();
</script>
</body>
</html>