/* =================================================================== 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 }); const AUFTRAEGE = [ { /* Die Buehne fuer breite Schirme. Studio, beide Figuren aussen, dunkle Mitte. */ quelle: "studio.png", ziel: "buehne-breit.webp", breite: 1800, hoehe: 1013, ausschnitt: [0, 0, 1, 1], 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, }, { /* Fuer schmale Schirme ein hochkant-Ausschnitt aus derselben Szene -- ein auf 9:16 gequetschtes Breitbild zeigt nur noch Wand. */ quelle: "studio.png", ziel: "buehne-schmal.webp", breite: 800, hoehe: 1400, ausschnitt: [0.17, 0, 0.66, 1], 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(""); 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/.");