Eigener Videoplayer
Natives video mit eigener, ruhiger Steuerung: Zeitleiste mit Vorschaubild, Lautstärke, selbst gesetzte WebVTT-Untertitel, Vollbild, Tastatur und Zustände per :playing/:paused mit Klassen als Fallback.
Film- und Fotostudios, Regie, Produktionsfirmen: ein Werk im Mittelpunkt, Steuerung tritt beim Abspielen zurück.
Ein natives video-Element mit eigener Steuerung: Play und Pause, Zeitleiste mit Vorschaubild beim Überfahren, Lautstärke, Untertitel an und aus, Vollbild. Untertitel kommen als WebVTT und werden selbst gesetzt, damit sie nie unter der Steuerung verschwinden. Zustände wie spielt, pausiert und lädt steuern das Aussehen: per Media-Pseudoklassen (:playing, :paused, :buffering), wo der Browser sie kennt, sonst per Klassen aus JavaScript. Die Tastatur bedient alles.
Die Standard-Steuerung sieht in jedem Browser anders aus und bricht die Gestaltung einer Film- oder Kursseite. Eine eigene, ruhige Leiste in der Schrift der Marke lässt das Bild wirken. Untertitel und Tastatur machen den Film für mehr Menschen nutzbar, und das Vorschaubild auf der Zeitleiste macht langes Material schnell durchsuchbar.
Wann einsetzen
Showreel und Werkfilm bei Film- und Fotografie-Studios. Kursvideos mit Kapiteln und Tempo-Wahl. Produkt- und Imagefilme, die selbst gehostet werden.
Wann nicht
Wenn Videos bei YouTube oder Vimeo liegen und deren Player eingebunden wird (dort 2-Klick-Lösung statt eigener Steuerung). Für stumme Hintergrund-Loops (dann nur ein Pause-Knopf). Wenn kein Budget für Untertitel da ist: ein Film mit Sprache braucht sie. Raumlicht nicht auf hellem Grund, nicht bei Produkt- oder Kursvideos, in denen der Inhalt zählt, und nicht neben Text, der darauf lesbar bleiben muss.
Varianten
- Showreel: dunkle Bühne, Steuerung blendet beim Abspielen nach kurzer Ruhe aus
- Kursvideo mit Kapiteln: Kapitelmarken auf der Zeitleiste, Kapitelliste, Tempo 1× bis 1,5×, Transkript
- Raumlicht (Ambilight): Die Farben des laufenden Films strahlen weich in die Fläche um den Player (verkleinertes Videobild im Canvas, stark weichgezeichnet), abschaltbar
- Nur-Poster mit Play-Knopf, Video lädt erst nach Klick
- Mini-Player, der beim Wegscrollen in die Ecke wandert
Umsetzung
Markup: figure.player > video (playsinline, preload=metadata, poster) mit source WebM und MP4, darüber eine Steuerleiste aus echten button-Elementen und einer Zeitleiste aus input type=range (aria-valuetext „0:05 von 0:16“). Untertitel: track kind=subtitles mit WebVTT (im Einsatz als Datei, in der Demo als Blob-URL), mode = hidden und die Cues bei cuechange selbst über der Leiste setzen. Vorschau: ein Sprite aus 16 Standbildern (160 × 90) wird beim Überfahren der Zeitleiste auf die Sekunde gesetzt; im Einsatz besser eine Thumbnail-VTT-Datei. Zustände: CSS.supports('selector(:playing)') prüfen; wenn ja, Regeln mit .player:has(video:playing), sonst setzt JavaScript bei play, pause, waiting, playing die Klassen .spielt, .pausiert, .laedt. Beide Wege in getrennten Regeln schreiben, sonst verwirft der Browser die ganze Regel. Tastatur auf dem Player: Leertaste oder K spielt und pausiert, Pfeil links und rechts springen 5 s, Pfeil hoch und runter ändern die Lautstärke, M stumm, C Untertitel, F Vollbild. Vollbild auf dem figure-Element (requestFullscreen), auf iPhone video.webkitEnterFullscreen. Kein Autoplay mit Ton. Videos mit kurzem Keyframe-Abstand kodieren (-g 24), damit das Springen schnell ist. Browserstand (02.11.2026): Safari ab 15.4 und Firefox ab 150 kennen alle Media-Pseudoklassen. Chrome und Edge laut Web Features Explorer und MDN noch nicht (Limited availability); web.dev nannte sie für die Beta von Chrome 156, für die stabile Version fand sich kein Beleg. Der web.dev-Digest August 2026 führt :paused zwar als Baseline, das widerspricht diesen Kompatibilitätsdaten. Deshalb bleiben die JS-Klassen der Hauptweg und die Pseudoklassen eine Verbesserung.
Robust umsetzen
Telefon. Steuerung bleibt sichtbar, solange das Video pausiert. Tippen auf das Bild zeigt die Leiste, ein zweites Tippen spielt oder pausiert. Kein Lautstärke-Regler (iOS regelt nur über die Tasten), nur stumm. Die Zeitleiste hat eine große Trefferfläche, die Vorschau erscheint beim Ziehen. Vollbild über webkitEnterFullscreen.
Ohne Bewegung. Kein automatisches Abspielen, Steuerung ohne Übergänge und ohne Ausblenden.
Barrierefrei. Alle Bedienelemente sind button oder input mit Namen, Play, Untertitel und Stumm melden ihren Zustand mit aria-pressed bzw. wechselndem Namen. Die Zeitleiste nennt die Zeit als aria-valuetext. Untertitel standardmäßig verfügbar und sichtbar (WCAG 1.2.2), Transkript als Alternative. Kein Autoplay mit Ton, Pause jederzeit möglich (WCAG 2.2.2). Fokus sichtbar, die Steuerung blendet bei Fokus nicht aus. Tastenkürzel nur, solange der Player den Fokus hat.
Code
<!DOCTYPE html>
<!--
Eigener Videoplayer
Fundus · Sektionen & Funktionen · https://cecodesign.de/lab/fundus/sektionen/eigener-videoplayer/
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>Eigener Videoplayer · 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 Serif'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/instrument-serif@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
@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: 'Fragment Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/fragment-mono@latest/latin-400-normal.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@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; }
/* Rezept „Bild zuerst“: neutrale Fläche, dunkel für Film; bewegt wird nur die Oberfläche, nie das Bild */
:root {
--grund: #0e0e0e; --text: #f2f2f2; --leise: #8a8a8a; --linie: rgba(242, 242, 242, .16); --platz: #1b1b1b;
--kurve: cubic-bezier(0.455, 0.03, 0.515, 0.955); /* easeInOutQuad, gemessen bei Nadav Kander */
--ui: 200ms; --blende: 350ms;
}
body[data-v="kapitel"] { --grund: #f4f4f2; --text: #141414; --leise: #6b6b6b; --linie: rgba(20, 20, 20, .14); --platz: #e6e6e6; }
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: #0e0e0e; }
body { min-height: 100svh; background: var(--grund); color: var(--text); font: 400 15px/1.5 'Instrument Sans', system-ui, sans-serif; -webkit-font-smoothing: antialiased; transition: background-color var(--blende) var(--kurve), color var(--blende) var(--kurve); }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
a { color: inherit; }
.mono { font: 400 12px/1.45 'Fragment Mono', ui-monospace, monospace; letter-spacing: .02em; font-variant-numeric: tabular-nums; }
:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }
/* ── Ecken-Navigation ─────────────────────────────── */
.kopf { display: flex; justify-content: space-between; align-items: baseline; gap: 24px; padding: 22px clamp(18px, 3vw, 40px); }
.name { font: 400 1.375rem/1 'Instrument Serif', Georgia, serif; text-decoration: none; letter-spacing: -.005em; }
.name span { color: var(--leise); }
.nav { display: flex; gap: 22px; list-style: none; font-size: 14px; }
.nav a { text-decoration: none; color: var(--leise); transition: color var(--ui) var(--kurve); }
.nav a:hover, .nav a[aria-current] { color: var(--text); }
.buehne { width: min(100% - 2 * clamp(18px, 3vw, 40px), 1180px); margin: 0 auto; padding-bottom: 80px; }
.kapitelkopf { display: none; }
/* ── Player ──────────────────────────────────────── */
.player { position: relative; aspect-ratio: 16 / 9; background: #000; overflow: hidden; color: #f2f2f2; container-type: inline-size; user-select: none; -webkit-user-select: none; }
.player video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; display: block; background: #000; }
.player:fullscreen { aspect-ratio: auto; width: 100vw; height: 100vh; }
.flaeche { position: absolute; inset: 0; z-index: 1; cursor: pointer; }
.kein-zeiger .flaeche { cursor: none; }
/* großer Startknopf */
.gross { position: absolute; z-index: 3; left: 50%; top: 50%; width: 76px; height: 76px; margin: -38px 0 0 -38px; border-radius: 50%; display: grid; place-items: center; background: rgba(14, 14, 14, .42); box-shadow: inset 0 0 0 1px rgba(242, 242, 242, .55); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); transition: opacity var(--blende) var(--kurve), background-color var(--ui) var(--kurve); }
.gross:hover { background: rgba(14, 14, 14, .62); }
.gross svg { width: 26px; height: 26px; margin-left: 3px; }
.titelkarte { position: absolute; z-index: 2; left: clamp(16px, 3cqi, 32px); top: clamp(14px, 3cqi, 28px); display: grid; gap: 4px; transition: opacity var(--blende) var(--kurve); pointer-events: none; }
.titelkarte b { font: 400 clamp(1.5rem, 4.4cqi, 2.75rem)/1.05 'Instrument Serif', Georgia, serif; letter-spacing: -.01em; }
.titelkarte span { color: rgba(242, 242, 242, .72); }
/* Lade-Ring */
.ring { position: absolute; z-index: 3; left: 50%; top: 50%; width: 44px; height: 44px; margin: -22px 0 0 -22px; border-radius: 50%; border: 2px solid rgba(242, 242, 242, .2); border-top-color: #f2f2f2; opacity: 0; pointer-events: none; transition: opacity var(--ui) var(--kurve); animation: drehen 900ms linear infinite; }
@keyframes drehen { to { rotate: 360deg; } }
/* Hinweis-Blitz bei Tastatur (+5 s, Lautstärke, Untertitel) */
.blitz { position: absolute; z-index: 4; left: 50%; top: 22%; translate: -50% 0; padding: 8px 14px; border-radius: 999px; background: rgba(14, 14, 14, .72); opacity: 0; pointer-events: none; white-space: nowrap; }
.blitz.an { animation: blitz 900ms var(--kurve) both; }
@keyframes blitz { 0% { opacity: 0; } 20%, 70% { opacity: 1; } 100% { opacity: 0; } }
/* Untertitel: selbst gesetzt, rutschen über die Leiste */
.untertitel { position: absolute; z-index: 2; left: 8%; right: 8%; bottom: 86px; text-align: center; pointer-events: none; transition: bottom var(--blende) var(--kurve), opacity var(--ui) var(--kurve); }
.player:has(.blick.an) .untertitel { opacity: 0; } /* Vorschau hat Vorrang vor dem Untertitel */
.untertitel p { display: inline; font: 500 clamp(15px, 2.3cqi, 24px)/1.5 'Instrument Sans', sans-serif; padding: .12em .45em; background: rgba(8, 8, 8, .74); box-decoration-break: clone; -webkit-box-decoration-break: clone; }
.untertitel p:empty { display: none; }
/* Steuerleiste */
.leiste { position: absolute; z-index: 3; left: 0; right: 0; bottom: 0; padding: 40px clamp(12px, 2.4cqi, 24px) clamp(8px, 1.6cqi, 14px); background: linear-gradient(to top, rgba(0, 0, 0, .72), rgba(0, 0, 0, 0)); transition: opacity var(--blende) var(--kurve), translate var(--blende) var(--kurve); }
.reihe { display: flex; align-items: center; gap: 4px; }
.reihe .luft { flex: 1; }
.knopf { position: relative; width: 40px; height: 40px; display: grid; place-items: center; border-radius: 50%; transition: background-color var(--ui) var(--kurve); }
.knopf:hover { background: rgba(242, 242, 242, .12); }
.knopf svg { width: 22px; height: 22px; }
.knopf:focus-visible { outline: 2px solid #f2f2f2; outline-offset: 0; }
.ut { font: 400 10.5px/1 'Fragment Mono', monospace; letter-spacing: .04em; padding: 4px 4px 3px; border: 1.5px solid currentColor; border-radius: 3px; opacity: .55; transition: opacity var(--ui) var(--kurve), background-color var(--ui) var(--kurve), color var(--ui) var(--kurve); }
[data-ut][aria-pressed="true"] .ut { opacity: 1; background: #f2f2f2; color: #0e0e0e; border-color: #f2f2f2; }
.zeit { padding: 0 10px; color: rgba(242, 242, 242, .85); white-space: nowrap; }
.laut { display: flex; align-items: center; }
.laut input { width: 0; opacity: 0; transition: width var(--ui) var(--kurve), opacity var(--ui) var(--kurve); }
.laut:hover input, .laut:focus-within input { width: 78px; opacity: 1; }
.tempo-knopf { display: none; }
/* Zeitleiste */
.zeitleiste { position: relative; height: 22px; margin: 0 4px 2px; display: flex; align-items: center; cursor: pointer; }
.spur { position: relative; width: 100%; height: 3px; background: rgba(242, 242, 242, .22); transition: height var(--ui) var(--kurve); }
.zeitleiste:hover .spur, .zeitleiste.zieht .spur, .zeitleiste:focus-within .spur { height: 5px; }
.spur i { position: absolute; left: 0; top: 0; bottom: 0; width: 0; }
.gepuffert { background: rgba(242, 242, 242, .3); }
.gespielt { background: #f2f2f2; }
.kopf-punkt { position: absolute; top: 50%; width: 13px; height: 13px; margin: -6.5px 0 0 -6.5px; border-radius: 50%; background: #f2f2f2; left: 0; scale: 0; transition: scale var(--ui) var(--kurve); }
.zeitleiste:hover .kopf-punkt, .zeitleiste.zieht .kopf-punkt, .zeitleiste:focus-within .kopf-punkt { scale: 1; }
.marken i { position: absolute; top: -1px; bottom: -1px; width: 3px; margin-left: -1.5px; background: rgba(0, 0, 0, .85); }
.zeitleiste input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; margin: 0; }
.zeitleiste:has(input:focus-visible) { outline: 2px solid #f2f2f2; outline-offset: 2px; border-radius: 2px; }
.blick { position: absolute; bottom: 30px; left: 0; width: 164px; translate: -50% 0; display: grid; gap: 6px; justify-items: center; opacity: 0; pointer-events: none; transition: opacity var(--ui) var(--kurve); }
.blick .bildchen { width: 160px; height: 90px; background: #1b1b1b url('bilder/eigener-videoplayer-vorschau.jpg') 0 0 / 640px 360px no-repeat; box-shadow: 0 0 0 2px #f2f2f2, 0 12px 30px rgba(0, 0, 0, .5); }
.blick span { padding: 3px 7px; background: rgba(14, 14, 14, .8); border-radius: 3px; white-space: nowrap; }
.blick.an { opacity: 1; }
/* Fehler */
.fehler-box { position: absolute; z-index: 5; inset: 0; display: none; place-content: center; justify-items: center; gap: 10px; text-align: center; padding: 24px; background: #141414; }
.fehler-box b { font: 400 1.6rem/1.1 'Instrument Serif', serif; }
.player.fehler .fehler-box { display: grid; }
/* ── Zustände ──────────────────────────────────────
Weg 1 (JS-Fallback): Klassen .spielt .pausiert .laedt am Player.
Weg 2 (nativ): Media-Pseudoklassen. Eigene Regeln, damit unbekannte Selektoren nichts mitreißen. */
.i-pause { display: none; }
.player.spielt .i-play { display: none; }
.player.spielt .i-pause { display: block; }
.player.laedt .ring { opacity: 1; }
.player.gestartet .gross, .player.gestartet .titelkarte { opacity: 0; pointer-events: none; }
.player.gestartet.pausiert .gross { opacity: 1; pointer-events: auto; }
.player.spielt.ruhig .leiste { opacity: 0; translate: 0 8px; }
.player.spielt.ruhig .untertitel { bottom: 28px; }
@supports selector(:playing) {
.player.nativ:has(video:playing) .i-play { display: none; }
.player.nativ:has(video:playing) .i-pause { display: block; }
.player.nativ:has(video:buffering) .ring { opacity: 1; }
.player.nativ.gestartet:has(video:paused) .gross { opacity: 1; pointer-events: auto; }
.player.nativ.ruhig:has(video:playing) .leiste { opacity: 0; translate: 0 8px; }
.player.nativ.ruhig:has(video:playing) .untertitel { bottom: 28px; }
}
.i-laut-aus { display: none; }
.player.stumm .i-laut { display: none; }
.player.stumm .i-laut-aus { display: block; }
.i-vollbild-aus { display: none; }
.player.vollbild .i-vollbild { display: none; }
.player.vollbild .i-vollbild-aus { display: block; }
.player:focus-within .leiste { opacity: 1 !important; translate: 0 0 !important; }
/* Unter dem Player */
.unterzeile { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 6px 24px; padding-top: 14px; color: var(--leise); }
.unterzeile b { font-weight: 400; color: var(--text); }
.tasten { display: flex; flex-wrap: wrap; gap: 4px 16px; margin-top: 6px; color: var(--leise); }
.tasten kbd { font: inherit; color: var(--text); padding: 0 4px; border: 1px solid var(--linie); border-radius: 3px; }
.arbeiten { list-style: none; margin-top: 48px; border-top: 1px solid var(--linie); }
.arbeiten li { display: grid; grid-template-columns: 3em minmax(0, 1.2fr) minmax(0, 1fr) auto; gap: 12px; align-items: baseline; padding: 14px 0; border-bottom: 1px solid var(--linie); color: var(--leise); }
.arbeiten b { font: 400 1.5rem/1.1 'Instrument Serif', Georgia, serif; color: var(--text); }
body[data-v="kapitel"] .arbeiten { display: none; }
/* ── Variante Kapitel (Kurs/Werkstatt) ───────────── */
.seite-raster { display: block; }
.seitenleiste { display: none; }
body[data-v="kapitel"] .kapitelkopf { display: grid; gap: 8px; padding: 6px 0 26px; }
.kapitelkopf h1 { font: 400 clamp(1.75rem, 1.2rem + 2vw, 2.75rem)/1.1 'Instrument Serif', Georgia, serif; letter-spacing: -.01em; }
body[data-v="kapitel"] .seite-raster { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: 32px; align-items: start; }
body[data-v="kapitel"] .seitenleiste { display: grid; gap: 26px; }
body[data-v="kapitel"] .titelkarte { display: none; }
body[data-v="kapitel"] .tempo-knopf { display: grid; width: auto; padding: 0 8px; border-radius: 20px; }
.kapitel { list-style: none; border-top: 1px solid var(--text); }
.kapitel button { width: 100%; display: grid; grid-template-columns: 2.4em 1fr auto; gap: 10px; align-items: baseline; padding: 12px 0; border-bottom: 1px solid var(--linie); text-align: left; transition: color var(--ui) var(--kurve); }
.kapitel button span:first-child, .kapitel button span:last-child { color: var(--leise); }
.kapitel button[aria-current="true"] { font-weight: 600; }
.kapitel button[aria-current="true"] span:first-child { color: var(--text); }
.kapitel button:hover { color: var(--leise); }
.feld h2 { font: 400 12px/1 'Fragment Mono', monospace; letter-spacing: .06em; text-transform: uppercase; color: var(--leise); margin-bottom: 10px; }
.segment { display: inline-flex; gap: 2px; padding: 3px; border-radius: 999px; box-shadow: inset 0 0 0 1px var(--linie); }
.segment button { padding: 6px 12px; border-radius: 999px; transition: background-color var(--ui) var(--kurve), color var(--ui) var(--kurve); }
.segment button[aria-pressed="true"] { background: var(--text); color: var(--grund); }
.transkript { grid-column: 1; margin-top: -10px; border-top: 1px solid var(--linie); }
.transkript summary { list-style: none; cursor: pointer; padding: 14px 0; display: flex; justify-content: space-between; }
.transkript summary::-webkit-details-marker { display: none; }
.transkript summary::after { content: '+'; font-family: 'Fragment Mono', monospace; }
.transkript[open] summary::after { content: '−'; }
.transkript ol { list-style: none; display: grid; gap: 6px; padding-bottom: 16px; }
.transkript button { display: grid; grid-template-columns: 4em 1fr; gap: 10px; text-align: left; }
.transkript button span:first-child { color: var(--leise); }
body:not([data-v="kapitel"]) .transkript { display: none; }
/* ── Variante Raumlicht (Ambilight): Farben des Films strahlen in die Fläche ── */
.licht { position: relative; }
.ambi, .raumlicht { display: none; }
body[data-v="ambilight"] { --grund: #070708; --licht-rand: clamp(24px, 9vw, 150px); }
body[data-v="ambilight"] .buehne { width: min(100% - 2 * var(--licht-rand), 960px); padding-top: clamp(12px, 4vh, 44px); }
/* Licht reicht seitlich höchstens bis an den Seitenrand (kein Querscrollen) */
body[data-v="ambilight"] .ambi { display: block; position: absolute; z-index: 0; --aus: min(12%, var(--licht-rand)); left: calc(-1 * var(--aus)); width: calc(100% + 2 * var(--aus)); top: -18%; height: 136%; pointer-events: none; filter: blur(40px) saturate(1.5); opacity: .85; -webkit-mask-image: radial-gradient(closest-side, #000 62%, transparent 100%); mask-image: radial-gradient(closest-side, #000 62%, transparent 100%); transition: opacity 600ms var(--kurve); }
body[data-v="ambilight"].licht-aus .ambi { opacity: 0; }
body[data-v="ambilight"] .player { z-index: 1; box-shadow: 0 0 0 1px rgba(242, 242, 242, .06), 0 30px 80px rgba(0, 0, 0, .55); }
body[data-v="ambilight"] .unterzeile, body[data-v="ambilight"] .tasten, body[data-v="ambilight"] .raumlicht, body[data-v="ambilight"] .arbeiten { position: relative; z-index: 1; }
body[data-v="ambilight"] .raumlicht { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; margin-top: 18px; color: var(--leise); }
.schalter { display: inline-flex; align-items: center; gap: 10px; min-height: 44px; padding: 0 16px 0 10px; border-radius: 999px; box-shadow: inset 0 0 0 1px var(--linie); color: var(--text); transition: box-shadow var(--ui) var(--kurve); }
.schalter:hover { box-shadow: inset 0 0 0 1px rgba(242, 242, 242, .4); }
.schiene { position: relative; width: 32px; height: 18px; border-radius: 9px; background: rgba(242, 242, 242, .22); transition: background-color var(--ui) var(--kurve); }
.schiene::after { content: ''; position: absolute; left: 3px; top: 3px; width: 12px; height: 12px; border-radius: 50%; background: #f2f2f2; transition: translate var(--ui) var(--kurve); }
.schalter[aria-pressed="true"] .schiene { background: #d9a05b; }
.schalter[aria-pressed="true"] .schiene::after { translate: 14px 0; background: #141414; }
/* ── Pult ─────────────────────────────────────────── */
.pult { position: fixed; right: 14px; bottom: 14px; z-index: 30; display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding: 8px 10px 8px 12px; background: rgba(20, 20, 20, .9); color: #e8e8e8; border: 1px solid rgba(242, 242, 242, .14); border-radius: 6px; max-width: calc(100vw - 28px); }
.pult, .pult button { font: 500 10.5px/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; }
.pult .gruppe { display: flex; gap: 2px; }
.pult button { color: rgba(232, 232, 232, .65); padding: 7px 9px; border-radius: 4px; }
.pult button[aria-pressed="true"] { background: #e8e8e8; color: #141414; }
.pult .werte { text-transform: none; letter-spacing: .02em; color: rgba(232, 232, 232, .55); }
/* Reduzierte Bewegung */
:root.reduziert *, :root.reduziert *::before, :root.reduziert *::after { transition-duration: .01ms !important; }
:root.reduziert .blitz.an { animation: none; opacity: 1; }
:root.reduziert .ring { animation: none; border-color: rgba(242, 242, 242, .7); }
:root.reduziert .player.spielt.ruhig .leiste { opacity: 1; translate: none; }
/* Vorschau: nur die Bühne */
.vorschau body { overflow: hidden; }
.vorschau .kopf { padding: 18px 28px; }
.vorschau .nav, .vorschau .tasten, .vorschau .transkript { display: none; }
.vorschau .buehne { width: calc(100% - 56px); }
.vorschau body[data-v="ambilight"] { --licht-rand: 80px; }
.vorschau body[data-v="ambilight"] .buehne { width: min(100% - 160px, 800px); padding-top: 8px; }
.vorschau .vorschau-nie { display: none; }
.vorschau body[data-v="kapitel"] .seite-raster { grid-template-columns: minmax(0, 1fr) 230px; gap: 22px; }
.vorschau body[data-v="kapitel"] .kapitelkopf { padding: 0 0 16px; }
.vorschau body[data-v="kapitel"] .kapitelkopf h1 { font-size: 30px; }
/* Telefon und Touch */
@media (max-width: 860px) {
body[data-v="kapitel"] .seite-raster { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
.nav { display: none; }
.buehne { width: 100%; }
.unterzeile, .tasten, body[data-v] .kapitelkopf, .seitenleiste, .transkript { padding-left: 16px; padding-right: 16px; }
.transkript { margin-top: 0; }
.arbeiten li { grid-template-columns: 2.4em 1fr auto; padding-inline: 16px; }
.arbeiten li span:nth-child(3) { grid-column: 2; grid-row: 2; }
.zeit { padding: 0 4px; }
.leiste { padding-top: 28px; }
.untertitel { bottom: 70px; }
.knopf { width: 38px; height: 38px; }
.gross { width: 64px; height: 64px; margin: -32px 0 0 -32px; }
.pult { bottom: 14px; }
body[data-v="ambilight"] { --licht-rand: 16px; }
body[data-v="ambilight"] .buehne { width: calc(100% - 32px); }
body[data-v="ambilight"] .unterzeile, body[data-v="ambilight"] .raumlicht { padding-left: 0; padding-right: 0; }
}
@media (hover: none) {
.laut input { display: none; } /* iOS regelt die Lautstärke nur über die Tasten */
.tasten { display: none; }
.zeitleiste { height: 30px; }
.spur { height: 5px; }
.kopf-punkt { scale: 1; }
}
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }
</style>
</head>
<body data-v="showreel">
<header class="kopf">
<a class="name" href="#start">Sundlicht Film <span>Stralsund</span></a>
<nav aria-label="Hauptnavigation"><ul class="nav"><li><a href="#arbeiten" aria-current="page">Arbeiten</a></li><li><a href="#regie">Regie</a></li><li><a href="#werkstatt">Werkstatt</a></li><li><a href="#kontakt">Kontakt</a></li></ul></nav>
</header>
<main class="buehne">
<div class="kapitelkopf">
<p class="mono" style="color: var(--leise)">Werkstatt · Bildgestaltung · Lektion 3 von 8</p>
<h1>Einen Morgen am Sund drehen</h1>
</div>
<div class="seite-raster">
<div>
<div class="licht">
<canvas class="ambi" width="24" height="14" aria-hidden="true" data-ambi></canvas>
<figure class="player pausiert" data-player aria-label="Videoplayer: Am Sund">
<video data-video playsinline preload="metadata" poster="bilder/eigener-videoplayer-poster.jpg">
<source src="bilder/eigener-videoplayer-am-sund.webm" type="video/webm">
<source src="bilder/eigener-videoplayer-am-sund.mp4" type="video/mp4">
</video>
<div class="flaeche" data-flaeche aria-hidden="true"></div>
<div class="titelkarte"><b>Am Sund</b><span class="mono">Showreel 2026 · 0:16</span></div>
<button type="button" class="gross" data-gross aria-label="Video abspielen"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 4.5v15l12.5-7.5z" fill="currentColor"/></svg></button>
<span class="ring" aria-hidden="true"></span>
<span class="blitz mono" data-blitz aria-hidden="true"></span>
<div class="untertitel" data-untertitel aria-hidden="true"><p></p></div>
<div class="leiste" data-leiste>
<div class="zeitleiste" data-zeitleiste>
<div class="spur"><i class="gepuffert" data-gepuffert></i><i class="gespielt" data-gespielt></i><span class="marken" data-marken></span><span class="kopf-punkt" data-punkt></span></div>
<input type="range" min="0" max="16" step="0.1" value="0" data-zeit aria-label="Zeitleiste" aria-valuetext="0:00 von 0:16">
<div class="blick" data-blick aria-hidden="true"><div class="bildchen" data-bildchen></div><span class="mono" data-blickzeit>0:00</span></div>
</div>
<div class="reihe">
<button type="button" class="knopf" data-play aria-label="Abspielen"><svg viewBox="0 0 24 24" aria-hidden="true"><path class="i-play" d="M7 4.5v15l12.5-7.5z" fill="currentColor"/><path class="i-pause" d="M6.5 5h4v14h-4zM13.5 5h4v14h-4z" fill="currentColor"/></svg></button>
<div class="laut">
<button type="button" class="knopf" data-stumm aria-label="Ton aus"><svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M4 9.5v5h3.5L12 18V6L7.5 9.5z" fill="currentColor" stroke="none"/><path class="i-laut" d="M15.5 9a4 4 0 0 1 0 6M18 6.5a7.5 7.5 0 0 1 0 11"/><path class="i-laut-aus" d="M16 9.5l5 5M21 9.5l-5 5"/></svg></button>
<input type="range" min="0" max="1" step="0.05" value="1" data-lautstaerke aria-label="Lautstärke" aria-valuetext="100 Prozent">
</div>
<span class="zeit mono" data-zeitanzeige>0:00 / 0:16</span>
<span class="luft"></span>
<button type="button" class="knopf tempo-knopf mono" data-tempo-knopf aria-label="Tempo 1-fach, ändern">1×</button>
<button type="button" class="knopf" data-ut aria-pressed="true" aria-label="Untertitel"><span class="ut" aria-hidden="true">UT</span></button>
<button type="button" class="knopf" data-vollbild aria-label="Vollbild"><svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="square"><path class="i-vollbild" d="M4.5 9V4.5H9M15 4.5h4.5V9M19.5 15v4.5H15M9 19.5H4.5V15"/><path class="i-vollbild-aus" d="M9 4.5V9H4.5M19.5 9H15V4.5M15 19.5V15h4.5M4.5 15H9v4.5"/></svg></button>
</div>
</div>
<div class="fehler-box" role="alert"><b>Das Video lässt sich hier nicht abspielen.</b><span class="mono">Ihr Browser kennt keines der Formate.</span><a class="mono" href="bilder/eigener-videoplayer-am-sund.mp4" download>Video laden (MP4, 0,6 MB)</a></div>
</figure>
</div>
<div class="unterzeile mono">
<span><b>Am Sund</b> · Showreel 2026 · Regie Tjark Prüter · Kamera Sundlicht Film</span>
<span>Stralsund, Rügen · 0:16</span>
</div>
<div class="raumlicht"><button type="button" class="schalter" data-raumlicht aria-pressed="true"><span class="schiene" aria-hidden="true"></span>Raumlicht</button><span class="mono">Farbe aus dem laufenden Bild, 12-mal pro Sekunde</span></div>
<p class="tasten mono" aria-label="Tastenkürzel">
<span><kbd>Leertaste</kbd> Play/Pause</span><span><kbd>←</kbd> <kbd>→</kbd> 5 s</span><span><kbd>↑</kbd> <kbd>↓</kbd> Lautstärke</span><span><kbd>M</kbd> stumm</span><span><kbd>C</kbd> Untertitel</span><span><kbd>F</kbd> Vollbild</span>
</p>
<ol class="arbeiten vorschau-nie" aria-label="Weitere Arbeiten">
<li><span class="mono">02</span><b>Kalkwerk</b><span>Dokumentarfilm, 42 min</span><span class="mono">2025</span></li>
<li><span class="mono">03</span><b>Hiddensee im Winter</b><span>Kurzfilm, 11 min</span><span class="mono">2024</span></li>
<li><span class="mono">04</span><b>Bernsteinweg</b><span>Werbefilm für eine Manufaktur</span><span class="mono">2026</span></li>
</ol>
</div>
<aside class="seitenleiste" aria-label="Kapitel und Einstellungen">
<div class="feld"><h2>Kapitel</h2><ol class="kapitel" data-kapitel></ol></div>
<div class="feld"><h2>Tempo</h2><div class="segment" role="group" aria-label="Wiedergabetempo" data-tempo>
<button type="button" data-rate="1" aria-pressed="true">1×</button><button type="button" data-rate="1.25" aria-pressed="false">1,25×</button><button type="button" data-rate="1.5" aria-pressed="false">1,5×</button>
</div></div>
<p class="mono" style="color: var(--leise)">Drehort: Strandpromenade, Alter Markt, Nordmole. Licht: 06:12 Sonnenaufgang, 17:48 blaue Stunde.</p>
</aside>
<details class="transkript">
<summary class="mono">Transkript</summary>
<ol data-transkript></ol>
</details>
</div>
</main>
<div class="pult">
<div class="gruppe" role="group" aria-label="Variante der Demo">
<button type="button" data-variante="showreel" aria-pressed="true">Showreel</button>
<button type="button" data-variante="kapitel" aria-pressed="false">Kapitel</button>
<button type="button" data-variante="ambilight" aria-pressed="false">Raumlicht</button>
</div>
<span class="werte" data-werte></span>
</div>
<script>
(() => {
const html = document.documentElement;
const reduziert = () => html.classList.contains('reduziert');
const $ = s => document.querySelector(s);
const player = $('[data-player]'), video = $('[data-video]');
const zeitInput = $('[data-zeit]'), leiste = $('[data-zeitleiste]'), blick = $('[data-blick]');
const lautInput = $('[data-lautstaerke]'), utKnopf = $('[data-ut]'), utBox = $('[data-untertitel] p');
const DAUER_SOLL = 16;
const KAPITEL = [[0, 'Morgen am Sund'], [5.3, 'Giebel am Alten Markt'], [10.3, 'Hafen bei Nacht']];
/* Zustände: nativ über Media-Pseudoklassen oder Klassen aus JS */
const NATIV = CSS.supports('selector(:playing)');
if (NATIV) player.classList.add('nativ');
const fmt = s => { s = Math.max(0, Math.floor(s || 0)); return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0'); };
const dauer = () => (isFinite(video.duration) && video.duration > 0 ? video.duration : DAUER_SOLL);
const zeitWort = s => { s = Math.floor(s); return (s >= 60 ? Math.floor(s / 60) + ' Minuten ' : '') + (s % 60) + ' Sekunden'; };
/* ── Untertitel als WebVTT (Blob-URL; im Einsatz eine .vtt-Datei) ── */
const VTT = `WEBVTT
1
00:00:00.600 --> 00:00:04.000
Um sechs liegt der Sund noch ganz still.
2
00:00:04.000 --> 00:00:06.300
Drüben schläft Rügen im Dunst.
3
00:00:06.700 --> 00:00:09.800
Wir drehen die Giebel, bevor die Stadt aufwacht.
4
00:00:11.000 --> 00:00:12.600
[Eine Glocke schlägt im Hafen]
5
00:00:13.000 --> 00:00:15.600
Mit dem letzten Licht gehört der Hafen uns.
`;
const track = document.createElement('track');
Object.assign(track, { kind: 'subtitles', srclang: 'de', label: 'Deutsch', default: true, src: URL.createObjectURL(new Blob([VTT], { type: 'text/vtt' })) });
video.appendChild(track);
let utAn = true;
const textspur = () => video.textTracks[0];
function zeigeCue() {
const tt = textspur(); if (!tt) return;
const c = utAn && tt.activeCues && tt.activeCues[0];
utBox.textContent = c ? c.text : '';
}
function bereiteSpur() { const tt = textspur(); if (!tt) return; tt.mode = 'hidden'; tt.oncuechange = zeigeCue; }
bereiteSpur(); video.textTracks.addEventListener && video.textTracks.addEventListener('addtrack', bereiteSpur);
// Transkript aus denselben Zeilen
const ZEILEN = VTT.split('\n\n').slice(1).map(b => { const z = b.trim().split('\n'); const t = z[1].split(' --> ')[0].split(':'); return [(+t[1]) * 60 + parseFloat(t[2]), z.slice(2).join(' ')]; });
$('[data-transkript]').innerHTML = ZEILEN.map(([t, x]) => `<li><button type="button" data-sprung="${t}"><span class="mono">${fmt(t)}</span><span>${x}</span></button></li>`).join('');
$('[data-kapitel]').innerHTML = KAPITEL.map(([t, n], i) => `<li><button type="button" data-sprung="${t}" aria-current="${!i}"><span class="mono">${String(i + 1).padStart(2, '0')}</span><span>${n}</span><span class="mono">${fmt(t)}</span></button></li>`).join('');
/* ── Wiedergabe ─────────────────────────────── */
function setzeKlassen() {
const spielt = !video.paused && !video.ended;
if (!NATIV) { player.classList.toggle('spielt', spielt); player.classList.toggle('pausiert', !spielt); } // Fallback ohne :playing/:paused
player.classList.toggle('stumm', video.muted || video.volume === 0);
const p = $('[data-play]'); p.setAttribute('aria-label', spielt ? 'Pause' : 'Abspielen');
$('[data-gross]').setAttribute('aria-label', spielt ? 'Pause' : 'Video abspielen');
$('[data-stumm]').setAttribute('aria-label', player.classList.contains('stumm') ? 'Ton an' : 'Ton aus');
zeigeWerte();
}
function spielen() { const p = video.play(); if (p && p.catch) p.catch(() => { player.classList.remove('laedt'); setzeKlassen(); }); }
function umschalten() { if (video.paused || video.ended) spielen(); else video.pause(); }
function springe(t) { video.currentTime = Math.max(0, Math.min(dauer() - .05, t)); zeichne(); }
video.addEventListener('play', () => { player.classList.add('gestartet'); setzeKlassen(); wecken(); });
video.addEventListener('pause', () => { setzeKlassen(); wecken(); });
video.addEventListener('ended', () => { setzeKlassen(); wecken(); if (html.classList.contains('vorfuehrung')) { video.currentTime = 0; } });
video.addEventListener('waiting', () => { if (!NATIV) player.classList.add('laedt'); });
['playing', 'canplay', 'seeked'].forEach(e => video.addEventListener(e, () => { player.classList.remove('laedt'); setzeKlassen(); }));
video.addEventListener('volumechange', () => { lautInput.value = video.muted ? 0 : video.volume; lautInput.setAttribute('aria-valuetext', Math.round((video.muted ? 0 : video.volume) * 100) + ' Prozent'); setzeKlassen(); });
video.addEventListener('loadedmetadata', () => { zeitInput.max = dauer(); zeichneMarken(); zeichne(); });
video.addEventListener('timeupdate', zeichne);
video.addEventListener('progress', zeichne);
video.addEventListener('error', () => player.classList.add('fehler'));
video.querySelectorAll('source')[1].addEventListener('error', () => player.classList.add('fehler'));
video.addEventListener('ratechange', () => { const r = video.playbackRate; document.querySelectorAll('[data-rate]').forEach(b => b.setAttribute('aria-pressed', String(+b.dataset.rate === r))); const k = $('[data-tempo-knopf]'); k.textContent = String(r).replace('.', ',') + '×'; k.setAttribute('aria-label', `Tempo ${String(r).replace('.', ',')}-fach, ändern`); });
let schleifeId = 0;
function zeichne() {
const d = dauer(), t = video.currentTime;
const p = (t / d) * 100;
$('[data-gespielt]').style.width = p + '%';
$('[data-punkt]').style.left = p + '%';
if (!leiste.classList.contains('zieht')) zeitInput.value = t;
zeitInput.setAttribute('aria-valuetext', `${zeitWort(t)} von ${zeitWort(d)}`);
$('[data-zeitanzeige]').textContent = `${fmt(t)} / ${fmt(d)}`;
const b = video.buffered; let ende = 0;
for (let i = 0; i < b.length; i++) if (b.start(i) <= t + .5) ende = Math.max(ende, b.end(i));
$('[data-gepuffert]').style.width = (ende / d * 100) + '%';
const k = KAPITEL.reduce((a, c, i) => (t >= c[0] ? i : a), 0);
document.querySelectorAll('[data-kapitel] button').forEach((x, i) => x.setAttribute('aria-current', String(i === k)));
zeigeCue();
}
function lauf() { zeichne(); schleifeId = requestAnimationFrame(lauf); }
video.addEventListener('play', () => { cancelAnimationFrame(schleifeId); lauf(); });
video.addEventListener('pause', () => cancelAnimationFrame(schleifeId));
function zeichneMarken() { $('[data-marken]').innerHTML = KAPITEL.slice(1).map(([t]) => `<i style="left:${t / dauer() * 100}%"></i>`).join(''); }
/* ── Vorschau auf der Zeitleiste (Sprite 4 × 4, ein Bild je Sekunde) ── */
function zeigeBlick(anteil) {
anteil = Math.max(0, Math.min(1, anteil));
const t = anteil * dauer(), i = Math.min(15, Math.floor(t)), breite = leiste.clientWidth;
$('[data-bildchen]').style.backgroundPosition = `${-(i % 4) * 160}px ${-Math.floor(i / 4) * 90}px`;
const kap = body.dataset.v === 'kapitel' ? ' · ' + KAPITEL.reduce((a, c) => (t >= c[0] ? c[1] : a), '') : '';
$('[data-blickzeit]').textContent = fmt(t) + kap;
blick.style.left = Math.max(82, Math.min(breite - 82, anteil * breite)) + 'px';
blick.classList.add('an');
}
const body = document.body;
const anteilVon = e => { const r = leiste.getBoundingClientRect(); return (e.clientX - r.left) / r.width; };
leiste.addEventListener('pointermove', e => { if (e.pointerType === 'mouse' || leiste.classList.contains('zieht')) zeigeBlick(anteilVon(e)); });
leiste.addEventListener('pointerleave', () => { if (!leiste.classList.contains('zieht')) blick.classList.remove('an'); });
leiste.addEventListener('pointerdown', e => { leiste.classList.add('zieht'); zeigeBlick(anteilVon(e)); });
addEventListener('pointerup', () => { if (leiste.classList.contains('zieht')) { leiste.classList.remove('zieht'); blick.classList.remove('an'); } });
zeitInput.addEventListener('input', () => { springe(+zeitInput.value); if (leiste.classList.contains('zieht')) zeigeBlick(+zeitInput.value / dauer()); });
/* ── Bedienung ─────────────────────────────── */
$('[data-play]').addEventListener('click', () => { stoppeVorfuehrung(); umschalten(); });
$('[data-gross]').addEventListener('click', () => { stoppeVorfuehrung(); umschalten(); });
$('[data-flaeche]').addEventListener('click', () => {
stoppeVorfuehrung();
// Touch: erstes Tippen zeigt die Leiste, zweites spielt oder pausiert
if (matchMedia('(hover: none)').matches && player.classList.contains('ruhig')) { wecken(); return; }
umschalten();
});
$('[data-flaeche]').addEventListener('dblclick', () => vollbild());
$('[data-stumm]').addEventListener('click', () => { video.muted = !video.muted; if (!video.muted && video.volume === 0) video.volume = .6; });
lautInput.addEventListener('input', () => { video.volume = +lautInput.value; video.muted = +lautInput.value === 0; });
function untertitel(an) { utAn = an; utKnopf.setAttribute('aria-pressed', String(an)); utKnopf.setAttribute('aria-label', an ? 'Untertitel aus' : 'Untertitel an'); zeigeCue(); zeigeWerte(); }
utKnopf.addEventListener('click', () => untertitel(!utAn));
function vollbild() {
if (document.fullscreenElement) return document.exitFullscreen();
if (player.requestFullscreen) player.requestFullscreen().catch(() => {});
else if (video.webkitEnterFullscreen) video.webkitEnterFullscreen(); // iPhone
}
$('[data-vollbild]').addEventListener('click', vollbild);
document.addEventListener('fullscreenchange', () => { const an = document.fullscreenElement === player; player.classList.toggle('vollbild', an); $('[data-vollbild]').setAttribute('aria-label', an ? 'Vollbild beenden' : 'Vollbild'); });
document.querySelectorAll('[data-sprung]').forEach(b => b.addEventListener('click', () => { stoppeVorfuehrung(); springe(+b.dataset.sprung + .01); if (video.paused) spielen(); }));
document.querySelectorAll('[data-rate]').forEach(b => b.addEventListener('click', () => { video.playbackRate = +b.dataset.rate; }));
$('[data-tempo-knopf]').addEventListener('click', () => { const r = [1, 1.25, 1.5]; video.playbackRate = r[(r.indexOf(video.playbackRate) + 1) % 3]; });
/* Tastatur, solange der Player den Fokus hat */
function blitz(text) { const b = $('[data-blitz]'); b.textContent = text; b.classList.remove('an'); void b.offsetWidth; b.classList.add('an'); }
player.addEventListener('keydown', e => {
if (e.ctrlKey || e.metaKey || e.altKey) return;
const aufLaut = e.target === lautInput, aufKnopf = e.target.tagName === 'BUTTON';
const k = e.key.length === 1 ? e.key.toLowerCase() : e.key;
if ((k === ' ' && !aufKnopf) || k === 'k') { e.preventDefault(); umschalten(); blitz(video.paused ? 'Pause' : 'Play'); }
else if (k === 'ArrowRight' && !aufLaut) { e.preventDefault(); springe(video.currentTime + 5); blitz('+5 s'); }
else if (k === 'ArrowLeft' && !aufLaut) { e.preventDefault(); springe(video.currentTime - 5); blitz('−5 s'); }
else if ((k === 'ArrowUp' || k === 'ArrowDown') && !aufLaut) { e.preventDefault(); video.muted = false; video.volume = Math.max(0, Math.min(1, Math.round((video.volume + (k === 'ArrowUp' ? .1 : -.1)) * 10) / 10)); blitz('Lautstärke ' + Math.round(video.volume * 100) + ' %'); }
else if (k === 'm') { video.muted = !video.muted; blitz(video.muted ? 'Ton aus' : 'Ton an'); }
else if (k === 'c') { untertitel(!utAn); blitz(utAn ? 'Untertitel an' : 'Untertitel aus'); }
else if (k === 'f') vollbild();
else return;
stoppeVorfuehrung(); wecken();
});
/* Leiste blendet beim Abspielen nach 2,5 s Ruhe aus (nicht bei Fokus, nicht reduziert) */
let ruheUhr = 0;
function wecken() {
player.classList.remove('ruhig', 'kein-zeiger'); clearTimeout(ruheUhr);
if (reduziert() || body.dataset.v === 'kapitel' || html.classList.contains('vorfuehrung')) return; // in der Vorführung bleibt die Leiste sichtbar
ruheUhr = setTimeout(() => { if (!video.paused && !player.matches(':focus-within')) player.classList.add('ruhig', 'kein-zeiger'); }, 2500);
}
player.addEventListener('pointermove', wecken);
player.addEventListener('focusin', wecken);
/* ── Variante Raumlicht: 24 × 14 px Canvas, 12 Bilder/s, weich überblendet, per CSS weichgezeichnet ── */
const ambi = $('[data-ambi]'), actx = ambi.getContext('2d');
const AMBI = { breite: 24, hoehe: 14, fps: 12, misch: .35 };
let ambiAn = true, ambiSichtbar = true, ambiLetzte = 0, ambiId = 0;
function ambiMale(quelle, misch) {
try { actx.globalAlpha = misch; actx.drawImage(quelle, 0, 0, AMBI.breite, AMBI.hoehe); actx.globalAlpha = 1; } catch (e) { /* ohne Bild bleibt das letzte Licht stehen */ }
}
const poster = new Image();
poster.onload = () => ambiMale(poster, 1); // Startlicht aus dem Posterbild, auch bei reduzierter Bewegung
poster.src = video.poster;
function ambiTakt(t) {
ambiId = 0;
if (body.dataset.v !== 'ambilight' || !ambiAn || reduziert() || video.paused || !ambiSichtbar || document.hidden) return;
if (t - ambiLetzte >= 1000 / AMBI.fps && video.readyState >= 2) { ambiLetzte = t; ambiMale(video, AMBI.misch); }
ambiId = requestAnimationFrame(ambiTakt);
}
const ambiStart = () => { if (!ambiId) ambiId = requestAnimationFrame(ambiTakt); };
video.addEventListener('play', ambiStart);
video.addEventListener('seeked', () => { if (video.paused && !reduziert() && video.readyState >= 2) ambiMale(video, 1); });
document.addEventListener('visibilitychange', ambiStart);
if ('IntersectionObserver' in window) new IntersectionObserver(e => { ambiSichtbar = e[0].isIntersecting; ambiStart(); }).observe(player);
const lichtKnopf = $('[data-raumlicht]');
lichtKnopf.addEventListener('click', () => { ambiAn = !ambiAn; lichtKnopf.setAttribute('aria-pressed', String(ambiAn)); body.classList.toggle('licht-aus', !ambiAn); ambiStart(); zeigeWerte(); });
/* ── Varianten (Regel 13) ─────────────────── */
const VARIANTEN = ['showreel', 'kapitel', 'ambilight'];
function zeigeWerte() {
if (body.dataset.v === 'ambilight') { $('[data-werte]').textContent = `Raumlicht ${ambiAn ? 'an' : 'aus'} · Canvas 24 × 14 px · ${reduziert() ? 'Standbild' : '12 Bilder/s, Mischung 35 %'} · blur 40 px`; return; }
$('[data-werte]').textContent = `Zustände: ${NATIV ? ':playing (nativ)' : 'Klassen (JS-Fallback)'} · easeInOutQuad 350 ms · Ruhe 2500 ms`;
}
function setzeVariante(v, url) {
v = VARIANTEN.includes(v) ? v : 'showreel';
body.dataset.v = v;
document.querySelectorAll('[data-variante]').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.variante === v)));
if (v === 'kapitel') wecken();
zeichneMarken(); zeigeWerte(); ambiStart();
if (url) { const q = new URLSearchParams(location.search); q.set('v', v); history.replaceState(null, '', '?' + q.toString().replace(/=(?=&|$)/g, '')); }
}
document.querySelectorAll('[data-variante]').forEach(b => b.addEventListener('click', () => setzeVariante(b.dataset.variante, true)));
document.querySelectorAll('a[href^="#"]').forEach(a => a.addEventListener('click', e => e.preventDefault()));
/* ── Vorführung: stumm abspielen, Vorschau auf der Zeitleiste, Untertitel, Sprung, Pause ── */
let uhren = [];
const nach = (ms, f) => uhren.push(setTimeout(f, ms));
function stoppeVorfuehrung() { uhren.forEach(clearTimeout); uhren = []; html.classList.remove('vorfuehrung'); }
function runde() {
video.muted = true; springe(0); untertitel(true); spielen();
nach(2400, () => { springe(video.currentTime + 5); blitz('+5 s'); });
nach(5000, () => zeigeBlick(.18));
[.3, .45, .6, .72].forEach((a, i) => nach(5400 + i * 450, () => zeigeBlick(a)));
nach(7400, () => blick.classList.remove('an'));
nach(7600, () => { untertitel(false); blitz('Untertitel aus'); });
nach(9200, () => { untertitel(true); blitz('Untertitel an'); });
nach(10400, () => { video.pause(); blitz('Pause'); });
nach(12200, () => { spielen(); });
nach(14600, runde);
}
function neustart() { stoppeVorfuehrung(); video.pause(); springe(0); player.classList.remove('gestartet', 'ruhig'); untertitel(true); setzeKlassen(); }
function vorfuehren() { stoppeVorfuehrung(); html.classList.add('vorfuehrung'); runde(); }
function pause() { uhren.forEach(clearTimeout); uhren = []; video.pause(); }
function weiter() { if (html.classList.contains('vorfuehrung') && !uhren.length) runde(); }
setzeVariante(new URLSearchParams(location.search).get('v'));
setzeKlassen(); zeichne();
})();
</script>
</body>
</html>