Files
dogfather-universe/tools/buehne-bauen.mjs
T
DogFatherGitandClaude Opus 5 fc4fde0f02 Buehnen hell, Flaechen dicht - und die Kopfleiste wieder kurz
DIE BUEHNEN WAREN ZU DUNKEL. Bei 0.42 Helligkeit sah man Schemen, nicht
die Szene -- "zu dunkel und sieht ganz komisch aus" traf es genau. Jetzt
0.66 mit etwas mehr Farbe (1.08), die Vignette schwaecher (0.52 statt
0.72). Die Bilder kommen durch.

MOEGLICH IST DAS NUR MIT DEM GEGENZUG: Alle Kacheln, Karten und Spalten
lagen bei 2 bis 3 Prozent WEISS -- also praktisch durchsichtig. Solange
der Hintergrund fast schwarz war, fiel das nicht auf; sobald er hell
wurde, lief das Bild mitten durch den Text. Jetzt gibt es --flaeche
(rgba(9,13,22,.78)), eine deckende dunkle Flaeche, und 44 Stellen in 16
Dateien holen sich ihre Farbe von dort.

Der Unterschied ist genau der, den Filipe beschrieben hat: Der
Hintergrund bleibt sichtbar, aber ZWISCHEN den Kacheln -- nicht hinter
der Schrift. Wer die Flaechen kuenftig dichter oder durchlaessiger will,
aendert eine einzige Zeile statt 44.

Die Kacheln selbst tragen ihren Farbton jetzt AUF der Flaeche
(color-mix mit --flaeche statt mit transparent) -- dadurch bleibt die
Farbe erkennbar, ohne dass die Kachel durchsichtig wird.

DIE KOPFLEISTE war so breit wie das ganze Fenster, obwohl der Text kurz
ist: .marke__text stand auf flex: 1 1 auto. Das war unsichtbar, solange
der Schriftzug nur Text war -- seit er einen Rahmen traegt, lief die
Pille quer durchs Bild. Jetzt flex: 0 1 auto (schrumpfen ja, wachsen
nein). Gemessen: 375 px statt 1280.

Kontrast an echten Bildpunkten auf allen sechs Seiten nachgemessen --
haelt trotz des viel helleren Bildes (schlechtester Wert 4,69:1).

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-01 11:20:02 +02:00

255 lines
11 KiB
JavaScript

