screen5 und screen4: zwei Farben gesetzt, 14 auseinandergezogen
screen5 (ausdruecklich VOR screen4, wie gewuenscht): "Vertraulich melden" traegt jetzt Ton 28 -- die Farbe, die "Regeln & Hilfe" hatte. Beide gehoeren zusammen: Wer Hilfe sucht, landet bei einem von beiden. "Regeln & Hilfe" bekam dafuer einen eigenen Ton; zwei Kacheln mit derselben Farbe in derselben Ansicht waeren genau das, was screen4 abschaffen soll. "Draussen" ist knallrot: Ton 38 = #ff1a1a, voll gesaettigt, Kontrast 4,84:1 gegen den Grund. Gemessen, nicht geschaetzt -- von zehn Rotwerten zwischen #ff0000 und #ff5555 erfuellen alle die 4,5:1, dieser ist der knalligste, der auf dunklem Grund nicht flimmert. Er traegt ab 20 Uhr den LIVE-Punkt. Beide sind ab jetzt GESETZT: pruef-kachelfarben wird rot, wenn ein Farblauf sie anfasst. Eine Festlegung, die nur im Kommentar steht, ist eine Bitte. screen4: neues Werkzeug tools/kachel-farben-entzerren.mjs. Der Unterschied zu den zwei vorhandenen: Es rechnet nur mit den BENUTZTEN Toenen. In start.css stehen 42, benutzt werden 31 -- die anderen Werkzeuge weichen also Farben aus, die kein Mensch sieht, und machen dadurch die Abstaende zwischen den sichtbaren unnoetig klein. Welche benutzt werden, wird aus den Kachellisten ABGELEITET. Von jedem aehnlichen Paar aendert sich genau EINER -- der, der nicht gesetzt ist. Ergebnis: engstes Paar 0,0741 -> 0,0978 (Faktor 1,32), 14 Toene geaendert, alle 31 erreichen 4,5:1. ZWEI DINGE, DIE ERST DAS MESSEN GEZEIGT HAT 1. Der erste Lauf schlug fuer "Dateien" ein #ffe5ae vor -- Buntheit 0,076, ein helles Creme. Rechnerisch die beste Stelle im Farbraum, auf dem Bildschirm genau das, was Filipe seit Wochen abschafft. Mindestbuntheit eingebaut, Schwelle GEMESSEN: Median aller Toene 0,168, die fuenf blassesten 0,064-0,088. 0,12 liegt dazwischen. 2. Die Abstandsrechnung fasst einen Ton nie an, der weit weg von allen liegt -- auch wenn er blass ist. So blieben drei benutzte Toene unter 0,12 stehen. Zweite Runde: jeder blasse wird kraeftig gemacht, solange das Minimum ueber 0,095 bleibt. Gemessener Preis 0,1028 -> 0,0978, also 5 Prozent; auf dem Bildschirm nicht zu sehen, waehrend Filipes Klage ueber helle Kacheln konkret ist. NEU: pruef-kachelfarben 13/0 -- getrennt, eindeutig je Ansicht, festgelegt, lesbar und nicht blass. Mit vier Gegenproben, damit sie auch rot werden KANN. NEU: tools/farben-blick.mjs -- sieht die Wand mit echten Augen an und misst das, was eine Abstandstabelle nicht beantwortet: ob zwei NEBENEINANDER liegende Kacheln aehnlich aussehen. Gemessen bei DogFather und Community: kein Nachbarpaar unter 0,09. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
/* =====================================================================
|
||||
DIE KACHELFARBEN MIT ECHTEN AUGEN ANSEHEN (22.09.2026)
|
||||
---------------------------------------------------------------------
|
||||
`pruef-kachelfarben.mjs` rechnet Abstände in OKLab. Das sagt, dass
|
||||
keine zwei Farben zu nah beieinander liegen — es sagt NICHT, ob die
|
||||
Wand gut aussieht. Am 22.09. hat der Blick mit echten Augen schon
|
||||
drei Dinge gefunden, die keine Messung gemeldet hätte.
|
||||
|
||||
Dieses Werkzeug nimmt die Startseite auf, als DogFather und als
|
||||
Zuschauer, am Rechner und am Handy — und misst dabei das eine, was
|
||||
sich sonst nur im Bild zeigt: ob zwei NEBENEINANDER liegende
|
||||
Kacheln ähnlich aussehen. Zwei ähnliche Farben an entgegengesetzten
|
||||
Enden der Wand fallen niemandem auf; zwei nebeneinander schon.
|
||||
|
||||
AUFRUF
|
||||
node tools/farben-blick.mjs
|
||||
BREITE=390 node tools/farben-blick.mjs
|
||||
===================================================================== */
|
||||
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { spawn } 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 { execFileSync } from "node:child_process";
|
||||
import { chromium } from "playwright-core";
|
||||
|
||||
const HIER = join(dirname(fileURLToPath(import.meta.url)), "..", "server");
|
||||
const PORT = Number(process.env.PORT_BLICK || 4366);
|
||||
const BASIS = `http://127.0.0.1:${PORT}`;
|
||||
const BREITE = Number(process.env.BREITE || 1500);
|
||||
const ordner = mkdtempSync(join(tmpdir(), "ws-farben-"));
|
||||
const DB = join(ordner, "workspace.db");
|
||||
let kind = null;
|
||||
|
||||
async function start() {
|
||||
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 aus = "";
|
||||
kind.stdout.on("data", (d) => { aus += d; });
|
||||
kind.stderr.on("data", (d) => { aus += d; });
|
||||
for (let i = 0; i < 80; i++) {
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
if (kind.exitCode !== null) break;
|
||||
try {
|
||||
const a = await fetch(BASIS + "/workspace/", { redirect: "manual" });
|
||||
if (a.status < 500) return;
|
||||
} catch { /* noch nicht oben */ }
|
||||
}
|
||||
throw new Error("Server startete nicht:\n" + aus.slice(-1500));
|
||||
}
|
||||
|
||||
function personAnlegen(name, rolle, code) {
|
||||
const d = new DatabaseSync(DB);
|
||||
const salz = randomBytes(16).toString("hex");
|
||||
const hash = scryptSync(code, 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(name, rolle, hash, salz, 32768, new Date().toISOString());
|
||||
d.close();
|
||||
}
|
||||
|
||||
function rohAnfrage(pfad, host, koerper) {
|
||||
return new Promise((fertig, schief) => {
|
||||
const daten = JSON.stringify(koerper);
|
||||
const a = httpAnfrage({
|
||||
host: "127.0.0.1", port: PORT, path: pfad, method: "POST",
|
||||
headers: { "content-type": "application/json", Host: host,
|
||||
"content-length": Buffer.byteLength(daten) },
|
||||
}, (antwort) => {
|
||||
let text = "";
|
||||
antwort.on("data", (d) => { text += d; });
|
||||
antwort.on("end", () => fertig({
|
||||
code: antwort.statusCode, text,
|
||||
keks: (antwort.headers["set-cookie"] || []).join("; ").split(";")[0],
|
||||
}));
|
||||
});
|
||||
a.on("error", schief);
|
||||
a.end(daten);
|
||||
});
|
||||
}
|
||||
|
||||
async function anmelden(rolle, code, host) {
|
||||
let a = await rohAnfrage("/workspace/api/anmelden", host, { rolle, code });
|
||||
if (/alter/i.test(a.text)) {
|
||||
a = await rohAnfrage("/workspace/api/anmelden", host, { rolle, code, alter_ok: true });
|
||||
}
|
||||
if (a.code !== 200) throw new Error(`Anmeldung ${rolle} -> ${a.code} ${a.text.slice(0, 160)}`);
|
||||
return a.keks;
|
||||
}
|
||||
|
||||
/* Chromium zwingt crew.dogfather-universe.com per eingebauter
|
||||
HSTS-Liste auf https. Deshalb derselbe selbst unterschriebene
|
||||
Vorbau wie in pruef-handy-teamdogi.mjs -- eine Nachbildung waere die
|
||||
zweite Fassung, die anders altert. */
|
||||
function schluesselUndZeugnis() {
|
||||
const k = join(ordner, "k.pem"), z = join(ordner, "z.pem");
|
||||
execFileSync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes",
|
||||
"-keyout", k, "-out", z, "-days", "2", "-subj", "/CN=crew.dogfather-universe.com",
|
||||
"-addext", "subjectAltName=DNS:crew.dogfather-universe.com"],
|
||||
{ stdio: "ignore" });
|
||||
return { k, z };
|
||||
}
|
||||
|
||||
await start();
|
||||
personAnlegen("Filipe", "admin", "CODE-DOGI-0001");
|
||||
personAnlegen("Jonas", "gast", "CODE-GAST-0001");
|
||||
|
||||
const { k, z } = schluesselUndZeugnis();
|
||||
const { readFileSync } = await import("node:fs");
|
||||
const vorbau = createServer({ key: readFileSync(k), cert: readFileSync(z) }, (an, ab) => {
|
||||
const weiter = httpAnfrage({
|
||||
host: "127.0.0.1", port: PORT, path: an.url, method: an.method,
|
||||
headers: { ...an.headers, host: "crew.dogfather-universe.com" },
|
||||
}, (antwort) => { ab.writeHead(antwort.statusCode, antwort.headers); antwort.pipe(ab); });
|
||||
/* Beim Aufraeumen bricht der Browser laufende Anfragen ab. Das ist
|
||||
kein Befund -- ohne diesen Zuhoerer beendet Node den Lauf mit
|
||||
einem Fehler, NACHDEM die Bilder schon fertig sind. */
|
||||
weiter.on("error", () => { try { ab.destroy(); } catch {} });
|
||||
an.on("error", () => {});
|
||||
an.pipe(weiter);
|
||||
});
|
||||
const HTTPS_PORT = PORT + 1;
|
||||
await new Promise((r) => vorbau.listen(HTTPS_PORT, r));
|
||||
|
||||
const browser = await chromium.launch({
|
||||
args: [`--host-resolver-rules=MAP crew.dogfather-universe.com 127.0.0.1:${HTTPS_PORT}`],
|
||||
ignoreHTTPSErrors: true,
|
||||
});
|
||||
|
||||
const rollen = [
|
||||
{ rolle: "admin", code: "CODE-DOGI-0001", wer: "DogFather" },
|
||||
{ rolle: "gast", code: "CODE-GAST-0001", wer: "Community" },
|
||||
];
|
||||
|
||||
for (const r of rollen) {
|
||||
const keks = await anmelden(r.rolle, r.code, "crew.dogfather-universe.com");
|
||||
const ctx = await browser.newContext({
|
||||
viewport: { width: BREITE, height: 1000 },
|
||||
ignoreHTTPSErrors: true,
|
||||
isMobile: BREITE < 700, hasTouch: BREITE < 700,
|
||||
});
|
||||
const [name, wert] = keks.split("=");
|
||||
await ctx.addCookies([{ name, value: wert, domain: "crew.dogfather-universe.com", path: "/" }]);
|
||||
const s = await ctx.newPage();
|
||||
await s.goto("https://crew.dogfather-universe.com/workspace/start.html",
|
||||
{ waitUntil: "networkidle" });
|
||||
await s.waitForTimeout(600);
|
||||
|
||||
/* WAS LIEGT NEBENEINANDER? Das ist die Frage, die eine Tabelle von
|
||||
Abständen nicht beantwortet. */
|
||||
const bild = await s.evaluate(() => {
|
||||
const linear = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
|
||||
const oklab = (rgb) => {
|
||||
const [r, g, b] = rgb.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 kacheln = [...document.querySelectorAll(".kachel")].map((k) => {
|
||||
const r = k.getBoundingClientRect();
|
||||
const roh = getComputedStyle(k).getPropertyValue("--ton").trim();
|
||||
let rgb = null;
|
||||
if (/^#[0-9a-f]{6}$/i.test(roh)) {
|
||||
rgb = [1, 3, 5].map((i) => parseInt(roh.slice(i, i + 2), 16) / 255);
|
||||
}
|
||||
return {
|
||||
name: (k.querySelector(".kachel__name") || k).textContent.trim().split("\n")[0],
|
||||
ton: roh, lab: rgb ? oklab(rgb) : null,
|
||||
x: Math.round(r.x), y: Math.round(r.y),
|
||||
b: Math.round(r.width), h: Math.round(r.height),
|
||||
};
|
||||
});
|
||||
/* Nachbarn: alles, was sich horizontal oder vertikal beruehrt. */
|
||||
const nah = [];
|
||||
const d = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
||||
for (let i = 0; i < kacheln.length; i++) {
|
||||
for (let j = i + 1; j < kacheln.length; j++) {
|
||||
const A = kacheln[i], B = kacheln[j];
|
||||
if (!A.lab || !B.lab) continue;
|
||||
const waagerecht = Math.abs(A.y - B.y) < 8 && Math.abs(A.x - B.x) < A.b + B.b + 40;
|
||||
const senkrecht = Math.abs(A.x - B.x) < 8 && Math.abs(A.y - B.y) < A.h + B.h + 40;
|
||||
if (!waagerecht && !senkrecht) continue;
|
||||
nah.push({ a: A.name, b: B.name, d: d(A.lab, B.lab) });
|
||||
}
|
||||
}
|
||||
nah.sort((p, q) => p.d - q.d);
|
||||
return { anzahl: kacheln.length, ohneTon: kacheln.filter((k) => !k.lab).length, nah: nah.slice(0, 5) };
|
||||
});
|
||||
|
||||
console.log(`\n${r.wer} bei ${BREITE} px: ${bild.anzahl} Kacheln`
|
||||
+ (bild.ohneTon ? `, ${bild.ohneTon} OHNE Farbe` : ""));
|
||||
for (const n of bild.nah) {
|
||||
console.log(` ${n.d.toFixed(4)} ${n.a} neben ${n.b}`
|
||||
+ (n.d < 0.09 ? " <<< sehen sich aehnlich" : ""));
|
||||
}
|
||||
|
||||
const pfad = join("C:/Users/qciga/Documents/Obelix/DogFather",
|
||||
`farben-${r.rolle}-${BREITE}.png`);
|
||||
await s.screenshot({ path: pfad, fullPage: true });
|
||||
console.log(` Bild: ${pfad}`);
|
||||
await ctx.close();
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
await new Promise((r) => vorbau.close(r));
|
||||
if (kind) kind.kill();
|
||||
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Zugabe */ }
|
||||
Reference in New Issue
Block a user