diff --git a/web/app/src/style.css b/web/app/src/style.css index 01b16eb..67886aa 100644 --- a/web/app/src/style.css +++ b/web/app/src/style.css @@ -513,8 +513,25 @@ svg { width: 20px; height: 20px; display: block; flex: none; } .detail-hintergrund { position: fixed; inset: 0; z-index: -1; + pointer-events: none; +} + +/* **Scharf, solange nichts darüberliegt.** Die Weichzeichnung steht nicht + * mehr fest hier, sondern wächst mit dem Bildlauf — gerechnet in + * `weichNachBildlauf()`. Ohne Skript bleibt sie bei 0; dann sieht man ein + * scharfes Bild, und das ist kein Fehler, sondern genau der Zustand, in dem + * die Seite aufgeht. */ +.detail-bild { + position: absolute; inset: 0; background-size: cover; background-position: center top; - opacity: .45; filter: blur(28px) saturate(1.1); + /* **Scharf etwas blasser als weich, und das ist gemessen.** Eine + * Weichzeichnung verrührt die hellen Stellen mit den dunklen; ein scharfes + * Bild hat die weisse Stelle genau dort, wo die Zeile steht. Bei „Dark" + * kam der kleinste Abstand der Eckdaten zum Hintergrund auf 4,07∶1, solange + * weichgezeichnet wurde — scharf bei derselben Deckung nur noch auf 2,44∶1. + * Bei 0,28 sind es wieder 4,02∶1: so lesbar wie vorher, nur eben scharf. */ + opacity: var(--det-deckung, .28); + filter: blur(var(--det-weich, 0px)) saturate(1.1); /* Weiter hochgezogen als das Bild selbst: die Unschärfe würde sonst am Rand als harte Kante sichtbar. */ transform: scale(1.1); diff --git a/web/app/src/views/detail.ts b/web/app/src/views/detail.ts index d9c4bbf..92f98cc 100644 --- a/web/app/src/views/detail.ts +++ b/web/app/src/views/detail.ts @@ -108,7 +108,19 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem const d = document.createElement('div'); d.className = 'detail-hintergrund'; const adresse = bildAdresse(schluessel); - if (adresse) d.style.backgroundImage = `url("${adresse}")`; + if (!adresse) return d; + + // Das Bild liegt in einem eigenen Kind: die Weichzeichnung gehört ihm + // allein, und die Fläche darüber bleibt frei für alles, was später noch + // darauf will. + const bild = document.createElement('div'); + bild.className = 'detail-bild'; + bild.style.backgroundImage = `url("${adresse}")`; + d.appendChild(bild); + + // Gesetzt wird auf dem Rahmen, nicht auf dem Bild: die Werte erben nach + // unten, und der Schleier hängt als `::after` am Rahmen. + weichNachBildlauf(d); return d; } @@ -743,3 +755,80 @@ export function detailseite(art: Art, id: string, zurueck: () => void): HTMLElem return wurzel; } + +/* ---------- Der Hintergrund wird erst weich, wenn etwas darüberläuft ---------- */ + +/** Stärkste Weichzeichnung in Bildpunkten — der Wert, der bisher immer galt. */ +const WEICH_HOECHSTENS = 28; + +/** Über wie viel Bildlauf sie aufgebaut wird, mindestens. */ +const STRECKE_MINDESTENS = 320; + +/** Wie kräftig das Bild steht — scharf blasser als weich. Warum: `.detail-bild`. */ +const DECKUNG_SCHARF = 0.28; +const DECKUNG_WEICH = 0.45; + +/** Lässt den Hintergrund mit dem Bildlauf unscharf werden. + * + * **Vorher war er immer unscharf** — 28 Bildpunkte über die ganze Seite, + * damit Text darüber lesbar bleibt. Oben steht aber gar kein Text darüber: + * dort liegt rechts neben dem Plakat freie Fläche, und das Bild ist das + * Beste, was die Seite zu bieten hat. Weich gezeichnet gehört es erst, wenn + * wirklich etwas darüberläuft — die Folgen, die Besetzung, die Medienangaben. + * + * **Die Strecke hängt am Fenster, nicht am Inhalt.** Eine Seite mit zwei + * Folgen und eine mit zweiundzwanzig sollen sich gleich anfühlen; gemessen + * wird deshalb in Bildschirmhöhen, nicht in Abschnitten. + * + * **Quadratisch, nicht geradlinig.** Geradlinig wäre nach einem Fingerbreit + * schon sichtbar weich — und „scharf, solange nichts darüberliegt" wäre + * dahin. So bleiben die ersten Bildpunkte wirkungslos und der Aufbau kommt + * dann, wenn der Kopfbereich tatsächlich verschwindet. + * + * **Auf ganze Bildpunkte gerundet**, weil jeder neue Wert ein neues Bild + * kostet: eine fest stehende, bildschirmfüllende Fläche neu weichzuzeichnen + * ist das Teuerste, was diese Seite tut. 28 Stufen reichen fürs Auge. + */ +function weichNachBildlauf(ziel: HTMLElement): void { + const abbruch = new AbortController(); + let angemeldet = false; + let zuletzt = -1; + + const rechnen = (): void => { + angemeldet = false; + // **Räumt sich selbst ab.** Die Detailseite wird nicht gemerkt; sie + // verschwindet beim Seitenwechsel ersatzlos, und `einhaengen` ruft für + // ungemerkte Ansichten kein Abbruchsignal. Hängt das Ziel nicht mehr am + // Dokument, ist dieser Zuhörer zu Ende. + if (!ziel.isConnected) { abbruch.abort(); return; } + + const strecke = Math.max(STRECKE_MINDESTENS, window.innerHeight * 0.7); + const anteil = Math.min(1, Math.max(0, window.scrollY / strecke)); + const weich = Math.round(anteil * anteil * WEICH_HOECHSTENS); + if (weich === zuletzt) return; + zuletzt = weich; + ziel.style.setProperty('--det-weich', `${weich}px`); + // Die Deckung geht denselben Weg mit — siehe `.detail-bild` im + // Regelwerk: scharf darf das Bild nicht so kräftig stehen wie weich, + // sonst steht die Zeile darüber auf einer weissen Stelle. + const deckung = DECKUNG_SCHARF + + (DECKUNG_WEICH - DECKUNG_SCHARF) * (weich / WEICH_HOECHSTENS); + ziel.style.setProperty('--det-deckung', deckung.toFixed(3)); + }; + + const anstossen = (): void => { + if (angemeldet) return; + angemeldet = true; + requestAnimationFrame(rechnen); + }; + + const wie = { passive: true, signal: abbruch.signal } as const; + window.addEventListener('scroll', anstossen, wie); + window.addEventListener('resize', anstossen, wie); + // **Einmal anstossen statt sofort rechnen.** Gerechnet wird hier noch zu + // früh: `hintergrund()` läuft, bevor die Ansicht am Dokument hängt und + // bevor `einhaengen` den Bildlauf setzt — gemessen würde die Stelle der + // *vorigen* Seite. Ein Bild später stimmt beides, und weil die Rückfrage + // vor dem ersten Zeichnen drankommt, sieht man dazwischen nichts. + anstossen(); +}