/* =================================================================== Stellt die beiden echten Marken frei: den DogFather-Husky und das Spicy-Media-Siegel. ANLASS (08.09.2026). Im Workspace lag `marke-husky.webp` als Maske hinter dem Schriftzug und in der Mitte des Zentrale-Rings. Nachgemessen war die Datei NICHT freigestellt: 0,3 % durchsichtige Punkte, alle vier Ecken mit Alpha 255. Als Maske ergibt so etwas einen KASTEN mit einem Husky darin -- egal wie richtig die CSS-Zeile ist. Filipe hat daraufhin die beiden Originale geliefert. WOHIN WAS KOMMT, und warum nicht andersherum: Husky -> Kopfleiste (`.kopfleiste .marke::before`, 22 px) Dort wird er als MASKE benutzt und mit dem Akzentverlauf eingefaerbt. Von einer Maske zaehlt nur der Alphakanal, also die Silhouette -- Ohren, Schnauze, Nackenlinie. Bei 22 px ist das erkennbar; die feinen schwarzen Linien waeren es nicht. Spicy -> Siegel in der Mitte des Zentrale-Rings (44 px), und davon NUR DIE CHILI (Begruendung unten beim Auftrag). Warum ueberhaupt Spicy Media und nicht der Husky: Dieser Ring zeigt DAS TEAM, ein Segment je Person. Der DogFather-Kopf in seiner Mitte haette Filipe bildlich ins Zentrum seines Teams gesetzt, mit allen anderen als Kranz darum herum. Genau das soll nicht sein (Nutzeranweisung 18.08.2026: nie hierarchisch ueber das Team stellen). Spicy Media ist die Marke, unter der alle gleich arbeiten, Filipe eingeschlossen. DER ZWEITE DURCHGANG AUS `logo-freistellen.mjs` LAEUFT HIER NICHT -- und das ist der wichtigste Unterschied. Dort werden nach der Flutfuellung auch die EINGESCHLOSSENEN Flaechen entfernt, die dem Grundton nahe kommen. Beim damaligen Symbol war der Grund hellblau und alles Hellblaue Hintergrund. Hier waere derselbe Schritt zerstoerisch: * Der Husky liegt auf Fast-Weiss (247,248,247) und ist SCHWARZ-WEISS gezeichnet. Schnauze, Wange, Ohrinnenseite und Brust sind weiss. Ein Farbvergleich gegen den Grundton haette dem Hund das halbe Gesicht ausgeschnitten. * Das Spicy-Siegel liegt auf reinem Schwarz und ist innen ebenfalls schwarz. Der Durchgang haette den Kreis ausgehoehlt und vom Wappen einen blossen Ring uebriggelassen. Es bleibt also bei der reinen Flutfuellung von aussen: Sie kommt nur an das heran, was mit dem Bildrand zusammenhaengt, und stoppt an der ersten Kontur. Genau das ist hier gewollt. Aufrufen mit: node tools/marken-freistellen.mjs =================================================================== */ import { readFileSync, writeFileSync, existsSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; const WURZEL = join(dirname(fileURLToPath(import.meta.url)), ".."); const ZIEL_ORDNER = join(WURZEL, "workspace", "assets", "img"); /* Die Quellen liegen MIT IM REPO, nicht nur in Filipes Bilderordner. Ein Werkzeug, dessen Eingangsdatei auf einem fremden Rechner liegt, laesst sich genau einmal ausfuehren -- danach ist es Dekoration. Wer die Marke spaeter in einer anderen Groesse oder mit anderer Kante braucht, muss die Originale haben. */ const AUFTRAEGE = [ { name: "Husky", quelle: join(WURZEL, "assets", "img", "marke", "quelle-dogfather.jpg"), ziel: join(ZIEL_ORDNER, "marke-husky.webp"), /* Unter dem Kopf steht der Graffiti-Schriftzug "DOGFATHER". Bei 22 px waere er ein grauer Fleck, und als Maske zieht er die Silhouette in die Breite. Er wird deshalb abgeschnitten -- die Trennlinie wird gemessen, nicht geraten (siehe kopfTrennen). */ kopfTrennen: true, }, { name: "Spicy Media", quelle: join(WURZEL, "assets", "img", "marke", "quelle-spicy-media.jpg"), ziel: join(ZIEL_ORDNER, "marke-spicy.webp"), kopfTrennen: false, /* NUR DIE CHILI, NICHT DAS GANZE SIEGEL -- und das ist gemessen, nicht Geschmack. Der erste Versuch nahm das vollstaendige Logo: Doppelring, Chili, "SPICY MEDIA", "FATIH TRABZON". Im Bildschirmfoto bei 44 px war davon ein dunkler Fleck mit einem roten Strich uebrig, der Schriftzug unlesbarer Matsch. Ein Logo MIT TEXT funktioniert in dieser Groesse nicht -- kein Kniff aendert daran etwas, es sind schlicht zu wenige Bildpunkte fuer Buchstaben. Die Chili mit der Flamme ist das eigentliche Erkennungszeichen und traegt auch klein: eine kraeftige rote Form mit gruenem Stiel. Gefunden wird sie ueber die FARBE statt ueber feste Koordinaten -- alles, was bunt ist (max-min der Kanaele > 40), gehoert dazu; der weisse Ring, die weisse Schrift und der schwarze Grund sind grau und fallen von selbst heraus. Wer das Logo spaeter austauscht, muss deshalb keine Zahlen nachtragen. */ nurFarbmotiv: true, }, ]; const KANTE = 512; // Ausgabegroesse, quadratisch for (const auftrag of AUFTRAEGE) { if (!existsSync(auftrag.quelle)) { console.error(`Quelle fehlt: ${auftrag.quelle}`); process.exit(1); } } 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()).newPage(); const arbeiten = async (auftrag) => await seite.evaluate(async ({ quelle, kopfTrennen, nurFarbmotiv, KANTE }) => { const bild = new Image(); bild.src = quelle; await bild.decode(); const b = bild.naturalWidth, h = bild.naturalHeight; const leinwand = document.createElement("canvas"); leinwand.width = b; leinwand.height = h; const stift = leinwand.getContext("2d", { willReadFrequently: true }); stift.drawImage(bild, 0, 0); const daten = stift.getImageData(0, 0, b, h); const p = daten.data; /* GRUNDTON AUS DEM HAEUFIGSTEN RANDTON, nicht aus den vier Ecken. Uebernommen aus logo-freistellen.mjs, wo die Eckenmittelung an abgerundeten Ecken gescheitert ist. Der Weg ueber den ganzen Rand ueberlebt Rahmen, runde Ecken und einzelne Ausreisser. */ const haufen = new Map(); const merke = (x, y) => { const i = (y * b + x) * 4; if (p[i + 3] < 200) return; const schluessel = `${p[i] >> 3},${p[i + 1] >> 3},${p[i + 2] >> 3}`; const e = haufen.get(schluessel) || { n: 0, r: 0, g: 0, bl: 0 }; e.n++; e.r += p[i]; e.g += p[i + 1]; e.bl += p[i + 2]; haufen.set(schluessel, e); }; for (let x = 0; x < b; x++) { merke(x, 0); merke(x, h - 1); } for (let y = 0; y < h; y++) { merke(0, y); merke(b - 1, y); } if (!haufen.size) return { fehler: "Der ganze Rand ist durchsichtig." }; const groesster = [...haufen.values()].sort((a, c) => c.n - a.n)[0]; const randPunkte = [...haufen.values()].reduce((s, e) => s + e.n, 0); const randAnteil = groesster.n / randPunkte; if (randAnteil < 0.5) { return { fehler: `Der Rand ist nicht einfarbig genug ` + `(haeufigster Ton nur ${Math.round(randAnteil * 100)} % des Randes).` }; } const grund = [ Math.round(groesster.r / groesster.n), Math.round(groesster.g / groesster.n), Math.round(groesster.bl / groesster.n), ]; const abstand = (i) => Math.sqrt( (p[i] - grund[0]) ** 2 + (p[i + 1] - grund[1]) ** 2 + (p[i + 2] - grund[2]) ** 2); /* NAH bis hierher gilt ein Punkt als Hintergrund WEIT ab hier gehoert er zum Motiv; dazwischen wird weich abgestuft. Etwas grosszuegiger als die 42/96 der Symbolfassung: Beide Quellen sind JPEG-Fotos, keine erzeugten PNG. Um jede Kontur liegt ein Saum aus Kompressionsartefakten, der weder Grund noch Motiv ist. Wer ihn stehen laesst, bekommt einen schmutzigen Rand -- bei einer Maske faellt das als ausgefranste Silhouette auf. */ const NAH = 58, WEIT = 118; const besucht = new Uint8Array(b * h); const liste = []; for (let x = 0; x < b; x++) { liste.push(x, x + (h - 1) * b); } for (let y = 0; y < h; y++) { liste.push(y * b, b - 1 + y * b); } let entfernt = 0, weich = 0; while (liste.length) { const feld = liste.pop(); if (besucht[feld]) continue; besucht[feld] = 1; const i = feld * 4; const a = abstand(i); if (a > WEIT) continue; // Motivkante erreicht, hier ist Schluss if (a <= NAH) { p[i + 3] = 0; entfernt++; } else { p[i + 3] = Math.round(255 * ((a - NAH) / (WEIT - NAH))); weich++; } const x = feld % b, y = (feld - x) / b; if (x > 0) liste.push(feld - 1); if (x < b - 1) liste.push(feld + 1); if (y > 0) liste.push(feld - b); if (y < h - 1) liste.push(feld + b); } /* ---- Wo steht das Motiv? ---------------------------------------- */ const zeile = new Array(h).fill(0); let x0 = b, y0 = h, x1 = -1, y1 = -1, sichtbar = 0; for (let y = 0; y < h; y++) { for (let x = 0; x < b; x++) { if (p[(y * b + x) * 4 + 3] > 40) { zeile[y]++; sichtbar++; if (x < x0) x0 = x; if (x > x1) x1 = x; if (y < y0) y0 = y; if (y > y1) y1 = y; } } } if (x1 < 0) return { fehler: "Nach dem Freistellen ist nichts uebrig." }; /* ---- Kopf vom Schriftzug trennen -------------------------------- */ let trennung = null; if (kopfTrennen) { /* GEMESSEN, NICHT GERATEN. Gesucht wird die duennste Stelle des Motivs im mittleren Drittel -- zwischen Kinn/Nacken und der Oberkante der Buchstaben liegt eine Einschnuerung. Der Bereich ist bewusst eng gefasst (45-78 % der Motivhoehe): Weiter oben liegt die Taille zwischen den Ohren, weiter unten die Luecke zwischen zwei Buchstabenreihen -- beide waeren falsch. */ const hoehe = y1 - y0 + 1; const von = y0 + Math.round(hoehe * 0.45); const bis = y0 + Math.round(hoehe * 0.78); let besteZeile = von, bestesMass = Infinity; for (let y = von; y <= bis; y++) { if (zeile[y] < bestesMass) { bestesMass = zeile[y]; besteZeile = y; } } trennung = { y: besteZeile, punkte: bestesMass, von, bis, profil: [von, Math.round((von + bis) / 2), bis].map((y) => `${y}:${zeile[y]}`) }; /* Alles ab der Trennlinie wegwerfen und den Rahmen neu bestimmen. */ for (let y = besteZeile; y < h; y++) for (let x = 0; x < b; x++) p[(y * b + x) * 4 + 3] = 0; x0 = b; y0 = h; x1 = -1; y1 = -1; sichtbar = 0; for (let y = 0; y < h; y++) for (let x = 0; x < b; x++) { if (p[(y * b + x) * 4 + 3] > 40) { sichtbar++; if (x < x0) x0 = x; if (x > x1) x1 = x; if (y < y0) y0 = y; if (y > y1) y1 = y; } } if (x1 < 0) return { fehler: "Nach dem Abschneiden des Schriftzugs ist nichts uebrig." }; } /* ---- Nur das farbige Motiv (siehe `nurFarbmotiv` oben) ----------- */ let farbrahmen = null; if (nurFarbmotiv) { let fx0 = b, fy0 = h, fx1 = -1, fy1 = -1, bunt = 0; for (let y = 0; y < h; y++) for (let x = 0; x < b; x++) { const i = (y * b + x) * 4; if (p[i + 3] < 60) continue; const max = Math.max(p[i], p[i + 1], p[i + 2]); const min = Math.min(p[i], p[i + 1], p[i + 2]); if (max - min > 40) { bunt++; if (x < fx0) fx0 = x; if (x > fx1) fx1 = x; if (y < fy0) fy0 = y; if (y > fy1) fy1 = y; } } /* Die Anzahl gehoert in die Bedingung: Ein Logo ohne bunte Punkte haette sonst einen leeren Rahmen ergeben, und das Ergebnis waere ein leeres Bild gewesen -- das sauberste von allen. */ if (bunt < 500) return { fehler: `Nur ${bunt} bunte Punkte gefunden -- zu wenig fuer ein ` + `Farbmotiv. Ist das Logo einfarbig?` }; /* Alles ausserhalb des Farbrahmens verschwindet, damit Ring und Schriftzug nicht als Reste am Bildrand stehenbleiben. */ for (let y = 0; y < h; y++) for (let x = 0; x < b; x++) { if (x < fx0 || x > fx1 || y < fy0 || y > fy1) p[(y * b + x) * 4 + 3] = 0; } /* UND DER GRUND INNERHALB DES RAHMENS MUSS AUCH WEG. Das war der erste Versuch nicht, und die Zahlen haben es NICHT verraten: "60,8 % deckend, Ecken-Alpha 0/0/0/0" klang tadellos. Im Bild war es ein schwarzes RECHTECK mit einer Chili darin. Der Grund ist die Flutfuellung: Sie laeuft von aussen und bleibt am weissen Doppelring stehen -- hinter den Ring, wo die Chili sitzt, kommt sie nie. Dort ist der schwarze Grund also unberuehrt. Hier ist ein reiner Farbvergleich gefahrlos, und das laesst sich nachrechnen statt hoffen: Der Grundton ist rgb(0,0,0), die Chili liegt bei rund (200,30,30) -- Abstand ueber 200 --, der gruene Stiel bei rund (40,80,30) mit Abstand 94, die weisse Flamme bei 441. Alle drei liegen weit ueber der Schwelle NAH (58). Genau deshalb steht diese Rechnung hier und nicht nur das Ergebnis: Wer die Marke austauscht, muss sie nachrechnen koennen. Fuer das VOLLE Siegel waere derselbe Schritt zerstoerisch (er hoehlte den schwarzen Kreis aus) -- deshalb haengt er an `nurFarbmotiv` und nicht am Werkzeug allgemein. */ let kern = 0; for (let y = fy0; y <= fy1; y++) for (let x = fx0; x <= fx1; x++) { const i = (y * b + x) * 4; if (p[i + 3] === 0) continue; const a = abstand(i); if (a <= NAH) { p[i + 3] = 0; kern++; } else if (a < WEIT) p[i + 3] = Math.min(p[i + 3], Math.round(255 * ((a - NAH) / (WEIT - NAH)))); } if (kern < 500) return { fehler: `Im Farbrahmen wurden nur ${kern} Grundton-Punkte ` + `entfernt -- der Hintergrund hinter dem Motiv steht vermutlich noch.` }; x0 = fx0; y0 = fy0; x1 = fx1; y1 = fy1; sichtbar = bunt; farbrahmen = `${fx1 - fx0 + 1}x${fy1 - fy0 + 1} (${bunt} bunte Punkte)`; } stift.putImageData(daten, 0, 0); /* ---- Quadratisch und zentriert ausgeben -------------------------- Beide Einsatzorte sind Quadrate (44 px Siegel, 22 px Maske). Wer ein nicht-quadratisches Bild mit `contain` hineinlegt, bekommt es kleiner als gedacht und aus der Mitte geruecht. Deshalb wird der Motivrahmen hier einmal sauber quadriert -- mit einem schmalen Rand, damit die weiche Kante nicht am Bildrand abgeschnitten wird. */ const mb = x1 - x0 + 1, mh = y1 - y0 + 1; /* Etwas mehr Luft um ein Farbmotiv (14 % statt 6 %): Die Chili ist eine schraege Form mit Stiel und Flamme, die bis an den Rahmen stossen -- ohne Rand klebt sie an der Kante des Siegels. */ const seiteQ = Math.round(Math.max(mb, mh) * (nurFarbmotiv ? 1.14 : 1.06)); const mx = x0 + mb / 2, my = y0 + mh / 2; const ziel = document.createElement("canvas"); ziel.width = KANTE; ziel.height = KANTE; const zs = ziel.getContext("2d"); zs.imageSmoothingQuality = "high"; zs.drawImage(leinwand, mx - seiteQ / 2, my - seiteQ / 2, seiteQ, seiteQ, 0, 0, KANTE, KANTE); /* Wie viel ist im FERTIGEN Bild noch sichtbar? Die Zahl unten entscheidet, ob gespeichert wird -- ein leeres Bild waere sonst das sauberste Ergebnis von allen. */ const zp = zs.getImageData(0, 0, KANTE, KANTE).data; let deckend = 0, halb = 0; for (let i = 3; i < zp.length; i += 4) { if (zp[i] > 200) deckend++; else if (zp[i] > 10) halb++; } const eckenAlpha = [[0, 0], [KANTE - 1, 0], [0, KANTE - 1], [KANTE - 1, KANTE - 1]] .map(([x, y]) => zp[(y * KANTE + x) * 4 + 3]); return { webp: ziel.toDataURL("image/webp", 0.94), grund, randAnteil: Math.round(randAnteil * 100), entfernt, weich, trennung, farbrahmen, rahmen: `${mb}x${mh}`, quelle: `${b}x${h}`, deckend, halb, eckenAlpha, anteil: deckend / (KANTE * KANTE), }; }, { quelle: "data:image/jpeg;base64," + readFileSync(auftrag.quelle).toString("base64"), kopfTrennen: auftrag.kopfTrennen, nurFarbmotiv: !!auftrag.nurFarbmotiv, KANTE }); let fehlt = 0; for (const auftrag of AUFTRAEGE) { console.log(`\n=== ${auftrag.name} ===`); const e = await arbeiten(auftrag); if (e.fehler) { console.error(` Abgebrochen: ${e.fehler}`); fehlt++; continue; } console.log(` Quelle: ${e.quelle}, Grundton rgb(${e.grund.join(",")}) ` + `auf ${e.randAnteil} % des Randes`); console.log(` Freigestellt: ${e.entfernt} Punkte ganz, ${e.weich} weich`); if (e.trennung) { console.log(` Schnitt bei y=${e.trennung.y} (${e.trennung.punkte} Punkte breit, ` + `gesucht in ${e.trennung.von}..${e.trennung.bis}; Profil ${e.trennung.profil.join(" ")})`); } console.log(` Motivrahmen: ${e.rahmen}`); console.log(` Ergebnis: ${e.deckend} deckende Punkte (${(e.anteil * 100).toFixed(1)} %), ` + `${e.halb} weiche Kante, Ecken-Alpha ${e.eckenAlpha.join("/")}`); /* DREI BEDINGUNGEN, und alle drei muessen halten. Eine Pruefung, die immer bestaetigt, bestaetigt nichts -- deshalb steht die Anzahl in der Bedingung und nicht nur im Meldetext. */ if (e.anteil < 0.06) { console.error(` FEHLER: nur ${(e.anteil * 100).toFixed(1)} % uebrig -- die Fuellung hat das ` + `Motiv mitgenommen. Nichts gespeichert.`); fehlt++; continue; } if (e.anteil > 0.94) { console.error(` FEHLER: ${(e.anteil * 100).toFixed(1)} % uebrig -- es wurde praktisch nichts ` + `entfernt. Nichts gespeichert.`); fehlt++; continue; } if (e.eckenAlpha.some((a) => a > 20)) { console.error(` FEHLER: eine Ecke ist noch deckend (${e.eckenAlpha.join("/")}) -- genau ` + `daran ist die alte Datei gescheitert. Nichts gespeichert.`); fehlt++; continue; } writeFileSync(auftrag.ziel, Buffer.from(e.webp.split(",")[1], "base64")); const kb = (readFileSync(auftrag.ziel).length / 1024).toFixed(1); console.log(` Gespeichert: ${auftrag.ziel.replace(WURZEL, ".")} (${KANTE}x${KANTE}, ${kb} KB)`); } await browser.close(); if (fehlt) { console.error(`\n${fehlt} von ${AUFTRAEGE.length} nicht gespeichert.`); process.exit(1); } console.log(`\nBeide Marken freigestellt.`);