Wer mit der Tastatur bedient, sieht wieder, wo er steht

pruef-barrierefrei-workspace, wissen.html, Scout und Creator: Beim
Durchtabben veraendert sich an den Wissenskacheln NICHTS. Kein Rahmen,
kein Ring, keine Kante. Wer nicht mit der Maus arbeitet, tippt blind.

=== EIN SPEZIFITAETS-UNFALL, UND ER BETRIFFT NICHT NUR DIESE SEITE ===

Die Regel war da und richtig geschrieben:

  wissen.css   .kachel:focus-visible { box-shadow: 0 0 0 3px ... }

Sie kam nur nicht an. Gemessen mit einem neuen Werkzeug
(server/mess-fokus.mjs, echte Tastendruecke, kein focus()):

  :focus true   :focus-visible true
  boxShadow     gleich   rgba(0,0,0,0.95) 7px 7px 14px -10px inset
  outline       gleich   none

`:focus-visible` griff also, und trotzdem blieb alles, wie es war.
Der Grund steht in module.css, in einer Liste von 45 Klassennamen:

  :is(.eintrag-karte, ..., .kachel, ..., .gruppe[data-gruppe], ...)

`:is()` uebernimmt die Spezifitaet seines STAERKSTEN Arguments.
`.gruppe[data-gruppe]` ist eine Klasse PLUS ein Attribut. Damit ist die
ganze Liste (0,2,0) statt (0,1,0) -- genau so stark wie
`.kachel:focus-visible`. Bei Gleichstand gewinnt, was spaeter geladen
wird, und module.css wird zuletzt geladen. Ein einziges Attribut in
einer Aufzaehlung, sechs Zeilen weiter rechts, hat den Fokusring von
jedem Bauteil im Haus verschluckt, dessen Fokusregel aus einer Klasse
besteht.

=== GELOEST WIRD DAS NICHT, INDEM MAN DIE LISTE SCHWAECHER MACHT ===

Das war schon einmal so (`:where()`, Spezifitaet null) und ergab einen
Zwitter aus neuer Form und alter Kante -- der Kommentar in module.css
beschreibt es. Wer die Zahl senkt, verschiebt das Problem auf die
naechste Regel.

Geloest wird es, indem die ANTWORT dort steht: ein Fokusring fuer die
ganze Modulliste, in derselben Datei wie die Form, mit
`:focus-visible` also eine Klasse staerker als die Grundregel. Er gilt
damit fuer jedes Modul im Haus -- auch fuer die, die es noch nicht
gibt, und auch dort, wo nie jemand an eine Fokusregel gedacht hat.

`outline-offset` ist NEGATIV, und das ist kein Geschmack: `clip-path`
(die Fase an der Ecke) schneidet alles ab, was ausserhalb der Form
liegt. Ein Ring mit positivem Abstand waere unsichtbar gewesen -- der
alte war es ja auch. Nach innen gezeichnet bleibt er stehen. Gemessen,
nicht geschlossen.

=== WAS NICHT MITREPARIERT WURDE, UND WARUM ES DASTEHT ===

Derselbe Gleichstand trifft auch Hover-Regeln in frueher geladenen
Dateien: `.ablage:hover` (dateien.css), `.call:hover` (calls.css),
`.kk:hover` (scouting.css), `.fortschritt:hover` (uebersicht.css)
setzen alle `border-color`, und die Grundregel setzt `border: 0`.
Diese vier tun vermutlich nichts.

Angefasst habe ich sie nicht -- es gibt kein Messgeraet dafuer. Fokus
laesst sich pruefen (die Pruefung tabbt und vergleicht), Hover nicht.
Und die naheliegende Loesung wuerde die Form von 45 Bauteilen auf 38
Seiten neu entscheiden; das ohne Messgeraet zu tun waere Raten mit viel
Einsatz. Der Befund steht deshalb als Absatz in module.css, damit der
Naechste nicht wieder bei null anfaengt.

=== UND EINE BEHAUPTUNG VON MIR WIRD ZURUECKGENOMMEN ===

