Kinetisches Typo-Intro (Wortwiederholung, Kreistext)
Der Festivalname wechselt, stapelt oder dreht sich bildfüllend, bis er als einzelnes Wort zur Headline der Seite wird.
Ein Wort oder Markenname füllt die Fläche. Es wechselt im schnellen Takt, verdoppelt sich in Reihen oder dreht sich als Kreistext. Die Bewegung beschleunigt bis zu einem Höhepunkt. Dann fallen die Wiederholungen auf ein einzelnes Wort zusammen, das zur Headline wird, oder die Fläche öffnet sich zur Seite.
Laut, rhythmisch und plakativ: Typo wird zum Bild. Am stärksten bei Marken mit kurzem, prägnantem Namen.
Wann einsetzen
Bei Event-, Festival-, Musik-, Mode- und Kampagnenseiten, und nur auf der Startseite.
Wann nicht
Bei ruhigen, seriösen Auftritten, bei langen Namen und auf Seiten, deren Besucher sofort Termine oder Öffnungszeiten brauchen.
Varianten
- Wortwechsel: neun Wörter im Takt von 150 ms, danach fährt das Panel weg (Dennis Snellenberg)
- Stapel aus Outline-Typo, der zu einem vollen Wort zusammenfällt
- Kreisförmiger SVG-Text, der beschleunigt und sich öffnet
Umsetzung
Wortwechsel: eine Timeline tauscht den Text alle 150 ms, danach fährt das Panel in 800 ms mit power4.inOut nach oben und der Inhalt kommt mit expo.out und 50 ms Versatz aus 50vh nach. Stapel: das Wort N-mal klonen (aria-hidden), absolut stapeln, per Timeline mit y-Versätzen und Stagger auseinanderziehen, laufen lassen und wieder zusammenführen; die Headline per FLIP von der Mitte an ihren Platz fliegen lassen. Kreistext: <svg><path id=… d="Kreisbogen"/><text><textPath href=…>…</textPath></text></svg> mit Rotations-Tween, Öffnung über eine radiale mask-image. Viele große Textknoten kosten Paint-Zeit: will-change: transform, keine text-shadow. Nur auf der Startseite zeigen, Unterseiten bekommen höchstens ein kurzes Intro.
Robust umsetzen
Telefon. Weniger Klone. Schriftgröße per clamp() oder Container-Einheiten, kürzerer Weg für den Inhalt (10vh).
Ohne Bewegung. Nur die finale Headline.
Barrierefrei. Klone und Intro aria-hidden, lesbar ist nur das finale Wort als h1. Große Farbflächen nicht im Takt wechseln (Blitzgefahr), nur das Wort. Überspringen per Knopf und Esc, Fokus danach auf die Headline.
Code
<!DOCTYPE html>
<!--
Kinetisches Typo-Intro (Wortwiederholung, Kreistext)
Fundus · Preloader & Intro · https://cecodesign.de/lab/fundus/preloader-intro/kinetische-typo-intro/
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>quer · Kinetisches Typo-Intro · 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: 'Schibsted Grotesk'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/schibsted-grotesk:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 400 900; font-display: swap; }
@font-face { font-family: 'Atkinson Hyperlegible Next'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/atkinson-hyperlegible-next:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-display: swap; }
@font-face { font-family: 'IBM Plex Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/ibm-plex-mono@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'IBM Plex Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/ibm-plex-mono@latest/latin-500-normal.woff2') format('woff2'); font-weight: 500; font-display: swap; }
:root {
--text: #0b0b0b; --grund: #efede9; --leise: #55524c; --linie: #0b0b0b;
--musik: #bc3f19; --theater: #3d5f9a; --kunst: #2d7847; --film: #b0186b;
--ui: cubic-bezier(0.215, 0.61, 0.355, 1); /* Rezept: easeOutCubic, UI 200 ms */
--rand: clamp(16px, 3vw, 44px);
--kopf: 78px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--text); font-family: 'Atkinson Hyperlegible Next', system-ui, sans-serif; font-size: 16px; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; overflow-x: clip; }
body.intro-laeuft { overflow: hidden; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; }
.mono { font: 500 13px/1.4 'IBM Plex Mono', ui-monospace, monospace; letter-spacing: .02em; font-variant-numeric: tabular-nums; }
.label { font: 500 12px/1.3 'IBM Plex Mono', ui-monospace, monospace; letter-spacing: .08em; text-transform: uppercase; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.disp { font-family: 'Schibsted Grotesk', 'Arial Black', sans-serif; font-weight: 900; letter-spacing: -0.04em; }
:focus-visible { outline: 3px solid var(--theater); outline-offset: 2px; }
/* ---------- Seite ---------- */
.kopf { position: sticky; top: 0; z-index: 10; height: var(--kopf); display: flex; align-items: center; gap: 28px; padding: 0 var(--rand); background: var(--grund); border-bottom: 1px solid var(--linie); }
.marke { display: inline-flex; align-items: baseline; gap: 10px; }
.marke b { font: 900 30px/1 'Schibsted Grotesk', sans-serif; letter-spacing: -0.045em; }
.marke span { color: var(--leise); }
.nav { display: flex; gap: 22px; font-size: 17px; }
.nav a { padding: 6px 0; background: linear-gradient(currentColor, currentColor) 0 100% / 0 2px no-repeat; transition: background-size .2s var(--ui); }
.nav a:hover { background-size: 100% 2px; }
.kopf__rechts { margin-left: auto; display: flex; align-items: center; gap: 18px; }
.zugang { display: flex; gap: 14px; color: var(--leise); font-size: 15px; }
.zugang a { padding-block: 3px; } /* Zielhöhe ≥ 24 px */
.zugang a:hover { color: var(--text); }
.ticket { display: inline-flex; align-items: center; gap: 10px; height: 44px; padding: 0 18px; background: var(--text); color: var(--grund); font-weight: 700; font-size: 16px; transition: background-color .2s var(--ui); }
.ticket:hover { background: var(--musik); }
.held { display: grid; grid-template-columns: minmax(0, 8fr) minmax(0, 4fr); gap: 24px clamp(24px, 4vw, 64px); align-items: end; padding: clamp(18px, 4vh, 44px) var(--rand) clamp(22px, 4vh, 44px); min-height: calc(100svh - var(--kopf) - 120px); border-bottom: 1px solid var(--linie); }
.titel-box { container-type: inline-size; min-width: 0; }
.riesig { font-size: min(44cqi, 56vh); line-height: .8; padding-bottom: .17em; margin-left: -.05em; }
.held__info { display: grid; gap: 18px; padding-bottom: .4em; }
.held__info .unter { font: 800 clamp(1.5rem, 2.4vw, 2.4rem)/1.05 'Schibsted Grotesk', sans-serif; letter-spacing: -0.02em; text-wrap: balance; }
.held__info p { font-size: 1.125rem; line-height: 1.6; max-width: 40ch; }
.held__info .daten { display: flex; flex-wrap: wrap; gap: 6px 16px; font-size: 15px; line-height: 1.5; }
.sparten { display: flex; flex-wrap: wrap; gap: 8px; list-style: none; }
.sparten li { display: inline-flex; align-items: center; height: 30px; padding: 0 12px; color: #fff; }
.s-musik { background: var(--musik); } .s-theater { background: var(--theater); } .s-kunst { background: var(--kunst); } .s-film { background: var(--film); }
.ctas { display: flex; flex-wrap: wrap; gap: 10px; }
.knopf { display: inline-flex; align-items: center; height: 48px; padding: 0 20px; border: 2px solid var(--text); font-weight: 700; font-size: 17px; transition: background-color .2s var(--ui), color .2s var(--ui), border-color .2s var(--ui); }
.knopf--voll { background: var(--text); color: var(--grund); }
.knopf:hover { background: var(--musik); border-color: var(--musik); color: #fff; }
.programm { padding: clamp(24px, 5vh, 56px) var(--rand) 120px; }
.programm__kopf { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; margin-bottom: 20px; }
.programm__kopf h2 { font: 800 clamp(1.75rem, 3vw, 2.75rem)/1 'Schibsted Grotesk', sans-serif; letter-spacing: -0.02em; }
.karten { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; list-style: none; }
.karte { display: grid; gap: 10px; align-content: start; border-top: 8px solid var(--farbe); padding-top: 12px; }
.karte .label { color: var(--farbe); }
.karte h3 { font: 800 clamp(1.25rem, 1.8vw, 1.6rem)/1.08 'Schibsted Grotesk', sans-serif; letter-spacing: -0.015em; }
.karte p { font-size: 1.0625rem; line-height: 1.5; color: var(--leise); }
.karte .knopf { justify-self: start; height: 40px; padding: 0 14px; font-size: 15px; }
/* ---------- Intro ---------- */
.intro { position: fixed; inset: 0; z-index: 30; background: var(--text); color: var(--grund); overflow: hidden; display: none; }
.intro.an { display: block; }
.intro.hell { background: var(--grund); color: var(--text); }
.intro__teil { position: absolute; inset: 0; display: none; }
.intro__teil.an { display: block; }
.intro__ecke { position: absolute; left: var(--rand); top: 24px; opacity: .7; }
.intro__skip { position: absolute; z-index: 31; right: var(--rand); top: 18px; height: 34px; padding: 0 12px; border: 1px solid currentColor; background: none; cursor: pointer; opacity: .8; transition: opacity .2s var(--ui); }
.intro__skip:hover { opacity: 1; }
.intro.an:not(.hell) ~ .intro__skip { color: var(--grund); } /* über dem dunklen Intro hell */
/* Variante Wortwechsel */
.wort { position: absolute; inset: 0; display: grid; place-items: center; }
.wort__innen { display: flex; align-items: center; gap: .18em; font-size: clamp(64px, 17vw, 280px); line-height: 1; }
.wort__punkt { width: .16em; height: .16em; border-radius: 50%; background: currentColor; flex: none; transform: translateY(.06em); }
.wort__sparte { position: absolute; left: 50%; bottom: clamp(28px, 9vh, 90px); translate: -50% 0; opacity: .75; white-space: nowrap; }
/* Variante Stapel */
.reihe { position: absolute; left: 0; top: 50%; white-space: nowrap; line-height: .8; color: transparent; -webkit-text-stroke: 2px var(--text); will-change: transform; }
.reihe.mitte { color: var(--text); }
.flieger { position: fixed; z-index: 31; margin: 0; width: max-content; pointer-events: none; transform-origin: 0 0; will-change: transform; visibility: hidden; }
/* Variante Kreis */
.kreis { position: absolute; left: 50%; top: 50%; width: min(86vmin, 760px); aspect-ratio: 1; translate: -50% -50%; }
.kreis svg { width: 100%; height: 100%; overflow: visible; }
.kreis__text { font: 800 54px 'Schibsted Grotesk', sans-serif; letter-spacing: .12em; text-transform: uppercase; fill: var(--grund); }
.kreis__mitte { position: absolute; inset: 0; display: grid; place-items: center; font-size: min(22vmin, 190px); line-height: .8; }
/* ---------- Demo-Steuerung ---------- */
.dock { position: fixed; z-index: 40; left: 50%; bottom: 14px; translate: -50% 0; width: max-content; max-width: calc(100% - 24px); display: grid; gap: 8px; padding: 10px 12px; background: rgba(11, 11, 11, .92); color: var(--grund); }
.dock__zeile { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.modi { display: flex; gap: 4px; }
.modi button, .dock__knopf { height: 32px; padding: 0 12px; border: 1px solid rgba(239, 237, 233, .3); background: none; color: var(--grund); font: 500 12px/1 'IBM Plex Mono', monospace; letter-spacing: .06em; text-transform: uppercase; cursor: pointer; transition: background-color .2s var(--ui), color .2s var(--ui); }
.modi button[aria-pressed='true'] { background: var(--grund); color: var(--text); border-color: var(--grund); }
.modi button:hover, .dock__knopf:hover { border-color: var(--grund); }
.werte { font: 400 10.5px/1.6 'IBM Plex Mono', monospace; color: rgba(239, 237, 233, .7); }
.werte span { display: block; white-space: nowrap; }
.werte b { color: #fff; font-weight: 500; }
/* Reduzierte Bewegung: kein Intro, nur die finale Headline */
:root.reduziert * { transition-duration: .01ms !important; animation-duration: .01ms !important; }
/* Galerie-Vorschau: nur die Bühne */
.vorschau body { overflow: hidden; }
@media (hover: none) {
.modi button, .dock__knopf { height: 38px; }
.intro__skip { height: 40px; }
}
@media (max-width: 1100px) {
.zugang { display: none; }
.karten { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 760px) {
:root { --kopf: 64px; }
.nav { display: none; }
.marke b { font-size: 26px; }
.held { grid-template-columns: minmax(0, 1fr); min-height: 0; align-items: start; }
.riesig { font-size: 44cqi; padding-bottom: .12em; }
.held__info .unter { font-size: 1.5rem; }
.karten { grid-template-columns: minmax(0, 1fr); }
.werte .lang { display: none; }
.werte span { white-space: normal; }
.reihe { -webkit-text-stroke-width: 1.5px; }
}
</style>
</head>
<body class="intro-laeuft">
<header class="kopf">
<a class="marke" href="#" aria-label="quer, Startseite"><b>quer</b><span class="mono">Weimar 2026</span></a>
<nav class="nav" aria-label="Hauptnavigation"><a href="#programm">Programm</a><a href="#">Orte</a><a href="#">Festival</a><a href="#">Presse</a></nav>
<div class="kopf__rechts">
<div class="zugang"><a href="#">Leichte Sprache</a><a href="#">Barrierefrei besuchen</a><a href="#">Vorlesen</a></div>
<a class="ticket" href="#">Tickets</a>
</div>
</header>
<main>
<section class="held" aria-labelledby="titel">
<div class="titel-box"><h1 class="riesig disp" id="titel" data-titel tabindex="-1">quer</h1></div>
<div class="held__info">
<p class="unter" data-rein>Festival für Musik, Tanz, Kunst und Film</p>
<p class="daten mono" data-rein><span>13. bis 22. November 2026</span><span>10 Tage · 46 Veranstaltungen · 9 Orte</span></p>
<p data-rein>Zehn Tage lang geht in Weimar alles quer: Konzerte in der Turnhalle, Tanz im Park, Filme im Gewölbekeller. Viele Veranstaltungen sind frei, alle Orte sind stufenlos erreichbar.</p>
<ul class="sparten label" data-rein aria-label="Sparten"><li class="s-musik">Musik</li><li class="s-theater">Tanz</li><li class="s-kunst">Kunst</li><li class="s-film">Film</li></ul>
<div class="ctas" data-rein><a class="knopf knopf--voll" href="#programm">Programm ansehen</a><a class="knopf" href="#">Tickets ab 9 €</a></div>
</div>
</section>
<section class="programm" id="programm" aria-labelledby="programm-titel">
<div class="programm__kopf" data-rein><h2 id="programm-titel">Die ersten vier Tage</h2><a class="mono" href="#">Ganzes Programm →</a></div>
<ul class="karten" data-rein>
<li class="karte" style="--farbe: var(--musik)"><span class="label">Musik</span><span class="mono">Fr 13.11. · 20:00 · Alte Turnhalle</span><h3>Eröffnung: Querstand</h3><p>Ein Streichorchester und drei Modularsynthesizer spielen gegeneinander an.</p><a class="knopf" href="#">Tickets 18 €</a></li>
<li class="karte" style="--farbe: var(--theater)"><span class="label">Tanz</span><span class="mono">Sa 14.11. · 19:30 · Pavillon im Park</span><h3>Kompanie Fallhöhe: Kipppunkt</h3><p>Sechs Tänzerinnen auf einer schiefen Ebene, 70 Minuten ohne Pause.</p><a class="knopf" href="#">Tickets 14 €</a></li>
<li class="karte" style="--farbe: var(--kunst)"><span class="label">Kunst</span><span class="mono">ab So 15.11. · 11:00 · Alte Feuerwache</span><h3>Quer zum Raster</h3><p>Studierende lesen hundert Weimarer Plakate neu. Eintritt frei.</p><a class="knopf" href="#">Mehr erfahren</a></li>
<li class="karte" style="--farbe: var(--film)"><span class="label">Film</span><span class="mono">Mo 16.11. · 21:00 · Gewölbekeller</span><h3>Kurzfilmnacht unter der Stadt</h3><p>Elf Kurzfilme aus Thüringen, mit Untertiteln und Gespräch.</p><a class="knopf" href="#">Tickets 9 €</a></li>
</ul>
</section>
</main>
<!-- Intro: alles hier ist Bewegung und aria-hidden. Lesbar bleibt nur die Headline der Seite. -->
<div class="intro" data-intro aria-hidden="true">
<span class="intro__ecke label">quer · Weimar · 13.–22.11.2026</span>
<div class="intro__teil" data-teil="wortwechsel">
<div class="wort"><div class="wort__innen disp"><i class="wort__punkt" data-punkt></i><span data-wort>Musik</span></div></div>
<span class="wort__sparte label" data-sparte></span>
</div>
<div class="intro__teil" data-teil="stapel"></div>
<div class="intro__teil" data-teil="kreis">
<div class="kreis"><svg viewBox="-500 -500 1000 1000" aria-hidden="true" data-ring>
<defs><path id="kreisbahn" d="M 0,-390 a 390,390 0 1,1 0,780 a 390,390 0 1,1 0,-780"/></defs>
<text class="kreis__text"><textPath href="#kreisbahn" textLength="2440" lengthAdjust="spacing">Musik · Tanz · Kunst · Film · quer · Weimar ·</textPath></text>
</svg><div class="kreis__mitte disp" data-kreismitte>quer</div></div>
</div>
</div>
<button class="intro__skip label" type="button" data-skip hidden>Überspringen · Esc</button>
<p class="sr" aria-live="polite" data-ansage></p>
<aside class="dock" aria-label="Demo-Steuerung">
<div class="dock__zeile">
<div class="modi" role="group" aria-label="Variante des Intros">
<button type="button" data-variante="wortwechsel" aria-pressed="true">Wortwechsel</button>
<button type="button" data-variante="stapel" aria-pressed="false">Stapel</button>
<button type="button" data-variante="kreis" aria-pressed="false">Kreistext</button>
</div>
<button type="button" class="dock__knopf" data-nochmal>Nochmal</button>
</div>
<p class="werte" data-werte></p>
</aside>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const $ = s => document.querySelector(s), $$ = s => [...document.querySelectorAll(s)];
const intro = $('[data-intro]'), titel = $('[data-titel]'), ansage = $('[data-ansage]'), werteFeld = $('[data-werte]'), skip = $('[data-skip]');
const telefon = () => matchMedia('(max-width: 760px)').matches;
/* ---------- Werte ----------
Gemessen bei dennissnellenberg.com (initLoaderHome): neun Wörter im Stagger .15 s,
Panel .8 s Power4.easeInOut nach oben, Inhalt Expo.easeOut mit Stagger .05,
y 50vh → 0 (Mobil 10vh). */
const WORT = 0.15, PANEL = 0.8, STAGGER = 0.05, INHALT = 1.0;
const ERSTES = 0.4, LETZTES = 0.35;
const WOERTER = [
['Musik', 'var(--musik)', 'Sparte Musik'], ['Tanz', 'var(--theater)', 'Sparte Tanz'], ['Kunst', 'var(--kunst)', 'Sparte Kunst'], ['Film', 'var(--film)', 'Sparte Film'],
['Lesung', '', ''], ['Nacht', '', ''], ['Park', '', ''], ['Weimar', '', ''], ['quer', '', '13.–22. November 2026']
];
let variante = 'wortwechsel';
const wegY = () => innerHeight * (telefon() ? 0.1 : 0.5);
function zeigeWerte() {
let t;
if (reduziert()) t = '<span><b>reduziert</b> kein Intro · nur die finale Headline · keine Klone</span>';
else {
const v = {
wortwechsel: `<span><b>Wortwechsel</b> ${telefon() ? 7 : 9} Wörter · erstes ${ERSTES * 1000} ms, dann je ${WORT * 1000} ms · Punkt in Spartenfarbe, Text bleibt</span>`,
stapel: `<span><b>Stapel</b> ${telefon() ? 5 : 7} Reihen Outline-Typo · auffächern expo.out 700 ms, Versatz 30 ms ab Mitte · Lauf power4.in 1,2 s</span>`,
kreis: `<span><b>Kreistext</b> SVG textPath r 390 · Drehung 0 → 420° power4.in 1,5 s · Öffnung als Kreismaske</span>`
}[variante];
t = v + `<span class="lang"><b>Panel</b> ${PANEL * 1000} ms power4.inOut cubic-bezier(0.83, 0, 0.17, 1) · <b>Inhalt</b> y ${telefon() ? '10vh' : '50vh'} → 0, expo.out ${INHALT * 1000} ms, Versatz ${STAGGER * 1000} ms</span>`
+ `<span><b>Hover/UI</b> 200 ms cubic-bezier(0.215, 0.61, 0.355, 1) · Klone aria-hidden</span>`;
}
werteFeld.innerHTML = t;
}
/* ---------- Bausteine ---------- */
const teil = n => intro.querySelector(`[data-teil="${n}"]`);
function zeigeTeil(n) { $$('.intro__teil').forEach(t => t.classList.toggle('an', t.dataset.teil === n)); }
const rein = () => $$('[data-rein]');
let tl = null;
// Solange das Intro liegt, ist die Seite darunter nicht fokussierbar (Fokus würde sonst verdeckt)
const sperre = an => $$('.kopf, main').forEach(el => { el.inert = an; });
function zuruecksetzen() {
if (tl) tl.kill();
tl = null;
gsap.killTweensOf([intro, titel, ...rein(), '.wort__innen', '[data-ring]', '[data-kreismitte]', '.reihe', '.flieger']);
gsap.set([intro, ...rein(), titel, '[data-ring]', '[data-kreismitte]', '.wort__innen'], { clearProps: 'all' });
intro.style.webkitMaskImage = intro.style.maskImage = '';
teil('stapel').innerHTML = '';
const f = $('.flieger'); if (f) f.remove();
intro.classList.remove('an', 'hell');
sperre(false);
skip.hidden = true;
document.body.classList.remove('intro-laeuft');
scrollTo(0, 0);
}
function fertig() {
intro.classList.remove('an');
sperre(false);
skip.hidden = true;
document.body.classList.remove('intro-laeuft');
const f = $('.flieger'); if (f) f.remove();
titel.style.visibility = '';
ansage.textContent = 'quer, Festival für Musik, Tanz, Kunst und Film, Weimar.';
}
// Sobald das Intro abgeht, verschwindet „Überspringen“ (läge sonst über dem Seitenkopf); Fokus wandert zur Headline
function skipWeg() { const hatte = document.activeElement === skip; skip.hidden = true; if (hatte) titel.focus({ preventScroll: true }); }
// Panel fährt nach oben, Inhalt kommt nach (gemessene Werte)
function abgang(t, an) {
t.call(skipWeg, null, an);
t.to(intro, { yPercent: -100, duration: PANEL, ease: 'power4.inOut' }, an);
t.fromTo([titel, ...rein()], { y: wegY }, { y: 0, duration: INHALT, ease: 'expo.out', stagger: STAGGER, immediateRender: false }, `${an}+=0.2`);
}
/* Variante 1: Wortwechsel (Panel bleibt schwarz, nur das Wort wechselt: kein Flackern großer Farbflächen) */
function wortwechsel() {
const t = gsap.timeline();
const w = $('[data-wort]'), p = $('[data-punkt]'), s = $('[data-sparte]');
const setze = i => { const [wort, farbe, sp] = WOERTER[i]; w.textContent = wort; p.style.background = farbe || 'currentColor'; s.textContent = sp; };
const liste = telefon() ? WOERTER.filter((_, i) => i !== 4 && i !== 6) : WOERTER;
zeigeTeil('wortwechsel');
setze(0);
t.set([titel, ...rein()], { y: wegY });
t.fromTo('.wort__innen', { opacity: 0, y: 24 }, { opacity: 1, y: 0, duration: ERSTES, ease: 'expo.out' });
liste.slice(1).forEach(x => t.call(() => setze(WOERTER.indexOf(x)), null, `+=${WORT}`));
t.to({}, { duration: LETZTES });
abgang(t, '>');
return t;
}
/* Variante 2: Stapel aus Outline-Typo, kollabiert zu einem Wort, das zur Headline wird */
function stapel() {
const t = gsap.timeline();
const n = telefon() ? 5 : 7, mitte = (n - 1) / 2, box = teil('stapel');
intro.classList.add('hell');
zeigeTeil('stapel');
const h = innerHeight / n, fs = h * 1.18;
box.innerHTML = '';
const reihen = [];
for (let i = 0; i < n; i++) {
const r = document.createElement('div');
r.className = 'reihe disp' + (i === mitte ? ' mitte' : '');
r.style.fontSize = fs + 'px';
r.style.marginTop = (-fs * .4) + 'px';
r.textContent = 'quer quer quer quer quer quer quer quer';
box.appendChild(r); reihen.push(r);
}
// Flieger: Kopie der echten Headline, startet in der Mitte und landet auf ihr
const f = titel.cloneNode(true);
f.removeAttribute('id'); f.removeAttribute('data-titel'); f.removeAttribute('tabindex');
f.classList.add('flieger'); f.setAttribute('aria-hidden', 'true');
document.body.appendChild(f);
const z = titel.getBoundingClientRect();
Object.assign(f.style, { left: z.left + 'px', top: z.top + 'px' });
// Start: so groß wie die mittlere Reihe und mittig; Ende: genau auf der Headline
const s0 = fs / parseFloat(getComputedStyle(titel).fontSize), w = f.offsetWidth;
const mitteX = innerWidth / 2 - w * s0 / 2 - z.left, mitteY = innerHeight / 2 - fs * 0.4 - z.top;
const breite = reihen[0].getBoundingClientRect().width;
t.set(titel, { visibility: 'hidden' });
t.set(rein(), { y: wegY });
t.set(f, { visibility: 'visible', x: mitteX, y: mitteY, scale: s0, opacity: 0 });
t.set(reihen, { x: i => -breite * 0.2 + (i % 2 ? -1 : 1) * fs * 0.6, y: 0, opacity: 0 });
t.to(reihen, { y: i => (i - mitte) * h, opacity: 1, duration: 0.7, ease: 'expo.out', stagger: { each: 0.03, from: 'center' } });
t.to(reihen, { x: i => '+=' + (i % 2 ? 1 : -1) * breite * 0.14, duration: 1.2, ease: 'power4.in' }, '<0.15');
t.addLabel('zu', '>-0.1');
t.to(reihen, { y: 0, duration: PANEL, ease: 'power4.inOut' }, 'zu');
t.to(reihen.filter((_, i) => i !== mitte), { opacity: 0, duration: 0.3, ease: 'none' }, 'zu+=0.45');
t.to(f, { opacity: 1, duration: 0.2, ease: 'none' }, 'zu+=0.6');
t.to(reihen[mitte], { opacity: 0, duration: 0.2, ease: 'none' }, 'zu+=0.6');
// Landen: Grund blendet weg, Wort fliegt an seinen Platz, Inhalt kommt nach
t.addLabel('landen', 'zu+=' + (PANEL + 0.05));
t.call(skipWeg, null, 'landen');
t.to(intro, { opacity: 0, duration: PANEL * 0.6, ease: 'none' }, 'landen');
t.to(f, { x: 0, y: 0, scale: 1, duration: PANEL, ease: 'power4.inOut' }, 'landen');
t.fromTo(rein(), { y: wegY }, { y: 0, duration: INHALT, ease: 'expo.out', stagger: STAGGER, immediateRender: false }, 'landen+=0.2');
t.set(titel, { visibility: 'visible' }, 'landen+=' + PANEL);
t.set(f, { visibility: 'hidden' }, 'landen+=' + PANEL);
return t;
}
/* Variante 3: Kreistext, beschleunigt, öffnet sich als Kreis zur Seite */
function kreis() {
const t = gsap.timeline();
zeigeTeil('kreis');
const ring = $('[data-ring]'), mitteW = $('[data-kreismitte]');
const maxR = Math.hypot(innerWidth, innerHeight) / 2 + 20, m = { r: 0 };
const maske = () => { const g = `radial-gradient(circle at 50% 50%, transparent ${m.r.toFixed(1)}px, #000 ${(m.r + 1).toFixed(1)}px)`; intro.style.webkitMaskImage = g; intro.style.maskImage = g; };
t.set([titel, ...rein()], { y: wegY });
t.fromTo(ring, { scale: 0.7, opacity: 0, rotation: 0, transformOrigin: '50% 50%' }, { scale: 1, opacity: 1, duration: 0.6, ease: 'expo.out' });
t.fromTo(mitteW, { opacity: 0, scale: .8 }, { opacity: 1, scale: 1, duration: 0.6, ease: 'expo.out' }, '<0.1');
t.to(ring, { rotation: 420, duration: 1.5, ease: 'power4.in' }, 0);
t.addLabel('auf', 1.45);
t.call(skipWeg, null, 'auf');
t.to(m, { r: maxR, duration: PANEL, ease: 'power4.inOut', onUpdate: maske }, 'auf');
t.to(ring, { scale: 3.2, rotation: '+=120', opacity: 0, duration: PANEL, ease: 'power4.inOut' }, 'auf');
t.to(mitteW, { scale: 2.4, opacity: 0, duration: PANEL * 0.7, ease: 'power4.inOut' }, 'auf');
t.fromTo([titel, ...rein()], { y: wegY }, { y: 0, duration: INHALT, ease: 'expo.out', stagger: STAGGER, immediateRender: false }, 'auf+=0.2');
return t;
}
const VARIANTEN = { wortwechsel, stapel, kreis };
function spiele(v = variante) {
zuruecksetzen();
zeigeWerte();
if (reduziert()) { fertig(); return gsap.timeline(); }
intro.classList.add('an');
sperre(true);
skip.hidden = false;
document.body.classList.add('intro-laeuft');
tl = VARIANTEN[v]();
tl.call(fertig);
return tl;
}
function ueberspringen() { if (!intro.classList.contains('an')) return; stoppe(); zuruecksetzen(); fertig(); titel.focus({ preventScroll: true }); }
function setzeVariante(v) {
variante = v;
$$('[data-variante]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.variante === v)));
zeigeWerte();
}
$$('[data-variante]').forEach(b => b.addEventListener('click', () => { stoppe(); setzeVariante(b.dataset.variante); spiele(); }));
$('[data-nochmal]').addEventListener('click', () => { stoppe(); spiele(); });
skip.addEventListener('click', ueberspringen);
addEventListener('keydown', e => { if (e.key === 'Escape') ueberspringen(); });
$$('main a, .kopf a').forEach(a => a.addEventListener('click', e => { if (a.getAttribute('href') === '#') e.preventDefault(); }));
/* ---------- Vorführung: Varianten im Wechsel ---------- */
const FOLGE = ['wortwechsel', 'stapel', 'kreis'];
let schleife = null, i = 0;
function stoppe() { if (schleife) schleife.kill(); schleife = null; }
function runde() {
setzeVariante(FOLGE[i++ % FOLGE.length]);
const t = spiele();
schleife = gsap.delayedCall(t.duration() + (reduziert() ? 3 : 1.8), runde);
}
function vorfuehren() { stoppe(); gsap.globalTimeline.resume(); i = 0; zuruecksetzen(); intro.classList.add('an'); zeigeTeil('wortwechsel'); gsap.set('.wort__innen', { opacity: 0 }); schleife = gsap.delayedCall(0.35, runde); }
function neustart() { stoppe(); gsap.globalTimeline.resume(); spiele(); }
function pause() { gsap.globalTimeline.pause(); }
function weiter() { gsap.globalTimeline.resume(); }
setzeVariante('wortwechsel');
// Auf der Startseite einmal beim Laden; Unterseiten bekämen nur ein kurzes Intro
if (!new URLSearchParams(location.search).has('vorschau')) {
if (!reduziert()) { intro.classList.add('an'); sperre(true); zeigeTeil('wortwechsel'); } // kein Aufblitzen der Seite vor dem Start
const start = () => spiele();
if (document.fonts) document.fonts.ready.then(start); else start();
}
addEventListener('resize', zeigeWerte);
})();
</script>
</body>
</html>