Files
dogfather-universe/tools/crew-buehne.mjs
T
DogFatherGitandClaude Opus 5 8bcbd83061 Die Modi-Seite gehoert Dogi und den Modis -- Wand, Zeichen, Reiter
Filipe zum Bildschirmfoto der Zugangswand auf crew.: "alles soll auf
dogfather und die modis perfektionniert werden auf der neuen modi seite."

Dort stand bis eben die gewohnte Wand: Spicy-Media-Logo, "Creator
Workspace", der Satz ueber Manager, Scouts und Creator, fuenf
Rollenkacheln. Fuer einen Modi war davon nichts richtig.

DIE KACHELREIHE FAELLT WEG, und das ist eine Verbesserung: Eine Reihe mit
genau einem Eintrag ist keine Auswahl, sondern eine Huerde -- man muesste
erst daraufdruecken, bevor das Codefeld etwas annimmt. gate.js prueft
jetzt, OB es Kacheln gibt, statt sie vorauszusetzen.

DIE BUEHNE IST AUS DEM VORHANDENEN MOTIV GEBAUT, nicht neu erfunden, und
das ist kein Sparen: Die Anmeldeseite ist eine MECHANIK. Rechts steht im
Bild eine leere Tafel, und gate.css setzt die Karte auf Hundertstel genau
dort hinein. Ein frei erfundenes Bild haette diese vier Zahlen
mitgenommen. Also dieselbe Szene, ueber den Mischmodus "color" ins Blau
umgefaerbt (hue-rotate haette Rot nach Cyan UND Blau nach Gelb gedreht),
Dogi anstelle des Spicy-Medaillons, "TEAM DOGI" darunter.
Gemessen: rote Bildpunkte 18,1 % -> 0,0 %, Karte auf 0 px genau.

NACH DER ANMELDUNG geht es weiter: kopf.js zieht Reitertitel und Zeichen
aus `ich.marke` nach -- "Aufgaben · Team Dogi" statt "· Spicy & Dogi", mit
dem eigenen Symbol. An einer Stelle statt in 30 HTML-Dateien.

DREI FUNDE, DIE NICHT AUS DEM KOPF KAMEN

1. DER KNOPF WAERE SCHLECHTER LESBAR GEWORDEN. Mein erstes Blau endete
   bei #4aa4cf -- weisse Schrift darauf: 2,8 zu 1. Der rot-blaue Verlauf,
   den er ersetzt, haelt ueber seine GANZE Laenge 4,65; er war offenbar
   genau darauf gebaut. Jetzt 5,10 zu 1, am fertigen Bildschirmfoto
   gemessen statt aus einer einzelnen Farbe hergeleitet.

2. DIE NEUE WAND WAR AUCH AUF workspace. ABRUFBAR. Sie liegt als Datei im
   selben Ordner. Aufgefallen ist das, weil der Wortleck-Test sie ueberhaupt
   las -- die Frage WARUM war die Antwort. Ausserhalb von crew. antwortet
   sie jetzt mit 404; auf den Pruefadressen bleibt sie erreichbar, sonst
   koennte die Pruefung sie nicht mehr oeffnen und waere gruen ohne etwas
   zu messen.

3. ZWEI GLEICHE KACHELN AUF DER STARTSEITE, seit dem letzten Deploy live.
   Die Team-Lage-Kachel von heute Mittag hatte Name, Zeichen, Ton UND
   Gruppe einer schon vorhandenen -- fuehrte aber woandershin. Gefunden
   von "jede Kachel hat ihre eigene Farbe (23 Farben auf 24 Kacheln)".
   Jetzt "Eingang", Gruppe Taeglich, neuer Ton 24 (#8a20cf, Abstand 38,5
   im CIELAB-Raum; reines Blau haette 52 gehabt und waere auf dunklem
   Grund am schlechtesten zu fokussieren). Eine Fehlermeldung zeigte
   dadurch auf die falsche Seite -- ebenfalls behoben.
   Und die Pruefung, die es fand, zaehlte nur bereiche.js: Vom Server
   angehaengte Kacheln kannte sie nicht. Sie fragt jetzt beide Quellen.