Im Commit d7bb0f7e steht, ein Mindestabstand von 0,10 sei fuer 45
Kachelfarben „rechnerisch nicht mehr moeglich", mit einer Tabelle von
„Decken". Das ist falsch herum gelesen: Das Werkzeug SUCHT eine
Anordnung. Findet es nichts Besseres, sagt das etwas ueber das
Verfahren, nicht ueber die Welt. Nachweisbar ist nur, was es GEFUNDEN
hat -- und wie wenig das eine Decke ist, zeigt der eigene Lauf: Fuer
dieselben 45 Farben kam dasselbe Verfahren je nach Rasterweite einmal
auf 0,0974 und einmal auf 0,0876.

Die Pruefung sagt das jetzt so, und das Werkzeug heisst zwar weiter
kachelton-decke.mjs, erklaert aber in den ersten zwanzig Zeilen, dass
es keine Decke misst. Beide Werkzeuge liegen jetzt ueberhaupt im
Verzeichnis: Sie hiessen `tools/_toene-*.mjs`, und `tools/_*` ist
ausgenommen -- die Kommentare verwiesen also auf Dateien, die es nach
dem Klonen nirgends gibt.

GEMESSEN, alles nach den Aenderungen:

  pruef-barrierefrei-workspace   18 Punkte, 0 Fehler (vorher 2)
  pruef-buehne                  230 Punkte, 0 Fehler
  pruef-kachelraster             15 Punkte, 0 Fehler
  pruef-start-ansicht           157 Punkte, 0 Fehler
  pruef-kopfleiste-farbe          9 Punkte, 0 Fehler
  pruef-rueckmeldung             30 Punkte, 0 Fehler
  pruef-entwicklung-kacheln      34 Punkte, 0 Fehler
  pruef-kachel-universum         13 Punkte, 0 Fehler

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-25 23:50:54 +02:00
co-authored by Claude Opus 5
parent 80988f2d23
commit 69ba533cee
46 changed files with 1332 additions and 663 deletions
+139
View File
@@ -0,0 +1,139 @@
/* =====================================================================
WIE WEIT BEKOMMT MAN N FARBEN AUSEINANDER? (25.09.2026)
Erst messen, dann bauen. Bevor an der Kachelpalette etwas geaendert
wird, muss die Frage beantwortet sein, welcher Mindestabstand fuer
eine gegebene Anzahl ueberhaupt zu SCHAFFEN ist -- sonst baut man
an einer Bedingung, die niemand erfuellen kann. Genau das war bei
„kein Paar unter 0,10" passiert.
WAS DIESES WERKZEUG SAGT -- und was nicht. Es sucht: aus allen
erlaubten sRGB-Farben werden N so gewaehlt, dass der kleinste
Abstand moeglichst gross wird (erst weitester Punkt, dann tauschen,
solange es besser wird).
Was herauskommt, ist ein Wert, der ERREICHBAR ist. Dafuer gibt es
eine Anordnung, und die kann man sich ansehen.
Was NICHT herauskommt, ist eine Obergrenze. „Mehr habe ich nicht
gefunden" heisst nicht „mehr gibt es nicht" -- das Verfahren ist
ein Naeherungsverfahren. Wie sehr das zaehlt, zeigt der eigene
Lauf: Fuer 45 Farben kam es je nach Rasterweite und
Buntheitsgrenze einmal auf 0,0974 und einmal auf 0,0876. Wer die
erste Zahl eine „Decke" nennt, liest die Ungleichung falsch
herum.
Die Schwellen kommen aus kachelton-regeln.mjs; ueber die Umgebung
lassen sie sich fuers Ausprobieren verschieben:
ANZAHL=52 KONTRAST=4.6 MAXLEUCHTE=0.9 node tools/kachelton-decke.mjs 0.10
Der Zahlenwert hinter dem Namen ist die geforderte Mindestbuntheit;
mehrere Werte hintereinander vergleichen mehrere Annahmen.
===================================================================== */
/* Die Schwellen kommen aus der Regeldatei -- abgeschrieben waeren sie
eine zweite Fassung derselben Regel, und genau daran ist am 22.09.
schon einmal ein Farbvorschlag gescheitert (siehe dort). Der kleine
Aufschlag beim Kontrast ist Luft fuers Runden und steht als solcher
da; alles andere laesst sich ueber die Umgebung setzen, weil das
Ausprobieren von Annahmen der Zweck dieses Werkzeugs ist. */
import { GRUND, MINDEST_KONTRAST } from "./kachelton-regeln.mjs";
const lin0 = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
const GRUND_LEUCHTE = [1, 3, 5].map((i) => lin0(parseInt(GRUND.slice(i, i + 2), 16) / 255))
.reduce((s, v, i) => s + [0.2126, 0.7152, 0.0722][i] * v, 0);
const MIN_KONTRAST = Number(process.env.KONTRAST || MINDEST_KONTRAST + 0.3);
const MAX_LEUCHTE = Number(process.env.MAXLEUCHTE || 0.82);
const ANZAHL = Number(process.env.ANZAHL || 45);
const lin = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
function lab(r0, g0, b0) {
const r = lin(r0 / 255), g = lin(g0 / 255), b = lin(b0 / 255);
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];
}
const leuchte = (r, g, b) => 0.2126 * lin(r / 255) + 0.7152 * lin(g / 255) + 0.0722 * lin(b / 255);
function kandidaten(minBunt, schritt) {
const raus = [];
for (let r = 0; r <= 255; r += schritt) {
for (let g = 0; g <= 255; g += schritt) {
for (let b = 0; b <= 255; b += schritt) {
const y = leuchte(r, g, b);
if ((y + 0.05) / (GRUND_LEUCHTE + 0.05) < MIN_KONTRAST) continue;
if (y > MAX_LEUCHTE) continue;
const L = lab(r, g, b);
if (Math.hypot(L[1], L[2]) < minBunt) continue;
raus.push({ r, g, b, L });
}
}
}
return raus;
}
const d2 = (a, b) => (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2;
function packen(k, n) {
/* 1. Weitester Punkt zuerst -- das gibt schon eine gute Verteilung. */
const gewaehlt = [k[Math.floor(k.length / 2)]];
const naechste = k.map((p) => d2(p.L, gewaehlt[0].L));
while (gewaehlt.length < n) {
let best = 0;
for (let i = 1; i < k.length; i++) if (naechste[i] > naechste[best]) best = i;
gewaehlt.push(k[best]);
for (let i = 0; i < k.length; i++) {
const d = d2(k[i].L, k[best].L); if (d < naechste[i]) naechste[i] = d;
}
}
/* 2. Tauschen: jeden Punkt durch den besten Kandidaten ersetzen. */
const minOhne = (idx) => {
let m = Infinity;
for (let i = 0; i < gewaehlt.length; i++) {
if (i === idx) continue;
for (let j = i + 1; j < gewaehlt.length; j++) {
if (j === idx) continue;
const d = d2(gewaehlt[i].L, gewaehlt[j].L); if (d < m) m = d;
}
}
return m;
};
for (let runde = 0; runde < 40; runde++) {
let geaendert = false;
for (let idx = 0; idx < gewaehlt.length; idx++) {
const grenze = minOhne(idx);
let besterWert = -1, bester = gewaehlt[idx];
for (const p of k) {
let m = Infinity;
for (let i = 0; i < gewaehlt.length; i++) {
if (i === idx) continue;
const d = d2(p.L, gewaehlt[i].L); if (d < m) { m = d; if (m <= besterWert) break; }
}
if (m > besterWert) { besterWert = m; bester = p; }
}
const jetzt = Math.min(grenze, Math.min(...gewaehlt.map((q, i) =>
(i === idx ? Infinity : d2(gewaehlt[idx].L, q.L)))));
if (Math.min(grenze, besterWert) > jetzt + 1e-12) { gewaehlt[idx] = bester; geaendert = true; }
}
if (!geaendert) break;
}
let m = Infinity;
for (let i = 0; i < gewaehlt.length; i++) {
for (let j = i + 1; j < gewaehlt.length; j++) m = Math.min(m, d2(gewaehlt[i].L, gewaehlt[j].L));
}
return { gewaehlt, min: Math.sqrt(m) };
}
const hex = (p) => "#" + [p.r, p.g, p.b].map((c) => c.toString(16).padStart(2, "0")).join("");
const stufen = process.argv.slice(2).length
? process.argv.slice(2).map(Number) : [0, 0.06, 0.09, 0.12, 0.15];
for (const mb of stufen) {
const k = kandidaten(mb, 5);
const { gewaehlt, min } = packen(k, ANZAHL);
console.log(`Buntheit >= ${mb.toFixed(2)} ${k.length} Kandidaten `
+ `${ANZAHL} Farben, kleinster Abstand ${min.toFixed(4)}`);
if (process.env.ZEIGEN) console.log(" " + gewaehlt.map(hex).join(" "));
}
+300
View File
@@ -0,0 +1,300 @@
/* =====================================================================
NUR DIE FARBEN BEWEGEN, DIE SICH WIRKLICH IN DIE QUERE KOMMEN
(25.09.2026, zweiter Anlauf)
Der erste Anlauf hat ALLE 45 Farben neu verteilt (die Rechnung
dazu steht in der Geschichte des Verzeichnisses, Commit d7bb0f7e).
Das Ergebnis war rechnerisch besser und praktisch falsch: Er ist
dabei ueber zwei ausdrueckliche Wuensche gelaufen (das knallrote
#ff1a1a fuer "Vertraulich melden", das Babyblau #a8d8ff fuer
"Support") und hat drei Toene unter die Buntheitsgrenze von 0,12
gedrueckt. pruef-kachelfarben hat alles fuenf gefunden.
DIE LEHRE IST NICHT "die Rechnung war falsch", sondern: Eine
Gesamtloesung fuer ein LOKALES Problem bezahlt man mit allem, was
an den nicht betroffenen Stellen schon richtig war. Acht Paare
standen zu eng -- also werden acht Paare auseinandergezogen und
sonst nichts angefasst.
VORGEHEN. Solange irgendein Paar unter dem Mindestabstand liegt:
das engste nehmen, den beweglicheren der beiden auf die naechste
erlaubte Farbe schieben, die alle Bedingungen haelt. Festgelegte
Toene sind unbeweglich.
ALLE SCHWELLEN KOMMEN AUS tools/kachelton-regeln.mjs. Keine Zahl
wird hier abgeschrieben -- genau dafuer gibt es die Datei.
node tools/kachelton-entzerren.mjs rechnen und zeigen
node tools/kachelton-entzerren.mjs schreiben start.css aendern
===================================================================== */
import { readFileSync, writeFileSync, mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
GRUND, MINDEST_ABSTAND, MINDEST_BUNTHEIT, HOECHST_BUNTHEIT,
MINDEST_KONTRAST, FESTGELEGT,
} from "./kachelton-regeln.mjs";
const DATEI = "workspace/assets/css/start.css";
/* Etwas Luft ueber jeder Schwelle -- gerundet wird auf ganze
Bildpunkte, und eine Farbe, die genau auf der Grenze steht, faellt
beim naechsten Runden darunter. */
const ZIEL_ABSTAND = MINDEST_ABSTAND + 0.004;
const ZIEL_KONTRAST = MINDEST_KONTRAST + 0.15;
const ZIEL_BUNT = MINDEST_BUNTHEIT + 0.004;
const lin = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
const srgb = (h) => [1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16) / 255);
function oklab(h) {
const [r, g, b] = srgb(h).map(lin);
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];
}
const leuchte = (h) => { const [r, g, b] = srgb(h).map(lin); return 0.2126 * r + 0.7152 * g + 0.0722 * b; };
const kontrast = (h) => (leuchte(h) + 0.05) / (leuchte(GRUND) + 0.05);
const bunt = (h) => { const [, a, b] = oklab(h); return Math.hypot(a, b); };
const abst = (a, b) => { const x = oklab(a), y = oklab(b); return Math.hypot(x[0] - y[0], x[1] - y[1], x[2] - y[2]); };
const hexVon = (r, g, b) => "#" + [r, g, b].map((c) => c.toString(16).padStart(2, "0")).join("");
/* ---------- Die Farben aus der Datei --------------------------------- */
const css = readFileSync(DATEI, "utf8");
const TOENE = [...css.matchAll(/\[data-ton="(\d+)"\]\s*\{\s*--ton:\s*(#[0-9a-fA-F]{6})/g)]
.map((m) => ({ n: Number(m[1]), alt: m[2].toLowerCase(), hex: m[2].toLowerCase() }));
if (TOENE.length < 20) { console.error("Muster passt nicht mehr"); process.exit(3); }
/* ---------- Wer darf sich nicht bewegen, wer darf blass sein --------- */
const FEST = new Map(FESTGELEGT.filter((f) => f.hex).map((f) => [f.ton, f.hex.toLowerCase()]));
const BLASS_ERLAUBT = new Map(FESTGELEGT.filter((f) => f.blassErlaubt).map((f) => [f.ton, f.blassErlaubt]));
for (const t of TOENE) {
if (FEST.has(t.n) && t.hex !== FEST.get(t.n)) {
console.log(` Ton ${t.n} steht auf ${t.hex}, festgelegt ist ${FEST.get(t.n)} -- wird zurueckgesetzt`);
t.hex = FEST.get(t.n);
}
}
/* ---------- WELCHE TOENE WERDEN UEBERHAUPT BENUTZT? ------------------
Die Buntheitsgrenze gilt nur fuer BENUTZTE Toene -- so steht es in
pruef-kachelfarben, und das ist der Grund, aus dem die Reparatur
ueberhaupt aufgeht: Mit Buntheit >= 0,12 fuer ALLE 45 ist ein
Mindestabstand von 0,09 rechnerisch unmoeglich (gemessene Decke:
0,0853). Die zwoelf unbenutzten duerfen blasser sein und nehmen
damit Druck aus dem engen Blau-Tuerkis-Bereich.
Gefragt wird das Programm selbst, nicht eine Liste. */
process.env.WORKSPACE_DB = process.env.WORKSPACE_DB
|| join(mkdtempSync(join(tmpdir(), "toene-")), "workspace.db");
const w = await import("../server/workspace.js");
const BENUTZT = new Set();
for (const r of ["admin", "hand", "linke", "modi", "gast", "manager", "scout", "creator", "spicy"]) {
for (const haus of ["crew", "agentur"]) {
let k = [];
try { k = [...(w.bereicheFuer({ id: 1, rolle: r, name: "X" }, haus) || []),
...(w.zusatzBereicheFuer({ id: 1, rolle: r, name: "X" }, haus) || [])]; }
catch { continue; }
for (const kachel of k) if (kachel?.ton != null) BENUTZT.add(Number(kachel.ton));
}
}
if (BENUTZT.size < 20) { console.error(`nur ${BENUTZT.size} benutzte Toene gefunden`); process.exit(5); }
console.log(`${BENUTZT.size} von ${TOENE.length} Toenen werden benutzt: ${[...BENUTZT].sort((a, b) => a - b).join(", ")}`);
/* Auch die unbenutzten sollen so bunt bleiben, wie es noch passt --
sonst steht beim naechsten Gebrauch ein blasser Ton bereit und die
Pruefung wird rot, obwohl niemand etwas falsch gemacht hat. */
const UNBENUTZT_BUNT = Number(process.env.UNBUNT || 0.045);
const buntNoetig = (n) => (BENUTZT.has(n) && !BLASS_ERLAUBT.has(n) ? ZIEL_BUNT
: Math.min(UNBENUTZT_BUNT, bunt(TOENE.find((t) => t.n === n).alt)));
/* Der verlangte Mindestabstand kann je Ton hoeher sein: Wer blass sein
darf, muss das beim Abstand ausgleichen (steht so in
pruef-kachelfarben und ist dort begruendet). */
const abstandFuer = (n) => Math.max(ZIEL_ABSTAND, (BLASS_ERLAUBT.get(n) ?? 0) + 0.004);
/* ---------- Alle erlaubten Farben ------------------------------------ */
const ALLE = [];
for (let r = 0; r <= 255; r += 2) {
for (let g = 0; g <= 255; g += 2) {
for (let b = 0; b <= 255; b += 2) {
const h = hexVon(r, g, b);
if (kontrast(h) < ZIEL_KONTRAST) continue;
const c = bunt(h);
if (c > HOECHST_BUNTHEIT) continue;
ALLE.push({ hex: h, bunt: c });
}
}
}
const kandidatenFuer = (n) => ALLE.filter((x) => x.bunt >= buntNoetig(n)).map((x) => x.hex);
console.log(`${ALLE.length} erlaubte Farben (Kontrast >= ${ZIEL_KONTRAST.toFixed(2)}, `
+ `Buntheit bis ${HOECHST_BUNTHEIT}; benutzte Toene ab ${ZIEL_BUNT.toFixed(3)})`);
/* ---------- Was steht zu eng? ---------------------------------------- */
const engstes = () => {
let m = Infinity, wo = null;
for (let i = 0; i < TOENE.length; i++) {
for (let j = i + 1; j < TOENE.length; j++) {
const noetig = Math.max(abstandFuer(TOENE[i].n), abstandFuer(TOENE[j].n));
const d = abst(TOENE[i].hex, TOENE[j].hex);
if (d < noetig && d - noetig < m) { m = d - noetig; wo = { i, j, d, noetig }; }
}
}
return wo;
};
const naechsterZu = (h, ausser) => {
let m = Infinity;
for (const t of TOENE) { if (t === ausser) continue; m = Math.min(m, abst(h, t.hex)); }
return m;
};
console.log("");
let bewegt = 0;
for (let runde = 0; runde < 200; runde++) {
const p = engstes();
if (!p) break;
const a = TOENE[p.i], b = TOENE[p.j];
/* Wer wird bewegt? Nicht der Festgelegte. Sonst der, fuer den die
naechste erlaubte Stelle naeher an seinem eigenen Ton liegt. */
const kandidatFuer = (t) => {
if (FEST.has(t.n)) return null;
let best = null, bestWeg = Infinity;
const noetig = abstandFuer(t.n);
for (const h of kandidatenFuer(t.n)) {
const weg = abst(t.alt, h);
if (weg >= bestWeg) continue;
let ok = true;
for (const u of TOENE) {
if (u === t) continue;
if (abst(h, u.hex) < Math.max(noetig, abstandFuer(u.n))) { ok = false; break; }
}
if (ok) { best = h; bestWeg = weg; }
}
return best ? { hex: best, weg: bestWeg } : null;
};
const ka = kandidatFuer(a), kb = kandidatFuer(b);
if (!ka && !kb) {
console.error(`Fuer ${a.hex} (Ton ${a.n}) und ${b.hex} (Ton ${b.n}) gibt es keinen Platz.`);
process.exit(4);
}
/* WER WEICHT AUS? ZUERST DER, DEN NIEMAND SIEHT.
Gemessen: In JEDEM der acht zu engen Paare ist mindestens einer
der beiden Toene gar nicht in Gebrauch -- 33 der 45 tragen eine
Kachel, zwoelf stehen nur bereit. Wer den unbenutzten verschiebt,
loest dasselbe Problem, ohne dass sich fuer irgendjemanden auch
nur eine Kachel veraendert.
Das ist mehr als Bequemlichkeit: Eine Kachel, die ihre Farbe
wechselt, muss neu gelernt werden. Wer zwei Jahre lang „das
Tuerkise" angesteuert hat, sucht danach. Diese Kosten entstehen
hier schlicht nicht.
Erst wenn beide benutzt sind (oder beide nicht), entscheidet der
kuerzere Weg. */
const frei = (t, k) => k && !BENUTZT.has(t.n);
const nimm = frei(a, ka) && !frei(b, kb) ? { t: a, k: ka }
: frei(b, kb) && !frei(a, ka) ? { t: b, k: kb }
: !ka ? { t: b, k: kb } : !kb ? { t: a, k: ka }
: (ka.weg <= kb.weg ? { t: a, k: ka } : { t: b, k: kb });
console.log(` ${p.d.toFixed(4)} (noetig ${p.noetig.toFixed(3)}) Ton ${a.n} ${a.hex} <-> `
+ `Ton ${b.n} ${b.hex} -> Ton ${nimm.t.n} wandert nach ${nimm.k.hex} `
+ `(${nimm.k.weg.toFixed(3)})`);
nimm.t.hex = nimm.k.hex;
bewegt++;
}
/* ---------- NACHZIEHEN -----------------------------------------------
Der Durchgang oben arbeitet sich vom engsten Paar nach vorn und
entscheidet dabei immer nur mit dem, was GERADE dasteht. Wer
frueh ausweichen muss, weicht deshalb manchmal weiter aus als
noetig -- die Stelle, die ihm gepasst haette, wird erst zwei
Schritte spaeter frei.
Gemessen war das kein Schoenheitsfehler: Ton 10 (#ff6877, ein
Korallrot) landete beim ersten Lauf auf #a86cb4, einem Violett --
0,178 entfernt, und niemand haette die Kachel wiedererkannt.
Also am Ende noch einmal von vorn: Jeder bewegte Ton darf so weit
zu seiner alten Farbe zurueck, wie die Bedingungen es zulassen.
Das aendert am Mindestabstand nichts -- es wird nur geprueft, ob
es NAEHER geht. */
for (let runde = 0; runde < 12; runde++) {
let geruckt = false;
for (const t of TOENE) {
if (FEST.has(t.n) || t.hex === t.alt) continue;
const jetzt = abst(t.alt, t.hex);
const noetig = abstandFuer(t.n);
let best = null, bestWeg = jetzt;
for (const h of kandidatenFuer(t.n)) {
const weg = abst(t.alt, h);
if (weg >= bestWeg) continue;
let ok = true;
for (const u of TOENE) {
if (u === t) continue;
if (abst(h, u.hex) < Math.max(noetig, abstandFuer(u.n))) { ok = false; break; }
}
if (ok) { best = h; bestWeg = weg; }
}
if (best) {
console.log(` nachgezogen: Ton ${t.n} ${t.hex} -> ${best} `
+ `(${jetzt.toFixed(3)} -> ${bestWeg.toFixed(3)} vom alten Ton)`);
t.hex = best; geruckt = true;
}
}
if (!geruckt) break;
}
/* ---------- Nachmessen ------------------------------------------------ */
let klein = Infinity, wo = null, unterSoll = 0;
for (let i = 0; i < TOENE.length; i++) {
for (let j = i + 1; j < TOENE.length; j++) {
const d = abst(TOENE[i].hex, TOENE[j].hex);
const noetig = Math.max(abstandFuer(TOENE[i].n), abstandFuer(TOENE[j].n));
if (d < noetig) unterSoll++;
if (d < klein) { klein = d; wo = [TOENE[i], TOENE[j]]; }
}
}
const geaendert = TOENE.filter((t) => t.hex !== t.alt);
const zuBlass = TOENE.filter((t) => BENUTZT.has(t.n)
&& bunt(t.hex) < MINDEST_BUNTHEIT && !BLASS_ERLAUBT.has(t.n));
const zuDunkel = TOENE.filter((t) => kontrast(t.hex) < MINDEST_KONTRAST);
const doppelt = TOENE.length - new Set(TOENE.map((t) => t.hex)).size;
console.log("");
console.log(`geaendert ${geaendert.length} von ${TOENE.length}`);
for (const t of geaendert) {
console.log(` Ton ${String(t.n).padStart(2)} ${t.alt} -> ${t.hex} `
+ `verschoben ${abst(t.alt, t.hex).toFixed(3)} Buntheit ${bunt(t.hex).toFixed(3)} `
+ `Kontrast ${kontrast(t.hex).toFixed(2)}:1`);
}
console.log(`kleinster Abstand ${klein.toFixed(4)} (Ton ${wo[0].n} ${wo[0].hex} / Ton ${wo[1].n} ${wo[1].hex})`);
console.log(`unter dem Soll ${unterSoll}`);
console.log(`zu blass ${zuBlass.map((t) => `Ton ${t.n} ${bunt(t.hex).toFixed(3)}`).join(", ") || "keiner"}`);
console.log(`zu dunkel ${zuDunkel.map((t) => `Ton ${t.n}`).join(", ") || "keiner"}`);
console.log(`doppelt ${doppelt}`);
for (const [n, h] of FEST) {
const t = TOENE.find((x) => x.n === n);
console.log(`festgelegt Ton ${n} ${t.hex} ${t.hex === h ? "(unveraendert)" : "*** ABWEICHUNG ***"}`);
}
if (process.argv[2] === "schreiben") {
if (unterSoll || zuBlass.length || zuDunkel.length || doppelt
|| [...FEST].some(([n, h]) => TOENE.find((x) => x.n === n).hex !== h)) {
console.error("\nNICHT geschrieben -- das Ergebnis haelt die Bedingungen nicht.");
process.exit(1);
}
let neu = css;
for (const t of TOENE) {
const muster = new RegExp(`(\\[data-ton="${t.n}"\\]\\s*\\{\\s*--ton:\\s*)#[0-9a-fA-F]{6}`);
if (!muster.test(neu)) { console.error(`Ton ${t.n} nicht wiedergefunden`); process.exit(2); }
neu = neu.replace(muster, `$1${t.hex}`);
}
writeFileSync(DATEI, neu);
console.log("\nstart.css geschrieben.");
}