Files
dogfather-universe/pruef-verwaltung-schmuck.mjs
T
DogFatherGitandClaude Opus 5 b147ed0d94 Verwaltung: das Bild wird zur Buehne statt zum Streifen
Der erste Anlauf hat die Motive kaputtgemacht. Sie standen in einem
schmalen Streifen, auf 190 % gezoomt, davon ein Ausschnitt gewaehlt und
die linke Haelfte voll zugedeckt -- alles nur, damit Text darauf lesbar
bleibt. Von einer Kristallwelt, die ueber das ganze Bild geht, war ein
Zipfel uebrig.

Der Denkfehler: Bild und Text auf dieselbe Ebene zwingen und dann das
Bild opfern. Jetzt liegen sie auf zwei Ebenen.

DAS BILD IST DIE BUEHNE. Bildschirmfuellend, fest stehend,
ungeschnitten, ungedimmt. Kein Zoom, kein einseitiges Abdunkeln. Beim
Bereichswechsel wechselt der ganze Raum.

DER INHALT SCHWEBT ALS GLAS DARUEBER. Karten, Reiter, Bedienknoepfe und
selbst die Meldung "Wird geladen" bekommen eine Rueckseiten-Unschaerfe.
Das Motiv bleibt sichtbar, verliert hinter dem Glas aber jede Struktur --
und genau das macht Text darauf ruhig lesbar. Dadurch muss das Bild
nirgends mehr weichen.

Die Kacheln tragen eine Leuchtkante in der Leitfarbe des Bereichs. Das
bindet Inhalt und Buehne zusammen, statt die Kacheln wie aufgeklebte
Zettel wirken zu lassen.

Weil der Text jetzt auf Glas steht statt auf dem Bild, konnte auch die
Toenung deutlich zurueckgenommen werden: von .42/.58/.72 auf
.18/.38/.60. Mehr Bild, gleiche Lesbarkeit -- gemessen 12,4 bis 14,5:1
auf der Kachel, verlangt sind 4,5:1.

Die Pruefung ist mitgedreht und misst jetzt das Gegenteil von vorher:
- Wird das Bild NICHT gezoomt und NICHT ausgeschnitten? (frueher stand
  hier "190% auto" und "84% 46%")
- Traegt jede Flaeche, auf der gelesen wird, wirklich Glas?
- Bleibt der Text lesbar -- gemessen an echten Bildpunkten, nicht am
  rechnerischen Wert des Stilblatts. Die Kachel ist halbdurchsichtig,
  ihr Sollwert sagt nichts darueber, was am Ende darunter liegt.

Dazu die Vergleichsmessung: Wie unruhig ist der Kachelgrund MIT Buehne
gegenueber ohne? Gemessen: minus 1 bis plus 1 in allen sechs Bereichen.
Das Glas arbeitet.

Was unveraendert gilt: Bewegung aus bei prefers-reduced-motion, aber die
Buehne bleibt sichtbar -- abschalten heisst nicht verschwinden. Auf dem
Handy die kleine Bildfassung und eine etwas dichtere Toenung, weil dort
mehr Inhalt uebereinander liegt.

Geprueft: 229 Pruefungen gruen (Verwaltung 31, Portfolio 15, CRYONOVA 53,
Blickfang 13, System 5, Abmelden 16, Abbruch 42, Angebot 54).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-25 00:41:13 +02:00

304 lines
13 KiB
JavaScript

/* =====================================================================
Prueft die Buehne im Verwaltungsbereich.
Das Konzept hat sich umgedreht, und damit auch die Pruefung.
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";
import fs from "fs";
import path from "path";
const TYP = {
".html": "text/html", ".css": "text/css", ".js": "text/javascript",
".png": "image/png", ".svg": "image/svg+xml", ".webp": "image/webp",
".json": "application/json", ".ico": "image/x-icon", ".jpg": "image/jpeg",
};
const fehlt = [];
const srv = http.createServer((q, r) => {
const u = decodeURIComponent(q.url.split("?")[0]);
const p = path.join(process.cwd(), u);
if (fs.existsSync(p) && !p.endsWith("/")) {
r.writeHead(200, { "content-type": TYP[path.extname(p)] || "text/plain" });
return r.end(fs.readFileSync(p));
}
fehlt.push(u);
r.writeHead(404);
r.end("");
});
let ok = 0, fehl = 0;
const pruefe = (name, gut, info) => {
if (gut) { ok++; console.log(" ok " + name + (info ? " -> " + 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"];
/* 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 =
'<div class="wd-karte" style="padding:1.4rem;margin-bottom:1rem">' +
'<h2 style="margin-top:0">Vanessa Geske</h2>' +
'<p class="wd-klein wd-leise">[email protected]</p>' +
'<p id="vw-probe-text">Zwei Projekte, eines davon laufend.</p></div>';
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) => {
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 < 120) 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();
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(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"),
};
});
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");
}
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;
});
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));
}
console.log("\n===== 3. BLEIBT DER TEXT LESBAR? =====");
for (const bereich of BEREICHE) {
await bereichAufbauen(seite, bereich);
const aufGlas = await unruheAn(seite, "#vw-probe .wd-karte");
await seite.evaluate(() => {
document.querySelector(".vw-buehne").style.visibility = "hidden";
});
await seite.waitForTimeout(350);
await bereichAufbauen(seite, bereich);
const ohneBuehne = await unruheAn(seite, "#vw-probe .wd-karte");
await seite.evaluate(() => {
document.querySelector(".vw-buehne").style.visibility = "";
});
await seite.waitForTimeout(250);
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");
}
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("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");
pruefe("kein Skriptfehler", skriptfehler.length === 0, skriptfehler[0] || "");
await bereichAufbauen(seite, "kunden");
await seite.screenshot({ path: "pruef-vw-schmuck-computer.png" });
await seite.close();
console.log("\n===== 5. REDUZIERTE BEWEGUNG =====");
const ruhig = await browser.newPage({
viewport: { width: 1440, height: 900 }, reducedMotion: "reduce" });
await ruhig.goto("http://127.0.0.1:4404/webdesign/verwaltung.html");
await ruhig.waitForTimeout(1300);
await bereichAufbauen(ruhig, "projekte");
const bewegung = await ruhig.evaluate(() => {
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);
/* Abschalten heisst NICHT verschwinden. */
pruefe("die Buehne ist trotzdem da", bewegung.sichtbar === "visible", bewegung.sichtbar);
await ruhig.close();
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(1300);
await bereichAufbauen(handy, "zahlungen");
const hd = await handy.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)", 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");
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");
console.log(`\n===== ${ok} bestanden, ${fehl} fehlgeschlagen =====`);
await browser.close();
srv.close();
process.exit(fehl ? 1 : 0);
});