diff --git a/server/pruef-buehne.mjs b/server/pruef-buehne.mjs index 2340d59e..27950047 100644 --- a/server/pruef-buehne.mjs +++ b/server/pruef-buehne.mjs @@ -247,17 +247,18 @@ for (const [breite, hoehe, wie] of [[1440, 950, "Computer"], [390, 780, "Handy"] el2 && el2 !== e && !el2.contains(e) && !e.contains(el2) && el2.children.length === 0 && el2.textContent.trim().length > 0; - let schlecht = null, anzahl = 0, ohneStelle = 0; + let schlecht = null, anzahl = 0, ohneStelle = 0, uebersprungen = 0; const alle = [...document.querySelectorAll("p, h1, h2, h3, span, a, li")] .filter((e) => e.children.length === 0 && e.textContent.trim().length > 2); for (const e of alle) { const r = e.getBoundingClientRect(); - if (r.width < 8 || r.height < 8) continue; - if (r.top < 0 || r.bottom > window.innerHeight) continue; + if (r.width < 8 || r.height < 8) { uebersprungen++; continue; } + if (r.top < 0 || r.bottom > window.innerHeight) { uebersprungen++; continue; } const mitte = r.top + r.height / 2; const st0 = getComputedStyle(e); const kandidaten = []; + let gemessenBei = null, gemessenAuf = null; /* ZUERST IN DER EIGENEN POLSTERUNG. @@ -286,9 +287,81 @@ for (const [breite, hoehe, wie] of [[1440, 950, "Computer"], [390, 780, "Handy"] for (const [kx, ky] of kandidaten) { if (kx < 0 || ky < 0 || kx >= window.innerWidth || ky >= window.innerHeight) continue; if (istFremderText(document.elementFromPoint(kx, ky), e)) continue; + /* UND KEIN FREMDER UNTERGRUND (22.09.2026). + ============================================================ + Diese Pruefung meldete tagelang 3,28:1 fuer den Markentext + in der Kopfleiste. Drei Vermutungen nacheinander gemessen + und alle drei widerlegt: nicht die Glut der Leiste, nicht + der Schein hinter der Schrift, nicht die Schriftfarbe. + + Die Antwort kam erst, als die Pruefung sagen konnte, WO + sie misst: bei 125,34 -- auf `button.wahl2__knopf`. Bei + 390 px liegt der Sicht-Umschalter direkt neben dem + Schriftzug, und der Abtastpunkt 4 px rechts daneben trifft + SEINEN Hintergrund. Der Text liegt dort nicht drauf. + + Dieselbe Lehre wie schon einmal oben, nur andersherum: + Damals mass sie daneben statt innen, hier misst sie + daneben und trifft einen Nachbarn. Ein Element mit + eigenem, deckendem Hintergrund, das KEIN Vorfahre des + Textes ist, taugt nicht als sein Untergrund. + + Die Regel ist schmal: Nur deckende Hintergruende zaehlen + als fremd. Ein durchsichtiger Nachbar laesst denselben + Grund durch wie der Text selbst -- dort darf weiter + gemessen werden. */ + const dort = document.elementFromPoint(kx, ky); + if (dort && dort !== e && !e.contains(dort) && !dort.contains(e)) { + /* WESSEN FLAECHE IST DAS? Der Knopf selbst ist oft + durchsichtig -- seine Flaeche traegt der Behaelter + darueber. Deshalb die Elternkette hoch bis zum ersten + deckenden Hintergrund und fragen, ob DER auch ueber dem + Text liegt. Tut er das nicht, ist es eine fremde + Flaeche, und der Text liegt dort nicht drauf. */ + const flaecheVon = (n) => { + for (let q = n; q && q !== document.documentElement; q = q.parentElement) { + const st2 = getComputedStyle(q); + const g2 = st2.backgroundColor || ""; + const teile = g2.match(/[\d.]+/g); + const deckend = teile && (teile.length < 4 || Number(teile[3]) >= 0.5) + && !/rgba\(0, 0, 0, 0\)/.test(g2); + const bild = st2.backgroundImage && st2.backgroundImage !== "none" + && (st2.webkitBackgroundClip || st2.backgroundClip || "") + .split(",")[0].trim() !== "text"; + if (deckend || bild) return q; + } + return null; + }; + const meine = flaecheVon(e); + const fremde = flaecheVon(dort); + if (fremde && fremde !== meine) continue; + /* AUCH EINE HALBDURCHSICHTIGE EIGENE FLAECHE ZAEHLT. + Der Knopf, der den Fehlalarm ausloeste, liegt in + DERSELBEN Kopfleiste wie der Text -- die deckende + Flaeche ist also dieselbe. Er traegt aber darueber noch + eine eigene, halbdurchsichtige, und genau die hebt den + gemessenen Punkt an. Ein Text liegt darauf nicht drauf, + er liegt DANEBEN. */ + { + let fremdeFlaeche = false; + for (let q = dort; q && q !== meine && !q.contains(e); q = q.parentElement) { + const st3 = getComputedStyle(q); + const t3 = (st3.backgroundColor || "").match(/[\d.]+/g); + const hatFarbe = t3 && (t3.length < 4 || Number(t3[3]) > 0.02); + const hatBild = st3.backgroundImage && st3.backgroundImage !== "none"; + if (hatFarbe || hatBild) { fremdeFlaeche = true; break; } + } + if (fremdeFlaeche) continue; + } + } const bx = Math.round(kx * skala), by = Math.round(ky * skala); if (bx < 0 || bx >= c.width || by < 0 || by >= c.height) continue; p = g.getImageData(bx, by, 1, 1).data; + gemessenBei = Math.round(kx) + "," + Math.round(ky); + const hier = document.elementFromPoint(kx, ky); + gemessenAuf = hier ? hier.tagName.toLowerCase() + + (typeof hier.className === "string" && hier.className + ? "." + hier.className.trim().split(/\s+/)[0] : "") : "nichts"; break; } /* Kein freier Punkt gefunden -- das wird GEZÄHLT und ausgegeben. @@ -328,10 +401,26 @@ for (const [breite, hoehe, wie] of [[1440, 950, "Computer"], [390, 780, "Handy"] const v = kontrast(farbe, [p[0], p[1], p[2]]); anzahl++; if (!schlecht || v / grenze < schlecht.v / schlecht.grenze) { - schlecht = { v, grenze, text: e.textContent.trim().slice(0, 26), grund: [p[0], p[1], p[2]] }; + /* WO ES STEHT, NICHT NUR WAS DASTEHT (22.09.2026). + Diese Pruefung meldete tagelang 3,28:1 bei "Spicy & Dogi" + -- und die Suche danach kostete eine halbe Stunde, weil + derselbe Text an mehreren Stellen vorkommt. Eine Pruefung, + die einen Fehler findet, ihn aber nicht auffindbar macht, + kostet mehr Zeit als sie spart. */ + const wo = e.tagName.toLowerCase() + + (typeof e.className === "string" && e.className + ? "." + e.className.trim().split(/\s+/).join(".") : ""); + schlecht = { v, grenze, text: e.textContent.trim().slice(0, 26), + grund: [p[0], p[1], p[2]], wo, + eltern: (() => { const k=[]; for (let q=e.parentElement; q && k.length<4; q=q.parentElement) + k.push(q.tagName.toLowerCase()+(typeof q.className==="string"&&q.className?"."+q.className.trim().split(/\s+/)[0]:"")); return k.join(" < "); })(), + lage: Math.round(r.left)+","+Math.round(r.top)+" "+Math.round(r.width)+"x"+Math.round(r.height), + gemessenBei, gemessenAuf, + groesse: Math.round(gr * 10) / 10, fett: st.fontWeight, + verlauf: clipText }; } } - return { ...schlecht, anzahl, ohneStelle, gesamt: alle.length }; + return { ...schlecht, anzahl, ohneStelle, uebersprungen, gesamt: alle.length }; }, [bild.toString("base64"), DUNKELSTER_QUELLE]); /* ---- GEGENPROBE ZUR VERLAUFSSCHRIFT ---------------------------- @@ -372,10 +461,35 @@ for (const [breite, hoehe, wie] of [[1440, 950, "Computer"], [390, 780, "Handy"] `${s}: Gegenprobe -- dunkler Verlauf wird als dunkel erkannt ` + `(${gegen.dunkel.dunkelster?.join(",")}), also faellt so etwas auf`); - ok(schlimmster.v >= schlimmster.grenze, + /* DIE ZAHL GEHOERT IN DIE BEDINGUNG (22.09.2026). + Sie stand bisher nur im Meldetext. Am 22.09. wurde die Abtastung + nachsichtiger gemacht (fremde Flaechen zaehlen nicht mehr als + Untergrund) -- richtig, aber eine Lockerung kann zu weit gehen. + Wuerde sie irgendwann JEDEN Punkt verwerfen, waere diese Zeile + fuer immer gruen, ohne je wieder etwas anzusehen. + ERSTER ANLAUF WAR `anzahl >= 5` -- und die Pruefung wurde auf + uebersicht.html sofort rot, weil die Seite schlicht nur vier + Texte hat. Eine feste Schwelle ist eine Rechnung von gestern; + sie war hier keine fuenf Minuten alt. + + Richtig ist die ABLEITUNG: Jeder gefundene Text muss in genau + einem der drei Toepfe landen -- gemessen, ohne freien Untergrund, + oder vorher aussortiert (zu klein / ausserhalb des Bildes). Geht + die Rechnung nicht auf, ist unterwegs etwas verschwunden, und + zwar still. Diese Bedingung braucht keine Zahl und veraltet nicht. + Wie VIELE davon gemessen werden duerfen, deckelt die Zeile + darunter ("hoechstens zwei Texte ohne freien Untergrund"). */ + ok(schlimmster.anzahl + schlimmster.ohneStelle + schlimmster.uebersprungen + === schlimmster.gesamt + && schlimmster.v >= schlimmster.grenze, `${s}: schlechtester Kontrast ${schlimmster.v.toFixed(2)}:1 (nötig ${schlimmster.grenze}:1) ` + `bei "${schlimmster.text}" auf rgb(${schlimmster.grund.join(",")}) · ` - + `${schlimmster.anzahl} Stellen gemessen`); + + `${schlimmster.anzahl} gemessen + ${schlimmster.ohneStelle} ohne Grund ` + + `+ ${schlimmster.uebersprungen} aussortiert = ${schlimmster.gesamt}` + + (schlimmster.wo ? ` -- ${schlimmster.wo}, ${schlimmster.groesse}px/${schlimmster.fett}` + + (schlimmster.verlauf ? ", Verlauf im Text" : "") + + ` [${schlimmster.lage}] in ${schlimmster.eltern}` + + ` -- gemessen bei ${schlimmster.gemessenBei} auf ${schlimmster.gemessenAuf}` : "")); /* Wenn rings um einen Text nirgends freier Untergrund liegt, ist er ungeprüft geblieben. Das darf vorkommen (dicht gesetzte Zeilen), aber nicht unbemerkt und nicht bei vielen -- sonst meldet die diff --git a/server/pruef-chatkachel.mjs b/server/pruef-chatkachel.mjs index fe0a13f3..05e41fc5 100644 --- a/server/pruef-chatkachel.mjs +++ b/server/pruef-chatkachel.mjs @@ -21,12 +21,15 @@ AUFRUF: node server/pruef-chatkachel.mjs ===================================================================== */ -import { mkdtempSync, rmSync } from "node:fs"; +import { mkdtempSync, rmSync, readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname } from "node:path"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { eigenerPort } from "./helfer-port.mjs"; import { notbremse } from "./helfer-notbremse.mjs"; +const HIER = dirname(fileURLToPath(import.meta.url)); const PORT = await eigenerPort(import.meta, "pruef-chatkachel"); const ordner = mkdtempSync(join(tmpdir(), "ws-kachel-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); @@ -66,23 +69,98 @@ const kontrast = (a, b) => { const [h, d] = [helligkeit(a), helligkeit(b)].sort((x, y) => y - x); return (h + 0.05) / (d + 0.05); }; -const SCHRIFT = "#e8f1fa"; /* die Textfarbe des Hauses */ +/* GEGEN WELCHE SCHRIFT? DIE, DIE WIRKLICH AUF DER BLASE STEHT. + --------------------------------------------------------------------- + Hier stand `const SCHRIFT = "#e8f1fa"` — die Textfarbe des Hauses, + abgeschrieben. Am 22.09.2026 bekam die Blase ihre EIGENE Schrift + (--blase-text / --blase-leise, beide heller), damit die zwölf Töne + überhaupt Farbe tragen können. Von da an maß diese Prüfung eine + Farbe, die auf einer Blase gar nicht mehr vorkommt. -let schlechtester = 99, schlechtesterName = ""; -for (const k of CHAT_KACHELN) { - const c = kontrast(SCHRIFT, k.farbe); - if (c < schlechtester) { schlechtester = c; schlechtesterName = k.name; } + Das ist genau der Fehler, der an den Startkacheln Wochen gekostet + hat: eine Prüfung, die grün oder rot ist, ohne das anzusehen, was + auf dem Bildschirm steht. Eine abgeschriebene Farbe ist eine + Bestandsliste, und Bestandslisten altern. + + Gelesen wird jetzt aus chat.css — derselben Stelle, aus der auch + `tools/chat-kacheln-rechnen.mjs` liest. Eine Quelle, zwei Leser. + Findet sie sich dort nicht, ist das der DRITTE AUSGANG: Die Prüfung + kann nichts messen und sagt es, statt eine Ersatzfarbe zu erfinden + und weiter grün zu leuchten. */ +const chatCss = readFileSync( + join(HIER, "..", "workspace", "assets", "css", "chat.css"), "utf8"); +function ausCss(name) { + const m = chatCss.match(new RegExp("--" + name + ":\\s*(#[0-9a-fA-F]{3,6})\\s*;")); + return m ? m[1] : null; +} +/* Die Zahlen rechts sind dieselben wie im Rechenwerkzeug — 7:1 (AAA) + für den Fließtext, weil der Chat die Seite ist, auf der am längsten + gelesen wird, und 4,5:1 für die vier Handgriffe der Fußzeile. */ +const BLASEN_SCHRIFT = [ + ["blase-text", ".chat-nachricht__text", 7], + ["blase-leise", ".chat-nachricht__fuss", 4.5], +]; +for (const [name, wo, noetig] of BLASEN_SCHRIFT) { + const farbe = ausCss(name); + if (!farbe) { + ok(false, `KONNTE NICHT NACHSEHEN: --${name} steht nicht in chat.css` + + " — ohne sie lässt sich der Kontrast der Kacheln nicht prüfen"); + continue; + } + let schlecht = Infinity, wer = ""; + for (const k of CHAT_KACHELN) { + const c = kontrast(farbe, k.farbe); + if (c < schlecht) { schlecht = c; wer = k.name; } + } + /* Die ANZAHL gehört in die Bedingung. Wäre CHAT_KACHELN je leer, + bliebe `schlecht` auf Infinity und die Zeile wäre grün, ohne einen + einzigen Ton angesehen zu haben. */ + ok(CHAT_KACHELN.length >= 8 && schlecht >= noetig, + `${wo} trägt auf allen ${CHAT_KACHELN.length} Tönen ` + + `(schlechtester ${wer}: ${schlecht.toFixed(2)}:1, nötig ${noetig})`); } -/* 7:1 ist die strengere WCAG-Stufe (AAA für Fließtext). Ein Chat ist - die Seite, auf der am längsten gelesen wird — hier die niedrigere - Schwelle zu nehmen wäre am falschen Ende gespart. */ -ok(schlechtester >= 7, - `auch der hellste Ton trägt die Schrift noch (${schlechtesterName}: ${schlechtester.toFixed(1)}:1, nötig 7)`); +/* GEGENPROBE: Eine Prüfung, die nur bestätigen kann, bestätigt nichts. + Ein absichtlich zu heller Ton MUSS durchfallen — sonst wäre das + Grün oben nur die Abwesenheit einer Messung. */ +{ + const hell = ausCss("blase-text") || "#f3f7fc"; + ok(kontrast(hell, "#8fa2bd") < 7, + "Gegenprobe — ein zu heller Ton fiele durch " + + `(${kontrast(hell, "#8fa2bd").toFixed(2)}:1 gegen nötige 7)`); +} + +/* ALLE AUF DERSELBEN LEUCHTDICHTE — und das ist jetzt eine harte + Aussage, keine Nachsicht mehr. Hier stand Faktor 6; das war die + Toleranz für zwölf von Hand gewählte Grautöne. Seit sie gerechnet + werden, liegen sie auf einem Wert, und der Rest ist Rundung auf + ganze Bildpunkte. Ein Faktor über 1,1 hieße: jemand hat eine Farbe + von Hand eingetragen und damit die Rechnung umgangen. */ const spanne = Math.max(...CHAT_KACHELN.map((k) => helligkeit(k.farbe))) / Math.max(1e-6, Math.min(...CHAT_KACHELN.map((k) => helligkeit(k.farbe)))); -ok(spanne < 6, - `die Töne liegen nah beieinander (Faktor ${spanne.toFixed(1)}) — keiner sticht heraus`); +ok(spanne < 1.1, + `alle Töne liegen auf derselben Leuchtdichte (Faktor ${spanne.toFixed(3)}) ` + + "— keiner ist heller oder dunkler als die anderen"); + +/* UND SIE MÜSSEN BUNT SEIN. Gemessen an dem, was vor dem 22.09.2026 + drinstand: Buntheit 0,002 bis 0,048, im Mittel 0,028 — zwölf + Grautöne. Ohne diese Zeile könnte die Liste unbemerkt dorthin + zurückfallen, und alle Kontrastprüfungen oben blieben grün. */ +const buntheit = (hexf) => { + const z = hexf.replace("#", ""); + const [r, g, b] = [0, 2, 4].map((i) => parseInt(z.slice(i, i + 2), 16) / 255) + .map((c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4)); + 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 Math.hypot(1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s, + 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s); +}; +/* „grund" ist der bewusst neutrale Standard und zählt hier nicht mit. */ +const bunt = CHAT_KACHELN.filter((k) => k.wert !== "grund").map((k) => buntheit(k.farbe)); +ok(bunt.length >= 8 && Math.min(...bunt) >= 0.06, + `alle ${bunt.length} Farbtöne sind wirklich bunt ` + + `(blassester ${Math.min(...bunt).toFixed(3)}, vor dem Umbau lag der bunteste bei 0,048)`); /* KEINE DOPPELTE FARBE. Zwei gleiche Töne wären zwei Namen für dieselbe Kachel — man wählt den zweiten und sieht keinen Unterschied. */ @@ -156,9 +234,42 @@ const verlauf = await (await fetch(BASIS + `/workspace/api/chat/raeume/${raum}/n const vonLuna = (verlauf.nachrichten || []).find((n) => n.von === "Luna"); ok(vonLuna?.von_kachel === "moos", `Filipe bekommt Lunas Kachel mitgeliefert (${vonLuna?.von_kachel})`); +/* UND WER NICHTS EINGESTELLT HAT, TRÄGT TROTZDEM EINE FARBE. + --------------------------------------------------------------------- + Hier stand bis zum 22.09.2026 das Gegenteil: `ok(!vonFilipe + ?.von_kachel, "und seine eigene bleibt Standard …")`. Das war + richtig, solange „nichts eingestellt" Grau hieß. + + Gemessen an der Live-Datenbank hatten vier von fünfzehn Personen + eine Farbe gewählt — der Chat war also für alle anderen einfarbig, + egal wie gut die zwölf Töne gerechnet sind. Seitdem bekommt jede + Person von sich aus einen der elf bunten Töne, stabil aus ihrer ID. + + Geprüft wird jetzt beides, und das Zweite ist das Wichtigere: + dass die Farbe SICH NICHT ÄNDERT. Eine Person, die heute grün und + morgen rot ist, ist schlechter als eine ohne Farbe — man merkt sich + die falsche. */ const vonFilipe = (verlauf.nachrichten || []).find((n) => n.von === "Filipe"); -ok(!vonFilipe?.von_kachel, - "und seine eigene bleibt Standard, solange er nichts einstellt"); +const bunteToene = CHAT_KACHELN.filter((k) => k.wert !== "grund").map((k) => k.wert); +ok(bunteToene.length >= 8 && bunteToene.includes(vonFilipe?.von_kachel), + `und wer nichts eingestellt hat, trägt trotzdem eine Farbe ` + + `(${vonFilipe?.von_kachel})`); + +/* Zweimal fragen muss dasselbe ergeben. */ +const nochmal = await (await fetch(BASIS + `/workspace/api/chat/raeume/${raum}/nachrichten`, + { headers: { Cookie: kFilipe } })).json(); +const vonFilipe2 = (nochmal.nachrichten || []).find((n) => n.von === "Filipe"); +ok(!!vonFilipe?.von_kachel && vonFilipe2?.von_kachel === vonFilipe?.von_kachel, + `und es ist beim zweiten Abruf dieselbe (${vonFilipe2?.von_kachel})`); + +/* Der Wähler darf sie NICHT als gewählt anhaken — sonst sähe es aus, + als hätte man selbst etwas eingestellt, und der neutrale Ton wäre + unerreichbar geworden. */ +const meins = await (await fetch(BASIS + "/workspace/api/chat/kacheln", + { headers: { Cookie: kFilipe } })).json(); +ok(meins.meine == null && bunteToene.includes(meins.automatisch), + `im Wähler steht trotzdem nichts angehakt (meine=${meins.meine}), ` + + `und die automatische wird genannt (${meins.automatisch})`); const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); const browser = await pw.chromium.launch(); @@ -313,7 +424,21 @@ try { return c.backgroundColor + " | " + c.backgroundImage; }); const vorher = await kachelBild(); - await s.click('.chat-kachelwahl__ton[data-wert="wein"]'); + /* WELCHEN TON ANKLICKEN? EINEN AUS DER LISTE, NICHT EINEN GEMERKTEN. + Hier stand `data-wert="wein"` fest im Text. Am 22.09.2026 wurden + die zwoelf Toene neu gerechnet, `wein` fiel weg -- und die Pruefung + wartete 30 Sekunden auf einen Knopf, den es nicht mehr gibt, und + brach ab. Der Fehler lag nicht im Chat, sondern in dieser Zeile. + + Genommen wird jetzt der erste Ton, der NICHT die Voreinstellung + ist. Damit ist garantiert, dass sich etwas aendert -- und die + Zeile ueberlebt jede kuenftige Umbenennung. */ + const probeTon = CHAT_KACHELN.find((k) => k.wert !== "grund"); + if (!probeTon) { + ok(false, "KONNTE NICHT NACHSEHEN: es gibt keinen Ton ausser dem Standard"); + throw new Error("keine Toene zum Pruefen"); + } + await s.click(`.chat-kachelwahl__ton[data-wert="${probeTon.wert}"]`); await s.waitForTimeout(600); const nachher = await kachelBild(); ok(vorher !== nachher, @@ -322,8 +447,9 @@ try { /* UND ES IST GESPEICHERT, nicht nur gemalt. */ const nachSetzen = await (await fetch(BASIS + "/workspace/api/chat/kacheln", { headers: { Cookie: kFilipe } })).json(); - ok(nachSetzen.meine === "wein", - `und die Wahl ist gespeichert, nicht nur gemalt (${nachSetzen.meine})`); + ok(nachSetzen.meine === probeTon.wert, + `und die Wahl ist gespeichert, nicht nur gemalt ` + + `(${nachSetzen.meine}, erwartet ${probeTon.wert})`); ok(konsole.length === 0, konsole.length ? "Fehler im Browser: " + konsole[0].slice(0, 80) : "keine Fehler im Browser"); diff --git a/server/pruef-kachelfarben.mjs b/server/pruef-kachelfarben.mjs index 4b67e4f5..fa94ce65 100644 --- a/server/pruef-kachelfarben.mjs +++ b/server/pruef-kachelfarben.mjs @@ -170,6 +170,72 @@ console.log("\n=== Kein Paar sieht gleich aus ==="); `${eng.d.toFixed(4)} zwischen Ton ${eng.a?.n} und ${eng.b?.n}`); } +/* ---- 1b. Die Willkommenskachel traegt WIRKLICH alle Farben ---------- */ +/* Filipe: „die kachel soll alle farben haben die wir benutzt haben." + --------------------------------------------------------------------- + `tools/kachel-regenbogen.mjs` schreibt den Streifenblock aus den + benutzten Toenen. Einmal gelaufen ist er aber ab da eine ABSCHRIFT -- + und Abschriften altern. Kommt morgen eine Kachel dazu, hat „alle + Farben" eben doch nicht alle, und niemand sieht es: 30 Streifen sehen + aus wie 31. + + Deshalb wird hier nicht geprueft, ob der Block existiert, sondern ob + er noch zu den heute benutzten Toenen passt -- in BEIDE Richtungen. + Fehlt eine Farbe, ist die Kachel unvollstaendig; steht eine zu viel + darin, zeigt sie eine Farbe, die es auf der Wand nicht mehr gibt. + + Die Pruefung sagt auch, was zu tun ist. Eine Pruefung, die einen + Fehler findet, ihn aber nicht behebbar macht, kostet mehr Zeit als + sie spart: + node tools/kachel-regenbogen.mjs --schreiben + -------------------------------------------------------------------- */ +console.log("\n=== Die Willkommenskachel hat alle Farben ==="); +{ + const ANF = "/* ---- ANFANG GERECHNET: kachel-regenbogen.mjs ---- */"; + const ENDE = "/* ---- ENDE GERECHNET ---- */"; + const a = css.indexOf(ANF); + const e = css.indexOf(ENDE); + if (a < 0 || e < 0) { + pruef("KONNTE NICHT NACHSEHEN: der gerechnete Block steht nicht in" + + " start.css", false, + "erwartet zwischen den Marken ANFANG/ENDE GERECHNET"); + } else { + const block = css.slice(a, e); + /* Gezaehlt werden die Farben im STREIFENVERLAUF, nicht im ganzen + Block -- Lesesaum und Vignette bringen eigene Werte mit, die + nichts mit den Kacheltoenen zu tun haben. */ + const imBlock = new Set( + [...block.matchAll(/color-mix\(in srgb, (#[0-9a-fA-F]{6}) /g)] + .map((m) => m[1].toLowerCase())); + const sollen = new Set([...benutzt.keys()] + .filter((n) => farben.has(n)) + .map((n) => farben.get(n).toLowerCase())); + + const fehlen = [...sollen].filter((f) => !imBlock.has(f)); + const zuviel = [...imBlock].filter((f) => !sollen.has(f)); + + pruef(`die Kachel zeigt ${imBlock.size} Farben, benutzt werden ${sollen.size}`, + imBlock.size >= 20 && imBlock.size === sollen.size, + `${imBlock.size} statt ${sollen.size}`); + pruef("keine benutzte Farbe fehlt in der Kachel", + fehlen.length === 0, + `es fehlen ${fehlen.length}: ${fehlen.join(", ")}` + + " — node tools/kachel-regenbogen.mjs --schreiben"); + pruef("und keine Farbe steht darin, die niemand mehr benutzt", + zuviel.length === 0, + `${zuviel.length} zu viel: ${zuviel.join(", ")}` + + " — node tools/kachel-regenbogen.mjs --schreiben"); + + /* GEGENPROBE. Eine Mengenpruefung, die nur bestaetigen kann, + bestaetigt nichts: Waere der Streifenverlauf leer, waeren alle + drei Zeilen oben trivial gruen. */ + const gefaelscht = new Set([...imBlock].filter((f) => f !== [...imBlock][0])); + pruef("Gegenprobe — fehlt eine Farbe, faellt es auf", + [...sollen].filter((f) => !gefaelscht.has(f)).length > 0, + "die Mengenpruefung merkt eine fehlende Farbe nicht"); + } +} + /* ---- 2. Eindeutig je Ansicht --------------------------------------- */ console.log("\n=== Keine Ansicht zeigt zwei gleiche ==="); { diff --git a/server/workspace-chat.js b/server/workspace-chat.js index 15c7ff39..68325ca1 100644 --- a/server/workspace-chat.js +++ b/server/workspace-chat.js @@ -188,8 +188,8 @@ function bildWegFuer(personId) { Kachel gehoert einem selbst, GESEHEN wird sie von allen anderen -- deshalb ist sie kein Platz fuer etwas, das man nicht abschalten kann. - Die zwoelf Toene hier sind alle gleich dunkel und gleich gesaettigt. - Sie unterscheiden sich im Farbton und in sonst nichts. Damit ist der + Die zwoelf Toene hier liegen alle auf DERSELBEN LEUCHTDICHTE. Sie + unterscheiden sich im Farbton und in sonst nichts. Damit ist der Kontrast zur Schrift bei jedem gleich -- er muss nicht bei jedem einzeln geprueft werden, und niemand kann sich unlesbar machen. Augenschonend ist hier keine Geschmacksfrage: Ein Chat ist die @@ -197,21 +197,55 @@ function bildWegFuer(personId) { KEINE der zwoelf ist hell. Das ist Absicht und nicht Sparsamkeit -- die Schrift ist hell, und eine helle Kachel waere fuer die Person, - die sie waehlt, hübsch und fuer alle anderen unlesbar. + die sie waehlt, huebsch und fuer alle anderen unlesbar. + + --------------------------------------------------------------------- + DIESE LISTE IST GERECHNET, NICHT GEWAEHLT (22.09.2026) + + Filipe: „ich will dass die farben viel klarer und stärker sind ... + die kachel von den texten von den leuten im chat soll auch klarer + und stärker sein bitte." + + Bis heute lagen alle zwoelf bei einer Buntheit zwischen 0,002 und + 0,048 -- auf dem Bildschirm zwoelf Grautoene mit einem Hauch Farbe. + Das lag nicht an fehlendem Mut, sondern an der Fusszeile der Blase: + Sie stand auf der leisen Hausschrift, und gegen die darf die Flaeche + kaum Farbe haben. + + Geaendert wurde deshalb ZUERST die Schrift der Blase (--blase-text / + --blase-leise in chat.css, beide heller als die der Seite) und DANN + die Flaeche. Andersherum waere es der Fehler vom selben Vormittag + gewesen, als 27 von 31 Startkacheln unlesbar wurden. + + NICHT VON HAND AENDERN. Die Werte kommen aus + `tools/chat-kacheln-rechnen.mjs`; es liest die beiden Schriftfarben + aus dem CSS und rechnet daraus die hellste Flaeche aus, die beide + noch tragen (7:1 fuer den Nachrichtentext, 4,8:1 fuer die + Fusszeile). Wer hier eine Farbe eintippt, umgeht genau die Rechnung, + die die Lesbarkeit garantiert. Richtig ist: + + node tools/chat-kacheln-rechnen.mjs --schreiben + node server/pruef-chatkachel.mjs + + DIE SCHLUESSEL LINKS STEHEN IN DER DATENBANK. Wer einen umbenennt, + setzt die Wahl der betroffenen Person still auf „Standard" zurueck. + Vor diesem Umbau nachgesehen: gewaehlt waren `veilchen`, `ziegel` + und `moos` -- alle drei stehen weiter unten und haben ihren + Farbbereich behalten. ===================================================================== */ export const CHAT_KACHELN = [ - { wert: "grund", name: "Standard", farbe: "#131a25" }, - { wert: "tinte", name: "Tinte", farbe: "#141c2e" }, - { wert: "veilchen", name: "Veilchen", farbe: "#1d1730" }, - { wert: "pflaume", name: "Pflaume", farbe: "#251729" }, - { wert: "wein", name: "Wein", farbe: "#2a1620" }, - { wert: "ziegel", name: "Ziegel", farbe: "#2a1a17" }, - { wert: "bernstein", name: "Bernstein", farbe: "#271f14" }, - { wert: "moos", name: "Moos", farbe: "#16251b" }, - { wert: "tanne", name: "Tanne", farbe: "#132622" }, - { wert: "lagune", name: "Lagune", farbe: "#122429" }, - { wert: "stahl", name: "Stahl", farbe: "#1b2128" }, - { wert: "schiefer", name: "Schiefer", farbe: "#20201f" }, + { wert: "grund", name: "Neutral", farbe: "#4f545c" }, + { wert: "tinte", name: "Tinte", farbe: "#0150af" }, + { wert: "veilchen", name: "Veilchen", farbe: "#6a01e5" }, + { wert: "pflaume", name: "Pflaume", farbe: "#9301a2" }, + { wert: "kirsche", name: "Kirsche", farbe: "#a5015e" }, + { wert: "ziegel", name: "Ziegel", farbe: "#ad0103" }, + { wert: "bernstein", name: "Bernstein", farbe: "#7e4601" }, + { wert: "oliv", name: "Oliv", farbe: "#635201" }, + { wert: "moos", name: "Moos", farbe: "#405c01" }, + { wert: "tanne", name: "Tanne", farbe: "#00603d" }, + { wert: "lagune", name: "Lagune", farbe: "#035e5c" }, + { wert: "ozean", name: "Ozean", farbe: "#045b74" }, ]; /** Nur ein Ton aus der Liste kommt durch -- sonst nichts. @@ -226,7 +260,63 @@ export function kachelPruefen(wert) { return CHAT_KACHELN.some((k) => k.wert === String(wert)) ? String(wert) : null; } +/* WER NICHTS EINSTELLT, BEKOMMT TROTZDEM EINE FARBE (22.09.2026) + --------------------------------------------------------------------- + Filipe: „die kachel von den texten von den leuten im chat soll auch + klarer und stärker sein ... so dass die leute richtig bekommen es zu + benutzen." + + Vorher hiess „nichts eingestellt" GRAU -- und eingestellt hatten es + vier von fuenfzehn. Auf dem Bildschirm war der Chat damit einfarbig, + egal wie gut die zwoelf Toene gerechnet sind. Eine Einstellung, die + man erst finden muss, damit ueberhaupt etwas passiert, benutzt kaum + jemand; das Ergebnis sehen aber alle. + + Jetzt bekommt jede Person von sich aus einen der elf bunten Toene. + Dieselbe Person hat immer denselben -- man erkennt sie an der Farbe, + noch bevor man den Namen liest. Wer einen anderen will, waehlt ihn + weiterhin; die eigene Wahl gewinnt immer. + + WARUM AUS DER ID UND NICHT AUS DEM NAMEN: Ein Name aendert sich + (Heirat, Schreibweise, Kuerzel), eine ID nie. Eine Farbe, die beim + Umbenennen springt, waere schlimmer als gar keine. + + Der neutrale Ton bleibt in der Liste -- er ist jetzt eine WAHL + („ich will keine Farbe"), keine Voreinstellung. */ +/* WARUM NICHT EINFACH `id % 11`. + --------------------------------------------------------------------- + Die elf Toene stehen in der Reihenfolge ihres Farbwinkels -- Nachbarn + in der Liste sind auch Nachbarn im Farbkreis. Mit `id % 11` bekommen + zwei Leute, deren Nummern aufeinanderfolgen, deshalb zwei aehnliche + Farben. Gemessen am Bildschirm: Person 13 und 14 sassen im selben + Gespraech, eine magenta, eine rot -- auf dem Bildschirmfoto kaum zu + trennen, und genau das, was die Farbe verhindern soll. + + Elf ist eine Primzahl. Damit ist JEDE Schrittweite von 1 bis 10 + teilerfremd, laeuft also durch alle elf Toene, bevor sich einer + wiederholt -- keiner geht verloren. Die 4 ist gewaehlt, weil sie den + groessten Sprung ergibt: vier Schritte sind rund 131 Grad im + Farbkreis, fast das Maximum von 180. Aufeinanderfolgende Nummern + landen damit so weit auseinander wie moeglich. */ +const SPRUNG = 4; + +export function autoKachelFuer(personId) { + const bunte = CHAT_KACHELN.filter((k) => k.wert !== "grund"); + if (!bunte.length) return null; + const n = Number(personId); + if (!Number.isFinite(n) || n < 0) return null; + return bunte[(n * SPRUNG) % bunte.length].wert; +} + +/** Der Ton, den diese Person TRAEGT -- eigene Wahl, sonst die + * automatische. Fuer die Frage „was hat sie eingestellt?" gibt es + * `gewaehlteKachelFuer`; die beiden zu verwechseln hiesse, dass im + * Waehler ploetzlich ein Ton angehakt waere, den niemand gewaehlt hat. */ function kachelFuer(personId) { + return gewaehlteKachelFuer(personId) || autoKachelFuer(personId); +} + +function gewaehlteKachelFuer(personId) { try { return kachelPruefen(db().prepare( "SELECT chat_kachel FROM personen WHERE id = ?").get(personId)?.chat_kachel); @@ -543,7 +633,12 @@ chatRouter.get("/workspace/api/chat/kacheln", (req, res) => { try { res.json({ kacheln: CHAT_KACHELN, - meine: kachelFuer(req.person.id), + /* Die eigene WAHL, nicht der getragene Ton -- sonst stuende im + Waehler ein Haken bei einer Farbe, die niemand gewaehlt hat. */ + meine: gewaehlteKachelFuer(req.person.id), + /* Damit der Waehler beim neutralen Eintrag zeigen kann, was + stattdessen getragen wird, wenn man nichts waehlt. */ + automatisch: autoKachelFuer(req.person.id), }); } catch (f) { console.error("[chat] Kacheln:", f?.message); @@ -1091,7 +1186,9 @@ chatRouter.get("/workspace/api/chat/raeume/:id/nachrichten", (req, res) => { auch die von gestern. Die Farbe an jede Zeile zu schreiben waere eine zweite Wahrheit -- und nach der ersten Aenderung haette dieselbe Person zwei Farben im selben Gespraech. */ - von_kachel: kachelPruefen(n.von_kachel), + /* Eigene Wahl, sonst die automatische -- dieselbe Regel wie + ueberall, damit eine Person in jeder Liste gleich aussieht. */ + von_kachel: kachelPruefen(n.von_kachel) || autoKachelFuer(n.person_id), text: n.weg_am ? null : n.text, zurueckgenommen: !!n.weg_am, erstellt: n.erstellt, diff --git a/server/workspace.js b/server/workspace.js index a98f7278..baf76f27 100644 --- a/server/workspace.js +++ b/server/workspace.js @@ -1223,8 +1223,20 @@ const TREFF_BEREICHE = [ der Treff-Chat daneben 29 Nachrichten -- geredet wird im Chat, das Brett war eine Kachel ohne Inhalt. Sie zeigt jetzt auf die Seite, die erklaert, was es hier ueberhaupt gibt. */ + /* ALLE FARBEN DES HAUSES IN EINER KACHEL (22.09.2026). + + Filipe: „die farbe dieser kachel soll alle farben haben die es auf + der ganze seite gibt ... nicht gemischt ... soll komplett farbig + sein und ultra geil aussehen." + + `ton: 34` bleibt stehen und ist kein Widerspruch: Er faerbt Rand, + Zeichen und Pfeil und ist der Rueckfall, falls das Stilblatt + einmal nicht laedt. Die Streifen kommen aus einer eigenen Regel, + die `tools/kachel-regenbogen.mjs` rechnet -- aus den benutzten + Toenen, nicht aus einer Liste hier. */ { ...TREFF_GRUPPE, name: "Willkommen", unter: "Was es hier gibt, kurz erklaert", - zeichen: "community", ton: 34, gross: true, ziel: "willkommen.html", szene: "lounge" }, + zeichen: "community", ton: 34, gross: true, regenbogen: true, + ziel: "willkommen.html", szene: "lounge" }, /* DER CHAT (19.09.2026). Filipe: "eine geile mischung von punkt 2 und drei ... nach mitternacht bis morgens 6 uhr keiner schreiben kann damit auch die privatsphaere beruecksichtig wird ueber nacht @@ -1261,14 +1273,23 @@ const TREFF_BEREICHE = [ hat nur diese eine. */ { ...TREFF_GRUPPE, name: "Treff-Chat", unter: "Miteinander reden – nachts schläft er", zeichen: "chat", ton: 41, ziel: "chat.html?raum=treff", szene: "lounge" }, + /* HIGHLIGHTS AN DIE SPITZE (22.09.2026). + + Filipe: „da wo anschlagbrett ist soll highlights sein und dan den + rest nochmal weiter fuehren." + + Kein Tausch, sondern ein Aufruecken: Highlights nimmt den Platz, + alle anderen wandern eine Stelle weiter. Inhaltlich stimmt das + auch -- was die Leute selbst gemacht haben, steht vor dem, was + ihnen angesagt wird. */ + { ...TREFF_GRUPPE, name: "Highlights", unter: "Eure Clips und Bilder", + zeichen: "live", ton: 29, ziel: "bereich.html?b=highlight", szene: "arena" }, { ...TREFF_GRUPPE, name: "Anschlagbrett", unter: "Was gerade gilt", zeichen: "reports", ton: 33, ziel: "bereich.html?b=anschlag", szene: "halle" }, { ...TREFF_GRUPPE, name: "Was ansteht", unter: "Die n\u00e4chsten Streams und Events", zeichen: "kalender", ton: 31, ziel: "bereich.html?b=ansteht", szene: "skyline" }, { ...TREFF_GRUPPE, name: "Wunschliste", unter: "Was ihr euch w\u00fcnscht \u2013 und wer mitwill", zeichen: "content", ton: 30, ziel: "bereich.html?b=wunsch", szene: "portal" }, - { ...TREFF_GRUPPE, name: "Highlights", unter: "Eure Clips und Bilder", - zeichen: "live", ton: 29, ziel: "bereich.html?b=highlight", szene: "arena" }, /* HIESS BIS ZUM 21.09.2026 "Unsere Seiten" (Ziel und Datei bleiben gleich, nur der Name auf der Kachel aendert sich). diff --git a/tools/chat-kacheln-rechnen.mjs b/tools/chat-kacheln-rechnen.mjs new file mode 100644 index 00000000..9b58ba87 --- /dev/null +++ b/tools/chat-kacheln-rechnen.mjs @@ -0,0 +1,390 @@ +/* ===================================================================== + DIE ZWÖLF CHAT-KACHELN NEU RECHNEN (22.09.2026) + --------------------------------------------------------------------- + Filipe: „ich will dass die farben viel klarer und stärker sind und + die schrift vom chat soll dan auch angepasst werden ... die kachel + von den texten von den leuten im chat soll auch klarer und stärker + sein bitte." + + WAS VORHER DA WAR UND WARUM ES SO BLASS WAR + Die zwölf Töne lagen bei einer Helligkeit von rund 1 % (#131a25 und + Verwandte). Auf dem Bildschirm sind das zwölf Grautöne mit einem + Hauch Farbe — genau das, was auf Filipes Bildschirmfoto zu sehen ist. + + Der Grund dafür war richtig gedacht: Die Fußzeile der Blase (Uhrzeit, + „antworten", „kopieren") steht auf var(--text-leise) — einem leisen + Grau. Gegen eine kräftige Fläche wäre die unlesbar geworden. Die + dunkle Kachel war also keine Geschmacksentscheidung, sondern die + Folge einer zu leisen Schrift. + + DESHALB WIRD BEIDES ZUSAMMEN GERECHNET, nicht nacheinander. Die + Blase bekommt ihre EIGENE Schriftfarbe (--blase-text/--blase-leise, + heller als die der Seite), und dieses Werkzeug rechnet aus, wie + kräftig die Fläche dann werden DARF. + + Eine Farbe erst stärker zu machen und hinterher zu messen, ob die + Schrift noch geht, ist genau die Reihenfolge, mit der am selben Tag + schon einmal 27 von 31 Startkacheln unlesbar geworden sind. + + DAS VERFAHREN + 1. Die Schriftfarben werden aus dem CSS GELESEN, nicht hier + abgeschrieben. Eine zweite Liste wäre die, die beim nächsten + Farbwechsel vergessen wird. + 2. Aus dem nötigen Kontrast und der dunkelsten dieser Schriften + folgt GENAU EINE Leuchtdichte — die hellste Fläche, die beide + Schriften noch tragen. Sie wird ausgerechnet, nicht gesucht. + 3. Auf dieser einen Leuchtdichte bekommt jeder der elf Farbtöne die + höchste Buntheit, die der Bildschirm hergibt. + + WARUM ALLE AUF DERSELBEN LEUCHTDICHTE LIEGEN: Die Kachel gehört + einem selbst, GESEHEN wird sie von allen anderen. Gleiche + Leuchtdichte heißt, dass der Kontrast bei jedem der zwölf derselbe + ist — niemand kann sich durch seine Wahl schlechter lesbar machen + als die anderen. + + Nicht dieselbe empfundene Helligkeit, sondern dieselbe Leuchtdichte: + Das ist die Größe, an der Lesbarkeit hängt. Der erste Anlauf hat die + andere genommen und deshalb behauptet, es gebe gar keine Lösung. + + AUFRUF + node tools/chat-kacheln-rechnen.mjs nur zeigen + node tools/chat-kacheln-rechnen.mjs --schreiben eintragen + ===================================================================== */ + +import { readFileSync, writeFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const HIER = dirname(fileURLToPath(import.meta.url)); +const W = (...t) => join(HIER, "..", ...t); + +/* ---- Farbrechnen ---------------------------------------------------- */ +const zuLinear = (v) => (v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4); +const ausLinear = (v) => (v <= 0.0031308 ? v * 12.92 : 1.055 * v ** (1 / 2.4) - 0.055); + +function helligkeit([r, g, b]) { + return 0.2126 * zuLinear(r / 255) + 0.7152 * zuLinear(g / 255) + 0.0722 * zuLinear(b / 255); +} +function kontrast(a, b) { + const ha = helligkeit(a), hb = helligkeit(b); + const [h, d] = ha > hb ? [ha, hb] : [hb, ha]; + return (h + 0.05) / (d + 0.05); +} +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) ** 3; + const m_ = (L - 0.1055613458 * a - 0.0638541728 * bb) ** 3; + const s_ = (L - 0.0894841775 * a - 1.2914855480 * bb) ** 3; + const r = 4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_; + const g = -1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_; + const b2 = -0.0041960863 * l_ - 0.7034186147 * m_ + 1.7076147010 * s_; + return [r, g, b2]; +} +/** Lässt der Bildschirm diese Farbe überhaupt zu? */ +function imRaum(L, C, h) { + const [r, g, b] = ausOklch(L, C, h); + return r >= -1e-4 && g >= -1e-4 && b >= -1e-4 + && r <= 1.0001 && g <= 1.0001 && b <= 1.0001; +} +function zuRgb(L, C, h) { + return ausOklch(L, C, h) + .map((v) => Math.max(0, Math.min(255, Math.round(ausLinear(v) * 255)))); +} +const hex = (rgb) => "#" + rgb.map((x) => x.toString(16).padStart(2, "0")).join(""); +function ausHex(t) { + const m = t.replace("#", ""); + const v = m.length === 3 + ? [...m].map((c) => c + c) + : [m.slice(0, 2), m.slice(2, 4), m.slice(4, 6)]; + return v.map((x) => parseInt(x, 16)); +} + +/* ---- Die Schriftfarben AUS DEM CSS holen ---------------------------- */ +/* Drei Dateien, weil die Blase in beiden Häusern vorkommt und das + Crew-Haus eigene Farben setzt. Genommen wird je Rolle die DUNKELSTE + der gefundenen — die bestimmt den schlechtesten Fall, und nur der + zählt. */ +function farbenLesen() { + const dateien = ["chat.css", "crew-haus.css", "gate.css"]; + const gefunden = new Map(); + for (const d of dateien) { + const t = readFileSync(W("workspace", "assets", "css", d), "utf8"); + for (const m of t.matchAll(/--([a-z0-9-]+):\s*(#[0-9a-fA-F]{3,6})\s*;/g)) { + if (!gefunden.has(m[1])) gefunden.set(m[1], []); + gefunden.get(m[1]).push(ausHex(m[2])); + } + } + return (name) => { + const l = gefunden.get(name); + if (!l || !l.length) return null; + return l.slice().sort((a, b) => helligkeit(a) - helligkeit(b))[0]; + }; +} +const dunkelste = farbenLesen(); + +/* WELCHE SCHRIFT STEHT AUF EINER BLASE, UND WAS BRAUCHT SIE. + --------------------------------------------------------------------- + JEDE SCHRIFT HAT IHREN EIGENEN ANSPRUCH — eine gemeinsame Zahl war + hier falsch. + + Erster Anlauf verlangte für beide 4,8:1. Das Ergebnis erreichte für + den Nachrichtentext 6,86:1 — und riss damit die 7:1, auf die + pruef-chatkachel seit dem 20.09. besteht. Die Begründung dort steht + und gilt: Ein Chat ist die Seite, auf der am längsten gelesen wird, + und AAA ist hier nicht zu viel verlangt. + + Die Fußzeile ist kein Fließtext, sondern vier Handgriffe (Uhrzeit, + antworten, kopieren, anheften). Für sie gelten die 4,5:1 der Norm, + plus Spielraum auf 4,8 — ein Wert genau auf der Grenze wird beim + nächsten Rundungsschritt rot. + + Bindend ist am Ende die STRENGERE der beiden, bezogen auf ihre + jeweilige Farbe. Welche das ist, rechnet das Werkzeug aus; es steht + hier bewusst nicht als Zahl. */ +const SCHRIFTEN = [ + ["blase-text", ".chat-nachricht__text", 7.0], + ["blase-leise", ".chat-nachricht__fuss", 4.8], +]; +const pruefFarben = []; +for (const [name, wo, noetig] of SCHRIFTEN) { + const f = dunkelste(name); + if (!f) { + console.error("\nKONNTE NICHT NACHSEHEN: --" + name + " steht in keiner" + + " der drei CSS-Dateien.\n Ohne sie lässt sich nicht ausrechnen, wie" + + " kräftig die Fläche sein darf.\n Erst --" + name + " in chat.css" + + " anlegen, dann dieses Werkzeug erneut aufrufen."); + process.exit(2); + } + pruefFarben.push({ name, wo, noetig, farbe: f }); +} + +console.log("Schriftfarben aus dem CSS gelesen:"); +for (const s of pruefFarben) { + console.log(" --" + s.name.padEnd(12) + " " + hex(s.farbe) + " braucht " + + s.noetig.toFixed(1) + ":1 " + s.wo); +} + +/* ---- Die eine Leuchtdichte, auf der alle zwölf liegen --------------- + --------------------------------------------------------------------- + ZWEITER ANLAUF, UND DIESMAL DIE RICHTIGE GRÖSSE. + + Der erste suchte eine gemeinsame OKLab-Helligkeit — also gleiche + EMPFUNDENE Helligkeit — und meldete danach, es gebe gar keine Lösung. + Das lag nicht an der Schrift, sondern an der Größe, die ich gleich + halten wollte: Lesbarkeit hängt nicht an der empfundenen Helligkeit, + sondern an der LEUCHTDICHTE. Ein gesättigtes Grün und ein gesättigtes + Blau bei derselben OKLab-Helligkeit unterscheiden sich in der + Leuchtdichte um ein Vielfaches — und damit im Kontrast zur Schrift. + Gelb fiel deshalb bei jeder Helligkeit durch. + + Das Versprechen der zwölf Töne lautet wörtlich „damit bleibt die + Schrift überall gleich gut lesbar". Gleiche Leuchtdichte heißt: + jeder der zwölf hat EXAKT denselben Kontrast. Das ist das + Versprechen, gemessen statt umschrieben. + + UND SIE WIRD NICHT GESUCHT, SONDERN AUSGERECHNET. Aus dem nötigen + Kontrast und der dunkelsten Schrift auf der Blase folgt genau ein + Wert. Suchen müsste man nur, wenn er sich nicht herleiten ließe. + -------------------------------------------------------------------- */ +const ANZAHL_BUNT = 11; /* + 1 neutraler Standard = 12 */ +/* Start bei 258 Grad (Blau-Violett), wo die Hausfarbe ohnehin liegt — + der Sprung vom Standard zur ersten bunten Wahl ist dann keiner. */ +const HUES = Array.from({ length: ANZAHL_BUNT }, + (_, i) => ((i * 360) / ANZAHL_BUNT + 258) % 360); + +/* WIE BUNT MUSS ES MINDESTENS WERDEN, DAMIT DER UMBAU ETWAS BRINGT. + Gemessen an den zwölf, die bis heute drinstanden: ihre Buntheit liegt + zwischen 0,002 und 0,048, im Mittel 0,028 — deshalb sahen sie auf + Filipes Bildschirmfoto aus wie zwölf Grautöne. 0,06 liegt über dem + buntesten davon. Erreicht ein Ton das nicht, ist die Schrift auf der + Blase zu dunkel — dann sagt das Werkzeug das, statt wieder blasse + Farben auszuliefern. */ +const MINDEST_BUNTHEIT = 0.06; +const HOECHST_BUNTHEIT = 0.33; + +/* Die hellste Fläche, die JEDE Schrift auf der Blase noch trägt. */ +let zielLeuchte = Infinity; +for (const sf of pruefFarben) { + zielLeuchte = Math.min(zielLeuchte, (helligkeit(sf.farbe) + 0.05) / sf.noetig - 0.05); +} +if (!(zielLeuchte > 0)) { + console.error("\nKONNTE NICHT NACHSEHEN: Selbst eine schwarze Fläche" + + " erreicht den noetigen Kontrast nicht.\n Dann ist die Schrift auf der" + + " Blase zu dunkel — erst --blase-leise aufhellen."); + process.exit(2); +} + +/** Der buntestmögliche Ton dieses Farbtons auf genau dieser Leuchtdichte. */ +function buntest(hGrad) { + let besteC = 0, besteRgb = null; + for (let L = 0.20; L <= 0.80; L += 0.002) { + for (let c = HOECHST_BUNTHEIT; c >= 0.004; c -= 0.002) { + if (!imRaum(L, c, hGrad)) continue; + const rgb = zuRgb(L, c, hGrad); + /* Gerundete Bildpunkte treffen den Zielwert nie genau — ein + Prozent Spielraum. Der Kontrast wird danach am GERUNDETEN + Ergebnis nachgeprüft, nicht am gerechneten: ausgeliefert wird + das gerundete. */ + /* EINE OBERGRENZE, KEIN MITTELWERT. Erster Anlauf erlaubte ein + Prozent nach BEIDEN Seiten -- und ein Prozent nach oben + reisst genau die Schwelle, aus der dieser Wert hergeleitet + ist. Nach unten sind drei Prozent erlaubt, damit gerundete + Bildpunkte ueberhaupt einen Treffer haben. */ + const y = helligkeit(rgb); + if (y > zielLeuchte || y < zielLeuchte * 0.97) continue; + if (!pruefFarben.every((sf) => kontrast(rgb, sf.farbe) >= sf.noetig)) continue; + if (c > besteC) { besteC = c; besteRgb = rgb; } + break; + } + } + return besteRgb ? { c: besteC, rgb: besteRgb } : null; +} + +const bunte = HUES.map((h) => ({ h, ...(buntest(h) || {}) })); +const fehlen = bunte.filter((b) => !b.rgb || b.c < MINDEST_BUNTHEIT); +if (fehlen.length) { + console.error("\nKONNTE NICHT NACHSEHEN: " + fehlen.length + " von " + + ANZAHL_BUNT + " Farbtönen bleiben unter Buntheit " + MINDEST_BUNTHEIT + + ".\n Betroffen: " + fehlen.map((f) => Math.round(f.h) + " Grad").join(", ") + + "\n Heißt: die Schrift auf der Blase ist zu dunkel für kräftige" + + " Farben.\n Erst --blase-leise in chat.css aufhellen, dann erneut" + + " rechnen."); + process.exit(2); +} + +/* Der neutrale Standard liegt auf derselben Leuchtdichte — sonst wäre + ausgerechnet die Voreinstellung die Ausnahme. */ +/* UND ER WIRD GENAUSO NACHGEPRÜFT WIE DIE ANDEREN ELF. + Erster Anlauf prüfte hier nur die Leuchtdichte und verließ sich auf + das Prozent Spielraum. Das Ergebnis war #50555e mit 6,97:1 — um drei + Hundertstel unter der eigenen Schwelle, und ausgerechnet bei der + Voreinstellung, die die meisten sehen. Die Schlusszeile des Werkzeugs + hat es gemeldet; gefunden hat es also die Prüfung, nicht der Blick. + Ein Spielraum, der in EINE Richtung wirkt, ist kein Spielraum. */ +let grundRgb = null; +for (let L = 0.20; L <= 0.80; L += 0.001) { + const rgb = zuRgb(L, 0.016, 262); + const y = helligkeit(rgb); + if (y > zielLeuchte || y < zielLeuchte * 0.97) continue; + if (!pruefFarben.every((sf) => kontrast(rgb, sf.farbe) >= sf.noetig)) continue; + grundRgb = rgb; break; +} +if (!grundRgb) { + console.error("\nKONNTE NICHT NACHSEHEN: für den neutralen Standard ließ" + + " sich die Leuchtdichte nicht treffen."); + process.exit(2); +} + +/* ---- Die zwölf ------------------------------------------------------ */ +/* DIE SCHLÜSSEL LINKS SIND KEINE BESCHRIFTUNG — SIE STEHEN IN DER + DATENBANK. Wer sie umbenennt, setzt die Wahl der betroffenen Leute + still auf „Standard" zurück: `kachelPruefen` kennt den alten Wert + dann nicht mehr und gibt null. + + Vor diesem Umbau nachgesehen (nur lesend auf der Live-Datenbank): + gewählt sind `veilchen` (2×), `ziegel` (1×) und `moos` (1×), sonst + nichts. Alle drei stehen weiter unten und behalten ihren Farbbereich + — Veilchen bleibt violett, Ziegel bleibt rot, Moos bleibt grün. + Niemand verliert seine Einstellung. + + Die Reihenfolge ist NICHT frei: Sie gehört zu HUES, Eintrag für + Eintrag. Wer hier umsortiert, vertauscht die Farben. */ +const TOENE = [ + ["tinte", "Tinte"], ["veilchen", "Veilchen"], ["pflaume", "Pflaume"], + ["kirsche", "Kirsche"], ["ziegel", "Ziegel"], ["bernstein", "Bernstein"], + ["oliv", "Oliv"], ["moos", "Moos"], ["tanne", "Tanne"], + ["lagune", "Lagune"], ["ozean", "Ozean"], +]; +if (TOENE.length !== HUES.length) { + console.error("\nKONNTE NICHT NACHSEHEN: " + TOENE.length + " Namen für " + + HUES.length + " Farbtöne."); + process.exit(2); +} +/* „Neutral" und nicht mehr „Standard": Seit dem 22.09.2026 ist dieser + Ton keine Voreinstellung mehr, sondern eine Wahl -- „ich will keine + Farbe". Wer nichts einstellt, bekommt automatisch einen der elf + bunten. Ein Eintrag, der „Standard" heisst und nicht der Standard + ist, waere die unfreundlichste Art, das zu verschweigen. */ +const liste = [{ wert: "grund", name: "Neutral", farbe: hex(grundRgb) }]; +bunte.forEach((b, i) => { + liste.push({ wert: TOENE[i][0], name: TOENE[i][1], farbe: hex(b.rgb) }); +}); + +console.log("\nGemeinsame Leuchtdichte: " + zielLeuchte.toFixed(4) + + " (die zwölf bisherigen lagen bei rund 0,010 — daher das Grau)"); +console.log("Buntheit: " + Math.min(...bunte.map((b) => b.c)).toFixed(3) + + " bis " + Math.max(...bunte.map((b) => b.c)).toFixed(3) + + " (bisher 0,002 bis 0,048)"); +let schlechtester = Infinity; +for (const k of liste) { + const rgb = ausHex(k.farbe); + const ks = pruefFarben.map((s) => kontrast(rgb, s.farbe)); + schlechtester = Math.min(schlechtester, ...ks); + console.log(" " + k.name.padEnd(10) + " " + k.farbe + " " + + ks.map((v) => v.toFixed(2) + ":1").join(" ")); +} +console.log("\nJede Schrift gegen alle zwoelf, schlechtester Fall:"); +for (const sf of pruefFarben) { + const m = Math.min(...liste.map((k) => kontrast(ausHex(k.farbe), sf.farbe))); + console.log(" --" + sf.name.padEnd(12) + " " + m.toFixed(2) + ":1 (noetig " + + sf.noetig.toFixed(1) + ") " + (m >= sf.noetig ? "haelt" : "ZU WENIG")); +} + +/* ---- Eintragen ------------------------------------------------------ */ +if (!process.argv.includes("--schreiben")) { + console.log("\nNur gezeigt. Mit --schreiben wird es eingetragen."); + process.exit(0); +} +const datei = W("server", "workspace-chat.js"); +const quelle = readFileSync(datei, "utf8"); +const anf = quelle.indexOf("export const CHAT_KACHELN = ["); +const ende = quelle.indexOf("\n];", anf); +if (anf < 0 || ende < 0) { + console.error("KONNTE NICHT NACHSEHEN: CHAT_KACHELN steht nicht mehr da," + + " wo dieses Werkzeug sie sucht."); + process.exit(2); +} +const breite = Math.max(...liste.map((k) => k.wert.length)) + 3; +const neu = "export const CHAT_KACHELN = [\n" + + liste.map((k) => " { wert: " + ('"' + k.wert + '",').padEnd(breite) + + " name: " + ('"' + k.name + '",').padEnd(13) + + " farbe: \"" + k.farbe + "\" },").join("\n"); +writeFileSync(datei, quelle.slice(0, anf) + neu + quelle.slice(ende), "utf8"); +console.log("\nEingetragen in server/workspace-chat.js"); + +/* ---- UND DER RUECKFALL IM STILBLATT --------------------------------- + Das Stilblatt braucht den Standardton EBENFALLS: Wer nie etwas + eingestellt hat, bekommt vom Server keinen Wert, und die Blase faellt + auf das zurueck, was in chat.css steht. + + Genau daran ist der erste Anlauf am 22.09.2026 gescheitert. Die + zwoelf Toene waren gerechnet und kraeftig -- und auf dem Bildschirm + sahen ALLE Blasen aus wie vorher, weil dort noch #131a25 stand, die + alte Farbe. Gemeldet hat das kein Werkzeug; gesehen hat es erst das + Bildschirmfoto. Eine Rechnung, die nur die Haelfte der Stellen + erreicht, ist keine Rechnung, sondern eine zweite Meinung. + + Deshalb schreibt dasselbe Werkzeug beide Stellen. Von Hand gepflegt + liefen sie wieder auseinander -- die Frage waere nur, wann. */ +const stil = W("workspace", "assets", "css", "chat.css"); +const ANFANG = "/* ---- ANFANG GERECHNET: chat-kacheln-rechnen.mjs ---- */"; +const SCHLUSS = "/* ---- ENDE GERECHNET ---- */"; +const stilAlt = readFileSync(stil, "utf8"); +const a2 = stilAlt.indexOf(ANFANG); +const e2 = stilAlt.indexOf(SCHLUSS); +if (a2 < 0 || e2 < 0) { + console.error("\nKONNTE NICHT NACHSEHEN: Die Marken fuer den Rueckfall" + + " stehen nicht in chat.css."+QQ.replace(QQ,"")+"\n Erwartet:\n " + ANFANG + "\n " + + SCHLUSS + "\n Ohne sie faellt jede Blase ohne eigene Wahl auf einen" + + " alten Wert zurueck."); + process.exit(2); +} +const stilNeu = ANFANG + "\n" + + "/* ERZEUGT VON tools/chat-kacheln-rechnen.mjs -- NICHT VON HAND AENDERN.\n" + + " Der Ton, den eine Blase traegt, solange niemand etwas eingestellt\n" + + " hat. Derselbe Wert wie der Eintrag Standard in CHAT_KACHELN. */\n" + + ".chat-nachricht { --kachel-standard: " + liste[0].farbe + "; }\n" + + SCHLUSS; +writeFileSync(stil, stilAlt.slice(0, a2) + stilNeu + stilAlt.slice(e2 + SCHLUSS.length), "utf8"); +console.log("Rueckfall in chat.css gesetzt: " + liste[0].farbe); diff --git a/tools/kachel-regenbogen.mjs b/tools/kachel-regenbogen.mjs new file mode 100644 index 00000000..0ce3369b --- /dev/null +++ b/tools/kachel-regenbogen.mjs @@ -0,0 +1,220 @@ +/* ===================================================================== + DIE WILLKOMMENSKACHEL BEKOMMT ALLE FARBEN DES HAUSES (22.09.2026) + --------------------------------------------------------------------- + Filipe: „die farbe dieser kachel soll alle farben haben die es auf + der ganze seite gibt, also nicht gemischt sondern ich will alle + farben übelst geil in der kachel haben die kachel soll alle farben + haben die wir benutzt haben. soll komplett farbig sein und ultra geil + aussehen lass dir was einfallen und überrasch mich bitte." + + „NICHT GEMISCHT" IST DIE EIGENTLICHE ANSAGE. + Ein Verlauf über alle Töne wäre ein Brei: Zwischen zwei Nachbarfarben + entstehen Zwischentöne, die es im Haus gar nicht gibt, und die + einzelne Farbe ist nicht mehr zu erkennen. Deshalb harte Kanten — + jede Farbe bekommt ihren eigenen Streifen und bleibt sie selbst. + Man kann die Kachel ablaufen und jede Kachel der Wand darin + wiederfinden. + + WARUM DAS EIN WERKZEUG IST UND KEINE HANDARBEIT + Die Liste der Farben steht nicht hier, sie wird ABGELEITET — aus + denselben Kachellisten, die die Startseite baut (`bereicheFuer` / + `zusatzBereicheFuer` über alle Rollen und beide Häuser), und die + Farbwerte dazu aus `start.css`. Eine abgeschriebene Liste wäre die, + die beim nächsten neuen Bereich vergessen wird — und dann hätte die + Kachel „alle Farben" eben doch nicht alle. + + Kommt morgen eine Kachel dazu, genügt ein Lauf: + node tools/kachel-regenbogen.mjs nur zeigen + node tools/kachel-regenbogen.mjs --schreiben eintragen + + Und `pruef-willkommen` zählt nach, ob der Block noch zu den + tatsächlich benutzten Tönen passt — sonst wäre „einmal gelaufen" + dasselbe wie „von Hand gepflegt", nur unsichtbar. + + AUGENSCHONEND BLEIBT PFLICHT + Die Töne werden nicht roh nebeneinandergelegt. Jeder wird mit dem + dunklen Hausgrund gemischt, genau wie auf jeder anderen Kachel auch + (dort: `color-mix(--ton 62%, --flaeche)`). Die Kachel ist dadurch + voller Farbe und trotzdem so dunkel wie ihre Nachbarn — und der + Lesesaum unten, der auf allen Kacheln liegt, hält den Text lesbar. + Keine Bewegung, kein Flackern. + ===================================================================== */ + +import { readFileSync, writeFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const HIER = dirname(fileURLToPath(import.meta.url)); +const CSS = join(HIER, "..", "workspace", "assets", "css", "start.css"); +const ANFANG = "/* ---- ANFANG GERECHNET: kachel-regenbogen.mjs ---- */"; +const ENDE = "/* ---- ENDE GERECHNET ---- */"; + +/* ---- Welche Töne werden wirklich benutzt? --------------------------- */ +/* Dasselbe Verfahren wie in kachel-farben-entzerren.mjs. Es steht hier + noch einmal und nicht in einer gemeinsamen Datei, weil beide + Werkzeuge sonst voneinander abhängen — und das eine soll laufen + können, auch wenn am anderen gerade gebaut wird. Was zählt, ist dass + BEIDE ableiten statt abzuschreiben. */ +async function benutzteToene() { + const w = await import("../server/workspace.js"); + const rollen = ["admin", "hand", "linke", "modi", "gast", + "manager", "scout", "creator", "spicy"]; + const toene = new Map(); /* ton -> Set(Kachelnamen) */ + for (const r of rollen) { + for (const haus of ["crew", "agentur"]) { + const p = { id: 1, rolle: r, name: "X", haus }; + let k = []; + try { + k = [...(w.bereicheFuer(p, haus) || []), ...(w.zusatzBereicheFuer(p, haus) || [])]; + } catch { continue; } + for (const kachel of k) { + if (!kachel || kachel.ton == null) continue; + if (!toene.has(kachel.ton)) toene.set(kachel.ton, new Set()); + toene.get(kachel.ton).add(kachel.name); + } + } + } + return toene; +} + +/* ---- Die Farbwerte dazu --------------------------------------------- */ +function farbenAusCss(css) { + const m = new Map(); + for (const t of css.matchAll(/\[data-ton="(\d+)"\]\s*\{\s*--ton:\s*(#[0-9a-fA-F]{6})/g)) { + m.set(Number(t[1]), t[2].toLowerCase()); + } + return m; +} + +/* ---- Farbton für die Sortierung ------------------------------------- */ +/* Sortiert wird nach Farbton, nicht nach Tonnummer. Die Nummern sind + die Reihenfolge, in der Bereiche entstanden sind — nebeneinander + ergäbe das ein Durcheinander. Nach Farbton sortiert läuft die Kachel + einmal durch den Regenbogen, und genau das soll man sehen. */ +function farbwinkel(hex) { + const [r, g, b] = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255) + .map((c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4)); + 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); + const A = 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s; + const B = 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s; + return (Math.atan2(B, A) * 180) / Math.PI; +} + +/* ===================================================================== */ +const css = readFileSync(CSS, "utf8"); +const farben = farbenAusCss(css); +const benutzt = await benutzteToene(); + +const fehlend = [...benutzt.keys()].filter((t) => !farben.has(t)); +if (fehlend.length) { + console.error("\nKONNTE NICHT NACHSEHEN: Für " + fehlend.length + " benutzte" + + " Töne steht keine Farbe in start.css.\n Betroffen: " + fehlend.join(", ") + + "\n Ohne sie wäre die Kachel unvollständig, und niemand sähe es."); + process.exit(2); +} +if (benutzt.size < 8) { + console.error("\nKONNTE NICHT NACHSEHEN: nur " + benutzt.size + " benutzte" + + " Töne gefunden.\n Das sind zu wenige, um glaubhaft zu sein — vermutlich" + + " hat sich der Aufbau\n der Kachellisten geändert und die Ableitung" + + " greift ins Leere."); + process.exit(2); +} + +const liste = [...benutzt.keys()] + .map((t) => ({ ton: t, farbe: farben.get(t), wer: [...benutzt.get(t)].join(", ") })) + .sort((a, b) => farbwinkel(a.farbe) - farbwinkel(b.farbe)); + +console.log(liste.length + " benutzte Töne, nach Farbwinkel sortiert:\n"); +for (const e of liste) { + console.log(" Ton " + String(e.ton).padStart(2) + " " + e.farbe + + " " + String(Math.round(farbwinkel(e.farbe))).padStart(4) + " Grad " + e.wer); +} + +/* ---- Den Streifenverlauf bauen -------------------------------------- */ +/* Jede Farbe bekommt genau denselben Anteil. Die Breite ist damit eine + Rechnung aus der Anzahl, keine eingetragene Zahl — kommt eine Kachel + dazu, rücken alle zusammen, und es bleibt lückenlos. */ +const anteil = 100 / liste.length; +const streifen = liste.map((e, i) => { + const von = (i * anteil).toFixed(3); + const bis = ((i + 1) * anteil).toFixed(3); + /* GEGEN EINEN DECKENDEN GRUND, NICHT GEGEN --flaeche. + Erster Lauf nahm `var(--flaeche)` -- dieselbe Mischung wie auf + jeder anderen Kachel. Am Bildschirm sahen die Baender dadurch + milchig aus: --flaeche ist halbdurchsichtig (Alpha 0,89), und + durch jedes Band schien das Buehnenbild. Auf einer einfarbigen + Kachel faellt das nicht auf, bei 31 schmalen Streifen nebeneinander + schon -- sie verlieren genau das, wofuer sie da sind. + + Diese eine Kachel deckt deshalb. Das ist auch inhaltlich richtig: + Sie soll die Farben des Hauses ZEIGEN, nicht durch sie hindurch + etwas anderes. */ + const f = `color-mix(in srgb, ${e.farbe} 72%, #070b14)`; + return ` ${f} ${von}%,\n ${f} ${bis}%`; +}).join(",\n"); + +const block = `${ANFANG} +/* ERZEUGT VON tools/kachel-regenbogen.mjs — NICHT VON HAND ÄNDERN. + ${liste.length} Streifen, einer je benutztem Kachelton, sortiert nach + Farbwinkel. Die Liste ist aus den Kachellisten abgeleitet; wer eine + Kachel hinzufügt, lässt das Werkzeug erneut laufen. Die Prüfung + pruef-willkommen zählt nach, ob beides noch zusammenpasst. */ +.kachel[data-regenbogen="ja"] { + background-image: + /* 1. DER LESESAUM — er liegt ZUERST, also obenauf, und ist der + Grund, warum diese Kachel bunt sein darf, ohne dass die + Schrift leidet. + + STAERKER ALS AUF DEN ANDEREN KACHELN, und zwar gemessen: + Mit den Werten der normalen Kachel (.90/.70/.24) kam der + Text hier auf 4,45:1 -- fuenf Hundertstel unter der Grenze. + Kein Wunder: Dort liegt EIN dunkler Ton unter der Schrift, + hier liegen deckende Farbbaender. Ein heller Untergrund + braucht einen dunkleren Saum, sonst ist die Kachel schoen + und der Text weg. Gefunden hat das pruef-kachelfarben, nicht + der Blick -- 4,45 gegen 4,50 sieht man nicht. */ + linear-gradient(to top, + rgba(4, 6, 14, .95) 0%, rgba(4, 6, 14, .82) 26%, + rgba(4, 6, 14, .32) 48%, transparent 66%), + /* 2. DIE VIGNETTE — aussen dunkler als innen. Ohne sie wäre die + Kachel eine Fläche, mit ihr ein Ausschnitt. */ + radial-gradient(120% 120% at 50% 45%, transparent 44%, rgba(3, 5, 12, .42) 100%), + /* 3. EIN SCHRÄGER GLANZ quer über die Streifen. Er bindet die + ${liste.length} Bänder zu einer Fläche zusammen — ohne ihn sieht es + aus wie ein Farbmuster, mit ihm wie ein Gegenstand. */ + linear-gradient(104deg, + rgba(255, 255, 255, .10) 0%, + rgba(255, 255, 255, .02) 34%, + rgba(0, 0, 0, .10) 68%, + rgba(0, 0, 0, .26) 100%), + /* 4. DIE FARBEN DES HAUSES, harte Kanten, kein Verlauf dazwischen. + Jede Farbe bleibt sie selbst — genau das war die Ansage. */ + linear-gradient(100deg, +${streifen}); + background-size: 100% 100%, 100% 100%, 100% 100%, 100% 100%; + /* Die Kante links trägt alle Farben mit, in derselben Reihenfolge — + sonst wäre ausgerechnet der farbigste Streifen der Kachel einfarbig. */ + border-color: color-mix(in srgb, var(--ton) 38%, transparent); +} +${ENDE}`; + +if (!process.argv.includes("--schreiben")) { + console.log("\n--- so sähe der Block aus ---\n"); + console.log(block); + console.log("\nNur gezeigt. Mit --schreiben wird es eingetragen."); + process.exit(0); +} + +let neu; +if (css.includes(ANFANG) && css.includes(ENDE)) { + const a = css.indexOf(ANFANG); + const e = css.indexOf(ENDE) + ENDE.length; + neu = css.slice(0, a) + block + css.slice(e); +} else { + neu = css.replace(/\s*$/, "\n\n") + block + "\n"; +} +writeFileSync(CSS, neu, "utf8"); +console.log("\nEingetragen in workspace/assets/css/start.css (" + + liste.length + " Streifen)."); diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index 8d6f79c8..aac28178 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,15 +16,15 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +