diff --git a/pruef-verwaltung-schmuck.mjs b/pruef-verwaltung-schmuck.mjs index 43a49ecc..ba0282d6 100644 --- a/pruef-verwaltung-schmuck.mjs +++ b/pruef-verwaltung-schmuck.mjs @@ -1,14 +1,21 @@ /* ===================================================================== - Prueft den Bildschmuck im Verwaltungsbereich. + Prueft die Buehne im Verwaltungsbereich. - Die eigentliche Frage ist nicht "sieht man das Bild?", sondern: - 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. + Das Konzept hat sich umgedreht, und damit auch die Pruefung. - 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. + Vorher: ein schmaler Streifen, auf 190 % gezoomt, davon ein Ausschnitt + gewaehlt und die linke Haelfte voll zugedeckt -- damit Text darauf + lesbar bleibt. Gemessen wurde, ob das Bild leise genug ist. Uebrig + blieb ein Zipfel. + + Jetzt: Das Motiv ist die Buehne -- bildschirmfuellend, ungeschnitten, + ungedimmt. Der Inhalt schwebt als Milchglas darueber. Text steht nie + direkt auf dem Bild. + + Deshalb wird jetzt das Gegenteil geprueft: + 1. Ist das Bild wirklich GANZ da? (kein Zoom, kein Ausschnitt) + 2. Traegt jede Flaeche, auf der gelesen wird, auch Glas? + 3. Bleibt der Text darauf lesbar -- an echten Bildpunkten gemessen? ===================================================================== */ import { chromium } from "playwright"; import http from "http"; @@ -54,8 +61,39 @@ function verhaeltnis(a, b) { 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. */ +/* Setzt einen Bereich und legt Beispielinhalt ein. Ohne Inhalt liesse + sich nicht messen, ob Text ueber der Buehne lesbar bleibt -- und genau + das ist die Frage, an der das ganze Konzept haengt. + + Der Aufbau passiert vor JEDER Messung neu: Der Datenabruf laeuft im + Hintergrund weiter und klappt den Arbeitsbereich verzoegert wieder zu + (Zugangspruefung, kein Fehler). */ +async function bereichAufbauen(seite, bereich) { + await seite.evaluate((b) => { + document.querySelector("#vw-anmeldung").setAttribute("hidden", ""); + const bereichEl = document.querySelector("#vw-bereich"); + bereichEl.removeAttribute("hidden"); + document.documentElement.setAttribute("data-vw-bereich", b); + document.querySelectorAll("[data-tab]").forEach((x) => + x.classList.toggle("vw-an", x.dataset.tab === b)); + + let probe = document.querySelector("#vw-probe"); + if (!probe) { + probe = document.createElement("div"); + probe.id = "vw-probe"; + probe.innerHTML = + '
' + + '

Vanessa Geske

' + + '

vanessa@example.com

' + + '

Zwei Projekte, eines davon laufend.

'; + bereichEl.appendChild(probe); + } + }, bereich); + await seite.waitForTimeout(700); +} + +/* Misst die Schwankung des Untergrunds an echten Bildpunkten. Hohe + Schwankung heisst: Da liegt Bildstruktur unter der Schrift. */ async function unruheAn(seite, wahl) { const bild = await seite.locator(wahl).first().screenshot(); return seite.evaluate(async (daten) => { @@ -68,7 +106,7 @@ async function unruheAn(seite, wahl) { 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 (h < 120) grund.push(h); } if (grund.length < 50) return null; const m = grund.reduce((a, b) => a + b, 0) / grund.length; @@ -78,296 +116,177 @@ async function unruheAn(seite, wahl) { 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===== ANMELDUNG (${gr}) =====`); - const seite = await browser.newPage({ viewport: vp }); - await seite.goto("http://127.0.0.1:4404/webdesign/verwaltung.html"); - await seite.waitForTimeout(1400); - - const login = await seite.evaluate(() => { - const el = document.querySelector("#vw-anmeldung"); - if (!el) return null; - el.removeAttribute("hidden"); - const vor = getComputedStyle(el, "::before"); - const treffer = vor.backgroundImage.match(/img\/([a-z0-9-]+\.webp)/i); - 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); - - await seite.waitForTimeout(500); - const messen = async () => { - const bild = await seite.locator("#vw-anmeldung p.wd-klein").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")); - }; - const mitBild = await messen(); - /* 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.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}`); - - await seite.close(); - } - - /* ================================================================= - TEIL 2 — die sechs Bereichs-Signaturen - ================================================================= */ - const seite = await browser.newPage({ viewport: { width: 1440, height: 950 } }); + const seite = await browser.newPage({ viewport: { width: 1440, height: 900 } }); 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(1500); + + console.log("===== 1. IST DAS BILD GANZ DA? ====="); + await bereichAufbauen(seite, "kunden"); + const buehne = await seite.evaluate(() => { + const el = document.querySelector(".vw-buehne"); + if (!el) return null; + const st = getComputedStyle(el); + const r = el.getBoundingClientRect(); + const treffer = st.backgroundImage.match(/img\/([a-z0-9-]+\.webp)/i); + return { + datei: treffer ? treffer[1] : "(keins)", + groesse: st.backgroundSize, + lage: st.backgroundPosition, + haltung: st.position, + breite: Math.round(r.width), + hoehe: Math.round(r.height), + z: parseInt(st.zIndex, 10), + klicks: st.pointerEvents, + versteckt: el.getAttribute("aria-hidden"), + }; }); - await seite.waitForTimeout(500); + if (!buehne) { pruefe("Buehne vorhanden", false); } + else { + /* Das ist der Kern der Korrektur. Beim ersten Anlauf stand hier + "190% auto" plus ein Ausschnitt wie "84% 46%" -- damit war der + groesste Teil des Bildes weggeschnitten. */ + pruefe("das Bild wird NICHT gezoomt", buehne.groesse === "cover", buehne.groesse); + pruefe("und NICHT einseitig ausgeschnitten", + /50%\s+50%|center/.test(buehne.lage), buehne.lage); + pruefe("die Buehne fuellt den ganzen Bildschirm", + buehne.breite >= 1430 && buehne.hoehe >= 890, `${buehne.breite}x${buehne.hoehe}`); + pruefe("sie steht fest, der Inhalt scrollt darueber", + buehne.haltung === "fixed", buehne.haltung); + pruefe("sie liegt hinter allem", buehne.z < 0, "z-index " + buehne.z); + pruefe("sie faengt keine Klicks ab", buehne.klicks === "none", buehne.klicks); + pruefe("sie ist als Zierde ausgezeichnet", buehne.versteckt === "true"); + } - /* 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"); + console.log("\n===== 2. TRAEGT ALLES GLAS, WORAUF GELESEN WIRD? ====="); + const glas = await seite.evaluate(() => { + const pruefen = [".wd-karte", ".vw-filter-knopf", ".vw-wer"]; + const raus = {}; + for (const w of pruefen) { + const el = document.querySelector("#vw-bereich " + w); + if (!el) { raus[w] = "(nicht gefunden)"; continue; } + const st = getComputedStyle(el); + raus[w] = st.backdropFilter || st.webkitBackdropFilter || "keins"; + } + return raus; }); - await seite.waitForTimeout(300); + for (const [w, wert] of Object.entries(glas)) { + if (wert === "(nicht gefunden)") continue; + pruefe(` ${w} hat Rueckseiten-Unschaerfe`, /blur/.test(wert), wert.slice(0, 34)); + } - const gesehen = []; + console.log("\n===== 3. BLEIBT DER TEXT LESBAR? ====="); 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); + await bereichAufbauen(seite, bereich); - 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"); + const aufGlas = await unruheAn(seite, "#vw-probe .wd-karte"); await seite.evaluate(() => { - document.querySelector(".vw-schmuck").style.visibility = "hidden"; + document.querySelector(".vw-buehne").style.visibility = "hidden"; }); - await offen(); await seite.waitForTimeout(350); - const grundOhne = await unruheAn(seite, ".vw-tabs"); + await bereichAufbauen(seite, bereich); + const ohneBuehne = await unruheAn(seite, "#vw-probe .wd-karte"); await seite.evaluate(() => { - document.querySelector(".vw-schmuck").style.visibility = ""; + document.querySelector(".vw-buehne").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}`); + + const mehr = (aufGlas ?? 99) - (ohneBuehne ?? 0); + /* Etwas Struktur DARF durchkommen -- das ist ja der Reiz. Sie darf + nur nicht so stark werden, dass die Schrift darauf flimmert. */ + pruefe(` ${bereich}: der Kachelgrund bleibt ruhig`, + aufGlas !== null && ohneBuehne !== null && mehr <= 8, + `mit Buehne ${aufGlas}, ohne ${ohneBuehne} → plus ${mehr}`); + + await bereichAufbauen(seite, bereich); + /* Der tatsaechliche Kachelgrund wird aus dem BILD gelesen, nicht aus + dem Stilblatt: Die Kachel ist halbdurchsichtig, ihr rechnerischer + Wert sagt nichts darueber, was am Ende darunter liegt. */ + const stelle = await seite.evaluate(() => { + const k = document.querySelector("#vw-probe .wd-karte"); + const r = k.getBoundingClientRect(); + return { x: Math.round(r.x + r.width * 0.72), y: Math.round(r.y + r.height * 0.62) }; + }); + const punkt = await seite.screenshot({ + clip: { x: stelle.x, y: stelle.y, width: 10, height: 10 } }); + const grundfarbe = await 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; + let r = 0, g = 0, b = 0, n = 0; + for (let i = 0; i < d.length; i += 4) { r += d[i]; g += d[i + 1]; b += d[i + 2]; n++; } + return [Math.round(r / n), Math.round(g / n), Math.round(b / n)]; + }, punkt.toString("base64")); + + const schrift = await seite.evaluate(() => + getComputedStyle(document.querySelector("#vw-probe-text")) + .color.match(/[\d.]+/g).slice(0, 3).map(Number)); + + const v = verhaeltnis(schrift, grundfarbe); + pruefe(` ${bereich}: Text auf der Kachel erfuellt AA (4,5:1)`, + v >= 4.5, v.toFixed(2) + ":1"); } - /* ---- 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`); + console.log("\n===== 4. WECHSELT DER RAUM WIRKLICH? ====="); + const gesehen = []; + for (const bereich of BEREICHE) { + await bereichAufbauen(seite, bereich); + const d = await seite.evaluate(() => { + const st = getComputedStyle(document.querySelector(".vw-buehne")); + const t = st.backgroundImage.match(/img\/([a-z0-9-]+\.webp)/i); + return { + datei: t ? t[1] : "(keins)", + farbe: getComputedStyle(document.documentElement) + .getPropertyValue("--vw-farbe").trim(), + }; + }); + gesehen.push({ bereich, ...d }); + console.log(` ${bereich.padEnd(11)} ${d.datei.padEnd(22)} ${d.farbe}`); } - pruefe("mehrfach genutzte Motive zeigen einen anderen Ausschnitt", - ausschnittOk, hinweis.join("; ") || "kein Motiv doppelt"); + pruefe("jeder Bereich hat eine eigene Leitfarbe", + new Set(gesehen.map((g) => g.farbe)).size === 6, + new Set(gesehen.map((g) => g.farbe)).size + " von 6"); + pruefe("mindestens fuenf verschiedene Motive", + new Set(gesehen.map((g) => g.datei)).size >= 5, + new Set(gesehen.map((g) => g.datei)).size + " verschiedene"); - /* ---- 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 bereichAufbauen(seite, "kunden"); 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 ====="); + console.log("\n===== 5. REDUZIERTE BEWEGUNG ====="); const ruhig = await browser.newPage({ - viewport: { width: 1440, height: 950 }, reducedMotion: "reduce" }); + viewport: { width: 1440, height: 900 }, 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); + await ruhig.waitForTimeout(1300); + await bereichAufbauen(ruhig, "projekte"); 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"), - }; + const st = getComputedStyle(document.querySelector(".vw-buehne")); + return { animation: st.animationName, sichtbar: st.visibility }; }); 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); + /* Abschalten heisst NICHT verschwinden. */ + pruefe("die Buehne ist trotzdem da", bewegung.sichtbar === "visible", bewegung.sichtbar); await ruhig.close(); - /* ---- Handy ---- */ - console.log("\n===== HANDY ====="); + console.log("\n===== 6. 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); + await handy.waitForTimeout(1300); + await bereichAufbauen(handy, "zahlungen"); const hd = await handy.evaluate(() => { - const st = getComputedStyle(document.querySelector(".vw-schmuck")); + const st = getComputedStyle(document.querySelector(".vw-buehne")); const t = st.backgroundImage.match(/img\/([a-z0-9-]+\.webp)/i); - return { datei: t ? t[1] : "(keins)", deckkraft: parseFloat(st.opacity) }; + return { datei: t ? t[1] : "(keins)", groesse: st.backgroundSize }; }); pruefe("Handy: kleine Bildfassung", hd.datei.includes("-klein"), hd.datei); + pruefe("Handy: auch hier kein Zoom", hd.groesse === "cover", hd.groesse); const ueberlauf = await handy.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth); pruefe("Handy: nichts laeuft seitlich ueber", ueberlauf <= 1, ueberlauf + "px"); diff --git a/pruef-vw-schmuck-computer.png b/pruef-vw-schmuck-computer.png index 71ca0860..69b140f1 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 1aa35952..a1ce5733 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 ed69a480..89d4d712 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-v36"; +const CACHE_NAME = "dogfather-webdesign-v37"; /* 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 379f399e..14569756 100644 --- a/webdesign/verwaltung.html +++ b/webdesign/verwaltung.html @@ -133,130 +133,136 @@ --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. + /* ---------- DIE BUEHNE ---------- - 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. + Das Motiv fuellt den ganzen Bildschirm. Fest stehend, ungeschnitten, + in voller Leuchtkraft. Es wird NICHT gezoomt und NICHT einseitig + abgedunkelt. - 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; - /* Genau die Kopfzone: Titel plus Reiterreihe, mehr nicht. Der - Streifen ist zu Ende, BEVOR die erste Kachel anfaengt. + Warum das der zweite Anlauf ist: Vorher war es ein schmaler Streifen + mit 190 % Zoom, aus dem ein Ausschnitt gewaehlt und dessen linke + Haelfte voll zugedeckt wurde -- damit der Text darauf lesbar bleibt. + Uebrig blieb ein Zipfel. Von einer Kristallwelt, die ueber das ganze + Bild geht, war praktisch nichts mehr zu sehen. - 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; + Der Denkfehler war, Bild und Text auf dieselbe Ebene zu zwingen und + dann das Bild zu opfern. Jetzt liegen sie auf zwei Ebenen: Das Bild + ist die Buehne, der Inhalt schwebt als Milchglas darueber. Dadurch + steht Text NIE direkt auf dem Bild -- und das Bild muss nirgends + mehr weichen. */ + + .vw-buehne { + position: fixed; + inset: 0; + z-index: -2; pointer-events: none; - - /* 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%); + background: var(--vw-motiv, url("/assets/img/cryo-wappen.webp")) + center center / cover no-repeat; + /* Kein Zoom: "cover" zeigt bei einem 16:9-Motiv auf einem + ueblichen Bildschirm praktisch das ganze Bild. */ } - /* Die Kristallkante als eigene Ebene: ein sehr schmaler Leuchtsaum in - der Leitfarbe, der den Streifen nach unten abschliesst. */ - .vw-schmuck::after { + /* Eine ruhige Toenung darueber. Sie nimmt dem Bild nicht die Zeichnung, + sondern legt nur so viel Tiefe darunter, dass die Glasflaechen sich + abheben. Oben etwas dichter, weil dort die Kopfleiste sitzt. */ + .vw-buehne::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%); + inset: 0; + background: + radial-gradient(125% 95% at 50% 8%, + rgb(3 8 16 / .18) 0%, rgb(3 8 16 / .38) 52%, rgb(3 8 16 / .60) 100%), + linear-gradient(180deg, + rgb(3 8 16 / .14) 0%, rgb(6 19 35 / .20) 55%, rgb(3 8 16 / .40) 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; } + /* Beim Bereichswechsel wechselt der ganze Raum. */ + @keyframes vw-buehne-ein { + from { opacity: 0; transform: scale(1.06); } + to { opacity: 1; transform: none; } + } + .vw-buehne { animation: vw-buehne-ein .7s cubic-bezier(.16,.8,.3,1) both; } + + @media (prefers-reduced-motion: reduce) { + .vw-buehne { animation: none; } } - .vw-schmuck { animation: vw-signatur-ein .55s ease-out both; } @media (max-width: 900px) { - /* 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 { + /* Auf dem Handy die kleine Bildfassung. Und "cover" schneidet bei + einem hochkant gehaltenen Telefon zwangslaeufig die Seiten weg -- + deshalb hier der Bildmittelpunkt, wo bei allen Motiven das + Hauptgeschehen sitzt. */ + .vw-buehne { + background-image: var(--vw-motiv-klein, url("/assets/img/cryo-wappen-klein.webp")); + background-position: center center; + } + /* Auf kleinen Schirmen liegt mehr Inhalt uebereinander, also etwas + mehr Tiefe unter dem Glas. */ + .vw-buehne::after { 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; } + rgb(3 8 16 / .52) 0%, rgb(6 19 35 / .58) 50%, rgb(3 8 16 / .72) 100%); } } - /* Wer Bewegung reduziert haben will, bekommt den Wechsel ohne - Animation -- das Motiv ist trotzdem da. */ - @media (prefers-reduced-motion: reduce) { - .vw-schmuck { animation: none; } + /* ---------- GLAS ---------- + + Alles, worauf gelesen wird, bekommt eine Rueckseiten-Unschaerfe. + Das Bild bleibt sichtbar, verliert hinter dem Glas aber jede + Struktur -- und genau das macht Text darauf ruhig lesbar. + + Wo der Browser backdrop-filter nicht kann, traegt die Farbe allein: + Deshalb ist der Hintergrund auch ohne Unschaerfe schon dicht genug. + Das ist kein Schoenheitsfehler, sondern der Rueckfall. */ + #vw-bereich .wd-karte, + #vw-bereich .vw-liste, + #vw-bereich .wd-notiz, + #vw-bereich .vw-kachel, + #vw-bereich table { + -webkit-backdrop-filter: blur(22px) saturate(1.15); + backdrop-filter: blur(22px) saturate(1.15); + } + + /* Die Reiter und die Bedienknoepfe schweben ebenfalls. */ + #vw-bereich .vw-filter-knopf, + #vw-bereich .vw-wer { + -webkit-backdrop-filter: blur(16px) saturate(1.1); + backdrop-filter: blur(16px) saturate(1.1); + } + + /* Meldungen wie "Wird geladen" oder "Nichts vorhanden" stehen frei + zwischen den Kacheln -- also direkt auf der Buehne. Sie bekommen + deshalb ihre eigene kleine Glasflaeche statt nur Text zu sein. + Sonst haengt ausgerechnet die Zeile, die etwas erklaeren soll, + ungeschuetzt vor einer Kristallwand. */ + #vw-bereich .vw-laedt, + #vw-bereich .vw-leer { + display: inline-block; + padding: .55rem 1.1rem; + border-radius: 999px; + background: rgb(6 19 35 / .72); + border: 1px solid rgb(var(--vw-farbe) / .22); + -webkit-backdrop-filter: blur(16px); + backdrop-filter: blur(16px); + } + + /* Die Glaskacheln bekommen eine Leuchtkante in der Leitfarbe. Das + bindet Inhalt und Buehne zusammen, statt die Kacheln wie + aufgeklebte Zettel wirken zu lassen. */ + #vw-bereich .wd-karte { + border-color: rgb(var(--vw-farbe) / .30); + box-shadow: + 0 24px 60px -30px rgb(0 0 0 / .85), + inset 0 1px 0 rgb(var(--vw-farbe) / .16); + } + + /* Der Titel steht als einziges frei auf der Buehne. Damit er das + aushaelt, bekommt er einen weichen dunklen Hof -- kein Kasten, der + die Buehne zerschneiden wuerde. */ + #vw-bereich .vw-leiste h1 { + text-shadow: + 0 2px 18px rgb(3 8 16 / .95), + 0 1px 4px rgb(3 8 16 / .9); } /* Suchen und Abmelden sitzen oben rechts -- also genau dort, wo das @@ -1397,7 +1403,7 @@ - +
@@ -3320,7 +3326,7 @@ if (!ANSICHTEN[bereich]) return; // unbekannt: lieber nichts tun document.documentElement.setAttribute("data-vw-bereich", bereich); - var schmuck = document.querySelector(".vw-schmuck"); + var schmuck = document.querySelector(".vw-buehne"); if (!schmuck) return; /* Wer Bewegung reduziert haben will, bekommt keinen Neustart -- die Animation ist fuer ihn ohnehin abgeschaltet. */