--- screen3 + screen1: die Hintergruende draengeln nicht mehr --- "die hintergrunde sollen ueberall so sein dass die sich nicht in den vordergrund draengeln." / "mach den hintergrund von dieser kachel dunkler, so dass die die kacheln drin viel mehr auffallen." `--raster` stand auf .26 Deckkraft -- auf dunkler Flaeche kein Hauch mehr, sondern ein gezeichnetes Gitter. Im Tagdialog lief es sichtbar durch die Ueberschrift, in den Sammelkacheln stand es VOR den Karten darin. Jetzt .09: Man sieht eine Struktur, man zaehlt keine Linien. Eine Zahl fuer das ganze Haus -- sie steht einmal in module.css und wird an fuenf Stellen benutzt. Die Anlasskachel im Kalender lag mit rgba(19,26,38,.88) auf demselben Helligkeitsniveau wie die Karten darin: keine Vitrine, sondern eine dritte Flaeche gleicher Lautstaerke. Jetzt fast schwarz. An den Karten musste dafuer nichts geaendert werden -- der Abstand entsteht von selbst. --- screen2: ein Knopf wandert nach links --- "eins von diesen buttons soll links bei dem anderen kreis sein." Die GLOCKE geht nach links zum Ring, der WECKER bleibt rechts bei der Uhr. Das ist nicht ausgewuerfelt: Der Wecker ist eine Uhrzeit. Die Glocke entscheidet, ob man ueberhaupt etwas ueber sein Team erfaehrt -- und der Ring links zeigt genau das. Die Kachel ist damit spiegelsymmetrisch belegt: 48 px Knopf + 14 px Abstand + 248 px Instrument auf beiden Seiten. Genau das rechnet `--spalte`. Die Zentrierung des Rings ist weggefallen -- sie war noetig, solange er allein in einer fuer Instrument PLUS Knopf bemessenen Spalte stand. Der reservierte Platz schrumpft von 104 auf 48 px je Seite; die Begruendung fuer das Reservieren bleibt: Ein Platz, der erst mit der Antwort entsteht, laesst die Zeile springen. --- screen4: zwei Chilis und zwei Huskys dazu --- "setz in den hintergrund noch 1-2 peperonis und dan das logo von dogfather, also nur den husky." DER HUSKY IST EINE MASKE, KEIN BILD. Die Datei ist schwarz-weiss und freigestellt (nachgemessen: 49 % durchsichtig). Als Hintergrundbild bei 15 % verschwaenden die schwarzen Flaechen im dunklen Grund und uebrig blieben die hellen -- ein zerrissener Umriss, kein Hund. Als Maske ueber einer Farbflaeche wird daraus eine geschlossene Silhouette in DogFathers Babyblau. Im Universum schweben jetzt beide Marken in ihren beiden Farben: sieben Chilis rot, zwei Huskys blau. --- screen5: die Punkte sind ersetzt, das letzte Glied leuchtet --- "ich will dass du die punkte ersetzt und immer der letzte soll mehr strahlen oder so." / "alles ist mega ausser die stunden muss du noch perfektionnieren." DIE DREI UMLAUFENDEN PERLEN SIND WEG -- samt 60 Zeilen Rechnung. Sie waren ein zweites Ding an einer zweiten Stelle: eigene Uhr ab dem ersten Takt (weil die volle Unixzeit `rotate(1.07333e+10deg)` ergab), eigener Startwinkel je Bahn, drei Winkel, die die kleineren Einheiten anteilig mittragen mussten. All das war noetig, WEIL der Kopf neben der Reihe herlief statt Teil von ihr zu sein. Genau deshalb trugen sie am 08.09. noch die alten Farben, als die Bahnen getauscht wurden. Jetzt zeichnet eine zweite SVG-Lage genau EIN Glied heller -- dasselbe, das die Reihe darunter zuletzt gesetzt hat, aus denselben Zahlen. Sie kann gar nicht danebenstehen. Heller statt groesser: Waere der Kopf groesser, waere er ein Fremdkoerper in der Reihe. DIE STUNDEN WAREN BREITER ALS LANG -- 5,5 lang bei 7 breit, also 0,79:1. Ein Segment, das breiter ist als lang, liest sich als Klotz quer auf der Bahn statt als Balken entlang. Und weil die Kantenlage nur 0,4 versetzt ist, lief der 0,9 breite Lichtstrich MITTEN DURCH jeden Balken statt an seiner Kante. Jetzt 10 lang bei 5,8 breit (1,7:1), und der Kantenversatz ist nach Bandbreite gestaffelt: (Breite - 0,9) / 2, also 0,75 / 1,65 / 2,05 zusaetzlich zur Gruppe. --- screen6: die Scout-Pipeline wird knallrot --- "die farbe von dieser kategorie soll knall rot sein." Die anderen zwanzig Kachelfarben sind gerechnet (OKLab, groesstmoeglicher Abstand). Diese eine ist gewuenscht -- und wurde deshalb GEGEN den Satz geprueft statt eingetragen: Der engste vorhandene Abstand liegt bei 0,0973. #ff1f2e kommt seinem naechsten Nachbarn auf 0,1228 nahe, ist also weiter entfernt als das engste vorhandene Paar. Kontrast 5,01:1 (noetig 4,5). Von sechs geprueften Rottoenen der mit dem groessten Abstand UND genug Kontrast. tools/kachel-farben.mjs weiss jetzt davon: Ein kuenftiger Lauf wuerde wieder Rosa vorschlagen, und das waere eine stille Ruecknahme einer ausdruecklichen Entscheidung. --- Ein Messfehler, der festgehalten gehoert --- Beim Pruefen von screen4 meldete meine Foto-Methode vier Beschriftungen unter 4,5:1 -- bei einem Verlust von 0,00 bis 0,15 durch das Universum. Dass die Ursache nicht das Universum sein konnte, stand damit schon in den Zahlen. Exakt gerechnet (Vordergrundfarbe gegen die tatsaechliche Flaeche darunter) liegen dieselben Texte bei 9,13:1 bis 10,76:1. Die Foto-Methode mittelt ueber alle helleren Bildpunkte, und bei duenner Grossbuchstabenschrift ist die Haelfte davon halb ausgeleuchtete Kantenpunkte. Fuer grosse Schrift taugt sie, fuer kleine Versalien meldet sie systematisch zu wenig. Haette ich ihr geglaubt, haette ich vier Farben "repariert", die in Ordnung sind. Geprueft: pruef-start-ansicht, pruef-css-klassen, pruef-kalender, pruef-buehne, pruef-handy, pruef-workspace-seiten, pruef-tagesruf -- alle in Ordnung. Dazu Ring und Uhr auf 248/248 nachgemessen, die Kopf-Muster gegen die Uhrzeit nachgerechnet (01:24:59 -> Sekunde bei 281,115, Minute bei 96,76, Stunde bei 16,493) und die Kachelfarbe gegen alle zwanzig anderen in OKLab geprueft. Co-Authored-By: Claude Opus 5 <[email protected]>
267 lines
12 KiB
JavaScript
267 lines
12 KiB
JavaScript
/* ===================================================================
|
|
Die 21 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!!!!"
|
|
|
|
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:
|
|
|
|
Kleinster Abstand zweier Toene: 0,033 (Dateien gegen Creator)
|
|
Paare unter 0,05 (kaum trennbar): 21 von 210
|
|
Spanne der Helligkeit: 0,002
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
DREI 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.
|
|
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.
|
|
|
|
Aufrufen mit: node tools/kachel-farben.mjs
|
|
Die Ausgabe wird nach start.css uebertragen (Abschnitt "Kacheln").
|
|
|
|
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.
|
|
=================================================================== */
|
|
|
|
/* ---------- Farbenlehre ------------------------------------------- */
|
|
|
|
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);
|
|
|
|
function oklabAusRgb(r, g, b) {
|
|
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,
|
|
];
|
|
}
|
|
|
|
/** 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. */
|
|
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 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
|
|
};
|
|
|
|
/* ---------- 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 });
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ---------- 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.
|
|
|
|
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. */
|
|
|
|
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);
|
|
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; }
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ---------- Zuordnung: Wunschton, wo er ohne Verlust geht ---------- */
|
|
|
|
const frei = gewaehlt.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++;
|
|
}
|
|
|
|
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`);
|
|
|
|
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`);
|
|
|
|
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} */`);
|
|
});
|
|
|
|
/* 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 (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.`);
|