Filipe: "ich raste aus im ernst, was verstehst du nicht darunter wenn
ich sage das alle kacheln eine andere farbe haben sollen ich seh
immer wieder sehr viele die einfach komplett die gleiche farbe haben."
ER HATTE RECHT, UND MEINE PRUEFUNG WAR TROTZDEM GRUEN.
Der Grund war ein Messfehler, und zwar meiner: pruef-kachelfarben las
`--ton` aus start.css -- die ROHFARBE. Die lag zwischen allen 31
Kacheln weit auseinander (0,0978 in OKLab). Nur steht sie so nie auf
dem Bildschirm.
GEMESSEN an echten Bildpunkten (neu: tools/kachel-echtfarbe.mjs):
Rohfarben: 0,0978 auseinander
Auf dem Bildschirm: 0,0141
Es kamen an: 14,4 Prozent
Fuer ein Auge gleich: ACHT Paare
Drei Stellen haben die Farbe geschluckt, alle in .kachel:
1. Eine Wolke mit 16 Prozent -- in immer DEMSELBEN Blau, auf jeder
Kachel. Der staerkste Gleichmacher der ganzen Wand. Sie traegt
jetzt den Ton der Kachel.
2. Die Vignette legte bis zu 55 Prozent Dunkelblau darueber.
3. Der Grund trug den Ton zu 24 Prozent und ging ab 58 Prozent in
fast reines Schwarz. Zwei Drittel jeder Kachel waren damit
dieselbe Farbe, egal welcher Ton darueber stand.
Danach: 51,1 Prozent kamen an, NULL ununterscheidbare Paare.
UND DANN HAETTE ICH EINEN FEHLER GEGEN EINEN ANDEREN GETAUSCHT.
Gemessen an jedem einzelnen Kacheltext: VORHER war 1 von 31 unter
4,5:1, DANACH 27 von 31. Also genau das, was Filipe schon zweimal
gemeldet hat ("man bekommt fast nichts gelesen"). Kein Messwert hat
es angezeigt -- pruef-buehne nimmt Stichproben und traf die
Kacheltexte nicht.
Behoben mit einem Lesesaum: ein dunkler Verlauf VON UNTEN, der genau
den Streifen abdunkelt, auf dem gelesen wird, und die oberen zwei
Drittel in Ruhe laesst. Dasselbe Mittel, mit dem jede Bildunterschrift
auf einem Foto lesbar gemacht wird.
Texte unter 4,5:1: 1 -> 27 -> 0
Farbe kommt an: 14,4 % -> 51,1 % -> 36,5 %
VERTRAULICH MELDEN IST JETZT KNALLROT.
Filipe: "nicht die kachel draussen soll rot sein sondern die kachel
vertraulich melden nur die soll knall rot sein!!!" Ich hatte screen5
und screen6 vertauscht. Ringtausch ohne neue Farbe:
Vertraulich melden 28 -> 38 (#ff1a1a, knallrot)
Draussen 38 -> 39 (die Farbe, die Regeln & Hilfe hatte)
Regeln & Hilfe 39 -> 28 (das frei gewordene Gruen)
NEU, UND DER EIGENTLICHE LERNEFFEKT:
server/helfer-kachel-echtfarbe.mjs misst beides in EINER Messung --
Farbunterschied UND Lesbarkeit. Wer sie trennt, verbessert das eine
und verliert das andere; genau das ist heute passiert. Benutzt von
der Pruefung und vom Werkzeug, damit es nur eine Fassung gibt.
pruef-kachelfarben 18/0 (war 13), davon fuenf am echten Bildschirm:
- kein Paar sieht auf dem Bildschirm gleich aus (0,0357)
- mindestens ein Drittel der Rohfarbe kommt an (36,5 %)
- jeder Kacheltext erreicht 4,5:1 (schlechtester: 4,80:1)
Mit drittem Ausgang: kein Browser heisst "konnte nicht nachsehen",
nicht "in Ordnung".
Co-Authored-By: Claude Opus 5 <[email protected]>
194 lines
8.7 KiB
JavaScript
194 lines
8.7 KiB
JavaScript
/* =====================================================================
|
|
WELCHE FARBE HAT EINE KACHEL WIRKLICH? (22.09.2026)
|
|
---------------------------------------------------------------------
|
|
Filipe: „ich seh immer wieder sehr viele die einfach komplett die
|
|
gleiche farbe haben ich will dass sie wirklich alle alle alle
|
|
komplett unterschiedlich sind."
|
|
|
|
Und er hat recht, obwohl `pruef-kachelfarben` grün war. Der Grund
|
|
ist ein Messfehler, und zwar meiner:
|
|
|
|
`pruef-kachelfarben` misst `--ton` — die ROHFARBE aus start.css.
|
|
Die ist zwischen allen 31 Kacheln weit auseinander (0,0978 in
|
|
OKLab). Nur steht sie so nie auf dem Bildschirm: Die Kachel zeichnet
|
|
sie als Nebel mit 5 bis 9 Prozent Deckkraft über einen dunklen
|
|
Grund. Bei 5 Prozent sind 31 verschiedene Farben 31 mal fast
|
|
dasselbe Dunkelblau.
|
|
|
|
Dieses Werkzeug fotografiert die Wand und liest die ECHTEN
|
|
Bildpunkte aus — genau das, was ein Auge sieht. Es nimmt bewusst
|
|
NICHT eine Stelle, sondern den Durchschnitt über die ganze Kachel:
|
|
Ein Sternenfeld hat helle Punkte, und eine Einzelmessung träfe
|
|
vielleicht einen davon.
|
|
|
|
AUFRUF
|
|
node tools/kachel-echtfarbe.mjs
|
|
===================================================================== */
|
|
|
|
import { mkdtempSync, rmSync, readFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { spawn, execFileSync } from "node:child_process";
|
|
import { DatabaseSync } from "node:sqlite";
|
|
import { scryptSync, randomBytes } from "node:crypto";
|
|
import { createServer } from "node:https";
|
|
import { request as httpAnfrage } from "node:http";
|
|
import { chromium } from "playwright-core";
|
|
|
|
const HIER = join(dirname(fileURLToPath(import.meta.url)), "..", "server");
|
|
const CREW = "crew.dogfather-universe.com";
|
|
const PORT = Number(process.env.PORT_ECHT || 4368);
|
|
const BASIS = `http://127.0.0.1:${PORT}`;
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-echt-"));
|
|
const DB = join(ordner, "workspace.db");
|
|
let kind = null;
|
|
|
|
kind = spawn(process.execPath, [join(HIER, "index.js")], {
|
|
env: { ...process.env, WORKSPACE_DB: DB, PORT: String(PORT),
|
|
SITE_ACCESS_SECRET: "lokaler-test",
|
|
SITE_PUBLIC_LAUNCH_AT: "2020-01-01T00:00:00+01:00" },
|
|
stdio: ["ignore", "pipe", "pipe"] });
|
|
for (let i = 0; i < 80; i++) {
|
|
await new Promise((r) => setTimeout(r, 250));
|
|
try { const a = await fetch(BASIS + "/workspace/", { redirect: "manual" });
|
|
if (a.status < 500) break; } catch { /* noch nicht oben */ }
|
|
}
|
|
|
|
const d = new DatabaseSync(DB);
|
|
const salz = randomBytes(16).toString("hex");
|
|
const hash = scryptSync("CODE-DOGI-0001", salz, 64,
|
|
{ N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
|
|
d.prepare("INSERT INTO personen (name,rolle,code_hash,code_salt,code_n,aktiv,erstellt)"
|
|
+ " VALUES (?,?,?,?,?,1,?)")
|
|
.run("Filipe", "admin", hash, salz, 32768, new Date().toISOString());
|
|
d.close();
|
|
|
|
const keks = await new Promise((fertig, schief) => {
|
|
const daten = JSON.stringify({ rolle: "admin", code: "CODE-DOGI-0001" });
|
|
const a = httpAnfrage({ host: "127.0.0.1", port: PORT,
|
|
path: "/workspace/api/anmelden", method: "POST",
|
|
headers: { "content-type": "application/json", Host: CREW,
|
|
"content-length": Buffer.byteLength(daten) } }, (antwort) => {
|
|
let t = ""; antwort.on("data", (x) => { t += x; });
|
|
antwort.on("end", () => fertig((antwort.headers["set-cookie"] || [])
|
|
.join("; ").split(";")[0]));
|
|
});
|
|
a.on("error", schief); a.end(daten);
|
|
});
|
|
|
|
/* Chromium zwingt crew.… per eingebauter HSTS-Liste auf https. */
|
|
const zKey = join(ordner, "k.pem"), zCrt = join(ordner, "z.pem");
|
|
execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes",
|
|
"-keyout", zKey, "-out", zCrt, "-days", "2", "-subj", `/CN=${CREW}`,
|
|
"-addext", `subjectAltName=DNS:${CREW}`], { stdio: "ignore" });
|
|
const HTTPS_PORT = PORT + 1;
|
|
const vorbau = createServer({ key: readFileSync(zKey), cert: readFileSync(zCrt) },
|
|
(an, ab) => {
|
|
const weiter = httpAnfrage({
|
|
host: "127.0.0.1", port: PORT, path: an.url, method: an.method,
|
|
headers: { ...an.headers, host: CREW },
|
|
}, (antwort) => { ab.writeHead(antwort.statusCode, antwort.headers); antwort.pipe(ab); });
|
|
weiter.on("error", () => { try { ab.destroy(); } catch { /* Zugabe */ } });
|
|
an.on("error", () => {});
|
|
an.pipe(weiter);
|
|
});
|
|
await new Promise((r) => vorbau.listen(HTTPS_PORT, "127.0.0.1", r));
|
|
|
|
const browser = await chromium.launch({
|
|
args: [`--host-resolver-rules=MAP ${CREW} 127.0.0.1:${HTTPS_PORT}`],
|
|
ignoreHTTPSErrors: true,
|
|
});
|
|
const ctx = await browser.newContext({
|
|
viewport: { width: 1500, height: 1000 }, ignoreHTTPSErrors: true });
|
|
const [n, w] = keks.split("=");
|
|
await ctx.addCookies([{ name: n, value: w, domain: CREW, path: "/" }]);
|
|
const s = await ctx.newPage();
|
|
await s.goto(`https://${CREW}/workspace/start.html`, { waitUntil: "networkidle" });
|
|
await s.waitForTimeout(1200);
|
|
|
|
/* ---- Jede Kachel einzeln fotografieren und mitteln ------------------ */
|
|
const kacheln = await s.evaluate(() => [...document.querySelectorAll(".kachel")]
|
|
.map((k, i) => ({
|
|
i,
|
|
name: (k.querySelector(".kachel__name") || k).textContent.trim().split("\n")[0],
|
|
ton: getComputedStyle(k).getPropertyValue("--ton").trim(),
|
|
})));
|
|
|
|
const zeiger = await s.$$(".kachel");
|
|
const gemessen = [];
|
|
for (let i = 0; i < zeiger.length; i++) {
|
|
const bild = await zeiger[i].screenshot({ type: "png" });
|
|
/* PNG von Hand lesen waere ein eigenes Werkzeug. Stattdessen laesst
|
|
der Browser selbst zaehlen: Bild in ein <canvas>, Mittelwert. */
|
|
const b64 = bild.toString("base64");
|
|
const mittel = await s.evaluate(async (daten) => {
|
|
const bild = new Image();
|
|
await new Promise((f) => { bild.onload = f; bild.src = "data:image/png;base64," + daten; });
|
|
const c = document.createElement("canvas");
|
|
c.width = bild.width; c.height = bild.height;
|
|
const g = c.getContext("2d");
|
|
g.drawImage(bild, 0, 0);
|
|
const p = g.getImageData(0, 0, c.width, c.height).data;
|
|
let r = 0, gr = 0, bl = 0, n = 0;
|
|
for (let k = 0; k < p.length; k += 4) { r += p[k]; gr += p[k + 1]; bl += p[k + 2]; n++; }
|
|
return [Math.round(r / n), Math.round(gr / n), Math.round(bl / n)];
|
|
}, b64);
|
|
gemessen.push({ ...kacheln[i], echt: mittel });
|
|
}
|
|
|
|
/* ---- Wie weit liegen die ECHTEN Farben auseinander? ----------------- */
|
|
const linear = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
|
|
function oklab([R, G, B]) {
|
|
const [r, g, b] = [R / 255, G / 255, B / 255].map(linear);
|
|
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
|
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
|
const s2 = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
return [0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s2,
|
|
1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s2,
|
|
0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s2];
|
|
}
|
|
const roh = (hex) => (/^#[0-9a-f]{6}$/i.test(hex)
|
|
? [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16)) : null);
|
|
const abst = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
|
|
const hex = (c) => "#" + c.map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
console.log("\nKachel Rohfarbe was man WIRKLICH sieht");
|
|
for (const k of gemessen) {
|
|
console.log(" " + k.name.padEnd(24) + (k.ton || "—").padEnd(11) + hex(k.echt));
|
|
}
|
|
|
|
let engRoh = { d: Infinity }, engEcht = { d: Infinity };
|
|
for (let i = 0; i < gemessen.length; i++) {
|
|
for (let j = i + 1; j < gemessen.length; j++) {
|
|
const a = gemessen[i], b = gemessen[j];
|
|
const dE = abst(oklab(a.echt), oklab(b.echt));
|
|
if (dE < engEcht.d) engEcht = { d: dE, a: a.name, b: b.name };
|
|
const ra = roh(a.ton), rb = roh(b.ton);
|
|
if (ra && rb) {
|
|
const dR = abst(oklab(ra), oklab(rb));
|
|
if (dR < engRoh.d) engRoh = { d: dR, a: a.name, b: b.name };
|
|
}
|
|
}
|
|
}
|
|
console.log(`\n ${gemessen.length} Kacheln gemessen.`);
|
|
console.log(` Engstes Paar der ROHFARBEN: ${engRoh.d.toFixed(4)} (${engRoh.a} <-> ${engRoh.b})`);
|
|
console.log(` Engstes Paar auf dem SCHIRM: ${engEcht.d.toFixed(4)} (${engEcht.a} <-> ${engEcht.b})`);
|
|
console.log(` Davon kommt an: ${(engEcht.d / engRoh.d * 100).toFixed(1)} Prozent.`);
|
|
|
|
/* Wie viele Paare sind auf dem Schirm praktisch gleich? Unter 0,02 in
|
|
OKLab sieht ein Auge keinen Unterschied mehr. */
|
|
let gleich = 0, paare = 0;
|
|
for (let i = 0; i < gemessen.length; i++) {
|
|
for (let j = i + 1; j < gemessen.length; j++) {
|
|
paare++;
|
|
if (abst(oklab(gemessen[i].echt), oklab(gemessen[j].echt)) < 0.02) gleich++;
|
|
}
|
|
}
|
|
console.log(` Paare, die ein Auge NICHT unterscheiden kann: ${gleich} von ${paare}`);
|
|
|
|
await browser.close();
|
|
await new Promise((r) => vorbau.close(r));
|
|
if (kind) kind.kill();
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Zugabe */ }
|