/* ===================================================================== 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 , 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 */ }