/* =================================================================== Das App-Symbol der Modi-App ("Team Dogi", 10.09.2026). DER HUSKY IST DERSELBE. Filipe am 09.09.2026 zum Rollenbild: "das modi symbol soll das gleiche sein wie bei dogfather." Das gilt hier genauso -- es ist dieselbe Vorlage, dieselbe Technik. DIE CHILI IST ES NICHT, UND ZWAR ABSICHTLICH. Sie steht fuer Spicy Media. Die Modis gehoeren nicht dorthin; ihre Marke heisst "Team Dogi". Eine Chili auf ihrem Startbildschirm waere kein Schmuck, sondern eine falsche Aussage. An ihre Stelle tritt der Ton, der auch ihre Kacheln traegt: das gedeckte Stahlblau #5f8a9f. WARUM EINE EIGENE DATEI UND KEIN SCHALTER IN workspace-symbol.mjs: Jenes Werkzeug PRUEFT, dass die Chili in jeder Fassung sichtbar ist. Ein Schalter haette diese Pruefung fuer den halben Aufruf abschalten muessen -- und eine Pruefung, die man abschalten kann, ist bald abgeschaltet. Zwei Bilder mit zwei Regeln bekommen zwei Werkzeuge, die jeweils ihre eigenen Regeln durchsetzen. DIE DRITTE GEGENPROBE, DIE ES DORT NICHT GIBT: Am Ende wird das erzeugte Symbol gegen das des Workspace gehalten. Waeren beide gleich, haette der ganze Umzug seinen Sinn verloren -- zwei Apps mit demselben Zeichen sind auf einem Startbildschirm dasselbe Symbol zweimal. Gemessen wird das, nicht behauptet. Aufrufen mit: node tools/crew-symbol.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 HUSKY = join(WURZEL, "workspace", "assets", "img", "marke-husky.webp"); /* Umlenkbar wie beim Schwesterwerkzeug: Ein Symbol beurteilt man, indem man es ansieht -- und dafuer darf man die echten Dateien nicht bei jedem Versuch ueberschreiben. */ const ZIEL = process.env.SYMBOL_ZIEL || join(WURZEL, "assets", "img", "app-symbole"); /* Zum Vergleichen -- immer die echten Symbole, auch wenn das Ziel umgelenkt ist. Fehlt eines, ist die dritte Gegenprobe NICHT durchfuehrbar, und das ist ein Fehler, kein Ueberspringen. */ const VERGLEICH = join(WURZEL, "assets", "img", "app-symbole"); if (!existsSync(HUSKY)) { console.error(`Fehlt: ${HUSKY}`); process.exit(1); } const alsDaten = (p) => "data:image/webp;base64," + readFileSync(p).toString("base64"); const GROESSEN = [ { n: 512, endung: "512" }, { n: 192, endung: "192" }, { n: 512, endung: "512-maskable", maskable: true }, { n: 192, endung: "192-maskable", maskable: true }, { n: 180, endung: "180" }, { n: 32, endung: "32" }, ]; function html(n, maskable, husky, weglassen = "") { /* KLEIN = alles unter 64 px. Dort faellt die Gesichtszeichnung weg: achtfach vergroessert verglichen ist der Kopf mit Zeichnung bei 32 px ein dunkler Fleck, als reine Silhouette ein Hund. Dieselbe Messung wie beim Workspace-Symbol, dasselbe Ergebnis. */ const klein = n < 64; const rand = Math.round(n * 0.22); /* ALLES IN EINER GRUPPE, damit fuer die maskable-Fassung genau EINE Zahl kleiner wird und nicht mehrere auseinanderlaufen. 0,74 haelt alles im 80-%-Sicherheitskreis, den Android als einziges garantiert. Ohne Chili darf der Kopf etwas groesser stehen -- er ist hier allein und muss die Flaeche selbst tragen. */ const g = Math.round(n * (maskable ? 0.74 : 0.96)); const ecke = maskable ? "" : `border-radius:${rand}px;`; return `
`; } 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({ deviceScaleFactor: 1 })).newPage(); if (!existsSync(ZIEL)) mkdirSync(ZIEL, { recursive: true }); const husky = alsDaten(HUSKY); let gebaut = 0; const maengel = []; async function aufnehmen(n, maskable, weglassen = "") { await seite.setViewportSize({ width: n, height: n }); await seite.setContent(html(n, maskable, husky, weglassen), { waitUntil: "networkidle" }); return await seite.screenshot({ omitBackground: false }); } /* Streuung der Helligkeit. Ein leeres Feld hat keine -- das beweist, dass ueberhaupt etwas draufsteht, mehr nicht. Die Bytezahl beweist auch das nicht: ein einfarbiges PNG ist genauso gross wie ein volles. */ async function streuungVon(bild) { return await seite.evaluate(async (b64) => { const b = new Image(); b.src = "data:image/png;base64," + b64; await b.decode(); const c = document.createElement("canvas"); c.width = b.width; c.height = b.height; const g2 = c.getContext("2d"); g2.drawImage(b, 0, 0); const d = g2.getImageData(0, 0, c.width, c.height).data; let s = 0, s2 = 0, m = 0; for (let i = 0; i < d.length; i += 4) { const h = 0.2126 * d[i] + 0.7152 * d[i + 1] + 0.0722 * d[i + 2]; s += h; s2 += h * h; m++; } return Math.sqrt(s2 / m - (s / m) ** 2); }, bild.toString("base64")); } /* Anteil der Bildpunkte, die sich sichtbar unterscheiden. Acht Stufen Abstand, damit Kantenglaettung allein den Wert nicht hochtreibt. -1 heisst "nicht vergleichbar" (verschiedene Groessen) und ist ausdruecklich etwas anderes als 0. */ async function unterschied(a, b) { return await seite.evaluate(async ([x, y]) => { const laden = async (b64) => { const b = new Image(); b.src = "data:image/png;base64," + b64; await b.decode(); const c = document.createElement("canvas"); c.width = b.width; c.height = b.height; c.getContext("2d").drawImage(b, 0, 0); return c.getContext("2d").getImageData(0, 0, c.width, c.height).data; }; const [p, q] = [await laden(x), await laden(y)]; if (p.length !== q.length) return -1; let anders = 0, m = 0; for (let i = 0; i < p.length; i += 4) { const w = Math.max(Math.abs(p[i] - q[i]), Math.abs(p[i + 1] - q[i + 1]), Math.abs(p[i + 2] - q[i + 2]), Math.abs(p[i + 3] - q[i + 3])); if (w > 8) anders++; m++; } return anders / m; }, [a.toString("base64"), b.toString("base64")]); } let verglichen = 0; for (const g of GROESSEN) { const bild = await aufnehmen(g.n, !!g.maskable); writeFileSync(join(ZIEL, `crew-${g.endung}.png`), bild); gebaut++; const streuung = await streuungVon(bild); if (streuung < 12) maengel.push(`${g.endung}: kaum Zeichnung (Streuung ${streuung.toFixed(1)})`); const klein = g.n < 64; const ohneZeichnung = await unterschied(bild, await aufnehmen(g.n, !!g.maskable, "zeichnung")); if (klein) { /* Unter 64 px ist die schlichte Silhouette Absicht. Geprueft wird, dass sie es auch bleibt -- sonst waere die Regel bloss ein Kommentar, der beim naechsten Umbau stillschweigend kippt. */ if (ohneZeichnung > 0.001) maengel.push(`${g.endung}: klein soll ohne Gesichtszeichnung sein`); } else if (ohneZeichnung < 0.04) { maengel.push(`${g.endung}: das Gesicht kommt nicht durch (${(ohneZeichnung * 100).toFixed(1)} %)`); } /* DRITTE GEGENPROBE: anders als das Workspace-Symbol. */ const gegen = join(VERGLEICH, `workspace-${g.endung}.png`); let anders = -2; if (!existsSync(gegen)) { maengel.push(`${g.endung}: Vergleichsbild workspace-${g.endung}.png fehlt ` + `-- nicht nachsehbar, ob sich die Symbole unterscheiden`); } else { anders = await unterschied(bild, readFileSync(gegen)); verglichen++; if (anders < 0) { maengel.push(`${g.endung}: Vergleichsbild hat eine andere Groesse -- nicht vergleichbar`); } else if (anders < 0.10) { maengel.push(`${g.endung}: kaum vom Workspace-Symbol zu unterscheiden ` + `(${(anders * 100).toFixed(1)} %)`); } } console.log(` crew-${g.endung}.png`.padEnd(30) + `${(bild.length / 1024).toFixed(1)} KB · Streuung ${streuung.toFixed(1)}` + ` · Gesicht ${(ohneZeichnung * 100).toFixed(1)} %` + ` · anders als Workspace ${anders < 0 ? "?" : (anders * 100).toFixed(1) + " %"}`); } await browser.close(); /* Die Zahlen gehoeren in die Bedingung, nicht nur in den Bericht. */ if (gebaut !== GROESSEN.length) { console.error(`\nFEHLER: ${gebaut} von ${GROESSEN.length} erzeugt.`); process.exit(1); } if (verglichen !== GROESSEN.length) { console.error(`\nFEHLER: nur ${verglichen} von ${GROESSEN.length} gegen das ` + `Workspace-Symbol gehalten.`); process.exit(1); } if (maengel.length) { console.error("\nFEHLER:\n " + maengel.join("\n ")); process.exit(1); } console.log(`\n${gebaut} Symbole erzeugt. Der Husky ist derselbe wie bei DogFather,` + ` das Gesicht ist in jeder grossen Fassung nachweisbar sichtbar, und alle` + ` ${verglichen} unterscheiden sich messbar vom Workspace-Symbol.`);