/* ===================================================================== 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(" ")); }