Files
dogfather-universe/tools/kachel-farbe-einzeln.mjs
T
DogFatherGitandClaude Opus 5 b79b75ab70 A3: Calls & Protokolle gibt es jetzt auch bei Team Dogi
Filipe, 22.09.2026: „ich will diese kachel vom workspace auch im team
dogi seite. ich will dass jeder immer nur seine eigenen sachen von
seinem kalender sieht mehr nicht. perfektioniere das bitte. jeder der
einen kalender hat soll auch sowas haben danke."

GEMESSEN, BEVOR GEBAUT WURDE:

    Rolle   Kacheln   Kalender   Calls
    admin      31       ja        NEIN
    hand       31       ja        NEIN
    linke      30       ja        NEIN
    modi       26       ja        NEIN
    gast       12       nein      nein

Die Community hat keinen Kalender. Sein Satz „jeder der einen kalender
hat" beschreibt die Lage also genau -- „alle bekommen es" waere falsch
gewesen.

ES IST EINE REGEL, KEINE VIERFACHE EINTRAGUNG. Hinter jeden Kalender
einer Liste kommt eine Calls-Kachel, in derselben Gruppe. Vier Stellen
von Hand zu pflegen waere die Stelle, an der es beim naechsten neuen
Rollenzuschnitt auseinanderlaeuft. Erkannt wird der Kalender am ZIEL,
nicht am Namen -- Namen sind im Haus schon gewandert, Ziele nicht.

„JEDER SIEHT NUR SEINE EIGENEN" GAB ES SCHON, seit dem 07.09.
(`termineSichtbar`). Es wird jetzt aber nicht mehr GELESEN, sondern am
laufenden Server ausprobiert: Zwei Leute legen je einen Call an und
sehen den des anderen nicht -- auch DogFather nicht. Wer als
Teilnehmer eingetragen ist, sieht ihn schon; das ist die Gegenprobe,
ohne die „niemand sieht etwas" auch bei einer kaputten Liste gruen
waere.

DER TON WAR EINE RECHNUNG UEBER DREI ANLAEUFE

  1. Ton 15, derselbe wie im anderen Haus. Als Zahl frei, auf dem
     BILDSCHIRM nicht: 0,0139 zu „Was ansteht", unter der Grenze 0,02.
  2. Ton 16, ausgerechnet als der entfernteste brauchbare. Die Pruefung
     kennt aber zwei Regeln mehr, die ich nicht beruecksichtigt hatte:
     Rohabstand >= 0,090 (er lag bei 0,0854) und Buntheit >= 0,12 (er
     lag bei 0,116). Und Ton 16 gehoert im anderen Haus zu
     „Start-Check" -- ihn umzufaerben haette dort eine Kachel
     veraendert, nach der niemand gefragt hat.
  3. Unter allen elf freien Toenen erfuellte KEINER alle drei Regeln.
     Eine 32. Kachel braucht einen 32. Ton. Also einen neuen gerechnet,
     additiv, ohne einen vorhandenen anzufassen:

         Ton 43  #027afb   Abstand 0,0925  Buntheit 0,212  Kontrast 4,63:1

     Es bleibt ein Blau -- die Kachel ist damit als dieselbe
     wiedererkennbar wie im anderen Haus.

EIN WIDERSPRUCH IM HAUS, DABEI GEFUNDEN
`tools/kachel-farbe-einzeln.mjs` suchte die Buntheit von 0,30 bis
herunter auf 0,04, `pruef-kachelfarben` verlangt mindestens 0,12. Das
Werkzeug lieferte fuer Ton 43 zuerst ein blasses #bcdbff (Buntheit
0,060) mit dem besten Abstand weit und breit -- und die Pruefung lehnte
es ab. Beide hatten recht; zwei Zahlen fuer dieselbe Regel in zwei
Dateien, die nichts voneinander wissen. Ein Werkzeug, dessen
Vorschlaege die Pruefung danach verwirft, ist schlimmer als keines --
man haelt das Ergebnis fuer fertig.

