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]>
342 lines
15 KiB
JavaScript
342 lines
15 KiB
JavaScript
/* =====================================================================
|
|
ÄHNLICHE KACHELFARBEN AUSEINANDERZIEHEN (22.09.2026)
|
|
---------------------------------------------------------------------
|
|
Filipe: „es sind viele kacheln die noch ähnliche farbe haben, alle
|
|
die sich ähnlich sind bitte immer verändern bis auf eins und farben
|
|
nehmen die wir noch nicht haben, alle kacheln sollen eine andere
|
|
farbe haben."
|
|
|
|
WARUM ES DIE ZWEI VORHANDENEN WERKZEUGE NICHT SCHON TUN
|
|
`kachel-farben.mjs` rechnet ALLE Töne neu — auch die zwei, die
|
|
Filipe am 22.09. ausdrücklich festgelegt hat („diese zwei kacheln
|
|
sollen egal wie dan auch so bleiben wie hier erwartet"). Es wäre
|
|
das richtige Verfahren mit dem falschen Ergebnis.
|
|
|
|
`kachel-farbe-einzeln.mjs` zieht EINEN Ton nach — man müsste selbst
|
|
wissen, welchen, und nach jedem Lauf neu messen, welcher jetzt der
|
|
engste ist. Genau das macht diese Datei von selbst.
|
|
|
|
DER UNTERSCHIED, AUF DEN ES ANKOMMT: NUR BENUTZTE TÖNE ZÄHLEN
|
|
In `start.css` stehen 42 Töne, benutzt werden 30. Beide anderen
|
|
Werkzeuge rechnen gegen alle 42 — sie weichen also Farben aus, die
|
|
kein Mensch je sieht, und machen dadurch die Abstände zwischen den
|
|
Farben, die man WIRKLICH nebeneinander sieht, unnötig klein.
|
|
|
|
Welche Töne benutzt werden, wird aus den Kachellisten ABGELEITET
|
|
(`bereicheFuer` / `zusatzBereicheFuer` über alle Rollen und beide
|
|
Häuser), nicht danebengeschrieben. Eine zweite Liste wäre die, die
|
|
beim nächsten neuen Bereich vergessen wird.
|
|
|
|
DAS VERFAHREN
|
|
Solange sich der kleinste Abstand verbessern lässt: den engsten
|
|
nicht gesetzten Ton herausnehmen und dorthin legen, wo er am
|
|
weitesten von allen übrigen entfernt ist. Von jedem ähnlichen Paar
|
|
ändert sich also genau EINER — wie gewünscht.
|
|
|
|
Bei einem Paar, in dem BEIDE gesetzt sind, wird nichts geändert und
|
|
das Werkzeug sagt es. Der dritte Ausgang: in Ordnung / nicht in
|
|
Ordnung / konnte hier nichts tun, mit Grund.
|
|
|
|
AUFRUF
|
|
node tools/kachel-farben-entzerren.mjs nur zeigen
|
|
node tools/kachel-farben-entzerren.mjs --schreiben eintragen
|
|
===================================================================== */
|
|
|
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import { dirname, join } from "node:path";
|
|
|
|
const HIER = dirname(fileURLToPath(import.meta.url));
|
|
const CSS = join(HIER, "..", "workspace", "assets", "css", "start.css");
|
|
const GRUND = "#0a121e";
|
|
|
|
/* DIE ZWEI, DIE FILIPE AM 22.09.2026 FESTGELEGT HAT.
|
|
Sie stehen hier mit Begründung, damit ein späterer Lauf sie nicht
|
|
für einen Zufall hält:
|
|
28 — „Vertraulich melden", dieselbe Farbe wie „Regeln & Hilfe"
|
|
war; wer Hilfe sucht, landet bei einem von beiden.
|
|
38 — „Draußen", knalliges Rot. Es trägt ab 20 Uhr den LIVE-Punkt;
|
|
Rot ist dort die Aussage, nicht die Dekoration. */
|
|
const GESETZT = new Set([28, 38]);
|
|
|
|
/* ---- Farbrechnung (gleich wie in kachel-farbe-einzeln.mjs) --------- */
|
|
const linear = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
|
|
const zurueck = (c) => (c <= 0.0031308 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055);
|
|
const srgb = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
|
|
|
|
function oklab(hex) {
|
|
const [r, g, b] = srgb(hex).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,
|
|
];
|
|
}
|
|
|
|
function ausOklch(L, C, Hgrad) {
|
|
const h = (Hgrad * Math.PI) / 180;
|
|
const a = C * Math.cos(h), b2 = C * Math.sin(h);
|
|
const l_ = (L + 0.3963377774 * a + 0.2158037573 * b2) ** 3;
|
|
const m_ = (L - 0.1055613458 * a - 0.0638541728 * b2) ** 3;
|
|
const s_ = (L - 0.0894841775 * a - 1.2914855480 * b2) ** 3;
|
|
const r = +4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_;
|
|
const g = -1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_;
|
|
const b = -0.0041960863 * l_ - 0.7034186147 * m_ + 1.7076147010 * s_;
|
|
const kanal = (x) => {
|
|
const v = zurueck(x);
|
|
if (v < -0.001 || v > 1.001) return null;
|
|
return Math.round(Math.min(1, Math.max(0, v)) * 255);
|
|
};
|
|
const [R, G, B] = [kanal(r), kanal(g), kanal(b)];
|
|
if (R === null || G === null || B === null) return null;
|
|
return "#" + [R, G, B].map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
}
|
|
|
|
const lum = (hex) => {
|
|
const [r, g, b] = srgb(hex).map(linear);
|
|
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
};
|
|
const kontrast = (hex) => (lum(hex) + 0.05) / (lum(GRUND) + 0.05);
|
|
const abstand = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
|
|
/* ---- start.css lesen, vollständig oder gar nicht ------------------- */
|
|
function lies(css) {
|
|
const aus = [];
|
|
for (const zeile of css.split("\n")) {
|
|
const m = zeile.match(/\[data-ton="(\d+)"\]\s*\{\s*--ton:\s*(#[0-9a-fA-F]{6})/);
|
|
if (!m) continue;
|
|
const wofuer = (zeile.match(/\/\*\s*([^*]+?)\s*\*\//) || [])[1] || "";
|
|
aus.push({ n: Number(m[1]), hex: m[2].toLowerCase(), lab: oklab(m[2]), zeile, wofuer });
|
|
}
|
|
return aus;
|
|
}
|
|
|
|
function vollstaendigOderAbbruch(css) {
|
|
const roh = new Set([...css.matchAll(/\[data-ton="(\d+)"\]/g)].map((m) => Number(m[1])));
|
|
const gelesen = lies(css);
|
|
if (roh.size !== gelesen.length) {
|
|
console.error(`ABBRUCH: ${roh.size} Toene stehen in der Datei, gelesen wurden ${gelesen.length}.`);
|
|
console.error(" Die Rechnung waere gegen einen Stand gelaufen, den es nicht gibt.");
|
|
process.exit(2);
|
|
}
|
|
return gelesen;
|
|
}
|
|
|
|
/* ---- Welche Töne ein Mensch wirklich sieht (ABGELEITET) ------------ */
|
|
async function benutzteToene() {
|
|
const w = await import("../server/workspace.js");
|
|
const rollen = ["admin", "hand", "linke", "modi", "gast",
|
|
"manager", "scout", "creator", "spicy"];
|
|
/* Je Ansicht (Rolle x Haus) merken, welche Töne darin vorkommen.
|
|
Zwei Kacheln mit demselben Ton sind nur dann ein Problem, wenn sie
|
|
in DERSELBEN Ansicht stehen -- sonst sieht sie niemand nebeneinander. */
|
|
const ansichten = [];
|
|
const alle = new Map(); /* ton -> Set(Kachelnamen) */
|
|
for (const r of rollen) {
|
|
for (const haus of ["crew", "agentur"]) {
|
|
const p = { id: 1, rolle: r, name: "X" };
|
|
let k = [];
|
|
try {
|
|
k = [...(w.bereicheFuer(p, haus) || []), ...(w.zusatzBereicheFuer(p, haus) || [])];
|
|
} catch { continue; }
|
|
const hier = new Map();
|
|
for (const kachel of k) {
|
|
if (!kachel || kachel.ton == null) continue;
|
|
if (!alle.has(kachel.ton)) alle.set(kachel.ton, new Set());
|
|
alle.get(kachel.ton).add(kachel.name);
|
|
if (!hier.has(kachel.ton)) hier.set(kachel.ton, []);
|
|
hier.get(kachel.ton).push(kachel.name);
|
|
}
|
|
if (hier.size) ansichten.push({ rolle: r, haus, toene: hier });
|
|
}
|
|
}
|
|
return { alle, ansichten };
|
|
}
|
|
|
|
/* ---- Die beste freie Stelle im Farbraum ---------------------------- */
|
|
/* NICHT BLASS (22.09.2026).
|
|
|
|
Der erste Lauf schlug fuer "Dateien" ein #ffe5ae vor -- Buntheit
|
|
0,076, ein helles Creme. Rechnerisch war es die beste Stelle im
|
|
Farbraum; auf dem Bildschirm ist es das, was Filipe seit Wochen
|
|
abschafft ("die kacheln sind viel zu hell, man bekommt fast nichts
|
|
gelesen"), und das Gegenteil von "uebelst geil".
|
|
|
|
Die Schwelle ist GEMESSEN, nicht geraten: Der Median der Buntheit
|
|
aller vorhandenen Toene liegt bei 0,168, die fuenf blassesten
|
|
zwischen 0,064 und 0,088. 0,12 laesst alles zu, was heute als
|
|
kraeftig durchgeht, und schliesst genau die blassen aus. */
|
|
const MINDEST_BUNTHEIT = 0.12;
|
|
|
|
function beste(andere) {
|
|
let treffer = null, wert = -1;
|
|
for (let H = 0; H < 360; H += 1) {
|
|
for (let L = 0.56; L <= 0.94; L += 0.01) {
|
|
for (let C = 0.30; C >= 0.04; C -= 0.004) {
|
|
const hex = ausOklch(L, C, H);
|
|
if (!hex) continue;
|
|
if (kontrast(hex) < 4.5) break;
|
|
/* Mehr Buntheit geht bei diesem L/H nicht -- dann taugt die
|
|
ganze Kombination nicht, statt sie blass zu nehmen. */
|
|
if (C < MINDEST_BUNTHEIT) break;
|
|
const lab = oklab(hex);
|
|
let naechster = Infinity;
|
|
for (const v of andere) naechster = Math.min(naechster, abstand(lab, v.lab));
|
|
if (naechster > wert) { wert = naechster; treffer = { hex, lab }; }
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return { ...treffer, abstand: wert };
|
|
}
|
|
|
|
function engstesPaar(liste) {
|
|
let best = { d: Infinity, a: null, b: null };
|
|
for (let i = 0; i < liste.length; i++) {
|
|
for (let j = i + 1; j < liste.length; j++) {
|
|
const d = abstand(liste[i].lab, liste[j].lab);
|
|
if (d < best.d) best = { d, a: liste[i], b: liste[j] };
|
|
}
|
|
}
|
|
return best;
|
|
}
|
|
|
|
/* ===================================================================== */
|
|
const schreiben = process.argv.includes("--schreiben");
|
|
let css = readFileSync(CSS, "utf8");
|
|
const inDatei = vollstaendigOderAbbruch(css);
|
|
const { alle, ansichten } = await benutzteToene();
|
|
|
|
console.log(`${inDatei.length} Toene in start.css, ${alle.size} davon benutzt.`);
|
|
|
|
/* Zwei Kacheln mit demselben Ton IN DERSELBEN Ansicht -- das ist der
|
|
Fall, den ein Mensch als "zwei gleiche" sieht. */
|
|
let doppelt = 0;
|
|
for (const a of ansichten) {
|
|
for (const [ton, namen] of a.toene) {
|
|
if (namen.length > 1) {
|
|
console.log(` DOPPELT bei ${a.rolle}/${a.haus}: Ton ${ton} = ${namen.join(" + ")}`);
|
|
doppelt++;
|
|
}
|
|
}
|
|
}
|
|
console.log(doppelt === 0
|
|
? " Keine Ansicht zeigt zwei Kacheln mit derselben Farbe."
|
|
: ` ${doppelt} Doppelbelegungen -- die muessen von Hand getrennt werden.`);
|
|
|
|
/* Ab hier wird nur noch mit den BENUTZTEN gerechnet. */
|
|
let liste = inDatei.filter((t) => alle.has(t.n));
|
|
const start = engstesPaar(liste);
|
|
console.log(`\nEngstes Paar der benutzten: ${start.d.toFixed(4)}`
|
|
+ ` Ton ${start.a.n} (${[...alle.get(start.a.n)].join("/")})`
|
|
+ ` <-> Ton ${start.b.n} (${[...alle.get(start.b.n)].join("/")})`);
|
|
|
|
const geaendert = [];
|
|
for (let runde = 1; runde <= 40; runde++) {
|
|
const p = engstesPaar(liste);
|
|
/* WELCHEN DER BEIDEN? Den, der NICHT gesetzt ist. Sind beide
|
|
gesetzt, ist hier nichts zu tun -- dann sagt es das Werkzeug,
|
|
statt eine Festlegung zu uebergehen. */
|
|
const kandidat = GESETZT.has(p.a.n) ? (GESETZT.has(p.b.n) ? null : p.b) : p.a;
|
|
if (!kandidat) {
|
|
console.log(`\nBeide Toene des engsten Paares sind gesetzt (${p.a.n}, ${p.b.n}).`);
|
|
console.log(" Hier aendert das Werkzeug nichts -- das ist die Festlegung.");
|
|
break;
|
|
}
|
|
const andere = liste.filter((t) => t.n !== kandidat.n);
|
|
const neu = beste(andere);
|
|
/* NICHT SCHLECHTER ALS VORHER: Nur uebernehmen, wenn der kleinste
|
|
Abstand des GANZEN Satzes danach groesser ist. Sonst dreht sich
|
|
das Werkzeug im Kreis. */
|
|
const probe = liste.map((t) => (t.n === kandidat.n ? { ...t, hex: neu.hex, lab: neu.lab } : t));
|
|
const nachher = engstesPaar(probe).d;
|
|
if (nachher <= p.d + 1e-6) {
|
|
console.log(`\nKeine Verbesserung mehr moeglich (${p.d.toFixed(4)} bleibt).`);
|
|
break;
|
|
}
|
|
console.log(` Runde ${runde}: Ton ${kandidat.n} (${[...alle.get(kandidat.n)].join("/")})`
|
|
+ ` ${kandidat.hex} -> ${neu.hex} Minimum ${p.d.toFixed(4)} -> ${nachher.toFixed(4)}`);
|
|
geaendert.push({ n: kandidat.n, alt: kandidat.hex, neu: neu.hex, zeile: kandidat.zeile });
|
|
liste = probe;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------
|
|
ZWEITE RUNDE: DIE BLASSEN KRAEFTIG MACHEN
|
|
---------------------------------------------------------------------
|
|
Die erste Runde optimiert den ABSTAND. Ein Ton kann dabei weit weg
|
|
von allen anderen liegen und trotzdem blass sein -- dann fasst sie
|
|
ihn nie an. Gemessen am 22.09.2026 blieben so drei benutzte Toene
|
|
unter 0,12 stehen (#feb9b8, #a2c6fe, #b4f1fe), und genau darueber
|
|
klagt Filipe: "die kacheln sind viel zu hell, man bekommt fast
|
|
nichts gelesen".
|
|
|
|
Hier wird jeder blasse benutzte Ton neu gerechnet -- aber nur
|
|
uebernommen, wenn der kleinste Abstand des GANZEN Satzes dadurch
|
|
nicht sinkt. Kraeftiger werden darf nichts kosten. */
|
|
const blassRunde = [];
|
|
for (const t of [...liste]) {
|
|
if (GESETZT.has(t.n)) continue;
|
|
const [, a, b] = t.lab;
|
|
const c = Math.hypot(a, b);
|
|
if (c >= MINDEST_BUNTHEIT) continue;
|
|
const andere = liste.filter((x) => x.n !== t.n);
|
|
const vorher = engstesPaar(liste).d;
|
|
const neu = beste(andere);
|
|
const probe = liste.map((x) => (x.n === t.n ? { ...x, hex: neu.hex, lab: neu.lab } : x));
|
|
const nachher = engstesPaar(probe).d;
|
|
/* KRAEFTIG SCHLAEGT EIN PAAR TAUSENDSTEL ABSTAND.
|
|
Erster Entwurf verlangte, dass der Abstand gar nicht sinkt --
|
|
damit blieben zwei blasse Toene stehen, und der gemessene Preis
|
|
waere 0,1028 -> 0,1010 gewesen: 1,7 Prozent, auf dem Bildschirm
|
|
nicht zu sehen. Filipes Klage ueber helle Kacheln ist dagegen
|
|
konkret und wiederholt.
|
|
|
|
Die Untergrenze ist dieselbe Zahl, die auch die Pruefung
|
|
verlangt (0,090), mit etwas Luft. Darunter wird nicht getauscht --
|
|
dann waere aus einem blassen Paar ein aehnliches geworden. */
|
|
if (nachher < 0.095) {
|
|
console.log(` BLASS Ton ${t.n} (${[...alle.get(t.n)].join("/")}) ${t.hex} `
|
|
+ `bleibt -- kraeftiger ginge nur auf Kosten des Abstands `
|
|
+ `(${vorher.toFixed(4)} -> ${nachher.toFixed(4)})`);
|
|
continue;
|
|
}
|
|
const [, na, nb] = neu.lab;
|
|
console.log(` BLASS Ton ${t.n} (${[...alle.get(t.n)].join("/")}) ${t.hex} -> ${neu.hex}`
|
|
+ ` Buntheit ${c.toFixed(3)} -> ${Math.hypot(na, nb).toFixed(3)}`
|
|
+ `, Minimum ${vorher.toFixed(4)} -> ${nachher.toFixed(4)}`);
|
|
blassRunde.push({ n: t.n, alt: t.hex, neu: neu.hex, zeile: t.zeile });
|
|
geaendert.push({ n: t.n, alt: t.hex, neu: neu.hex, zeile: t.zeile });
|
|
liste = probe;
|
|
}
|
|
if (blassRunde.length) console.log(` ${blassRunde.length} blasse Toene kraeftiger gemacht.`);
|
|
|
|
const ende = engstesPaar(liste);
|
|
console.log(`\nEngstes Paar danach: ${ende.d.toFixed(4)}`
|
|
+ ` (${start.d.toFixed(4)} vorher, Faktor ${(ende.d / start.d).toFixed(2)})`);
|
|
console.log(`Geaendert: ${geaendert.length} Toene.`);
|
|
|
|
let schlecht = 0;
|
|
for (const t of liste) if (kontrast(t.hex) < 4.5) schlecht++;
|
|
console.log(schlecht === 0
|
|
? "Alle benutzten Toene erreichen 4,5:1 gegen den Grund."
|
|
: `ACHTUNG: ${schlecht} Toene unter 4,5:1.`);
|
|
|
|
if (!schreiben) {
|
|
console.log("\n(nur gezeigt -- mit --schreiben eintragen)");
|
|
process.exit(0);
|
|
}
|
|
|
|
for (const g of geaendert) {
|
|
const neueZeile = g.zeile.replace(g.alt, g.neu).replace(g.alt.toUpperCase(), g.neu);
|
|
if (!css.includes(g.zeile)) {
|
|
console.error(`ABBRUCH: Zeile fuer Ton ${g.n} nicht mehr auffindbar.`);
|
|
process.exit(2);
|
|
}
|
|
css = css.replace(g.zeile, neueZeile);
|
|
}
|
|
writeFileSync(CSS, css);
|
|
console.log(`\n${geaendert.length} Zeilen in start.css geschrieben.`);
|