Gebrochenes / asymmetrisches Raster
Ein Studio-Hero auf 24 Spalten, das zwischen versetzten Bildpaaren, Titel-über-Foto und Kreis als Drehpunkt umbaut, mit Parallaxe in drei Tempi und einem Bild, das in die nächste Sektion bricht.
Grundlage ist ein strenges Raster mit 12 bis 24 Spalten. Die Elemente starten bewusst auf verschiedenen Spalten: Bild über Spalte 2 bis 6, Text über 8 bis 11, das nächste Bild über 5 bis 12. Es gibt keine Mittelachse. An wenigen, geplanten Stellen brechen Elemente aus: Typo liegt auf dem Bild, ein Bild ragt in die Nachbarsektion, große Flächen bleiben leer. Beim Scrollen laufen die Ebenen leicht unterschiedlich schnell und verstärken den Versatz.
Der kontrollierte Regelbruch erzeugt Spannung. Die Seite wirkt kuratiert und von Hand gesetzt statt aus Komponenten zusammengesteckt. Damit ist sie eine Gegenbewegung zu polierten Bento-Seiten.
Wann einsetzen
Portfolios, Mode, Kunst, Kreativstudios, Kampagnen und Startseiten mit wenigen starken Bildern.
Wann nicht
Komplexe Informationsseiten, Formulare und Shops mit vielen Produkten. Ohne festes Grundraster wird das Layout chaotisch.
Varianten
- Versetzte Bildpaare in unterschiedlichen Formaten
- Überlappung: Der Titel liegt halb auf dem Foto
- Geometrische Form als Drehpunkt, um den sich Bilder und Typo ordnen
- Anti-Grid-Brutalismus: rau und absichtlich roh
Umsetzung
Ein feines Raster mit 24 Spalten bietet viele Startpunkte und bleibt trotzdem geordnet. Platzierung per grid-column und grid-row bzw. grid-area. Überlappungen entstehen durch gemeinsame Zellen und z-index, nicht durch absolute Positionierung. Ausbrüche in die Nachbarsektion per negativem margin-block oder translate. Parallaxe sparsam mit 2 bis 3 Geschwindigkeiten (Faktoren 0.9 bis 1.15, scrub, linear) und auf einer inneren Ebene, damit Layout und Bewegung getrennt bleiben. Regeln festhalten: wo gebrochen werden darf, höchstens 1 bis 2 Brüche pro Ansicht.
Robust umsetzen
Telefon. Die Asymmetrie über Breitenwechsel erhalten (100 %, 70 % links, 70 % rechts) statt alles auf volle Breite zu setzen. Überlappungen reduzieren, Parallaxe weglassen.
Ohne Bewegung. Parallaxe und Übergänge aus. Das Layout bleibt asymmetrisch.
Barrierefrei. Die DOM-Reihenfolge muss der Lesereihenfolge entsprechen, auch wenn die Elemente visuell versetzt sind. Text nur dort über Bilder legen, wo die Bildfläche ruhig und kontrastreich genug ist.
Code
<!DOCTYPE html>
<!--
Gebrochenes / asymmetrisches Raster
Fundus · Hero & Section-Layouts · https://cecodesign.de/lab/fundus/hero-layouts/broken-grid-asymmetrisch/
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>Gebrochenes / asymmetrisches Raster · 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'; 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; }
@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; }
:root {
--papier: #ebe9e3; --tinte: #151513; --leise: rgba(21, 21, 19, .66); --linie: rgba(21, 21, 19, .14);
--nacht: #151513; --hell: #efede7; --hell-leise: rgba(239, 237, 231, .56);
--akzent: #d8432a;
--kurve: cubic-bezier(0.16, 1, 0.3, 1); /* expo.out, nur für den Layoutwechsel */
--spalten: 24; --rinne: 12px; --rand: clamp(16px, 2.8vw, 40px);
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--papier); color: var(--tinte); font-family: 'Instrument Sans', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
img { display: block; max-width: 100%; }
.mono { font: 500 11px/1.35 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
/* ---------- Kopf ---------- */
.held { height: max(100svh, 640px); display: grid; grid-template-rows: auto minmax(0, 1fr) auto; padding: 0 var(--rand); }
.kopf { display: flex; justify-content: space-between; align-items: center; gap: 12px 24px; flex-wrap: wrap; padding: 18px 0 14px; border-bottom: 1px solid var(--linie); }
.marke { display: flex; align-items: baseline; gap: 14px; }
.marke b { font-size: 17px; font-weight: 600; letter-spacing: -.02em; color: var(--tinte); }
.regler { display: flex; gap: 4px; flex-wrap: wrap; }
.regler 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: color .15s, background-color .15s, border-color .15s; }
.regler button:hover { color: var(--tinte); border-color: rgba(21, 21, 19, .34); }
.regler button[aria-pressed='true'] { background: var(--tinte); color: var(--papier); border-color: var(--tinte); }
.regler .netzknopf { margin-left: 8px; }
.regler .netzknopf[aria-pressed='true'] { background: var(--akzent); border-color: var(--akzent); color: #fff; }
/* ---------- Die Bühne: 24 Spalten × 12 Zeilen ---------- */
.buehne {
position: relative; display: grid;
grid-template-columns: repeat(var(--spalten), minmax(0, 1fr));
grid-template-rows: repeat(12, minmax(0, 1fr));
column-gap: var(--rinne); padding: clamp(18px, 3vh, 32px) 0;
}
.feld { position: relative; min-width: 0; min-height: 0; z-index: 1; }
.ebene { height: 100%; }
/* Hilfsnetz: dieselben Spalten per subgrid, liegt hinter allem */
.netz { grid-area: 1 / 1 / -1 / -1; display: grid; grid-template-columns: subgrid; z-index: 0; pointer-events: none; opacity: 0; transition: opacity .6s var(--kurve); margin-block: calc(-1 * clamp(18px, 3vh, 32px)); }
.netz span { background: rgba(216, 67, 42, .075); border-inline: 1px solid rgba(216, 67, 42, .16); font: 500 9px/1 'Geist Mono', monospace; letter-spacing: .06em; color: var(--akzent); text-align: center; padding-top: 6px; }
.buehne.netz-an .netz, .buehne.netz-blitz .netz { opacity: 1; }
.f-label { align-self: start; }
.f-titel { z-index: 3; display: flex; }
.titel { font-size: clamp(40px, min(7.1vw, 10.6vh), 136px); font-weight: 600; line-height: .9; letter-spacing: -.047em; align-self: var(--titel-y, start); }
.titel em { font-family: 'Instrument Serif', Georgia, serif; font-weight: 400; letter-spacing: -.02em; color: var(--akzent); padding-right: .05em; }
.f-text { z-index: 2; }
.f-text p { font-size: clamp(14px, 1.15vw, 17px); line-height: 1.5; color: var(--leise); max-width: 34ch; }
.f-text p b { color: var(--tinte); font-weight: 500; }
/* Bilder: Bild füllt die Zelle, Bildzeile darunter */
.bild .ebene { display: grid; grid-template-rows: minmax(0, 1fr) auto; gap: 8px; }
.bild .rahmen { overflow: hidden; background: #d9d6ce; }
.bild img { width: 100%; height: 100%; object-fit: cover; transition: scale 1s var(--kurve); }
.bild figcaption { display: flex; justify-content: space-between; gap: 10px; font-size: 10px; white-space: nowrap; overflow: hidden; }
.bild figcaption b { color: var(--tinte); font-weight: 500; overflow: hidden; text-overflow: ellipsis; }
.lage { color: var(--akzent); display: none; }
.buehne.netz-an .lage, .buehne.netz-blitz .lage, .bild:hover .lage { display: inline; }
@media (hover: hover) { .bild:hover img { scale: 1.035; } }
/* Kreis als Drehpunkt: bleibt rund, egal wie die Zelle geschnitten ist */
.f-kreis { container-type: size; z-index: 2; }
.kreis { width: min(100cqw, 100cqh); aspect-ratio: 1; border-radius: 50%; background: var(--akzent); margin: auto; position: absolute; inset: 0; }
/* Variante 1: versetzte Bildpaare, keine Mittelachse, viel Leerraum */
[data-variante='versetzt'] .f-label { grid-area: 1 / 2 / 2 / 16; }
[data-variante='versetzt'] .f-titel { grid-area: 2 / 2 / 6 / 16; }
[data-variante='versetzt'] .f-text { grid-area: 2 / 18 / 5 / 24; }
[data-variante='versetzt'] .f-a { grid-area: 6 / 4 / 13 / 9; }
[data-variante='versetzt'] .f-b { grid-area: 5 / 11 / 11 / 21; }
[data-variante='versetzt'] .f-c { grid-area: 10 / 22 / 13 / 25; }
[data-variante='versetzt'] .f-kreis { grid-area: 11 / 11 / 12 / 12; }
/* Variante 2: Titel liegt halb auf dem Foto (helle Bildfläche, Kontrast bleibt) */
[data-variante='ueberlappend'] { --titel-y: end; }
[data-variante='ueberlappend'] .f-label { grid-area: 1 / 2 / 2 / 9; } /* endet vor dem Foto, darf umbrechen */
[data-variante='ueberlappend'] .f-b { grid-area: 1 / 9 / 9 / 24; }
[data-variante='ueberlappend'] .f-titel { grid-area: 7 / 2 / 11 / 17; }
[data-variante='ueberlappend'] .f-text { grid-area: 10 / 18 / 13 / 24; }
[data-variante='ueberlappend'] .f-a { grid-area: 2 / 3 / 6 / 7; }
[data-variante='ueberlappend'] .f-c { grid-area: 10 / 11 / 13 / 16; }
[data-variante='ueberlappend'] .f-kreis { grid-area: 8 / 22 / 10 / 24; }
[data-variante='ueberlappend'] .f-b figcaption { visibility: hidden; } /* Titel übernimmt die Bildzeile */
/* Variante 3: Kreis als Drehpunkt, Titel kreuzt ihn */
[data-variante='drehpunkt'] { --titel-y: center; }
[data-variante='drehpunkt'] .titel em { color: var(--tinte); } /* nie Rot auf Rot */
[data-variante='drehpunkt'] .f-label { grid-area: 1 / 2 / 2 / 16; }
[data-variante='drehpunkt'] .f-kreis { grid-area: 2 / 9 / 11 / 18; }
[data-variante='drehpunkt'] .f-titel { grid-area: 4 / 5 / 8 / 23; }
[data-variante='drehpunkt'] .f-text { grid-area: 11 / 12 / 13 / 18; }
[data-variante='drehpunkt'] .f-a { grid-area: 1 / 19 / 5 / 24; }
[data-variante='drehpunkt'] .f-b { grid-area: 8 / 2 / 13 / 9; }
[data-variante='drehpunkt'] .f-c { grid-area: 8 / 20 / 11 / 25; }
/* Fußzeile der Bühne: die verwendeten Werte */
.fuss { position: relative; z-index: 4; background: var(--papier); display: flex; flex-wrap: wrap; align-content: start; gap: 6px 18px; padding: 14px 40% 22px 0; min-height: 78px; border-top: 1px solid var(--linie); } /* liegt über der Bühne: langsame Bilder gleiten darunter statt in die Werte */ /* rechts frei: dort bricht das nächste Bild herein */
.fuss b { color: var(--tinte); font-weight: 500; }
/* ---------- Zweite Sektion: Arbeiten, mit Ausbruch über die Kante ---------- */
.arbeiten { background: var(--nacht); color: var(--hell); padding: 0 var(--rand) 18vh; display: grid; grid-template-columns: repeat(24, minmax(0, 1fr)); column-gap: var(--rinne); align-items: start; }
.arbeiten .mono { color: var(--hell-leise); }
.arbeiten .bild figcaption b { color: var(--hell); }
.arbeiten .bild .rahmen { background: #24231f; }
.a-label { grid-column: 2 / 10; grid-row: 1; padding-top: 48px; }
.a-plakat { grid-column: 14 / 23; grid-row: 1 / 3; margin-top: -104px; z-index: 5; } /* ragt in den Kopf hinein, über die Werte-Zeile */
.a-satz { grid-column: 2 / 13; grid-row: 2; margin-top: 64px; font-size: clamp(30px, 3.9vw, 62px); font-weight: 500; line-height: 1.02; letter-spacing: -.035em; }
.a-satz em { font-family: 'Instrument Serif', Georgia, serif; font-weight: 400; letter-spacing: -.01em; color: var(--akzent); }
.a-flakon { grid-column: 4 / 10; grid-row: 3 / 5; margin-top: 12vh; }
.a-buch { grid-column: 12 / 21; grid-row: 3; margin-top: 22vh; }
.a-text { grid-column: 15 / 21; grid-row: 4; margin-top: 56px; }
.a-text p { font-size: 16px; line-height: 1.5; color: var(--hell-leise); max-width: 36ch; }
.a-text a { display: inline-block; margin-top: 22px; color: var(--hell); text-decoration: none; border-bottom: 1px solid var(--akzent); padding-bottom: 4px; }
.a-brot { grid-column: 20 / 25; grid-row: 5; margin-top: 10vh; }
.arbeiten .bild .ebene { height: auto; }
.a-plakat .rahmen { aspect-ratio: 3 / 2; }
.a-flakon .rahmen { aspect-ratio: 4 / 5; }
.a-buch .rahmen { aspect-ratio: 3 / 2; }
.a-brot .rahmen { aspect-ratio: 4 / 5; }
/* ---------- Telefon: 8 Spalten, Zeilen à 40 px ---------- */
@media (max-width: 760px) {
:root { --spalten: 8; --rinne: 8px; }
.held { height: auto; }
.fuss { padding: 14px 0 72px; }
.buehne { grid-template-rows: repeat(22, 40px); padding: 20px 0 28px; }
.netz { margin-block: -20px -28px; }
.netz span:nth-child(n+9) { display: none; }
.titel { font-size: clamp(38px, 12.4vw, 56px); }
.f-text p { font-size: 15px; }
.regler .netzknopf { margin-left: 0; }
.bild figcaption .zusatz { display: none; }
[data-variante] .f-label { grid-area: 1 / 1 / 2 / 9; }
[data-variante='versetzt'] .f-titel { grid-area: 2 / 1 / 6 / 9; }
[data-variante='versetzt'] .f-text { grid-area: 6 / 3 / 9 / 9; }
[data-variante='versetzt'] .f-a { grid-area: 9 / 1 / 16 / 6; }
[data-variante='versetzt'] .f-c { grid-area: 11 / 6 / 14 / 9; }
[data-variante='versetzt'] .f-kreis { grid-area: 14 / 7 / 15 / 8; }
[data-variante='versetzt'] .f-b { grid-area: 16 / 3 / 22 / 9; }
[data-variante='ueberlappend'] .f-b { grid-area: 2 / 3 / 10 / 9; }
[data-variante='ueberlappend'] .f-titel { grid-area: 7 / 1 / 11 / 9; }
[data-variante='ueberlappend'] .f-text { grid-area: 11 / 2 / 15 / 9; align-self: center; }
[data-variante='ueberlappend'] .f-a { grid-area: 15 / 1 / 22 / 5; }
[data-variante='ueberlappend'] .f-c { grid-area: 18 / 5 / 22 / 9; }
[data-variante='ueberlappend'] .f-kreis { grid-area: 16 / 6 / 17 / 7; }
[data-variante='drehpunkt'] .f-kreis { grid-area: 2 / 2 / 9 / 8; }
[data-variante='drehpunkt'] .f-titel { grid-area: 3 / 1 / 8 / 9; }
[data-variante='drehpunkt'] .f-text { grid-area: 9 / 1 / 13 / 6; }
[data-variante='drehpunkt'] .f-a { grid-area: 9 / 6 / 15 / 9; }
[data-variante='drehpunkt'] .f-b { grid-area: 14 / 1 / 19 / 6; }
[data-variante='drehpunkt'] .f-c { grid-area: 17 / 6 / 21 / 9; }
.arbeiten { grid-template-columns: repeat(8, minmax(0, 1fr)); padding-bottom: 72px; }
.a-label { grid-column: 1 / 9; grid-row: 2; padding-top: 32px; }
.a-plakat { grid-column: 1 / 9; grid-row: 1; margin-top: -56px; } /* 100 % */
.a-satz { grid-column: 1 / 9; grid-row: 3; margin-top: 24px; font-size: 32px; }
.a-flakon { grid-column: 1 / 7; grid-row: 4; margin-top: 48px; } /* 70 % links */
.a-buch { grid-column: 3 / 9; grid-row: 5; margin-top: 40px; } /* 70 % rechts */
.a-text { grid-column: 1 / 8; grid-row: 6; margin-top: 40px; }
.a-brot { grid-column: 4 / 9; grid-row: 7; margin-top: 40px; }
}
/* 320 px: Bildzeilen ohne Zusatz, damit nichts mit Auslassungspunkten endet */
@media (max-width: 380px) { .bild figcaption .zusatz-schmal { display: none; } }
/* Reduzierte Bewegung: keine Parallaxe, kein Gleiten, Layout bleibt asymmetrisch */
.reduziert .bild img { transition: none; }
.reduziert .netz { transition: opacity .2s linear; }
/* Galerie-Vorschau: nur die Bühne */
.vorschau body { overflow: hidden; }
.vorschau .arbeiten { display: none; }
.vorschau .fuss { padding-right: 0; min-height: 0; }
/* Kontrastmodus: Knöpfe mit Rahmen */
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }
</style>
</head>
<body>
<header class="held">
<div class="kopf">
<div class="marke"><b>Studio Halde</b><span class="mono">Marken & Editorial · Köln</span></div>
<div class="regler mono" role="group" aria-label="Raster-Variante">
<button type="button" data-variante-wahl="versetzt" aria-pressed="true">Versetzt</button>
<button type="button" data-variante-wahl="ueberlappend" aria-pressed="false">Überlappend</button>
<button type="button" data-variante-wahl="drehpunkt" aria-pressed="false">Drehpunkt</button>
<button type="button" class="netzknopf" data-netz aria-pressed="false">Raster zeigen</button>
</div>
</div>
<div class="buehne" data-buehne data-variante="versetzt">
<div class="netz" aria-hidden="true"></div>
<p class="feld f-label mono">Büro für Markengestaltung · seit 2014</p>
<div class="feld f-titel"><h1 class="titel">Marken, von <em>Hand</em> gesetzt.</h1></div>
<div class="feld f-text"><p><b>Wir gestalten Erscheinungsbilder</b> für Cafés, Handwerk und Kulturhäuser. Auf einem strengen Raster, das wir an genau zwei Stellen brechen.</p></div>
<figure class="feld bild f-a" data-tempo="0.9">
<div class="ebene"><div class="rahmen"><img src="bilder/magazin-doppelseite-hoch.jpg" alt="Aufgeschlagenes Magazin mit der Überschrift „Die Ruhe der Dinge“ und zweispaltigem Text" width="800" height="1000"></div>
<figcaption class="mono"><b>Hausheft</b><span class="lage" data-lage></span></figcaption></div>
</figure>
<figure class="feld bild f-b" data-tempo="1">
<div class="ebene"><div class="rahmen"><img src="bilder/marke-roestkontor.jpg" alt="Wortbildmarke Röstkontor: Kaffeebohne im Kreis über cremefarbenen Versalien auf Ziegelrot" width="1200" height="800"></div>
<figcaption class="mono"><b>Röstkontor <span class="zusatz">· Marke 2023</span></b><span class="lage" data-lage></span></figcaption></div>
</figure>
<figure class="feld bild f-c" data-tempo="1.15">
<div class="ebene"><div class="rahmen"><img src="bilder/keramik-stillleben.jpg" alt="Keramik-Stillleben: helle Flasche, schwarze Kugelvase, gelber Becher und Schalen auf Sockeln vor Terrakotta" width="1200" height="800"></div>
<figcaption class="mono"><b>Töpferei</b><span class="lage" data-lage></span></figcaption></div>
</figure>
<div class="feld f-kreis" data-tempo="1.15" aria-hidden="true"><div class="ebene"><span class="kreis"></span></div></div>
</div>
<p class="fuss mono" data-werte aria-live="polite"></p>
</header>
<main class="arbeiten">
<p class="a-label mono">Ausgewählte Arbeiten · 04</p>
<figure class="bild a-plakat" data-tempo="1.15">
<div class="ebene"><div class="rahmen"><img src="bilder/typo-plakat.jpg" alt="Plakat in Neongelb: riesige schwarze Versalien LICHT, oranger Kreis und kursive Zeile „und was es übrig lässt“" width="1200" height="800" loading="lazy"></div>
<figcaption class="mono"><b>Werkschau Licht · Plakat</b><span>Sp. 14–22</span></figcaption></div>
</figure>
<h2 class="a-satz">Ein strenges Raster. <em>Zwei</em> kalkulierte Brüche pro Ansicht.</h2>
<figure class="bild a-flakon" data-tempo="0.9">
<div class="ebene"><div class="rahmen"><img src="bilder/flakon-hoch.jpg" alt="Bernsteinfarbener Parfumflakon auf weißem Podest vor kräftigem Kobaltblau" width="800" height="1000" loading="lazy"></div>
<figcaption class="mono"><b>Ambrette · Packaging</b><span>Sp. 4–9</span></figcaption></div>
</figure>
<figure class="bild a-buch" data-tempo="1">
<div class="ebene"><div class="rahmen"><img src="bilder/marke-eselsohr.jpg" alt="Wortbildmarke Eselsohr: Blatt mit rosa Eselsohr neben kursivem Schriftzug, Buchhandlung und Lesecafé, auf Anthrazit" width="1200" height="800" loading="lazy"></div>
<figcaption class="mono"><b>Eselsohr<span class="zusatz-schmal"> · Buchhandlung</span></b><span>Sp. 12–20</span></figcaption></div>
</figure>
<div class="a-text">
<p>Jedes Bild beginnt auf einer anderen Spalte. Die Ordnung bleibt, nur die Mittelachse fehlt. So liest sich die Seite wie ein gesetztes Magazin und nicht wie ein Baukasten.</p>
<a class="mono" href="#">Projekt anfragen</a>
</div>
<figure class="bild a-brot" data-tempo="1.15">
<div class="ebene"><div class="rahmen"><img src="bilder/marke-krume-hoch.jpg" alt="Wortmarke krume in fetten dunkelbraunen Kleinbuchstaben unter einem Brotlaib auf Senfgelb" width="800" height="1000" loading="lazy"></div>
<figcaption class="mono"><b>Krume<span class="zusatz-schmal"> · Backstube</span></b><span>Sp. 20–24</span></figcaption></div>
</figure>
</main>
<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 src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/Flip.min.js"></script>
<script>
(() => {
gsap.registerPlugin(ScrollTrigger, Flip);
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const buehne = document.querySelector('[data-buehne]');
const felder = [...buehne.querySelectorAll('.feld')];
const werte = document.querySelector('[data-werte]');
const netzKnopf = document.querySelector('[data-netz]');
// Wechsel zwischen den Varianten: GSAP Flip, eine Kurve für alles
const WECHSEL = { dauer: 1.2, versatz: 0.04, ease: 'expo.out' };
const PARALLAXE = [0.9, 1, 1.15]; // Faktoren aus den Messwerten, Scrub linear
const TEXTE = {
versetzt: 'Bildpaar <b>5 + 10 Sp.</b>, Versatz <b>2 Sp. / 1 Z.</b>',
ueberlappend: 'Titel überlappt das Foto auf <b>8 Sp. × 2 Z.</b>',
drehpunkt: 'Kreis <b>9 Sp.</b> als Drehpunkt, Titel kreuzt ihn',
};
let variante = 'versetzt';
// Netz: 24 Spalten mit Nummern (auf dem Telefon 8)
const netz = buehne.querySelector('.netz');
netz.innerHTML = Array.from({ length: 24 }, (_, i) => `<span>${i + 1}</span>`).join('');
// Tatsächliche Lage jedes Bildes aus dem berechneten Stil lesen
function lageZeigen() {
buehne.querySelectorAll('.bild').forEach(f => {
const s = getComputedStyle(f);
f.querySelector('[data-lage]').textContent = `Sp. ${s.gridColumnStart}–${+s.gridColumnEnd - 1}`;
});
}
function werteZeigen() {
const spalten = getComputedStyle(html).getPropertyValue('--spalten').trim();
werte.innerHTML = `<span>Raster <b>${spalten} Sp.</b>, Rinne <b>${getComputedStyle(html).getPropertyValue('--rinne').trim()}</b></span><span>${TEXTE[variante]}</span>`
+ (reduziert()
? '<span>Reduziert: <b>Parallaxe aus, Wechsel 300 ms Einblenden</b></span>'
: `<span>Wechsel <b>Flip ${WECHSEL.ease} ${WECHSEL.dauer * 1000} ms</b>, Versatz <b>${WECHSEL.versatz * 1000} ms</b></span><span>Parallaxe <b>${PARALLAXE.join(' / ')}</b>, scrub linear</span>`);
}
function knoepfe() {
document.querySelectorAll('[data-variante-wahl]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.varianteWahl === variante)));
}
function setzen(neu, animiert = true) {
if (neu === variante && animiert) return;
gsap.killTweensOf(felder);
if (!animiert || reduziert()) {
variante = neu; buehne.dataset.variante = neu;
lageZeigen();
if (animiert) gsap.fromTo(felder, { opacity: 0 }, { opacity: 1, duration: 0.3, ease: 'none' });
else gsap.set(felder, { clearProps: 'opacity,transform' });
} else {
const zustand = Flip.getState(felder);
variante = neu; buehne.dataset.variante = neu;
lageZeigen(); // vor Flip.from lesen: absolute setzt die Rasterlage kurz außer Kraft
Flip.from(zustand, { duration: WECHSEL.dauer, ease: WECHSEL.ease, stagger: WECHSEL.versatz, absolute: true });
}
knoepfe(); werteZeigen();
}
function einstieg() {
if (reduziert()) { gsap.fromTo(felder, { opacity: 0 }, { opacity: 1, duration: 0.3, ease: 'none' }); return; }
gsap.fromTo(felder, { opacity: 0, y: 48 }, { opacity: 1, y: 0, duration: WECHSEL.dauer, ease: WECHSEL.ease, stagger: 0.06, clearProps: 'transform' });
}
// Parallaxe: drei Geschwindigkeiten auf der inneren Ebene (Flip bewegt die äußere Zelle)
const mm = gsap.matchMedia();
function parallaxe() {
mm.revert();
if (reduziert()) return;
mm.add('(min-width: 761px)', () => {
buehne.querySelectorAll('[data-tempo]').forEach(f => {
const t = +f.dataset.tempo;
if (t === 1) return;
gsap.to(f.querySelector('.ebene'), { y: () => -(t - 1) * buehne.offsetHeight, ease: 'none',
scrollTrigger: { trigger: '.held', start: 'top top', end: 'bottom top', scrub: true, invalidateOnRefresh: true } });
});
document.querySelectorAll('.arbeiten [data-tempo]').forEach(f => {
const t = +f.dataset.tempo;
if (t === 1) return;
const weg = () => (t - 1) * (innerHeight + f.offsetHeight) / 2;
gsap.fromTo(f.querySelector('.ebene'), { y: () => weg() }, { y: () => -weg(), ease: 'none',
scrollTrigger: { trigger: f, start: 'top bottom', end: 'bottom top', scrub: true, invalidateOnRefresh: true } });
});
});
}
document.querySelectorAll('[data-variante-wahl]').forEach(b => b.addEventListener('click', () => { pause(); setzen(b.dataset.varianteWahl); }));
netzKnopf.addEventListener('click', () => {
const an = !buehne.classList.contains('netz-an');
buehne.classList.toggle('netz-an', an);
netzKnopf.setAttribute('aria-pressed', String(an));
});
matchMedia('(max-width: 760px)').addEventListener('change', () => { lageZeigen(); werteZeigen(); });
// Vorführung: Varianten im Wechsel, das Netz blitzt bei jedem Umbau auf
const REIHE = ['versetzt', 'ueberlappend', 'drehpunkt'];
let uhr = null, blitz = null;
function vorfuehren() {
pause();
scrollTo(0, 0);
setzen('versetzt', false);
einstieg();
let i = 0;
const takt = (warte) => {
uhr = setTimeout(() => {
i = (i + 1) % REIHE.length;
buehne.classList.add('netz-blitz');
clearTimeout(blitz);
blitz = setTimeout(() => buehne.classList.remove('netz-blitz'), 1900);
setzen(REIHE[i]);
takt(3600);
}, warte);
};
takt(2200);
}
function pause() { clearTimeout(uhr); uhr = null; }
function neustart() { pause(); scrollTo(0, 0); setzen('versetzt', false); einstieg(); }
document.fonts.ready.then(() => { setzen(variante, false); einstieg(); parallaxe(); ScrollTrigger.refresh(); });
new MutationObserver(() => { werteZeigen(); parallaxe(); }).observe(html, { attributes: true, attributeFilter: ['class'] });
})();
</script>
</body>
</html>