Die Schwellen stehen jetzt in tools/kachelton-regeln.mjs, und beide
Seiten lesen sie von dort.

NEU: server/pruef-calls-rudel.mjs, 32/0. Sie prueft die REGEL (Calls
genau dann, wenn Kalender -- und direkt dahinter, in derselben
Gruppe), dass Kachel und Zutrittsrecht zusammenpassen, und am
laufenden Server, dass jeder nur seine eigenen sieht.

pruef-kachelfarben 22/0, pruef-start-ansicht, pruef-willkommen,
pruef-struktur, pruef-treff, pruef-modi-checkliste gruen.

ZWEI ALTLASTEN DABEI GEFUNDEN, nicht von dieser Aenderung und noch
nicht behoben (beide vorher schon rot, nachgemessen mit `git stash`):
  pruef-kachelraster        3 Fehler (erwartet acht Community-Kacheln,
                            es sind zehn)
  pruef-jeder-hat-eine-seite 1 Fehler („fehlt: linke" -- die Rolle kam
                            dazu, die Pruefung nicht mit)

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-22 15:15:00 +02:00

271 lines
12 KiB
JavaScript

/* =====================================================================
EINE KACHELFARBE NACHZIEHEN — ohne die anderen anzufassen (19.09.2026)
---------------------------------------------------------------------
WOZU ES DAS BRAUCHT, OBWOHL kachel-farben.mjs EXISTIERT
Das große Werkzeug rechnet ALLE Töne gegeneinander neu. Das ist die
richtige Rechnung und das falsche Mittel, wenn eine einzige Kachel
dazukommt: Am 19.09.2026 nachgemessen hätte ein Lauf für die 40.
Farbe alle 39 anderen verändert — jede Kachel im Haus bekäme eine
neue Farbe, ohne dass jemand danach gefragt hat.
Dieses Werkzeug macht das Gegenteil: Es hält alle vorhandenen Töne
fest und sucht für EINEN die Farbe, die am weitesten von allen
anderen entfernt ist.
Gemessen am selben Tag: Für die neue Kachel „Entwicklung“ fand es
einen Abstand von 0,0988 — fast das Dreifache des engsten Paares,
das im Haus ohnehin schon stand (0,0362 zwischen Ton 15 und 39).
Ein Gesamtneuanstrich war also gar nicht nötig.
---------------------------------------------------------------------
DIE VIER BEDINGUNGEN sind dieselben wie im großen Werkzeug:
1. LESBAR. Mindestens 4,5:1 gegen den dunklen Grund (#0a121e).
Nicht verhandelbar — eine Kachelfarbe trägt auch Text.
2. SPECIELL. Buntheit bis 0,30. Augenschonend heißt dunkles
Schema, kein Flackern — NICHT blass.
3. TRENNBAR. Der Abstand zur nächstliegenden vorhandenen Farbe
wird so groß wie möglich gemacht. Gerechnet in OKLab, weil
dort der Zahlenabstand dem entspricht, was das Auge sieht.
4. NICHT SCHLECHTER ALS VORHER. Findet sich nichts Besseres,
bleibt die alte Farbe stehen — und das Werkzeug sagt es.
---------------------------------------------------------------------
UND DIE FÜNFTE, DIE HIER DAZUKOMMT: ES MUSS ALLES LESEN KÖNNEN
Der erste Anlauf dieser Datei las 31 von 40 Tönen und rechnete
fröhlich weiter. Der Grund: Einstellige Nummern stehen in start.css
mit ZWEI Leerzeichen da (`[data-ton="1"] { …`), zur Ausrichtung.
Das Ergebnis war eine plausible, falsche Farbe — sie hätte mit einer
der neun ungelesenen kollidieren können, ohne dass es auffällt.
Deshalb bricht es jetzt ab, wenn die Zahl der gelesenen Töne nicht
zur Zahl der vorhandenen passt. Das ist der dritte Ausgang:
in Ordnung / nicht in Ordnung / KONNTE NICHT NACHSEHEN, mit Grund.
---------------------------------------------------------------------
AUFRUF
node tools/kachel-farbe-einzeln.mjs 40 nur zeigen
node tools/kachel-farbe-einzeln.mjs 40 --schreiben eintragen
node tools/kachel-farbe-einzeln.mjs --pruefen nur nachmessen
Mehrere Nummern gehen auch: `… 38 39 --schreiben` zieht sie
NACHEINANDER nach, jede gegen den Stand INKLUSIVE der vorherigen.
===================================================================== */
import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { GRUND, MINDEST_BUNTHEIT, HOECHST_BUNTHEIT, MINDEST_KONTRAST }
from "./kachelton-regeln.mjs";
const HIER = dirname(fileURLToPath(import.meta.url));
const CSS = join(HIER, "..", "workspace", "assets", "css", "start.css");
/* ---------- Farbrechnung (gleich wie in kachel-farben.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]);
/* ---------- Lesen, und zwar vollstaendig ---------------------------- */
/** Alle Toene als {nummer, hex, lab, zeile}.
*
* `\s*` UND NICHT EIN LEERZEICHEN: Einstellige Nummern stehen mit zwei
* Leerzeichen da. Genau daran ist der erste Anlauf gescheitert. */
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;
}
/** Bricht ab, wenn nicht jeder vorhandene Ton auch gelesen wurde. */
function vollstaendigOderAbbruch(css) {
const roh = new Set([...css.matchAll(/\[data-ton="(\d+)"\]/g)].map((m) => Number(m[1])));
const gelesen = lies(css);
const nummern = gelesen.map((t) => t.n);
if (roh.size !== nummern.length) {
console.error(`ABBRUCH: ${roh.size} Toene stehen in der Datei, gelesen wurden ${nummern.length}.`);
console.error(" nicht gelesen: " + [...roh].filter((n) => !nummern.includes(n)).join(", "));
console.error(" Wahrscheinlich hat sich die Schreibweise der Zeile geaendert.");
process.exit(2);
}
return gelesen;
}
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;
}
/** Die Farbe mit dem groessten Abstand zu allen uebergebenen. */
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) {
/* Von oben herunter, damit die Farbe so satt wie erlaubt wird
und nicht blass -- Filipe: „richtig geil und speziell“. */
/* DIE UNTERE GRENZE IST NICHT 0,04, SONDERN DIE DER PRUEFUNG.
Bis zum 22.09.2026 lief die Suche bis 0,04 herunter -- und
lieferte fuer Ton 43 ein blasses #bcdbff mit Buntheit 0,060.
Abstand 0,1339, der beste Wert weit und breit, und
pruef-kachelfarben hat ihn trotzdem abgelehnt: zu blass.
Ein Werkzeug, dessen Vorschlaege die Pruefung danach
verwirft, ist schlimmer als keines -- man haelt das
Ergebnis fuer fertig. Beide lesen die Zahl jetzt aus
kachelton-regeln.mjs. */
for (let C = HOECHST_BUNTHEIT; C >= MINDEST_BUNTHEIT; C -= 0.004) {
const hex = ausOklch(L, C, H);
if (!hex) continue;
if (kontrast(hex) < 4.5) 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, L, C, H }; }
break;
}
}
}
return { ...treffer, abstand: wert };
}
/* ---------- Ablauf --------------------------------------------------- */
const argumente = process.argv.slice(2);
const schreiben = argumente.includes("--schreiben");
const nurPruefen = argumente.includes("--pruefen");
const nummern = argumente.filter((a) => /^\d+$/.test(a)).map(Number);
let css = readFileSync(CSS, "utf8");
const anfang = vollstaendigOderAbbruch(css);
console.log(`${anfang.length} Toene gelesen — vollstaendig.`);
{
const p = engstesPaar(anfang);
console.log(`Engstes Paar jetzt: ${p.d.toFixed(4)} `
+ `Ton ${p.a.n} ${p.a.hex} (${p.a.wofuer}) <-> Ton ${p.b.n} ${p.b.hex} (${p.b.wofuer})`);
}
if (nurPruefen || !nummern.length) {
if (!nummern.length && !nurPruefen) {
console.log("\nKeine Nummer angegeben. Aufruf: node tools/kachel-farbe-einzeln.mjs <nr> [--schreiben]");
}
const sortiert = [];
for (let i = 0; i < anfang.length; i++) {
for (let j = i + 1; j < anfang.length; j++) {
sortiert.push({ d: abstand(anfang[i].lab, anfang[j].lab), a: anfang[i], b: anfang[j] });
}
}
sortiert.sort((x, y) => x.d - y.d);
console.log("\nDie fuenf engsten Paare:");
for (const p of sortiert.slice(0, 5)) {
console.log(` ${p.d.toFixed(4)} Ton ${p.a.n} (${p.a.wofuer}) <-> Ton ${p.b.n} (${p.b.wofuer})`);
}
const schwach = anfang.filter((t) => kontrast(t.hex) < 4.5);
console.log(schwach.length
? `\nZU DUNKEL (< 4,5:1): ${schwach.map((t) => `Ton ${t.n}`).join(", ")}`
: "\nAlle Toene erreichen 4,5:1 gegen den Grund.");
process.exit(0);
}
for (const ziel of nummern) {
/* JEDES MAL NEU LESEN: Wer zwei Nummern nacheinander nachzieht, soll
die zweite gegen den Stand INKLUSIVE der ersten rechnen. Sonst
bekommen beide dieselbe Farbe -- genau das ist beim ersten Anlauf
passiert. */
const alle = lies(css);
const vorher = alle.find((t) => t.n === ziel);
if (!vorher) { console.error(`Ton ${ziel} gibt es nicht.`); process.exit(2); }
const andere = alle.filter((t) => t.n !== ziel);
const alt = Math.min(...andere.map((t) => abstand(vorher.lab, t.lab)));
const neu = beste(andere);
if (neu.abstand <= alt) {
console.log(`\nTon ${ziel} (${vorher.wofuer}): bleibt ${vorher.hex} — `
+ `nichts Besseres gefunden (${alt.toFixed(4)}).`);
continue;
}
console.log(`\nTon ${ziel} (${vorher.wofuer})`);
console.log(` vorher ${vorher.hex} Abstand zum Nachbarn ${alt.toFixed(4)}`);
console.log(` nachher ${neu.hex} Abstand zum Nachbarn ${neu.abstand.toFixed(4)}`
+ ` (${(neu.abstand / alt).toFixed(1)}-fach)`);
console.log(` Kontrast ${kontrast(neu.hex).toFixed(2)}:1, Buntheit ${neu.C.toFixed(3)}`);
if (!schreiben) { console.log(" (nur gezeigt — mit --schreiben eintragen)"); continue; }
const neueZeile = vorher.zeile.replace(vorher.hex, neu.hex);
if (neueZeile === vorher.zeile) {
console.error(" ABBRUCH: Die Zeile liess sich nicht ersetzen.");
process.exit(2);
}
css = css.replace(vorher.zeile, neueZeile);
}
if (schreiben) {
writeFileSync(CSS, css);
const nachher = vollstaendigOderAbbruch(readFileSync(CSS, "utf8"));
const p = engstesPaar(nachher);
console.log(`\nGeschrieben. ${nachher.length} Toene, engstes Paar jetzt ${p.d.toFixed(4)} `
+ `(Ton ${p.a.n} <-> Ton ${p.b.n}).`);
const schwach = nachher.filter((t) => kontrast(t.hex) < 4.5);
if (schwach.length) {
console.error(`WARNUNG: ${schwach.map((t) => `Ton ${t.n}`).join(", ")} unter 4,5:1.`);
process.exit(1);
}
}