Die Zentrale bekommt die echten Marken -- und drei rote Pruefungen waren keine
Die drei Befunde in pruef-start-ansicht kamen NICHT vom Licht. Sie hingen
alle an einem boundingBox(), das einmal am Anfang ohne Vorrollen gemessen
wurde. Der Umbau zur Zentrale hatte die Begruessungskachel von 244 auf
404 px wachsen lassen, die zweite Kachel rutschte von y=971 auf y=1131,
ihre Mitte lag bei 1207 -- ausserhalb eines 1200 px hohen Fensters. Dorthin
faehrt kein Zeiger, 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.
Beides ist jetzt behoben, nicht nur eines:
* Die Pruefung holt die Kachel ueber scrollIntoView({block:"center"})
ins Bild und misst DANACH, vor jeder Benutzung. Passt sie trotzdem
nicht ins Fenster, ist das ein harter Fehler statt einer stillen
Fehlmessung.
* Die Kachel selbst faellt von 404 auf 344 px. Groesster Posten war die
Anrede-Pille mit 88 px: In ihr steckt <h1 class="titel">, und
.willkommen .titel ist die grosse Seitenueberschrift -- es standen
also zwei Ueberschriften in Titelgroesse uebereinander. Der Rang von
#gruss aendert sich nicht, nur die Groesse.
Nebenbefund, den die Reparatur mit aufgedeckt hat: Die Randmessung stand
auf "nah 51 gegen fern 0". Diese 0 war kein Messwert, sondern der
Bildpunkt ausserhalb des Fensters. Jetzt "nah 43 gegen fern 7" -- dieselbe
Pruefung misst zum ersten Mal wirklich.
DIE MARKEN. marke-husky.webp war nie freigestellt (0,3 % durchsichtig,
alle vier Ecken Alpha 255) -- als Maske ergab das einen Kasten mit einem
Husky darin. Ersetzt durch das echte Original, damit repariert sich die
Kopfleiste ohne eine einzige geaenderte CSS-Zeile mit.
In der Mitte des Rings steht jetzt Spicy Media, nicht der Husky: Der Ring
zeigt DAS TEAM, ein Segment je Person. Der DogFather-Kopf in seiner Mitte
haette Filipe bildlich ins Zentrum seines eigenen Teams gesetzt.
Und davon nur die Chili: Das volle Siegel war bei 44 px unlesbarer Matsch.
Beim ersten Ausschneiden meldete das Werkzeug "60,8 % deckend, Ecken
0/0/0/0" -- klang tadellos und war ein schwarzes RECHTECK mit Chili darin.
Die Flutfuellung laeuft von aussen und kommt nie hinter den weissen Ring.
Gefunden hat das kein Kennwert, sondern das Hinsehen.
Gemessen: pruef-start-ansicht EXIT=0 (137 -> 140 Pruefungen, die drei
roten sind gruen, keine ist verschwunden), pruef-css-klassen EXIT=0
(472 Groessen), pruef-handy EXIT=0.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
@@ -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})`);
|
||||
|
||||
@@ -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.`);
|
||||
@@ -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 `<h1 class="titel" id="gruss">`, und
|
||||
`.willkommen .titel` (start.css) ist mit clamp(1.9rem, 3.4vw, 2.6rem)
|
||||
die grosse Seitenueberschrift. Nachgemessen war die Pille dadurch
|
||||
88 px hoch -- der groesste Einzelposten der ganzen Kachel, fuer eine
|
||||
Zeile "Guten Abend, Filipe".
|
||||
|
||||
Damit standen ZWEI Ueberschriften in Titelgroesse uebereinander: die
|
||||
Anrede und "Die Zentrale" direkt darunter. Im Vorbild ist genau das
|
||||
nicht so -- dort ist die Anrede klein und ruhig, und der Titel traegt
|
||||
allein. Die Pille faellt damit von 88 auf rund 56 px, ohne dass
|
||||
irgendwo Polsterung weggenommen werden musste.
|
||||
|
||||
Die Schriftgroesse aendert die Gliederung NICHT: `#gruss` bleibt das
|
||||
h1 der Seite. Wie gross eine Ueberschrift gesetzt ist und welchen
|
||||
Rang sie hat, sind zwei verschiedene Dinge. */
|
||||
.zgruss .titel {
|
||||
font-size: 1.05rem; font-weight: 700; line-height: 1.25;
|
||||
letter-spacing: -.005em;
|
||||
margin: 0;
|
||||
/* Der Verlauf aus `.willkommen .titel` wuerde hier mitkommen und die
|
||||
Anrede in dieselbe gefraeste Metalloptik setzen wie den Titel. Bei
|
||||
17 px ist das nur unruhig -- also zurueck auf eine feste Farbe. */
|
||||
background: none; -webkit-text-fill-color: currentColor;
|
||||
color: #e8f1fa; text-shadow: none;
|
||||
}
|
||||
/* .72rem ist die Hausuntergrenze (11,52 px), nicht Geschmack: Darunter
|
||||
liest auf einem Telefon niemand mehr mit. Hier stand .66rem = 10,6 px.
|
||||
Beim Bau des Rings war dieselbe Regel bei `.zring__unter` schon
|
||||
angewandt worden -- diese beiden Stellen sind dabei durchgerutscht,
|
||||
weil sie in einem anderen Abschnitt der Datei stehen. */
|
||||
.zgruss__rolle {
|
||||
flex: 0 0 auto;
|
||||
padding: 4px 11px; border-radius: 999px;
|
||||
font-size: .66rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
|
||||
font-size: .72rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase;
|
||||
color: #e6c88a;
|
||||
border: 1px solid rgba(227, 179, 65, .34);
|
||||
background: rgba(227, 179, 65, .09);
|
||||
}
|
||||
|
||||
/* ---- Zierzeile und Titel ------------------------------------------ */
|
||||
/* Ebenfalls auf die Hausuntergrenze gehoben (.68rem waren 10,9 px).
|
||||
Das breite letter-spacing macht die Zeile dadurch etwas laenger --
|
||||
sie steht in der mittleren Spalte, die mit `minmax(0, 1fr)` mitgeht,
|
||||
und bricht daher um statt ueberzulaufen. */
|
||||
.zmarke {
|
||||
display: inline-flex; align-items: center; gap: 11px;
|
||||
font-size: .68rem; font-weight: 600; letter-spacing: .24em; text-transform: uppercase;
|
||||
font-size: .72rem; font-weight: 600; letter-spacing: .24em; text-transform: uppercase;
|
||||
color: rgba(196, 214, 234, .6);
|
||||
margin-top: 3px;
|
||||
}
|
||||
@@ -543,7 +611,11 @@
|
||||
|
||||
.ztitel {
|
||||
margin: 0;
|
||||
font-size: clamp(1.9rem, 3.6vw, 2.9rem);
|
||||
/* Obergrenze von 2.9 auf 2.45rem. Bei 1440 px lief der Titel vorher
|
||||
auf 46 px hinaus und war damit fast so hoch wie die Anrede daneben.
|
||||
Die untere Grenze bleibt, damit er auf dem Telefon nicht schrumpft
|
||||
-- dort ist er die einzige Ueberschrift im Bild. */
|
||||
font-size: clamp(1.9rem, 3.6vw, 2.45rem);
|
||||
font-weight: 700; line-height: 1.06; letter-spacing: -.01em;
|
||||
color: #dce9f6;
|
||||
/* Ein Verlauf statt einer Volltonfarbe: Der Titel bekommt oben Licht
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Reference in New Issue
Block a user