diff --git a/assets/img/marke/quelle-dogfather.jpg b/assets/img/marke/quelle-dogfather.jpg new file mode 100644 index 00000000..988eee40 Binary files /dev/null and b/assets/img/marke/quelle-dogfather.jpg differ diff --git a/assets/img/marke/quelle-spicy-media.jpg b/assets/img/marke/quelle-spicy-media.jpg new file mode 100644 index 00000000..5a977737 Binary files /dev/null and b/assets/img/marke/quelle-spicy-media.jpg differ diff --git a/server/pruef-start-ansicht.mjs b/server/pruef-start-ansicht.mjs index 8cd4794e..9486548e 100644 --- a/server/pruef-start-ansicht.mjs +++ b/server/pruef-start-ansicht.mjs @@ -434,7 +434,49 @@ console.log("\n=== Das Licht folgt dem Zeiger"); const kontext = await browser.newContext({ viewport: { width: 1440, height: 1200 } }); const seite = await anmelden(kontext, "admin", "CODE-DOGI-0001"); const kachel = seite.locator(".kachel").nth(1); - const kasten = await kachel.boundingBox(); + + /* ERST INS BILD ROLLEN, DANN MESSEN -- und zwar VOR JEDER Benutzung. + + Hier stand `const kasten = await kachel.boundingBox()` ein einziges + Mal, ganz am Anfang, ohne vorher zu rollen. Am 08.09.2026 hat das + drei Pruefungen dieses Blocks umgeworfen, ohne dass am Licht + irgendetwas kaputt war: Die Begruessungskachel wuchs beim Umbau zur + Zentrale von 244 auf 404 px, die zweite Kachel rutschte damit von + y=971 auf y=1131, und ihre Mitte lag bei 1207 -- ausserhalb eines + 1200 px hohen Fensters. Dorthin kann kein Zeiger fahren, also + entstand kein Licht. + + Verraten hat es die MISCHUNG aus gruen und rot: "links" (30 % der + Hoehe) bestand, "rechts" (60 %) nicht. Eine Kachel, die nur zur + Haelfte getroffen wird, ist nicht kaputt -- sie haengt halb aus dem + Bild. Genau dieser Fehler steht zwanzig Zeilen weiter unten schon + einmal beschrieben, fuer den 07.09.2026. Dort wurde er an der + einzelnen Stelle geflickt, hier nicht. + + Deshalb jetzt nicht noch einmal eine feste Annahme, sondern eine + Funktion: Sie holt die Kachel MITTIG ins Bild und misst danach. + `block: "center"` statt `scrollIntoViewIfNeeded()` mit Absicht -- + letzteres rollt nur so weit, dass die Kante gerade eben anliegt, + und dann liegt der 60-%-Punkt wieder am Rand. Wer die Kachel spaeter + noch hoeher macht, merkt davon nichts mehr. */ + const kachelHolen = async () => { + await kachel.evaluate((k) => k.scrollIntoView({ block: "center", behavior: "instant" })); + await seite.waitForTimeout(150); + const k = await kachel.boundingBox(); + /* Die Anzahl gehoert in die Bedingung, nicht in den Meldetext: Eine + Kachel, die trotz Rollen nicht ganz ins Fenster passt, wuerde die + naechsten Messungen still verfaelschen. Dann lieber hier lautstark + scheitern als drei Zeilen weiter unten raetselhaft. */ + const fenster = await seite.evaluate(() => window.innerHeight); + if (k.y < 0 || k.y + k.height > fenster) { + throw new Error(`Die Kachel passt auch nach dem Rollen nicht ins Fenster ` + + `(y=${Math.round(k.y)}, Hoehe=${Math.round(k.height)}, Fenster=${fenster}). ` + + `Ist sie groesser geworden?`); + } + return k; + }; + + const kasten = await kachelHolen(); const ruhe = await kachel.evaluate((k) => k.style.getPropertyValue("--mx") || "(Ruhelage)"); @@ -491,8 +533,14 @@ console.log("\n=== Das Licht folgt dem Zeiger"); const anzahlAn = () => seite.evaluate(() => document.querySelectorAll('[data-licht="an"]').length); - /* 1. Ueber die Luecke zwischen zwei Kacheln hinaus. */ - const k1 = await seite.locator(".kachel").nth(1).boundingBox(); + /* 1. Ueber die Luecke zwischen zwei Kacheln hinaus. + + Auch hier ueber `kachelHolen()` statt ueber ein blankes + `boundingBox()`: Die Mitte dieser Kachel lag am 08.09.2026 + ausserhalb des Fensters, der Zeiger kam nie an, und die Pruefung + meldete "leuchtet genau eine Kachel (0)" -- als waere das Licht + kaputt. Es war nur niemand da, der es haette ausloesen koennen. */ + const k1 = await kachelHolen(); await seite.mouse.move(k1.x + k1.width / 2, k1.y + k1.height / 2); await seite.waitForTimeout(300); ok(await anzahlAn() === 1, `beim Ueberfahren leuchtet genau eine Kachel (${await anzahlAn()})`); @@ -719,11 +767,19 @@ console.log("\n=== Das Licht folgt dem Zeiger"); } ok(ruhe === "(Ruhelage)", `ruhend keine eigene Position gesetzt (${ruhe})`); - await seite.mouse.move(kasten.x + kasten.width * 0.2, kasten.y + kasten.height * 0.3); + /* NEU GEMESSEN, nicht der Kasten von oben. Der Block darueber endet + mit `window.scrollTo({ top: 0 })` -- ab dieser Zeile steht die Seite + also wieder ganz oben, und jede Koordinate von vorhin zeigt um die + gerollte Strecke daneben. Genau daran ist "rechts" gescheitert. */ + const kastenJetzt = await kachelHolen(); + + await seite.mouse.move(kastenJetzt.x + kastenJetzt.width * 0.2, + kastenJetzt.y + kastenJetzt.height * 0.3); await seite.waitForTimeout(250); const links = await kachel.evaluate((k) => k.style.getPropertyValue("--mx")); - await seite.mouse.move(kasten.x + kasten.width * 0.85, kasten.y + kasten.height * 0.6); + await seite.mouse.move(kastenJetzt.x + kastenJetzt.width * 0.85, + kastenJetzt.y + kastenJetzt.height * 0.6); await seite.waitForTimeout(250); const rechts = await kachel.evaluate((k) => k.style.getPropertyValue("--mx")); @@ -734,7 +790,7 @@ console.log("\n=== Das Licht folgt dem Zeiger"); /* Beim Verlassen zurück in die Ruhelage. Ohne das blieben die Kacheln nach einer Weile alle unterschiedlich beleuchtet stehen, ohne dass jemand wüsste, warum. */ - await seite.mouse.move(kasten.x - 250, kasten.y - 250); + await seite.mouse.move(kastenJetzt.x - 250, kastenJetzt.y - 250); await seite.waitForTimeout(250); const danach = await kachel.evaluate((k) => k.style.getPropertyValue("--mx") || "(Ruhelage)"); ok(danach === "(Ruhelage)", `nach dem Verlassen wieder in Ruhe (${danach})`); diff --git a/tools/marken-freistellen.mjs b/tools/marken-freistellen.mjs new file mode 100644 index 00000000..adde4bef --- /dev/null +++ b/tools/marken-freistellen.mjs @@ -0,0 +1,385 @@ +/* =================================================================== + 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.`); diff --git a/workspace/assets/css/heim.css b/workspace/assets/css/heim.css index df48696e..54d6cdab 100644 --- a/workspace/assets/css/heim.css +++ b/workspace/assets/css/heim.css @@ -94,7 +94,21 @@ --k-rand: 4px; position: relative; - padding: 26px 30px; + /* 20px oben/unten statt 26. + + JEDER PIXEL HIER SCHIEBT DIE KACHELN DARUNTER NACH UNTEN, und das + ist keine Feinheit: Mit dem dreispaltigen Umbau wuchs diese Kachel + von 244 auf 404 px. Die erste Kachelreihe rutschte dadurch von + y=971 auf y=1131 -- auf einem 1200 px hohen Fenster war damit keine + einzige Kachel mehr zu sehen, ohne zu rollen. Die Startseite BESTEHT + aber aus diesen Kacheln. + + Zusammen mit der kleineren Anrede (siehe `.zgruss .titel`), dem + engeren Zeilenabstand und der Titelgroesse sind es rund 60 px + weniger. Die Kachel bleibt dem Vorbild treu -- VanVans Werktisch + hat mit minmax(200px, 260px) sogar eine breitere Ringspalte als + unsere 180-232 --, steht der Seite darunter aber nicht mehr im Weg. */ + padding: 20px 30px; border: 0; border-radius: 0; @@ -373,7 +387,10 @@ } .willkommen__text { min-width: 0; - display: flex; flex-direction: column; align-items: center; gap: 9px; + /* 7px statt 9: vier Luecken uebereinander, also 8 px weniger Hoehe. + Siehe die Rechnung ueber `.willkommen`, warum jeder Pixel hier + unten bei den Kacheln ankommt. */ + display: flex; flex-direction: column; align-items: center; gap: 7px; text-align: center; } @@ -397,37 +414,51 @@ display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; pointer-events: none; } -/* DAS SIEGEL IST RUND -- und das ist eine Reparatur, keine Stilfrage. +/* IN DER MITTE STEHT SPICY MEDIA, NICHT DER DOGFATHER-KOPF. - Erst stand hier dieselbe Maske wie in der Kopfleiste: - -webkit-mask: url("../img/marke-husky.webp") ... - Im Nahaufnahme-Bildschirmfoto war das Ergebnis ein KASTEN mit einem - Husky darin, kein freigestellter Husky. + Das ist keine Stilfrage, sondern eine inhaltliche. Dieser Ring zeigt + DAS TEAM -- ein Segment je Person. Der DogFather-Kopf in seiner Mitte + haette Filipe bildlich ins Zentrum seines eigenen Teams gesetzt, mit + allen anderen als Kranz darum herum. Genau das soll nicht sein + (Anweisung 18.08.2026: nie hierarchisch ueber das Team stellen). Das + Spicy-Media-Siegel steht fuer alle gleich -- es ist die Marke, unter + der das ganze Team arbeitet, Filipe eingeschlossen. - Nachgemessen: Die Datei hat einen Alphakanal (VP8X mit ALPH-Chunk, - Alpha-Flag gesetzt) -- er ist nur RECHTECKIG. Das Bild ist nicht - freigestellt. Als Maske taugt es damit nicht, egal wie richtig die - CSS-Zeile ist. + Optisch trifft es ausserdem genau, was ein Siegel hier braucht: Es + ist von sich aus rund, mit hellem Doppelring auf dunklem Kern. Auf + dem dunklen Untergrund des Workspace leuchtet der Ring ruhig, ohne zu + blenden -- der Kern bleibt dunkel und zieht den Blick nicht von der + Zahl darunter ab, um die es geht. - BEFUND FUER SPAETER: Dasselbe gilt fuer `.kopfleiste .marke::before` - in start.css -- dort ist es bei 22 px nur nicht aufgefallen. Wer das - Logo einmal freistellt, repariert beide Stellen auf einmal. + VORGESCHICHTE, damit sie niemand zweimal macht: Hier stand + `marke-husky.webp`, und in der Kopfleiste dieselbe Datei als Maske. + Die Datei hatte zwar einen Alphakanal, der war aber RECHTECKIG -- als + Maske ergab das einen Kasten mit einem Husky darin. Seit dem + 08.09.2026 ist sie durch das echte, freigestellte Original ersetzt + (`tools/marken-freistellen.mjs`, Ecken-Alpha nachgemessen 0/0/0/0), + und die Kopfleiste ist damit ohne eine einzige geaenderte CSS-Zeile + mitrepariert. - Hier wird es deshalb als BILD gezeigt und rund beschnitten, mit - einem feinen Ring darum. Das ist naeher am Vorbild als die Maske es - je war: VanVans Werktisch traegt in der Mitte ein rundes Siegel, kein - ausgeschnittenes Zeichen. */ + `contain` statt der frueheren 128 %: Das Werkzeug gibt das Motiv + bereits quadratisch gerahmt aus, ein Nachskalieren wuerde den + Doppelring nur wieder anschneiden. */ .zring__siegel { width: 44px; height: 44px; margin-bottom: 5px; border-radius: 50%; - background: url("../img/marke-husky.webp") center / 128% no-repeat, #0b1420; + /* 74 % statt `contain`: Bei voller Breite stiess die Chili an den + Rand des Siegels und wirkte hineingezwaengt. Mit etwas Luft + ringsum sitzt sie im Kreis, statt ihn auszufuellen. */ + background: url("../img/marke-spicy.webp") center / 74% no-repeat, #0b1420; box-shadow: 0 0 0 1px rgba(180, 210, 240, .28), inset 0 0 12px rgba(0, 0, 0, .55); - /* Etwas entsaettigt und abgedunkelt: Das Siegel ist der ruhige Kern - des Rings, nicht sein hellster Punkt -- sonst zieht es den Blick von - der Zahl darunter ab, um die es geht. */ - filter: saturate(.55) brightness(.92); + /* Nur leicht zurueckgenommen. Die alte Fassung stand auf + saturate(.55) brightness(.92) -- das war fuer den schwarzweissen + Husky gedacht. Dem Spicy-Siegel nimmt so viel Entsaettigung das + Rot der Chili, also bleibt es bei einem sanften Daempfer: ruhig + genug, um nicht mit der Zahl zu konkurrieren, farbig genug, um als + Marke erkennbar zu sein. */ + filter: saturate(.86) brightness(.95); } .zring__prozent { font-size: 1.85rem; font-weight: 700; line-height: 1; @@ -477,19 +508,56 @@ } .zgruss__text { display: flex; flex-direction: column; gap: 1px; min-width: 0; text-align: left; } .zgruss__datum { font-size: .78rem; color: rgba(168, 192, 218, .72); } + +/* DIE ANREDE IST EINE PILLE, KEINE ZWEITE UEBERSCHRIFT. + + In der Pille steckt `