Files
dogfather-universe/tools/workspace-symbol.mjs
T
DogFatherGitandClaude Opus 5 d0ac8ee8d1 Das Logo: aus dem Fleck wird wieder ein Hund
Filipe: "kannst du bitte das logo wenn man es installiert auf pc oder
handy und oben rechts in der leiste noch perfektionnieren."

Beide Stellen hatten denselben Fehler, und er war derselbe wie an
mehreren Stellen davor: Der Husky lag als MASKE auf einer Farbflaeche.
Von einer Maske zaehlt nur der Alphakanal, und die Vorlage ist rundum
freigestellt -- uebrig blieb eine geschlossene Flaeche in Hundeform.
Kein Auge, keine Schnauze, kein Ohrinneres. Ein Fleck.

Jetzt liegt an beiden Stellen dieselbe Zeichnung im Mischmodus "screen"
ueber einer stahlblauen Silhouette: Schwarz laesst das Fell dunkel,
Weiss hebt Gesicht, Ohren und Auge heraus. Gleiche Farben, gleicher
Daempfer (.88) -- ein Zeichen, zwei Orte.

App-Symbol ausserdem:
- Die Chili lief mit ihrem Stiel quer ueber den Fang. Sie ist jetzt
  gespiegelt, kleiner und liegt hinter dem Hals.
- Der Hals endete in einer geraden Kante (die Vorlage ist unten
  angeschnitten). Eine zweite Maske blendet ihn aus, statt ihn
  abzuschneiden.
- Der Grund war matschig: Rot und Blau trafen sich diagonal genau dort,
  wo der Kopf steht. Jetzt kaltes Licht oben, warmes unten.
- Unter 64 px faellt die Gesichtszeichnung weg, die Chili aber NICHT --
  klein erkennt man ein Zeichen zuerst an der Farbe.
- Alle Masse haengen an einer Zahl (Groesse der Gruppe), nicht an sechs.

Kopfleiste ausserdem:
- Die Chili links hatte einen BLAUEN Schein -- aus der Zeit, als dort
  der Husky stand. Der Schein ist nie mitgewandert. Jetzt warm.
- `filter` ersetzt, es ergaenzt nicht: Beim Ueberfahren wurde der Schein
  geloescht und das Zeichen dabei flacher statt heller. Behoben.

Damit die Aenderung auch ankommt:
- Der Stempel gilt jetzt auch fuer die App-Symbole und fuer das
  Manifest. Bilder werden einen Tag zwischengespeichert, das Symbol der
  INSTALLIERTEN App gar nicht neu geholt -- ohne Stempel haette niemand
  das neue Zeichen gesehen.
- pruef-zwischenspeicher prueft das (18 -> 21 Pruefungen).

Werkzeuge:
- tools/ausschnitt.mjs (neu): schneidet aus einem Bildschirmfoto ein
  Stueck heraus und vergroessert die PNG-Datei. Noetig, weil eine
  Vergroesserung per CSS-transform den Browser NEU rechnen laesst --
  ich habe damit 264 px beurteilt und geglaubt, es seien 22, und daraus
  den falschen Schluss gezogen, ein Gesicht trage bei 22 px nicht.
- tools/ansicht.mjs: AUSSCHNITT=<selektor> nimmt nur ein Element auf.
- workspace-symbol.mjs: SYMBOL_ZIEL lenkt die Ausgabe um (Entwuerfe
  ansehen, ohne die sechs echten Dateien zu ueberschreiben), und zwei
  Gegenproben pruefen jetzt, dass Gesicht und Chili wirklich zu sehen
  sind -- die alte Pruefung sagte nur "es steht etwas drauf" und haette
  den Fleck anstandslos durchgewunken.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-09 18:24:20 +02:00

315 lines
16 KiB
JavaScript