Der Wortleck-Test meldete ausserdem sieben Fundstellen -- allesamt
Kommentare, die ich beim Bauen selbst geschrieben hatte.

GEMESSEN
pruef-crew-adresse     103   pruef-crew-wand-bild    43 (neu)
pruef-modi-checkliste   59   pruef-modi-verborgen    75
pruef-rollen           245   pruef-zwischenspeicher  21
pruef-modi-wortleck      4   pruef-start-ansicht     gruen

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-10 13:59:54 +02:00

524 lines
23 KiB
JavaScript

/* ===================================================================
Die Buehne der Modi-App (crew.dogfather-universe.com, 10.09.2026).
Filipe: "alles soll auf dogfather und die modis perfektionniert
werden auf der neuen modi seite."
---------------------------------------------------------------------
WARUM AUS DEM VORHANDENEN MOTIV UND NICHT NEU
Die Anmeldeseite ist kein Bild mit Text darauf, sondern eine
MECHANIK: Rechts steht im Motiv eine leere Tafel mit Neonrahmen, und
gate.css setzt die Anmeldekarte in genau dieses Rechteck --
55,45 % / 13,15 % / 38,65 % / 63,85 %, gemessen von gate-bauen.mjs.
Ein frei erfundenes Bild haette diese vier Zahlen mitgenommen, und
die Karte laege irgendwo.
Also dasselbe Motiv, dieselbe Geometrie -- nur gehoert es jetzt Dogi:
1. BLAU STATT ROT. Die Szene wird ueber den Mischmodus "color"
umgefaerbt: Helligkeit und Zeichnung bleiben Bildpunkt fuer
Bildpunkt erhalten, nur Farbton und Saettigung kommen neu. Aus
dem roten Neon wird blaues, aus dem roten Ahorn blaues Laub.
Ein Filter wie hue-rotate haette Rot nach Cyan und Blau nach
Gelb gedreht -- also die eine Haelfte repariert und die andere
kaputtgemacht.
2. DOGI STATT DES SPICY-MEDAILLONS. Wo das Chili-Logo im Kreis
stand, steht jetzt der Husky aus assets/img/marke -- weich
eingeblendet, damit sein dunkler Grund in der Szene verschwindet
statt als Rechteck darauf zu liegen.
3. "TEAM DOGI" darunter, in Metall wie im uebrigen Markenbild.
---------------------------------------------------------------------
DREI GEGENPROBEN, DIE ALLE DREI SCHRITTE NACHWEISEN
Ein Bild, das entstanden ist, beweist nicht, dass darauf steht, was
draufstehen soll. Deshalb wird gemessen:
* ROT IST WEG. Der Anteil deutlich roter Bildpunkte muss von ueber
zehn Prozent im Original auf nahe null fallen. Faellt er nicht,
hat die Umfaerbung nicht gegriffen.
* DIE TAFEL IST FREI. Das Rechteck, in dem die Karte sitzt, muss
dunkel und ruhig bleiben. Male ich versehentlich hinein, sitzt
spaeter Text auf Muster.
* DOGI IST WIRKLICH DA. Dasselbe Bild wird ein zweites Mal OHNE
ihn gebaut und verglichen. Sind beide gleich, war er nie zu
sehen -- genau der Fehler, der beim App-Symbol am 09.09. eine
Fassung lang unbemerkt blieb.
Aufrufen mit: node tools/crew-buehne.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 SZENE = join(QUELLE, "gate-spicy-dogfather.jpeg");
const DOGI = join(QUELLE, "dogfather-husky.png");
const ZIEL = process.env.BUEHNE_ZIEL || join(WURZEL, "workspace", "assets", "img", "buehne");
for (const q of [SZENE, DOGI]) {
if (!existsSync(q)) { console.error(`Fehlt: ${q}`); process.exit(1); }
}
const alsDaten = (p) => {
const art = p.endsWith(".png") ? "png" : "jpeg";
return `data:image/${art};base64,` + readFileSync(p).toString("base64");
};
/* Die Tafel, in der die Anmeldekarte sitzt. Wortgleich mit gate.css --
hier nur, um sie FREIZUHALTEN, nicht um sie zu zeichnen. */
const TAFEL = { x: 0.5545, y: 0.1315, b: 0.3865, h: 0.6385 };
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({ deviceScaleFactor: 1 })).newPage();
await seite.setViewportSize({ width: 400, height: 300 });
if (!existsSync(ZIEL)) mkdirSync(ZIEL, { recursive: true });
const szeneDaten = alsDaten(SZENE);
const dogiDaten = alsDaten(DOGI);
/**
* Baut eine Fassung und gibt sie als WebP-Puffer zurueck.
* `ohneDogi` ist nur fuer die Gegenprobe da.
*/
async function bauen({ breite, ausschnitt = null, guete = 0.92, dunkel = 0.08,
vignette = 0.26, ohneDogi = false }) {
const roh = await seite.evaluate(async (a) => {
const laden = async (d) => { const i = new Image(); i.src = d; await i.decode(); return i; };
const szene = await laden(a.szene);
const dogi = await laden(a.dogi);
const qx = a.ausschnitt ? a.ausschnitt[0] * szene.width : 0;
const qy = a.ausschnitt ? a.ausschnitt[1] * szene.height : 0;
const qb = a.ausschnitt ? a.ausschnitt[2] * szene.width : szene.width;
const qh = a.ausschnitt ? a.ausschnitt[3] * szene.height : szene.height;
const zb = a.breite;
const zh = Math.round(zb * (qh / qb));
const c = document.createElement("canvas");
c.width = zb; c.height = zh;
const g = c.getContext("2d");
g.imageSmoothingQuality = "high";
/* ---- 1. Die Szene, etwas kraeftiger gezeichnet ---------------- */
g.filter = "contrast(1.12) saturate(1.10)";
g.drawImage(szene, qx, qy, qb, qh, 0, 0, zb, zh);
g.filter = "none";
/* ---- 2. UMFAERBEN --------------------------------------------
"color" nimmt Farbton und Saettigung von der Farbe und die
Helligkeit vom Bild darunter. Deshalb bleibt jede Kante, jeder
Nebelschwaden und der ganze Glanz erhalten -- es wechselt nur
die Farbe. Zwei Lagen: unten ein tiefes Marineblau fuer die
Flaeche, darueber ein Verlauf, damit oben das kalte Licht und
unten das waermere Wasserblau steht und die Szene nicht flach
wird. */
g.globalCompositeOperation = "color";
g.fillStyle = "#1d4f80";
g.fillRect(0, 0, zb, zh);
const farbe = g.createLinearGradient(0, 0, zb * 0.35, zh);
farbe.addColorStop(0, "#2f7fc4");
farbe.addColorStop(0.55, "#1d4f80");
farbe.addColorStop(1, "#123a63");
g.globalAlpha = 0.65;
g.fillStyle = farbe;
g.fillRect(0, 0, zb, zh);
g.globalAlpha = 1;
g.globalCompositeOperation = "source-over";
/* ---- 3. Das alte Medaillon zudecken --------------------------
Ein weicher, dunkler Kreis genau dort, wo das Chili-Logo stand.
Weich, nicht hart: Eine scharfe Kante saehe man sofort als
Flicken. */
const mx = zb * 0.295, my = zh * 0.375, mr = zh * 0.62;
/* GANZ DECKEND BIS 0,66 -- nicht 0,90.
Beim ersten Versuch stand hier 0,97 aussen abfallend. Zehn
Prozent Rest genuegen: Im Bildschirmfoto las man "SP...DIA"
quer durch Dogi hindurch und darunter den alten
DOGFATHER-Schriftzug ueber seinem Hoodie. Hoher Kontrast
braucht fast nichts, um lesbar zu bleiben.
Deshalb bis weit hinter beide Schriftzuege voll deckend und
erst dann weich auslaufen. Der Kreis wird dadurch gross und
dunkel -- was darin fehlt, bringen Lichtschein und Dogi
zurueck. */
const deckel = g.createRadialGradient(mx, my, 0, mx, my, mr);
deckel.addColorStop(0, "rgba(4,10,18,1)");
deckel.addColorStop(0.66, "rgba(4,10,18,1)");
deckel.addColorStop(0.84, "rgba(4,10,18,0.58)");
deckel.addColorStop(1, "rgba(4,10,18,0)");
g.fillStyle = deckel;
g.fillRect(0, 0, zb, zh);
/* Ein kalter Lichtschein an derselben Stelle -- sonst ist dort
jetzt bloss ein Loch. */
const schein = g.createRadialGradient(mx, my * 1.05, 0, mx, my * 1.05, mr * 0.92);
schein.addColorStop(0, "rgba(84,168,235,0.30)");
schein.addColorStop(0.45, "rgba(52,120,190,0.15)");
schein.addColorStop(1, "rgba(24,60,110,0)");
g.fillStyle = schein;
g.fillRect(0, 0, zb, zh);
/* ---- 4. DOGI ---------------------------------------------------
Er steht auf dem Podest, das im Motiv ohnehin da ist. Sein
eigener Grund ist dunkel; damit daraus kein Rechteck wird, wird
er durch eine weiche Ellipse maskiert und mit "lighter"
aufgetragen -- Dunkles bleibt dadurch unsichtbar, Helles
(Fell, Hoodie, Augen) kommt heraus. */
let dogiKasten = null;
if (!a.ohneDogi) {
const dh = zh * 0.84;
const db = dh * ((dogi.width * 0.82) / (dogi.height * 0.91));
const dx = zb * 0.295 - db / 2;
const dy = zh * 0.90 - dh;
dogiKasten = [dx, dy, db, dh];
const h = document.createElement("canvas");
h.width = Math.max(1, Math.round(db)); h.height = Math.max(1, Math.round(dh));
const gh = h.getContext("2d");
gh.imageSmoothingQuality = "high";
/* SEIN GRUND MUSS AUF SCHWARZ, bevor er aufgetragen wird.
"lighter" ADDIERT. Schwarz addiert nichts und bleibt deshalb
unsichtbar -- sein Grund ist aber nicht schwarz, sondern ein
warmer brauner Schein. Der wurde mitaddiert, und genau das war
der helle Kasten um ihn herum im ersten Versuch.
Kontrast druckt den Schein nach unten, ohne ihn wegzuschneiden;
eine harte Schwelle haette sein schwarzes Fell gleich mit
erwischt. Dazu etwas Blau ueber den Mischmodus "color", damit
sein warmes Streiflicht zur kalten Szene passt -- nur zu einem
Drittel, sonst verliert das Fell seine Zeichnung. */
gh.filter = "contrast(1.42) brightness(1.04) saturate(1.12)";
/* NUR SEINE SILHOUETTE, nicht das ganze Blatt. In der Vorlage
steht er mit reichlich Luft ringsum, und in dieser Luft liegt
der warme Schein, der als Kasten sichtbar wurde. Gemessen an
der Vorlage steht er zwischen 11 und 93 Prozent der Breite und
zwischen 8 und 99 Prozent der Hoehe (seine Rute reicht weiter
nach rechts, als es aussieht -- beim ersten Zuschnitt war sie
ab und hinterliess eine senkrechte Kante) -- alles ausserhalb ist
nur Grund und wird gar nicht erst mitgenommen. */
gh.drawImage(dogi,
dogi.width * 0.11, dogi.height * 0.08,
dogi.width * 0.82, dogi.height * 0.91,
0, 0, h.width, h.height);
gh.filter = "none";
gh.globalCompositeOperation = "color";
gh.globalAlpha = 0.34;
gh.fillStyle = "#2a6ea8";
gh.fillRect(0, 0, h.width, h.height);
gh.globalAlpha = 1;
gh.globalCompositeOperation = "source-over";
/* IHN UEBER DIE HELLIGKEIT FREISTELLEN.
Kontrast allein hat den Kasten nicht wegbekommen: Der warme
Dunst um seine Pfoten liegt bei einer Helligkeit von 40 bis 70
-- "lighter" addiert das sichtbar dazu, und im Bildschirmfoto
stand ein hellerer Rahmen um ihn.
Also bekommt jeder Bildpunkt seine Deckkraft aus seiner
HELLIGKEIT: unter 42 ganz weg, ab 88 ganz da, dazwischen ein
weicher Uebergang (die uebliche glatte Kurve, damit keine
Stufenkante entsteht). 26 war der erste Versuch und zu tief --
sein Streiflicht liegt bei 50 bis 75 und blieb stehen.
DASS SEIN SCHWARZES FELL DABEI DURCHSICHTIG WIRD, IST KEIN
VERLUST: Unter "lighter" traegt Schwarz ohnehin nichts bei --
es ist mit und ohne Deckkraft gleich unsichtbar. Und hinter
ihm liegt der deckende Kreis, also ist dort ohnehin Nacht.
Sein Fell liest sich deshalb weiter als Fell, nicht als Loch. */
const bd = gh.getImageData(0, 0, h.width, h.height);
const px = bd.data;
for (let i = 0; i < px.length; i += 4) {
const L = 0.2126 * px[i] + 0.7152 * px[i + 1] + 0.0722 * px[i + 2];
let t = (L - 42) / 46;
t = t < 0 ? 0 : t > 1 ? 1 : t;
px[i + 3] = Math.round(px[i + 3] * (t * t * (3 - 2 * t)));
}
gh.putImageData(bd, 0, 0);
/* Die Maske bleibt als Netz darunter -- sie faengt, was die
Helligkeit stehen laesst (etwa eine helle Ecke im Grund). */
/* SIE MUSS EINE ELLIPSE SEIN, KEIN KREIS -- und das war der
eigentliche Fehler.
Hier stand ein Kreis mit dem Radius `max(Breite, Hoehe) * 0.56`.
Seine Bildflaeche ist aber hochkant (rund 1 zu 2): Der Radius
richtete sich also nach der HOEHE und reichte seitlich weit
ueber den Rand hinaus. Links und rechts wurde damit ueberhaupt
nie ausgeblendet -- genau dort blieb im Bildschirmfoto der
helle Kasten stehen, und zwar durch alle Nachbesserungen an der
Schwelle hindurch. Man sieht einer solchen Zeile nicht an, dass
sie eine ganze Achse auslaesst.
Ueber `scale` wird aus dem Kreis eine Ellipse, die zur Flaeche
passt: waagerecht 0,52 der Breite, senkrecht 0,52 der Hoehe.
Beide Achsen blenden dadurch vor dem Rand aus. */
gh.globalCompositeOperation = "destination-in";
gh.save();
/* MITTIG UND KNAPP UNTER DER HALBEN BREITE.
Vorher: Mitte bei 0,52 der Hoehe, Radius 0,52 -- unten reichte
die Ellipse damit bis 1,04 und blendete am unteren Rand NIE
aus. Die Randprobe hat genau das gemeldet (4,1 % statt unter
3 %), bevor es jemand gesehen hat. Mittig und 0,50 laesst beide
Kanten sauber auslaufen; dass seine Pfoten unten einen Hauch
weicher werden, ist kein Verlust -- dort steht im Motiv ohnehin
Nebel ueber dem Wasser. */
gh.translate(h.width / 2, h.height * 0.50);
gh.scale(1, h.height / h.width);
const r = h.width * 0.50;
const m = gh.createRadialGradient(0, 0, 0, 0, 0, r);
m.addColorStop(0, "rgba(0,0,0,1)");
m.addColorStop(0.62, "rgba(0,0,0,1)");
m.addColorStop(0.85, "rgba(0,0,0,0.45)");
m.addColorStop(1, "rgba(0,0,0,0)");
gh.fillStyle = m;
gh.fillRect(-h.width, -h.height, h.width * 2, h.height * 2);
gh.restore();
g.save();
g.globalCompositeOperation = "lighter";
g.globalAlpha = 1;
g.filter = "saturate(1.15) contrast(1.06)";
g.drawImage(h, dx, dy, db, dh);
g.restore();
}
/* ---- 5. "TEAM DOGI" ------------------------------------------- */
const schriftHoehe = Math.round(zh * 0.072);
g.save();
g.textAlign = "center";
g.textBaseline = "alphabetic";
g.font = `800 ${schriftHoehe}px "Segoe UI", system-ui, sans-serif`;
/* 0,955 lag halb unter dem Bildrand -- im Bildschirmfoto war von
"TEAM DOGI" die untere Haelfte abgeschnitten. Jetzt so weit
herauf, dass die Unterlaengen noch auf dem Wasser stehen. */
const ty = zh * 0.915;
/* Erst ein kalter Schein darunter, dann die Schrift in Metall --
so liest sie sich vor dem dunklen Wasser, ohne zu blenden. */
g.shadowColor = "rgba(70,150,220,0.85)";
g.shadowBlur = schriftHoehe * 0.9;
g.fillStyle = "rgba(10,25,45,0.9)";
g.fillText("TEAM DOGI", mx, ty);
g.shadowBlur = 0;
const metall = g.createLinearGradient(0, ty - schriftHoehe, 0, ty);
metall.addColorStop(0, "#ffffff");
metall.addColorStop(0.45, "#cfe2f2");
metall.addColorStop(0.55, "#7ea6c6");
metall.addColorStop(1, "#e8f2fa");
g.fillStyle = metall;
g.fillText("TEAM DOGI", mx, ty);
g.restore();
/* ---- 6. Vignette und ein Hauch Dunkel ------------------------- */
if (a.vignette > 0) {
const v = g.createRadialGradient(zb / 2, zh / 2, Math.min(zb, zh) * 0.28,
zb / 2, zh / 2, Math.max(zb, zh) * 0.72);
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);
}
if (a.dunkel > 0) {
g.fillStyle = `rgba(0,0,0,${a.dunkel})`;
g.fillRect(0, 0, zb, zh);
}
/* ---- Messwerte gleich mit zurueckgeben ------------------------ */
const d = g.getImageData(0, 0, zb, zh).data;
let rot = 0, alle = 0;
for (let i = 0; i < d.length; i += 4) {
alle++;
if (d[i] > d[i + 2] + 25 && d[i] > 60) rot++;
}
/* Die Tafel: dunkel und ruhig? */
const tx = Math.round(a.tafel.x * zb), ty2 = Math.round(a.tafel.y * zh);
const tb = Math.round(a.tafel.b * zb), th = Math.round(a.tafel.h * zh);
const t = g.getImageData(tx, ty2, tb, th).data;
let s = 0, s2 = 0, m2 = 0;
for (let i = 0; i < t.length; i += 4) {
const hh = 0.2126 * t[i] + 0.7152 * t[i + 1] + 0.0722 * t[i + 2];
s += hh; s2 += hh * hh; m2++;
}
const mittel = s / m2;
return {
bild: c.toDataURL("image/webp", a.guete),
rotAnteil: rot / alle,
tafelMittel: mittel,
tafelStreuung: Math.sqrt(s2 / m2 - mittel * mittel),
breite: zb, hoehe: zh,
dogiKasten,
};
}, { szene: szeneDaten, dogi: dogiDaten, breite, ausschnitt, guete, dunkel, vignette,
ohneDogi, tafel: TAFEL });
return { ...roh, puffer: Buffer.from(roh.bild.split(",")[1], "base64") };
}
/* Wie rot ist das ORIGINAL? Ohne diesen Wert waere "wenig Rot" eine
Behauptung ohne Bezug. */
const rotVorher = await seite.evaluate(async (d) => {
const i = new Image(); i.src = d; await i.decode();
const c = document.createElement("canvas");
c.width = 800; c.height = Math.round(800 * i.height / i.width);
const g = c.getContext("2d");
g.drawImage(i, 0, 0, c.width, c.height);
const p = g.getImageData(0, 0, c.width, c.height).data;
let rot = 0, alle = 0;
for (let k = 0; k < p.length; k += 4) {
alle++;
if (p[k] > p[k + 2] + 25 && p[k] > 60) rot++;
}
return rot / alle;
}, szeneDaten);
console.log(`\nRot im Original: ${(rotVorher * 100).toFixed(1)} %`);
/* Anteil sichtbar verschiedener Bildpunkte zwischen zwei Fassungen.
*
* `rahmen` schraenkt den Vergleich auf den RANDSTREIFEN eines
* Rechtecks ein -- damit wird der Kasten messbar, der zwei
* Nachbesserungen lang sichtbar blieb, weil die Maske ein Kreis statt
* einer Ellipse war. Ein Blick auf das ganze Bild sagt nur "Dogi ist
* da"; erst der Blick auf seinen Rand sagt, ob man SEINE FLAECHE sieht
* statt ihn.
*/
async function unterschied(a, b, rahmen = null) {
return await seite.evaluate(async ([x, y, rr]) => {
const laden = async (b64) => {
const i = new Image(); i.src = "data:image/webp;base64," + b64; await i.decode();
const c = document.createElement("canvas");
c.width = i.width; c.height = i.height;
c.getContext("2d").drawImage(i, 0, 0);
return c.getContext("2d").getImageData(0, 0, c.width, c.height).data;
};
const breiteVon = async (b64) => {
const i = new Image(); i.src = "data:image/webp;base64," + b64; await i.decode();
return i.width;
};
const [p, q, breite] = [await laden(x), await laden(y), await breiteVon(x)];
if (p.length !== q.length) return -1;
let anders = 0, m = 0;
for (let i = 0; i < p.length; i += 4) {
if (rr) {
const px = (i / 4) % breite, py = Math.floor((i / 4) / breite);
const [bx, by, bb, bh] = rr;
const drin = px >= bx && px < bx + bb && py >= by && py < by + bh;
if (!drin) continue;
/* Nur der Randstreifen: innerhalb der Flaeche, aber ausserhalb
ihrer um 8 Prozent eingerueckten Mitte. */
const ix = bx + bb * 0.08, iy = by + bh * 0.08;
if (px >= ix && px < bx + bb * 0.92 && py >= iy && py < by + bh * 0.92) continue;
}
const w = Math.max(Math.abs(p[i] - q[i]), Math.abs(p[i + 1] - q[i + 1]),
Math.abs(p[i + 2] - q[i + 2]));
if (w > 8) anders++;
m++;
}
return m === 0 ? -1 : anders / m;
}, [a.toString("base64"), b.toString("base64"), rahmen]);
}
const maengel = [];
let gebaut = 0;
console.log("\n--- Quer ---");
for (const b of [960, 1280, 1600, 1920, 2560]) {
const r = await bauen({ breite: b, guete: 0.93, dunkel: 0.08, vignette: 0.26 });
writeFileSync(join(ZIEL, `crew-gate-${b}.webp`), r.puffer);
gebaut++;
if (r.rotAnteil > 0.01) maengel.push(
`crew-gate-${b}: noch ${(r.rotAnteil * 100).toFixed(1)} % rote Bildpunkte -- Umfaerbung hat nicht gegriffen`);
if (r.tafelMittel > 46) maengel.push(
`crew-gate-${b}: die Tafel ist zu hell (${r.tafelMittel.toFixed(1)}) -- die Karte braucht einen ruhigen Grund`);
if (r.tafelStreuung > 34) maengel.push(
`crew-gate-${b}: in der Tafel ist zu viel Muster (Streuung ${r.tafelStreuung.toFixed(1)})`);
console.log(` crew-gate-${b}.webp`.padEnd(30)
+ `${(r.puffer.length / 1024).toFixed(0)} KB · ${r.breite}x${r.hoehe}`
+ ` · Rot ${(r.rotAnteil * 100).toFixed(2)} %`
+ ` · Tafel ${r.tafelMittel.toFixed(1)}/${r.tafelStreuung.toFixed(1)}`);
}
/* HOCHKANT: der Ausschnitt um DOGI, nicht um die Tafel -- die ist leer,
und auf dem Handy liegt die Karte ohnehin davor. Genau derselbe
Gedanke wie beim Original, nur ist die linke Seite jetzt er. */
console.log("\n--- Hoch ---");
for (const b of [440, 580, 900]) {
const r = await bauen({
breite: b, /* HOCHKANT UND UM DOGI HERUM. Erster Versuch war
[0.03, 0.06, 0.50, 0.94] -- fast quadratisch. Auf einem Handy
(412 zu 915) schneidet `cover` davon die Seiten weg, und im
Bildschirmfoto stand Dogi halb ausserhalb, dafuer der leere
Deckkreis mitten im Bild. Jetzt ein hochkantiger Streifen, dessen
Mitte auf seiner Mitte liegt (0,13 bis 0,45 -- Mitte 0,29, er
steht bei 0,295). */
ausschnitt: [0.13, 0.02, 0.32, 0.98], guete: 0.88,
dunkel: 0.22, vignette: 0.30,
});
writeFileSync(join(ZIEL, `crew-gate-hoch-${b}.webp`), r.puffer);
gebaut++;
if (r.rotAnteil > 0.01) maengel.push(
`crew-gate-hoch-${b}: noch ${(r.rotAnteil * 100).toFixed(1)} % rote Bildpunkte`);
console.log(` crew-gate-hoch-${b}.webp`.padEnd(30)
+ `${(r.puffer.length / 1024).toFixed(0)} KB · ${r.breite}x${r.hoehe}`
+ ` · Rot ${(r.rotAnteil * 100).toFixed(2)} %`);
}
/* ---- DIE GEGENPROBE: ist Dogi wirklich zu sehen? ------------------- */
console.log("\n--- Gegenprobe ---");
const mit = await bauen({ breite: 1280 });
const ohne = await bauen({ breite: 1280, ohneDogi: true });
const anders = await unterschied(mit.puffer, ohne.puffer);
if (anders < 0) {
maengel.push("Gegenprobe nicht durchfuehrbar -- die beiden Fassungen haben verschiedene Groessen");
} else if (anders < 0.03) {
maengel.push(`Dogi ist nicht zu sehen: mit und ohne unterscheiden sich nur um `
+ `${(anders * 100).toFixed(2)} %`);
}
console.log(` mit Dogi gegen ohne Dogi: ${(anders * 100).toFixed(2)} % der Bildpunkte anders`);
/* SIEHT MAN IHN ODER SEINE FLAECHE? Am Rand seines Bildbereichs darf
fast nichts passieren -- sonst liegt dort eine sichtbare Kante. */
const randAnders = await unterschied(mit.puffer, ohne.puffer, mit.dogiKasten);
if (randAnders < 0) {
maengel.push("Randprobe nicht durchfuehrbar -- Dogis Flaeche ist nicht bekannt");
} else if (randAnders > 0.03) {
maengel.push(`Am Rand von Dogis Flaeche sind ${(randAnders * 100).toFixed(1)} % der `
+ `Bildpunkte anders -- man sieht seinen Kasten statt ihn`);
}
console.log(` davon am Rand seiner Flaeche: ${(randAnders * 100).toFixed(2)} %`
+ ` (ueber 3 % waere ein sichtbarer Kasten)`);
console.log(` Rot vorher ${(rotVorher * 100).toFixed(1)} % -> nachher `
+ `${(mit.rotAnteil * 100).toFixed(2)} %`);
await browser.close();
if (gebaut !== 8) {
console.error(`\nFEHLER: ${gebaut} von 8 Fassungen erzeugt.`); process.exit(1);
}
if (rotVorher < 0.05) {
console.error(`\nFEHLER: Das Original ist gar nicht rot (${(rotVorher * 100).toFixed(1)} %) -- `
+ `dann misst die Rot-Probe nichts. Quelle pruefen.`); process.exit(1);
}
if (maengel.length) {
console.error("\nFEHLER:\n " + maengel.join("\n ")); process.exit(1);
}
console.log(`\n${gebaut} Fassungen erzeugt. Das Rot ist weg, die Tafel bleibt frei,`
+ ` und Dogi ist nachweisbar zu sehen.`);