/* ===================================================================
Baut den Hintergrund des Workspace aus den Marken-Bildern.
ZWEITER ANLAUF (01.09.2026). Der erste war schlecht, und Filipe hatte
recht: Ich hatte einen fremden Neon-Husky aus dem alten Bilderordner
genommen -- nicht DogFather. Ausserdem klebten zwei Bilder in den
Ecken, ohne dass daraus ein Bild geworden waere.
Jetzt richtig, aus den echten Marken-Bildern:
1. Eine BUEHNE ueber die ganze Flaeche: das Studio, in dem HasiDog
und DogFather stehen. Warum genau dieses von den fuenf: Die beiden
stehen weit aussen, die MITTE ist dunkel -- und dort steht der
Inhalt. Ein Bild mit Figuren in der Mitte waere hinter dem Text
gelandet.
2. Die beiden Figuren zusaetzlich freigestellt in den unteren Ecken,
aus den Einzelbildern. Sie stehen VOR der Buehne und sind das,
was man wiedererkennt.
Behandlung in beiden Faellen:
* stark abgedunkelt -- eine Buehne, kein Poster
* Mitte zusaetzlich verdunkelt (Vignette), dort steht der Text
* weiche Raender eingerechnet statt als Maske im Browser: eine
Maske ueber ein grosses Bild kostet bei jedem Bildaufbau Zeit
* WebP statt PNG -- aus 1,9 MB werden rund 60 KB
Gerechnet mit dem Browser, der ohnehin fuer die Pruefungen da ist.
Kein Bildprogramm, keine neue Abhaengigkeit, 0 EUR.
Aufrufen mit: node tools/buehne-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");
if (!existsSync(ZIEL)) mkdirSync(ZIEL, { recursive: true });
/* ---------------------------------------------------------------------
NEUN SZENEN, NICHT EINE.
Bis zum 01.09.2026 wurde aus den Bildern, die Filipe geschickt hat,
genau eines benutzt (das Studio) -- die uebrigen acht lagen unbenutzt
im Ordner. Zu Recht beanstandet: Er hatte sie ausdruecklich geschickt,
damit es VERSCHIEDENE Hintergruende gibt.
Jetzt wird aus jeder Szene eine Buehne, breit und schmal, und jede
Seite bekommt die, die zu ihr passt (Zuordnung in bereiche.js):
studio Startseite -- der neutrale Ort, alles beginnt hier
showbuehne Dashboard -- die grosse Buehne, alles im Blick
garage Aufgaben -- Werkstatt, hier wird gearbeitet
skyline Kalender -- Nacht ueber der Stadt, Zeit
lounge Calls -- Sitzecke, hier wird geredet
arena Dateien/Wissen -- Archiv hinter dem Portal
halle Profil/Personen -- die Halle, in der jemand steht
wald Start-Check/Scouting -- der Weg, den man erst sucht
portal Content/Reports -- Durchgang, hier entsteht etwas
Alle bekommen dieselbe Behandlung: abgedunkelt, Vignette in der Mitte
(dort steht der Text), oben ausgeblendet (dort sitzt die Kopfleiste).
Dadurch sehen sie verschieden aus und verhalten sich doch gleich --
was der eigentliche Punkt ist. Eine Buehne, die je Seite anders hell
ist, waere Unruhe statt Vielfalt.
--------------------------------------------------------------------- */
const SZENEN = [
/* [Name, Ausschnitt breit, Ausschnitt schmal]
Der schmale Ausschnitt ist je Szene ein anderer: Ein auf 9:16
gequetschtes Breitbild zeigt nur noch Wand. Gewaehlt wird jeweils
der Bereich, in dem die Figuren stehen. */
["studio", [0, 0, 1, 1], [0.17, 0, 0.66, 1]],
["showbuehne", [0, 0, 1, 1], [0.20, 0, 0.62, 1]],
["garage", [0, 0, 1, 1], [0.02, 0, 0.60, 1]],
["skyline", [0, 0, 1, 1], [0.00, 0, 0.58, 1]],
["lounge", [0, 0, 1, 1], [0.15, 0, 0.68, 1]],
["arena", [0, 0, 1, 1], [0.20, 0, 0.62, 1]],
["halle", [0, 0, 1, 1], [0.24, 0, 0.60, 1]],
["wald", [0, 0, 1, 1], [0.30, 0, 0.62, 1]],
["portal", [0, 0, 1, 1], [0.20, 0, 0.62, 1]],
];
const AUFTRAEGE = [
...SZENEN.flatMap(([name, breitA, schmalA]) => [
{
quelle: `szene-${name}.png`,
ziel: `buehne-${name}-breit.webp`,
breite: 1800, hoehe: 1013,
ausschnitt: breitA,
/* 01.09.2026 deutlich aufgehellt: von 0.42 auf 0.66, Farbe von
0.95 auf 1.08. Vorher war von den Bildern kaum etwas zu sehen --
"zu dunkel und sieht ganz komisch aus" traf es genau: Man sah
Schemen, nicht die Szene. Jetzt kommt sie durch.
Moeglich wird das nur, weil die Kacheln und Karten im selben
Zug DICHTER geworden sind (siehe --glas in gate.css). Der Text
steht dadurch auf eigener Flaeche statt direkt auf dem Bild --
das Bild darf hell sein, ohne die Lesbarkeit anzugreifen. */
helligkeit: 0.66, saettigung: 1.08, kontrast: 1.04,
/* Die Vignette macht die Mitte ruhig, in der der Inhalt steht.
Schwaecher als vorher (0.52 statt 0.72), weil die Flaechen die
Arbeit jetzt selbst machen. */
vignette: { x: 0.5, y: 0.42, rx: 0.5, ry: 0.66, staerke: 0.52 },
/* Oben ausblenden: dort sitzt die Kopfleiste. */
verlauf: [{ von: [0, 0], nach: [0, 0.14], stopps: [[0, 0.55], [1, 0]] }],
guete: 0.72,
},
{
quelle: `szene-${name}.png`,
ziel: `buehne-${name}-schmal.webp`,
breite: 800, hoehe: 1400,
ausschnitt: schmalA,
/* Auf dem Handy etwas zurueckhaltender als am Rechner: Der Text
nimmt dort fast die ganze Flaeche ein, das Bild traegt weniger. */
helligkeit: 0.56, saettigung: 1.02, kontrast: 1.03,
vignette: { x: 0.5, y: 0.45, rx: 0.72, ry: 0.64, staerke: 0.6 },
verlauf: [{ von: [0, 0], nach: [0, 0.12], stopps: [[0, 0.55], [1, 0]] }],
guete: 0.68,
},
]),
{
/* DogFather selbst -- der RICHTIGE Husky, im blauen Hoodie. Steht
unten rechts. */
quelle: "dogfather-husky.png",
ziel: "figur-dogfather.webp",
breite: 620, hoehe: 1120,
ausschnitt: [0.04, 0.06, 0.92, 0.92],
helligkeit: 0.72, saettigung: 1.0, kontrast: 1.03,
/* Nach unten und zur Seite offen lassen, oben und innen ausblenden --
so waechst er aus der Ecke heraus, statt als Ausschnitt zu kleben. */
verlauf: [
{ von: [0, 0], nach: [0, 0.3], stopps: [[0, 1], [1, 0]] },
{ von: [0.55, 0], nach: [0, 0], stopps: [[0, 0], [1, 0.9]] },
],
guete: 0.78,
},
{
/* HasiDog. Unten links. */
quelle: "hasidog-hase.png",
ziel: "figur-hasidog.webp",
breite: 560, hoehe: 1120,
ausschnitt: [0.05, 0.05, 0.9, 0.93],
helligkeit: 0.66, saettigung: 0.95, kontrast: 1.03,
verlauf: [
{ von: [0, 0], nach: [0, 0.3], stopps: [[0, 1], [1, 0]] },
{ von: [0.45, 0], nach: [1, 0], stopps: [[0, 0], [1, 0.9]] },
],
guete: 0.78,
},
{
/* Der Kopf aus dem DogFather-Logo -- als MASKE, nicht als Bild.
Die Vorlage liegt schwarz auf weiss; hier wird die Dunkelheit zur
Deckkraft. Ergebnis: eine Form, die sich im Stil beliebig
einfaerben laesst (mask-image + background). Ein fertig
eingefaerbtes Bild muesste man fuer jede Farbe neu bauen. */
quelle: "logo.png",
ziel: "marke-husky.webp",
breite: 96, hoehe: 96,
ausschnitt: [0.14, 0.03, 0.72, 0.68],
alsMaske: true,
guete: 0.9,
},
];
const { chromium } = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await chromium.launch();
const seite = await browser.newPage();
await seite.setContent("<html><body></body></html>");
for (const a of AUFTRAEGE) {
const roh = readFileSync(join(QUELLE, a.quelle));
const daten = `data:image/png;base64,${roh.toString("base64")}`;
const webp = await seite.evaluate(async ({ daten, a }) => {
const bild = new Image();
bild.src = daten;
await bild.decode();
const c = document.createElement("canvas");
c.width = a.breite; c.height = a.hoehe;
const g = c.getContext("2d");
const [ax, ay, aw, ah] = a.ausschnitt;
/* Als Maske: schwarz auf weiss wird zu Form auf durchsichtig. Fuer
jeden Bildpunkt wird die Helligkeit zur Durchsichtigkeit -- weiss
verschwindet, schwarz bleibt stehen. */
if (a.alsMaske) {
g.drawImage(bild,
bild.naturalWidth * ax, bild.naturalHeight * ay,
bild.naturalWidth * aw, bild.naturalHeight * ah,
0, 0, a.breite, a.hoehe);
const d2 = g.getImageData(0, 0, a.breite, a.hoehe);
for (let i = 0; i < d2.data.length; i += 4) {
const hell = (d2.data[i] * 0.299 + d2.data[i + 1] * 0.587 + d2.data[i + 2] * 0.114) / 255;
d2.data[i] = d2.data[i + 1] = d2.data[i + 2] = 255;
d2.data[i + 3] = Math.round((1 - hell) * 255);
}
g.putImageData(d2, 0, 0);
return c.toDataURL("image/webp", a.guete);
}
g.filter = `brightness(${a.helligkeit}) saturate(${a.saettigung}) contrast(${a.kontrast})`;
g.drawImage(bild,
bild.naturalWidth * ax, bild.naturalHeight * ay,
bild.naturalWidth * aw, bild.naturalHeight * ah,
0, 0, a.breite, a.hoehe);
g.filter = "none";
/* Vignette: dunkler Fleck in der Mitte. Er wird DRAUFGEMALT (nicht
ausradiert), damit dort, wo der Text steht, wirklich Ruhe ist. */
if (a.vignette) {
const v = a.vignette;
g.save();
g.translate(v.x * a.breite, v.y * a.hoehe);
g.scale(v.rx * a.breite, v.ry * a.hoehe);
const gr = g.createRadialGradient(0, 0, 0, 0, 0, 1);
gr.addColorStop(0, `rgba(5,7,13,${v.staerke})`);
gr.addColorStop(0.62, `rgba(5,7,13,${v.staerke * 0.72})`);
gr.addColorStop(1, "rgba(5,7,13,0)");
g.fillStyle = gr;
g.fillRect(-1.6, -1.6, 3.2, 3.2);
g.restore();
}
/* Weiche Raender: destination-out radiert dort, wo der Verlauf
deckend ist. */
g.globalCompositeOperation = "destination-out";
for (const v of (a.verlauf || [])) {
const gr = g.createLinearGradient(
v.von[0] * a.breite, v.von[1] * a.hoehe,
v.nach[0] * a.breite, v.nach[1] * a.hoehe);
for (const [pos, deckung] of v.stopps) gr.addColorStop(pos, `rgba(0,0,0,${deckung})`);
g.fillStyle = gr;
g.fillRect(0, 0, a.breite, a.hoehe);
}
g.globalCompositeOperation = "source-over";
return c.toDataURL("image/webp", a.guete);
}, { daten, a });
const puffer = Buffer.from(webp.split(",")[1], "base64");
writeFileSync(join(ZIEL, a.ziel), puffer);
console.log(`${a.ziel.padEnd(24)} ${String(Math.round(roh.length / 1024)).padStart(5)} KB`
+ ` -> ${String(Math.round(puffer.length / 1024)).padStart(4)} KB`);
}
await browser.close();
console.log("\nFertig. Die Dateien liegen in workspace/assets/img/.");