Filipe hat neun Bilder geschickt, damit es VERSCHIEDENE Hintergruende gibt. Benutzt wurde davon genau eines (das Studio), die uebrigen acht lagen unbenutzt im Ordner -- lounge.png und skyline.png sogar schon fertig kopiert. Zu Recht beanstandet. Jetzt wird aus jeder Szene eine Buehne, breit und schmal, und jede Seite bekommt die, die zu ihr passt: studio Startseite der neutrale Ort, alles beginnt hier showbuehne Dashboard, Review die grosse Buehne, alles im Blick garage Aufgaben, Technik Werkstatt, hier wird gearbeitet skyline Kalender Nacht ueber der Stadt, Zeit lounge Calls, Community 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, LIVE Durchgang, hier entsteht etwas Die Zuordnung steht in bereiche.js -- derselben Liste, aus der schon Zeichen und Farbton kommen. Eine Seite traegt damit dreierlei aus einer einzigen Quelle: Farbe, Zeichen und Buehne. Sie koennen nicht auseinanderlaufen. Alle neun bekommen dieselbe Behandlung (abgedunkelt auf 0.42, Vignette in der Mitte, wo der Text steht, oben ausgeblendet, wo die Kopfleiste sitzt). Sie sehen verschieden aus und verhalten sich gleich -- eine Buehne, die je Seite anders hell waere, waere Unruhe statt Vielfalt. Der schmale Ausschnitt ist je Szene ein anderer, weil ein auf 9:16 gequetschtes Breitbild nur noch Wand zeigt; genommen wird der Bereich, in dem die Figuren stehen. 18 Dateien, zusammen 580 KB -- pro Seite werden davon zwei geladen (breit ODER schmal), also 16 bis 48 KB. Die Rohbilder waren 2,4 MB je Stueck. Kontrast auf allen sechs geprueften Seiten an echten Bildpunkten nachgemessen: haelt (schlechtester Wert 4,59:1). FUND: Die Buehnenpruefung suchte nach "buehne-breit" und fand die neuen Namen nicht mehr -- sie meldete "die Buehne liegt im Hintergrund: FEHL", obwohl sie da war. Die Seitenpruefung sieht jetzt nach, dass jede Seite ihre Szene hat UND dass das Bild wirklich ankommt: Ein data-buehne ohne passende CSS-Regel waere still wirkungslos. Co-Authored-By: Claude Opus 5 <[email protected]>
243 lines
10 KiB
JavaScript
243 lines
10 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,
|
|
helligkeit: 0.42, saettigung: 0.95, kontrast: 1.02,
|
|
/* Die Vignette macht genau den Streifen dunkel, in dem der Inhalt
|
|
steht -- ohne sie waere Text ueber dem Logo unlesbar. */
|
|
vignette: { x: 0.5, y: 0.42, rx: 0.46, ry: 0.62, staerke: 0.72 },
|
|
/* 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,
|
|
helligkeit: 0.34, saettigung: 0.9, kontrast: 1.02,
|
|
vignette: { x: 0.5, y: 0.45, rx: 0.7, ry: 0.6, staerke: 0.78 },
|
|
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/.");
|