Filipe: "eine community rolle und community kategorie, wo die community auch dann nur die sieht" · "wieso nur eine kachel — ich will mehrere, mit mehreren optionen" · "egal welche rolle hinzugefügt wird soll immer nur das sehen was ich erlaube". DIE DRITTE ADRESSE. treff.dogfather-universe.com bekommt eine eigene Weiche (treff-adresse.js), eine eigene Zugangswand, ein eigenes Manifest und acht eigene Bühnenbilder. Ohne sie stünde dort die Wand der Agentur: Spicy-Logo, "Creator Workspace", fünf Rollenkacheln mit den Namen Admin, Manager, Scout, Creator — die komplette Struktur des Unternehmens, vor der Anmeldung, für jeden mit der Adresse. Welche Wand zu welcher Tür gehört, steht jetzt in EINER Tafel (istFremdeWand) statt als Sonderfall im Code. Bei zwei Wänden war ein Sonderfall richtig; bei drei wären es drei geworden, bei vier sechs. SIEBEN BRETTER UND EIN ACHTES NUR FÜRS TEAM. Anschlagbrett, Was ansteht, Der Treff, Wunschliste, Highlights, Regeln & Hilfe, Mitmachen — dazu "Meldungen & Maßnahmen", das ausdrücklich NICHT in TREFF_BEREICHE steht: dort wäre es lautlos bei der Community gelandet und hätte ausgesehen wie die anderen sieben. WAS AUF DEN BRETTERN PASSIERT: "Will ich auch" (eine Stimme je Person, erzwungen durch den Schlüssel, nicht durch eine Prüfung) · Anheften, höchstens drei, gezählt IN der Transaktion · Freigabe vor Sichtbarkeit für Termine und Highlights, wobei Abwesenheit der Ruhezustand ist · Melden mit Pflicht-Grund (DSA Art. 16) · Entfernen mit Grund, der den Beitrag ÜBERLEBT (DSA Art. 17) · drei Stufen, gerechnet statt gespeichert · die Bannleiter: Modi bis 7 Tage, dauerhaft nur DogFather (seine Entscheidung) · "Mitmachen" wird zum Talent, genau einmal. Der Ausschluss wirkt in sitzungLesen() — also nicht nur beim Schreiben. Wer ausgeschlossen ist, ist weg, nicht stumm. ZWEI SEITEN MEHR. treff-regeln.html holt seine ZAHLEN vom Server (Mindestalter 18, Fristen, Anschläge) — ein Regeltext, der eine andere Zahl nennt als die Regel, ist schlimmer als keiner. rechte.html rechnet "Wer sieht was" aus derselben Tafel aus, aus der die Schranke ihre Entscheidung holt. Die Community steht dort bei 3 von 23. SECHS BEFUNDE, KEINEN HAT DAS LESEN GEFUNDEN: · Ein Modi kam über den verborgenen Zugang in den Treff — die Bedingung war durch Ausschluss formuliert und nahm die neue Wand automatisch mit · /api/personen gab einem Mitglied Namen und Rolle von DogFather · Ein DELETE mit Körper wird von Nodes HTTP-Parser mit einem leeren 400 abgewiesen, bevor Express ihn sieht (gemessen). Der Grund reist jetzt in der Adresse · Der Kommentar, der erklärt, warum die Zugangswand keine Rollennamen nennt, nannte selbst einen — in einer ausgelieferten Datei · Zwei Listen für "welche Seiten sind ohne Anmeldung offen". Sie stimmten, weil beide am selben Tag entstanden · Ein Portkonflikt, den ich selbst gebaut hatte (4391 gehört pruef-chat-aufloesen) Dazu zwei feste Zahlen abgeschafft (die 20 in pruef-rechtetafel, die Kachelzahl in pruef-treff) und zwei Werkzeuge, die jetzt SUCHEN statt aufzuzählen: Der Versionsstempel findet seine Manifeste selbst — das dritte hätte sonst keinen bekommen, und die neue App hätte bis zu vier Stunden lang das falsche Zeichen getragen. Das Bildwerkzeug baut beide Zugangswände aus einer Vorlage mit zwei Werten. Zwei neue Kacheltöne, gemessen statt gewählt: #2f2f7f (Abstand 31,4 in Lab) und #7c2771 (29,7). Das schwächste Paar der vorhandenen 34 liegt bei 24,9. Geprüft: treff 69 · treff-werkzeuge 60 · rollen 312 · start-ansicht 147 · crew-adresse 129 · sicht 84 · modi-verborgen 80 · haus-trennung 62 · bereiche-lesend 37 · css-klassen 30 · zwischenspeicher 21 · alle-wege 19 · rechtetafel 19 — alles grün. Co-Authored-By: Claude Opus 5 <[email protected]>
553 lines
24 KiB
JavaScript
553 lines
24 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");
|
|
|
|
/* ZWEI TUEREN, EIN WERKZEUG (11.09.2026).
|
|
|
|
Der Treff bekommt seine eigene Zugangswand, und sie braucht ihr
|
|
eigenes Motiv -- nicht weil sie anders aussehen soll, sondern weil
|
|
in diesem Bild "TEAM DOGI" EINGEBRANNT ist. Die Community traegt
|
|
diesen Namen nicht; auf ihrer Tuer stuende sonst der Name eines
|
|
Bereichs, zu dem sie nicht gehoert.
|
|
|
|
WARUM KEINE ZWEITE DATEI: Eine Kopie dieser 520 Zeilen haette die
|
|
drei Gegenproben mitkopiert -- und beim naechsten Nachbessern waere
|
|
genau eine der beiden verbessert worden. Hier aendern sich zwei
|
|
Dinge, also sind es zwei Werte und kein zweites Werkzeug.
|
|
|
|
DIE FARBE BLEIBT DIESELBE, und das ist eine Entscheidung, keine
|
|
Bequemlichkeit: Gemessen liegt der naechstbeste eigene Farbton
|
|
(Tannengruen) 60 Lab-Einheiten vom Marineblau entfernt -- er waere
|
|
also klar unterscheidbar gewesen. Nur haette der Treff damit wie ein
|
|
DRITTES Haus ausgesehen, neben Spicy Media und Team Dogi. Er ist
|
|
aber kein drittes Haus, sondern eine zweite Tuer in demselben:
|
|
"lila und babyblau ueberall" (Filipe, 10.09.2026). Unterschieden
|
|
werden die Tueren durch das WORT darauf, nicht durch die Farbe --
|
|
so, wie man es in einem Gebaeude auch macht.
|
|
|
|
node tools/crew-buehne.mjs
|
|
WAND_TEXT="DER TREFF" WAND_PRAEFIX=treff-gate node tools/crew-buehne.mjs
|
|
*/
|
|
const WAND_TEXT = process.env.WAND_TEXT || "TEAM DOGI";
|
|
const WAND_PRAEFIX = process.env.WAND_PRAEFIX || "crew-gate";
|
|
|
|
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. Der Name auf der Tuer --------------------------------- */
|
|
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
|
|
dem Namen 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(a.text, 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(a.text, 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, text: WAND_TEXT });
|
|
|
|
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, `${WAND_PRAEFIX}-${b}.webp`), r.puffer);
|
|
gebaut++;
|
|
|
|
if (r.rotAnteil > 0.01) maengel.push(
|
|
`${WAND_PRAEFIX}-${b}: noch ${(r.rotAnteil * 100).toFixed(1)} % rote Bildpunkte -- Umfaerbung hat nicht gegriffen`);
|
|
if (r.tafelMittel > 46) maengel.push(
|
|
`${WAND_PRAEFIX}-${b}: die Tafel ist zu hell (${r.tafelMittel.toFixed(1)}) -- die Karte braucht einen ruhigen Grund`);
|
|
if (r.tafelStreuung > 34) maengel.push(
|
|
`${WAND_PRAEFIX}-${b}: in der Tafel ist zu viel Muster (Streuung ${r.tafelStreuung.toFixed(1)})`);
|
|
|
|
console.log(` ${WAND_PRAEFIX}-${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, `${WAND_PRAEFIX}-hoch-${b}.webp`), r.puffer);
|
|
gebaut++;
|
|
if (r.rotAnteil > 0.01) maengel.push(
|
|
`${WAND_PRAEFIX}-hoch-${b}: noch ${(r.rotAnteil * 100).toFixed(1)} % rote Bildpunkte`);
|
|
console.log(` ${WAND_PRAEFIX}-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.`);
|