diff --git a/assets/img/cryo-thron-klein.webp b/assets/img/cryo-thron-klein.webp new file mode 100644 index 00000000..63534776 Binary files /dev/null and b/assets/img/cryo-thron-klein.webp differ diff --git a/assets/img/cryo-thron.webp b/assets/img/cryo-thron.webp new file mode 100644 index 00000000..0f3d3fb5 Binary files /dev/null and b/assets/img/cryo-thron.webp differ diff --git a/pruef-verwaltung-schmuck.mjs b/pruef-verwaltung-schmuck.mjs index 9a957bc9..43a49ecc 100644 --- a/pruef-verwaltung-schmuck.mjs +++ b/pruef-verwaltung-schmuck.mjs @@ -5,6 +5,10 @@ Kostet es Lesbarkeit? Die Verwaltung ist ein Arbeitsplatz -- ein Motiv, das hinter einer Zahlenspalte liegt, ist genau die Art von Schoenheit, die ein Werkzeug unbrauchbar macht. + + Seit der Bereichs-Signatur kommt eine zweite Frage dazu: Sind die + sechs Bereiche wirklich unterscheidbar, oder sieht nur der Code so + aus? Deshalb wird jeder Reiter einzeln angeklickt und gemessen. ===================================================================== */ import { chromium } from "playwright"; import http from "http"; @@ -36,81 +40,67 @@ const pruefe = (name, gut, info) => { else { fehl++; console.log(" FEHL " + name + (info ? " -> " + info : "")); } }; +function leuchtkraft([r, g, b]) { + const f = (v) => { + v /= 255; + return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); + }; + return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b); +} +function verhaeltnis(a, b) { + const l1 = leuchtkraft(a), l2 = leuchtkraft(b); + return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05); +} + +const BEREICHE = ["uebersicht", "anfragen", "projekte", "kunden", "zahlungen", "postfach"]; + +/* Misst, wie stark der dunkle Grund in einem Bereich schwankt. Hohe + Schwankung heisst: Da liegt ein Bild unter der Schrift. */ +async function unruheAn(seite, wahl) { + const bild = await seite.locator(wahl).first().screenshot(); + return seite.evaluate(async (daten) => { + const bl = await fetch("data:image/png;base64," + daten).then((r) => r.blob()); + const bm = await createImageBitmap(bl); + const c = new OffscreenCanvas(bm.width, bm.height); + const x = c.getContext("2d"); + x.drawImage(bm, 0, 0); + const d = x.getImageData(0, 0, bm.width, bm.height).data; + const grund = []; + for (let i = 0; i < d.length; i += 4) { + const h = 0.2126 * d[i] + 0.7152 * d[i + 1] + 0.0722 * d[i + 2]; + if (h < 110) grund.push(h); + } + if (grund.length < 50) return null; + const m = grund.reduce((a, b) => a + b, 0) / grund.length; + return Math.round(Math.sqrt(grund.reduce((a, b) => a + (b - m) ** 2, 0) / grund.length)); + }, bild.toString("base64")); +} + srv.listen(4404, async () => { const browser = await chromium.launch(); + /* ================================================================= + TEIL 1 — die Anmeldemaske + ================================================================= */ for (const [gr, vp] of [["Computer", { width: 1440, height: 950 }], ["Handy", { width: 390, height: 844 }]]) { - console.log(`\n===== ${gr} =====`); + console.log(`\n===== ANMELDUNG (${gr}) =====`); const seite = await browser.newPage({ viewport: vp }); - const skriptfehler = []; - seite.on("pageerror", (e) => skriptfehler.push(String(e).slice(0, 120))); - await seite.goto("http://127.0.0.1:4404/webdesign/verwaltung.html"); - await seite.waitForTimeout(1500); + await seite.waitForTimeout(1400); - /* ---- Der Schmuckstreifen ---- */ - const streifen = await seite.evaluate(() => { - const el = document.querySelector(".vw-schmuck"); - if (!el) return null; - const s = getComputedStyle(el); - const r = el.getBoundingClientRect(); - const treffer = s.backgroundImage.match(/img\/([a-z0-9-]+\.webp)/i); - return { - datei: treffer ? treffer[1] : "(keins)", - deckkraft: parseFloat(s.opacity), - z: parseInt(s.zIndex, 10), - oben: Math.round(r.top), - hoehe: Math.round(r.height), - klicks: s.pointerEvents, - versteckt: el.getAttribute("aria-hidden"), - }; - }); - if (!streifen) { pruefe(`${gr}: Schmuckstreifen vorhanden`, false); continue; } - - const kleinErwartet = vp.width <= 900; - pruefe(`${gr}: richtige Bilddatei`, - kleinErwartet === streifen.datei.includes("-klein"), streifen.datei); - /* Das ist der Kern: Der Streifen darf ANGEDEUTET sein, nicht mehr. */ - pruefe(`${gr}: bleibt sehr leise (unter 20 %)`, - streifen.deckkraft > 0 && streifen.deckkraft < 0.20, - Math.round(streifen.deckkraft * 100) + " %"); - pruefe(`${gr}: liegt hinter dem Inhalt`, streifen.z < 0, "z-index " + streifen.z); - pruefe(`${gr}: faengt keine Klicks ab`, streifen.klicks === "none", streifen.klicks); - pruefe(`${gr}: ist als Zierde ausgezeichnet`, streifen.versteckt === "true"); - /* 67px ist kein Zufallswert, sondern die Hoehe der Kopfleiste: Der - Streifen beginnt direkt darunter. Gemessen wird deshalb gegen die - tatsaechliche Kopfhoehe, nicht gegen eine geratene Zahl. */ - const kopfHoehe = await seite.evaluate(() => { - const k = document.querySelector("#wd-kopf .wd-kopf, #wd-kopf header, #wd-kopf"); - return k ? Math.round(k.getBoundingClientRect().height) : 0; - }); - pruefe(`${gr}: sitzt direkt unter der Kopfleiste`, - streifen.oben <= kopfHoehe + 8, `${streifen.oben}px, Kopf ${kopfHoehe}px`); - - /* ---- Die Anmeldekarte traegt das Wappen ---- */ const login = await seite.evaluate(() => { const el = document.querySelector("#vw-anmeldung"); if (!el) return null; - el.removeAttribute("hidden"); // sonst nichts zu messen + el.removeAttribute("hidden"); const vor = getComputedStyle(el, "::before"); const treffer = vor.backgroundImage.match(/img\/([a-z0-9-]+\.webp)/i); - return { - klassen: el.className, - datei: treffer ? treffer[1] : "(keins)", - deckkraft: parseFloat(vor.opacity), - }; + return { datei: treffer ? treffer[1] : "(keins)", deckkraft: parseFloat(vor.opacity) }; }); pruefe(`${gr}: die Anmeldekarte hat ein Motiv`, login && login.datei.includes("wappen"), login && login.datei); - /* Der eigentliche Test. Deckkraft allein sagt nichts: Ein Motiv mit - hellen Kanten ist bei 20 % stoerender als ein ruhiges bei 50 %. - Was zaehlt, ist wie stark der Untergrund UNTER DER SCHRIFT - schwankt. Gemessen wird das an echten Bildpunkten aus dem - Erklaertext -- genau dort lag beim Bauen der Hundekopf, und die - Zeilen liefen quer ueber Schnauze und Schriftzug. */ - await seite.waitForTimeout(500); + await seite.waitForTimeout(500); const messen = async () => { const bild = await seite.locator("#vw-anmeldung p.wd-klein").first().screenshot(); return seite.evaluate(async (daten) => { @@ -120,8 +110,6 @@ srv.listen(4404, async () => { const x = c.getContext("2d"); x.drawImage(bm, 0, 0); const d = x.getImageData(0, 0, bm.width, bm.height).data; - /* Nur die dunklen Bildpunkte: Die hellen SIND die Schrift, die - soll ja kontrastieren. Interessant ist der Grund dazwischen. */ const grund = []; for (let i = 0; i < d.length; i += 4) { const h = 0.2126 * d[i] + 0.7152 * d[i + 1] + 0.0722 * d[i + 2]; @@ -132,52 +120,260 @@ srv.listen(4404, async () => { return Math.round(Math.sqrt(grund.reduce((a, b) => a + (b - m) ** 2, 0) / grund.length)); }, bild.toString("base64")); }; - const mitBild = await messen(); - /* Die Vergleichsmessung ohne Motiv ist der Kern. Ein fester - Grenzwert waere geraten: Schon die weichgezeichneten Kanten der - Buchstaben allein erzeugen eine Schwankung um 12 -- ein Test - gegen "unter 14" haette also fast nur diese Kanten gemessen und - waere je nach Schriftgroesse zufaellig gruen oder rot. Gefragt - ist, wie viel Unruhe DAS MOTIV hinzufuegt. */ + /* Die Vergleichsmessung ist der Kern. Ein fester Grenzwert waere + geraten: Schon die weichgezeichneten Buchstabenkanten allein + erzeugen eine Schwankung um 12. Gefragt ist, wie viel Unruhe DAS + MOTIV hinzufuegt. */ await seite.evaluate(() => document.querySelector("#vw-anmeldung").classList.remove("wd-motiv", "wd-motiv--wappen")); await seite.waitForTimeout(400); const ohneBild = await messen(); - await seite.evaluate(() => - document.querySelector("#vw-anmeldung").classList.add("wd-motiv", "wd-motiv--wappen")); - await seite.waitForTimeout(400); - + await seite.waitForTimeout(200); const zusatz = (mitBild ?? 99) - (ohneBild ?? 0); pruefe(`${gr}: das Motiv macht den Textgrund kaum unruhiger`, mitBild !== null && ohneBild !== null && zusatz <= 5, `mit ${mitBild}, ohne ${ohneBild} → plus ${zusatz}`); - /* ---- Und jetzt die eigentliche Regel ---- */ - const stoerung = await seite.evaluate(() => { - const schlimm = []; - for (const el of document.querySelectorAll("table, .wd-tabelle, .vw-liste")) { - let p = el; - while (p && p !== document.body) { - if (p.classList && (p.classList.contains("wd-motiv") || - p.classList.contains("vw-schmuck"))) { - schlimm.push(el.tagName + "." + (el.className || "").slice(0, 20)); - break; - } - p = p.parentElement; - } - } - return schlimm; - }); - pruefe(`${gr}: kein Motiv hinter Tabellen oder Listen`, - stoerung.length === 0, stoerung.join(", ") || "keine"); - - pruefe(`${gr}: kein Skriptfehler`, skriptfehler.length === 0, skriptfehler[0] || ""); - - await seite.screenshot({ path: `pruef-vw-schmuck-${gr.toLowerCase()}.png` }); await seite.close(); } + /* ================================================================= + TEIL 2 — die sechs Bereichs-Signaturen + ================================================================= */ + const seite = await browser.newPage({ viewport: { width: 1440, height: 950 } }); + const skriptfehler = []; + seite.on("pageerror", (e) => skriptfehler.push(String(e).slice(0, 140))); + await seite.goto("http://127.0.0.1:4404/webdesign/verwaltung.html"); + await seite.waitForTimeout(1400); + await seite.evaluate(() => { + document.querySelector("#vw-anmeldung").setAttribute("hidden", ""); + document.querySelector("#vw-bereich").removeAttribute("hidden"); + }); + await seite.waitForTimeout(500); + + /* Erst EINMAL echt klicken, um zu beweisen, dass der Reiter die + Signatur ueberhaupt umschaltet. Danach geht das nicht mehr: Beim + Klick laedt die Seite Daten, merkt dass niemand angemeldet ist, und + klappt den Arbeitsbereich korrekterweise wieder zu. Das ist die + Zugangspruefung, kein Fehler -- aber es beendet den Rundgang. + + Deshalb wird der Bereich danach direkt gesetzt, so wie der + Klick-Listener es auch tut. Geprueft wird dabei das Stilblatt, und + das kennt nur das Kennzeichen am Wurzelelement. */ + await seite.click('[data-tab="anfragen"]'); + await seite.waitForTimeout(900); + const nachKlick = await seite.evaluate(() => + document.documentElement.getAttribute("data-vw-bereich")); + console.log("\n===== SCHALTET DER REITER UM? ====="); + pruefe("ein echter Reiterklick setzt den Bereich", nachKlick === "anfragen", nachKlick); + + /* Zurueck in den sichtbaren Zustand fuer die Messreihe. */ + await seite.evaluate(() => { + document.querySelector("#vw-anmeldung").setAttribute("hidden", ""); + document.querySelector("#vw-bereich").removeAttribute("hidden"); + }); + await seite.waitForTimeout(300); + + const gesehen = []; + for (const bereich of BEREICHE) { + console.log(`\n===== BEREICH: ${bereich} =====`); + /* Der Datenabruf laeuft im Hintergrund weiter und klappt den + Arbeitsbereich verzoegert wieder zu (Zugangspruefung). Deshalb vor + JEDER Messung erneut aufmachen, nicht nur einmal am Anfang. */ + await seite.evaluate((b) => { + document.querySelector("#vw-anmeldung").setAttribute("hidden", ""); + document.querySelector("#vw-bereich").removeAttribute("hidden"); + document.documentElement.setAttribute("data-vw-bereich", b); + document.querySelectorAll("[data-tab]").forEach((x) => + x.classList.toggle("vw-an", x.dataset.tab === b)); + }, bereich); + await seite.waitForTimeout(500); + + const d = await seite.evaluate(() => { + const wurzel = document.documentElement.getAttribute("data-vw-bereich"); + const s = document.querySelector(".vw-schmuck"); + const st = getComputedStyle(s); + const treffer = st.backgroundImage.match(/img\/([a-z0-9-]+\.webp)/i); + const reiter = document.querySelector(".vw-filter-knopf.vw-an"); + const rs = getComputedStyle(reiter); + const titel = document.querySelector("#vw-titel"); + /* Untergrund der Seite suchen -- die Reiterflaeche ist + halbdurchsichtig und muss darueber verrechnet werden. */ + const seitenGrund = getComputedStyle(document.body).backgroundColor + .match(/[\d.]+/g).slice(0, 3).map(Number); + return { + wurzel, + datei: treffer ? treffer[1] : "(keins)", + deckkraft: parseFloat(st.opacity), + blick: st.backgroundPosition, + reiterText: reiter.textContent.trim().slice(0, 14), + reiterFarbe: rs.color.match(/[\d.]+/g).slice(0, 3).map(Number), + reiterFlaeche: rs.backgroundColor.match(/[\d.]+/g), + titelFarbe: getComputedStyle(titel).color.match(/[\d.]+/g).slice(0, 3).map(Number), + seitenGrund, + lumen: getComputedStyle(document.querySelector("#vw-bereich")).getPropertyValue("--vw-farbe").trim(), + }; + }); + + pruefe(` Kennzeichen steht am Wurzelelement`, d.wurzel === bereich, d.wurzel); + gesehen.push({ bereich, datei: d.datei, farbe: d.lumen, blick: d.blick }); + + /* Der aktive Reiter muss lesbar bleiben -- auch im Bereich + "Kunden", dessen Leitfarbe Prism Indigo als SCHRIFT durchfaellt. + Genau deshalb faerbt die Leitfarbe dort nur die Flaeche. */ + const a = d.reiterFlaeche.length === 4 ? parseFloat(d.reiterFlaeche[3]) : 1; + const flaeche = d.seitenGrund.map((u, i) => + Math.round(Number(d.reiterFlaeche[i]) * a + u * (1 - a))); + const v = verhaeltnis(d.reiterFarbe, flaeche); + pruefe(` der aktive Reiter bleibt lesbar (AA = 4,5:1)`, v >= 4.5, v.toFixed(2) + ":1"); + + const vt = verhaeltnis(d.titelFarbe, d.seitenGrund); + pruefe(` der Titel bleibt lesbar`, vt >= 4.5, vt.toFixed(2) + ":1"); + /* Frueher stand hier "Deckkraft unter 20 %". Dieser Massstab ist + hinfaellig, seit das Motiv nach rechts gerueckt ist: Es darf jetzt + kraeftig sein, WEIL es nicht mehr hinter der Schrift liegt. Eine + Zahl allein sagt darueber nichts. + + Geprueft wird stattdessen das, worauf es ankommt -- wie ruhig der + Grund unter der Reiterzeile ist, mit Motiv gegen ohne Motiv. */ + const offen = () => seite.evaluate(() => { + document.querySelector("#vw-anmeldung").setAttribute("hidden", ""); + document.querySelector("#vw-bereich").removeAttribute("hidden"); + }); + await offen(); + const grundMit = await unruheAn(seite, ".vw-tabs"); + await seite.evaluate(() => { + document.querySelector(".vw-schmuck").style.visibility = "hidden"; + }); + await offen(); + await seite.waitForTimeout(350); + const grundOhne = await unruheAn(seite, ".vw-tabs"); + await seite.evaluate(() => { + document.querySelector(".vw-schmuck").style.visibility = ""; + }); + await seite.waitForTimeout(250); + const mehr = (grundMit ?? 99) - (grundOhne ?? 0); + pruefe(` der Grund unter den Reitern bleibt ruhig`, + grundMit !== null && grundOhne !== null && mehr <= 6, + `mit ${grundMit}, ohne ${grundOhne} → plus ${mehr}`); + } + + /* ---- Die Kernfrage: sind die sechs wirklich unterscheidbar? ---- */ + console.log("\n===== SIND DIE BEREICHE UNTERSCHEIDBAR? ====="); + for (const g of gesehen) console.log(` ${g.bereich.padEnd(11)} ${g.datei.padEnd(22)} ${g.farbe}`); + + const farben = new Set(gesehen.map((g) => g.farbe)); + pruefe("jeder Bereich hat eine eigene Leitfarbe", farben.size === 6, + farben.size + " von 6 verschieden"); + + /* Motive duerfen sich wiederholen -- es gibt fuenf Bilder fuer sechs + Bereiche. Aber dann MUSS der Ausschnitt ein anderer sein, sonst + sehen zwei Bereiche gleich aus. */ + const doppelt = {}; + for (const g of gesehen) (doppelt[g.datei] ||= []).push(g); + let ausschnittOk = true, hinweis = []; + for (const [datei, liste] of Object.entries(doppelt)) { + if (liste.length < 2) continue; + const blicke = new Set(liste.map((l) => l.blick)); + if (blicke.size !== liste.length) { ausschnittOk = false; hinweis.push(datei); } + else hinweis.push(`${datei}: ${liste.length}x, aber je anderer Ausschnitt`); + } + pruefe("mehrfach genutzte Motive zeigen einen anderen Ausschnitt", + ausschnittOk, hinweis.join("; ") || "kein Motiv doppelt"); + + /* ---- Und die alte Regel gilt weiter ---- */ + const stoerung = await seite.evaluate(() => { + const schlimm = []; + for (const el of document.querySelectorAll("table, .wd-tabelle, .vw-liste")) { + let p = el; + while (p && p !== document.body) { + if (p.classList && (p.classList.contains("wd-motiv") || + p.classList.contains("vw-schmuck"))) { + schlimm.push(el.tagName); + break; + } + p = p.parentElement; + } + } + return schlimm; + }); + pruefe("kein Motiv hinter Tabellen oder Listen", stoerung.length === 0, + stoerung.join(", ") || "keine"); + pruefe("kein Skriptfehler", skriptfehler.length === 0, skriptfehler[0] || ""); + + await seite.evaluate(() => { + document.documentElement.setAttribute("data-vw-bereich", "uebersicht"); + document.querySelectorAll("[data-tab]").forEach((x) => + x.classList.toggle("vw-an", x.dataset.tab === "uebersicht")); + }); + await seite.waitForTimeout(700); + await seite.screenshot({ path: "pruef-vw-schmuck-computer.png" }); + await seite.close(); + + /* ---- Bewegung aus, wenn der Nutzer das so eingestellt hat ---- */ + console.log("\n===== REDUZIERTE BEWEGUNG ====="); + const ruhig = await browser.newPage({ + viewport: { width: 1440, height: 950 }, reducedMotion: "reduce" }); + await ruhig.goto("http://127.0.0.1:4404/webdesign/verwaltung.html"); + await ruhig.waitForTimeout(1200); + await ruhig.evaluate(() => { + document.querySelector("#vw-anmeldung").setAttribute("hidden", ""); + document.querySelector("#vw-bereich").removeAttribute("hidden"); + }); + await ruhig.click('[data-tab="projekte"]'); + await ruhig.waitForTimeout(700); + /* Der Klick klappt den Bereich zu (Zugangspruefung) -- fuer die + Messung wieder aufmachen. Das Kennzeichen hat der Klick gesetzt. */ + await ruhig.evaluate(() => { + document.querySelector("#vw-anmeldung").setAttribute("hidden", ""); + document.querySelector("#vw-bereich").removeAttribute("hidden"); + }); + await ruhig.waitForTimeout(300); + const bewegung = await ruhig.evaluate(() => { + const s = document.querySelector(".vw-schmuck"); + const st = getComputedStyle(s); + return { + animation: st.animationName, + sichtbar: parseFloat(st.opacity), + bereich: document.documentElement.getAttribute("data-vw-bereich"), + }; + }); + pruefe("keine Animation bei reduzierter Bewegung", + bewegung.animation === "none", bewegung.animation); + /* Wichtig: abschalten heisst NICHT verschwinden. Das Motiv muss da + sein, nur ohne Bewegung. */ + pruefe("das Motiv ist trotzdem sichtbar", bewegung.sichtbar > 0.05, + Math.round(bewegung.sichtbar * 100) + " %"); + pruefe("der Bereichswechsel wirkt trotzdem", bewegung.bereich === "projekte", + bewegung.bereich); + await ruhig.close(); + + /* ---- Handy ---- */ + console.log("\n===== HANDY ====="); + const handy = await browser.newPage({ viewport: { width: 390, height: 844 } }); + await handy.goto("http://127.0.0.1:4404/webdesign/verwaltung.html"); + await handy.waitForTimeout(1200); + await handy.evaluate(() => { + document.querySelector("#vw-anmeldung").setAttribute("hidden", ""); + document.querySelector("#vw-bereich").removeAttribute("hidden"); + }); + await handy.evaluate(() => { + document.documentElement.setAttribute("data-vw-bereich", "kunden"); + }); + await handy.waitForTimeout(600); + const hd = await handy.evaluate(() => { + const st = getComputedStyle(document.querySelector(".vw-schmuck")); + const t = st.backgroundImage.match(/img\/([a-z0-9-]+\.webp)/i); + return { datei: t ? t[1] : "(keins)", deckkraft: parseFloat(st.opacity) }; + }); + pruefe("Handy: kleine Bildfassung", hd.datei.includes("-klein"), hd.datei); + const ueberlauf = await handy.evaluate(() => + document.documentElement.scrollWidth - document.documentElement.clientWidth); + pruefe("Handy: nichts laeuft seitlich ueber", ueberlauf <= 1, ueberlauf + "px"); + await handy.screenshot({ path: "pruef-vw-schmuck-handy.png" }); + await handy.close(); + const verpasst = fehlt.filter((f) => /\.(webp|jpg|png|css|js|svg)$/.test(f)); pruefe("keine fehlende Datei", verpasst.length === 0, verpasst.join(", ") || "keine"); diff --git a/pruef-vw-schmuck-computer.png b/pruef-vw-schmuck-computer.png index c66eb238..71ca0860 100644 Binary files a/pruef-vw-schmuck-computer.png and b/pruef-vw-schmuck-computer.png differ diff --git a/pruef-vw-schmuck-handy.png b/pruef-vw-schmuck-handy.png index 7ff44180..1aa35952 100644 Binary files a/pruef-vw-schmuck-handy.png and b/pruef-vw-schmuck-handy.png differ diff --git a/webdesign/sw.js b/webdesign/sw.js index 1f59d14c..ed69a480 100644 --- a/webdesign/sw.js +++ b/webdesign/sw.js @@ -34,7 +34,7 @@ Start alle alten Zwischenspeicher weg. Muss bei jeder Änderung an den Dateien unten hochgezählt werden, sonst hängen Nutzer auf einem alten Stand fest. */ -const CACHE_NAME = "dogfather-webdesign-v35"; +const CACHE_NAME = "dogfather-webdesign-v36"; /* Bausteine, die die Oberfläche zum Anzeigen braucht. Bewusst KEINE HTML-Datei in dieser Liste. */ diff --git a/webdesign/verwaltung.html b/webdesign/verwaltung.html index bd4c67c1..379f399e 100644 --- a/webdesign/verwaltung.html +++ b/webdesign/verwaltung.html @@ -50,28 +50,265 @@ Tabellen oder Zahlen. Ein Bild hinter einer Zahlenspalte ist genau die Art von Schoenheit, die ein Werkzeug unbrauchbar macht. */ + /* ---------- DIE BEREICHS-SIGNATUR ---------- + + Jeder der sechs Bereiche bekommt ein eigenes Motiv UND eine eigene + Leitfarbe. Beides wechselt beim Reiterklick. + + Die Zuordnung ist nicht ausgewuerfelt, sondern gelesen: + + Uebersicht Wappen Die Zentrale, wo alles zusammenlaeuft. + Anfragen Portal Ein Tor. Hier kommt Neues herein. + Projekte Monolith Etwas, das aufrecht steht und gebaut wird. + Kunden Thron Wer bestellt, steht auf dem Podest. + Zahlungen Kristall Der Wert selbst. Dazu Liquid Gold. + Postfach Portal Wieder ein Tor -- Nachrichten gehen durch. + Anderer Ausschnitt, andere Farbe, damit es + trotzdem nicht wie "Anfragen" aussieht. + + Warum das mehr ist als Deko: Nach ein paar Tagen erkennt man den + Bereich an der Farbe, bevor man den Titel gelesen hat. Der Schmuck + wird zur Orientierung. + + Die Regel von vorher gilt unveraendert weiter: NIE ein Motiv hinter + Listen, Tabellen oder Zahlen. Der Streifen ist verschwunden, bevor + die erste Tabelle anfaengt. Ein Bild hinter einer Zahlenspalte ist + genau die Art von Schoenheit, die ein Werkzeug unbrauchbar macht. */ + + /* Die Variablen haengen am WURZELELEMENT, nicht am Arbeitsbereich. + + Beim Bauen hingen sie an #vw-bereich -- und alle sechs Bereiche + zeigten dasselbe Bild. Grund: Der Schmuckstreifen liegt ausserhalb + von #vw-bereich, erbt die Variablen also nie und faellt auf den + Rueckfallwert zurueck. Die Farben wechselten (Reiter und Titel + liegen drinnen), die Motive nicht. */ + :root { + /* Rueckfall, falls das Kennzeichen mal fehlt: Dann sieht es aus wie + die Uebersicht, statt gar nicht. */ + --vw-motiv: url("/assets/img/cryo-wappen.webp"); + --vw-motiv-klein: url("/assets/img/cryo-wappen-klein.webp"); + --vw-farbe: var(--wd-blau-rgb); + --vw-blick: center top; + } + [data-vw-bereich="uebersicht"] { + --vw-motiv: url("/assets/img/cryo-wappen.webp"); + --vw-motiv-klein: url("/assets/img/cryo-wappen-klein.webp"); + --vw-farbe: var(--wd-blau-rgb); + --vw-blick: 76% 52%; + } + [data-vw-bereich="anfragen"] { + --vw-motiv: url("/assets/img/cryo-ring.webp"); + --vw-motiv-klein: url("/assets/img/cryo-ring-klein.webp"); + --vw-farbe: var(--wd-aqua-rgb); + --vw-blick: 72% 46%; + } + [data-vw-bereich="projekte"] { + --vw-motiv: url("/assets/img/cryo-monolith.webp"); + --vw-motiv-klein: url("/assets/img/cryo-monolith-klein.webp"); + --vw-farbe: var(--wd-lila-rgb); + --vw-blick: 80% 46%; + } + [data-vw-bereich="kunden"] { + --vw-motiv: url("/assets/img/cryo-thron.webp"); + --vw-motiv-klein: url("/assets/img/cryo-thron-klein.webp"); + /* Prism Indigo -- hier unbedenklich, weil es NUR Flaeche und Kante + faerbt. Als Schriftfarbe faellt es durch (3,25:1), deshalb steht + weiter unten ausdruecklich, dass die Schrift davon unberuehrt + bleibt. */ + --vw-farbe: var(--wd-indigo-rgb); + --vw-blick: 84% 52%; + } + [data-vw-bereich="zahlungen"] { + --vw-motiv: url("/assets/img/cryo-kristall.webp"); + --vw-motiv-klein: url("/assets/img/cryo-kristall-klein.webp"); + --vw-farbe: var(--wd-gold-rgb); + --vw-blick: center 38%; + } + [data-vw-bereich="postfach"] { + --vw-motiv: url("/assets/img/cryo-ring.webp"); + --vw-motiv-klein: url("/assets/img/cryo-ring-klein.webp"); + --vw-farbe: var(--wd-ion-rgb); + /* Bewusst der andere Bildrand als bei "Anfragen": gleiches Bild, + voellig anderer Ausschnitt. */ + --vw-blick: 12% 54%; + } + + /* Warum 190% statt cover: Bei "cover" skaliert der Browser das Bild + auf die Breite des Streifens -- die volle Bildbreite ist dann immer + sichtbar, und eine waagerechte Ausschnittsangabe bewirkt schlicht + nichts. Genau das ist hier passiert: Alle Werte von 62% bis 84% + zeigten dasselbe Bild. Erst mit einem Zoom ueber 100% entsteht + ueberhaupt Spielraum, den --vw-blick verschieben kann. + + 190% ist so gewaehlt, dass der Streifen auf jeder Bildschirmbreite + gefuellt bleibt: Die Bildbreite ist immer 1,9-mal so breit wie der + Streifen, die Hoehe waechst proportional mit. + + Zu den Ausschnitten (--vw-blick): Sie sind so gewaehlt, dass KEIN + Logo angeschnitten im Streifen haengt. Ein halber Hundekopf mitten + im Bild sieht nach Versehen aus, nicht nach Absicht -- und das Logo + steht ohnehin schon oben links in der Kopfleiste. Gezeigt werden + stattdessen die Kristallformationen, also das, was an den Motiven + tatsaechlich spektakulaer ist. */ .vw-schmuck { position: absolute; top: 0; left: 0; right: 0; - height: clamp(220px, 34vh, 380px); + /* Genau die Kopfzone: Titel plus Reiterreihe, mehr nicht. Der + Streifen ist zu Ende, BEVOR die erste Kachel anfaengt. + + Vorher war er fast doppelt so hoch und reichte in den + Kachelbereich hinein. Dadurch musste er blass bleiben, sonst + haette er durch die halbdurchsichtigen Kacheln geschienen. Kurz + und kraeftig ist beides besser: mehr Wirkung dort, wo Platz ist, + und null Stoerung dort, wo gearbeitet wird. */ + height: clamp(186px, 25vh, 250px); z-index: -1; pointer-events: none; - background: url("/assets/img/cryo-ring.webp") center top / cover no-repeat; - /* Sehr leise. Der Wert ist bewusst niedriger als bei den - oeffentlichen Motiven (.55): Dort traegt das Bild die Stimmung, - hier darf es die Kopfzeile nur andeuten. */ - opacity: .16; - /* Nach unten sauber ausblenden, damit es keine sichtbare Kante - gibt, an der das Bild einfach aufhoert. */ - -webkit-mask-image: linear-gradient(180deg, #000 0%, rgb(0 0 0 / .55) 45%, transparent 100%); - mask-image: linear-gradient(180deg, #000 0%, rgb(0 0 0 / .55) 45%, transparent 100%); + + /* ZWEI Ebenen, und die Reihenfolge ist der ganze Trick: + oben eine Deckschicht, darunter das Motiv. + + Die Deckschicht ist LINKS fast undurchsichtig und wird nach + rechts hin offen. Links stehen Titel und Reiter -- dort bleibt es + ruhig. Rechts ist die Flaeche leer, dort darf das Bild richtig + auftreten. + + Der erste Versuch war gleichmaessig bei 17 % Deckkraft. Ergebnis: + Das Wappen war ueberall gleich schwach zu ahnen, ein Fleck statt + eines Bildes -- und trotzdem lag es hinter der Schrift. Diese + Loesung ist beides zugleich staerker und ruhiger, weil Bild und + Text sich schlicht nicht mehr denselben Platz teilen. */ + background: + /* Bis 46 % voll deckend -- das ist genau die Zone, in der Titel + und Reiterreihe liegen. Der Wert ist gemessen, nicht geschaetzt: + Bei 34 % schob das Thron-Motiv seine hellen Kristallfluegel bis + unter die Reiter und machte den Grund dort merklich unruhiger + als ohne Bild. */ + linear-gradient(100deg, + rgb(3 8 16 / 1) 0%, + rgb(3 8 16 / 1) 46%, + rgb(3 8 16 / .52) 64%, + rgb(3 8 16 / .05) 86%, + rgb(3 8 16 / 0) 100%), + var(--vw-motiv, url("/assets/img/cryo-wappen.webp")) + var(--vw-blick, center top) / 190% auto no-repeat; + + opacity: .82; + + /* Unten ausblenden, damit es keine sichtbare Kante gibt, an der das + Bild einfach aufhoert. */ + -webkit-mask-image: + linear-gradient(180deg, #000 0%, #000 55%, rgb(0 0 0 / .45) 82%, transparent 100%); + mask-image: + linear-gradient(180deg, #000 0%, #000 55%, rgb(0 0 0 / .45) 82%, transparent 100%); } + + /* Die Kristallkante als eigene Ebene: ein sehr schmaler Leuchtsaum in + der Leitfarbe, der den Streifen nach unten abschliesst. */ + .vw-schmuck::after { + content: ""; + position: absolute; + left: 0; right: 0; bottom: 0; + height: 2px; + background: linear-gradient(90deg, + transparent 0%, + rgb(var(--vw-farbe) / .55) 22%, + rgb(var(--vw-farbe) / .18) 50%, + rgb(var(--vw-farbe) / .55) 78%, + transparent 100%); + clip-path: polygon( + 0% 0%, 6% 100%, 13% 0%, 21% 100%, 29% 0%, 37% 100%, 45% 0%, + 53% 100%, 61% 0%, 69% 100%, 77% 0%, 85% 100%, 93% 0%, 100% 100%); + } + + /* Beim Bereichswechsel blendet die Signatur kurz aus und wieder ein. + Ohne das springt das Bild hart um -- mit sieht es aus, als wuerde + der Raum wechseln. */ + @keyframes vw-signatur-ein { + from { opacity: 0; transform: scale(1.05); } + to { opacity: .82; transform: none; } + } + .vw-schmuck { animation: vw-signatur-ein .55s ease-out both; } + @media (max-width: 900px) { - /* Auf dem Handy die kleine Fassung -- 34 KB statt 113 KB fuer ein - Bild, das ohnehin nur angedeutet ist. */ - .vw-schmuck { background-image: url("/assets/img/cryo-ring-klein.webp"); opacity: .13; } + /* Auf dem Handy die kleine Fassung -- ein Bruchteil der Datenmenge + fuer ein Bild, das ohnehin nur angedeutet ist. */ + /* Auf dem Handy ist links kein Platz, den man freihalten koennte -- + die Reiter laufen ueber die ganze Breite. Deshalb hier eine + gleichmaessige Deckschicht und deutlich weniger Bild. */ + .vw-schmuck { + background: + linear-gradient(180deg, + rgb(3 8 16 / .82) 0%, + rgb(3 8 16 / .88) 60%, + rgb(3 8 16 / .95) 100%), + var(--vw-motiv-klein, url("/assets/img/cryo-wappen-klein.webp")) + var(--vw-blick, center top) / 190% auto no-repeat; + opacity: .62; + height: clamp(200px, 30vh, 300px); + } + @keyframes vw-signatur-ein { + from { opacity: 0; transform: scale(1.04); } + to { opacity: .62; transform: none; } + } } + /* Wer Bewegung reduziert haben will, bekommt den Wechsel ohne + Animation -- das Motiv ist trotzdem da. */ + @media (prefers-reduced-motion: reduce) { + .vw-schmuck { animation: none; } + } + + /* Suchen und Abmelden sitzen oben rechts -- also genau dort, wo das + Motiv am hellsten ist. Bedienelemente muessen immer lesbar sein, + egal was dahinter liegt. Deshalb bekommen sie hier einen eigenen, + dichten Grund statt der sonst halbdurchsichtigen Flaeche. + + backdrop-filter ist bewusst nur eine Zugabe: Wo der Browser es + nicht kann, traegt schon die Farbe allein. */ + #vw-bereich .vw-wer { + background: rgb(3 8 16 / .82); + border-radius: 999px; + padding: .3rem .5rem; + -webkit-backdrop-filter: blur(9px); + backdrop-filter: blur(9px); + } + + /* ---------- Die Leitfarbe faerbt mit ---------- */ + + /* Der aktive Reiter traegt die Farbe seines Bereichs. Das ist der + Moment, in dem aus Deko Orientierung wird. */ + #vw-bereich .vw-filter-knopf.vw-an { + border-color: rgb(var(--vw-farbe) / .55); + background: rgb(var(--vw-farbe) / .14); + box-shadow: 0 0 18px -6px rgb(var(--vw-farbe) / .5); + } + /* Die Schrift bleibt ABSICHTLICH hell und wird NICHT eingefaerbt. + Sonst waere der Reiter im Bereich "Kunden" in Prism Indigo + geschrieben -- die einzige Farbe der Palette, die als Text + durchfaellt (3,25:1 gegen die dunklen Flaechen). */ + #vw-bereich .vw-filter-knopf.vw-an { color: var(--wd-text); } + + /* Ein feiner Leuchtstrich unter dem Titel, in der Leitfarbe. + Traegt keine Bedeutung, die nur ueber Farbe laeuft -- der Titel + daneben sagt dasselbe in Worten. */ + #vw-bereich .vw-leiste h1 { position: relative; } + #vw-bereich .vw-leiste h1::after { + content: ""; + position: absolute; + left: 0; bottom: -.32em; + width: clamp(46px, 22%, 120px); + height: 2px; + border-radius: 2px; + background: linear-gradient(90deg, + rgb(var(--vw-farbe) / .95), rgb(var(--vw-farbe) / 0)); + } + + /* Das Licht, das den Kacheln unter dem Mauszeiger folgt, nimmt + ebenfalls die Leitfarbe an. Dadurch fuehlt sich jeder Bereich + anders an, ohne dass irgendwo ein zweites Bild noetig waere. */ + #vw-bereich .wd-karte { --wd-lumen: var(--vw-farbe); } + /* Die Anmeldekarte bekommt das Wappen. Sie ist halbdurchsichtig, das Motiv scheint also durch die Glasflaeche hindurch. @@ -3067,9 +3304,40 @@ var aktiverTab = "uebersicht"; document.getElementById("vw-titel").textContent = ANSICHTEN[aktiverTab].titel; + /* --------------------------------------------------------------------- + Die Bereichs-Signatur umschalten. + + Das Kennzeichen sitzt am Wurzelelement, nicht am Reiter: So kann das + Stilblatt allein daraus Motiv UND Leitfarbe ableiten, und es gibt + genau eine Stelle, an der sich der Bereich aendert. + + Der Umweg ueber "animation neu ausloesen" ist noetig, weil eine + CSS-Animation nicht von selbst neu startet, wenn sich nur eine + Variable aendert. Ohne ihn springt das Bild hart um, statt sanft + zu wechseln. + --------------------------------------------------------------------- */ + function signaturSetzen(bereich) { + if (!ANSICHTEN[bereich]) return; // unbekannt: lieber nichts tun + document.documentElement.setAttribute("data-vw-bereich", bereich); + + var schmuck = document.querySelector(".vw-schmuck"); + if (!schmuck) return; + /* Wer Bewegung reduziert haben will, bekommt keinen Neustart -- die + Animation ist fuer ihn ohnehin abgeschaltet. */ + try { + if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; + } catch (e) { /* aeltere Browser: dann eben mit Animation */ } + schmuck.style.animation = "none"; + void schmuck.offsetWidth; // erzwingt das Neuberechnen + schmuck.style.animation = ""; + } + + signaturSetzen(aktiverTab); + document.querySelectorAll("[data-tab]").forEach(function (b) { b.addEventListener("click", function () { aktiverTab = b.dataset.tab; + signaturSetzen(aktiverTab); document.querySelectorAll("[data-tab]").forEach(function (x) { x.classList.toggle("vw-an", x.dataset.tab === aktiverTab); });