/* ===================================================================
Das App-Symbol des Workspace: Husky UND Chili in einem Zeichen.
Filipe (08.09.2026): "ich will dass du da eine geile mischung machst
von diesen zwei logo ... mach was ultra krass geiles draus bitte."
DIE AUFGABE IST NICHT "zwei Bilder nebeneinander". Ein App-Symbol
steht bei 32 px im Browserreiter und bei 192 px auf dem
Startbildschirm. Zwei vollstaendige Logos nebeneinander ergeben dort
zwei unlesbare Haelften. Gebraucht wird EINE Form, in der beide
vorkommen.
DIE LOESUNG: Der Husky ist das Zeichen, die Chili seine Farbe. Er
steht gross und mittig, sie liegt klein darunter -- ein Hauptmotiv
und ein Beiklang, nicht zwei gleich laute Stimmen.
ÜBERARBEITET AM 09.09.2026 (Filipe: "kannst du bitte das logo wenn
man es installiert auf pc oder handy ... noch perfektionnieren. ich
finde es könnte noch besser aussehen"). Was falsch war und warum:
1. DER HUSKY WAR EIN FLECK. Er lag als Maske auf einer Silberflaeche.
Von einer Maske zaehlt nur der Alphakanal, und die Vorlage ist
rundum freigestellt -- also war das Ergebnis eine geschlossene
Flaeche in Hundeform: kein Auge, keine Schnauze, kein Ohrinneres.
Jetzt liegt die Originalzeichnung im Mischmodus "screen" ueber
einer stahlblauen Silhouette. Das Fell bleibt dunkel, das Gesicht
kommt heraus. Aus dem Fleck wird wieder ein Hund.
2. DIE CHILI LIEF UEBER DEN FANG. Als Bogen unten herum gedreht kreuzte
ihr Stiel die Schnauze. Jetzt ist sie gespiegelt (Stiel nach rechts,
weg vom Kopf), deutlich kleiner und liegt hinter dem Hals.
3. DER HALS ENDETE IN EINER GERADEN KANTE. Die Vorlage ist unten
angeschnitten; als Symbol sah das nach Fehler aus. Eine zweite
Maske blendet ihn nach unten aus, statt ihn abzuschneiden.
4. DER GRUND WAR MATSCHIG. Rot und Blau trafen sich diagonal genau in
der Mitte -- dort, wo der Kopf steht. Jetzt liegt das kalte Licht
oben (beim Kopf), das warme unten (bei der Chili), und dazwischen
ist ruhiges Nachtblau.
MASKABLE: Android schneidet bis zu 20 % ringsum weg. Dafuer wird genau
EINE Zahl kleiner -- die Groesse der Gruppe, in der Kopf und Chili in
Prozent sitzen. Nicht sechs einzelne Werte, die auseinanderlaufen.
Aufrufen mit: node tools/workspace-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");
const CHILI = join(WURZEL, "workspace", "assets", "img", "marke-spicy.webp");
/* Das Ziel ist umlenkbar (SYMBOL_ZIEL=/tmp/probe). Ein Symbol beurteilt
man nicht aus dem Quelltext, sondern indem man es ansieht -- und dafuer
darf man die sechs echten Dateien nicht bei jedem Versuch ueberschreiben.
Ohne die Variable bleibt alles wie bisher. */
const ZIEL = process.env.SYMBOL_ZIEL || join(WURZEL, "assets", "img", "app-symbole");
for (const q of [HUSKY, CHILI]) {
if (!existsSync(q)) { console.error(`Fehlt: ${q}`); process.exit(1); }
}
const alsDaten = (p) => "data:image/webp;base64," + readFileSync(p).toString("base64");
/* Die Groessen, die Manifest und HTML tatsaechlich anfordern. */
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" },
];
/* `weglassen` ist NUR fuer die Gegenprobe da: "zeichnung" oder "chili"
blendet genau einen Bestandteil aus. Verglichen wird danach das
erzeugte Bild mit und ohne -- sind beide gleich, war der Bestandteil
nie zu sehen. Eine Pruefung, die nur "es steht etwas drauf" sagt,
haette den Fleck von gestern anstandslos durchgewunken. */
function html(n, maskable, husky, chili, weglassen = "") {
/* KLEIN = alles unter 64 px. Dort faellt die GESICHTSZEICHNUNG weg,
nicht die Chili. Achtfach vergroessert nebeneinandergelegt: Mit
Zeichnung ist der Kopf bei 32 px ein dunkler Fleck, als reine
Silhouette ist er eindeutig ein Hund. Die Chili dagegen ist gerade
klein am nuetzlichsten -- im Reiterband erkennt man ein Zeichen
zuerst an der Farbe, und Rot hat sonst nichts hier. */
const klein = n < 64;
const rand = Math.round(n * 0.22);
/* ALLES STECKT IN EINER GRUPPE. Kopf und Chili sitzen darin in
Prozent. Fuer die maskable-Fassung wird deshalb genau EINE Zahl
kleiner -- nicht sechs einzelne, die auseinanderlaufen koennen.
0,72 ist nachgemessen (mit eingezeichnetem Kreis angesehen): Damit
liegt alles Wesentliche im Sicherheitskreis mit 80 % Durchmesser,
den Android als einziges garantiert. */
const g = Math.round(n * (maskable ? 0.72 : 0.94));
const ecke = maskable ? "" : `border-radius:${rand}px;`;
/* ALLE REGELN TRAGEN DENSELBEN VORSATZ `.gruppe > i`.
Nicht aus Ordnungsliebe: Waehrend des Bauens stand hier `.chili`
ohne Vorsatz -- und `.gruppe > i` (Klasse + Typ) ist spezifischer
als `.chili` (nur Klasse). Die Drehung der Chili wurde dadurch
stillschweigend von der allgemeinen Regel ueberschrieben. Zwei
Regeln fuer dieselbe Eigenschaft, die spaetere verliert trotzdem:
genau der Fehler, der im Haus schon mehrfach Zeit gekostet hat.
Gleicher Vorsatz ueberall heisst: die Reihenfolge entscheidet, und
die ist sichtbar. */
return `<!doctype html><html><head><meta charset="utf-8"><style>
html,body{margin:0;width:${n}px;height:${n}px;overflow:hidden}
.feld{position:relative;width:${n}px;height:${n}px;${ecke}
/* DER GRUND. Ruhiges Nachtblau von oben nach unten, darueber zwei
Lichter: kaltes Dogi-Blau oben (da steht der Kopf), warmes
Spicy-Rot unten (da liegt die Chili). Der alte diagonale
Blau-nach-Rot-Verlauf traf sich in der Mitte -- genau dort, wo
der Kopf steht, und machte ihn matschig. */
background:
radial-gradient(76% 56% at 50% 26%, rgba(95,189,255,.20) 0%, transparent 68%),
radial-gradient(88% 54% at 50% 104%, rgba(176,40,32,.40) 0%, transparent 68%),
linear-gradient(180deg,#1a2f47 0%,#0d1a29 50%,#060c14 100%);
box-shadow:inset 0 ${Math.max(1, Math.round(n * .004))}px 0 rgba(255,255,255,.24),
inset 0 0 ${Math.round(n * .06)}px rgba(0,0,0,.38);
display:grid;place-items:center}
.gruppe{position:relative;width:${g}px;height:${g}px}
.gruppe > i{position:absolute;left:50%;transform:translate(-50%,-50%)}
/* DIE CHILI liegt HINTER dem Kopf (der hat z-index 2, sie keinen).
Sie ist gespiegelt: Der Stiel zeigt damit nach rechts, weg von der
Schnauze. In der Fassung davor lief er quer ueber den Fang -- das
war der Grund, warum das Zeichen unruhig wirkte. */
.gruppe > i.chili{width:${klein ? 40 : 46}%;height:${klein ? 40 : 46}%;top:${klein ? 80 : 84}%;
transform:translate(-50%,-50%) rotate(8deg) scaleX(-1);
background:url("${chili}") center/contain no-repeat;
/* KLEIN OHNE SCHLAGSCHATTEN. Bei 32 px ist der Schatten so breit
wie die Chili selbst -- nachgesehen: aus dem roten Zeichen wurde
ein dunkler Schmier. Klein wird sie stattdessen eine Spur heller
gestellt, weil davon nur noch die Farbe uebrig bleibt. */
filter:${klein
? "saturate(1.06) brightness(1.08)"
: `saturate(.92)
drop-shadow(0 ${Math.max(1, Math.round(n * .004))}px ${Math.round(n * .01)}px rgba(0,0,0,.7))
drop-shadow(0 0 ${Math.round(n * .02)}px rgba(255,86,64,.34))`}}
/* DER KOPF -- hier steckt die eigentliche Verbesserung.
Vorher war er eine MASKE. Von einer Maske zaehlt nur der
Alphakanal, und die Vorlage ist rundum freigestellt: Uebrig blieb
eine geschlossene Flaeche -- ein heller Fleck in Hundeform, ohne
Auge, ohne Schnauze, ohne Ohrinneres. Bei 22 px in der Kopfleiste
war genau das der Grund, warum man nichts erkannt hat.
Jetzt zwei Lagen: unten die Silhouette in Stahlblau, darueber die
ORIGINALZEICHNUNG im Mischmodus "screen". Screen laesst Schwarz
stehen (das Fell bleibt stahlblau) und hebt Weiss heraus (Gesicht,
Ohrinneres, Auge). Aus der Silhouette wird wieder ein Gesicht --
ohne eine einzige neu gezeichnete Linie.
Warum nicht "mask-mode: luminance"? Chromium kann das (geprueft:
CSS.supports meldet true). Aber der Weg ueber zwei Masken ist
empfindlich: Eine Kurzschreibweise "-webkit-mask" in einer
spezifischeren Regel setzt "mask-mode" stillschweigend auf "alpha"
zurueck, und man sieht wieder den Fleck -- genau das ist beim
Bauen passiert, mit richtig aussehendem Quelltext. Der Mischmodus
hat diese Falle nicht.
ZWEITE MASKE, DER AUSLAUF: Die Vorlage ist am unteren Rand
abgeschnitten, der Hals endet gerade. Als Symbol sah das aus wie
ein Fehler beim Freistellen. Der Verlauf blendet den Hals nach
unten aus, statt ihn abzuschneiden. */
.gruppe > i.kopf{width:${klein ? 76 : 88}%;height:${klein ? 76 : 88}%;
top:${klein ? 44 : 43}%;
z-index:2;isolation:isolate;
background:linear-gradient(162deg,#5486b2 0%,#25507a 44%,#0e2b45 100%);
${klein
/* KLEIN OHNE AUSLAUF. Bei 32 px frisst der Verlauf gerade die
Schnauze weg -- nachgesehen und 8-fach vergroessert
verglichen: mit Auslauf ist es ein Berg, ohne Auslauf ein
Hundekopf. Die gerade Kante am Hals ist dort drei Bildpunkte
hoch und faellt nicht auf. */
? `-webkit-mask:url("${husky}") center/contain no-repeat;
mask:url("${husky}") center/contain no-repeat;`
: `-webkit-mask:url("${husky}") center/contain no-repeat,
linear-gradient(180deg,#000 58%, transparent 86%);
-webkit-mask-composite:source-in;
mask:url("${husky}") center/contain no-repeat,
linear-gradient(180deg,#000 58%, transparent 86%);
mask-composite:intersect;`}
filter:drop-shadow(0 ${Math.round(n * .006)}px ${Math.round(n * .016)}px rgba(0,0,0,.7))
drop-shadow(0 0 ${Math.round(n * .022)}px rgba(140,200,245,.34))}
.gruppe > i.kopf .zeichnung{position:absolute;inset:0;display:block;
background:url("${husky}") center/contain no-repeat;
mix-blend-mode:screen;
/* Ohne den Daempfer wird das Weiss der Vorlage zu reinem Weiss --
auf dunklem Grund blendet das. Augenschonend heisst hier: hell
genug zum Erkennen, nicht heller. */
filter:brightness(.88)}
.gruppe > i.kopf.flach{background:linear-gradient(168deg,#ffffff 4%,#e4eef8 38%,#adc0d4 100%)}
.gruppe > i.kopf.flach .zeichnung{display:none}
${weglassen === "zeichnung" ? ".gruppe > i.kopf .zeichnung{display:none}" : ""}
${weglassen === "chili" ? ".gruppe > i.chili{display:none}" : ""}
</style></head><body><div class="feld"><div class="gruppe">
<i class="kopf${klein ? " flach" : ""}"><b class="zeichnung"></b></i>
<i class="chili"></i>
</div></div></body></html>`;
}
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), chili = alsDaten(CHILI);
let gebaut = 0;
const maengel = [];
/* Eine Fassung aufnehmen. `weglassen` steuert die Gegenprobe. */
async function aufnehmen(n, maskable, weglassen = "") {
await seite.setViewportSize({ width: n, height: n });
await seite.setContent(html(n, maskable, husky, chili, 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 zwischen zwei Aufnahmen sichtbar
unterscheiden. Acht Stufen Abstand, damit Kantenglaettung allein den
Wert nicht hochtreibt. */
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)];
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")]);
}
for (const g of GROESSEN) {
const bild = await aufnehmen(g.n, !!g.maskable);
writeFileSync(join(ZIEL, `workspace-${g.endung}.png`), bild);
gebaut++;
const streuung = await streuungVon(bild);
if (streuung < 12) maengel.push(`${g.endung}: kaum Zeichnung (Streuung ${streuung.toFixed(1)})`);
/* DIE ZWEI GEGENPROBEN. Beide messen nicht "sieht gut aus", sondern
"ist ueberhaupt da": Wenn das Bild ohne die Zeichnung genauso
aussieht wie mit ihr, dann war die Zeichnung nie zu sehen -- und
genau das war der Fehler der alten Fassung, die trotzdem gruen war. */
const klein = g.n < 64;
const ohneZeichnung = await unterschied(bild, await aufnehmen(g.n, !!g.maskable, "zeichnung"));
const ohneChili = await unterschied(bild, await aufnehmen(g.n, !!g.maskable, "chili"));
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 {
/* Das Gesicht bedeckt gemessen rund ein Siebtel der Flaeche; 4 %
ist der Boden, unter den es nur durch einen Fehler faellt. */
if (ohneZeichnung < 0.04) maengel.push(
`${g.endung}: das Gesicht kommt nicht durch (${(ohneZeichnung * 100).toFixed(1)} %)`);
}
/* Die Chili gehoert in JEDE Fassung, gerade in die kleine. Sie nimmt
gemessen 4 bis 7 % ein; 1 % ist der Boden. */
if (ohneChili < 0.01) maengel.push(
`${g.endung}: die Chili fehlt (${(ohneChili * 100).toFixed(1)} %)`);
console.log(` workspace-${g.endung}.png`.padEnd(32)
+ `${(bild.length / 1024).toFixed(1)} KB · Streuung ${streuung.toFixed(1)}`
+ ` · Gesicht ${(ohneZeichnung * 100).toFixed(1)} %`
+ ` · Chili ${(ohneChili * 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 (maengel.length) {
console.error("\nFEHLER:\n " + maengel.join("\n ")); process.exit(1);
}
console.log(`\n${gebaut} Symbole erzeugt. Husky-Gesicht und Chili sind in jeder`
+ ` grossen Fassung nachweisbar sichtbar, die kleine bleibt bewusst schlicht.`);