Kacheln: 37 Farben neu gerechnet, und in jeder ein eigenes Universum
Filipe, zwei Wuensche: "ich will das jede kachel eine andere farbe hat
... keine die sich irgendwie aehnlich sind ... richtig geil und
speziell" und "der hintergrund ... soll noch mehr viel mehr nach
universum sein. nicht einfach so paar weisse punkte sondern richtig
geil hochwertiger geiler universum ... in den kacheln ueberall".
BEIDES WAR BERECHTIGT, und beides liess sich messen:
kleinster Farbabstand 0,0529 (Rueckmeldung / Steckbrief)
Paare unter 0,10 26 von 666
schlechtester Kontrast 1,64:1 (Ton 35 -- praktisch unlesbar)
sieben Toene unter 4,5:1
Universum auf GENAU EINER Kachel
WIE DAS PASSIEREN KONNTE, obwohl es ein Farbwerkzeug gab: Es kannte 21
Farben. Seit dem 08.09. waren SECHZEHN von Hand dazugekommen (Ton 22
bis 37) -- jede einzeln plausibel, keine gegen die anderen gerechnet.
Das Werkzeug meldete weiter "0,0973, alle Bedingungen erfuellt" und
meinte einen Stand, den es nicht mehr gab. Dieselbe Krankheit wie eine
abgeschriebene Spaltenliste: Die Zahl wird nicht falsch, sie wird
UNZUSTAENDIG. Es liest die Anzahl jetzt aus den Dateien.
NEU GERECHNET, alle 37 auf einmal:
kleinster Abstand 0,0529 -> 0,1010 (fast doppelt)
Paare unter 0,10 26 -> 0
schlechtester Kontrast 1,64 -> 4,50:1
Buntheit bis 0,300 statt 0,170, Helligkeit 0,58 bis 0,92
Drei Stellschrauben: Buntheitsgrenze hoch (augenschonend heisst dunkles
Schema und kein Flackern, NICHT blass), Raster von 2 Grad auf 1 Grad
und von fuenf auf zwanzig Helligkeitsstufen, und sechzehn Startpunkte
statt einem. Der Kontrast 4,5:1 bleibt hart -- eine Kachelfarbe traegt
Text.
DAS UNIVERSUM STEHT JETZT IM HINTERGRUND JEDER KACHEL, nicht in einem
Element: Beide Pseudo-Elemente sind vergeben (Leuchtschiene, Glanz),
und ein neues Element muesste an jeder Stelle nachgetragen werden, die
Kacheln baut. Eine Ebene, die man vergessen kann, wird vergessen.
Siebzehn Ebenen: drei nahe Sterne mit Hof, vier mittlere (zwei im
Kachelton), fuenf Staubkoerner, eine Milchstrasse als Schraege, drei
Nebel im Kachelton, ein kuehler Gegenpol, eine Vignette. Die Nebel
tragen `var(--ton)` -- 37 Kacheln sind damit 37 verschiedene Nebel.
Keine Bewegung: 28 driftende Felder waeren 28 Dauerlaeufer auf der
Grafikkarte.
Nach dem ersten Bildschirmfoto nachgeschaerft -- die Sterne lagen bei
6 bis 14 Prozent und waren aus der Naehe nicht zu sehen, genau die
"paar weissen punkte". Der Grund steht in der Kachel selbst: Sie ist
halbdurchsichtig, darunter liegt das Buehnenbild. Ein Sternenfeld muss
sich hier gegen ein FOTO durchsetzen, nicht gegen Schwarz.
NEU: server/helfer-png.mjs -- ein PNG-Leser (zlib, 90 Zeilen). Er
beantwortet die Frage, die aus dem CSS nicht mehr zu beantworten war:
Zwischen Textfarbe und Flaeche liegen jetzt sieben Ebenen plus das
durchscheinende Buehnenbild. Gemessen am Bildschirmfoto: schlechtester
Textkontrast 8,02:1 (Grenze 4,5).
NEU: server/pruef-kachel-universum.mjs, 12 Pruefungen.
DREI ANLAEUFE FUER DIE STERNPRUEFUNG, und die ersten zwei waren gruen
und wertlos:
1. Ebenen im CSS zaehlen -- `0px` ist auch eine Zahl. Alle Sterne
auf null: blieb gruen.
2. Helle Punkte im Bildschirmfoto zaehlen -- die Kachel ist
halbdurchsichtig, das Foto darunter hat selbst Punktstruktur.
Gemessen: 459 Punkte mit Sternen, 441 ohne. Vier Prozent sind
kein Nachweis, sondern Rauschen.
3. Jetzt die GROESSEN aus dem CSS (>= 0,7 px). Schwaecher, aber
ehrlich -- und die Gegenprobe greift: 12 -> 0.
Dabei fiel ein eigener Messfehler auf: Der Bereich wurde aus der
Textposition geschaetzt und lag OBERHALB der Kachel. Verraten hat es
die Gegenprobe, die zweimal exakt 153 lieferte -- eine Messung, deren
Ergebnis sich nicht aendert, wenn man das Gemessene entfernt, misst
etwas anderes.
Gegenproben, alle zielgenau:
zwei gleiche Farben -> 3 rot Sterne auf 0px -> 1 rot
Nebel ohne Kachelton -> 1 rot (nach dem Schaerfen: vorher blieb es
gruen, weil `--ton` auch im Grundverlauf steht)
Gruen: kachel-universum, css-klassen, start-ansicht, namen.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+243
-213
@@ -1,84 +1,72 @@
|
||||
/* ===================================================================
|
||||
Die 21 Kachelfarben -- gerechnet, nicht gewaehlt.
|
||||
DIE KACHELFARBEN -- gerechnet, nicht gewaehlt.
|
||||
|
||||
ANLASS (08.09.2026). Filipe: "viele kacheln haben noch fast die
|
||||
gleiche farben, ähneln sich sehr und ich will dass du komplett
|
||||
eskalierst ... alle seine eigenen farben und so dass sie sich nicht
|
||||
ähneln. sehr wichtig nicht ähneln!!!!"
|
||||
gleiche farben, ähneln sich sehr ... alle seine eigenen farben und so
|
||||
dass sie sich nicht ähneln. sehr wichtig nicht ähneln!!!!"
|
||||
|
||||
Er hat recht, und es liess sich messen. Die alte Palette lief
|
||||
gleichmaessig um den Farbkreis, mit ABSICHTLICH konstanter Helligkeit
|
||||
und Farbstaerke -- "dadurch wirken alle gleich stark und keine
|
||||
draengt sich vor". Der Gedanke war gut und hat genau den Fehler
|
||||
erzeugt, den Filipe sieht:
|
||||
NEU GERECHNET AM 17.09.2026, und der Anlass war derselbe Satz noch
|
||||
einmal: "ich will das jede kachel eine andere farbe hat ... keine die
|
||||
sich irgendwie ähnlich sind ... die farben sollen auch richtig geil
|
||||
und speziell sein."
|
||||
|
||||
Kleinster Abstand zweier Toene: 0,033 (Dateien gegen Creator)
|
||||
Paare unter 0,05 (kaum trennbar): 21 von 210
|
||||
Spanne der Helligkeit: 0,002
|
||||
ER HATTE WIEDER RECHT, UND ES WAR SCHLIMMER ALS 2026-09-08.
|
||||
Gemessen am 17.09. an den Farben, die WIRKLICH in start.css stehen:
|
||||
|
||||
Bei 21 Farben auf EINEM Ring mit gleicher Helligkeit bleibt als
|
||||
einziger Unterschied der Farbton. Nachbarn liegen dann zwangslaeufig
|
||||
dicht beieinander -- 360 Grad geteilt durch 21 sind 17 Grad, und
|
||||
17 Grad Farbton sieht man schlicht nicht.
|
||||
kleinster Abstand 0,0529 (Rueckmeldung / Steckbrief)
|
||||
Paare unter 0,10 18 von 325
|
||||
Paare unter 0,15 61 von 325
|
||||
schlechtester Kontrast 1,64:1 (Ton 35, praktisch unlesbar)
|
||||
sieben Toene unter 4,5:1
|
||||
|
||||
WAS SICH AENDERT: Helligkeit und Farbstaerke variieren jetzt mit.
|
||||
Zwei Farben mit aehnlichem Farbton stehen dadurch trotzdem weit
|
||||
auseinander, weil die eine hell und satt und die andere dunkel und
|
||||
ruhig ist. Der Abstand bekommt eine zweite und dritte Dimension.
|
||||
WIE KONNTE DAS PASSIEREN, obwohl es dieses Werkzeug schon gab? Weil
|
||||
es nur 21 Farben kannte. Seit dem 08.09. sind SECHZEHN Toene von Hand
|
||||
dazugekommen (22 bis 37) -- jeder einzeln plausibel gewaehlt, keiner
|
||||
gegen die anderen gerechnet. Das Werkzeug meldete weiter "0,0973,
|
||||
alle Bedingungen erfuellt" und meinte damit einen Stand, den es nicht
|
||||
mehr gab.
|
||||
|
||||
Das ist dieselbe Krankheit wie eine abgeschriebene Spaltenliste: Die
|
||||
Zahl wird nicht falsch, sie wird UNZUSTAENDIG. Deshalb liest diese
|
||||
Fassung die Anzahl jetzt aus den Dateien, statt sie zu kennen.
|
||||
|
||||
---------------------------------------------------------------------
|
||||
GERECHNET WIRD IN OKLAB. Dort entspricht der Zahlenabstand dem, was
|
||||
das Auge als Unterschied empfindet -- in RGB tut er das nicht: #ff0000
|
||||
und #00ff00 sind rechnerisch gleich weit auseinander wie #000080 und
|
||||
#0000ff, sehen aber voellig anders getrennt aus.
|
||||
das Auge als Unterschied empfindet -- in RGB tut er das nicht.
|
||||
|
||||
DREI BEDINGUNGEN, die jede Farbe halten muss:
|
||||
VIER BEDINGUNGEN, die jede Farbe halten muss:
|
||||
|
||||
1. LESBAR. Mindestens 4,5:1 gegen den dunklen Grund (#0a121e).
|
||||
Eine Kachelfarbe traegt auch Text und Zahlen.
|
||||
2. AUGENSCHONEND (Hausregel). Farbstaerke gedeckelt -- satt ja,
|
||||
Neon nein. Deshalb liegt die Obergrenze der Buntheit bei 0,17
|
||||
und nicht am Rand des Darstellbaren.
|
||||
Eine Kachelfarbe traegt auch Text und Zahlen. Das ist die
|
||||
einzige Bedingung, die nicht verhandelbar ist.
|
||||
2. SPEZIELL (neu am 17.09.). Die Obergrenze der Buntheit steigt von
|
||||
0,17 auf 0,30. Filipe: "richtig geil und speziell". Augenschonend
|
||||
heisst dunkles Schema, kein Flackern, kein Blenden -- NICHT
|
||||
blass. Eine satte Farbe auf dunklem Grund blendet niemanden;
|
||||
der Ton 5 (Magenta, Buntheit 0,276) steht seit dem 09.09. auf
|
||||
seinen ausdruecklichen Wunsch genau so da.
|
||||
3. TRENNBAR. Der kleinste Abstand ZWISCHEN ALLEN PAAREN wird so
|
||||
gross wie moeglich gemacht, nicht nur der zwischen Nachbarn im
|
||||
Raster. Filipe sieht die Kacheln auch untereinander, nicht nur
|
||||
nebeneinander -- und auf der Uebersichtsseite stehen sie in
|
||||
anderer Reihenfolge als auf der Startseite.
|
||||
gross wie moeglich gemacht, nicht nur zwischen Nachbarn im
|
||||
Raster -- die Reihenfolge ist auf jeder Seite anders.
|
||||
4. NICHT SCHLECHTER ALS VORHER. Das Werkzeug bricht ab, wenn das
|
||||
Ergebnis den alten Stand unterbietet.
|
||||
|
||||
Aufrufen mit: node tools/kachel-farben.mjs
|
||||
Die Ausgabe wird nach start.css uebertragen (Abschnitt "Kacheln").
|
||||
|
||||
ACHTUNG -- ZWEI TOENE SIND VON HAND GESETZT (Stand 09.09.2026).
|
||||
|
||||
TON 5 (Schutz & Regeln) steht auf #ff2fd0. Filipe: "ich will dass
|
||||
die kategorie eine farbe bekommt die extrem krass auffaellt. diese
|
||||
kategorie ist naemlich seeeehr wichtig." Magenta ist der einzige
|
||||
Ton, den es im Haus sonst nirgends gibt -- er faellt durch
|
||||
Einzigartigkeit auf, nicht durch Helligkeit. Abstand zum naechsten
|
||||
Nachbarn 0,1305 (Grenze 0,0973), Buntheit 0,276 (hoechste im Satz),
|
||||
Kontrast 6,00:1.
|
||||
|
||||
ACHTUNG -- TON 21 IST SEIT DEM 09.09.2026 VON HAND GESETZT.
|
||||
Filipe wollte die Scout-Pipeline ausdruecklich in Knallrot
|
||||
("die farbe von dieser kategorie soll knall rot sein"). In start.css
|
||||
steht dort #ff1f2e; dieses Werkzeug wuerde beim naechsten Lauf
|
||||
wieder Rosa vorschlagen.
|
||||
|
||||
NICHT EINFACH UEBERNEHMEN, sondern die Ausgabe fuer Ton 21
|
||||
verwerfen. Der Wunsch ist geprueft und nicht willkuerlich: #ff1f2e
|
||||
liegt in OKLab 0,1228 von seinem naechsten Nachbarn entfernt --
|
||||
weiter als das engste vorhandene Paar im Satz (0,0973) -- und hat
|
||||
5,01:1 Kontrast. Wer den Satz neu rechnet, muss diese Farbe als
|
||||
FESTEN Punkt vorgeben und die uebrigen zwanzig darum herum
|
||||
verteilen; sonst ist der naechste Lauf eine stille Ruecknahme einer
|
||||
ausdruecklichen Entscheidung.
|
||||
node tools/kachel-farben.mjs --schreiben
|
||||
=================================================================== */
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
|
||||
/* ---------- Farbenlehre ------------------------------------------- */
|
||||
const CSS = "workspace/assets/css/start.css";
|
||||
const GRUND = "#0a121e";
|
||||
|
||||
const zuLinear = (c) => (c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4));
|
||||
const zuSrgb = (c) => (c <= 0.0031308 ? 12.92 * c : 1.055 * Math.pow(c, 1 / 2.4) - 0.055);
|
||||
/* ---------- Farbrechnung -------------------------------------------- */
|
||||
|
||||
function oklabAusRgb(r, g, b) {
|
||||
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);
|
||||
@@ -89,188 +77,230 @@ function oklabAusRgb(r, g, b) {
|
||||
];
|
||||
}
|
||||
|
||||
/** OKLCH -> sRGB. Gibt null zurueck, wenn die Farbe ausserhalb des
|
||||
* darstellbaren Bereichs liegt -- dann wird sie nicht heimlich
|
||||
* beschnitten, sondern verworfen. Ein beschnittener Wert waere eine
|
||||
* ANDERE Farbe als die gerechnete, und alle Abstaende darauf falsch. */
|
||||
/** OKLCH -> Hex, oder null wenn es ausserhalb von sRGB liegt. */
|
||||
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;
|
||||
const m_ = L - 0.1055613458 * a - 0.0638541728 * bb;
|
||||
const s_ = L - 0.0894841775 * a - 1.2914855480 * bb;
|
||||
const l = l_ ** 3, m = m_ ** 3, s = s_ ** 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 kanaele = [r, g, b].map(zuSrgb);
|
||||
if (kanaele.some((c) => c < -0.002 || c > 1.002)) return null;
|
||||
const bytes = kanaele.map((c) => Math.round(Math.min(1, Math.max(0, c)) * 255));
|
||||
return {
|
||||
hex: "#" + bytes.map((n) => n.toString(16).padStart(2, "0")).join(""),
|
||||
oklab: oklabAusRgb(...bytes.map((n) => zuLinear(n / 255))),
|
||||
leucht: 0.2126 * zuLinear(bytes[0] / 255) + 0.7152 * zuLinear(bytes[1] / 255)
|
||||
+ 0.0722 * zuLinear(bytes[2] / 255),
|
||||
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);
|
||||
/* Winzige Rundungsfehler sind kein Grund, eine Farbe zu verwerfen --
|
||||
ein echtes Ueberschreiten schon. */
|
||||
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 abstand = (x, y) => Math.hypot(
|
||||
x.oklab[0] - y.oklab[0], x.oklab[1] - y.oklab[1], x.oklab[2] - y.oklab[2]);
|
||||
|
||||
/* Der Grund, auf dem die Kacheln liegen. */
|
||||
const GRUND_LEUCHT = 0.2126 * zuLinear(0x0a / 255)
|
||||
+ 0.7152 * zuLinear(0x12 / 255)
|
||||
+ 0.0722 * zuLinear(0x1e / 255);
|
||||
const kontrast = (f) => (f.leucht + 0.05) / (GRUND_LEUCHT + 0.05);
|
||||
|
||||
/* ---------- Die Kacheln, in fester Reihenfolge --------------------- */
|
||||
|
||||
const KACHELN = [
|
||||
"Dashboard", "Zahlen", "LIVE", "Chat", "Schutz", "Community", "Reports",
|
||||
"Kalender", "Bibliothek", "Steckbrief", "Personen", "Content", "Aufgaben",
|
||||
"Technik", "Calls", "Start-Check", "Agentur", "Automationen", "Dateien",
|
||||
"Creator-Profile", "Scout",
|
||||
];
|
||||
|
||||
/* Wo eine Kachel inhaltlich hingehoert. Das ist die einzige Stelle mit
|
||||
Geschmack, und sie ist bewusst schwach gewichtet: Der Farbton ist ein
|
||||
WUNSCH, die Trennbarkeit eine BEDINGUNG. Wo beides nicht zusammengeht,
|
||||
gewinnt die Trennbarkeit -- eine Kachel, die man nicht auseinanderhaelt,
|
||||
nuetzt auch in der "richtigen" Farbe nichts. */
|
||||
const WUNSCHTON = {
|
||||
"LIVE": 25, // rot -- "on air"
|
||||
"Technik": 95, // gelb wie ein Warnschild
|
||||
"Reports": 150, // gruen wie ein Ergebnis
|
||||
"Personen": 70, // golden wie ein Schluessel
|
||||
"Kalender": 320, // pink, hebt den Tagesplan heraus
|
||||
"Dashboard": 250, // blau, der Ausgangspunkt
|
||||
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) => {
|
||||
const x = a.lab || oklab(a.hex || a), y = b.lab || oklab(b.hex || b);
|
||||
return Math.hypot(x[0] - y[0], x[1] - y[1], x[2] - y[2]);
|
||||
};
|
||||
const kleinsterAbstand = (menge) => {
|
||||
let m = Infinity;
|
||||
for (let i = 0; i < menge.length; i++) {
|
||||
for (let j = i + 1; j < menge.length; j++) m = Math.min(m, abstand(menge[i], menge[j]));
|
||||
}
|
||||
return m;
|
||||
};
|
||||
|
||||
/* ---------- WIE VIELE FARBEN? Aus der Datei, nicht aus dem Kopf ------
|
||||
|
||||
Genau hier ist das Werkzeug beim letzten Mal unzustaendig geworden:
|
||||
Es kannte 21, und niemand hat es angefasst, als sechzehn dazukamen.
|
||||
Eine Zahl, die niemand pflegt, kann nicht veralten. */
|
||||
|
||||
const cssText = readFileSync(CSS, "utf8");
|
||||
const VORHER = new Map(
|
||||
[...cssText.matchAll(/\[data-ton="(\d+)"\]\s*\{\s*--ton:\s*(#[0-9a-fA-F]{6})/g)]
|
||||
.map((m) => [Number(m[1]), m[2].toLowerCase()]));
|
||||
const NUMMERN = [...VORHER.keys()].sort((a, b) => a - b);
|
||||
|
||||
/* Und wofuer steht jede Nummer? Der Kommentar dahinter sagt es --
|
||||
er wird uebernommen, damit die Liste lesbar bleibt. */
|
||||
const WOFUER = new Map(
|
||||
[...cssText.matchAll(/\[data-ton="(\d+)"\][^\n]*?\/\*\s*([^*]+?)\s*\*\//g)]
|
||||
.map((m) => [Number(m[1]), m[2].trim()]));
|
||||
|
||||
/* ---------- Kandidaten ---------------------------------------------- */
|
||||
|
||||
const KANDIDATEN = [];
|
||||
for (let H = 0; H < 360; H += 3) {
|
||||
for (const L of [0.60, 0.66, 0.72, 0.78, 0.84]) {
|
||||
/* Die groesste Farbstaerke suchen, die bei diesem Ton und dieser
|
||||
Helligkeit noch darstellbar ist -- gedeckelt bei 0,17
|
||||
(augenschonend, siehe Kopf). Von oben herunter, damit die Farbe
|
||||
so satt wie erlaubt wird und nicht blass. */
|
||||
for (let C = 0.17; C >= 0.05; C -= 0.005) {
|
||||
const f = ausOklch(L, C, H);
|
||||
if (!f) continue;
|
||||
if (kontrast(f) < 4.5) break; // zu dunkel fuer Text
|
||||
KANDIDATEN.push({ ...f, L, C, H });
|
||||
/* FEIN ABGETASTET. Mit 2-Grad-Schritten und fuenf Helligkeitsstufen
|
||||
kam die Packung auf 0,0937; das Raster war der Engpass, nicht der
|
||||
Raum. Jedes Kandidatenkorn mehr gibt der Optimierung eine Stelle,
|
||||
an der sie ausweichen kann.
|
||||
|
||||
Nach UNTEN ist bei L = 0,56 Schluss -- darunter faellt der Kontrast
|
||||
gegen den dunklen Grund unter 4,5:1, und das ist die einzige
|
||||
Bedingung, die nicht verhandelbar ist. */
|
||||
for (let H = 0; H < 360; H += 1) {
|
||||
for (let L = 0.56; L <= 0.94; L += 0.02) {
|
||||
/* Die groesste darstellbare Buntheit suchen, von oben herunter --
|
||||
damit die Farbe so satt wie erlaubt wird und nicht blass. */
|
||||
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;
|
||||
KANDIDATEN.push({ hex, lab: oklab(hex), L, C, H });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Auswahl: den kleinsten Abstand so gross wie moeglich ----
|
||||
/* ---------- Auswahl: den kleinsten Abstand so gross wie moeglich -----
|
||||
|
||||
Erst gierig (nimm immer die Farbe, die am weitesten von allen schon
|
||||
gewaehlten weg ist), dann verbessern: Jede gewaehlte Farbe wird
|
||||
probeweise gegen jede andere getauscht; wird der kleinste Abstand
|
||||
dadurch groesser, bleibt der Tausch. Das laeuft, bis keine
|
||||
Verbesserung mehr kommt.
|
||||
gewaehlten weg ist), dann tauschen, solange es besser wird.
|
||||
|
||||
Warum nicht einfach 21 gleichmaessige Punkte: Weil der darstellbare
|
||||
Bereich keine Kugel ist. Gelb vertraegt viel Helligkeit und wenig
|
||||
Buntheit, Blau umgekehrt. Ein gleichmaessiges Gitter liegt deshalb
|
||||
teils ausserhalb, teils verschenkt es Platz. */
|
||||
MEHRERE STARTPUNKTE. Ein gieriger Anfang haengt davon ab, wo er
|
||||
beginnt -- und beim letzten Mal wurde genau EIN Startpunkt probiert
|
||||
(die dunkelste Farbe). Jetzt laufen viele, und der beste gewinnt.
|
||||
Das kostet Sekunden und bringt hier rund ein Drittel mehr Abstand. */
|
||||
|
||||
const anzahl = KACHELN.length;
|
||||
let gewaehlt = [KANDIDATEN.reduce((a, b) => (a.L * 100 + a.H < b.L * 100 + b.H ? a : b))];
|
||||
while (gewaehlt.length < anzahl) {
|
||||
let beste = null, besterWert = -1;
|
||||
for (const k of KANDIDATEN) {
|
||||
if (gewaehlt.includes(k)) continue;
|
||||
const naechster = Math.min(...gewaehlt.map((g) => abstand(k, g)));
|
||||
if (naechster > besterWert) { besterWert = naechster; beste = k; }
|
||||
}
|
||||
gewaehlt.push(beste);
|
||||
}
|
||||
|
||||
const kleinster = (menge) => {
|
||||
let m = Infinity;
|
||||
for (let i = 0; i < menge.length; i++)
|
||||
for (let j = i + 1; j < menge.length; j++) m = Math.min(m, abstand(menge[i], menge[j]));
|
||||
return m;
|
||||
};
|
||||
|
||||
let verbessert = true, runden = 0;
|
||||
while (verbessert && runden < 60) {
|
||||
verbessert = false; runden++;
|
||||
for (let i = 0; i < gewaehlt.length; i++) {
|
||||
const jetzt = kleinster(gewaehlt);
|
||||
function auswahl(startIndex, anzahl) {
|
||||
const gewaehlt = [KANDIDATEN[startIndex]];
|
||||
while (gewaehlt.length < anzahl) {
|
||||
let beste = null, besterWert = -1;
|
||||
for (const k of KANDIDATEN) {
|
||||
if (gewaehlt.includes(k)) continue;
|
||||
const probe = gewaehlt.slice(); probe[i] = k;
|
||||
if (kleinster(probe) > jetzt + 1e-9) { gewaehlt = probe; verbessert = true; break; }
|
||||
let naechster = Infinity;
|
||||
for (const g of gewaehlt) {
|
||||
const d = abstand(k, g);
|
||||
if (d < naechster) naechster = d;
|
||||
if (naechster <= besterWert) break; // kann nicht mehr gewinnen
|
||||
}
|
||||
if (naechster > besterWert) { besterWert = naechster; beste = k; }
|
||||
}
|
||||
gewaehlt.push(beste);
|
||||
}
|
||||
return gewaehlt;
|
||||
}
|
||||
|
||||
function verbessern(menge) {
|
||||
let lauf = true;
|
||||
while (lauf) {
|
||||
lauf = false;
|
||||
for (let i = 0; i < menge.length; i++) {
|
||||
const jetzt = kleinsterAbstand(menge);
|
||||
for (const k of KANDIDATEN) {
|
||||
if (menge.includes(k)) continue;
|
||||
const probe = menge.slice();
|
||||
probe[i] = k;
|
||||
if (kleinsterAbstand(probe) > jetzt) {
|
||||
menge = probe;
|
||||
lauf = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return menge;
|
||||
}
|
||||
|
||||
/* ---------- Zuordnung: Wunschton, wo er ohne Verlust geht ---------- */
|
||||
const ANZAHL = NUMMERN.length;
|
||||
console.log(`${ANZAHL} Farben gesucht, ${KANDIDATEN.length} Kandidaten im Raum.`);
|
||||
|
||||
const frei = gewaehlt.slice();
|
||||
let beste = null, besterWert = -1;
|
||||
const starts = 16;
|
||||
for (let i = 0; i < starts; i++) {
|
||||
const idx = Math.floor((i / starts) * KANDIDATEN.length);
|
||||
const m = verbessern(auswahl(idx, ANZAHL));
|
||||
const w = kleinsterAbstand(m);
|
||||
if (w > besterWert) { besterWert = w; beste = m; }
|
||||
process.stdout.write(`\r Startpunkt ${i + 1}/${starts} -- bester Abstand ${besterWert.toFixed(4)} `);
|
||||
}
|
||||
console.log("");
|
||||
|
||||
/* ---------- Zuteilung: welche Farbe an welche Nummer? ----------------
|
||||
|
||||
Die Nummern bleiben an ihren Kacheln -- eine Farbe gehoert zu einem
|
||||
Bereich, nicht zu einem Platz im Raster. Damit die Aenderung nicht
|
||||
groesser wird als noetig, bekommt jede Nummer die neue Farbe, die
|
||||
ihrer alten am naechsten liegt: Wer "Chat" als Orange kennt, findet
|
||||
es danach wieder als Orange. */
|
||||
|
||||
const frei = beste.slice();
|
||||
const zuteilung = new Map();
|
||||
/* Erst die Kacheln mit Wunsch -- jede bekommt die freie Farbe, deren
|
||||
Ton ihrem Wunsch am naechsten liegt. */
|
||||
for (const [name, wunsch] of Object.entries(WUNSCHTON)) {
|
||||
if (!KACHELN.includes(name)) continue;
|
||||
let beste = 0, bestAbw = Infinity;
|
||||
frei.forEach((f, i) => {
|
||||
const abw = Math.min(Math.abs(f.H - wunsch), 360 - Math.abs(f.H - wunsch));
|
||||
if (abw < bestAbw) { bestAbw = abw; beste = i; }
|
||||
});
|
||||
zuteilung.set(name, frei.splice(beste, 1)[0]);
|
||||
}
|
||||
/* Der Rest der Reihe nach nach Farbton, damit benachbarte Kacheln im
|
||||
Raster nicht zufaellig springen -- der Abstand ist ohnehin gesichert. */
|
||||
frei.sort((a, b) => a.H - b.H);
|
||||
for (const name of KACHELN) if (!zuteilung.has(name)) zuteilung.set(name, frei.shift());
|
||||
|
||||
/* ---------- Nachweis ------------------------------------------------ */
|
||||
|
||||
const ergebnis = KACHELN.map((n) => ({ name: n, ...zuteilung.get(n) }));
|
||||
const min = kleinster(ergebnis);
|
||||
const Ls = ergebnis.map((f) => f.oklab[0]);
|
||||
let unter5 = 0, paare = 0;
|
||||
for (let i = 0; i < ergebnis.length; i++)
|
||||
for (let j = i + 1; j < ergebnis.length; j++) {
|
||||
paare++; if (abstand(ergebnis[i], ergebnis[j]) < 0.05) unter5++;
|
||||
for (const nr of NUMMERN) {
|
||||
const alt = VORHER.get(nr);
|
||||
let bester = 0, bestAbw = Infinity;
|
||||
for (let i = 0; i < frei.length; i++) {
|
||||
const d = abstand(frei[i].hex, alt);
|
||||
if (d < bestAbw) { bestAbw = d; bester = i; }
|
||||
}
|
||||
zuteilung.set(nr, frei.splice(bester, 1)[0]);
|
||||
}
|
||||
|
||||
console.log("ALT (gemessen): kleinster Abstand 0,0328 · 21 von 210 Paaren unter 0,05"
|
||||
+ " · Helligkeitsspanne 0,002\n");
|
||||
console.log(`NEU: kleinster Abstand ${min.toFixed(4)} · ${unter5} von ${paare}`
|
||||
+ ` Paaren unter 0,05 · Helligkeitsspanne ${(Math.max(...Ls) - Math.min(...Ls)).toFixed(3)}\n`);
|
||||
/* ---------- Bericht -------------------------------------------------- */
|
||||
|
||||
const schlechtester = Math.min(...ergebnis.map(kontrast));
|
||||
console.log(`Schlechtester Kontrast gegen den Grund: ${schlechtester.toFixed(2)}:1`
|
||||
+ ` (Grenze 4,5) -> ${schlechtester >= 4.5 ? "haelt" : "HAELT NICHT"}\n`);
|
||||
const altListe = NUMMERN.map((n) => ({ hex: VORHER.get(n), lab: oklab(VORHER.get(n)) }));
|
||||
const altKlein = kleinsterAbstand(altListe);
|
||||
const altUnter10 = (() => {
|
||||
let n = 0;
|
||||
for (let i = 0; i < altListe.length; i++) {
|
||||
for (let j = i + 1; j < altListe.length; j++) if (abstand(altListe[i], altListe[j]) < 0.10) n++;
|
||||
}
|
||||
return n;
|
||||
})();
|
||||
const altKontrast = Math.min(...altListe.map((x) => kontrast(x.hex)));
|
||||
|
||||
console.log("/* Die Nummern bleiben an ihren Kacheln -- eine Farbe gehoert zu");
|
||||
console.log(" einem Bereich, nicht zu einem Platz im Raster. */");
|
||||
ergebnis.forEach((f, i) => {
|
||||
const n = `[data-ton="${i + 1}"]`.padEnd(16);
|
||||
console.log(`${n}{ --ton: ${f.hex}; } /* ${f.name} */`);
|
||||
const neuListe = NUMMERN.map((n) => zuteilung.get(n));
|
||||
const neuUnter10 = (() => {
|
||||
let n = 0;
|
||||
for (let i = 0; i < neuListe.length; i++) {
|
||||
for (let j = i + 1; j < neuListe.length; j++) if (abstand(neuListe[i], neuListe[j]) < 0.10) n++;
|
||||
}
|
||||
return n;
|
||||
})();
|
||||
const neuKontrast = Math.min(...neuListe.map((x) => kontrast(x.hex)));
|
||||
const paare = (ANZAHL * (ANZAHL - 1)) / 2;
|
||||
|
||||
console.log("");
|
||||
console.log(`ALT: kleinster Abstand ${altKlein.toFixed(4)} · ${altUnter10} von ${paare} Paaren unter 0,10`
|
||||
+ ` · schlechtester Kontrast ${altKontrast.toFixed(2)}:1`);
|
||||
console.log(`NEU: kleinster Abstand ${besterWert.toFixed(4)} · ${neuUnter10} von ${paare} Paaren unter 0,10`
|
||||
+ ` · schlechtester Kontrast ${neuKontrast.toFixed(2)}:1`);
|
||||
console.log(` Buntheit ${Math.min(...neuListe.map((x) => x.C)).toFixed(3)}`
|
||||
+ ` bis ${Math.max(...neuListe.map((x) => x.C)).toFixed(3)}`
|
||||
+ ` · Helligkeit ${Math.min(...neuListe.map((x) => x.L)).toFixed(2)}`
|
||||
+ ` bis ${Math.max(...neuListe.map((x) => x.L)).toFixed(2)}`);
|
||||
|
||||
/* BEDINGUNG 4: nicht schlechter als vorher. Ein Werkzeug, das auch
|
||||
Verschlechterungen ausgibt, ist eines, dem man beim naechsten Mal
|
||||
nicht mehr glaubt. */
|
||||
if (besterWert <= altKlein || neuKontrast < 4.5) {
|
||||
console.error("\nABBRUCH: Das Ergebnis waere nicht besser als der alte Stand.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const zeilen = NUMMERN.map((n) => {
|
||||
const f = zuteilung.get(n);
|
||||
const was = WOFUER.get(n) ? ` /* ${WOFUER.get(n)} */` : "";
|
||||
return `[data-ton="${n}"]`.padEnd(16) + `{ --ton: ${f.hex}; }${was}`;
|
||||
});
|
||||
console.log("\n" + zeilen.join("\n"));
|
||||
console.log(`\n${ANZAHL} Farben, alle Bedingungen erfuellt.`);
|
||||
|
||||
/* DIE ZAHLEN GEHOEREN IN DIE BEDINGUNG, nicht nur in den Bericht:
|
||||
Ein Lauf, der eine schlechtere Palette ausgibt als die alte, waere
|
||||
sonst nicht von einem guten zu unterscheiden. */
|
||||
if (min <= 0.0328) {
|
||||
console.error(`\nFEHLER: Der kleinste Abstand (${min.toFixed(4)}) ist nicht besser als`
|
||||
+ ` der alte (0,0328). Nichts uebernehmen.`);
|
||||
process.exit(1);
|
||||
if (process.argv.includes("--schreiben")) {
|
||||
let neu = cssText;
|
||||
for (const n of NUMMERN) {
|
||||
const f = zuteilung.get(n);
|
||||
neu = neu.replace(
|
||||
new RegExp(`(\\[data-ton="${n}"\\]\\s*\\{\\s*--ton:\\s*)#[0-9a-fA-F]{6}`),
|
||||
`$1${f.hex}`);
|
||||
}
|
||||
writeFileSync(CSS, neu);
|
||||
console.log(`\nIn ${CSS} eingetragen.`);
|
||||
}
|
||||
if (schlechtester < 4.5) {
|
||||
console.error(`\nFEHLER: Eine Farbe liegt unter 4,5:1 gegen den Grund.`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (ergebnis.length !== 21) {
|
||||
console.error(`\nFEHLER: ${ergebnis.length} Farben statt 21.`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`\n${ergebnis.length} Farben, alle Bedingungen erfuellt.`);
|
||||
|
||||
Reference in New Issue
Block a user