Animationsstil „Luxus weich“
Hero eines Kitzbüheler Boutique-Hotels: Bildwechsel als Crossfade mit Blur, einmaliges Zurückskalieren oder Maske, alles in sine-Kurven mit langen Dauern; darunter Zimmer, die per Scroll durch eine Sinus-Kurve erscheinen.
Sehr sanfte, fast lineare Sinus-Kurven (sine.inOut, power1.inOut), lange Dauern von 1,2 bis 2,4 s und kleine Distanzen. Bilder blenden über Masken oder ein langsames, einmaliges Zurückskalieren von 1,08 auf 1 ein. Übergänge sind Crossfades mit leichtem Blur. Nie ruckartig, nie verspielt.
Ruhe und Großzügigkeit signalisieren Wertigkeit. Nichts drängt, Bild und Produkt stehen im Mittelpunkt.
Wann einsetzen
Mode, Schmuck, Hotels, Immobilien, Kosmetik, Weingüter, Kunst.
Wann nicht
Interaktive Werkzeuge, Formulare und Seiten mit hoher Informationsdichte. Auch auf einer Luxusseite gilt der Stil nur für Bilder und Hero-Wechsel; Buchung und Menüs brauchen ein schnelles UI-Tempo.
Varianten
- Crossfade mit Blur
- Einmaliges Zurückskalieren statt Dauerzoom (Ken-Burns-Ersatz)
- Masken-Reveal per clip-path mit innerem Zurückskalieren
- Scroll-gescrubbte Sinus-Timeline
Umsetzung
CSS-Token: --ease-lux: cubic-bezier(0.37, 0, 0.63, 1) (sine.inOut); --ease-lux-out: cubic-bezier(0.61, 1, 0.88, 1) (sine.out); --dur-lux: 1.4s; --dur-lux-lg: 2.2s. Bilder: Opazität 0 → 1 über 1,2 bis 1,4 s, scale 1,08 → 1 über 2,2 s, Blur 8 px → 0. Texte: y 10 bis 20 px, Versatz 30 ms (Raster) bis 120 ms (große Bilder). Scroll-Variante: Fortschritt durch −(cos(π·p) − 1) / 2 schicken. Smooth Scroll mit lerp 0,08 bis 0,1 nur, wenn keine Datums-Popover oder Buchungs-iFrames auf der Seite sind.
Robust umsetzen
Telefon. Blur weglassen, weil er auf schwachen Geräten teuer ist. Dauern leicht kürzen.
Ohne Bewegung. Nur Crossfade, kein Scale, kein Blur, kein Smooth Scroll, keine automatische Diashow.
Barrierefrei. Lange Einblendungen dürfen Inhalte nicht verstecken: Text steht sofort im DOM. Automatische Diashows brauchen einen Pause-Knopf; Bildwechsel per aria-live ansagen.
Code
<!DOCTYPE html>
<!--
Animationsstil „Luxus weich“
Fundus · Animationsstile · https://cecodesign.de/lab/fundus/animationsstile/token-luxus-weich/
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>Animationsstil „Luxus weich“ · 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: 'Newsreader'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/newsreader:vf@latest/latin-opsz-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-style: normal; font-display: swap; }
@font-face { font-family: 'Newsreader'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/newsreader:vf@latest/latin-opsz-italic.woff2') format('woff2-variations'); font-weight: 200 800; font-style: italic; font-display: swap; }
@font-face { font-family: 'Mona Sans'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/mona-sans:vf@latest/latin-wdth-normal.woff2') format('woff2-variations'); font-weight: 200 900; font-stretch: 75% 125%; font-display: swap; }
:root {
/* Rezept „Boutique-Hotel“: Sand und Creme, warmes Dunkel statt Schwarz, ein Hauston aus Zirbenholz */
--sand: #f3eee7; --creme: #fef2e3; --text: #3e3a37; --linie: #d5d1c8; --leise: #62645f; --haus: #6e5a47;
/* Token „Luxus weich“ */
--ease-lux: cubic-bezier(0.37, 0, 0.63, 1); /* sine.inOut */
--ease-lux-out: cubic-bezier(0.61, 1, 0.88, 1); /* sine.out */
--dur-lux: 1.4s; --dur-lux-lg: 2.2s;
/* Buchungsstrecke: eigenes, schnelles UI-Tempo (token-toggle-asymmetrisch) */
--ease-ui: cubic-bezier(0.4, 0, 0.2, 1);
--rand: clamp(14px, 2.4vw, 28px);
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--sand); color: var(--text); font: 400 17px/1.6 'Mona Sans', system-ui, sans-serif; font-stretch: 100%; -webkit-font-smoothing: antialiased; }
body { overflow-x: hidden; }
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
a { color: inherit; text-decoration: none; }
:focus-visible { outline: 1px solid var(--text); outline-offset: 3px; }
.label { font: 500 12px/1.3 'Mona Sans', sans-serif; font-stretch: 125%; letter-spacing: .06em; text-transform: uppercase; }
.leise { color: var(--leise); }
.nur-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
em { font-style: italic; }
/* ---------- Kopf ---------- */
.kopf { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; padding: 18px calc(var(--rand) + 8px); }
.kopf nav { display: flex; gap: 26px; }
.kopf nav a, .sprache a { transition: opacity 400ms var(--ease-lux); }
.kopf nav a:hover, .sprache a:hover { opacity: .6; }
.marke { text-align: center; line-height: 1; }
.marke b { display: block; font: 400 30px/1 'Newsreader', serif; font-variation-settings: 'opsz' 72; letter-spacing: -.015em; }
.marke span { display: block; margin-top: 5px; font-size: 10px; color: var(--leise); letter-spacing: .22em; }
.rechts { display: flex; justify-content: flex-end; align-items: center; gap: 22px; }
.sprache { display: flex; gap: 8px; color: var(--leise); }
.sprache [aria-current] { color: var(--text); }
.knopf { display: inline-flex; align-items: center; justify-content: center; height: 42px; padding: 0 22px; background: var(--haus); color: var(--creme); border-radius: 2px; transition: background-color 200ms var(--ease-ui); }
.knopf:hover { background: #5d4b3a; }
/* ---------- Hero: Bild trägt die Stimmung ---------- */
.hero { position: relative; margin: 0 var(--rand); height: clamp(460px, calc(100svh - 206px), 820px); overflow: hidden; background: #d9d2c7; isolation: isolate; }
.folie { position: absolute; inset: 0; opacity: 0; will-change: opacity, filter, clip-path; }
.folie.an { opacity: 1; }
.folie img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; will-change: transform; }
.hero::after { content: ''; position: absolute; inset: 0; z-index: 3; pointer-events: none; background: linear-gradient(to top, rgba(40, 33, 28, .62) 0%, rgba(40, 33, 28, .3) 34%, rgba(40, 33, 28, 0) 66%); }
.hero-text { position: absolute; z-index: 4; left: clamp(22px, 4vw, 64px); bottom: clamp(84px, 9vw, 120px); right: 34%; color: var(--creme); }
.hero-text .label { opacity: .9; margin-bottom: 18px; }
.hero h1 { font: 300 clamp(2.5rem, 1.5rem + 4vw, 5.5rem)/1.05 'Newsreader', serif; font-variation-settings: 'opsz' 72; letter-spacing: -.015em; }
.hero h1 .z { display: block; }
.hero-fuss { position: absolute; z-index: 4; right: clamp(22px, 4vw, 64px); bottom: clamp(84px, 9vw, 120px); display: flex; align-items: center; gap: 18px; color: var(--creme); }
.striche { display: flex; gap: 6px; }
.striche button { width: 46px; height: 22px; display: grid; align-items: center; }
.striche button::before { content: ''; height: 1px; background: rgba(254, 242, 227, .35); grid-area: 1 / 1; }
.striche i { grid-area: 1 / 1; height: 1px; background: var(--creme); transform-origin: 0 50%; transform: scaleX(0); display: block; }
.striche button[aria-current='true'] i { transform: scaleX(var(--f, 0)); }
.halt { width: 30px; height: 30px; border: 1px solid rgba(254, 242, 227, .45); border-radius: 50%; display: grid; place-items: center; transition: border-color 200ms var(--ease-ui); }
.halt:hover { border-color: var(--creme); }
.halt svg { width: 10px; height: 10px; }
.halt .dr, .halt[aria-pressed='true'] .ii { display: none; }
.halt[aria-pressed='true'] .dr { display: block; }
.vorschau .halt { display: none; }
/* ---------- Buchungsleiste direkt unter dem Hero ---------- */
.buchung { position: relative; z-index: 5; margin: -56px auto 0; width: min(1100px, calc(100% - 2 * var(--rand) - 48px)); background: var(--creme); display: grid; grid-template-columns: 1.25fr 1fr 1.35fr auto; align-items: stretch; box-shadow: 0 18px 40px -24px rgba(62, 58, 55, .35); }
.feld { position: relative; border-right: 1px solid var(--linie); }
.feld > button { width: 100%; height: 100%; text-align: left; padding: 18px 24px; display: grid; gap: 4px; transition: background-color 200ms var(--ease-ui); }
.feld > button:hover { background: rgba(213, 209, 200, .25); }
.feld .label { color: var(--leise); font-size: 11px; }
.feld .wert { font-size: 16px; }
.feld .wert svg { width: 10px; height: 10px; margin-left: 6px; vertical-align: middle; transition: transform 200ms var(--ease-ui); }
.feld > button[aria-expanded='true'] svg { transform: rotate(180deg); transition-duration: 300ms; }
.vorteil { padding: 16px 24px; display: grid; align-content: center; gap: 2px; font-size: 14px; line-height: 1.45; }
.vorteil b { font-weight: 500; }
.buchung .knopf { height: auto; padding: 0 30px; border-radius: 0; font-stretch: 112%; letter-spacing: .04em; font-size: 14px; font-weight: 500; }
/* Popover: öffnen 300 ms, schließen 200 ms (asymmetrisch) */
.pop { position: absolute; left: 0; top: calc(100% + 8px); min-width: 300px; background: var(--creme); padding: 10px; box-shadow: 0 18px 40px -18px rgba(62, 58, 55, .4); z-index: 10;
opacity: 0; translate: 0 -6px; visibility: hidden; transition: opacity 200ms var(--ease-ui), translate 200ms var(--ease-ui), visibility 0s 200ms; }
.pop.offen { opacity: 1; translate: 0 0; visibility: visible; transition: opacity 300ms var(--ease-ui), translate 300ms var(--ease-ui), visibility 0s 0s; }
.pop .option { width: 100%; display: flex; justify-content: space-between; gap: 18px; padding: 10px 12px; text-align: left; font-size: 15px; transition: background-color 150ms var(--ease-ui); }
.pop .option:hover, .pop .option[aria-checked='true'] { background: rgba(213, 209, 200, .35); }
.pop .option small { color: var(--leise); font-size: 13px; }
.pop .notiz { padding: 10px 12px 4px; border-top: 1px solid var(--linie); margin-top: 6px; color: var(--leise); font-size: 13px; }
.zaehlzeile { display: flex; justify-content: space-between; align-items: center; padding: 10px 12px; font-size: 15px; }
.zaehlzeile div { display: flex; align-items: center; gap: 12px; }
.zaehlzeile button { width: 32px; height: 32px; border: 1px solid var(--linie); border-radius: 50%; transition: border-color 150ms var(--ease-ui); }
.zaehlzeile button:hover { border-color: var(--text); }
.zaehlzeile output { min-width: 1.2em; text-align: center; font-variant-numeric: tabular-nums; }
/* Demo-Umschalter (im Fluss, nicht fest) */
.pult { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 22px; width: min(1100px, calc(100% - 2 * var(--rand) - 48px)); margin: 26px auto 0; color: var(--leise); font-size: 11px; }
.pult button { letter-spacing: inherit; text-transform: inherit; padding: 4px 0; border-bottom: 1px solid transparent; transition: color 200ms var(--ease-ui), border-color 200ms var(--ease-ui); }
.pult button:hover { color: var(--text); }
.pult button[aria-pressed='true'] { color: var(--text); border-color: currentColor; }
.pult .werte { margin-left: auto; text-transform: none; letter-spacing: .01em; font-stretch: 100%; font-weight: 400; font-size: 12px; text-align: right; }
/* ---------- Zimmer: scroll-gescrubbte Sinus-Timeline ---------- */
.zimmer { padding: clamp(80px, 10vw, 140px) calc(var(--rand) + clamp(0px, 3vw, 40px)) clamp(80px, 10vw, 140px); }
.zimmer-kopf { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; align-items: end; margin-bottom: clamp(36px, 5vw, 64px); }
.zimmer h2 { font: 300 clamp(2.2rem, 1.4rem + 2.6vw, 4rem)/1.05 'Newsreader', serif; font-variation-settings: 'opsz' 72; letter-spacing: -.015em; }
.zimmer-kopf p:not(.label) { max-width: 44ch; justify-self: end; }
.karten { display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(16px, 2.4vw, 36px); }
.karte figure { aspect-ratio: 4 / 5; overflow: hidden; background: #e4ddd2; }
.karte figure img { width: 100%; height: 100%; object-fit: cover; display: block; transform: scale(var(--s, 1)); opacity: var(--o, 1); }
.karte .unter { transform: translateY(var(--y, 0)); opacity: var(--o, 1); }
.karte h3 { font: 400 1.6rem/1.15 'Newsreader', serif; font-variation-settings: 'opsz' 48; margin: 18px 0 6px; }
.karte h3 em { font-weight: 300; }
.fakten { color: var(--leise); font-size: 15px; }
.preis { margin-top: 10px; font-size: 15px; }
.preis b { font-weight: 500; }
/* ---------- Vorschau 960 × 720: nur Hero und Leiste ---------- */
.vorschau body { overflow: hidden; height: 100vh; }
.vorschau .pult, .vorschau .zimmer { display: none; }
.vorschau .kopf nav, .vorschau .sprache { visibility: hidden; }
.vorschau .hero { height: 560px; }
.vorschau .hero-text { right: 22%; }
.vorschau .hero h1 { font-size: 64px; }
.vorschau .vorteil { display: none; }
.vorschau .buchung { grid-template-columns: 1.2fr 1fr auto; }
/* ---------- Reduziert: nur Überblenden, kein Scale, kein Blur ---------- */
:root.reduziert * { transition-duration: .01ms !important; animation-duration: .01ms !important; }
@media (max-width: 980px) {
.kopf nav a:nth-child(n+3) { display: none; }
.buchung { grid-template-columns: 1fr 1fr auto; }
.vorteil { display: none; }
.pult .werte { margin-left: 0; text-align: left; flex-basis: 100%; }
}
@media (max-width: 640px) {
html { font-size: 16px; }
.kopf { grid-template-columns: auto 1fr auto; padding: 14px var(--rand); }
.kopf nav { display: none; }
.marke { text-align: left; }
.marke b { font-size: 26px; }
.sprache { display: none; }
.knopf { height: 38px; padding: 0 16px; }
.hero { height: 68svh; min-height: 420px; }
.hero-text { right: 22px; bottom: 64px; }
.hero-fuss { bottom: 26px; left: 22px; right: auto; }
/* Mobil: die Buchungsleiste bleibt als feste untere Leiste erreichbar */
.buchung { position: sticky; bottom: 0; margin: 0; width: 100%; grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr); box-shadow: 0 -10px 30px -18px rgba(62, 58, 55, .4); }
.feld .wert { font-size: 15px; white-space: nowrap; }
.feld:nth-child(2) { border-right: 0; }
.feld > button { padding: 12px 14px; }
.buchung .knopf { grid-column: 1 / -1; height: 48px; }
.pop { top: auto; bottom: calc(100% + 8px); min-width: 0; width: calc(100vw - 20px); }
.feld:nth-child(2) .pop { left: auto; right: 0; }
.zimmer-kopf { grid-template-columns: 1fr; }
.zimmer-kopf p:not(.label) { justify-self: start; }
.karten { grid-template-columns: 1fr; gap: 44px; }
}
@media (max-width: 360px) {
.feld .wert { white-space: normal; }
}
@media (hover: none) {
.striche button { height: 36px; }
.zaehlzeile button { width: 40px; height: 40px; }
.pop .option { padding: 14px 12px; }
}
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }
</style>
</head>
<body>
<header class="kopf">
<nav class="label" aria-label="Hauptmenü"><a href="#zimmer">Zimmer</a><a href="#kulinarik">Kulinarik</a><a href="#spa">Spa</a><a href="#umgebung">Umgebung</a></nav>
<a class="marke" href="#oben" aria-label="Haus Kaiserlicht, Kitzbühel"><b>Kaiserlicht</b><span class="label">Kitzbühel</span></a>
<div class="rechts">
<p class="sprache label"><a href="#de" aria-current="true">DE</a><a href="#en" lang="en">EN</a></p>
<a class="knopf label" href="#buchen">Buchen</a>
</div>
</header>
<section class="hero" aria-roledescription="Diashow" aria-label="Bilder aus dem Haus" data-hero>
<div class="folie an" data-folie><img data-bild alt="Morgennebel über dem Schwarzsee, dahinter die Felsen des Wilden Kaisers, vorn ein hölzerner Steg."></div>
<div class="folie" data-folie><img data-bild alt="Zirbenstube: holzgetäfelte Wand, grüner Lodensessel mit Wolldecke, großes Fenster mit Blick auf den verschneiten Hang."></div>
<div class="folie" data-folie><img data-bild alt="Das Haus zur blauen Stunde im Schnee, alle Fenster warm erleuchtet, Fichten links und rechts."></div>
<div class="hero-text">
<p class="label" data-ort>Morgen am Schwarzsee · 7:40 Uhr</p>
<h1 data-titel><span class="z">Ein Haus am Fuß</span><span class="z">des <em>Wilden Kaisers</em></span></h1>
</div>
<div class="hero-fuss">
<p class="label" aria-live="polite"><span data-nr>1</span> / 3</p>
<div class="striche" data-striche></div>
<button type="button" class="halt" data-halt aria-pressed="false" aria-label="Diashow anhalten"><svg viewBox="0 0 12 12" fill="currentColor" aria-hidden="true"><rect class="ii" x="2.5" y="2" width="2" height="8"/><rect class="ii" x="7.5" y="2" width="2" height="8"/><path class="dr" d="M3 2v8l7-4z"/></svg></button>
</div>
</section>
<form class="buchung" id="buchen" aria-label="Verfügbarkeit prüfen" onsubmit="return false">
<div class="feld">
<button type="button" aria-expanded="false" aria-controls="pop-zeit" data-auf="pop-zeit"><span class="label">Zeitraum</span><span class="wert"><span data-zeit>12.–14. Dez · 2 Nächte</span><svg viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.2" aria-hidden="true"><path d="m2 3.5 3 3 3-3"/></svg></span></button>
<div class="pop" id="pop-zeit" role="radiogroup" aria-label="Zeitraum wählen">
<button type="button" class="option" role="radio" aria-checked="true" data-zeitraum="12.–14. Dez · 2 Nächte"><span>Fr 12. – So 14. Dez</span><small>2 Nächte · frei</small></button>
<button type="button" class="option" role="radio" aria-checked="false" data-zeitraum="18.–21. Dez · 3 Nächte"><span>Do 18. – So 21. Dez</span><small>3 Nächte · frei</small></button>
<button type="button" class="option" role="radio" aria-checked="false" data-zeitraum="2.–5. Jan · 3 Nächte"><span>Fr 2. – Mo 5. Jan</span><small>3 Nächte · 1 Zimmer frei</small></button>
<p class="notiz">Mindestaufenthalt 2 Nächte, über Silvester 5 Nächte.</p>
</div>
</div>
<div class="feld">
<button type="button" aria-expanded="false" aria-controls="pop-gaeste" data-auf="pop-gaeste"><span class="label">Gäste</span><span class="wert"><span data-gaeste>2 Erwachsene</span><svg viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.2" aria-hidden="true"><path d="m2 3.5 3 3 3-3"/></svg></span></button>
<div class="pop" id="pop-gaeste" role="group" aria-label="Gäste wählen">
<div class="zaehlzeile"><span>Erwachsene</span><div><button type="button" data-schritt="erw,-1" aria-label="Weniger Erwachsene">−</button><output data-erw>2</output><button type="button" data-schritt="erw,1" aria-label="Mehr Erwachsene">+</button></div></div>
<div class="zaehlzeile"><span>Kinder <small class="leise">bis 12</small></span><div><button type="button" data-schritt="kind,-1" aria-label="Weniger Kinder">−</button><output data-kind>0</output><button type="button" data-schritt="kind,1" aria-label="Mehr Kinder">+</button></div></div>
<p class="notiz">Hunde sind willkommen, 25 € pro Nacht.</p>
</div>
</div>
<p class="vorteil"><b>Direkt gebucht:</b> <span class="leise">Sauna ab Anreise, Check-out bis 13 Uhr. Ab € 290 pro Nacht, Dezember bis März.</span></p>
<button class="knopf" type="submit">Verfügbarkeit prüfen</button>
</form>
<div class="pult label" role="group" aria-label="Bildwechsel der Demo" data-umschalter>
<span>Bildwechsel</span>
<button type="button" data-variante="blur" aria-pressed="true">Crossfade mit Blur</button>
<button type="button" data-variante="skalieren" aria-pressed="false">Zurückskalieren</button>
<button type="button" data-variante="maske" aria-pressed="false">Maske</button>
<span class="werte" data-werte></span>
</div>
<section class="zimmer" id="zimmer" aria-labelledby="zimmer-titel">
<div class="zimmer-kopf">
<div><p class="label leise">Zimmer & Suiten · 14 Zimmer</p><h2 id="zimmer-titel">Schlafen unter <em>Zirbe</em></h2></div>
<p class="leise">Jedes Zimmer ist mit Holz aus dem eigenen Wald getäfelt. Die Bilder kommen mit dem Scrollen, ihre Kurve ist ein Sinus: erst zögerlich, dann ruhig, dann still.</p>
</div>
<div class="karten">
<article class="karte" data-karte><figure><img data-raum="1" alt="Zirbenstube mit Lodensessel am Fenster"></figure>
<div class="unter"><h3>Zirbenstube</h3><p class="fakten">28 m² · Doppelbett · Blick in den Garten</p><p class="preis">ab <b>€ 290</b> pro Nacht</p></div></article>
<article class="karte" data-karte><figure><img data-raum="0" alt="Blick vom Balkon auf den Schwarzsee und den Wilden Kaiser"></figure>
<div class="unter"><h3>Kaiserblick-<em>Suite</em></h3><p class="fakten">46 m² · Kingsize · Balkon zum Wilden Kaiser</p><p class="preis">ab <b>€ 420</b> pro Nacht</p></div></article>
<article class="karte" data-karte><figure><img data-raum="3" alt="Dachkammer: Bett mit Leinen und grüner Lodendecke vor Holzwand"></figure>
<div class="unter"><h3>Dachkammer</h3><p class="fakten">22 m² · Doppelbett · Gaubenfenster</p><p class="preis">ab <b>€ 240</b> pro Nacht</p></div></article>
</div>
</section>
<p class="nur-sr" aria-live="polite" data-ansage></p>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const beruehrung = matchMedia('(hover: none)').matches;
const $ = (s, el = document) => el.querySelector(s), $$ = (s, el = document) => [...el.querySelectorAll(s)];
/* ---------- Token „Luxus weich“ (meta.json → messwerte) ---------- */
const LUX = 'cubic-bezier(0.37, 0, 0.63, 1)', LUX_OUT = 'cubic-bezier(0.61, 1, 0.88, 1)';
const K = beruehrung ? .8 : 1; /* mobil: Dauern leicht kürzen */
const T = { hell: Math.round(1200 * K), lux: Math.round(1400 * K), maske: Math.round(1600 * K), luxLg: Math.round(2200 * K), text: Math.round(1200 * K), textAus: 500, stagger: 120, textY: 16, halte: 5200, reduziert: 500 };
const VARIANTEN = {
blur: { name: 'Crossfade mit Blur', werte: () => `Deckkraft 0 → 1 und blur(${beruehrung ? 0 : 8}px) → 0 · ${T.lux} ms · sine.inOut ${LUX}` },
skalieren: { name: 'Zurückskalieren', werte: () => `Deckkraft ${T.hell} ms sine.inOut · scale 1.08 → 1 · ${T.luxLg} ms sine.out ${LUX_OUT}` },
maske: { name: 'Maske', werte: () => `clip-path inset(100% 0 0 0) → inset(0) · ${T.maske} ms sine.inOut · Bild scale 1.06 → 1 · ${T.luxLg} ms sine.out` },
};
let variante = 'blur';
/* ---------- Eigene Bilder: auf Canvas gemalt ---------- */
function zufall(seed) { return () => { seed |= 0; seed = seed + 0x6D2B79F5 | 0; let t = Math.imul(seed ^ seed >>> 15, 1 | seed); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; }; }
function vl(c, x0, y0, x1, y1, st) { const g = c.createLinearGradient(x0, y0, x1, y1); st.forEach(([o, f]) => g.addColorStop(o, f)); return g; }
function glanz(c, x, y, r, f, a = 1, modus = 'lighter') { const g = c.createRadialGradient(x, y, 0, x, y, r); g.addColorStop(0, f); g.addColorStop(1, 'rgba(0,0,0,0)'); c.save(); c.globalCompositeOperation = modus; c.globalAlpha = a; c.fillStyle = g; c.fillRect(x - r, y - r, 2 * r, 2 * r); c.restore(); }
function vignette(c, W, H, s) { const g = c.createRadialGradient(W / 2, H / 2, H * .35, W / 2, H / 2, Math.max(W, H) * .7); g.addColorStop(0, 'rgba(40,30,22,0)'); g.addColorStop(1, `rgba(40,30,22,${s})`); c.fillStyle = g; c.fillRect(0, 0, W, H); }
function grat(c, punkte, unten, r, zitter) {
c.beginPath(); c.moveTo(punkte[0][0], unten);
for (let i = 0; i < punkte.length; i++) {
const [x, y] = punkte[i]; c.lineTo(x, y);
if (i < punkte.length - 1) { const [x2, y2] = punkte[i + 1]; for (let k = 1; k < 4; k++) c.lineTo(x + (x2 - x) * k / 4, y + (y2 - y) * k / 4 + (r() - .5) * zitter); }
}
c.lineTo(punkte[punkte.length - 1][0], unten); c.closePath();
}
/* Ostflanken abschatten: von jedem Gipfel, der nach rechts abfällt, eine dunklere Fläche bis zum Fuß */
function schatten(c, punkte, unten, farbe) {
c.fillStyle = farbe;
for (let i = 0; i < punkte.length - 1; i++) {
const [x, y] = punkte[i], [x2, y2] = punkte[i + 1];
if (y2 <= y) continue;
c.beginPath(); c.moveTo(x, y); c.lineTo(x2, y2); c.lineTo(x2 + (x2 - x) * .25, unten); c.lineTo(x + (x2 - x) * .15, unten); c.closePath(); c.fill();
}
}
function fichte(c, x, y, h, farbe, schnee) {
c.fillStyle = farbe; c.beginPath();
for (let k = 0; k < 4; k++) { const t = y - h + k * h * .22, b = h * (.16 + k * .09); c.moveTo(x, t); c.lineTo(x + b, t + h * .34); c.lineTo(x - b, t + h * .34); }
c.fill(); c.fillRect(x - h * .03, y - h * .1, h * .06, h * .1);
if (schnee) {
/* Schnee: helle Kappe auf jeder Astlage, links etwas mehr (Wind) */
c.fillStyle = schnee; c.beginPath();
for (let k = 0; k < 4; k++) { const t = y - h + k * h * .22, b = h * (.16 + k * .09); c.moveTo(x, t); c.lineTo(x + b * .35, t + h * .12); c.lineTo(x + b * .1, t + h * .1); c.lineTo(x - b * .2, t + h * .16); c.lineTo(x - b * .7, t + h * .26); c.closePath(); }
c.fill();
}
}
const MALER = [
/* 0 · Morgen am Schwarzsee, 1600 × 1000 */
(c, W, H, r) => {
const hz = H * .6;
c.fillStyle = vl(c, 0, 0, 0, hz, [[0, '#a7b6c3'], [.45, '#d6d4ce'], [.85, '#eedcc7'], [1, '#f2d8bf']]); c.fillRect(0, 0, W, hz);
glanz(c, W * .74, hz - 170, 640, 'rgba(255,234,206,1)', .45);
const kamm = [[0, hz - 130], [110, hz - 190], [210, hz - 160], [300, hz - 270], [370, hz - 215], [450, hz - 330], [520, hz - 250], [600, hz - 350], [690, hz - 270], [770, hz - 310], [850, hz - 230], [960, hz - 265], [1060, hz - 185], [1170, hz - 215], [1290, hz - 160], [1420, hz - 175], [1600, hz - 130]];
grat(c, kamm, hz, r, 26); c.fillStyle = vl(c, 0, hz - 350, 0, hz, [[0, '#8f9aa7'], [1, '#c6c6c2']]); c.fill();
c.save(); grat(c, kamm, hz, zufall(5), 26); c.clip(); c.fillStyle = vl(c, 0, hz - 360, 0, hz - 170, [[0, 'rgba(250,251,253,.75)'], [1, 'rgba(250,251,253,0)']]); c.fillRect(0, hz - 360, W, 200);
schatten(c, kamm, hz, 'rgba(70,80,96,.16)'); c.restore();
c.fillStyle = vl(c, 0, hz - 110, 0, hz, [[0, '#6f7c78'], [1, '#93998f']]); c.beginPath(); c.moveTo(0, hz);
for (let x = 0; x <= W; x += 20) c.lineTo(x, hz - 70 - 34 * Math.sin(x / 210 + 1) - 14 * Math.sin(x / 70)); c.lineTo(W, hz); c.fill();
for (let x = -10; x < W + 10; x += 9 + r() * 8) { const y = hz - 18 - 22 * Math.sin(x / 180) - r() * 8; fichte(c, x, y + 22, 34 + r() * 26, '#3e4a43'); }
c.fillStyle = '#3e4a43'; c.fillRect(0, hz - 14, W, 14);
c.fillStyle = vl(c, 0, hz, 0, H, [[0, '#bfc6c4'], [.35, '#98a3a1'], [1, '#5c6866']]); c.fillRect(0, hz, W, H - hz);
c.save(); c.globalAlpha = .28; c.filter = 'blur(5px)'; c.translate(0, 2 * hz); c.scale(1, -1); grat(c, kamm, hz, zufall(5), 26); c.fillStyle = '#8f9aa7'; c.fill(); c.restore();
c.save(); c.globalAlpha = .5; c.filter = 'blur(3px)'; c.fillStyle = '#3e4a43'; c.fillRect(0, hz, W, 26); c.restore();
c.save(); c.filter = 'blur(26px)'; c.fillStyle = vl(c, 0, hz - 50, 0, hz + 90, [[0, 'rgba(250,246,240,0)'], [.5, 'rgba(250,246,240,.6)'], [1, 'rgba(250,246,240,0)']]); c.fillRect(-40, hz - 50, W + 80, 140); c.restore();
c.save(); c.globalAlpha = .18; for (let i = 0; i < 90; i++) { c.fillStyle = r() < .5 ? '#ffffff' : '#7b8784'; c.fillRect(r() * W, hz + 30 + Math.pow(r(), 1.5) * (H - hz), 60 + r() * 260, 1.5); } c.restore();
c.fillStyle = '#4f3d2f';
c.beginPath(); c.moveTo(150, H); c.lineTo(560, H); c.lineTo(668, hz + 120); c.lineTo(612, hz + 120); c.fill();
c.strokeStyle = 'rgba(30,22,16,.55)'; c.lineWidth = 2; for (let k = 1; k < 16; k++) { const t = k / 16, y = H - (H - hz - 120) * Math.pow(t, .7); const xl = 150 + (612 - 150) * Math.pow(t, .7), xr = 560 + (668 - 560) * Math.pow(t, .7); c.beginPath(); c.moveTo(xl, y); c.lineTo(xr, y); c.stroke(); }
c.fillStyle = 'rgba(255,240,220,.18)'; c.beginPath(); c.moveTo(500, H); c.lineTo(560, H); c.lineTo(668, hz + 120); c.lineTo(655, hz + 120); c.fill();
c.fillStyle = '#3a2c21'; for (const [x, y, h] of [[160, H, 90], [560, H, 80], [618, hz + 122, 26], [668, hz + 122, 24]]) c.fillRect(x - 5, y - h, 10, h);
vignette(c, W, H, .22);
},
/* 1 · Zirbenstube */
(c, W, H, r) => {
for (let x = 0; x < W; x += 64) { c.fillStyle = vl(c, 0, 0, 0, H, [[0, (x / 64) % 2 ? '#c29a72' : '#ba916a'], [1, (x / 64) % 2 ? '#a57c58' : '#9e7553']]); c.fillRect(x, 0, 64, H); c.fillStyle = 'rgba(110,78,52,.45)'; c.fillRect(x, 0, 2, H); }
c.save(); c.globalAlpha = .12; for (let i = 0; i < 160; i++) { c.fillStyle = '#5b3f29'; c.beginPath(); c.ellipse(r() * W, r() * H * .78, 3 + r() * 5, 2 + r() * 3, 0, 0, 7); c.fill(); } c.restore();
c.fillStyle = '#7a593e'; c.fillRect(0, 0, W, 58); c.fillStyle = 'rgba(0,0,0,.18)'; c.fillRect(0, 58, W, 10);
const wx = W * .5, wy = H * .17, ww = W * .32, wh = H * .48;
glanz(c, wx + ww / 2, wy + wh / 2, 700, 'rgba(255,246,232,1)', .35);
c.fillStyle = vl(c, 0, wy, 0, wy + wh, [[0, '#dfe6ee'], [.45, '#f1efea'], [.46, '#f4f5f7'], [1, '#d3dae3']]); c.fillRect(wx, wy, ww, wh);
c.save(); c.beginPath(); c.rect(wx, wy, ww, wh); c.clip();
c.fillStyle = '#c9d0da'; c.beginPath(); c.moveTo(wx, wy + wh * .5); for (let x = wx; x <= wx + ww; x += 10) c.lineTo(x, wy + wh * .42 - 40 * Math.sin((x - wx) / 140)); c.lineTo(wx + ww, wy + wh); c.lineTo(wx, wy + wh); c.fill();
for (let i = 0; i < 16; i++) fichte(c, wx + r() * ww, wy + wh * (.5 + r() * .38), 30 + r() * 46, '#35443d', 'rgba(255,255,255,.85)');
c.restore();
c.fillStyle = '#6a4b33'; c.fillRect(wx - 24, wy - 24, ww + 48, 24); c.fillRect(wx - 24, wy + wh, ww + 48, 30); c.fillRect(wx - 24, wy, 24, wh); c.fillRect(wx + ww, wy, 24, wh);
c.fillRect(wx + ww / 2 - 8, wy, 16, wh); c.fillRect(wx, wy + wh * .45 - 7, ww, 14);
c.fillStyle = 'rgba(255,255,255,.08)'; c.fillRect(wx - 24, wy + wh + 2, ww + 48, 4);
const fy = H * .8;
c.fillStyle = vl(c, 0, fy, 0, H, [[0, '#7d5a3f'], [1, '#4f3826']]); c.fillRect(0, fy, W, H - fy);
c.fillStyle = 'rgba(40,28,18,.35)'; for (let x = -200; x < W + 200; x += 120) { c.beginPath(); c.moveTo(x, fy); c.lineTo(x + 2, fy); c.lineTo(x * 1.4 - W * .2 + 2, H); c.lineTo(x * 1.4 - W * .2, H); c.fill(); }
c.save(); c.filter = 'blur(18px)'; c.globalCompositeOperation = 'lighter'; c.fillStyle = 'rgba(255,236,210,.22)';
c.beginPath(); c.moveTo(wx - 40, fy + 10); c.lineTo(wx + ww + 60, fy + 10); c.lineTo(wx + ww - 120, H); c.lineTo(wx - 260, H); c.fill(); c.restore();
const sx = W * .14;
c.fillStyle = 'rgba(40,28,18,.35)'; c.save(); c.filter = 'blur(14px)'; c.beginPath(); c.ellipse(sx + 230, fy + 40, 300, 34, 0, 0, 7); c.fill(); c.restore();
c.fillStyle = '#56604c'; c.beginPath(); c.roundRect(sx, H * .4, 420, 300, 34); c.fill();
c.fillStyle = '#626c57'; c.beginPath(); c.roundRect(sx - 30, H * .55, 110, 260, 30); c.fill(); c.beginPath(); c.roundRect(sx + 350, H * .55, 110, 260, 30); c.fill();
c.fillStyle = '#6c7661'; c.beginPath(); c.roundRect(sx + 60, H * .6, 310, 120, 22); c.fill();
c.fillStyle = '#3b3328'; c.fillRect(sx + 10, fy + 30, 14, 40); c.fillRect(sx + 400, fy + 30, 14, 40);
/* Wolldecke über der rechten Armlehne */
c.fillStyle = '#e9dfcc'; c.beginPath(); c.roundRect(sx + 338, H * .52, 112, 200, 14); c.fill();
c.fillStyle = 'rgba(150,128,100,.28)'; for (let y = H * .54; y < H * .52 + 190; y += 16) c.fillRect(sx + 342, y, 104, 5);
c.fillStyle = 'rgba(90,70,50,.18)'; c.fillRect(sx + 338, H * .52 + 186, 112, 14);
glanz(c, sx + 240, H * .5, 260, 'rgba(255,240,220,.6)', .25);
const lx = W * .91;
c.fillStyle = '#3e3a37'; c.fillRect(lx - 3, H * .32, 6, fy - H * .32 + 40);
c.fillStyle = '#efe2cc'; c.beginPath(); c.moveTo(lx - 40, H * .24); c.lineTo(lx + 40, H * .24); c.lineTo(lx + 62, H * .33); c.lineTo(lx - 62, H * .33); c.fill();
glanz(c, lx, H * .34, 320, 'rgba(255,214,160,.9)', .45);
vignette(c, W, H, .3);
},
/* 2 · Das Haus zur blauen Stunde */
(c, W, H, r) => {
const hz = H * .6;
c.fillStyle = vl(c, 0, 0, 0, hz, [[0, '#26344c'], [.5, '#4f6285'], [.85, '#a39cab'], [1, '#d2b8ab']]); c.fillRect(0, 0, W, hz);
const kamm = [[0, hz - 120], [140, hz - 200], [260, hz - 170], [380, hz - 260], [470, hz - 210], [560, hz - 240], [700, hz - 150], [900, hz - 170], [1040, hz - 250], [1150, hz - 300], [1240, hz - 230], [1360, hz - 270], [1470, hz - 180], [1600, hz - 150]];
grat(c, kamm, hz, r, 22); c.fillStyle = vl(c, 0, hz - 300, 0, hz, [[0, '#d9e0ea'], [1, '#8d98ad']]); c.fill();
c.save(); grat(c, kamm, hz, zufall(9), 22); c.clip(); schatten(c, kamm, hz, 'rgba(50,62,92,.22)'); c.restore();
c.fillStyle = vl(c, 0, hz, 0, H, [[0, '#c3ccd9'], [1, '#8f9bb0']]); c.fillRect(0, hz - 30, W, H - hz + 30);
c.beginPath(); c.moveTo(0, hz); for (let x = 0; x <= W; x += 20) c.lineTo(x, hz - 30 + 18 * Math.sin(x / 300)); c.lineTo(W, H); c.lineTo(0, H); c.fillStyle = vl(c, 0, hz - 40, 0, H, [[0, '#d5dce6'], [1, '#9aa6ba']]); c.fill();
const x0 = W * .3, x1 = W * .7, y0 = H * .44, y1 = H * .8, ax = (x0 + x1) / 2, ay = H * .2;
c.fillStyle = 'rgba(60,70,95,.35)'; c.save(); c.filter = 'blur(12px)'; c.fillRect(x0 - 40, y1 - 10, x1 - x0 + 160, 40); c.restore();
c.fillStyle = '#4b3526'; c.fillRect(x0, y0, x1 - x0, y1 - y0);
c.fillStyle = 'rgba(30,20,14,.35)'; for (let x = x0; x < x1; x += 22) c.fillRect(x, y0, 2, y1 - y0);
c.fillStyle = '#7c776f'; c.fillRect(x0, H * .69, x1 - x0, y1 - H * .69);
c.fillStyle = '#2a1e16'; c.beginPath(); c.moveTo(ax, ay); c.lineTo(x1 + 70, y0 + 6); c.lineTo(x0 - 70, y0 + 6); c.fill();
c.fillStyle = '#4b3526'; c.beginPath(); c.moveTo(ax, ay + 36); c.lineTo(x1 - 10, y0); c.lineTo(x0 + 10, y0); c.fill();
c.fillStyle = '#eef2f7'; c.beginPath(); c.moveTo(ax, ay - 14); c.lineTo(x1 + 82, y0 - 2); c.lineTo(x1 + 70, y0 + 12); c.lineTo(ax, ay + 10); c.lineTo(x0 - 70, y0 + 12); c.lineTo(x0 - 82, y0 - 2); c.fill();
c.fillStyle = 'rgba(130,145,175,.5)'; c.beginPath(); c.moveTo(ax, ay + 10); c.lineTo(x1 + 70, y0 + 12); c.lineTo(x1 + 66, y0 + 18); c.lineTo(ax, ay + 18); c.fill();
const fenster = (x, y, w, h) => { c.fillStyle = '#ffd38f'; c.fillRect(x, y, w, h); glanz(c, x + w / 2, y + h / 2, w * 1.9, 'rgba(255,190,110,.9)', .45); c.fillStyle = '#4b3526'; c.fillRect(x + w / 2 - 2, y, 4, h); c.fillRect(x, y + h * .45, w, 4); };
for (const y of [H * .48, H * .59, H * .71]) for (let k = 0; k < 5; k++) fenster(x0 + 50 + k * ((x1 - x0 - 100 - 50) / 4), y, 50, 62);
fenster(ax - 26, H * .32, 52, 60);
for (const y of [H * .565, H * .675]) { c.fillStyle = '#3a281c'; c.fillRect(x0 - 26, y, x1 - x0 + 52, 30); c.fillStyle = 'rgba(255,190,110,.3)'; for (let x = x0 - 20; x < x1 + 26; x += 12) c.fillRect(x, y + 4, 3, 22); c.fillStyle = '#eef2f7'; c.fillRect(x0 - 30, y - 5, x1 - x0 + 60, 6); }
c.save(); c.filter = 'blur(20px)'; c.globalCompositeOperation = 'lighter'; c.fillStyle = 'rgba(255,190,120,.28)'; c.fillRect(x0 + 20, y1 + 6, x1 - x0 - 40, 90); c.restore();
for (let i = 0; i < 9; i++) { const x = r() * W * .26, h = 220 + r() * 260; fichte(c, x, H * .86 + r() * 60, h, '#1c2628', 'rgba(235,242,250,.8)'); }
for (let i = 0; i < 9; i++) { const x = W * .76 + r() * W * .26, h = 220 + r() * 280; fichte(c, x, H * .86 + r() * 60, h, '#1c2628', 'rgba(235,242,250,.8)'); }
glanz(c, ax + 90, H * .74, 60, 'rgba(255,200,120,1)', .8);
vignette(c, W, H, .3);
},
/* 3 · Dachkammer, Hochformat 800 × 1000 */
(c, W, H, r) => {
c.fillStyle = vl(c, 0, 0, W, 0, [[0, '#ece5da'], [1, '#d6ccbf']]); c.fillRect(0, 0, W, H);
c.fillStyle = '#c9bdad'; c.beginPath(); c.moveTo(0, 0); c.lineTo(W * .45, 0); c.lineTo(0, H * .3); c.fill();
glanz(c, 0, H * .35, 700, 'rgba(255,246,230,1)', .35);
c.fillStyle = '#9c7654'; c.fillRect(60, H * .3, W - 120, H * .26);
c.fillStyle = 'rgba(70,48,30,.35)'; for (let x = 60; x < W - 60; x += 38) c.fillRect(x, H * .3, 2, H * .26);
c.fillStyle = 'rgba(255,255,255,.12)'; c.fillRect(60, H * .3, W - 120, 6);
c.fillStyle = 'rgba(80,60,40,.25)'; c.save(); c.filter = 'blur(10px)'; c.fillRect(40, H * .5, W - 80, 60); c.restore();
c.fillStyle = '#faf7f2'; c.beginPath(); c.roundRect(110, H * .46, 270, 120, 28); c.fill(); c.beginPath(); c.roundRect(420, H * .46, 270, 120, 28); c.fill();
c.fillStyle = 'rgba(160,145,125,.25)'; c.fillRect(130, H * .55, 230, 8); c.fillRect(440, H * .55, 230, 8);
c.fillStyle = vl(c, 0, H * .56, 0, H, [[0, '#f6f2ec'], [1, '#e2dbd0']]); c.beginPath(); c.moveTo(40, H * .58); c.quadraticCurveTo(W / 2, H * .55, W - 40, H * .58); c.lineTo(W, H); c.lineTo(0, H); c.fill();
c.strokeStyle = 'rgba(150,135,115,.28)'; c.lineWidth = 3; for (let k = 0; k < 6; k++) { c.beginPath(); c.moveTo(80 + r() * 600, H * .62 + r() * 80); c.quadraticCurveTo(300 + r() * 200, H * .66 + r() * 60, 200 + r() * 500, H * .72 + r() * 50); c.stroke(); }
c.fillStyle = '#5c654f'; c.beginPath(); c.moveTo(0, H * .78); c.quadraticCurveTo(W / 2, H * .75, W, H * .78); c.lineTo(W, H * .9); c.quadraticCurveTo(W / 2, H * .87, 0, H * .9); c.fill();
c.strokeStyle = 'rgba(30,36,26,.25)'; c.lineWidth = 2; for (let y = H * .785; y < H * .9; y += 9) { c.beginPath(); c.moveTo(0, y); c.quadraticCurveTo(W / 2, y - 22, W, y); c.stroke(); }
glanz(c, 40, H * .42, 220, 'rgba(255,214,160,.9)', .5);
vignette(c, W, H, .25);
},
];
function malen(i, W, H) { const cv = document.createElement('canvas'); cv.width = W; cv.height = H; const c = cv.getContext('2d'); MALER[i](c, W, H, zufall(31 + i * 77)); return cv.toDataURL('image/jpeg', .9); }
const quellen = [malen(0, 1600, 1000), malen(1, 1600, 1000), malen(2, 1600, 1000), malen(3, 800, 1000)];
$$('[data-bild]').forEach((img, i) => { img.src = quellen[i]; });
$$('[data-raum]').forEach(img => { img.src = quellen[+img.dataset.raum]; });
$('[data-raum="1"]').style.objectPosition = '22% 50%';
$('[data-raum="0"]').style.objectPosition = '40% 50%';
/* ---------- Hero-Diashow ---------- */
const folien = $$('[data-folie]');
const TEXTE = [
{ ort: 'Morgen am Schwarzsee · 7:40 Uhr', zeilen: ['Ein Haus am Fuß', 'des <em>Wilden Kaisers</em>'] },
{ ort: 'Zirbenstube · erster Stock', zeilen: ['Holz, das nach', '<em>Zirbe</em> riecht'] },
{ ort: 'Haus Kaiserlicht · Dezember', zeilen: ['Abends brennt in', 'jedem Fenster <em>Licht</em>'] },
];
const titelEl = $('[data-titel]'), ortEl = $('[data-ort]'), nrEl = $('[data-nr]'), ansage = $('[data-ansage]');
const striche = TEXTE.map((t, i) => {
const b = document.createElement('button'); b.type = 'button'; b.setAttribute('aria-label', `Bild ${i + 1}: ${t.ort}`);
b.innerHTML = '<i></i>'; b.addEventListener('click', () => { stopVorfuehrung(); zeige(i); });
$('[data-striche]').append(b); return b;
});
let aktiv = 0, halte = null, laeuft = [], fortschrittAnim = null;
function stoppe(el) { el.getAnimations().forEach(a => a.cancel()); }
function texteWechseln(i) {
const t = TEXTE[i];
const setzen = () => { ortEl.textContent = t.ort; titelEl.innerHTML = t.zeilen.map(z => `<span class="z">${z}</span>`).join(''); };
if (reduziert()) { setzen(); titelEl.animate([{ opacity: 0 }, { opacity: 1 }], { duration: T.reduziert, easing: LUX }); return; }
const aus = [ortEl, titelEl].map(el => el.animate([{ opacity: 1 }, { opacity: 0 }], { duration: T.textAus, easing: LUX, fill: 'forwards' }));
aus[1].onfinish = () => {
setzen(); stoppe(ortEl); stoppe(titelEl);
/* Zeilen kommen weich: 16 px, sine.out, 1200 ms, 120 ms Versatz */
[ortEl, ...titelEl.children].forEach((el, k) => el.animate(
[{ opacity: 0, transform: `translateY(${T.textY}px)` }, { opacity: 1, transform: 'none' }],
{ duration: T.text, delay: 120 + k * T.stagger, easing: LUX_OUT, fill: 'backwards' }));
};
}
function zeige(i, v = variante) {
if (i === aktiv) return;
const neu = folien[i], alt = folien[aktiv], img = $('img', neu);
folien.forEach(f => { f.style.zIndex = f === neu ? 2 : f === alt ? 1 : 0; });
stoppe(neu); stoppe(img);
neu.classList.add('an');
const fertig = () => { folien.forEach(f => { if (f !== neu) { f.classList.remove('an'); stoppe(f); stoppe($('img', f)); } }); };
let a;
if (reduziert()) {
a = neu.animate([{ opacity: 0 }, { opacity: 1 }], { duration: T.reduziert, easing: LUX });
} else if (v === 'blur') {
const blur = beruehrung ? 0 : 8; /* Blur ist teuer: auf Touch-Geräten weglassen */
a = neu.animate([{ opacity: 0, filter: `blur(${blur}px)` }, { opacity: 1, filter: 'blur(0px)' }], { duration: T.lux, easing: LUX });
} else if (v === 'skalieren') {
a = neu.animate([{ opacity: 0 }, { opacity: 1 }], { duration: T.hell, easing: LUX });
img.animate([{ transform: 'scale(1.08)' }, { transform: 'scale(1)' }], { duration: T.luxLg, easing: LUX_OUT });
} else {
neu.style.opacity = 1;
a = neu.animate([{ clipPath: 'inset(100% 0 0 0)' }, { clipPath: 'inset(0% 0 0 0)' }], { duration: T.maske, easing: LUX });
img.animate([{ transform: 'scale(1.06)' }, { transform: 'scale(1)' }], { duration: T.luxLg, easing: LUX_OUT });
a.onfinish = () => { neu.style.opacity = ''; };
}
a.addEventListener('finish', fertig);
aktiv = i;
texteWechseln(i);
nrEl.textContent = i + 1;
ansage.textContent = `Bild ${i + 1} von ${TEXTE.length}: ${TEXTE[i].ort}`;
strichSetzen();
takt();
}
/* Der aktive Strich füllt sich linear über die Haltezeit (Zeitgeber, keine Gestaltungskurve) */
function strichSetzen() {
striche.forEach((b, k) => { b.setAttribute('aria-current', k === aktiv ? 'true' : 'false'); });
}
function haltezeit() { return html.classList.contains('vorfuehrung') ? 1400 + (variante === 'skalieren' || variante === 'maske' ? T.luxLg : T.lux) : T.halte; }
function takt() {
clearTimeout(halte);
if (fortschrittAnim) fortschrittAnim.cancel();
if (!diashow) return;
const ms = haltezeit();
const i = $('i', striche[aktiv]);
fortschrittAnim = i.animate([{ transform: 'scaleX(0)' }, { transform: 'scaleX(1)' }], { duration: ms, easing: 'linear', fill: 'forwards' });
halte = setTimeout(() => { if (html.classList.contains('vorfuehrung')) naechsteVariante(); zeige((aktiv + 1) % folien.length); }, ms);
}
let diashow = !reduziert();
const halt = $('[data-halt]');
function haltSetzen() { halt.setAttribute('aria-pressed', diashow ? 'false' : 'true'); halt.setAttribute('aria-label', diashow ? 'Diashow anhalten' : 'Diashow starten'); }
halt.addEventListener('click', () => { stopVorfuehrung(); diashow = !diashow; haltSetzen(); takt(); });
/* ---------- Varianten ---------- */
const knoepfe = $$('[data-variante]'), werteEl = $('[data-werte]');
function varianteSetzen(v) {
variante = v;
knoepfe.forEach(b => b.setAttribute('aria-pressed', b.dataset.variante === v ? 'true' : 'false'));
werteEl.textContent = reduziert() ? `Reduziert: nur Überblendung ${T.reduziert} ms, kein Scale, kein Blur` : VARIANTEN[v].werte();
}
knoepfe.forEach(b => b.addEventListener('click', () => { stopVorfuehrung(); varianteSetzen(b.dataset.variante); zeige((aktiv + 1) % folien.length); }));
const reihe = ['blur', 'skalieren', 'maske'];
function naechsteVariante() { varianteSetzen(reihe[(reihe.indexOf(variante) + 1) % reihe.length]); }
/* ---------- Buchungsleiste: Popover mit asymmetrischem Tempo ---------- */
const oeffner = $$('[data-auf]');
function schliesseAlle(ausser) { oeffner.forEach(o => { if (o !== ausser) { o.setAttribute('aria-expanded', 'false'); $('#' + o.dataset.auf).classList.remove('offen'); } }); }
oeffner.forEach(o => o.addEventListener('click', e => {
e.stopPropagation();
const pop = $('#' + o.dataset.auf), auf = o.getAttribute('aria-expanded') !== 'true';
schliesseAlle(o); o.setAttribute('aria-expanded', auf ? 'true' : 'false'); pop.classList.toggle('offen', auf);
}));
document.addEventListener('click', e => { if (!e.target.closest('.pop')) schliesseAlle(); });
document.addEventListener('keydown', e => { if (e.key === 'Escape') { const o = oeffner.find(x => x.getAttribute('aria-expanded') === 'true'); schliesseAlle(); if (o) o.focus(); } });
$$('[data-zeitraum]').forEach(b => b.addEventListener('click', () => {
$$('[data-zeitraum]').forEach(x => x.setAttribute('aria-checked', x === b ? 'true' : 'false'));
$('[data-zeit]').textContent = b.dataset.zeitraum; schliesseAlle();
}));
const gaeste = { erw: 2, kind: 0 };
$$('[data-schritt]').forEach(b => b.addEventListener('click', e => {
e.stopPropagation();
const [art, d] = b.dataset.schritt.split(','); gaeste[art] = Math.max(art === 'erw' ? 1 : 0, Math.min(art === 'erw' ? 4 : 3, gaeste[art] + +d));
$('[data-erw]').value = gaeste.erw; $('[data-kind]').value = gaeste.kind;
$('[data-gaeste]').textContent = `${gaeste.erw} Erwachsene${gaeste.erw === 1 ? 'r' : ''}` + (gaeste.kind ? `, ${gaeste.kind} Kind${gaeste.kind > 1 ? 'er' : ''}` : '');
}));
/* ---------- Zimmer: Scroll-Fortschritt durch eine Sinus-Kurve ---------- */
const karten = $$('[data-karte]');
const sinusInOut = p => -(Math.cos(Math.PI * p) - 1) / 2;
let rafScroll = 0;
function zimmerMalen() {
rafScroll = 0;
const vh = innerHeight;
karten.forEach((k, i) => {
const r = k.getBoundingClientRect();
/* 0 beim Eintritt unten, 1 nach 55 % der Fensterhöhe; Karten 8 % versetzt */
let p = (vh - r.top) / (vh * .55) - i * .08;
p = Math.min(1, Math.max(0, p));
const e = reduziert() ? 1 : sinusInOut(p);
k.style.setProperty('--o', e.toFixed(3));
k.style.setProperty('--s', (1.08 - .08 * e).toFixed(4));
k.style.setProperty('--y', ((1 - e) * 20).toFixed(1) + 'px');
});
}
addEventListener('scroll', () => { if (!rafScroll) rafScroll = requestAnimationFrame(zimmerMalen); }, { passive: true });
addEventListener('resize', zimmerMalen);
zimmerMalen();
/* ---------- Vorführung: Diashow im kurzen Takt, jede Runde eine andere Variante ---------- */
function stopVorfuehrung() { if (html.classList.contains('vorfuehrung')) { html.classList.remove('vorfuehrung'); takt(); } }
function vorfuehren() { html.classList.add('vorfuehrung'); diashow = true; haltSetzen(); clearTimeout(halte); halte = setTimeout(() => zeige((aktiv + 1) % folien.length), 700); }
function pause() { clearTimeout(halte); if (fortschrittAnim) fortschrittAnim.pause(); document.getAnimations().forEach(a => { if (a.playState === 'running') a.pause(); }); }
function weiter() { document.getAnimations().forEach(a => { if (a.playState === 'paused') a.play(); }); takt(); }
function neustart() {
clearTimeout(halte); html.classList.remove('vorfuehrung');
folien.forEach((f, k) => { stoppe(f); stoppe($('img', f)); f.classList.toggle('an', k === 0); f.style.zIndex = ''; });
aktiv = 0; texteWechseln(0); nrEl.textContent = 1; strichSetzen(); varianteSetzen('blur'); diashow = !reduziert(); haltSetzen(); takt();
}
new MutationObserver(() => { varianteSetzen(variante); zimmerMalen(); }).observe(html, { attributes: true, attributeFilter: ['class'] });
varianteSetzen('blur'); strichSetzen(); haltSetzen(); takt();
})();
</script>
</body>
</html>