Subtiler mehrschichtiger Parallax
Bilder wandern im Rahmen gegen den Scroll, Karten und Ziffern laufen mit eigenem Tempo: Tiefe, die man spürt, aber kaum sieht.
Bilder sitzen in einem Rahmen mit overflow: clip und sind innen höher als der Rahmen (rund 120–125 %). Beim Scrollen wandert das Bild im Rahmen langsam gegen die Scrollrichtung, der Rahmen selbst scrollt normal. Zusätzlich laufen einzelne Elemente wie Bildunterschriften, Detailbilder oder Deko-Ziffern mit leicht anderem Tempo (0,8 bis 1,2). So entstehen Ebenen. Keine großen Hintergrundsprünge wie in den 2010ern.
Flache Layouts bekommen Tiefe und Gewicht. Die Bewegung ist an den Scroll gekoppelt und linear, deshalb wirkt sie ruhig und hochwertig, ohne aufzufallen.
Wann einsetzen
Überall mit großen Bildern: Portfolio-Raster, Editorial, Hero, Teaser, Case Studies.
Wann nicht
Nicht bei Text in Lesespalten, Formularen und Tabellen. Nicht bei kleinen Bildern, dort wirkt es zittrig.
Varianten
- Bild im Rahmen: yPercent −10 → 10, Bild entsprechend höher als der Rahmen
- Ebenen per data-speed (ScrollSmoother, Locomotive Scroll): Elemente laufen mit 0,8 bis 1,2
- data-speed="auto" bei ScrollSmoother: bewegt das Element nur so weit, wie das Elternelement mit overflow: hidden Platz lässt
- Gegenläufige Spalten (Reverse-Scrolling Columns), auch rein mit CSS
- Text-Parallax: große Schriftzeilen wandern beim Senkrechtscrollen waagerecht
Umsetzung
CSS: .rahmen { overflow: clip } .rahmen img { height: 125%; top: -12.5%; animation: plx linear both; animation-timeline: view(); animation-range: cover } @keyframes plx { from { translate: 0 -10% } to { translate: 0 10% } }. Liegt das view() am transformierten Bild, wackelt die Messung. Besser den Rahmen als benannte Timeline nutzen (view-timeline-name am Rahmen). GSAP: gsap.fromTo(img, { yPercent: -10 }, { yPercent: 10, ease: 'none', scrollTrigger: { trigger: rahmen, start: 'top bottom', end: 'bottom top', scrub: true } }). Ebenen: y von −w bis +w mit w = (1 − speed) · (Viewport-Höhe + Elementhöhe) / 2, invalidateOnRefresh: true. Das Bild muss den ganzen Weg abdecken, sonst werden Kanten sichtbar: Bildhöhe ≥ 100 % + 2 × Weg. Rahmen-Seitenverhältnis × Überhöhe = Bildformat, dann wird seitlich nichts beschnitten.
Robust umsetzen
Telefon. Die CSS-Variante läuft auf dem Compositor flüssig. JS-Ebenen auf Touch eher abschalten und nur das Bild im Rahmen bewegen, wie Wokine per matchMedia. ScrollSmoother glättet Touch standardmäßig nicht.
Ohne Bewegung. Abschalten. Parallax ist ein klassischer Auslöser für vestibuläre Beschwerden. Bilder ruhen mittig im Rahmen.
Barrierefrei. Rein dekorativ. Schnellere Ebenen dürfen nie Inhalt verdecken, also genug Abstand für den ganzen Weg einplanen. Text in Lesespalten bleibt fest.
Code
<!DOCTYPE html>
<!--
Subtiler mehrschichtiger Parallax
Fundus · Scroll & Section-Animationen · https://cecodesign.de/lab/fundus/scroll/mehrschicht-parallax/
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>Subtiler mehrschichtiger Parallax · 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: '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; }
@font-face { font-family: 'Instrument Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-serif@latest/latin-400-italic.woff2') format('woff2'); font-weight: 400; font-style: italic; font-display: swap; }
:root {
--grund: #ece8df; --tinte: #1c1a17; --leise: rgba(28, 26, 23, .66); --linie: rgba(28, 26, 23, .16);
--rand: clamp(20px, 4vw, 56px); --spalt: clamp(12px, 2vw, 28px);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--tinte); font-family: 'Instrument Sans', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { background: var(--grund); overflow-x: clip; }
em, .serif { font-family: 'Instrument Serif', Georgia, serif; font-style: italic; font-weight: 400; }
.mono { font: 500 11px/1.45 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
.raster { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); column-gap: var(--spalt); padding-inline: var(--rand); }
/* Der Kern: Rahmen schneidet, Bild darin ist 125 % hoch und wandert yPercent −10 → 10 */
.rahmen { position: relative; overflow: clip; background: #d9d4c9; }
.rahmen img { position: absolute; left: 0; top: -12.5%; width: 100%; height: 125%; object-fit: cover; display: block; will-change: transform; }
/* Rahmen × 1,25 = Bildformat: 15/8 für 3:2-Bilder, 1/1 für 4:5-Bilder, so wird seitlich nichts beschnitten */
.r-quer { aspect-ratio: 15 / 8; }
.r-hoch { aspect-ratio: 1 / 1; }
[data-speed] { will-change: transform; }
/* Kopf */
.kopf { min-height: 100svh; grid-template-rows: auto 1fr auto; row-gap: clamp(28px, 5vh, 56px); padding-block: clamp(18px, 3vw, 32px) clamp(28px, 6vh, 64px); }
.leiste { grid-column: 1 / -1; display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; padding-bottom: 18px; border-bottom: 1px solid var(--linie); }
.marke { font-weight: 600; font-size: 17px; letter-spacing: -.02em; display: flex; gap: 14px; align-items: baseline; }
.marke .mono { font-size: 10px; }
.modi { display: flex; gap: 4px; }
.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-color 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(--grund); border-color: var(--tinte); }
.modi button:disabled { opacity: .35; cursor: not-allowed; }
.titel { grid-column: 1 / 9; grid-row: 2; align-self: end; font-size: clamp(48px, 8.4vw, 136px); font-weight: 600; letter-spacing: -.048em; line-height: .9; }
.titel em { letter-spacing: -.02em; font-size: 1.06em; }
.kopf__bild { grid-column: 8 / 13; grid-row: 2 / 4; align-self: end; display: grid; gap: 10px; }
.kopf__bild figcaption { text-align: right; }
.kopf__karte { grid-column: 7 / 10; grid-row: 3; align-self: end; margin-bottom: 30px; position: relative; z-index: 2; background: var(--tinte); color: var(--grund); padding: 18px 20px 20px; display: grid; gap: 10px; box-shadow: 0 30px 60px -30px rgba(28, 26, 23, .5); }
.kopf__karte .mono { color: rgba(236, 232, 223, .6); display: flex; justify-content: space-between; gap: 12px; }
.kopf__karte .serif { font-size: clamp(26px, 2.4vw, 36px); line-height: 1; text-wrap: balance; }
.kopf__unten { grid-column: 1 / 7; grid-row: 3; display: grid; gap: 22px; align-content: end; }
.kopf__text { font-size: clamp(16px, 1.3vw, 19px); line-height: 1.45; max-width: 36ch; }
/* Werk 01 */
.werk { position: relative; padding-block: clamp(64px, 12vh, 140px); }
.ziffer { position: absolute; top: clamp(10px, 6vh, 70px); left: var(--rand); font-family: 'Instrument Serif', Georgia, serif; font-style: italic; font-size: clamp(160px, 26vw, 400px); line-height: .8; color: rgba(28, 26, 23, .09); pointer-events: none; z-index: 0; }
.eins .werk__haupt { grid-column: 3 / 11; grid-row: 1; position: relative; z-index: 1; }
.farbkarte { grid-column: 10 / 13; grid-row: 1; align-self: end; margin-bottom: -14%; position: relative; z-index: 2; background: #f7f4ee; padding: clamp(16px, 1.8vw, 24px); display: grid; gap: 16px; box-shadow: 0 30px 60px -30px rgba(28, 26, 23, .35); }
.farbkarte ul { list-style: none; display: grid; gap: 10px; }
.farbkarte li { display: grid; grid-template-columns: 34px 1fr auto; align-items: center; gap: 12px; font-size: 15px; }
.farbkarte li::before { content: ''; height: 34px; border-radius: 50%; background: var(--f); box-shadow: inset 0 0 0 1px rgba(28, 26, 23, .1); }
.farbkarte li i { font: 500 11px 'Geist Mono', monospace; letter-spacing: .08em; color: var(--leise); font-style: normal; }
.eins .werk__text { grid-column: 3 / 8; grid-row: 2; padding-top: 28px; display: grid; gap: 8px; position: relative; z-index: 1; }
h2 { font-size: clamp(34px, 4.2vw, 64px); font-weight: 600; letter-spacing: -.04em; line-height: .95; }
.unterzeile { font-size: clamp(20px, 1.8vw, 28px); line-height: 1.1; color: var(--leise); }
/* Werk 02 */
.zwei .werk__spalte { grid-column: 1 / 6; grid-row: 1; align-self: center; display: grid; gap: 18px; max-width: 42ch; }
.zwei .werk__spalte p:not(.mono):not(.unterzeile) { font-size: 17px; line-height: 1.55; }
.fakten { display: grid; grid-template-columns: auto 1fr; gap: 8px 24px; padding-top: 18px; border-top: 1px solid var(--linie); }
.fakten dd { font-size: 15px; }
.zwei .werk__haupt { grid-column: 7 / 12; grid-row: 1; }
.zwei .werk__neben { grid-column: 5 / 9; grid-row: 1; align-self: end; margin-bottom: -18%; position: relative; z-index: 2; display: grid; gap: 10px; }
.zwei .werk__neben .rahmen { box-shadow: 0 30px 60px -30px rgba(28, 26, 23, .45); }
/* Band: Text-Parallax waagerecht */
.band { padding-block: clamp(90px, 16vh, 170px) clamp(40px, 8vh, 90px); overflow: clip; display: grid; gap: .1em; font-size: clamp(64px, 11vw, 168px); line-height: .95; letter-spacing: -.045em; font-weight: 600; white-space: nowrap; }
.band p { will-change: transform; }
.band p:nth-child(2) { font-family: 'Instrument Serif', Georgia, serif; font-style: italic; font-weight: 400; letter-spacing: -.02em; color: var(--leise); }
/* Werk 03–05 */
.trio { padding-block: clamp(40px, 8vh, 90px) clamp(100px, 18vh, 200px); row-gap: 28px; }
.trio__kopf { grid-column: 1 / -1; display: flex; justify-content: space-between; gap: 16px; padding-bottom: 18px; border-bottom: 1px solid var(--linie); }
.trio figure { display: grid; gap: 12px; align-content: start; }
.trio figure:nth-of-type(1) { grid-column: 1 / 7; }
.trio figure:nth-of-type(2) { grid-column: 7 / 13; margin-top: clamp(60px, 16vh, 180px); }
.trio figcaption { display: flex; justify-content: space-between; gap: 12px; }
.trio figcaption b { color: var(--tinte); font-weight: 500; }
.fuss { padding-block: clamp(60px, 12vh, 140px) clamp(28px, 5vh, 48px); border-top: 1px solid var(--linie); row-gap: 40px; }
.fuss h2 { grid-column: 1 / 10; font-size: clamp(42px, 6.6vw, 104px); }
.fuss .mono { grid-column: 1 / -1; display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
/* Werte-Anzeige */
.werte { position: fixed; right: 12px; bottom: 12px; z-index: 10; max-width: calc(100% - 24px); background: rgba(236, 232, 223, .88); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); border: 1px solid var(--linie); border-radius: 10px; padding: 8px 11px; font-size: 9.5px; letter-spacing: .1em; text-align: right; display: grid; gap: 2px; }
.werte b { color: var(--tinte); font-weight: 500; }
/* Reduzierte Bewegung: keine Parallaxe, Bilder ruhen mittig */
.reduziert .rahmen img, .reduziert [data-speed], .reduziert .band p { transform: none !important; will-change: auto; }
.vorschau body { overflow: hidden; }
.vorschau .ziffer { color: rgba(28, 26, 23, .11); }
@media (max-width: 760px) {
.kopf { min-height: auto; grid-template-rows: auto; row-gap: 28px; }
.titel, .kopf__bild, .kopf__unten { grid-column: 1 / -1; grid-row: auto; }
.kopf__bild { width: 82%; justify-self: end; }
.kopf__karte { grid-column: 1 / 9; grid-row: auto; margin: -90px 0 40px; }
.werte { left: 12px; right: auto; text-align: left; }
.werte span:not(:nth-child(2)) { display: none; }
.eins .werk__haupt { grid-column: 1 / -1; }
.eins .werk__haupt .rahmen { aspect-ratio: 1 / 1; }
.trio__kopf span:last-child { display: none; }
.farbkarte { grid-column: 4 / -1; grid-row: 2; margin: -22% 0 0; }
.eins .werk__text { grid-column: 1 / -1; grid-row: 3; padding-top: 120px; } /* Luft für die schnellere Farbkarte: nichts Inhaltliches verdecken */
.zwei .werk__spalte { grid-column: 1 / -1; grid-row: 1; margin-bottom: 40px; }
.zwei .werk__haupt { grid-column: 3 / -1; grid-row: 2; }
.zwei .werk__neben { grid-column: 1 / 8; grid-row: 3; margin: -30% 0 0; }
.trio figure:nth-of-type(n) { grid-column: 1 / -1; }
.trio figure:nth-of-type(2) { margin-top: 12px; width: 86%; justify-self: end; }
.fuss h2 { grid-column: 1 / -1; }
.werte { font-size: 9px; }
}
@media (forced-colors: active) {
button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; }
}
</style>
</head>
<body>
<header class="kopf raster">
<div class="leiste">
<p class="marke">Haus Marlow <span class="mono">Markenbüro · Freiburg</span></p>
<div class="modi mono" role="group" aria-label="Variante">
<button type="button" data-variante="rahmen" aria-pressed="false">Rahmen</button>
<button type="button" data-variante="ebenen" aria-pressed="true">Ebenen</button>
<button type="button" data-variante="schrift" aria-pressed="false">+ Schrift</button>
</div>
</div>
<h1 class="titel">Marken, die <em>Gewicht</em> haben.</h1>
<figure class="kopf__bild" data-speed="0.9">
<div class="rahmen r-hoch"><img src="bilder/marke-krume-hoch.jpg" alt="Wortmarke krume in Dunkelbraun auf Senfgelb, darüber ein Brotlaib" width="800" height="1000"></div>
<figcaption class="mono">Krume · Wortmarke</figcaption>
</figure>
<div class="kopf__karte" data-speed="1.15">
<p class="mono"><span>Im Fokus</span><span>00</span></p>
<p class="serif">Fünf Buchstaben, ein Brot.</p>
</div>
<div class="kopf__unten">
<p class="kopf__text">Wir gestalten Erscheinungsbilder für Cafés, Handwerk und Verlage. Wenige Projekte im Jahr, jedes bis ins letzte Detail.</p>
<p class="mono">Ausgewählte Arbeiten ↓</p>
</div>
</header>
<main>
<section class="werk eins raster">
<span class="ziffer" data-speed="0.8" aria-hidden="true">01</span>
<figure class="werk__haupt">
<div class="rahmen r-quer"><picture><source media="(max-width: 760px)" srcset="bilder/marke-roestkontor-hoch.jpg"><img src="bilder/marke-roestkontor.jpg" alt="Wortmarke Röstkontor in Creme auf Ziegelrot, darüber eine Kaffeebohne als Signet" width="1200" height="800"></picture></div>
</figure>
<div class="farbkarte" data-speed="1.2">
<p class="mono">Hausfarben</p>
<ul>
<li style="--f:#d14129">Ziegel <i>#D1 41 29</i></li>
<li style="--f:#291811">Röstbraun <i>#29 18 11</i></li>
<li style="--f:#f3e8d6">Milchschaum <i>#F3 E8 D6</i></li>
</ul>
</div>
<div class="werk__text" data-speed="0.92">
<p class="mono">01 · Erscheinungsbild</p>
<h2>Röstkontor</h2>
<p class="unterzeile serif">Kaffeerösterei mit Ausschank in der Altstadt</p>
</div>
</section>
<section class="werk zwei raster">
<div class="werk__spalte">
<p class="mono">02 · Editorial</p>
<h2>Gefüge</h2>
<p class="unterzeile serif">Ein Heft über Dinge, die bleiben</p>
<p>Raster, Bildstrecken und Typografie aus einem Guss. Eine Serif mit Kursive führt durch das ganze Heft, weite Ränder geben jeder Doppelseite Ruhe.</p>
<dl class="fakten mono">
<dt>Leistung</dt><dd>Konzept, Layout, Bildregie</dd>
<dt>Format</dt><dd>Magazin, Hochformat</dd>
</dl>
</div>
<figure class="werk__haupt">
<div class="rahmen r-hoch"><img src="bilder/magazin-doppelseite-hoch.jpg" alt="Gefüge, aufgeschlagene Doppelseite mit der Überschrift „Die Ruhe der Dinge“" width="800" height="1000"></div>
</figure>
<figure class="werk__neben" data-speed="1.15">
<div class="rahmen r-quer"><img src="bilder/keramik-stillleben.jpg" alt="Stillleben aus der Bildstrecke: Vasen und Schalen auf Sockeln vor Terrakotta" width="1200" height="800"></div>
<figcaption class="mono">Bildstrecke · Doppelseite 4–5</figcaption>
</figure>
</section>
<section class="band" aria-hidden="true">
<p>Ruhig gesetzt · Klar geführt · Ruhig gesetzt · Klar geführt ·</p>
<p>Bilder mit Tiefe · Marken mit Haltung · Bilder mit Tiefe ·</p>
</section>
<section class="trio raster">
<div class="trio__kopf mono"><span>03–04 · Marke & Plakat</span><span>Buchhandel · Kultur</span></div>
<figure data-speed="0.9">
<div class="rahmen r-quer"><img src="bilder/marke-eselsohr.jpg" alt="Bildmarke Eselsohr, Buchhandlung und Lesecafé, auf Anthrazit" width="1200" height="800"></div>
<figcaption class="mono"><b>Eselsohr</b><span>Buchhandlung</span></figcaption>
</figure>
<figure data-speed="1.12">
<div class="rahmen r-quer"><img src="bilder/typo-plakat.jpg" alt="Ausstellungsplakat Licht: schwarze Versalien und orange Scheibe auf Neongelb" width="1200" height="800"></div>
<figcaption class="mono"><b>Licht, Halle 4</b><span>Ausstellungsplakat</span></figcaption>
</figure>
</section>
</main>
<footer class="fuss raster">
<h2>Neues Projekt? <em>Erzählen Sie davon.</em></h2>
<p class="mono"><span>Haus Marlow · Gerberau 12 · 79098 Freiburg</span><span>Termine ab Januar 2027</span></p>
</footer>
<aside class="werte mono" aria-live="polite" data-werte></aside>
<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/ScrollTrigger.min.js"></script>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const anzeige = document.querySelector('[data-werte]');
const knoepfe = [...document.querySelectorAll('[data-variante]')];
gsap.registerPlugin(ScrollTrigger);
// Werte aus den Messwerten: ease none, scrub, Bild yPercent −10 → 10, Ebenen 0,8–1,2
const BILD = { von: -10, bis: 10 }; // yPercent; Bild 125 % hoch, top −12,5 % → keine Kanten
const BAND = 14; // xPercent der Schriftzeilen
const BEREICH = { start: 'top bottom', end: 'bottom top' };
const TEXTE = {
rahmen: ['Bild im Rahmen', 'Bild 125 % · yPercent <b>−10 → 10</b>', 'ease none · scrub'],
ebenen: ['Ebenen · data-speed', 'Bild <b>−10 → 10</b> · speed <b>0,8–1,2</b>', 'ease none · scrub'],
schrift: ['Ebenen + Schrift', 'speed <b>0,8–1,2</b> · Band xPercent <b>±14</b>', 'ease none · scrub']
};
let variante = matchMedia('(hover: none)').matches ? 'rahmen' : 'ebenen'; // Touch: nur Bild im Rahmen
let ctx = null;
function bauen() {
if (ctx) ctx.revert();
knoepfe.forEach(b => { b.setAttribute('aria-pressed', String(b.dataset.variante === variante)); b.disabled = reduziert(); });
if (reduziert()) {
anzeige.innerHTML = '<span>Reduzierte Bewegung</span><span><b>Parallax aus</b> · Bilder ruhen mittig</span>';
return;
}
const t = TEXTE[variante];
anzeige.innerHTML = t.map(z => `<span>${z}</span>`).join('');
ctx = gsap.context(() => {
// 1) Bild im Rahmen: Rahmen scrollt normal, Bild wandert innen gegen die Scrollrichtung
document.querySelectorAll('.rahmen').forEach(rahmen => {
gsap.fromTo(rahmen.querySelector('img'), { yPercent: BILD.von }, {
yPercent: BILD.bis, ease: 'none',
scrollTrigger: { trigger: rahmen.closest('[data-speed]') || rahmen, ...BEREICH, scrub: true }
});
});
// 2) Ebenen: Elemente laufen mit eigener Geschwindigkeit (wie data-speed bei ScrollSmoother)
if (variante !== 'rahmen') document.querySelectorAll('[data-speed]').forEach(el => {
const s = parseFloat(el.dataset.speed);
const weg = () => (1 - s) * (innerHeight + el.offsetHeight) / 2;
gsap.fromTo(el, { y: () => -weg() }, {
y: () => weg(), ease: 'none',
scrollTrigger: { trigger: el, ...BEREICH, scrub: true, invalidateOnRefresh: true }
});
});
// 3) Text-Parallax: zwei Zeilen laufen beim Senkrechtscrollen gegeneinander
if (variante === 'schrift') {
const [a, b] = document.querySelectorAll('.band p');
const st = { trigger: '.band', ...BEREICH, scrub: true };
gsap.fromTo(a, { xPercent: 0 }, { xPercent: -BAND, ease: 'none', scrollTrigger: st });
gsap.fromTo(b, { xPercent: -BAND }, { xPercent: 0, ease: 'none', scrollTrigger: { ...st } });
}
});
ScrollTrigger.refresh();
}
knoepfe.forEach(b => b.addEventListener('click', () => { variante = b.dataset.variante; bauen(); }));
// Schalter „reduziert“ der Detailseite oder Systemeinstellung zur Laufzeit
new MutationObserver(() => { if (reduziert() !== !!anzeige.dataset.r) { anzeige.dataset.r = reduziert() ? '1' : ''; bauen(); } })
.observe(html, { attributes: true, attributeFilter: ['class'] });
addEventListener('DOMContentLoaded', () => { anzeige.dataset.r = reduziert() ? '1' : ''; bauen(); });
addEventListener('load', () => ScrollTrigger.refresh());
// Vorführung: die Seite scrollt langsam hinunter und wieder hinauf (Pendel), Variante wechselt je Runde
let uhr = null, t0 = 0, runde = -1;
const RUNDE = 16000, FOLGE = ['ebenen', 'schrift'];
function vorfuehren() {
pause(); scrollTo(0, 0); t0 = performance.now(); runde = -1;
const schritt = t => {
const n = Math.floor((t - t0) / RUNDE), p = ((t - t0) % RUNDE) / RUNDE;
if (n !== runde && !reduziert()) { runde = n; variante = FOLGE[n % FOLGE.length]; bauen(); }
const max = html.scrollHeight - innerHeight;
scrollTo(0, max * 0.82 * (0.5 - 0.5 * Math.cos(p * Math.PI * 2)));
uhr = requestAnimationFrame(schritt);
};
uhr = requestAnimationFrame(schritt);
}
function pause() { cancelAnimationFrame(uhr); uhr = null; }
function neustart() { pause(); scrollTo(0, 0); bauen(); }
})();
</script>
</body>
</html>