ZWEI FEHLER, DIE FILIPE GESEHEN HAT.
1. DAS PROFILBILD LAG UEBER DER HALBEN SEITE. Als Patrick sein Bild
hochlud, zog sich ein roter Balken quer ueber die Startseite.
Ursache: Die CSS-Regel .wer__bild hat GEFEHLT. Das Element wurde in
kopf.js erzeugt, die Regel dazu nie geschrieben -- und ein <img> ohne
Groessenangabe nimmt seine natuerliche Groesse an, bei einem Handyfoto
also mehrere tausend Pixel. Dazu fehlte overflow: hidden am Traeger.
WARUM KEINE PRUEFUNG DAS GEFUNDEN HAT: Sie lud ein 1x1-Pixel-PNG hoch.
Klein, schnell, von Hand gebaut -- und voellig unfaehig, irgendetwas
zu ueberdecken. Die Pruefung war gruen, der Fehler war da, und gesehen
hat ihn der Nutzer. Sie arbeitet jetzt mit einem 1200x1200-Bild, also
in der Groesse, die wirklich hochgeladen wird, und misst danach: Bleibt
das Bild in seinem 28-px-Feld, ragt es irgendwo heraus, laeuft die
Seite ueber. Gegenprobe gemacht -- ohne die Regel meldet sie
1200x1200 in 28x28 und 918 px Ueberlauf.
2. ZWEI PERSONEN AUF EINEM BILDSCHIRM. Auf der Profilseite stand oben
"Profil: SpongBobSchwammKopf" und mittendrin "MEIN PROFIL: Dogfather".
Niemand konnte sagen, welche Angabe zu wem gehoert.
Es sind auch wirklich zwei verschiedene Dinge:
MEIN STECKBRIEF gehoert MIR -- Bild, ein Satz ueber mich, meine
Kanaele. Fuehre ich selbst.
CREATOR-PROFILE die BETREUUNGSAKTE eines anderen Menschen --
Ziele, 90-Tage-Plan, interne Notizen. Fuehren die
Betreuer.
Scouts, Manager und DogFather haben jetzt zwei getrennte Kacheln und
eine eigene Seite (steckbrief.html, serverseitig geschuetzt). Ein
Creator behaelt beides zusammen -- er hat nur eine Seite und sieht
dort ausschliesslich sich selbst. Die Logik liegt in einer eigenen
Datei statt hinten an profil.js: Sie gehoert der angemeldeten Person,
nicht der Akte.
ACHTZEHN KACHELN, ACHTZEHN FARBEN. Die neue Kachel haette sich ihren
Farbton mit den Creator-Profilen geteilt -- zwei Nachbarn in derselben
Farbe. Statt eine Farbe dazuzuerfinden, wurde tools/kachel-farben.mjs
fuer 18 Winkel neu gerechnet, samt der neuen Nachbarschaft im Raster.
Der kleinste Abstand zweier Nachbarn liegt weiterhin bei 100 Grad.
Gefunden hat das die Startseitenpruefung ("17 Farben auf 18 Kacheln").
Die Trennung wird jetzt fuer JEDE Rolle geprueft: welche Kacheln sie
sieht, dass auf der Akte kein fremder Steckbrief steht, und dass die
eigene Seite die richtige Person zeigt. Eine alte Pruefung, die das
Gegenteil verlangte, wurde ersetzt statt stehen gelassen.
Co-Authored-By: Claude Opus 5 <[email protected]>
150 lines
6.5 KiB
JavaScript
150 lines
6.5 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("");
|
|
|
|
/* 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. */
|
|
const ANZAHL = 18;
|
|
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", "Dateien",
|
|
"Mein Steckbrief", "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"],
|
|
["Mein Steckbrief", "Creator-Profile"], ["Creator-Profile", "Start-Check"],
|
|
["Mein Steckbrief", "LIVE"], ["Start-Check", "LIVE"], ["Start-Check", "Content"],
|
|
["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(ANZAHL).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 < 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(","));
|