Files
dogfather-universe/tools/gate-bauen.mjs
T
DogFatherGitandClaude Opus 5 aa1d05a5c0 Neues Anmeldebild -- die Karte sitzt IN der gemalten Tafel
Filipe: "integriere die zugangskachel links nach rechts und passe sie
perfekt an damit sie in dem neuen bild rechts perfekt und die
vorgemachte kachel passt."

Das neue Motiv (Spicy Media x DogFather) hat rechts eine leere Tafel mit
Neonrahmen. Die Anmeldekarte sitzt jetzt DARIN -- und zwar wirklich
darin, nicht ungefaehr in der Gegend.

DAS PROBLEM, DAS MAN LEICHT UEBERSIEHT: Das Bild liegt mit
`object-fit: cover` unter der Seite; je nach Fensterform schneidet der
Browser oben/unten oder links/rechts etwas ab. Ein `left: 58%` bezieht
sich aber auf das FENSTER. Auf genau einer Bildschirmgroesse saehe es
richtig aus und ueberall sonst falsch. Deshalb rechnet `.tafel-anker`
dieselbe Cover-Formel noch einmal nach und ist damit deckungsgleich mit
dem Bild -- Prozentwerte darin sind Prozent DES BILDES.

Die vier Zahlen sind GEMESSEN (tools/gate-bauen.mjs), nicht geschaetzt.
Zwei Anlaeufe standen daneben und haben sich selbst verraten:
  1. "Suche den leuchtenden Rahmen" fand den Mond, die roten Blueten und
     jede Spiegelung -- Ergebnis: die ganze rechte Bildhaelfte. Eine
     Messung, die das Offensichtliche zurueckgibt, hat nichts gemessen.
  2. "Suche die groesste dunkle Flaeche" fand die Breite richtig, aber
     94 % Hoehe: Ueber und unter der Tafel ist die Szene genauso
     schwarz.
Richtig ist der dritte Weg: vom Mittelpunkt der Tafel nach aussen
laufen, bis es hell ODER farbig wird -- auf diesem Weg liegt nichts
anderes, denn die Tafel ist leer. Danach eine Plausibilitaetspruefung,
die abbricht statt vier geratene Zahlen auszugeben.

DREI FEHLER IM EIGENEN ENTWURF, alle gemessen statt angesehen:
  * Die Karte hing 300 px unter dem Bildschirmrand. Auf `.tafel` liegt
    die Einblend-Animation, deren Endbild `transform: none` ist -- mein
    `translateY(-50%)` war damit wirkungslos. Zwei Wege, dasselbe
    Element zu verschieben, vertragen sich nicht.
  * Der Anker lag 3 % neben dem Bild: Das Bild traegt `scale(1.03)` als
    Reserve fuer die Parallaxe. Jetzt tragen beide dieselbe Verwandlung,
    und die Parallaxe laeuft ueber zwei CSS-Groessen am <body> -- so
    wandert die Karte mit, statt dass der Rahmen unter ihr wegrutscht.
  * Der Hochkant-Ausschnitt zeigte die LEERE Tafel: ein schwarzes
    Rechteck mit ein paar Saeulen. Auf dem Handy liegt die Karte ohnehin
    davor; dort gehoert das Logo hin. Jetzt faellt der Schriftzug
    SPICY MEDIA in den sichtbaren Streifen -- nachgerechnet, nicht
    probiert.

STARTSEITE: das Tresorbild als neuer Hintergrund. Die uebernommene
Abdunklung von 0,62 war zu viel (der Tresor ist von Haus aus dunkel) --
uebrig blieb ein Schemen. Und der "Leseweg" verdunkelte ausgerechnet die
MITTE: Bei den alten Szenen stand dort nichts, beim Tresor steht dort
die Tuer. Beides korrigiert und mit pruef-buehne an echten Bildpunkten
nachgemessen.

DABEI GEFUNDEN, ohne Zusammenhang mit dem Bild: Der abgeschaltete
"Ueberfaellig"-Filter kam auf 4,05:1 statt 4,5:1. Die Deckkraft zu
erhoehen half nichts -- die Pruefung rechnet mit der CSS-FARBE und dem
gemessenen Bildpunkt dahinter, und ein `opacity` am Elternknopf aendert
die Farbe nicht. Die Zahl blieb dreimal exakt gleich; genau das war der
Hinweis. Jetzt ein eigener, hellerer Ton. Der Kommentar daneben sagte
ohnehin, was gewollt ist: Man SOLL sehen, dass es null sind.

Und die vier Regeln von heute stehen in CLAUDE.md.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-06 23:44:10 +02:00

299 lines
14 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?
--------------------------------------------------------------------- */
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;
/* ZWEI ANLAEUFE WAREN FALSCH, und beide haben es selbst verraten.
(1) "Suche den LEUCHTENDEN Rahmen" fand den Mond, die roten Blueten
und jede Spiegelung im Wasser -- Ergebnis: die ganze rechte
Bildhaelfte. Eine Messung, die das Offensichtliche zurueckgibt,
hat nichts gemessen.
(2) "Suche die groesste DUNKLE Flaeche" fand die Breite richtig,
aber 94 % Hoehe: Ueber und unter der Tafel ist die Szene
genauso schwarz. Dunkelheit unterscheidet sie nicht.
Was sie WIRKLICH unterscheidet, ist der Rahmen -- aber nur, wenn
man ihn dort sucht, wo nichts anderes leuchtet: INNERHALB der
Tafel. Deshalb wird jetzt vom Mittelpunkt aus nach aussen gelaufen,
bis es hell wird. Auf diesem Weg liegt garantiert nichts anderes,
denn die Tafelflaeche ist leer.
Der Startpunkt ist dabei nicht die Bildmitte, sondern ein Punkt
sicher INNERHALB der Tafel -- gefunden ueber die dunkle Strecke aus
Anlauf (2), die waagerecht ja richtig lag. */
/* HELL ODER FARBIG. Nur nach Helligkeit zu suchen, hat die OBERE
Kante nicht gefunden: Sie ist ein duenner, dunkelroter Strich (der
rote Teil des Rahmens laeuft dort aus), waehrend die untere Kante
hell blau leuchtet. Der Rahmen ist aber an JEDER Stelle deutlich
FARBIG -- die Tafelflaeche daneben ist neutralgrau. Genau das
unterscheidet ihn, und zwar rundum. */
const hell = (x, y) => {
const p = (y * c.width + x) * 4;
const r = d[p], g2 = d[p + 1], b = d[p + 2];
const max = Math.max(r, g2, b), min = Math.min(r, g2, b);
return max > 118 || (max > 52 && max - min > 26);
};
const dunkel = (x, y) => {
const p = (y * c.width + x) * 4;
return Math.max(d[p], d[p + 1], d[p + 2]) < 62;
};
/* Grobe Waagerechte: die laengste dunkle Strecke auf halber Hoehe.
Sie liegt sicher in der Tafel -- daraus kommt der Startpunkt. */
function laengsteStrecke(von, bis, punkt) {
let bA = von, bB = von - 1, a = -1, luecke = 0;
for (let n = von; n <= bis; n++) {
if (punkt(n)) { if (a < 0) a = n; luecke = 0; }
else if (a >= 0 && ++luecke > 6) {
const b = n - luecke;
if (b - a > bB - bA) { bA = a; bB = b; }
a = -1; luecke = 0;
}
}
if (a >= 0 && bis - a > bB - bA) { bA = a; bB = bis; }
return [bA, bB];
}
const yMitte = Math.round(c.height * 0.45);
const [gLinks, gRechts] = laengsteStrecke(
Math.round(c.width * 0.45), c.width - 1, (x) => dunkel(x, yMitte));
const xStart = Math.round((gLinks + gRechts) / 2);
/** Vom Startpunkt aus in eine Richtung laufen, bis es hell wird.
* Verlangt werden ZWEI helle Punkte hintereinander -- ein einzelner
* waere Bildrauschen. */
function bisZumRahmen(x0, y0, dx, dy, grenze) {
let x = x0, y = y0, hintereinander = 0;
while (x >= 0 && y >= 0 && x < c.width && y < c.height) {
if (hell(x, y)) { if (++hintereinander >= 2) return dx ? x : y; }
else hintereinander = 0;
x += dx; y += dy;
if (dx && (dx > 0 ? x > grenze : x < grenze)) break;
if (dy && (dy > 0 ? y > grenze : y < grenze)) break;
}
return dx ? (dx > 0 ? grenze : grenze) : (dy > 0 ? grenze : grenze);
}
/* Auf mehreren Linien, damit ein Staubkorn nicht das Ergebnis
bestimmt -- genommen wird jeweils der mittlere Wert. */
const mitte = (w) => w.slice().sort((a, b) => a - b)[Math.floor(w.length / 2)];
const yProben = [0.35, 0.42, 0.5, 0.58, 0.65].map((f) => Math.round(c.height * f));
const links = mitte(yProben.map((y) => bisZumRahmen(xStart, y, -1, 0, Math.round(c.width * 0.42))));
const rechts = mitte(yProben.map((y) => bisZumRahmen(xStart, y, +1, 0, c.width - 1)));
const xProben = [0.3, 0.42, 0.5, 0.58, 0.7].map((f) => Math.round(links + (rechts - links) * f));
const oben = mitte(xProben.map((x) => bisZumRahmen(x, yMitte, 0, -1, 0)));
const unten = mitte(xProben.map((x) => bisZumRahmen(x, yMitte, 0, +1, c.height - 1)));
return { breite: c.width, hoehe: c.height, links, rechts, oben, unten };
}, 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;
const gut = 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.
Wahrscheinlich passt die Helligkeitsschwelle nicht `
+ `zum Bild -- in gate-bauen.mjs bei "dunkel()" nachsehen.`);
await browser.close();
process.exit(2);
}
}
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. */
async function bauen({ quelle, ziel, breite, ausschnitt = null, dunkel = 0, guete = 0.82,
seitenverhaeltnis = null }) {
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";
g.drawImage(i, qx, qy, qb, qh, 0, 0, zb, zh);
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 });
const puffer = Buffer.from(webp.split(",")[1], "base64");
writeFileSync(ziel, puffer);
return puffer.length;
}
const GATE = join(QUELLE, "gate-spicy-dogfather.jpeg");
const TRESOR = join(QUELLE, "szene-tresor.png");
console.log("\n--- Anmeldeseite ---");
for (const b of [960, 1280, 1600]) {
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.14, guete: 0.84,
});
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. Kein Logo, kein
* DogFather, nichts, woran man die Seite erkennt.
*
* Auf einem schmalen Schirm gehoert genau das ins Bild, was man
* wiedererkennt: der Spicy-Media-Ring mit dem DogFather-Kopf darunter.
* Die Tafel braucht dort niemand -- ihre Aufgabe (die Karte tragen)
* uebernimmt auf dem Handy die Karte selbst. */
for (const b of [440, 580]) {
const n = await bauen({
quelle: GATE, ziel: join(ZIEL_BUEHNE, `gate-hoch-${b}.webp`), breite: b,
/* Auch SENKRECHT beschnitten. Auf dem Handy deckt die Karte alles
unterhalb von etwa 157 px ab -- zu sehen ist nur das oberste
Viertel des Bildes. Ohne diesen Schnitt landet dort die
Spitze der Chili und sonst nichts. Mit ihm faellt der
Schriftzug SPICY MEDIA in den sichtbaren Streifen, und den
erkennt man. 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. */
ausschnitt: [0, 0.20, 0.52, 0.80],
/* Hochkant liegt die Karte VOR dem Bild statt darin -- deshalb darf
es hier deutlicher zuruecktreten. */
dunkel: 0.30, guete: 0.82,
});
console.log(` gate-hoch-${b}.webp ${Math.round(n / 1024)} KB`);
}
console.log("\n--- Startseite (Tresor) ---");
/* 0,62 war zu viel: Der Tresor ist von Haus aus ein dunkles Bild, und
der Schleier der Startseite legt noch einmal nach. Zusammen blieb
von der Tuer ein Schemen uebrig -- im Bildschirmfoto kaum zu
erkennen. Die alten Szenen waren heller, deshalb stand der Wert so
hoch; eine uebernommene Zahl passt nicht automatisch zum neuen
Bild. */
for (const [name, breite, dunkel] of [["breit", 1600, 0.34], ["schmal", 760, 0.40]]) {
const n = await bauen({
quelle: TRESOR, ziel: join(ZIEL, `buehne-dogfather-${name}.webp`), breite, dunkel,
guete: 0.8,
});
console.log(` buehne-dogfather-${name}.webp ${Math.round(n / 1024)} KB`);
}
await browser.close();
console.log("\nFertig.");