Verwaltung: Bildschmuck an zwei Stellen, beide ausserhalb der Arbeit

Die Verwaltung ist ein Arbeitsplatz. Hier wird nicht geworben, hier
werden Listen gelesen und Zahlen verglichen -- der Schmuck ist deshalb
deutlich zurueckhaltender als auf den oeffentlichen Seiten.

Zwei Stellen, beide bewusst ausserhalb des Arbeitsflusses:

- Ein Ring-Streifen ganz oben, der nach unten wegblendet. Er sitzt
  direkt unter der Kopfleiste und ist verschwunden, bevor die erste
  Tabelle anfaengt. Deckkraft 16 % (Handy 13 %) -- die oeffentlichen
  Motive liegen bei 55 %. Dort traegt das Bild die Stimmung, hier darf
  es die Kopfzeile nur andeuten.
- Das Wappen auf der Anmeldekarte. Dort wird nichts gelesen ausser drei
  Zeilen, also darf es sichtbarer sein.

Was hier ABSICHTLICH nicht passiert: kein Motiv hinter Listen, Tabellen
oder Zahlen. Ein Bild hinter einer Zahlenspalte ist genau die Art von
Schoenheit, die ein Werkzeug unbrauchbar macht. Ein Test haelt das fest.

Ein echter Fehler beim Bauen, den erst der Screenshot zeigte: Das Wappen
stand zuerst auf 38 % und mittig -- der Hundekopf lag genau im
Erklaertext, die Zeilen liefen quer ueber Schnauze und Schriftzug.
Jetzt 16 % und nach unten versetzt, sodass es hinter Eingabefeld und
Knopf sitzt statt hinter den Zeilen. Die Glasflaeche darueber ist hier
dichter als auf den oeffentlichen Seiten.

Der Test dazu misst nicht die Deckkraft, sondern das eigentliche
Problem: wie stark der Untergrund UNTER DER SCHRIFT schwankt, an echten
Bildpunkten aus dem Absatz. Deckkraft allein sagt naemlich nichts -- ein
Motiv mit hellen Kanten ist bei 20 % stoerender als ein ruhiges bei
50 %.

Und er misst im Vergleich, nicht gegen eine geratene Zahl: Schon die
weichgezeichneten Buchstabenkanten allein erzeugen eine Schwankung von
12. Ein fester Grenzwert "unter 14" haette also fast nur diese Kanten
gemessen und waere je nach Schriftgroesse zufaellig gruen oder rot. Der
Test schaltet das Motiv jetzt ab, misst erneut und prueft die Differenz.
Gemessen: mit 14, ohne 12, also plus 2.

Die Tag-Balance von verwaltung.html bleibt unveraendert bei Differenz 1
(vorher 192/191, jetzt 193/192) -- das neue Element ist ausgeglichen,
die alte Meldung ist Altbestand und wurde hier nicht angefasst.

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

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-24 23:41:41 +02:00
co-authored by Claude Opus 5
parent a6c6bbe3ca
commit 9df21fabe1
6 changed files with 270 additions and 3 deletions
+188
View File
@@ -0,0 +1,188 @@
/* =====================================================================
Prueft den Bildschmuck 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.
===================================================================== */
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 : "")); }
};
srv.listen(4404, async () => {
const browser = await chromium.launch();
for (const [gr, vp] of [["Computer", { width: 1440, height: 950 }],
["Handy", { width: 390, height: 844 }]]) {
console.log(`\n===== ${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);
/* ---- 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
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),
};
});
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);
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;
/* 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];
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 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. */
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);
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();
}
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);
});