diff --git a/server/helfer-kachel-echtfarbe.mjs b/server/helfer-kachel-echtfarbe.mjs new file mode 100644 index 00000000..7e7c9f58 --- /dev/null +++ b/server/helfer-kachel-echtfarbe.mjs @@ -0,0 +1,300 @@ +/* ===================================================================== + helfer-kachel-echtfarbe.mjs — welche Farbe eine Kachel WIRKLICH hat + --------------------------------------------------------------------- + Filipe am 22.09.2026: „ich seh immer wieder sehr viele die einfach + komplett die gleiche farbe haben ich will dass sie wirklich alle + alle alle komplett unterschiedlich sind." + + Er hatte recht, und `pruef-kachelfarben` war trotzdem grün. Der + Grund war ein Messfehler: Sie 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: Die + Kachel zeichnet sie als Verlauf unter einem Sternenfeld, einer + Vignette und (bis zu diesem Tag) einer fest blauen Wolke. + + Gemessen kamen 14,4 Prozent an. Acht Paare waren für ein Auge + nicht mehr zu unterscheiden. + + WARUM DER DURCHSCHNITT ÜBER DIE GANZE KACHEL + Eine Einzelmessung an einem Punkt träfe vielleicht einen Stern, + einen Rand oder das Zeichen. Gemittelt wird über alle Bildpunkte — + das ist ungefähr das, was ein Auge aus zwei Metern Entfernung + sieht, und genau darum geht es. + + WARUM DAS HIER STEHT UND NICHT ZWEIMAL + `tools/kachel-echtfarbe.mjs` zeigt die Liste zum Ansehen, + `server/pruef-kachelfarben.mjs` prüft sie. Zwei Fassungen derselben + Messung altern unterschiedlich — dann sagt die eine „gleich" und + die andere „verschieden", und man glaubt der bequemeren. + ===================================================================== */ + +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"; + +const HIER = dirname(fileURLToPath(import.meta.url)); +const CREW = "crew.dogfather-universe.com"; + +/* ---- Farbrechnung (OKLab, wie in den Farbwerkzeugen) --------------- */ +const linear = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4); +export 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 s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b); + return [ + 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s, + 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s, + 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s, + ]; +} +const abstand = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]); +const ausHex = (hex) => (/^#[0-9a-f]{6}$/i.test(hex) + ? [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16)) : null); +export const alsHex = (c) => "#" + c.map((x) => x.toString(16).padStart(2, "0")).join(""); + +/* UNTER 0,02 SIEHT EIN AUGE KEINEN UNTERSCHIED MEHR. + In OKLab entspricht 1 Einheit ungefähr dem ganzen Helligkeitsbereich; + 0,02 ist damit die Grenze, ab der zwei Flächen nebeneinander noch + als „zwei Farben" gelesen werden. Darunter ist es eine. */ +export const NICHT_UNTERSCHEIDBAR = 0.02; + +/** + * Fotografiert die Kachelwand und liest die echten Bildpunkte aus. + * + * Gibt IMMER ein Ergebnis zurück — auch wenn es nicht messen konnte. + * `moeglich: false` mit Grund ist der dritte Ausgang: nicht „in + * Ordnung", nicht „kaputt", sondern „konnte nicht nachsehen". + */ +export async function pruefeEchteFarben({ port = 4369, breite = 1500 } = {}) { + let chromium; + try { + ({ chromium } = await import("playwright-core")); + } catch { + return { moeglich: false, grund: "playwright-core ist nicht da" }; + } + + const ordner = mkdtempSync(join(tmpdir(), "ws-echtfarbe-")); + const DB = join(ordner, "workspace.db"); + let kind = null, vorbau = null, browser = null; + + try { + 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"] }); + let oben = false; + for (let i = 0; i < 80; i++) { + await new Promise((r) => setTimeout(r, 250)); + if (kind.exitCode !== null) break; + try { + const a = await fetch(`http://127.0.0.1:${port}/workspace/`, { redirect: "manual" }); + if (a.status < 500) { oben = true; break; } + } catch { /* noch nicht oben */ } + } + if (!oben) return { moeglich: false, grund: `der Server kam auf Port ${port} nicht hoch` }; + + 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, 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); + }); + if (!keks) return { moeglich: false, grund: "die Anmeldung hat keinen Keks geliefert" }; + + /* Chromium zwingt crew.… per eingebauter HSTS-Liste auf https -- + deshalb derselbe selbst unterschriebene Vorbau wie überall. */ + const zKey = join(ordner, "k.pem"), zCrt = join(ordner, "z.pem"); + try { + execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes", + "-keyout", zKey, "-out", zCrt, "-days", "2", "-subj", `/CN=${CREW}`, + "-addext", `subjectAltName=DNS:${CREW}`], { stdio: "ignore" }); + } catch { + return { moeglich: false, grund: "openssl ist nicht da (fuer den https-Vorbau)" }; + } + + const HTTPS_PORT = port + 1; + vorbau = createServer({ key: readFileSync(zKey), cert: readFileSync(zCrt) }, + (an, ab) => { + const weiter = httpAnfrage({ + host: "127.0.0.1", 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)); + + 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: breite, height: 1000 }, ignoreHTTPSErrors: true }); + const [n, w] = keks.split("="); + await ctx.addCookies([{ name: n, value: w, domain: CREW, path: "/" }]); + const seite = await ctx.newPage(); + await seite.goto(`https://${CREW}/workspace/start.html`, { waitUntil: "networkidle" }); + await seite.waitForTimeout(1200); + + const beschriftung = await seite.evaluate(() => [...document.querySelectorAll(".kachel")] + .map((k) => ({ + name: (k.querySelector(".kachel__name") || k).textContent.trim().split("\n")[0], + ton: getComputedStyle(k).getPropertyValue("--ton").trim(), + }))); + + /* WO DER TEXT STEHT -- fuer den Lesbarkeitsteil weiter unten. */ + const textlagen = await seite.evaluate(() => [...document.querySelectorAll(".kachel")] + .map((k) => { + const t = k.querySelector(".kachel__unter") || k.querySelector(".kachel__name"); + if (!t) return null; + const r = t.getBoundingClientRect(), kr = k.getBoundingClientRect(); + return { + x: Math.round(r.x - kr.x), y: Math.round(r.y - kr.y), + b: Math.round(r.width), h: Math.round(r.height), + farbe: getComputedStyle(t).color, + }; + })); + + const zeiger = await seite.$$(".kachel"); + const liste = []; + for (let i = 0; i < zeiger.length; i++) { + const png = (await zeiger[i].screenshot({ type: "png" })).toString("base64"); + /* Der Browser zählt selbst -- ein PNG hier von Hand zu lesen + wäre ein eigenes Werkzeug, und eines mehr, das altert. */ + const mittel = await seite.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, z = 0; + for (let k = 0; k < p.length; k += 4) { r += p[k]; gr += p[k + 1]; bl += p[k + 2]; z++; } + return [Math.round(r / z), Math.round(gr / z), Math.round(bl / z)]; + }, png); + /* WIE LESBAR IST DER TEXT DARAUF? + ============================================================ + Das ist die Frage, die am 22.09.2026 beinahe untergegangen + waere. Die Farben kraeftiger zu machen war richtig -- gemessen + stieg der Anteil, der ankommt, von 14,4 auf 51,1 Prozent. + Nur waren damit 27 von 31 Kacheltexten unter 4,5:1; vorher + war es EINER. Ein Fehler gegen einen anderen getauscht, und + zwar genau der, den Filipe schon zweimal gemeldet hat ("man + bekommt fast nichts gelesen"). + + Gemessen wird der HAEUFIGSTE Farbwert hinter dem Text, nicht + der hellste: Der hellste Punkt IST der Text (ein erster Anlauf + meldete darum bei allen 31 einen Kontrast von 1,00 -- also + einen unmoeglichen Wert). Der Text deckt nur einen Bruchteil + der Flaeche; der haeufigste Wert ist der Grund. */ + const l = textlagen[i]; + let kontrast = null; + if (l && l.b > 4 && l.h > 4) { + const grund = await seite.evaluate(async ([daten, lage]) => { + 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 x = Math.max(0, lage.x), y = Math.max(0, lage.y); + const bb = Math.min(lage.b, c.width - x), hh = Math.min(lage.h, c.height - y); + if (bb <= 0 || hh <= 0) return null; + const p = g.getImageData(x, y, bb, hh).data; + const zaehler = new Map(); + for (let k = 0; k < p.length; k += 4) { + const sch = ((p[k] >> 3) << 10) | ((p[k + 1] >> 3) << 5) | (p[k + 2] >> 3); + zaehler.set(sch, (zaehler.get(sch) || 0) + 1); + } + let haeufig = 0, wie = -1; + for (const [sch, n2] of zaehler) if (n2 > wie) { wie = n2; haeufig = sch; } + return [((haeufig >> 10) & 31) << 3, ((haeufig >> 5) & 31) << 3, (haeufig & 31) << 3]; + }, [png, l]); + if (grund) { + const tf = (l.farbe.match(/\d+/g) || [255, 255, 255]).slice(0, 3).map(Number); + const hell = (c) => { + const [r, g2, b] = c.map((x) => { + const v = x / 255; + return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; + }); + return 0.2126 * r + 0.7152 * g2 + 0.0722 * b; + }; + const [a1, a2] = hell(tf) > hell(grund) ? [hell(tf), hell(grund)] : [hell(grund), hell(tf)]; + kontrast = (a1 + 0.05) / (a2 + 0.05); + } + } + + liste.push({ ...beschriftung[i], echt: mittel, kontrast }); + } + + /* ---- Auswerten ---------------------------------------------- */ + let engstes = { d: Infinity, a: "", b: "" }; + let engstesRoh = Infinity; + let ununterscheidbar = 0, paare = 0; + for (let i = 0; i < liste.length; i++) { + for (let j = i + 1; j < liste.length; j++) { + paare++; + const dE = abstand(oklab(liste[i].echt), oklab(liste[j].echt)); + if (dE < engstes.d) engstes = { d: dE, a: liste[i].name, b: liste[j].name }; + if (dE < NICHT_UNTERSCHEIDBAR) ununterscheidbar++; + const ra = ausHex(liste[i].ton), rb = ausHex(liste[j].ton); + if (ra && rb) engstesRoh = Math.min(engstesRoh, abstand(oklab(ra), oklab(rb))); + } + } + + const mitKontrast = liste.filter((k) => k.kontrast !== null); + const zuDunkel = mitKontrast.filter((k) => k.kontrast < 4.5); + + return { + moeglich: true, + anzahl: liste.length, + /* Wie viele Kacheltexte nicht lesbar sind -- und welche. */ + textGemessen: mitKontrast.length, + textZuWenig: zuDunkel.length, + textSchlechteste: mitKontrast.length + ? mitKontrast.reduce((a, b) => (a.kontrast < b.kontrast ? a : b)) + : null, + textNamen: zuDunkel.slice(0, 5).map((k) => `${k.name} (${k.kontrast.toFixed(2)}:1)`), + liste, + engstes, + engstesRoh, + ununterscheidbar, + paare, + /* Wie viel von der Rohfarbe auf dem Bildschirm ankommt. Faellt + das unter ein Drittel, nuetzt die schoenste Rohfarbe nichts. */ + anteil: engstesRoh > 0 ? (engstes.d / engstesRoh) * 100 : 0, + }; + } catch (f) { + return { moeglich: false, grund: f?.message || String(f) }; + } finally { + try { if (browser) await browser.close(); } catch { /* Zugabe */ } + try { if (vorbau) await new Promise((r) => vorbau.close(r)); } catch { /* Zugabe */ } + try { if (kind) kind.kill(); } catch { /* Zugabe */ } + try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Zugabe */ } + } +} diff --git a/server/pruef-kachelfarben.mjs b/server/pruef-kachelfarben.mjs index 435a31fb..4b67e4f5 100644 --- a/server/pruef-kachelfarben.mjs +++ b/server/pruef-kachelfarben.mjs @@ -57,10 +57,14 @@ const MINDEST_BUNTHEIT = 0.12; /* Was festgelegt ist — mit dem Grund, aus dem es festgelegt wurde. */ const FESTGELEGT = [ - { ton: 28, kachel: "Vertraulich melden", - warum: "dieselbe Farbe wie fuer Regeln & Hilfe -- wer Hilfe sucht, landet bei einem von beiden" }, - { ton: 38, kachel: "Draußen", hex: "#ff1a1a", - warum: "knalliges Rot, traegt ab 20 Uhr den LIVE-Punkt" }, + /* BERICHTIGT AM 22.09.2026. Ich hatte screen5 und screen6 vertauscht + und das Rot auf "Draussen" gelegt. Filipe: "nicht die kachel + draussen soll rot sein sondern die kachel vertraulich melden nur + die soll knall rot sein!!!" */ + { ton: 38, kachel: "Vertraulich melden", hex: "#ff1a1a", + warum: "knalliges Rot -- wer den vertraulichen Weg sucht, soll ihn finden, ohne zu lesen" }, + { ton: 39, kachel: "Draußen", + warum: "die Farbe, die Regeln & Hilfe hatte -- ausdruecklicher Wunsch (screen5)" }, ]; let ok = 0, fehl = 0; @@ -225,5 +229,80 @@ console.log("\n=== Gegenprobe ==="); pruef("ein blasser Ton wuerde auffallen (#ffe5ae)", buntheit("#ffe5ae") < MINDEST_BUNTHEIT); } +/* ===================================================================== + 5. WAS AUF DEM BILDSCHIRM ANKOMMT -- der Teil, der gefehlt hat + ===================================================================== + Alles bisher misst `--ton`: die ROHFARBE aus start.css. Und genau + damit war diese Pruefung gruen, waehrend Filipe am 22.09.2026 sagte: + "ich seh immer wieder sehr viele die einfach komplett die gleiche + farbe haben." + + Er hatte recht. Die Kachel zeichnet ihren Ton nicht als Flaeche, + sondern als Verlauf unter einem Sternenfeld, einer Vignette und + (bis heute) einer fest blauen Wolke. Gemessen an echten + Bildpunkten: Von 0,0978 Abstand kamen 0,0141 an -- 14,4 Prozent. + ACHT Paare waren fuer ein Auge nicht mehr zu unterscheiden. + + Eine Pruefung, die die Rohfarbe misst, beantwortet die Frage + "sehen die Kacheln verschieden aus" NICHT. Sie beantwortet + "stehen verschiedene Zahlen in der Datei". Das ist derselbe Fehler + wie am 01.09.2026 beim `every()` auf einem leeren Feld: Sie lief, + sie war gruen, und sie war wertlos. + + Deshalb hier der Browser. Er kostet zwei Minuten -- und er ist der + einzige, der die Frage beantworten kann. + ===================================================================== */ +if (process.env.OHNE_BROWSER !== "ja") { + console.log("\n=== Und was davon auf dem Bildschirm ankommt ==="); + const { pruefeEchteFarben } = await import("./helfer-kachel-echtfarbe.mjs"); + const e = await pruefeEchteFarben(); + + if (!e.moeglich) { + /* DER DRITTE AUSGANG. Kein Browser, kein openssl, kein Port -- + das ist "konnte nicht nachsehen", nicht "in Ordnung". */ + console.log(` KONNTE NICHT NACHSEHEN: ${e.grund}`); + console.log(" (mit OHNE_BROWSER=ja laeuft die Pruefung bewusst ohne diesen Teil)"); + fehl++; + } else { + pruef(`${e.anzahl} Kacheln am echten Bildschirm gemessen`, e.anzahl >= 20, + `nur ${e.anzahl} gefunden`); + + /* DIE ZAHL GEHOERT IN DIE BEDINGUNG -- sonst waere dieser ganze + Abschnitt bei null Kacheln trivial gruen. */ + pruef("kein Paar sieht auf dem Bildschirm gleich aus " + + `(engstes: ${e.engstes.d.toFixed(4)}, ${e.engstes.a} <-> ${e.engstes.b})`, + e.anzahl >= 20 && e.ununterscheidbar === 0, + `${e.ununterscheidbar} von ${e.paare} Paaren unter 0,02`); + + /* WIE VIEL VON DER ROHFARBE UEBERHAUPT ANKOMMT. Faellt das wieder + unter ein Drittel, ist die Gestaltung der Kachel der Grund -- + und dann nuetzt die schoenste Rohfarbe nichts. */ + pruef(`mindestens ein Drittel der Rohfarbe kommt an (${e.anteil.toFixed(1)} %)`, + e.anteil >= 33, + `nur ${e.anteil.toFixed(1)} % -- die Kachelgestaltung schluckt die Farbe`); + + /* UND DER TEXT MUSS DARAUF LESBAR BLEIBEN. + ================================================================ + Diese zwei Zeilen sind die wichtigsten des ganzen Abschnitts. + Am 22.09.2026 wurde die Farbe kraeftiger gemacht -- richtig, und + gemessen. Dabei fielen 27 von 31 Kacheltexten unter 4,5:1; + vorher war es EINER. Kein Messwert hat das gemeldet, weil keine + Pruefung danach gefragt hat: `pruef-buehne` nimmt Stichproben + und traf die Kacheltexte nicht. + + Farbe und Lesbarkeit gehoeren in DIESELBE Messung. Wer sie + trennt, verbessert das eine und verliert das andere -- und + merkt es erst, wenn jemand sagt, er koenne nichts mehr lesen. */ + pruef(`${e.textGemessen} Kacheltexte auf Lesbarkeit gemessen`, + e.textGemessen >= 20, `nur ${e.textGemessen}`); + pruef("jeder Kacheltext erreicht 4,5:1 auf seiner Kachel" + + (e.textSchlechteste + ? ` (schlechtester: ${e.textSchlechteste.name} ${e.textSchlechteste.kontrast.toFixed(2)}:1)` + : ""), + e.textGemessen >= 20 && e.textZuWenig === 0, + `${e.textZuWenig} zu dunkel: ${e.textNamen.join(", ")}`); + } +} + console.log(`\n ${ok} bestanden, ${fehl} fehlgeschlagen\n`); process.exit(fehl ? 1 : 0); diff --git a/server/workspace.js b/server/workspace.js index a3822f75..a98f7278 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -1299,8 +1299,14 @@ const TREFF_BEREICHE = [ besser als der erste Vorschlag), Kontrast 6,08:1. */ { ...TREFF_GRUPPE, name: "Material", unter: "Bilder und Videos zum Posten – jedes nur einmal", zeichen: "galerie", ton: 42, ziel: "material.html", szene: "studio" }, + /* DIE FARBE, DIE "REGELN & HILFE" HATTE (22.09.2026). + Filipes screen5: "die farbe dieser kachel soll die farbe von der + kachel: regeln & hilfe werden". Regeln & Hilfe bekommt dafuer + eine eigene -- zwei Kacheln mit derselben Farbe in derselben + Ansicht waeren genau das, was screen4 abschafft. + Ton 39 ist GESETZT. */ { ...TREFF_GRUPPE, name: "Draußen", unter: "Ist er live? Kanäle, Shop & dein Rabatt", - zeichen: "hinaus", ton: 38, ziel: "unsere-seiten.html", szene: "portal" }, + zeichen: "hinaus", ton: 39, ziel: "unsere-seiten.html", szene: "portal" }, { ...TREFF_GRUPPE, name: "Mitmachen", unter: "Wie du dazugeh\u00f6ren kannst", zeichen: "personen", ton: 32, ziel: "bereich.html?b=mitmachen", szene: "garage" }, /* UNSERE SEITEN (19.09.2026) -- die einzige Kachel, die HINAUSFUEHRT. @@ -1343,11 +1349,11 @@ const TREFF_BEREICHE = [ Das Brett gibt es weiter; treff-regeln.html verlinkt darauf. Ein Ort fuer die Frage, nicht zwei. */ - /* EIGENER TON SEIT 22.09.2026. Ton 28 traegt jetzt "Vertraulich - melden" -- ausdruecklicher Wunsch, dort gesetzt. Zwei Kacheln - mit derselben Farbe in derselben Ansicht waeren genau das, was - screen4 abschaffen soll. */ - zeichen: "schutz", ton: 39, ziel: "treff-regeln.html", szene: "studio" }, + /* EIGENER TON (22.09.2026). "Draussen" traegt jetzt die Farbe, + die hier stand -- ausdruecklicher Wunsch. Zwei Kacheln mit + derselben Farbe in derselben Ansicht waeren genau das, was + screen4 abschafft. */ + zeichen: "schutz", ton: 28, ziel: "treff-regeln.html", szene: "studio" }, ]; /* MELDUNGEN & MASSNAHMEN -- FUER DAS TEAM, NICHT FUER DIE COMMUNITY @@ -1466,15 +1472,21 @@ export const TREFF_ROLLEN = new Set(["gast", "modi", "hand", "linke", "admin"]); const HILFE_KACHEL = { gruppe: "Für dich", gruppeUnter: "Deine eigene Seite", name: "Vertraulich melden", unter: "Nur DogFather und die rechte Hand", - /* DIESELBE FARBE WIE "REGELN & HILFE" -- AUSDRUECKLICH SO GEWOLLT - (22.09.2026, Filipe: "die farbe dieser kachel soll die farbe von - der kachel: regeln & hilfe werden ... und diese zwei kacheln - sollen egal wie dan auch so bleiben wie hier erwartet"). + /* KNALLROT -- AUSDRUECKLICH DIESE KACHEL (berichtigt 22.09.2026). + ================================================================== + Filipe: "nicht die kachel draussen soll rot sein sondern die + kachel vertraulich melden nur die soll knall rot sein!!!" - Beide gehoeren zusammen: Wer Hilfe sucht, landet bei einem von - beiden. Ton 28 ist damit GESETZT -- die Farbverteilung von - screen4 fasst ihn nicht an, sie weicht ihm aus. */ - zeichen: "schutz", ton: 28, ziel: "hilfe.html", szene: "studio", + Ich hatte screen5 und screen6 vertauscht und das Rot auf + "Draussen" gelegt. Hier sitzt es richtig: Ein vertraulicher + Meldeweg ist das, was jemand im schlimmsten Moment sucht -- und + dann zaehlt, dass man ihn auf der Wand sofort findet, ohne zu + lesen. Rot ist hier die Aussage, nicht die Dekoration. + + #ff1a1a, voll gesaettigt, Kontrast 4,84:1 gegen den Grund. + Ton 38 ist GESETZT: pruef-kachelfarben wird rot, wenn ein + Farblauf ihn anfasst. */ + zeichen: "schutz", ton: 38, ziel: "hilfe.html", szene: "studio", }; /* DAS TEAM SIEHT DEN TREFF MIT -- als eigene Gruppe unter seinen diff --git a/tools/kachel-echtfarbe.mjs b/tools/kachel-echtfarbe.mjs new file mode 100644 index 00000000..a1963540 --- /dev/null +++ b/tools/kachel-echtfarbe.mjs @@ -0,0 +1,193 @@ +/* ===================================================================== + 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 */ } diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index 707cb9d7..8d6f79c8 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,15 +16,15 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +