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]>
299 lines
14 KiB
JavaScript
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.");
|