/* =================================================================== 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(""); /* 01.09.2026: 18 statt 17. Der eigene Steckbrief hat eine eigene Kachel bekommen (vorher teilte er sich die Seite mit der Creator-Akte). Damit aendert sich der Abstand zwischen allen Toenen -- deshalb wird hier neu gerechnet und nicht eine Farbe dazuerfunden. 06.09.2026: 21. Drei Kacheln sind an einem Tag dazugekommen -- Chat, Zahlen und Agentur -- und jede hatte sich zunaechst einen Ton mit einer bestehenden geteilt. Das faellt am Rechner kaum auf und ist trotzdem falsch: Filipe wollte ausdruecklich, dass JEDE Kachel ihre eigene Farbe hat, und pruef-start-ansicht hat es gemessen (18 Farben auf 21 Kacheln). Eine Farbe dazuzuerfinden waere der bequeme Weg und der falsche -- sie laege dann irgendwo zwischen zweien und saehe nach Zufall aus. Also wird der ganze Kreis neu geteilt. */ const ANZAHL = 21; const winkel = (i) => (i * 360 / ANZAHL + 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", "Chat", "Dateien", "Zahlen", "Mein Steckbrief", "Creator-Profile", "Start-Check", "LIVE", "Content", "Community", "Technik", "Schutz", "Agentur", "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 = [ /* Taeglich: Dashboard ueber zwei Spalten, dann Aufgaben daneben; darunter Kalender, Calls, Chat; darunter Dateien. */ ["Dashboard", "Aufgaben"], ["Dashboard", "Kalender"], ["Dashboard", "Calls"], ["Aufgaben", "Chat"], ["Kalender", "Calls"], ["Calls", "Chat"], ["Kalender", "Dateien"], /* Rund um den Creator: elf Kacheln in drei Spalten. */ ["Zahlen", "Mein Steckbrief"], ["Mein Steckbrief", "Creator-Profile"], ["Zahlen", "Start-Check"], ["Mein Steckbrief", "LIVE"], ["Creator-Profile", "Content"], ["Start-Check", "LIVE"], ["LIVE", "Content"], ["Start-Check", "Community"], ["LIVE", "Technik"], ["Content", "Schutz"], ["Community", "Technik"], ["Technik", "Schutz"], ["Community", "Agentur"], ["Technik", "Reports"], ["Agentur", "Reports"], /* Team & System. */ ["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, /* Die drei Neuen (06.09.2026): Chat gruen wie ein gelesener Haken, Zahlen blau wie eine Messung, Agentur orange -- die Farbe, die "von aussen" sagt, ohne zu warnen. */ Chat: 150, Zahlen: 265, Agentur: 40, }; 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(ANZAHL).keys()]; /* ANZAHL - 1, nicht 16. Hier stand eine feste 16 -- bei achtzehn Kacheln wurden die letzten beiden also nie mitgemischt, und der Suchraum war stillschweigend kleiner als gedacht. Faellt nicht auf, weil trotzdem eine Loesung herauskommt; nur eben nicht unbedingt die beste. */ for (let i = ANZAHL - 1; 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 < ANZAHL; a++) for (let b = a + 1; b < ANZAHL; 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(ANZAHL).keys()].map((i) => farbe(i)).join(","));