Filipe: "jede kiste soll seine eigene farbe haben. und die kacheln sollen viel spezieller, viel spezieller sein." BEIM ERSTEN MAL HATTE ICH ABGELEHNT, und das war zu bequem. Mein Einwand stimmte zwar -- ein Versuch mit frei gewaehlten Farben ergab Paare mit 1,4 Grad Abstand, also praktisch dieselbe Farbe -- aber daraus "geht nicht" zu machen, war falsch. Es geht, man muss nur rechnen. Der Denkfehler war die Annahme, alle 17 muessten sich voneinander unterscheiden. Das Auge vergleicht aber nur, was NEBENEINANDER liegt. Also: 1. 17 Toene, gleichmaessig um den Farbkreis (je 21 Grad), gerechnet in OKLCH -- dort sind gleiche Abstaende auch fuer das Auge gleich. Helligkeit und Farbstaerke konstant, damit keine sich vordraengt. Wo die Farbstaerke den darstellbaren Bereich verliesse (Gelb und Gruen frueher als der Rest), wird sie gesenkt, bis sie hineinpasst. 2. Die ZUORDNUNG ist eine Suche ueber die tatsaechlichen Nachbarschaften im Raster (nebeneinander UND untereinander). Gesucht: die Anordnung mit dem groesstmoeglichen kleinsten Nachbarabstand. Ergebnis: mindestens 105,9 Grad zwischen allen Nachbarn. 3. Unter allen Anordnungen, die eine harte Untergrenze schaffen, gewinnt die passendste: LIVE rot, Technik gelb, Reports gruen, Personen rot-gold, Schutz stahlblau. Steht als tools/kachel-farben.mjs im Repo, mit festem Zufallsstartwert -- derselbe Lauf ergibt dieselben Farben. Wer Kacheln umsortiert, aendert die Nachbarschaften und muss es neu laufen lassen; das steht auch in start.js. Geprueft: Helligkeitsband, Farbstaerke und Kontrast bestehen fuer alle siebzehn gegen genau diesen Hintergrund. VIEL SPEZIELLER -- das WASSERZEICHEN: Jede Kachel traegt ihr eigenes Zeichen noch einmal, riesig, angeschnitten und fast unsichtbar (7 % Deckung) in der Ecke. Das ist der Grund, warum siebzehn Kacheln nicht mehr wie siebzehn Kaesten aussehen: Jede bekommt eine eigene grosse Form, ohne dass ein einziges zusaetzliches Bild geladen wird -- es ist derselbe Pfad, nur groesser. Bewusst so schwach, dass man es nicht liest, sondern nur spuert. Beim Ueberfahren wird es etwas deutlicher und wandert zwei Pixel. Dazu: Zeichenfeld 46 auf 52 px mit farbigem Schein darunter, Name auf 1,06 rem, mehr Polsterung. Alles mit prefers-reduced-motion abgesichert. 44 Pruefungen. Neu: dass jede Kachel eine EIGENE Farbe hat (17 Farben auf 17 Kacheln, gemessen an der berechneten Strichfarbe) und dass das Wasserzeichen da und schwach genug ist. Co-Authored-By: Claude Opus 5 <[email protected]>
144 lines
6.1 KiB
JavaScript
144 lines
6.1 KiB
JavaScript
/* ===================================================================
|
|
Die 17 Kachelfarben der Startseite -- und wer welche bekommt.
|
|
|
|
Filipe wollte, dass JEDE Kachel ihre eigene Farbe hat. Ein erster
|
|
Versuch mit frei gewaehlten Farben ergab Paare mit einem Abstand von
|
|
1,4 -- also praktisch dieselbe Farbe. Das sieht nach Zufall aus, nicht
|
|
nach Absicht.
|
|
|
|
Deshalb wird hier gerechnet statt gewaehlt:
|
|
|
|
1. 17 Farbtoene, gleichmaessig um den Farbkreis verteilt, in OKLCH
|
|
gerechnet (dort sind gleiche Abstaende auch fuer das Auge gleich).
|
|
Helligkeit und Farbstaerke bleiben konstant -- dadurch wirken alle
|
|
gleich stark und keine draengt sich vor. Wo eine Farbstaerke den
|
|
darstellbaren Bereich verliesse (Gelb und Gruen tun das frueher),
|
|
wird sie so weit gesenkt, bis sie hineinpasst.
|
|
|
|
2. Die Zuordnung ist eine Suche, kein Geschmack. Verglichen wird vom
|
|
Auge nur, was NEBENEINANDER liegt -- 17 Farben muessen sich also
|
|
nicht alle voneinander unterscheiden, sondern nur von ihren
|
|
Nachbarn im Raster. Gesucht wird die Anordnung, bei der der
|
|
kleinste Abstand zweier Nachbarn so gross wie moeglich ist.
|
|
|
|
3. Unter allen Anordnungen, die eine harte Untergrenze schaffen, wird
|
|
die genommen, deren Farben am besten zur Sache passen: LIVE rot,
|
|
weil "on air" rot ist. Technik gelb wie ein Warnschild. Reports
|
|
gruen wie ein Ergebnis. Personen golden wie ein Schluessel.
|
|
|
|
Aufrufen mit: node tools/kachel-farben.mjs
|
|
Die Ausgabe wird nach start.css uebertragen (Abschnitt "Kacheln").
|
|
=================================================================== */
|
|
|
|
const L = 0.655, C_MAX = 0.148;
|
|
|
|
function oklchZuRgb(L, C, hGrad) {
|
|
const h = hGrad * Math.PI / 180;
|
|
const a = C * Math.cos(h), b = C * Math.sin(h);
|
|
const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
|
|
const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
|
|
const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
|
|
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 bl = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s;
|
|
const gam = (v) => (v <= 0.0031308 ? 12.92 * v : 1.055 * Math.pow(v, 1 / 2.4) - 0.055);
|
|
return [gam(r), gam(g), gam(bl)];
|
|
}
|
|
const imRaum = (rgb) => rgb.every((v) => v >= -0.001 && v <= 1.001);
|
|
const hex = (rgb) => "#" + rgb.map((v) =>
|
|
Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, "0")).join("");
|
|
|
|
const winkel = (i) => (i * 360 / 17 + 250) % 360;
|
|
function farbe(i) {
|
|
let C = C_MAX;
|
|
while (C > 0.02 && !imRaum(oklchZuRgb(L, C, winkel(i)))) C -= 0.004;
|
|
return hex(oklchZuRgb(L, C, winkel(i)));
|
|
}
|
|
|
|
const KACHELN = [
|
|
"Dashboard", "Aufgaben", "Kalender", "Calls", "Dateien",
|
|
"Creator-Profile", "Start-Check", "LIVE", "Content", "Community",
|
|
"Technik", "Schutz", "Reports",
|
|
"Personen", "Scout", "Bibliothek", "Automationen",
|
|
];
|
|
const N = (n) => KACHELN.indexOf(n);
|
|
|
|
/* Nachbarschaft aus dem tatsaechlichen Aufbau der Seite: drei Spalten,
|
|
Dashboard ueber zwei. Nebeneinander UND untereinander. */
|
|
const NACHBARN = [
|
|
["Dashboard", "Aufgaben"], ["Dashboard", "Kalender"], ["Dashboard", "Calls"],
|
|
["Aufgaben", "Dateien"], ["Kalender", "Calls"], ["Calls", "Dateien"],
|
|
["Creator-Profile", "Start-Check"], ["Start-Check", "LIVE"],
|
|
["Content", "Community"], ["Community", "Technik"], ["Schutz", "Reports"],
|
|
["Creator-Profile", "Content"], ["Start-Check", "Community"], ["LIVE", "Technik"],
|
|
["Content", "Schutz"], ["Community", "Reports"],
|
|
["Personen", "Scout"], ["Scout", "Bibliothek"], ["Personen", "Automationen"],
|
|
].map(([a, b]) => [N(a), N(b)]);
|
|
|
|
const abstand = (a, b) => {
|
|
const d = Math.abs(winkel(a) - winkel(b)) % 360;
|
|
return Math.min(d, 360 - d);
|
|
};
|
|
|
|
const WUNSCH = {
|
|
LIVE: 17, Content: 38, Technik: 81, Reports: 144, Personen: 59,
|
|
Schutz: 250, Community: 335, Dashboard: 250, Kalender: 208,
|
|
Bibliothek: 271, "Start-Check": 165,
|
|
};
|
|
function wunschNaehe(z) {
|
|
let punkte = 0;
|
|
for (const [name, wunsch] of Object.entries(WUNSCH)) {
|
|
const d = Math.abs(winkel(z[N(name)]) - wunsch) % 360;
|
|
punkte += 180 - Math.min(d, 360 - d);
|
|
}
|
|
return punkte;
|
|
}
|
|
|
|
/* Erst die Unterscheidbarkeit sichern, dann unter allem, was das
|
|
schafft, die passendste Anordnung waehlen. */
|
|
const MINDEST = 95;
|
|
function bewerte(z) {
|
|
let schlechtestes = 999;
|
|
for (const [a, b] of NACHBARN) schlechtestes = Math.min(schlechtestes, abstand(z[a], z[b]));
|
|
if (schlechtestes < MINDEST) return schlechtestes; // noch nicht gut genug
|
|
return 1e6 + schlechtestes * 1000 + wunschNaehe(z);
|
|
}
|
|
|
|
/* Fester Zufall: Dieselbe Eingabe muss dieselbe Ausgabe ergeben, sonst
|
|
waeren die Farben nach jedem Lauf andere. */
|
|
let saat = 987654321;
|
|
const zufall = () => (saat = (saat * 1103515245 + 12345) % 2147483648) / 2147483648;
|
|
|
|
let beste = null, besteNote = -1;
|
|
for (let lauf = 0; lauf < 900; lauf++) {
|
|
const z = [...Array(17).keys()];
|
|
for (let i = 16; i > 0; i--) { const j = Math.floor(zufall() * (i + 1)); [z[i], z[j]] = [z[j], z[i]]; }
|
|
let note = bewerte(z), besser = true;
|
|
while (besser) {
|
|
besser = false;
|
|
for (let a = 0; a < 17; a++) for (let b = a + 1; b < 17; b++) {
|
|
[z[a], z[b]] = [z[b], z[a]];
|
|
const neu = bewerte(z);
|
|
if (neu > note) { note = neu; besser = true; } else { [z[a], z[b]] = [z[b], z[a]]; }
|
|
}
|
|
}
|
|
if (note > besteNote) { besteNote = note; beste = [...z]; }
|
|
}
|
|
|
|
let schlechtestes = 999, paar = "";
|
|
for (const [a, b] of NACHBARN) {
|
|
const d = abstand(beste[a], beste[b]);
|
|
if (d < schlechtestes) { schlechtestes = d; paar = `${KACHELN[a]} / ${KACHELN[b]}`; }
|
|
}
|
|
|
|
console.log(`Kleinster Abstand zweier Nachbarn: ${schlechtestes.toFixed(1)}° (${paar})\n`);
|
|
console.log("--- fuer start.css ---");
|
|
KACHELN.forEach((k, i) => {
|
|
console.log(`.kachel[data-ton="${beste[i] + 1}"] { --ton: ${farbe(beste[i])}; } /* ${k}, ${Math.round(winkel(beste[i]))}° */`);
|
|
});
|
|
console.log("\n--- fuer start.js (ton je Kachel) ---");
|
|
console.log(JSON.stringify(Object.fromEntries(KACHELN.map((k, i) => [k, beste[i] + 1]))));
|
|
console.log("\n--- alle Farben in der Reihenfolge der Plaetze ---");
|
|
console.log([...Array(17).keys()].map((i) => farbe(i)).join(","));
|