Files
dogfather-universe/tools/kachel-farbe-einzeln.mjs
T
DogFatherGitandClaude Opus 5 19e9f471d3 Entwicklung: die Auswertung je Person -- und elf Seiten ohne Farbe
Filipe: "dan perfektionierst du eine neue wo diagramme texte und so zu
jedem modi gemacht wird und diese kategorie kannst du entwicklung nenen."

1. DAS EIGENTLICHE PROBLEM WAR, DASS ES KEINE VERGANGENHEIT GAB.

`entwicklung_stand` hat den Schluessel (person, punkt, beurteiler) und
wird bei jeder Aenderung UEBERSCHRIEBEN -- dort richtig, eine Beobachtung
ueber einen Menschen soll aktuell sein. Nur: Damit existierte nichts,
woraus sich ein Verlauf zeichnen liesse. Nachgemessen: 43 Zeilen Stand,
NULL Zeilen Geschichte. Ein Diagramm ueber einen einzigen Zeitpunkt ist
kein Diagramm, sondern ein Balken.

Neu: `entwicklung_lauf` haengt jede Aenderung an, statt zu ueberschreiben.
Sie steht in workspace-entwicklung.js, bei der Tabelle, zu der sie
gehoert -- die Auswertung liest nur. Andersherum gaebe es einen Ring
zwischen zwei Modulen, und Ringe halten in JavaScript genau so lange,
bis jemand eine Konstante auf oberster Ebene benutzt.

KEIN ANLASS, NIE. Neben jedem "da hakt es" steht ein Satz, woran man es
gemerkt hat -- fuer ein Gespraech aufgeschrieben. Eine Chronik dieser
Saetze waere das Belastendste, was dieses Haus speichern kann. Die Spur
hat gar keine Spalte dafuer, und die Pruefung liest die TABELLE, nicht
die Ausgabe: Eine Spalte, die es gibt, wird eines Tages gefuellt.

Die Startaufnahme uebernimmt den heutigen Stand mit seinen ECHTEN Daten,
damit die Seite am ersten Tag nicht leer ist -- und zaehlt ausdruecklich
NICHT als Bewegung. Der Tag, an dem die Spur entstand, war kein Tag, an
dem 43 Dinge gleichzeitig passiert sind.

2. WAS GEZEIGT WIRD: BEWEGUNG, KEINE NOTE.

Keine Punktzahl, kein Prozentwert, kein Durchschnitt, keine Rangliste,
kein Vergleich zwischen zwei Menschen. Das ist die Recherche, nicht
Vorsicht: Ueberrechtfertigung (Deci), sozialer Vergleich senkt die
Leistung derer, die schlechter dastehen, Schwaechenfokus ebenso.

Stattdessen das progress principle (Amabile/Kramer, ~12 000
Tagebucheintraege): Sichtbarer Fortschritt ist der staerkste einzelne
Treiber. Deshalb Diagramm 1 "Was sich bewegt hat" -- zwei Richtungen um
eine Nulllinie, ein Massstab fuer beide. Diagramm 2 der Streifen (Punkt
x Monat), fortgeschrieben, aber nach 90 Tagen sichtbar verblasst.
Und die Texte in dieser Reihenfolge: was sich bewegt hat, was traegt,
erst danach was liegt.

Selbst gezeichnet, kein Diagrammpaket: 60 kB ueber die Leitung von
jemandem, der mit dem Handy im Stream steht -- und es braechte eigene
Farben neben die Hausfarben. Jede Zelle traegt ihr Zeichen als Text,
jeder Balken seine Zahl, darunter eine Legende: Die Farbe ist nie die
Auskunft.

Zwei Gesichter: Die Leitung sieht die Karten, ein Modi ausschliesslich
die Bewegung in der eigenen -- ohne Namen, ohne Anlass, ohne Streifen.

3. NEBENBEFUND, GROESSER ALS ERWARTET: ELF SEITEN OHNE FARBE.

Der Umbau von heute frueh heisst "Jede Seite traegt die Farbe ihrer
Kachel". Nachgemessen traf das auf 18 von 34 Seiten zu. Elf Seiten mit
echter Kachel gingen leer aus -- ausgerechnet die von Team Dogi:
befinden, bewerben, bewerbungen, entwicklung, hilfe, rechte, talente,
teamlage, teilen, treff-moderation, treff-regeln, unsere-seiten.

Der Grund: `zuSeite()` sucht nur in `Bereiche.GRUPPEN`, und dort stehen
ausschliesslich die Kacheln der Agentur. Die von Team Dogi liefert der
Server (`ich.bereiche`, `ich.bereiche_zusatz`). Auffallen konnte das
nicht -- eine Seite ohne Farbe sieht aus wie eine, die eben keine hat.

kopf.js faerbt jetzt zweimal: sofort aus GRUPPEN (Agenturseiten ohne
Flackern wie bisher), und noch einmal aus den Serverkacheln, sobald
`werZeigen` sie bringt. Nur, wenn beim ersten Mal nichts gefunden wurde.

pruef-buehne misst den Kontrast an echten Bildpunkten und kannte diese
zwoelf Seiten nicht. Jetzt stehen sie in der Liste; ihre Frist waechst
mit der Laenge, statt als feste Zahl irgendwann zu reissen. Gemessen
fuer werdegang.html: 4,83:1 am Computer, 6,22:1 am Handy.

4. TON 38 SAH FREI AUS UND WAR ES NICHT.

Erster Anlauf war Ton 38, weil 37 die hoechste Nummer in workspace.js
ist. Gezaehlt in start.css sind es 39, alle vergeben. Genau davor warnt
das Farbwerkzeug im eigenen Kopf ("Ton 22, weil er frei AUSSAH").

Dabei aufgefallen: Die zwei Kacheln von heute Nacht wurden ohne das
Werkzeug gesetzt. Ton 39 lag 0,0362 von Ton 15 entfernt -- der engste
Abstand im ganzen Haus, zwei praktisch gleiche Cyan-Toene. Filipe dazu
mehrfach: "keine die sich irgendwie aehnlich sind".

Neues Werkzeug `tools/kachel-farbe-einzeln.mjs`: zieht EINE Farbe nach,
ohne die anderen anzufassen. Das grosse Werkzeug haette alle 40 neu
gefaerbt -- jede Kachel im Haus, ungefragt. Es bricht ab, wenn es nicht
alle Toene lesen kann: Der erste Anlauf las 31 von 40, weil einstellige
Nummern mit ZWEI Leerzeichen dastehen, und rechnete froehlich weiter.

Ergebnis: kleinster Abstand 0,0362 -> 0,0840, durch Aendern von zwei
Kacheln, die es erst seit gestern Nacht gibt.

5. anruf-probe.html lud `basis.css` und `kopf.css` -- beide gibt es
nicht. Ausgerechnet dort faellt es nicht auf, weil die Seite ihre eigene
Stilangabe im Kopf hat. pruef-struktur war deshalb rot (5 Befunde) und
ist jetzt gruen.

GEMESSEN: pruef-werdegang 95/0 (neu), pruef-entwicklung 43/0,
pruef-befinden 75/0, pruef-rechtetafel 19/0, pruef-zwischenspeicher 21/0,
pruef-start-ansicht 0 Fehler, pruef-struktur 0 Fehler (vorher 5),
pruef-buehne fuer werdegang.html 0 Fehler.

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

260 lines
11 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";
const HIER = dirname(fileURLToPath(import.meta.url));
const CSS = join(HIER, "..", "workspace", "assets", "css", "start.css");
const GRUND = "#0a121e";
/* ---------- 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“. */
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;
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);
}
}