Files
dogfather-universe/tools/chat-kacheln-rechnen.mjs
T
DogFatherGitandClaude Opus 5 7a6749630c Das Rudel meint den ganzen Raum -- und die Kachelfarbe wird ein Ring
Zwei Auftraege vom 23.09.2026.

SCREEN 1: "da steht links immer noch der treff anstatt das rudel"

Und er hatte recht, auf eine Art, die niemand sehen konnte:
TREFF_NAME stand schon seit der Umbenennung auf "Das Rudel". Nur
setzt treffRaumId() den Namen ausschliesslich BEIM ANLEGEN -- der
Raum existierte laengst, also blieb die Zeile in der Datenbank auf
"Der Treff" stehen. Im Code das eine, auf dem Bildschirm das andere.

Dieselbe Falle stand direkt nebenan schon beschrieben ("drei Stellen,
von denen die dritte vergessen wird") -- die Vorsorge galt aber nur
fuer die Teilnehmer, nicht fuer den Namen. Eine halbe Selbstheilung
heilt die andere Haelfte nicht. Jetzt gleicht treffAngleichen() auch
den Namen ab; eine kuenftige Umbenennung ist wieder eine Zeile.
(IS NOT statt !=: Bei NULL ergaebe != in SQLite NULL, und die Zeile
bliebe unveraendert liegen.)

"und wenn wir da im chat @rudel machen will ich dass jeder in dem
chat markiert wird. nicht nur team ... und sehr wichtig ich rede nur
von dem chat wo die ganze community auch drin ist."

Bis heute erreichte der Ruf ueberall nur Team Dogi. Die Begruendung
stand im Code und war nicht falsch -- im Rudel-Raum sitzt die
Community. Genau das will Filipe jetzt, und zwar nur dort:

  im Rudel-Raum   -> alle, die drin sind
  ueberall sonst  -> Team Dogi, unveraendert

Was die alte Sorge entschaerft: RUFEN darf weiterhin nur Team Dogi --
kein Zuschauer kann alle wecken. Und die Nachtruhe gilt dort ohnehin.

darf_rudel folgt derselben Frage. Sonst entstuende ein stiller
Widerspruch: Ein Modi mit neun Zuschauern und ohne zweites
Teammitglied traefe mit @rudel neun Leute -- der Vorschlag beim
Tippen waere aber ausgeblendet. Die Funktion gaebe es, und niemand
faende sie.

Das Schild sagt jetzt die Wahrheit: "alle in diesem Chat" statt
"alle im Team". Stuende dort weiter das alte, waere es im Rudel-Raum
gelogen -- und zwar nach unten, also in die Richtung, in der man
leichtfertig drueckt.

Gegenprobe in pruef-erwaehnung: In der Testgruppe sitzen DogFather
und zwei Scouts; ein @rudel dort ruft NIEMANDEN. Waere die Regel
versehentlich im ganzen Haus aktiv, waeren die Scouts markiert.
"sonst nichts anfassen" ist damit gemessen, nicht versprochen.

SCREEN 2: "so eine art diagramm, wo man mit einem kreis herum gehen
kann und die farbe ganz genau selber auswaehlen kann"

Der Haken daran ist nicht die Optik. Die zwoelf Kacheln waren
GERECHNET, damit niemand sich unlesbar machen kann -- alle auf
derselben Leuchtdichte. Ein Farbkreis, der einfach den Farbton dreht,
wirft das weg: Ein gesaettigtes Gelb ist um ein Vielfaches heller als
ein gesaettigtes Blau, und helle Schrift ist darauf nicht mehr zu
lesen.

Der Ring ist deshalb KEINE neue Rechnung, sondern dieselbe in 360
Schritten statt in zwoelf. tools/chat-kacheln-rechnen.mjs --kreis
schreibt server/chat-kreis.js; buntest() und die Kontrastpruefung
darin gelten unveraendert. Ergebnis: ein Ring gleicher Helligkeit --
kein blendendes Gelb, kein abgesoffenes Blau. Das sieht ruhiger aus
als der uebliche Farbkreis, und genau das ist der Punkt.

Der Browser rechnet nichts. Er bekommt die 360 fertigen Farben und
legt sie in dieselbe Tabelle wie die Kacheln -- ab da ist "ton-214"
ein Schluessel wie "veilchen", und jede Stelle, die eine Farbe
nachschlaegt, funktioniert unveraendert.

Welche Kacheln neben dem Ring bleiben, wird ABGELEITET statt
aufgezaehlt: Grau hat keinen Farbton, Babyblau ist die eine helle mit
eigener Schrift. Nachgemessen liegen die elf bunten zwischen 0,0 und
2,4 von ihrer Ringfarbe entfernt, Grau bei 75,4 und Babyblau bei
192,9 -- zwischen 2,4 und 75 ist so viel Luft, dass die Schwelle
nicht knapp ist.

Gespeichert wird erst beim Loslassen. Wer einmal um den Ring fuehrt,
erzeugte sonst dreihundert Anfragen.

Gemessen (pruef-chat-neu, jetzt 32 Pruefungen, als Modi auf crew. mit
dem Finger): 265 px gross, aus 361 Farben gemalt, touch-action none
(sonst schiebt das Handy die Seite weg statt zu drehen), Drehen auf
drei Uhr ergibt genau Winkel 90, die Mitte zeigt exakt #675102,
vorgelesen als "Goldbraun, 90 Grad", in der Datenbank steht ton-90,
Pfeiltaste dreht ein Grad weiter. Und das eigentliche Versprechen:
alle 360 Toene tragen die Schrift der Blase, knappster Faktor 1,000.

Dabei zwei eigene Fehler, beide aufgeschrieben: Der erste Anlauf
wartete 30 Sekunden auf den Farbknopf -- am Handy weicht die Spalte
mit den Gespraechen zur Seite, sobald ein Chat offen ist. "Ist im
HTML" und "ist erreichbar" sind zwei Aussagen. Und die Schriftfarben
wurden an der falschen Klasse gemessen, was einen roten Haken an
einer heilen Stelle ergab.

Ausserdem: Das native confirm() beim Herausnehmen eines GIFs ist
weg -- meine eigene Abkuerzung vom Morgen. Gemeldet hat es
pruef-nachfrage, allerdings erst, nachdem der verlorene Backslash in
derselben Datei repariert war.

Zahlen: pruef-erwaehnung 122 (vorher 119), pruef-chat-neu 32
(vorher 19), pruef-nachfrage 46 bestanden.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-23 14:30:02 +02:00

510 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
DIE ZWÖLF CHAT-KACHELN NEU RECHNEN (22.09.2026)
---------------------------------------------------------------------
Filipe: „ich will dass die farben viel klarer und stärker sind und
die schrift vom chat soll dan auch angepasst werden ... die kachel
von den texten von den leuten im chat soll auch klarer und stärker
sein bitte."
WAS VORHER DA WAR UND WARUM ES SO BLASS WAR
Die zwölf Töne lagen bei einer Helligkeit von rund 1 % (#131a25 und
Verwandte). Auf dem Bildschirm sind das zwölf Grautöne mit einem
Hauch Farbe — genau das, was auf Filipes Bildschirmfoto zu sehen ist.
Der Grund dafür war richtig gedacht: Die Fußzeile der Blase (Uhrzeit,
„antworten", „kopieren") steht auf var(--text-leise) — einem leisen
Grau. Gegen eine kräftige Fläche wäre die unlesbar geworden. Die
dunkle Kachel war also keine Geschmacksentscheidung, sondern die
Folge einer zu leisen Schrift.
DESHALB WIRD BEIDES ZUSAMMEN GERECHNET, nicht nacheinander. Die
Blase bekommt ihre EIGENE Schriftfarbe (--blase-text/--blase-leise,
heller als die der Seite), und dieses Werkzeug rechnet aus, wie
kräftig die Fläche dann werden DARF.
Eine Farbe erst stärker zu machen und hinterher zu messen, ob die
Schrift noch geht, ist genau die Reihenfolge, mit der am selben Tag
schon einmal 27 von 31 Startkacheln unlesbar geworden sind.
DAS VERFAHREN
1. Die Schriftfarben werden aus dem CSS GELESEN, nicht hier
abgeschrieben. Eine zweite Liste wäre die, die beim nächsten
Farbwechsel vergessen wird.
2. Aus dem nötigen Kontrast und der dunkelsten dieser Schriften
folgt GENAU EINE Leuchtdichte — die hellste Fläche, die beide
Schriften noch tragen. Sie wird ausgerechnet, nicht gesucht.
3. Auf dieser einen Leuchtdichte bekommt jeder der elf Farbtöne die
höchste Buntheit, die der Bildschirm hergibt.
WARUM ALLE AUF DERSELBEN LEUCHTDICHTE LIEGEN: Die Kachel gehört
einem selbst, GESEHEN wird sie von allen anderen. Gleiche
Leuchtdichte heißt, dass der Kontrast bei jedem der zwölf derselbe
ist — niemand kann sich durch seine Wahl schlechter lesbar machen
als die anderen.
Nicht dieselbe empfundene Helligkeit, sondern dieselbe Leuchtdichte:
Das ist die Größe, an der Lesbarkeit hängt. Der erste Anlauf hat die
andere genommen und deshalb behauptet, es gebe gar keine Lösung.
AUFRUF
node tools/chat-kacheln-rechnen.mjs nur zeigen
node tools/chat-kacheln-rechnen.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 W = (...t) => join(HIER, "..", ...t);
/* ---- Farbrechnen ---------------------------------------------------- */
const zuLinear = (v) => (v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4);
const ausLinear = (v) => (v <= 0.0031308 ? v * 12.92 : 1.055 * v ** (1 / 2.4) - 0.055);
function helligkeit([r, g, b]) {
return 0.2126 * zuLinear(r / 255) + 0.7152 * zuLinear(g / 255) + 0.0722 * zuLinear(b / 255);
}
function kontrast(a, b) {
const ha = helligkeit(a), hb = helligkeit(b);
const [h, d] = ha > hb ? [ha, hb] : [hb, ha];
return (h + 0.05) / (d + 0.05);
}
function ausOklch(L, C, hGrad) {
const h = (hGrad * Math.PI) / 180;
const a = C * Math.cos(h), bb = C * Math.sin(h);
const l_ = (L + 0.3963377774 * a + 0.2158037573 * bb) ** 3;
const m_ = (L - 0.1055613458 * a - 0.0638541728 * bb) ** 3;
const s_ = (L - 0.0894841775 * a - 1.2914855480 * bb) ** 3;
const r = 4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_;
const g = -1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_;
const b2 = -0.0041960863 * l_ - 0.7034186147 * m_ + 1.7076147010 * s_;
return [r, g, b2];
}
/** Lässt der Bildschirm diese Farbe überhaupt zu? */
function imRaum(L, C, h) {
const [r, g, b] = ausOklch(L, C, h);
return r >= -1e-4 && g >= -1e-4 && b >= -1e-4
&& r <= 1.0001 && g <= 1.0001 && b <= 1.0001;
}
function zuRgb(L, C, h) {
return ausOklch(L, C, h)
.map((v) => Math.max(0, Math.min(255, Math.round(ausLinear(v) * 255))));
}
const hex = (rgb) => "#" + rgb.map((x) => x.toString(16).padStart(2, "0")).join("");
function ausHex(t) {
const m = t.replace("#", "");
const v = m.length === 3
? [...m].map((c) => c + c)
: [m.slice(0, 2), m.slice(2, 4), m.slice(4, 6)];
return v.map((x) => parseInt(x, 16));
}
/* ---- Die Schriftfarben AUS DEM CSS holen ---------------------------- */
/* Drei Dateien, weil die Blase in beiden Häusern vorkommt und das
Crew-Haus eigene Farben setzt. Genommen wird je Rolle die DUNKELSTE
der gefundenen — die bestimmt den schlechtesten Fall, und nur der
zählt. */
function farbenLesen() {
const dateien = ["chat.css", "crew-haus.css", "gate.css"];
const gefunden = new Map();
for (const d of dateien) {
const t = readFileSync(W("workspace", "assets", "css", d), "utf8");
for (const m of t.matchAll(/--([a-z0-9-]+):\s*(#[0-9a-fA-F]{3,6})\s*;/g)) {
if (!gefunden.has(m[1])) gefunden.set(m[1], []);
gefunden.get(m[1]).push(ausHex(m[2]));
}
}
return (name) => {
const l = gefunden.get(name);
if (!l || !l.length) return null;
return l.slice().sort((a, b) => helligkeit(a) - helligkeit(b))[0];
};
}
const dunkelste = farbenLesen();
/* WELCHE SCHRIFT STEHT AUF EINER BLASE, UND WAS BRAUCHT SIE.
---------------------------------------------------------------------
JEDE SCHRIFT HAT IHREN EIGENEN ANSPRUCH — eine gemeinsame Zahl war
hier falsch.
Erster Anlauf verlangte für beide 4,8:1. Das Ergebnis erreichte für
den Nachrichtentext 6,86:1 — und riss damit die 7:1, auf die
pruef-chatkachel seit dem 20.09. besteht. Die Begründung dort steht
und gilt: Ein Chat ist die Seite, auf der am längsten gelesen wird,
und AAA ist hier nicht zu viel verlangt.
Die Fußzeile ist kein Fließtext, sondern vier Handgriffe (Uhrzeit,
antworten, kopieren, anheften). Für sie gelten die 4,5:1 der Norm,
plus Spielraum auf 4,8 — ein Wert genau auf der Grenze wird beim
nächsten Rundungsschritt rot.
Bindend ist am Ende die STRENGERE der beiden, bezogen auf ihre
jeweilige Farbe. Welche das ist, rechnet das Werkzeug aus; es steht
hier bewusst nicht als Zahl. */
const SCHRIFTEN = [
["blase-text", ".chat-nachricht__text", 7.0],
["blase-leise", ".chat-nachricht__fuss", 4.8],
];
const pruefFarben = [];
for (const [name, wo, noetig] of SCHRIFTEN) {
const f = dunkelste(name);
if (!f) {
console.error("\nKONNTE NICHT NACHSEHEN: --" + name + " steht in keiner"
+ " der drei CSS-Dateien.\n Ohne sie lässt sich nicht ausrechnen, wie"
+ " kräftig die Fläche sein darf.\n Erst --" + name + " in chat.css"
+ " anlegen, dann dieses Werkzeug erneut aufrufen.");
process.exit(2);
}
pruefFarben.push({ name, wo, noetig, farbe: f });
}
console.log("Schriftfarben aus dem CSS gelesen:");
for (const s of pruefFarben) {
console.log(" --" + s.name.padEnd(12) + " " + hex(s.farbe) + " braucht "
+ s.noetig.toFixed(1) + ":1 " + s.wo);
}
/* ---- Die eine Leuchtdichte, auf der alle zwölf liegen ---------------
---------------------------------------------------------------------
ZWEITER ANLAUF, UND DIESMAL DIE RICHTIGE GRÖSSE.
Der erste suchte eine gemeinsame OKLab-Helligkeit — also gleiche
EMPFUNDENE Helligkeit — und meldete danach, es gebe gar keine Lösung.
Das lag nicht an der Schrift, sondern an der Größe, die ich gleich
halten wollte: Lesbarkeit hängt nicht an der empfundenen Helligkeit,
sondern an der LEUCHTDICHTE. Ein gesättigtes Grün und ein gesättigtes
Blau bei derselben OKLab-Helligkeit unterscheiden sich in der
Leuchtdichte um ein Vielfaches — und damit im Kontrast zur Schrift.
Gelb fiel deshalb bei jeder Helligkeit durch.
Das Versprechen der zwölf Töne lautet wörtlich „damit bleibt die
Schrift überall gleich gut lesbar". Gleiche Leuchtdichte heißt:
jeder der zwölf hat EXAKT denselben Kontrast. Das ist das
Versprechen, gemessen statt umschrieben.
UND SIE WIRD NICHT GESUCHT, SONDERN AUSGERECHNET. Aus dem nötigen
Kontrast und der dunkelsten Schrift auf der Blase folgt genau ein
Wert. Suchen müsste man nur, wenn er sich nicht herleiten ließe.
-------------------------------------------------------------------- */
const ANZAHL_BUNT = 11; /* + 1 neutraler Standard = 12 */
/* Start bei 258 Grad (Blau-Violett), wo die Hausfarbe ohnehin liegt —
der Sprung vom Standard zur ersten bunten Wahl ist dann keiner. */
const HUES = Array.from({ length: ANZAHL_BUNT },
(_, i) => ((i * 360) / ANZAHL_BUNT + 258) % 360);
/* WIE BUNT MUSS ES MINDESTENS WERDEN, DAMIT DER UMBAU ETWAS BRINGT.
Gemessen an den zwölf, die bis heute drinstanden: ihre Buntheit liegt
zwischen 0,002 und 0,048, im Mittel 0,028 — deshalb sahen sie auf
Filipes Bildschirmfoto aus wie zwölf Grautöne. 0,06 liegt über dem
buntesten davon. Erreicht ein Ton das nicht, ist die Schrift auf der
Blase zu dunkel — dann sagt das Werkzeug das, statt wieder blasse
Farben auszuliefern. */
const MINDEST_BUNTHEIT = 0.06;
const HOECHST_BUNTHEIT = 0.33;
/* Die hellste Fläche, die JEDE Schrift auf der Blase noch trägt. */
let zielLeuchte = Infinity;
for (const sf of pruefFarben) {
zielLeuchte = Math.min(zielLeuchte, (helligkeit(sf.farbe) + 0.05) / sf.noetig - 0.05);
}
if (!(zielLeuchte > 0)) {
console.error("\nKONNTE NICHT NACHSEHEN: Selbst eine schwarze Fläche"
+ " erreicht den noetigen Kontrast nicht.\n Dann ist die Schrift auf der"
+ " Blase zu dunkel — erst --blase-leise aufhellen.");
process.exit(2);
}
/** Der buntestmögliche Ton dieses Farbtons auf genau dieser Leuchtdichte. */
function buntest(hGrad) {
let besteC = 0, besteRgb = null;
for (let L = 0.20; L <= 0.80; L += 0.002) {
for (let c = HOECHST_BUNTHEIT; c >= 0.004; c -= 0.002) {
if (!imRaum(L, c, hGrad)) continue;
const rgb = zuRgb(L, c, hGrad);
/* Gerundete Bildpunkte treffen den Zielwert nie genau — ein
Prozent Spielraum. Der Kontrast wird danach am GERUNDETEN
Ergebnis nachgeprüft, nicht am gerechneten: ausgeliefert wird
das gerundete. */
/* EINE OBERGRENZE, KEIN MITTELWERT. Erster Anlauf erlaubte ein
Prozent nach BEIDEN Seiten -- und ein Prozent nach oben
reisst genau die Schwelle, aus der dieser Wert hergeleitet
ist. Nach unten sind drei Prozent erlaubt, damit gerundete
Bildpunkte ueberhaupt einen Treffer haben. */
const y = helligkeit(rgb);
if (y > zielLeuchte || y < zielLeuchte * 0.97) continue;
if (!pruefFarben.every((sf) => kontrast(rgb, sf.farbe) >= sf.noetig)) continue;
if (c > besteC) { besteC = c; besteRgb = rgb; }
break;
}
}
return besteRgb ? { c: besteC, rgb: besteRgb } : null;
}
/* =====================================================================
DER GANZE KREIS (23.09.2026)
Filipe: "anstatt sowas um die farben auszusuchen, kannst du nicht so
eine art diagramm machen wo man mit einem kreis herum gehen kann und
sie die farbe ganz genau selber auswaehlen kann?"
---------------------------------------------------------------------
WARUM DAS HIER STEHT UND NICHT IM BROWSER
Die zwoelf Kacheln sind nicht ausgesucht, sondern GERECHNET: Jede
liegt auf derselben Leuchtdichte, damit die Schrift ueberall gleich
gut lesbar ist und niemand sich durch seine Wahl unlesbar machen
kann. Ein Farbkreis, der einfach den Farbton dreht, wirft genau das
weg -- ein gesaettigtes Gelb ist um ein Vielfaches heller als ein
gesaettigtes Blau, und helle Schrift darauf ist nicht mehr zu lesen.
Der Kreis ist deshalb KEINE neue Rechnung, sondern dieselbe in 360
Schritten statt in zwoelf. buntest() entscheidet fuer jeden Winkel,
und die Kontrastpruefung darin gilt unveraendert. Was dabei
herauskommt, ist ein Ring gleicher Helligkeit: Er sieht ruhiger aus
als ein ueblicher Farbkreis -- kein blendendes Gelb, kein
abgesoffenes Blau -- und genau das ist der Punkt.
DER BROWSER RECHNET NICHTS. Er bekommt die 360 fertigen Farben. Wer
die Formel in JavaScript nachbaut, hat zwei Rechnungen, und die
zweite ist ab dem naechsten Schriftwechsel falsch -- ohne dass es
jemand merkt, weil beide "irgendeine Farbe" liefern.
===================================================================== */
if (process.argv.includes("--kreis")) {
const SCHRITTE = 360;
const toene = [];
/* DIE ZAHLEN BLEIBEN LIEGEN, nicht nur das Hex. kontrast() und
helligkeit() erwarten [r,g,b] -- gaebe man ihnen die
Zeichenkette "#aabbcc", zerlegt die Destrukturierung sie in
"#", "a", "a" und rechnet ab da mit NaN. Der Vergleich
"NaN < 1" ist immer falsch, die Pruefung waere also gruen und
haette nichts gemessen. */
const rgbs = [];
const schwach = [];
let fehlend = 0;
for (let h = 0; h < SCHRITTE; h++) {
const b = buntest(h);
if (!b) { fehlend++; toene.push(null); rgbs.push(null); continue; }
toene.push(hex(b.rgb));
rgbs.push(b.rgb);
if (b.c < MINDEST_BUNTHEIT) schwach.push(h);
}
/* DRITTER AUSGANG. Findet sich fuer einen Winkel ueberhaupt keine
Farbe, die den Kontrast haelt, ist der Ring unterbrochen -- dann
wird NICHT stillschweigend etwas Naheliegendes eingesetzt,
sondern gesagt, dass es nicht geht. Eine Luecke, die als Farbe
getarnt ist, faellt niemandem auf. */
if (fehlend) {
console.error("\nKONNTE NICHT NACHSEHEN: " + fehlend + " von " + SCHRITTE
+ " Winkeln halten den Kontrast nicht."
+ "\n Heisst: die Schrift auf der Blase ist zu dunkel."
+ "\n Erst --blase-leise in chat.css aufhellen, dann erneut rechnen.");
process.exit(2);
}
/* DIE NACHPRUEFUNG AM AUSGELIEFERTEN WERT, nicht am gerechneten.
Ausgeliefert wird das gerundete Hex -- und gerundet wird nach der
Kontrastpruefung. Das ist die Stelle, an der am 22.09. schon
einmal drei Hundertstel gefehlt haben. */
let schlechtester = Infinity, schlechtesterWinkel = -1;
for (let h = 0; h < SCHRITTE; h++) {
for (const sf of pruefFarben) {
const k = kontrast(rgbs[h], sf.farbe);
if (k / sf.noetig < schlechtester) { schlechtester = k / sf.noetig; schlechtesterWinkel = h; }
}
}
if (schlechtester < 1) {
console.error("\nNICHT IN ORDNUNG: Winkel " + schlechtesterWinkel
+ " verfehlt seine Schwelle (Faktor " + schlechtester.toFixed(3) + ").");
process.exit(2);
}
console.log("Kreis gerechnet: " + SCHRITTE + " Winkel, alle ueber ihrer Schwelle"
+ " (knappster: Winkel " + schlechtesterWinkel + ", Faktor "
+ schlechtester.toFixed(3) + ").");
if (schwach.length) {
console.log(" Unter Buntheit " + MINDEST_BUNTHEIT + ": " + schwach.length
+ " Winkel. Das ist im Kreis in Ordnung -- dort darf es blasse"
+ " Stellen geben, bei den zwoelf Kacheln nicht.");
}
const ziel = new URL("../server/chat-kreis.js", import.meta.url);
const zeilen = [];
for (let i = 0; i < SCHRITTE; i += 6) {
zeilen.push(" " + toene.slice(i, i + 6).map((t) => '"' + t + '"').join(", ") + ",");
}
writeFileSync(ziel, [
"/* ERZEUGT von tools/chat-kacheln-rechnen.mjs --kreis.",
" NICHT VON HAND AENDERN -- wer hier eine Farbe eintippt, umgeht",
" genau die Kontrastrechnung, die die Lesbarkeit garantiert.",
"",
" 360 Farbtoene, Schritt 1 Grad, alle auf derselben Leuchtdichte",
" wie die zwoelf Kacheln. Der Index IST der Winkel: KREIS[214] ist",
" 214 Grad. */",
"export const KREIS = [",
...zeilen,
"];",
"",
"/** Der Winkel zu einem Schluessel der Form \"ton-214\", sonst null. */",
"export function winkelAus(wert) {",
" const t = /^ton-(\\d{1,3})$/.exec(String(wert ?? \"\"));",
" if (!t) return null;",
" const n = Number(t[1]);",
" return n >= 0 && n < KREIS.length ? n : null;",
"}",
"",
].join("\n"), "utf8");
console.log("Geschrieben: server/chat-kreis.js");
process.exit(0);
}
const bunte = HUES.map((h) => ({ h, ...(buntest(h) || {}) }));
const fehlen = bunte.filter((b) => !b.rgb || b.c < MINDEST_BUNTHEIT);
if (fehlen.length) {
console.error("\nKONNTE NICHT NACHSEHEN: " + fehlen.length + " von "
+ ANZAHL_BUNT + " Farbtönen bleiben unter Buntheit " + MINDEST_BUNTHEIT
+ ".\n Betroffen: " + fehlen.map((f) => Math.round(f.h) + " Grad").join(", ")
+ "\n Heißt: die Schrift auf der Blase ist zu dunkel für kräftige"
+ " Farben.\n Erst --blase-leise in chat.css aufhellen, dann erneut"
+ " rechnen.");
process.exit(2);
}
/* Der neutrale Standard liegt auf derselben Leuchtdichte — sonst wäre
ausgerechnet die Voreinstellung die Ausnahme. */
/* UND ER WIRD GENAUSO NACHGEPRÜFT WIE DIE ANDEREN ELF.
Erster Anlauf prüfte hier nur die Leuchtdichte und verließ sich auf
das Prozent Spielraum. Das Ergebnis war #50555e mit 6,97:1 — um drei
Hundertstel unter der eigenen Schwelle, und ausgerechnet bei der
Voreinstellung, die die meisten sehen. Die Schlusszeile des Werkzeugs
hat es gemeldet; gefunden hat es also die Prüfung, nicht der Blick.
Ein Spielraum, der in EINE Richtung wirkt, ist kein Spielraum. */
let grundRgb = null;
for (let L = 0.20; L <= 0.80; L += 0.001) {
const rgb = zuRgb(L, 0.016, 262);
const y = helligkeit(rgb);
if (y > zielLeuchte || y < zielLeuchte * 0.97) continue;
if (!pruefFarben.every((sf) => kontrast(rgb, sf.farbe) >= sf.noetig)) continue;
grundRgb = rgb; break;
}
if (!grundRgb) {
console.error("\nKONNTE NICHT NACHSEHEN: für den neutralen Standard ließ"
+ " sich die Leuchtdichte nicht treffen.");
process.exit(2);
}
/* ---- Die zwölf ------------------------------------------------------ */
/* DIE SCHLÜSSEL LINKS SIND KEINE BESCHRIFTUNG — SIE STEHEN IN DER
DATENBANK. Wer sie umbenennt, setzt die Wahl der betroffenen Leute
still auf „Standard" zurück: `kachelPruefen` kennt den alten Wert
dann nicht mehr und gibt null.
Vor diesem Umbau nachgesehen (nur lesend auf der Live-Datenbank):
gewählt sind `veilchen` (2×), `ziegel` (1×) und `moos` (1×), sonst
nichts. Alle drei stehen weiter unten und behalten ihren Farbbereich
— Veilchen bleibt violett, Ziegel bleibt rot, Moos bleibt grün.
Niemand verliert seine Einstellung.
Die Reihenfolge ist NICHT frei: Sie gehört zu HUES, Eintrag für
Eintrag. Wer hier umsortiert, vertauscht die Farben. */
const TOENE = [
["tinte", "Tinte"], ["veilchen", "Veilchen"], ["pflaume", "Pflaume"],
["kirsche", "Kirsche"], ["ziegel", "Ziegel"], ["bernstein", "Bernstein"],
["oliv", "Oliv"], ["moos", "Moos"], ["tanne", "Tanne"],
["lagune", "Lagune"], ["ozean", "Ozean"],
];
if (TOENE.length !== HUES.length) {
console.error("\nKONNTE NICHT NACHSEHEN: " + TOENE.length + " Namen für "
+ HUES.length + " Farbtöne.");
process.exit(2);
}
/* „Neutral" und nicht mehr „Standard": Seit dem 22.09.2026 ist dieser
Ton keine Voreinstellung mehr, sondern eine Wahl -- „ich will keine
Farbe". Wer nichts einstellt, bekommt automatisch einen der elf
bunten. Ein Eintrag, der „Standard" heisst und nicht der Standard
ist, waere die unfreundlichste Art, das zu verschweigen. */
const liste = [{ wert: "grund", name: "Neutral", farbe: hex(grundRgb) }];
bunte.forEach((b, i) => {
liste.push({ wert: TOENE[i][0], name: TOENE[i][1], farbe: hex(b.rgb) });
});
console.log("\nGemeinsame Leuchtdichte: " + zielLeuchte.toFixed(4)
+ " (die zwölf bisherigen lagen bei rund 0,010 — daher das Grau)");
console.log("Buntheit: " + Math.min(...bunte.map((b) => b.c)).toFixed(3)
+ " bis " + Math.max(...bunte.map((b) => b.c)).toFixed(3)
+ " (bisher 0,002 bis 0,048)");
let schlechtester = Infinity;
for (const k of liste) {
const rgb = ausHex(k.farbe);
const ks = pruefFarben.map((s) => kontrast(rgb, s.farbe));
schlechtester = Math.min(schlechtester, ...ks);
console.log(" " + k.name.padEnd(10) + " " + k.farbe + " "
+ ks.map((v) => v.toFixed(2) + ":1").join(" "));
}
console.log("\nJede Schrift gegen alle zwoelf, schlechtester Fall:");
for (const sf of pruefFarben) {
const m = Math.min(...liste.map((k) => kontrast(ausHex(k.farbe), sf.farbe)));
console.log(" --" + sf.name.padEnd(12) + " " + m.toFixed(2) + ":1 (noetig "
+ sf.noetig.toFixed(1) + ") " + (m >= sf.noetig ? "haelt" : "ZU WENIG"));
}
/* ---- Eintragen ------------------------------------------------------ */
if (!process.argv.includes("--schreiben")) {
console.log("\nNur gezeigt. Mit --schreiben wird es eingetragen.");
process.exit(0);
}
const datei = W("server", "workspace-chat.js");
const quelle = readFileSync(datei, "utf8");
const anf = quelle.indexOf("export const CHAT_KACHELN = [");
const ende = quelle.indexOf("\n];", anf);
if (anf < 0 || ende < 0) {
console.error("KONNTE NICHT NACHSEHEN: CHAT_KACHELN steht nicht mehr da,"
+ " wo dieses Werkzeug sie sucht.");
process.exit(2);
}
const breite = Math.max(...liste.map((k) => k.wert.length)) + 3;
const neu = "export const CHAT_KACHELN = [\n"
+ liste.map((k) => " { wert: " + ('"' + k.wert + '",').padEnd(breite)
+ " name: " + ('"' + k.name + '",').padEnd(13)
+ " farbe: \"" + k.farbe + "\" },").join("\n");
writeFileSync(datei, quelle.slice(0, anf) + neu + quelle.slice(ende), "utf8");
console.log("\nEingetragen in server/workspace-chat.js");
/* ---- UND DER RUECKFALL IM STILBLATT ---------------------------------
Das Stilblatt braucht den Standardton EBENFALLS: Wer nie etwas
eingestellt hat, bekommt vom Server keinen Wert, und die Blase faellt
auf das zurueck, was in chat.css steht.
Genau daran ist der erste Anlauf am 22.09.2026 gescheitert. Die
zwoelf Toene waren gerechnet und kraeftig -- und auf dem Bildschirm
sahen ALLE Blasen aus wie vorher, weil dort noch #131a25 stand, die
alte Farbe. Gemeldet hat das kein Werkzeug; gesehen hat es erst das
Bildschirmfoto. Eine Rechnung, die nur die Haelfte der Stellen
erreicht, ist keine Rechnung, sondern eine zweite Meinung.
Deshalb schreibt dasselbe Werkzeug beide Stellen. Von Hand gepflegt
liefen sie wieder auseinander -- die Frage waere nur, wann. */
const stil = W("workspace", "assets", "css", "chat.css");
const ANFANG = "/* ---- ANFANG GERECHNET: chat-kacheln-rechnen.mjs ---- */";
const SCHLUSS = "/* ---- ENDE GERECHNET ---- */";
const stilAlt = readFileSync(stil, "utf8");
const a2 = stilAlt.indexOf(ANFANG);
const e2 = stilAlt.indexOf(SCHLUSS);
if (a2 < 0 || e2 < 0) {
console.error("\nKONNTE NICHT NACHSEHEN: Die Marken fuer den Rueckfall"
+ " stehen nicht in chat.css."+QQ.replace(QQ,"")+"\n Erwartet:\n " + ANFANG + "\n "
+ SCHLUSS + "\n Ohne sie faellt jede Blase ohne eigene Wahl auf einen"
+ " alten Wert zurueck.");
process.exit(2);
}
const stilNeu = ANFANG + "\n"
+ "/* ERZEUGT VON tools/chat-kacheln-rechnen.mjs -- NICHT VON HAND AENDERN.\n"
+ " Der Ton, den eine Blase traegt, solange niemand etwas eingestellt\n"
+ " hat. Derselbe Wert wie der Eintrag Standard in CHAT_KACHELN. */\n"
+ ".chat-nachricht { --kachel-standard: " + liste[0].farbe + "; }\n"
+ SCHLUSS;
writeFileSync(stil, stilAlt.slice(0, a2) + stilNeu + stilAlt.slice(e2 + SCHLUSS.length), "utf8");
console.log("Rueckfall in chat.css gesetzt: " + liste[0].farbe);