Wunsch Filipe: "die hintergrund bilder sollen alle so strahlen und
schoen sein wie dieses. dieses ist wirklich mega und perfekt."
(Massstab ist die Scouting-Seite, Szene "wald".)
DER FUND: `chat.html` hat weder `.kopf-zeile` noch `.k-kopf` -- sie
traegt ihren Titel im eigenen `.chat__kopf`. In kopf.js stand ganz
oben:
const kopf = document.querySelector('.kopf-zeile, .k-kopf');
if (!B || !kopf) return;
Damit hing ALLES an der Frage, ob es eine Kopfzeile gibt, an die man
eine Plakette haengen kann -- auch der Farbton der Seite und ihr
Hintergrundbild, die damit nichts zu tun haben. Der Chat ist an dieser
Zeile ausgestiegen und hat WEDER Ton NOCH Buehne bekommen, obwohl in
bereiche.js seit jeher `szene: 'lounge'` fuer ihn steht. Eine
Zuordnung, die es gibt und die nie ankam.
Jetzt stehen Ton und Buehne VOR der Plakette: Sie brauchen nur den
Bereich. Die Plakette braucht zusaetzlich einen Kopf -- gibt es den
nicht, faellt eben nur sie aus.
WARUM DAS KEINE PRUEFUNG GEFUNDEN HAT, gleich zweimal:
1. `chat.html` stand nicht in der Seitenliste von
pruef-workspace-seiten. Eine Pruefung, die eine Seite nicht kennt,
kann auf ihr nichts finden. Jetzt drin, zusammen mit
leistung.html -- 18 Seiten statt 16.
2. Die Buehnenregel lautete `r.buehne ? r.buehneBild === r.buehne :
!!r.buehneBild` -- fehlt das Merkmal, reichte IRGENDEIN Bild.
Gedacht war die Ausnahme fuer die Startseite, geschrieben war sie
fuer jede Seite. Der Chat verlor sein `data-buehne`, fiel auf die
Grundszene zurueck, und die Pruefung sagte "ein Motiv ist da,
alles gut". Eine Bedingung, die bei fehlender Angabe MILDER wird
statt strenger, kann den Verlust dieser Angabe nicht melden --
sie belohnt ihn. Die Ausnahme haengt jetzt an der Startseite, nicht
am Fehlen des Merkmals.
Die Regel ist dafuer aus der Schleife herausgeloest (`buehneRichtig`)
und hat sieben Gegenproben bekommen -- darunter genau den Chat-Fall.
Ohne sie waere "alles in Ordnung" nur die Aussage, dass die Regel
nichts gemeldet hat, nicht dass sie etwas melden koennte.
DIE AUSNAHME DES CHATS STEHT JETZT MIT NAMEN in der Pruefung, statt
dass die Seite aus der Liste faellt: Plakette und Wasserzeichen
entfallen dort, weil es den Ort dafuer nicht gibt -- Ton und Buehne
gelten. Ob der Chat eine Plakette bekommen soll, ist eine
Gestaltungsfrage fuer Filipe, keine Fehlerfrage.
ZWEI FALSCHE AUSSAGEN in tools/gate-bauen.mjs richtiggestellt:
"halle: dieselbe Sammlung, GESPIEGELT" -- nachgemessen haben beide
Dateien dieselbe Pruefsumme, es gibt in diesem Werkzeug keine
Spiegelung. Und "0,42 ist gemessen" ueber `const DUNKEL = 0.26`; der
Wert wurde gesenkt, die Zeile ist nicht mitgegangen. Ein Kommentar, der
mehr behauptet als der Code tut, ist schlimmer als keiner.
ZUM EIGENTLICHEN WUNSCH, ehrlich: Ich habe die Hintergrundhelligkeit
aller 18 Seiten nachgemessen. Die Scouting-Seite ist tatsaechlich die
hellste (0,0263), alle anderen liegen 19 bis 65 Prozent darunter --
aber der Grund ist NICHT die Bildbehandlung. Schleier und Abdunklung
sind fuer alle Seiten gleich und mehrfach nachgemessen. Der Unterschied
ist, WIE VIEL vom Bild noch zu sehen ist: Die Scouting-Seite traegt
eine schmale Karte, die anderen dichte Tabellen und Kachelraster. Das
liesse sich aendern, aber es ist eine Entscheidung ueber die
Inhaltsdichte von 17 Seiten -- die gehoert Filipe, nicht mir um zwei
Uhr nachts.
(Meine erste Messung sagte das Gegenteil. Sie nahm einen Streifen bei
x 0..150 -- ausgerechnet die dunkelste Spalte jedes Motivs. Danach
schien die Scouting-Seite fast schwarz, waehrend das Bildschirmfoto
derselben Seite hell und farbig ist. Ein Messfeld, das nicht
repraesentativ ist, misst zuverlaessig das Falsche.)
Geprueft: pruef-workspace-seiten (18 Seiten, alles in Ordnung),
pruef-buehne, pruef-css-klassen -- alle in Ordnung.
Co-Authored-By: Claude Opus 5 <[email protected]>
703 lines
32 KiB
JavaScript
703 lines
32 KiB
JavaScript
/* =====================================================================
|
|
gate-bauen.mjs — Anmeldebild und Startbild aus den Markenbildern.
|
|
|
|
Zwei Dinge, ein Werkzeug, weil beide dieselbe Behandlung brauchen:
|
|
verkleinern, abdunkeln, nach WebP.
|
|
|
|
1. DIE ANMELDESEITE (gate-*.webp)
|
|
Aus dem Spicy-Media-x-DogFather-Bild. Es hat rechts eine LEERE
|
|
TAFEL mit Neonrahmen -- genau dort soll die Anmeldekarte sitzen.
|
|
|
|
2. DIE STARTSEITE (buehne-dogfather-*.webp)
|
|
Aus dem Tresorbild.
|
|
|
|
---------------------------------------------------------------------
|
|
DIE TAFEL WIRD GEMESSEN, NICHT GESCHAETZT
|
|
|
|
Die Karte an einer im Bild GEMALTEN Tafel auszurichten, geht nur, wenn
|
|
man weiss, wo die Tafel wirklich sitzt. Ich haette Prozentwerte
|
|
abschaetzen koennen -- und laege dann um ein paar Pixel daneben, was
|
|
man bei einem Neonrahmen sofort sieht.
|
|
|
|
Deshalb sucht dieses Werkzeug den Rahmen selbst: Es geht Zeile fuer
|
|
Zeile und Spalte fuer Spalte durch die rechte Bildhaelfte und sucht
|
|
die hellen, gesaettigten Bildpunkte des Neonrandes. Heraus kommen
|
|
vier Prozentwerte, die genau so in gate.css stehen.
|
|
|
|
Gerechnet wird im Browser, der ohnehin fuer die Pruefungen da ist --
|
|
kein Bildprogramm, keine neue Abhaengigkeit, 0 EUR. Derselbe Weg wie
|
|
in tools/buehne-bauen.mjs.
|
|
|
|
Aufrufen mit: node tools/gate-bauen.mjs
|
|
===================================================================== */
|
|
|
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
import { join, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const WURZEL = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
const QUELLE = join(WURZEL, "assets", "img", "marke");
|
|
const ZIEL = join(WURZEL, "workspace", "assets", "img");
|
|
const ZIEL_BUEHNE = join(ZIEL, "buehne");
|
|
for (const o of [ZIEL, ZIEL_BUEHNE]) if (!existsSync(o)) mkdirSync(o, { recursive: true });
|
|
|
|
const alsDatenAdresse = (pfad) => {
|
|
const b = readFileSync(pfad);
|
|
const art = pfad.endsWith(".png") ? "image/png" : "image/jpeg";
|
|
return `data:${art};base64,${b.toString("base64")}`;
|
|
};
|
|
|
|
const { chromium } = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await chromium.launch();
|
|
const seite = await (await browser.newContext()).newPage();
|
|
await seite.setContent("<!doctype html><meta charset=utf-8><body>");
|
|
|
|
/* ---------------------------------------------------------------------
|
|
1. Wo sitzt die Tafel?
|
|
|
|
DRITTER ANLAUF, und der davor war es, der Filipe geaergert hat
|
|
(07.09.2026).
|
|
|
|
Anlauf 1 suchte den LEUCHTENDEN Rahmen und fand den Mond, die Blueten
|
|
und jede Spiegelung im Wasser -- also die halbe Bildhaelfte.
|
|
Anlauf 2 suchte die groesste DUNKLE Flaeche und fand 94 % Hoehe: Ueber
|
|
und unter der Tafel ist die Szene genauso schwarz.
|
|
Anlauf 3 lief vom Tafelinneren nach aussen und hielt beim ersten
|
|
farbigen Punkt an. Das klang richtig und war es fast -- nur glueht der
|
|
linke Rahmen im Motiv viel breiter als der rechte (breites Rot gegen
|
|
duennes Blau), also hielt er links FRUEHER an. Heraus kamen 58,59 %
|
|
statt 55,37 %. Ich habe den Unterschied damals nicht als Messfehler
|
|
erkannt, sondern in der CSS "um 1,1 % nach links" von Hand
|
|
nachjustiert -- eine Schaetzung auf eine Messung, der ich nicht
|
|
traute. Danach stand links ein 32 px breiter schwarzer Streifen der
|
|
gemalten Tafel offen, und genau den hat Filipe gesehen.
|
|
|
|
WAS JETZT GESUCHT WIRD, IST NICHT "hell" UND NICHT "dunkel", SONDERN
|
|
DIE EINZIGE EIGENSCHAFT, DIE NUR DER RAHMEN HAT: kraeftig UND stark
|
|
farbig zugleich (Maximum > 150 und Spanne zwischen groesstem und
|
|
kleinstem Kanal > 60). Mond und Nebel sind hell, aber grau. Die
|
|
Tafelflaeche ist dunkel und grau. Nur die vier Neonlinien sind beides.
|
|
|
|
Gezaehlt wird spaltenweise und zeilenweise, wie viele Punkte das
|
|
erfuellen. Die Linien stehen dann als klare Spitzen heraus -- kein
|
|
Hinlaufen, kein Anhalten, kein Richtungsproblem. Die Oberkante ist
|
|
dunkelrot und faellt unter die Neonschwelle; fuer sie gibt es eine
|
|
zweite Messung ueber das Helligkeitsprofil (136 gegen 20 daneben).
|
|
--------------------------------------------------------------------- */
|
|
const tafel = await seite.evaluate(async (daten) => {
|
|
const i = new Image(); i.src = daten; await i.decode();
|
|
const c = document.createElement("canvas");
|
|
c.width = i.width; c.height = i.height;
|
|
const g = c.getContext("2d");
|
|
g.drawImage(i, 0, 0);
|
|
const d = g.getImageData(0, 0, c.width, c.height).data;
|
|
const W = c.width, H = c.height;
|
|
|
|
const neon = (x, y) => {
|
|
const p = (y * W + x) * 4;
|
|
const r = d[p], gg = d[p + 1], b = d[p + 2];
|
|
const max = Math.max(r, gg, b), min = Math.min(r, gg, b);
|
|
return max > 150 && max - min > 60;
|
|
};
|
|
const helligkeit = (x, y) => {
|
|
const p = (y * W + x) * 4;
|
|
return Math.max(d[p], d[p + 1], d[p + 2]);
|
|
};
|
|
|
|
/* Alle Spalten der rechten Bildhaelfte, in denen ueber die Tafelhoehe
|
|
hinweg mindestens die Haelfte der Punkte Neon ist. */
|
|
const yA = Math.round(H * 0.16), yB = Math.round(H * 0.74);
|
|
const spalten = [];
|
|
for (let x = Math.round(W * 0.50); x < W; x++) {
|
|
let n = 0;
|
|
for (let y = yA; y < yB; y++) if (neon(x, y)) n++;
|
|
if (n / (yB - yA) > 0.55) spalten.push(x);
|
|
}
|
|
/* Die beiden aeussersten sind die senkrechten Rahmenlinien. */
|
|
const links = spalten.length ? Math.min(...spalten) : -1;
|
|
const rechts = spalten.length ? Math.max(...spalten) : -1;
|
|
|
|
/* Dasselbe waagerecht, INNERHALB der eben gefundenen Breite. */
|
|
const xA = links > 0 ? links + Math.round((rechts - links) * 0.08) : Math.round(W * 0.58);
|
|
const xB = rechts > 0 ? rechts - Math.round((rechts - links) * 0.08) : Math.round(W * 0.90);
|
|
const zeilen = [];
|
|
for (let y = Math.round(H * 0.05); y < Math.round(H * 0.90); y++) {
|
|
let n = 0;
|
|
for (let x = xA; x < xB; x++) if (neon(x, y)) n++;
|
|
if (n / (xB - xA) > 0.55) zeilen.push(y);
|
|
}
|
|
const unten = zeilen.length ? Math.max(...zeilen) : -1;
|
|
|
|
/* DIE OBERKANTE EINZELN. Sie ist ein duenner dunkelroter Strich und
|
|
erreicht die Neonschwelle nicht. Ueber das Helligkeitsprofil ist sie
|
|
trotzdem eindeutig: die hellste Zeile im oberen Fuenftel, weit ueber
|
|
allem daneben. */
|
|
let oben = -1, bestes = 0;
|
|
for (let y = Math.round(H * 0.06); y < Math.round(H * 0.24); y++) {
|
|
let summe = 0;
|
|
for (let x = xA; x < xB; x++) summe += helligkeit(x, y);
|
|
const mittel = summe / (xB - xA);
|
|
if (mittel > bestes) { bestes = mittel; oben = y; }
|
|
}
|
|
|
|
return { breite: W, hoehe: H, links, rechts, oben, unten, obenHelligkeit: Math.round(bestes) };
|
|
}, alsDatenAdresse(join(QUELLE, "gate-spicy-dogfather.jpeg")));
|
|
|
|
const p = (v, g) => Math.round((v / g) * 10000) / 100;
|
|
|
|
/* DAS ERGEBNIS MUSS PLAUSIBEL SEIN. Findet die Suche "die ganze rechte
|
|
Haelfte", hat sie nichts gefunden -- genau das ist beim ersten Anlauf
|
|
passiert, und es waere fast unbemerkt in die CSS gewandert. Lieber
|
|
ein Abbruch mit Ansage als vier Prozentwerte, die nach Messung
|
|
aussehen und geraten sind. */
|
|
{
|
|
const bAnteil = (tafel.rechts - tafel.links) / tafel.breite;
|
|
const hAnteil = (tafel.unten - tafel.oben) / tafel.hoehe;
|
|
/* "Nichts gefunden" ist ein eigener Ausgang, nicht bloss ein
|
|
unplausibler Wert: -1 heisst, es gab ueberhaupt keine Spitze. */
|
|
const gefunden = tafel.links > 0 && tafel.rechts > 0 && tafel.unten > 0 && tafel.oben > 0;
|
|
const gut = gefunden
|
|
&& bAnteil > 0.25 && bAnteil < 0.62 && hAnteil > 0.35 && hAnteil < 0.92
|
|
&& tafel.links / tafel.breite > 0.45;
|
|
if (!gut) {
|
|
console.error(`
|
|
ABBRUCH: Die gefundene Tafel ist nicht plausibel `
|
|
+ `(${p(bAnteil * 100, 100)} % breit, ${p(hAnteil * 100, 100)} % hoch, `
|
|
+ `links bei ${p(tafel.links, tafel.breite)} %).`);
|
|
console.error(`Erwartet wird ein Feld zwischen 25 und 62 Prozent Breite in der `
|
|
+ `rechten Bildhaelfte.
|
|
${gefunden
|
|
? "Die vier Linien wurden gefunden, liegen aber falsch -- passt das Motiv noch?"
|
|
: "Es wurde ueberhaupt keine Neonlinie gefunden. Die Schwelle in neon() "
|
|
+ "(max > 150, Spanne > 60) passt dann nicht zum Bild."}`);
|
|
await browser.close();
|
|
process.exit(2);
|
|
}
|
|
|
|
/* GEGENPROBE: Kann diese Messung ueberhaupt "nein" sagen? Dieselben
|
|
Zaehlungen auf der LINKEN Bildhaelfte duerfen KEINE durchgehende
|
|
senkrechte Linie finden -- dort steht das Logo, hell und farbig,
|
|
aber ohne Rahmen. Faende sie auch dort etwas, wuerde sie auf
|
|
"irgendetwas Buntes" reagieren statt auf den Rahmen, und ihre
|
|
vier Zahlen waeren Zufall. */
|
|
const gegenprobe = await seite.evaluate(async (daten) => {
|
|
const i = new Image(); i.src = daten; await i.decode();
|
|
const c = document.createElement("canvas");
|
|
c.width = i.width; c.height = i.height;
|
|
const g = c.getContext("2d"); g.drawImage(i, 0, 0);
|
|
const d = g.getImageData(0, 0, c.width, c.height).data;
|
|
const neon = (x, y) => {
|
|
const p = (y * c.width + x) * 4;
|
|
const r = d[p], gg = d[p + 1], b = d[p + 2];
|
|
const max = Math.max(r, gg, b), min = Math.min(r, gg, b);
|
|
return max > 150 && max - min > 60;
|
|
};
|
|
const yA = Math.round(c.height * 0.16), yB = Math.round(c.height * 0.74);
|
|
let treffer = 0;
|
|
for (let x = 0; x < Math.round(c.width * 0.45); x++) {
|
|
let n = 0;
|
|
for (let y = yA; y < yB; y++) if (neon(x, y)) n++;
|
|
if (n / (yB - yA) > 0.55) treffer++;
|
|
}
|
|
return treffer;
|
|
}, alsDatenAdresse(join(QUELLE, "gate-spicy-dogfather.jpeg")));
|
|
if (gegenprobe > 4) {
|
|
console.error(`
|
|
ABBRUCH (Gegenprobe): Auch links vom Rahmen wurden ${gegenprobe} `
|
|
+ `durchgehende Neonspalten gefunden. Die Suche reagiert dann nicht auf den `
|
|
+ `Rahmen, sondern auf irgendetwas Buntes -- ihre vier Zahlen waeren Zufall.`);
|
|
await browser.close();
|
|
process.exit(2);
|
|
}
|
|
console.log(`Gegenprobe: links vom Rahmen ${gegenprobe} durchgehende Neonspalten `
|
|
+ `(erwartet 0 bis 4) -- die Suche findet wirklich den Rahmen.`);
|
|
}
|
|
console.log(`Quellbild: ${tafel.breite}x${tafel.hoehe}`);
|
|
console.log("Die Tafel im Bild:");
|
|
console.log(` links ${p(tafel.links, tafel.breite)} % rechts ${p(tafel.rechts, tafel.breite)} %`);
|
|
console.log(` oben ${p(tafel.oben, tafel.hoehe)} % unten ${p(tafel.unten, tafel.hoehe)} %`);
|
|
console.log(` Breite ${p(tafel.rechts - tafel.links, tafel.breite)} % `
|
|
+ `Hoehe ${p(tafel.unten - tafel.oben, tafel.hoehe)} %`);
|
|
console.log("\nFuer gate.css:");
|
|
console.log(` --tafel-links: ${p(tafel.links, tafel.breite)}%;`);
|
|
console.log(` --tafel-oben: ${p(tafel.oben, tafel.hoehe)}%;`);
|
|
console.log(` --tafel-breite: ${p(tafel.rechts - tafel.links, tafel.breite)}%;`);
|
|
console.log(` --tafel-hoehe: ${p(tafel.unten - tafel.oben, tafel.hoehe)}%;`);
|
|
console.log(` --gate-seiten: ${tafel.breite} / ${tafel.hoehe};`);
|
|
|
|
/* ---------------------------------------------------------------------
|
|
2. Die Bilder bauen
|
|
--------------------------------------------------------------------- */
|
|
|
|
/** Verkleinert, dunkelt ab und gibt WebP zurueck.
|
|
*
|
|
* `ausschnitt` erlaubt einen Bildausschnitt in Anteilen (0..1) -- fuer
|
|
* die Hochkant-Fassung, die nur den rechten Teil mit der Tafel zeigt.
|
|
*
|
|
* `dunkel` legt einen schwarzen Schleier darueber. Auf der Anmeldeseite
|
|
* bleibt er MASSVOLL: Das Bild ist der Grund, warum die Seite gut
|
|
* aussieht, und die Karte bringt ihren eigenen dunklen Untergrund mit.
|
|
* Auf der Startseite darf er kraeftig sein -- dort liegt Text direkt
|
|
* darauf. */
|
|
/** Verkleinert, VEREDELT und gibt WebP zurueck.
|
|
*
|
|
* ---------------------------------------------------------------------
|
|
* WARUM HIER MEHR PASSIERT ALS VERKLEINERN (07.09.2026)
|
|
*
|
|
* Filipe: "die hintergrund bilder will ich auch dass die viel viel viel
|
|
* besser aussehen."
|
|
*
|
|
* Bis hierher tat dieses Werkzeug genau zwei Dinge: kleiner rechnen und
|
|
* einen schwarzen Schleier darueberlegen. Abdunkeln macht ein Bild aber
|
|
* nicht ruhiger, sondern TOT -- es zieht jede Farbe zur Mitte, und was
|
|
* bleibt, ist ein grauer Schleier mit einer Ahnung von Motiv. Genau so
|
|
* sahen sie aus.
|
|
*
|
|
* Was ein Bild stattdessen hochwertig macht, sind drei Dinge, die jeder
|
|
* Fotograf zuerst anfasst -- und keines davon ist "dunkler":
|
|
*
|
|
* 1. TIEFE STATT SCHWAERZE. Ein leichter Kontrastzug trennt die
|
|
* hellen von den dunklen Stellen wieder, die das Verkleinern
|
|
* zusammengeschoben hat. Verkleinern MITTELT Bildpunkte; das ist
|
|
* der Grund, warum jedes verkleinerte Bild flauer wirkt als das
|
|
* Original, und der Grund, warum man danach nachschaerft.
|
|
*
|
|
* 2. FARBE, DIE DAS MOTIV HAT. Ein Hauch Saettigung holt das Rot der
|
|
* Chili und das Gruen der Blaetter zurueck, die der Schleier
|
|
* herausgewaschen hat.
|
|
*
|
|
* 3. EINE VIGNETTE. Das ist der eigentliche Unterschied zwischen
|
|
* "Screenshot" und "Aufnahme". Ein echtes Objektiv laesst zu den
|
|
* Ecken hin Licht verlieren; das Auge liest das als Tiefe und
|
|
* fuehrt den Blick zur Mitte. Sie ersetzt hier ausserdem einen
|
|
* Teil des flachen Schleiers: Dunkel wird, wo ohnehin nichts
|
|
* steht, und nicht dort, wo das Motiv ist.
|
|
*
|
|
* Damit sinkt der flache Schleier von 0,42 auf 0,26 -- das Bild wird
|
|
* heller UND der Text bleibt lesbar, weil die Vignette genau an den
|
|
* Raendern arbeitet, an denen die Kacheln liegen. Nachgemessen von
|
|
* pruef-buehne an echten Bildpunkten.
|
|
*
|
|
* Alles im Browser gerechnet, der ohnehin fuer die Pruefungen da ist --
|
|
* kein Bildprogramm, keine neue Abhaengigkeit, 0 EUR.
|
|
*
|
|
* ---------------------------------------------------------------------
|
|
* ZWEITER ANLAUF (07.09.2026): DER SATZ OBEN WAR RICHTIG UND ICH HABE
|
|
* IHN NICHT BEFOLGT
|
|
*
|
|
* Filipe: "gerade sehen die sogar leicht verschwommen aus, das ist
|
|
* richtig scheisse ... die sollen wirklich sogar glaenzen."
|
|
*
|
|
* Er hat recht, und der Grund stand die ganze Zeit vier Absaetze weiter
|
|
* oben in dieser Datei: "Verkleinern MITTELT Bildpunkte ... und der
|
|
* Grund, warum man danach NACHSCHAERFT." Nachgeschaerft wurde nie.
|
|
* Kontrast und Saettigung waren da, der eine Schritt, der Kanten
|
|
* wirklich wieder Kanten macht, fehlte.
|
|
*
|
|
* Dazu ein zweiter, groesserer Fehler, und der ist meiner:
|
|
*
|
|
* DIE QUELLBILDER SIND 1647 x 955 PIXEL GROSS.
|
|
* ICH HABE DARAUS "uhd"-FASSUNGEN MIT 2400 PIXELN GEBAUT.
|
|
*
|
|
* Das ist eine VERGROESSERUNG um das 1,46-fache. Am 07.09. habe ich
|
|
* genau hier notiert, sein Bildschirm sei 2550 px breit und das groesste
|
|
* Bild nur 1600, deshalb werde jede Kante weich -- und habe als "Loesung"
|
|
* die Ausgabe auf 2400 hochgerechnet. Es gab nie mehr Bildpunkte. Ich
|
|
* habe die Datei um zwei Drittel groesser gemacht und dabei erfundene,
|
|
* weiche Bildpunkte ausgeliefert, in einer Datei, in der daneben steht,
|
|
* dass man Bildpunkte nicht zurueckholen kann.
|
|
*
|
|
* Behoben wird das nicht durch "kleiner ausliefern" -- dann rechnet der
|
|
* Browser hoch und das Ergebnis ist genauso weich, nur ohne dass man es
|
|
* steuern kann. Behoben wird es durch die Reihenfolge, die jedes
|
|
* Druckvorstufen-Handbuch vorschreibt: erst auf die ENDGROESSE rechnen,
|
|
* DANN schaerfen (Ausgabeschaerfung). Was der Browser danach noch
|
|
* skaliert, ist ein Zehntel statt der Haelfte.
|
|
*
|
|
* 4. UNSCHARFMASKE. Der Name taeuscht: Es wird eine WEICHE Kopie
|
|
* gebaut und die Differenz zum Original wieder DRAUFGERECHNET.
|
|
* Wo beide gleich sind (glatte Flaechen) passiert nichts; wo sie
|
|
* auseinandergehen (Kanten) wird der Unterschied verstaerkt. Genau
|
|
* deshalb schaerft es Kanten, ohne Rauschen in glatten Flaechen
|
|
* hochzuziehen.
|
|
*
|
|
* 5. GLANZ (Bloom). Das ist der Unterschied zwischen "hell" und
|
|
* "leuchtet". Aus dem Bild werden nur die HELLSTEN Stellen
|
|
* herausgezogen (Neon, nasser Stein, Wasserspiegelung), weich
|
|
* gezeichnet und ADDITIV wieder darueber gelegt. Licht, das ueber
|
|
* seine Kante hinausstrahlt -- so sieht jede Kameraaufnahme aus,
|
|
* und ohne das wirkt jedes Neonmotiv wie eine Zeichnung.
|
|
* Additiv (`lighter`) und nicht deckend: Der Glanz LEGT LICHT DAZU,
|
|
* er ueberdeckt nichts. Die dunklen Stellen bleiben unberuehrt,
|
|
* damit der Text darueber lesbar bleibt.
|
|
*/
|
|
|
|
/* Die beiden neuen Schritte laufen im Browser und bekommen deshalb ihren
|
|
Quelltext als Zeichenkette mit -- `evaluate` schickt nur Daten, keine
|
|
Funktionen aus dem Node-Teil. */
|
|
const BILD_WERKZEUG = `
|
|
/* Unscharfmaske: Original + staerke * (Original - Weichzeichnung). */
|
|
function schaerfen(g, c, staerke, radius) {
|
|
if (staerke <= 0) return;
|
|
const b = document.createElement("canvas");
|
|
b.width = c.width; b.height = c.height;
|
|
const gb = b.getContext("2d");
|
|
gb.filter = "blur(" + radius + "px)";
|
|
gb.drawImage(c, 0, 0);
|
|
const o = g.getImageData(0, 0, c.width, c.height);
|
|
const u = gb.getImageData(0, 0, c.width, c.height);
|
|
const A = o.data, B = u.data;
|
|
for (let i = 0; i < A.length; i += 4) {
|
|
for (let k = 0; k < 3; k++) {
|
|
const v = A[i + k] + staerke * (A[i + k] - B[i + k]);
|
|
A[i + k] = v < 0 ? 0 : v > 255 ? 255 : v;
|
|
}
|
|
}
|
|
g.putImageData(o, 0, 0);
|
|
}
|
|
|
|
/* Glanz: die hellsten Stellen weich machen und Licht dazugeben. */
|
|
function glanz(g, c, staerke, schwelle, radius) {
|
|
if (staerke <= 0) return;
|
|
const h = document.createElement("canvas");
|
|
h.width = c.width; h.height = c.height;
|
|
const gh = h.getContext("2d");
|
|
gh.drawImage(c, 0, 0);
|
|
const d = gh.getImageData(0, 0, c.width, c.height);
|
|
const D = d.data;
|
|
for (let i = 0; i < D.length; i += 4) {
|
|
const m = Math.max(D[i], D[i + 1], D[i + 2]);
|
|
if (m <= schwelle) { D[i] = 0; D[i + 1] = 0; D[i + 2] = 0; continue; }
|
|
/* Weich einsetzend, nicht als Stufe -- eine harte Schwelle malt
|
|
Raender um jede helle Flaeche. */
|
|
const f = (m - schwelle) / (255 - schwelle);
|
|
D[i] *= f; D[i + 1] *= f; D[i + 2] *= f;
|
|
}
|
|
gh.putImageData(d, 0, 0);
|
|
const w = document.createElement("canvas");
|
|
w.width = c.width; w.height = c.height;
|
|
const gw = w.getContext("2d");
|
|
gw.filter = "blur(" + radius + "px)";
|
|
gw.drawImage(h, 0, 0);
|
|
g.globalCompositeOperation = "lighter";
|
|
g.globalAlpha = staerke;
|
|
g.drawImage(w, 0, 0);
|
|
g.globalAlpha = 1;
|
|
g.globalCompositeOperation = "source-over";
|
|
}
|
|
|
|
/* S-Kurve auf der Helligkeit. staerke 0 = nichts, 0.15 = deutlich. */
|
|
function tiefe(g, c, staerke) {
|
|
if (staerke <= 0) return;
|
|
const d = g.getImageData(0, 0, c.width, c.height);
|
|
const D = d.data;
|
|
/* Nachschlagetabelle statt Rechnung je Bildpunkt: 256 Werte einmal,
|
|
statt drei Millionen Mal dieselbe Formel. */
|
|
const t = new Uint8Array(256);
|
|
for (let i = 0; i < 256; i++) {
|
|
const x = i / 255;
|
|
/* Glatte S-Kurve (smoothstep), gemischt mit dem Original. */
|
|
const s = x * x * (3 - 2 * x);
|
|
const v = x + (s - x) * staerke * 3;
|
|
t[i] = Math.max(0, Math.min(255, Math.round(v * 255)));
|
|
}
|
|
for (let i = 0; i < D.length; i += 4) {
|
|
const max = Math.max(D[i], D[i + 1], D[i + 2]);
|
|
if (max === 0) continue;
|
|
/* Ein Faktor fuer alle drei Kanaele -- der Farbton bleibt. */
|
|
const f = t[max] / max;
|
|
D[i] = Math.min(255, D[i] * f);
|
|
D[i + 1] = Math.min(255, D[i + 1] * f);
|
|
D[i + 2] = Math.min(255, D[i + 2] * f);
|
|
}
|
|
g.putImageData(d, 0, 0);
|
|
}
|
|
`;
|
|
|
|
async function bauen({ quelle, ziel, breite, ausschnitt = null, dunkel = 0, guete = 0.82,
|
|
seitenverhaeltnis = null, kontrast = 1.14, saettigung = 1.26, vignette = 0.34,
|
|
schaerfe = 1.05, glanzStaerke = 0.46, tiefe = 0.10 }) {
|
|
const webp = await seite.evaluate(async (a) => {
|
|
const i = new Image(); i.src = a.daten; await i.decode();
|
|
|
|
const qx = a.ausschnitt ? a.ausschnitt[0] * i.width : 0;
|
|
const qy = a.ausschnitt ? a.ausschnitt[1] * i.height : 0;
|
|
const qb = a.ausschnitt ? a.ausschnitt[2] * i.width : i.width;
|
|
const qh = a.ausschnitt ? a.ausschnitt[3] * i.height : i.height;
|
|
|
|
const zb = a.breite;
|
|
const zh = Math.round(a.seitenverhaeltnis ? zb / a.seitenverhaeltnis : zb * (qh / qb));
|
|
|
|
const c = document.createElement("canvas");
|
|
c.width = zb; c.height = zh;
|
|
const g = c.getContext("2d");
|
|
g.imageSmoothingQuality = "high";
|
|
|
|
/* 1 + 2: Kontrast und Saettigung BEIM ZEICHNEN -- der Filter wirkt
|
|
auf die Bildpunkte, die gerade entstehen, nicht auf eine zweite
|
|
Ebene darueber. Deshalb bleibt es scharf. */
|
|
g.filter = `contrast(${a.kontrast}) saturate(${a.saettigung})`;
|
|
g.drawImage(i, qx, qy, qb, qh, 0, 0, zb, zh);
|
|
g.filter = "none";
|
|
|
|
/* Die beiden Werkzeuge stehen als Text im Aufruf -- hier
|
|
eingesetzt. */
|
|
// eslint-disable-next-line no-eval
|
|
eval(a.werkzeug);
|
|
|
|
/* 4: NACHSCHAERFEN -- und zwar HIER, direkt nach dem Rechnen auf die
|
|
Endgroesse und VOR Vignette und Schleier.
|
|
|
|
Die Reihenfolge ist nicht Geschmack. Vignette und Schleier sind
|
|
weiche Verlaeufe; wer danach schaerft, schaerft deren Uebergaenge
|
|
mit und bekommt Ringe um die dunklen Ecken. Und wer vor dem
|
|
Verkleinern schaerft, wirft die Schaerfe beim Verkleinern wieder
|
|
weg -- genau deshalb heisst dieser Schritt "Ausgabeschaerfung".
|
|
|
|
Radius nach Bildgroesse: Eine feste Zahl waere bei 900 px eine
|
|
Kante und bei 2400 px unsichtbar. Ueber die Breite gerechnet
|
|
bleibt die Wirkung gleich -- dieselbe Ueberlegung wie bei einer
|
|
Schwelle, die man besser messen als festschreiben laesst. */
|
|
const radius = Math.max(0.6, zb / 1400);
|
|
schaerfen(g, c, a.schaerfe, radius);
|
|
|
|
/* 5: GLANZ. Nach dem Schaerfen, damit das Licht die geschaerften
|
|
Kanten umspielt und nicht umgekehrt der Glanz mitgeschaerft wird
|
|
(das gaebe harte Lichtsaeume statt weichem Strahlen). */
|
|
glanz(g, c, a.glanzStaerke, 172, Math.max(3, zb / 190));
|
|
|
|
/* 6: TIEFE. Die dunkelsten Stellen werden noch etwas dunkler, die
|
|
hellsten noch etwas heller -- eine S-Kurve, wie sie jeder
|
|
Bildbearbeiter als Erstes zieht.
|
|
|
|
Das ist NICHT dasselbe wie mehr Kontrast: Kontrast dehnt alles
|
|
gleichmaessig und frisst dabei Zeichnung in den Lichtern. Die
|
|
S-Kurve laesst die MITTE in Ruhe (dort sitzt das Motiv) und
|
|
arbeitet nur an den Enden. Genau daran erkennt man ein poliertes
|
|
Bild -- der schwarze Stein ist wirklich schwarz, das Neon wirklich
|
|
hell, und dazwischen bleibt alles lesbar.
|
|
|
|
Gerechnet auf dem Maximum der drei Kanaele, nicht je Kanal
|
|
einzeln: Je Kanal wuerde die Kurve die FARBE verschieben (ein
|
|
dunkles Rot wird sonst braun). So bleibt der Farbton, nur die
|
|
Helligkeit wandert. */
|
|
tiefe(g, c, a.tiefe);
|
|
|
|
/* 3: Die Vignette. Elliptisch ueber die ganze Flaeche, in der Mitte
|
|
voellig klar, zu den Ecken hin dunkel. Der weiche Verlauf ist
|
|
wichtig -- eine harte Kante saehe aus wie ein Fehler und nicht wie
|
|
ein Objektiv. */
|
|
if (a.vignette > 0) {
|
|
const r = Math.max(zb, zh) * 0.75;
|
|
const v = g.createRadialGradient(zb / 2, zh * 0.46, r * 0.18, zb / 2, zh * 0.46, r);
|
|
v.addColorStop(0, "rgba(0,0,0,0)");
|
|
v.addColorStop(0.55, `rgba(0,0,0,${(a.vignette * 0.22).toFixed(3)})`);
|
|
v.addColorStop(0.8, `rgba(0,0,0,${(a.vignette * 0.62).toFixed(3)})`);
|
|
v.addColorStop(1, `rgba(0,0,0,${a.vignette.toFixed(3)})`);
|
|
g.fillStyle = v;
|
|
g.fillRect(0, 0, zb, zh);
|
|
}
|
|
|
|
/* Und erst danach der flache Schleier -- so wenig wie noetig. */
|
|
if (a.dunkel > 0) {
|
|
g.fillStyle = `rgba(0,0,0,${a.dunkel})`;
|
|
g.fillRect(0, 0, zb, zh);
|
|
}
|
|
return c.toDataURL("image/webp", a.guete);
|
|
}, { daten: alsDatenAdresse(quelle), breite, ausschnitt, dunkel, guete, seitenverhaeltnis,
|
|
kontrast, saettigung, vignette, schaerfe, glanzStaerke, tiefe,
|
|
werkzeug: BILD_WERKZEUG });
|
|
|
|
const puffer = Buffer.from(webp.split(",")[1], "base64");
|
|
writeFileSync(ziel, puffer);
|
|
return puffer.length;
|
|
}
|
|
|
|
const GATE = join(QUELLE, "gate-spicy-dogfather.jpeg");
|
|
|
|
/* =====================================================================
|
|
1. DIE ANMELDESEITE
|
|
|
|
BIS 2560 PIXEL, nicht bis 1600 (07.09.2026).
|
|
|
|
Filipe: "die grafik soll viieeeel besser aussehen bitte. die
|
|
hintergrund bilder sollen viel hochwertiger und spezieller aussehen."
|
|
|
|
Die Ursache war nicht die Bildqualitaet, sondern die GROESSE. Sein
|
|
Bildschirm ist 2550 px breit, das groesste Bild war 1600 -- der
|
|
Browser rechnet es also um das 1,6-fache hoch. Jede Kante wird dabei
|
|
weich, und zwar gleichmaessig ueber das ganze Bild. Genau das sieht
|
|
man als "billig", ohne benennen zu koennen, warum.
|
|
|
|
Eine hoehere WebP-Guete haette daran nichts geaendert: Man kann keine
|
|
Bildpunkte zurueckholen, die nie ausgeliefert wurden.
|
|
|
|
Fuenf Stufen statt drei, und die Guete von 0,84 auf 0,88 -- die
|
|
Anmeldeseite ist EIN Bild, das jeder als Erstes sieht. Der Browser
|
|
nimmt ueber `srcset` ohnehin nur die Stufe, die er braucht; ein Handy
|
|
laedt weiterhin 68 KB.
|
|
===================================================================== */
|
|
console.log("\n--- Anmeldeseite ---");
|
|
for (const b of [960, 1280, 1600, 1920, 2560]) {
|
|
const n = await bauen({
|
|
quelle: GATE, ziel: join(ZIEL_BUEHNE, `gate-${b}.webp`), breite: b,
|
|
/* Nur ein Hauch. Der Neonrahmen der Tafel ist das, was die Karte
|
|
traegt -- wer ihn wegdunkelt, hat ein huebsches Bild und eine
|
|
Karte, die im Nichts schwebt. */
|
|
dunkel: 0.08, guete: 0.93, vignette: 0.26, schaerfe: 1.15, glanzStaerke: 0.42,
|
|
});
|
|
console.log(` gate-${b}.webp ${Math.round(n / 1024)} KB`);
|
|
}
|
|
|
|
/* HOCHKANT: der Ausschnitt um das LOGO, nicht um die Tafel.
|
|
*
|
|
* Erster Anlauf war falsch, und das Bildschirmfoto hat es sofort
|
|
* gezeigt: Ich hatte den Bereich um die Tafel genommen -- die ist aber
|
|
* LEER, und auf dem Handy liegt die Anmeldekarte ohnehin davor. Uebrig
|
|
* blieb ein schwarzes Rechteck mit ein paar Saeulen.
|
|
*
|
|
* Der senkrechte Schnitt ist nachgerechnet, nicht probiert: 557 px
|
|
* Bildhoehe geteilt durch 764 Quellpixel ergibt 0,729 -- die sichtbaren
|
|
* 157 px sind damit 215 Quellpixel, also genau der Bereich um den
|
|
* Schriftzug SPICY MEDIA. */
|
|
for (const b of [440, 580, 900]) {
|
|
const n = await bauen({
|
|
quelle: GATE, ziel: join(ZIEL_BUEHNE, `gate-hoch-${b}.webp`), breite: b,
|
|
ausschnitt: [0, 0.20, 0.52, 0.80],
|
|
dunkel: 0.30, guete: 0.86,
|
|
});
|
|
console.log(` gate-hoch-${b}.webp ${Math.round(n / 1024)} KB`);
|
|
}
|
|
|
|
/* =====================================================================
|
|
2. DIE NEUN BUEHNEN
|
|
|
|
Bis zum 07.09.2026 zeigten alle neun Szenen DASSELBE Bild. Das war
|
|
nie so gemeint -- die Zuordnung stand seit dem 01.09. in start.css,
|
|
nur zeigten alle Zeilen auf dieselbe Datei. Jetzt hat jede Gruppe von
|
|
Seiten ihr eigenes Motiv.
|
|
|
|
WELCHES BILD WOHIN -- und das ist nicht Geschmack, sondern folgt dem,
|
|
was auf der Seite passiert:
|
|
|
|
studio Startseite Chili-Wasserfall, "More Than
|
|
Media" -- der Eingang, das
|
|
lebendigste der sieben
|
|
showbuehne Dashboard, Reports Spiegelkabinett: viele Logos auf
|
|
einmal, wie viele Creator auf
|
|
einen Blick
|
|
portal LIVE, Content der Splash mit den Woertern
|
|
IDEAS / BRAND / CONTENT --
|
|
woertlich das Thema der Seite
|
|
garage Aufgaben, Technik Kohle und Glut, goldene Ringe:
|
|
Werkstatt, dort wird gearbeitet
|
|
arena Dateien, Wissen die Medaillon-Sammlung -- ein
|
|
Archiv sieht so aus
|
|
halle Profile, Steckbrief, DIESELBE Sammlung, Bild fuer Bild
|
|
Schutz gleich (nachgemessen: gleiche
|
|
Pruefsumme). Hier stand bis zum
|
|
09.09.2026 "gespiegelt" -- das war
|
|
nie so, es gibt in diesem Werkzeug
|
|
keine Spiegelung. Ein Kommentar,
|
|
der mehr behauptet als der Code
|
|
tut, ist schlimmer als keiner:
|
|
Man sucht den Unterschied und
|
|
findet ihn nicht.
|
|
wald Start-Check, Scout roter Ahorn am Wasser -- etwas,
|
|
das waechst
|
|
skyline Kalender Podest unter dem Mond, Nacht
|
|
lounge Calls, Chat dieselbe Nachtbuehne, ruhig
|
|
|
|
Zwei Motive kommen doppelt vor (sieben Bilder auf neun Plaetze). Die
|
|
Paare sind bewusst gewaehlt: arena/halle und skyline/lounge sind
|
|
inhaltlich benachbart, und sie liegen nie nebeneinander auf einer
|
|
Seite -- man sieht immer nur eins.
|
|
|
|
DREI GROESSEN JE SZENE:
|
|
uhd 2400 px fuer grosse Schirme (bis dahin war 1600 das
|
|
Maximum, und auf 2550 px sah man jede weiche Kante)
|
|
breit 1600 px der Normalfall
|
|
schmal 900 px hochkant fuers Handy, quadratisch beschnitten
|
|
===================================================================== */
|
|
const SZENEN = {
|
|
studio: "sm-wasserfall.png",
|
|
showbuehne: "sm-spiegel.png",
|
|
portal: "sm-ideen.png",
|
|
garage: "sm-glut.png",
|
|
arena: "sm-medaillons.png",
|
|
halle: "sm-medaillons.png",
|
|
wald: "sm-ahorn.png",
|
|
skyline: "sm-podest.png",
|
|
lounge: "sm-podest.png",
|
|
};
|
|
|
|
/* WIE STARK ABGEDUNKELT.
|
|
|
|
Die Bilder sind hell und farbig -- ueber ihnen steht Text. Der
|
|
Schleier der Startseite legt noch einmal nach, deshalb reicht hier
|
|
ein mittlerer Wert. Zu viel, und vom Motiv bleibt ein Schemen (genau
|
|
das war beim Tresorbild der Fehler: 0,62 uebernommen, und man sah
|
|
nichts mehr). Zu wenig, und pruef-buehne meldet zu Recht schlechten
|
|
Kontrast an echten Bildpunkten.
|
|
|
|
0,26 ist gemessen, nicht geraten -- siehe den Lauf von pruef-buehne
|
|
nach dem Bau. (Hier stand "0,42" und darunter 0.26: Der Wert wurde
|
|
gesenkt -- die Begruendung dafuer steht oben bei "Damit sinkt der
|
|
flache Schleier von 0,42 auf 0,26" --, und diese Zeile ist nicht
|
|
mitgegangen. Zwei Zahlen fuer dieselbe Sache, und nur eine
|
|
geaendert; genau das Muster, das mich in dieser Nacht schon
|
|
viermal aufgehalten hat.) */
|
|
const DUNKEL = 0.26;
|
|
|
|
console.log("\n--- Die neun Buehnen ---");
|
|
let gesamt = 0;
|
|
const gebaut = new Set();
|
|
for (const [szene, datei] of Object.entries(SZENEN)) {
|
|
const quelle = join(QUELLE, datei);
|
|
const zeile = [];
|
|
for (const [name, breite, guete] of [["uhd", 2400, 0.88], ["breit", 1600, 0.88]]) {
|
|
const n = await bauen({
|
|
quelle, ziel: join(ZIEL, `buehne-${szene}-${name}.webp`), breite,
|
|
dunkel: DUNKEL, guete,
|
|
});
|
|
gesamt += n;
|
|
zeile.push(`${name} ${Math.round(n / 1024)} KB`);
|
|
}
|
|
/* HOCHKANT: die MITTE des Bildes, hochkant beschnitten. Ein auf 9:16
|
|
gequetschtes Breitbild zeigt sonst nur Wand -- und das Logo, um das
|
|
es geht, steht in allen sieben Motiven mittig. */
|
|
const n = await bauen({
|
|
quelle, ziel: join(ZIEL, `buehne-${szene}-schmal.webp`), breite: 900,
|
|
/* HOCHKANT DARF NICHT SO HELL SEIN WIE BREIT (07.09.2026).
|
|
|
|
Der Glanz hebt die hellen Stellen an -- auf dem Handy trifft das
|
|
ausgerechnet die Bildmitte, und dort liegen die kleinsten Texte
|
|
der ganzen Anwendung. pruef-buehne hat es an echten Bildpunkten
|
|
gemeldet: "Ueberfaellig" kam auf 4,16:1 statt 4,5:1, gemessen auf
|
|
rgb(79, 76, 79) -- und dieser Punkt lag nicht auf dem Knopf,
|
|
sondern in der Luecke DANEBEN, also direkt auf dem Bild.
|
|
|
|
Drei Aenderungen am Knopf haben die Zahl auf die Stelle genau
|
|
nicht bewegt (dreimal exakt 4,16). Genau das ist das Zeichen, dass
|
|
man an der falschen Stelle dreht -- die Ursache lag im Bild, nicht
|
|
im Bedienelement.
|
|
|
|
Deshalb hier: mehr Schleier und weniger Glanz, aber NUR fuer die
|
|
Hochkant-Fassung. Auf dem grossen Schirm bleibt alles so hell und
|
|
glaenzend, wie Filipe es wollte -- dort ist der Text groesser und
|
|
die Kacheln decken mehr ab. */
|
|
ausschnitt: [0.22, 0, 0.56, 1], dunkel: DUNKEL + 0.12, guete: 0.87,
|
|
glanzStaerke: 0.20,
|
|
});
|
|
gesamt += n;
|
|
zeile.push(`schmal ${Math.round(n / 1024)} KB`);
|
|
gebaut.add(datei);
|
|
console.log(` ${szene.padEnd(11)} ${datei.padEnd(20)} ${zeile.join(" · ")}`);
|
|
}
|
|
console.log(`\n ${gebaut.size} Motive auf ${Object.keys(SZENEN).length} Buehnen, `
|
|
+ `${Math.round(gesamt / 1024)} KB gesamt`);
|
|
|
|
await browser.close();
|
|
console.log("\nFertig.");
|