Files
DogFatherGitandClaude Opus 5 2872f714f3 Ein App-Symbol aus beiden Marken: Husky in der Mitte, Chili als Sockel
Filipe: "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.

Der Husky steht im Zentrum -- eine Silhouette traegt bei kleiner Groesse
am besten. Die Chili liegt als Bogen darunter, wie ein Sockel. Der
Hintergrund traegt beide Farben: Chili-Rot unten links, Dogi-Blau oben
rechts, und sie treffen sich in der Mitte -- dieselbe Klammer wie im
Schriftzug "Spicy & Dogi" in der Kopfleiste.

Der Husky wird als MASKE eingesetzt und mit Silber gefuellt: Das
Original ist schwarzweiss und waere auf dunklem Grund ein dunkler Fleck.

UNTER 64 px FAELLT DIE CHILI WEG. Sie waere dort ein verwaschener Fleck
und wuerde die Husky-Silhouette anfressen. Ein Symbol, das klein noch
erkennbar ist, ist mehr wert als eins, das alle Bestandteile zeigt und
dabei zu Matsch wird.

EINMAL NACHGEBESSERT nach dem Blick aufs Bild: Die Chili sass zuerst
hoeher und schnitt dem Husky die Brust ab. Jetzt liegt sie tiefer, er
steht vollstaendig.

GEPRUEFT WIRD NICHT DIE DATEIGROESSE, sondern die Streuung der
Helligkeit. Ein Symbol, das aus einem Fehler heraus einfarbig ist, hat
dieselbe Bytezahl wie eines mit Motiv -- die beweist also nichts. Ein
leeres Feld hat keine Streuung; gemessen wurden 48 bis 69 bei einer
Untergrenze von 12, unter der das Werkzeug abbricht.

EINE FALLE MITENTSCHAERFT: `tools/app-symbole.mjs` fuehrte den Workspace
noch in seiner Liste und haette die sechs Dateien beim naechsten Lauf
stillschweigend ueberschrieben -- gleiche Namen, gleicher Ordner, kein
Fehler, nur wieder das alte Symbol. Er steht dort nicht mehr.

pruef-workspace-seiten EXIT=0 (32), pruef-assets EXIT=0.
Nebenbefund, NICHT von hier: pruef-verwaltung-app scheitert an einem
MODULE_NOT_FOUND -- gegen den alten Stand gegengeprueft, dort derselbe
Fehler. Vorbestehend, gehoert auf die offene Liste.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-08 14:19:08 +02:00

678 lines
32 KiB
JavaScript

/* ===================================================================
DIE APP-SYMBOLE: ein Logo, zehn Farben, ein Stil.
Wunsch Filipe (04.09.2026): "alle sollen mein Logo drauf haben, aber
alle so, dass sie anders aussehen durch Farbänderung ... und die
sollen alle extrem speziell aussehen."
ZWEI AUFGABEN IN EINER DATEI:
node tools/app-symbole.mjs entwurf rendert alle Stile nebeneinander
als EIN Übersichtsbild zum Wählen
node tools/app-symbole.mjs bauen erzeugt die echten Symbole im
gewählten Stil, in allen Größen
WARUM ERST ENTWERFEN, DANN BAUEN. Genau so ist in diesem Haus das
Knopf-System entstanden ("von Filipe aus drei gerenderten Entwürfen
gewählt"). Die Notiz dazu sagt: *Raten hilft hier nicht.* Vierzig
Symbole zu erzeugen, bevor jemand den Stil gesehen hat, hieße vierzig
Symbole zweimal zu erzeugen.
DIE FARBEN WERDEN GERECHNET, NICHT GEWÄHLT — dieselbe Regel wie bei
den 17 Kachelfarben (tools/kachel-farben.mjs). Zehn Töne gleichmäßig
über den Farbkreis in OKLCH: Dort sind gleiche Abstände auch für das
Auge gleich groß. Frei gewählte Farben ergeben regelmäßig Paare, die
praktisch gleich aussehen — das wirkt nach Zufall, nicht nach Absicht.
AUGENSCHONEND, dauerhafte Hausregel: gedeckte Töne statt Neon,
dunkler Grund, keine blendenden Kontraste. Die Symbole liegen auf
einem Startbildschirm und werden hundertmal am Tag angesehen.
Gerechnet im Browser, der für die Prüfungen ohnehin da ist — kein
Bildprogramm, keine neue Abhängigkeit, 0 EUR.
=================================================================== */
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 LOGO = join(WURZEL, "assets", "img", "marke", "logo-kopf-frei.png");
if (!existsSync(LOGO)) {
console.error(`Der freigestellte Logokopf fehlt: ${LOGO}`);
console.error("Erst node tools/logo-freistellen.mjs laufen lassen.");
process.exit(1);
}
/* ---------------------------------------------------------------------
DIE APPS. Alles, was an dogfather-universe.com hängt -- ohne
safeaddress (ausdrücklich ausgenommen) und ohne die beiden Seiten,
die VanVan gehören (andere Domain).
Die Reihenfolge ist die Reihenfolge auf dem Farbkreis: Nachbarn in
dieser Liste bekommen weit auseinanderliegende Töne, damit zwei
Symbole nebeneinander nie ähnlich aussehen.
------------------------------------------------------------------ */
export const APPS = [
{ schluessel: "universe", name: "Dogfather Universe", kurz: "Universe", wo: "dogfather-universe.com" },
/* DER WORKSPACE STEHT HIER NICHT MEHR (08.09.2026).
Sein Symbol ist seit heute kein eingefaerbtes Logo mehr, sondern
eine Mischung aus Husky UND Chili -- gebaut von
tools/workspace-symbol.mjs (Wunsch Filipe: "eine geile mischung
von diesen zwei logo").
Bliebe er in dieser Liste, wuerde der naechste Lauf von
`app-symbole.mjs bauen` die sechs Dateien stillschweigend
ueberschreiben -- gleiche Namen, gleicher Ordner. Nichts wuerde
scheitern, das Symbol waere nur wieder das alte, und niemand
wuesste warum. Eine Zeile, die man nicht loescht, ist eine Falle,
die man spaeter selbst stellt. */
{ schluessel: "webdesign", name: "Dogfather Webdesign", kurz: "Webdesign", wo: "/webdesign" },
{ schluessel: "portal", name: "Kundenportal", kurz: "Portal", wo: "/webdesign/portal.html" },
{ schluessel: "wdverwalt", name: "Webdesign-Verwaltung", kurz: "WD-Verw.", wo: "/webdesign/verwaltung.html" },
{ schluessel: "verwaltung", name: "DogiCrew-Verwaltung", kurz: "Verwaltung", wo: "/verwaltung.html" },
{ schluessel: "zocker", name: "Dogi's ZockerAnstalt", kurz: "Zocker", wo: "zockeranstalt." },
{ schluessel: "analyse", name: "DogFather Analyse", kurz: "Analyse", wo: "analyse." },
{ schluessel: "git", name: "DogFather Git", kurz: "Git", wo: "git." },
{ schluessel: "cloud", name: "DogFather Cloud", kurz: "Cloud", wo: "cloud." },
];
/* ---------------------------------------------------------------------
FARBEN IN OKLCH -- übernommen aus tools/kachel-farben.mjs, damit im
Haus nur eine Rechenweise existiert.
------------------------------------------------------------------ */
function oklchZuRgb(L, C, hGrad) {
const h = hGrad * Math.PI / 180;
const a = C * Math.cos(h), b = C * Math.sin(h);
const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
const l = l_ ** 3, m = m_ ** 3, s = s_ ** 3;
const r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
const g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
const bl = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s;
const gam = (v) => (v <= 0.0031308 ? 12.92 * v : 1.055 * Math.pow(v, 1 / 2.4) - 0.055);
return [gam(r), gam(g), gam(bl)];
}
const imRaum = (rgb) => rgb.every((v) => v >= -0.001 && v <= 1.001);
const hex = (rgb) => "#" + rgb.map((v) =>
Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, "0")).join("");
/** Ein Ton bei gegebener Helligkeit -- die Farbstärke wird so weit
* gesenkt, bis sie darstellbar ist (Gelb und Grün verlassen den Raum
* früher als Blau). */
function ton(hGrad, L, cWunsch) {
let C = cWunsch;
while (C > 0.01 && !imRaum(oklchZuRgb(L, C, hGrad))) C -= 0.004;
return hex(oklchZuRgb(L, C, hGrad));
}
/* Startwinkel so gelegt, dass Universe das Blau der Marke behält.
0,655 Helligkeit und 0,148 Farbstärke sind die Hauswerte aus den
Kachelfarben -- kräftig genug zum Unterscheiden, weit weg von Neon. */
const START = 232;
export const FARBEN = APPS.map((app, i) => {
const h = (START + (360 / APPS.length) * i) % 360;
return {
...app,
winkel: Math.round(h),
hell: ton(h, 0.78, 0.115), // Lichtkante, Schein
mittel: ton(h, 0.655, 0.148), // die Kennfarbe
tief: ton(h, 0.42, 0.115), // Verlauf nach unten
dunkel: ton(h, 0.26, 0.075), // Grundfläche
};
});
/* ---------------------------------------------------------------------
DIE STILE — einer je App, nicht einer für alle.
Filipe, nachdem er die ersten vier Entwürfe gesehen hatte
(04.09.2026): *"sie sollen alle alle alle alle unterschiedlich sein,
also benutz die alle und du wirst noch mehr brauchen. alle
verknüpfungen sollen unterschiedlich sein und alle apps von meiner
domain."*
Damit ist die Aufgabe eine andere als zuerst gedacht: nicht ein Stil
in zehn Farben, sondern ZEHN STILE in zehn Farben. Jedes Symbol ist
damit in Farbe UND Form einmalig.
WAS DIE FAMILIE ZUSAMMENHÄLT, wenn alles verschieden ist: dreierlei,
und das ist überall gleich:
* dasselbe Logo, immer mittig, in derselben Größe
* dieselbe Rundung der Ecken (22,4 %, die Apple-Rundung)
* gedeckte Töne aus einer gerechneten Reihe, nie Neon
Ohne diesen Anker wären es zehn fremde Symbole statt einer Familie.
HELL ODER DUNKEL — daran hängt, ob das Logo gedreht werden muss. Der
Kopf ist überwiegend schwarz: Auf hellem Grund bleibt er wie er ist,
auf dunklem wird er per `invert` umgekehrt (die Zeichnung bleibt
dabei erhalten, anders als bei einer flachen Weißfärbung).
------------------------------------------------------------------ */
export const STILE = {
glut: {
name: "A · Glut",
beschreibung: "Farbverlauf von oben, dunkler Fuß, feine Lichtkante obenauf — "
+ "dieselbe Sprache wie das Knopf-System „Geschliffen“.",
grund: (f) => `linear-gradient(160deg, ${f.mittel} 0%, ${f.tief} 52%, ${f.dunkel} 100%)`,
zusatz: (f) => `
.rand { box-shadow: inset 0 0 0 var(--r2) ${f.hell}55,
inset 0 var(--r3) var(--r6) ${f.hell}30; }
.glanz { background: linear-gradient(180deg, #ffffff38, transparent 46%); }`,
logo: "filter: drop-shadow(0 var(--r2) var(--r4) #0009);",
},
ring: {
name: "B · Neonring",
beschreibung: "Fast schwarzer Grund, ein leuchtender Ring in der Kennfarbe. "
+ "Am stärksten unterscheidbar, weil die Farbe als Form auftritt.",
grund: () => "radial-gradient(circle at 50% 42%, #141a24 0%, #0a0e15 70%, #070a10 100%)",
zusatz: (f) => `
.rand { box-shadow: inset 0 0 0 var(--r5) ${f.mittel},
inset 0 0 var(--r10) ${f.hell}66,
0 0 var(--r6) ${f.mittel}44; }
.glanz { background: radial-gradient(circle at 50% 30%, ${f.mittel}22, transparent 62%); }`,
/* UMGEKEHRT. Der Logokopf ist ueberwiegend schwarz; auf dem fast
schwarzen Grund dieses Stils verschwand er im ersten Entwurf
nahezu vollstaendig -- sichtbar blieb ein Ring um ein dunkles
Nichts. `invert` dreht nur die Helligkeit um: Aus dem schwarzen
Fell wird helles, die Zeichnung (Augen, Schnauze, Ohren) bleibt
vollstaendig erhalten. Eine reine Weissfaerbung haette daraus
eine flache Silhouette gemacht. */
logo: "filter: invert(1) drop-shadow(0 0 var(--r5) #000c);",
},
schraeg: {
name: "C · Schräg geteilt",
beschreibung: "Zwei Töne derselben Farbe, diagonal getrennt — ruhig, "
+ "modern, auf kleinen Symbolen gut auseinanderzuhalten.",
grund: (f) => `linear-gradient(118deg, ${f.mittel} 0%, ${f.mittel} 48%, `
+ `${f.tief} 48%, ${f.dunkel} 100%)`,
zusatz: (f) => `
.rand { box-shadow: inset 0 0 0 var(--r2) #ffffff1f; }
.glanz { background: linear-gradient(118deg, transparent 46%, ${f.hell}18 48%, transparent 56%); }`,
logo: "filter: drop-shadow(0 var(--r2) var(--r3) #0008);",
},
aura: {
name: "D · Aura",
beschreibung: "Dunkler Grund, ein weicher Schein hinter dem Kopf. "
+ "Das Logo steht frei, die Farbe atmet dahinter.",
grund: (f) => `radial-gradient(circle at 50% 52%, ${f.tief} 0%, `
+ `${f.dunkel} 55%, #080b11 100%)`,
zusatz: (f) => `
.rand { box-shadow: inset 0 0 0 var(--r2) ${f.mittel}4d; }
.glanz { background: radial-gradient(circle at 50% 50%, ${f.hell}30, transparent 58%); }`,
/* Ebenfalls umgekehrt -- gleicher Grund wie bei B: dunkler Grund,
dunkles Logo. Der farbige Schein liegt dahinter und traegt den
jetzt hellen Kopf. */
logo: (f) => `filter: invert(1) drop-shadow(0 0 var(--r8) ${f.mittel}) drop-shadow(0 var(--r2) var(--r4) #000a);`,
},
/* ---- ab hier die sechs weiteren, damit jede App ihren eigenen bekommt ---- */
medaillon: {
name: "E · Medaillon",
beschreibung: "Heller Farbring auf dunklem Grund, der Kopf sitzt in einer "
+ "runden Scheibe — offiziell, wie ein Siegel.",
grund: (f) => `radial-gradient(circle at 50% 50%, ${f.hell} 0%, ${f.hell} 33%, `
+ `${f.mittel} 33.5%, ${f.tief} 55%, ${f.dunkel} 56%, ${f.dunkel} 100%)`,
zusatz: (f) => `
.rand { box-shadow: inset 0 0 0 var(--r3) ${f.mittel}88,
inset 0 0 var(--r8) #0007; }
.glanz { background: linear-gradient(180deg, #ffffff22, transparent 40%); }`,
logo: "filter: drop-shadow(0 var(--r2) var(--r3) #0007);",
},
streifen: {
name: "F · Streifen",
beschreibung: "Feine Schrägstreifen in zwei Tönen — technisch, ruhig, "
+ "erkennbar auch wenn das Symbol sehr klein ist.",
grund: (f) => `repeating-linear-gradient(126deg, ${f.mittel} 0 8%, ${f.tief} 8% 16%)`,
zusatz: (f) => `
.rand { box-shadow: inset 0 0 0 var(--r2) ${f.hell}44,
inset 0 0 var(--r10) #0006; }
.glanz { background: radial-gradient(circle at 50% 38%, #ffffff26, transparent 60%); }`,
logo: "filter: drop-shadow(0 var(--r2) var(--r4) #000b);",
},
praegung: {
name: "G · Prägung",
beschreibung: "Der Kopf wirkt in die Fläche eingedrückt — Licht von oben, "
+ "Schatten darunter. Plastisch, ohne bunt zu werden.",
grund: (f) => `linear-gradient(180deg, ${f.tief} 0%, ${f.mittel} 48%, ${f.tief} 100%)`,
zusatz: (f) => `
.rand { box-shadow: inset 0 var(--r3) var(--r6) ${f.hell}55,
inset 0 calc(-1 * var(--r3)) var(--r6) #0008,
inset 0 0 0 var(--r2) #ffffff1a; }
.glanz { background: linear-gradient(180deg, #ffffff2e, transparent 34%); }`,
logo: "filter: drop-shadow(0 calc(-1 * var(--r2)) 0 #ffffff55) "
+ "drop-shadow(0 var(--r2) var(--r2) #0009);",
},
ecke: {
name: "H · Eckband",
beschreibung: "Ein breites Farbband über die untere Ecke, darüber ruhiger "
+ "Grund — wie eine Schärpe.",
grund: (f) => `linear-gradient(315deg, ${f.mittel} 0 34%, ${f.tief} 34% 41%, `
+ `${f.dunkel} 41% 100%)`,
zusatz: (f) => `
.rand { box-shadow: inset 0 0 0 var(--r2) ${f.mittel}55; }
.glanz { background: radial-gradient(circle at 50% 40%, ${f.hell}1f, transparent 62%); }`,
logo: "filter: invert(1) drop-shadow(0 var(--r2) var(--r4) #000b);",
},
raster: {
name: "I · Raster",
beschreibung: "Ein feines Gitter aus Punkten in der Kennfarbe — für alles, "
+ "wo es um Zahlen und Daten geht.",
grund: (f) => `radial-gradient(${f.mittel} 22%, transparent 23%) 0 0 / 12% 12%, `
+ `linear-gradient(160deg, ${f.dunkel}, #080b11)`,
zusatz: (f) => `
.rand { box-shadow: inset 0 0 0 var(--r3) ${f.mittel}77,
inset 0 0 var(--r10) #000a; }
/* Die Beruhigung gehört HINTER den Kopf, nicht darüber -- sonst
verdeckt sie genau das, was sie lesbar machen soll. */
.unterlage { background: radial-gradient(circle at 50% 48%,
#070a10ee 0%, #070a10cc 34%, transparent 62%); }
.glanz { background: radial-gradient(circle at 50% 26%, ${f.hell}14, transparent 58%); }`,
logo: (f) => `filter: invert(1) drop-shadow(0 0 var(--r5) #000c) `
+ `drop-shadow(0 var(--r2) var(--r3) #000b);`,
},
doppelring: {
name: "J · Doppelring",
beschreibung: "Zwei Ringe ineinander, außen kräftig, innen fein — "
+ "wirkt wie ein Abzeichen.",
grund: (f) => `radial-gradient(circle at 50% 50%, ${f.dunkel} 0 40%, `
+ `${f.mittel} 40.5% 45%, ${f.dunkel} 45.5% 52%, ${f.tief} 52.5% 100%)`,
zusatz: (f) => `
.rand { box-shadow: inset 0 0 0 var(--r4) ${f.mittel},
inset 0 0 0 var(--r6) ${f.dunkel},
inset 0 0 var(--r8) ${f.hell}33; }
.glanz { background: linear-gradient(200deg, #ffffff1c, transparent 44%); }`,
logo: (f) => `filter: invert(1) drop-shadow(0 0 var(--r5) ${f.dunkel}) `
+ `drop-shadow(0 var(--r2) var(--r3) #000a);`,
},
};
/* WER BEKOMMT WELCHEN. Nicht ausgewürfelt, sondern nach dem, was die
App ist: Das Spiel bekommt den Neonring, die Datenauswertung das
Raster, die Verwaltung das Abzeichen, das Kundenportal das Siegel.
So trägt die Form dieselbe Aussage wie die Farbe. */
export const STIL_JE_APP = {
universe: "glut", // die Hauptmarke: edel, Verlauf, Lichtkante
workspace: "aura", // Arbeitsplatz: ruhig, das Logo steht frei
webdesign: "schraeg", // Design: die klare, moderne Teilung
portal: "medaillon", // Kundenseite: offiziell wie ein Siegel
wdverwalt: "praegung", // technische Verwaltung: plastisch, sachlich
verwaltung: "doppelring", // DogiCrew-Verwaltung: Abzeichen
zocker: "ring", // Spielhalle: Neon, das Auffälligste
analyse: "raster", // Zahlen und Daten: das Gitter
git: "streifen", // Technik: Schrägstreifen
cloud: "ecke", // Ablage: das ruhige Eckband
};
/* ---------------------------------------------------------------------
Ein einzelnes Symbol als HTML. Alle Maße hängen an der Kantenlänge,
damit 192 und 512 wirklich dasselbe Bild ergeben und nicht zwei
verschieden proportionierte.
------------------------------------------------------------------ */
/** Nur der Bauteil (CSS + Markup) eines Symbols, mit EIGENER Klasse.
*
* ERSTER ENTWURF WAR FALSCH: Er gab je Symbol ein vollständiges
* Dokument mit den Klassen `.symbol`, `.rand`, `.glanz` zurück. In der
* Übersicht landeten dadurch vierzig gleichnamige Regelsätze in einem
* Dokument -- der letzte gewann, und alle vierzig Symbole sahen
* identisch blau aus. Die Farben waren gerechnet, kamen aber nie an.
*
* Jede Kachel bekommt deshalb einen eigenen Namen aus Stil und App.
* Damit lässt sich beliebig viel davon in ein Dokument legen, ohne
* dass sich zwei ins Gehege kommen. */
export function symbolTeil(f, stilName, n, { maskable = false, apple = false, logoDaten } = {}) {
const stil = STILE[stilName];
const logoFilter = typeof stil.logo === "function" ? stil.logo(f) : stil.logo;
/* MASKABLE: DAS GANZE SYMBOL VERKLEINERN, nicht nur das Logo.
BEFUND 05.09.2026, von Filipe gemeldet: *"die symbole sehen aber auf
meinem pc nicht aus wie auf dem browser mit diesen geilen farben."*
Und er hatte recht -- die maskable-Fassungen sahen aus wie eine
schlechte Kopie: blass, das Medaillon riesig, der Doppelring über
den Rand hinaus.
Der erste Entwurf verkleinerte nur das LOGO (52 % statt 68 %) und
liess den Hintergrund unveraendert. Das geht bei einer glatten
Flaeche gut und bei allem anderen schief: Medaillon, Doppelring,
Raster und Eckband rechnen ihre Kreise und Kanten in PROZENT DER
FLAECHE. Wird aussen ein Fünftel weggeschnitten, sitzt der Ring
nicht mehr da, wo er hingehoert -- und das Logo schwimmt verloren
in der Mitte.
Richtig ist, das FERTIGE Symbol als Ganzes auf den sicheren Bereich
zu verkleinern und den Rest mit dem dunkelsten Ton der App zu
fuellen. Dann bleibt jedes Verhaeltnis erhalten: Ring zu Flaeche,
Logo zu Ring, Verlauf zu Kante. Was das System wegschneidet, ist
nur ruhige Randfarbe.
78 % ist der Anteil, der bei jeder gaengigen Maske heil bleibt
(Kreis, Quadrat mit runden Ecken, Tropfen). */
/* APPLE-TOUCH-ICON: quadratisch, undurchsichtig, OHNE eigene Rundung.
BEFUND 05.09.2026 ("auf dem einen klappt es und auf dem anderen
nicht"). iOS holt sein Symbol NICHT aus dem Manifest, sondern aus
<link rel="apple-touch-icon">. Und es behandelt diese Datei anders
als jeder andere Browser: Es rundet die Ecken SELBST ab und fuellt
alles Durchsichtige mit SCHWARZ.
Unser Symbol brachte seine eigene Rundung mit -- also durchsichtige
Ecken. Auf dem iPhone wurden daraus schwarze Zipfel, die dann ein
zweites Mal beschnitten wurden. Auf dem PC faellt das nicht auf,
weil Windows und Chrome mit Transparenz umgehen koennen. Genau
daher der Unterschied zwischen den beiden Geraeten.
Deshalb hier: randvoll, eckig, deckend. Die Rundung macht iOS. */
const radius = (maskable || apple) ? 0 : 22.4; // 22,4 % ist die Apple-Rundung
const k = `s-${stilName}-${f.schluessel}${maskable ? "-m" : ""}${apple ? "-a" : ""}`;
/* Das eigentliche Symbol ist bei maskable kleiner als die Datei. */
const innen = maskable ? Math.round(n * 0.78) : n;
const logoBreite = 68; // immer gleich -- siehe oben
const css = `
${maskable ? `
.${k}-huelle {
width: ${n}px; height: ${n}px; flex: none;
display: flex; align-items: center; justify-content: center;
background: ${f.dunkel};
}` : ""}
.${k} {
position: relative; width: ${innen}px; height: ${innen}px; overflow: hidden;
border-radius: ${maskable ? 18 : radius}%; flex: none;
--r2: ${innen * 0.006}px; --r3: ${innen * 0.012}px; --r4: ${innen * 0.016}px;
--r5: ${innen * 0.022}px; --r6: ${innen * 0.03}px; --r8: ${innen * 0.05}px;
--r10: ${innen * 0.07}px;
background: ${stil.grund(f)};
}
.${k} > .rand, .${k} > .glanz, .${k} > .unterlage {
position: absolute; inset: 0; border-radius: ${radius}%; pointer-events: none;
}
.${k} > .logo {
position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
width: ${logoBreite}%; ${logoFilter}
}
${stil.zusatz(f)
.replace(/\.rand/g, `.${k} > .rand`)
.replace(/\.glanz/g, `.${k} > .glanz`)
.replace(/\.unterlage/g, `.${k} > .unterlage`)}`;
/* DREI EBENEN, und die Reihenfolge ist wichtig:
unterlage liegt HINTER dem Logo -- zum Abdunkeln oder Aufhellen
des Grundes genau dort, wo der Kopf steht
logo der Kopf
glanz Lichtstimmung ÜBER dem Kopf
rand Kante und Ring, ganz oben
Der erste Entwurf hatte keine `unterlage`. Beim Raster-Stil sollte
der Grund hinter dem Kopf abgedunkelt werden, damit der helle Kopf
lesbar bleibt -- die Abdunklung lag mangels eigener Ebene in
`glanz` und damit ÜBER dem Kopf. Ergebnis: Das Logo war fast
unsichtbar. Genau so verschwindet ein Fehler in einem Bild, das
"irgendwie dunkel" aussieht. */
const kern = `<div class="${k}">
<div class="unterlage"></div>
<img class="logo" src="${logoDaten}" alt="">
<div class="glanz"></div>
<div class="rand"></div>
</div>`;
/* Bei maskable liegt das Symbol in einer Huelle aus der Grundfarbe --
die faengt ab, was das System wegschneidet. */
const markup = maskable ? `<div class="${k}-huelle">${kern}</div>` : kern;
return { css, markup, klasse: maskable ? `${k}-huelle` : k };
}
/** Ein vollständiges Dokument für genau ein Symbol -- für den Bau. */
export function symbolHtml(f, stilName, n, opt = {}) {
const t = symbolTeil(f, stilName, n, opt);
return `<!doctype html><html><head><meta charset="utf-8"><style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: ${n}px; height: ${n}px; background: transparent; }
${t.css}
</style></head><body>${t.markup}</body></html>`;
}
/* Ein durchsichtiger Punkt -- damit lässt sich dasselbe Symbol ohne
Logo zeichnen, für die Vergleichsmessung beim Bauen. */
export const LEER = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
export const logoAlsDaten = () =>
"data:image/png;base64," + readFileSync(LOGO).toString("base64");
/* ---------------------------------------------------------------------
ENTWURF: alle Stile nebeneinander, zum Aussuchen.
------------------------------------------------------------------ */
async function entwerfen() {
const { chromium } = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await chromium.launch();
const logoDaten = logoAlsDaten();
const K = 116; // Kantenlänge je Musterbild
let reihen = "";
let allesCss = "";
/* ---- Zuerst: die ECHTE Zuordnung, so wie sie am Ende aussieht ----
Das ist die eigentliche Auskunft. Die Stilreihen darunter dienen
nur noch dem Vergleich. */
{
let zellen = "";
for (const f of FARBEN) {
const stilName = STIL_JE_APP[f.schluessel];
const gross = symbolTeil(f, stilName, 132, { logoDaten });
allesCss += gross.css;
zellen += `<div class="zelle gross">${gross.markup}
<span class="bez"><b>${f.kurz}</b><br>${STILE[stilName].name}</span></div>`;
}
reihen += `<section class="reihe">
<h2>SO WIRD ES — jede App ihr eigener Stil, ihre eigene Farbe</h2>
<p>Zehn Apps, zehn Stile, zehn gerechnete Farben. Zusammengehalten wird das
durch dasselbe Logo, dieselbe Eckenrundung und eine gemeinsame Farbreihe.</p>
<div class="zeile">${zellen}</div></section>
<hr class="trenn">`;
}
for (const [schluessel, stil] of Object.entries(STILE)) {
let zellen = "";
for (const f of FARBEN) {
const t = symbolTeil(f, schluessel, K, { logoDaten });
allesCss += t.css;
const eigner = Object.entries(STIL_JE_APP)
.find(([app, s]) => s === schluessel && app === f.schluessel);
zellen += `<div class="zelle${eigner ? " markiert" : ""}">${t.markup}`
+ `<span class="bez">${f.kurz}</span></div>`;
}
reihen += `<section class="reihe">
<h2>${stil.name}</h2><p>${stil.beschreibung}</p>
<div class="zeile">${zellen}</div></section>`;
}
const seite = await (await browser.newContext({
viewport: { width: 1560, height: 1200 }, deviceScaleFactor: 2 })).newPage();
await seite.setContent(`<!doctype html><html><head><meta charset="utf-8"><style>
* { box-sizing: border-box; }
body { margin: 0; padding: 30px 34px; background: #0a0d13;
font-family: "Segoe UI", system-ui, sans-serif; color: #dbe4ef; }
h1 { font-size: 21px; margin: 0 0 4px; letter-spacing: .01em; }
.unter { color: #94a5bb; font-size: 13px; margin: 0 0 22px; }
.reihe { margin-bottom: 26px; }
.reihe h2 { font-size: 15px; margin: 0 0 3px; color: #3fbdf5; letter-spacing: .04em; }
.reihe p { font-size: 12.5px; color: #94a5bb; margin: 0 0 12px; max-width: 900px; line-height: 1.5; }
.zeile { display: flex; gap: 14px; flex-wrap: nowrap; }
.zelle { text-align: center; width: ${K}px; flex: none; }
.bez { display: block; margin-top: 6px; font-size: 10.5px; color: #75859a; line-height: 1.4; }
.zelle.gross { width: 132px; }
.zelle.gross .bez { font-size: 11px; color: #94a5bb; }
.zelle.gross .bez b { color: #dbe4ef; }
.zelle.markiert { position: relative; }
.zelle.markiert::after {
content: "gewählt"; position: absolute; left: 50%; transform: translateX(-50%);
bottom: -15px; font-size: 9px; color: #3fbdf5; letter-spacing: .05em; }
.zelle.markiert .bez { color: #3fbdf5; }
.trenn { border: 0; border-top: 1px solid #1e2836; margin: 4px 0 24px; }
.reihe h2 { text-transform: none; }
${allesCss}
</style></head><body>
<h1>App-Symbole — jede App eigener Stil, eigene Farbe</h1>
<p class="unter">Oben die endgültige Zuordnung. Darunter jeder Stil einmal in allen
zehn Farben, damit der Vergleich möglich bleibt.</p>
${reihen}
</body></html>`, { waitUntil: "networkidle" });
await seite.waitForTimeout(600);
const ziel = join(WURZEL, "entwurf-app-symbole.png");
await seite.screenshot({ path: ziel, fullPage: true });
await browser.close();
console.log("Farben (gerechnet, nicht gewählt):");
for (const f of FARBEN) {
console.log(` ${f.kurz.padEnd(12)} ${String(f.winkel).padStart(3)}° ${f.mittel} ${f.wo}`);
}
console.log(`\nEntwurf gespeichert: ${ziel.replace(WURZEL, ".")}`);
}
/* ---------------------------------------------------------------------
BAUEN: die echten Dateien, in allen Größen, für alle Apps.
------------------------------------------------------------------ */
/* Welche Größen jede App braucht.
192/512 any die zwei Pflichtgrößen eines Manifests
192/512 maskable Android schneidet bis zu 20 % ringsum weg; dafür
sitzt das Logo kleiner und die Ecken bleiben eckig
180 apple-touch-icon (iPhone-Startbildschirm)
32 das Lesezeichen-Symbol im Browserreiter */
const GROESSEN = [
{ n: 192, maskable: false, endung: "192" },
{ n: 512, maskable: false, endung: "512" },
{ n: 192, maskable: true, endung: "192-maskable" },
{ n: 512, maskable: true, endung: "512-maskable" },
{ n: 180, maskable: false, apple: true, endung: "180" },
{ n: 32, maskable: false, endung: "32" },
];
async function bauen() {
const { chromium } = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await chromium.launch();
const logoDaten = logoAlsDaten();
const ziel = join(WURZEL, "assets", "img", "app-symbole");
if (!existsSync(ziel)) mkdirSync(ziel, { recursive: true });
const seite = await (await browser.newContext({ deviceScaleFactor: 1 })).newPage();
let gebaut = 0;
const schwach = [];
for (const f of FARBEN) {
const stilName = STIL_JE_APP[f.schluessel];
if (!stilName || !STILE[stilName]) {
console.error(`Kein Stil für ${f.schluessel} — abgebrochen.`);
process.exit(1);
}
for (const g of GROESSEN) {
await seite.setViewportSize({ width: g.n, height: g.n });
await seite.setContent(symbolHtml(f, stilName, g.n, { maskable: g.maskable, apple: g.apple, logoDaten }),
{ waitUntil: "networkidle" });
await seite.waitForTimeout(60);
const datei = join(ziel, `${f.schluessel}-${g.endung}.png`);
await seite.screenshot({ path: datei, omitBackground: true });
gebaut++;
}
/* ---- KONTROLLE: hebt sich das Logo wirklich ab? --------------------
ERSTER VERSUCH WAR FALSCH und ist an sich selbst aufgeflogen: Er
verglich die MITTLERE Helligkeit in der Bildmitte mit der am
Rand. Beim Streifen-Stil ergab das "1 Stufe Unterschied" und
damit eine Warnung -- obwohl der Kopf dort bestens zu sehen ist.
Der Grund ist offensichtlich, sobald man ihn kennt: Ein
gleichmäßig gestreifter Grund hat überall denselben Mittelwert,
und ein Logo aus Schwarz UND Weiß hebt ihn kaum an. Gemessen
wurde also etwas, das mit Sichtbarkeit nichts zu tun hat.
Richtig ist die direkte Frage: *Wie stark verändert das Logo das
Bild?* Dafür wird dasselbe Symbol zweimal gezeichnet -- einmal
mit Kopf, einmal ohne -- und Punkt für Punkt verglichen. Was sich
nicht ändert, ist unsichtbar; je größer der mittlere Unterschied
in der Fläche des Kopfes, desto deutlicher steht er da. Das gilt
für jeden Stil gleichermaßen, für gestreifte wie für glatte. */
const messe = async (mitLogo) => {
await seite.setViewportSize({ width: 512, height: 512 });
await seite.setContent(
symbolHtml(f, stilName, 512, { logoDaten: mitLogo ? logoDaten : LEER }),
{ waitUntil: "networkidle" });
await seite.waitForTimeout(60);
const puffer = await seite.screenshot({ omitBackground: true });
return "data:image/png;base64," + puffer.toString("base64");
};
const [mitBild, ohneBild] = [await messe(true), await messe(false)];
const unterschied = await seite.evaluate(async ([a, b]) => {
const lade = async (q) => { const i = new Image(); i.src = q; await i.decode(); return i; };
const [ia, ib] = [await lade(a), await lade(b)];
const mach = (bild) => {
const l = document.createElement("canvas");
l.width = 512; l.height = 512;
const s = l.getContext("2d", { willReadFrequently: true });
s.drawImage(bild, 0, 0);
return s.getImageData(120, 110, 272, 292).data; // die Fläche des Kopfes
};
const da = mach(ia), db = mach(ib);
let summe = 0, n = 0;
for (let i = 0; i < da.length; i += 4) {
const ha = 0.2126 * da[i] + 0.7152 * da[i + 1] + 0.0722 * da[i + 2];
const hb = 0.2126 * db[i] + 0.7152 * db[i + 1] + 0.0722 * db[i + 2];
summe += Math.abs(ha - hb);
n++;
}
return n ? summe / n : 0;
}, [mitBild, ohneBild]);
if (unterschied < 18) {
schwach.push(`${f.kurz} (${STILE[stilName].name}): das Logo ändert das Bild nur um `
+ `${unterschied.toFixed(0)} Helligkeitsstufen`);
}
console.log(` ${f.kurz.padEnd(12)} ${STILE[stilName].name.padEnd(18)} ${f.mittel} `
+ `Logo hebt sich um ${unterschied.toFixed(0)} Stufen ab`);
}
await browser.close();
console.log(`\n${gebaut} Dateien gebaut in ${ziel.replace(WURZEL, ".")}`);
if (gebaut !== FARBEN.length * GROESSEN.length) {
console.error(`FEHLER: erwartet waren ${FARBEN.length * GROESSEN.length}.`);
process.exit(1);
}
if (schwach.length) {
console.error(`\nWARNUNG — hier hebt sich das Logo kaum vom Grund ab:`);
for (const z of schwach) console.error(` ${z}`);
process.exit(1);
}
console.log("Auf jedem Symbol hebt sich das Logo deutlich vom Grund ab.");
}
/* NUR AUSFUEHREN, WENN DIESE DATEI SELBST AUFGERUFEN WIRD.
Ohne diese Abfrage lief der Entwurf auch dann, wenn ein anderes
Werkzeug hier nur die Farben und die Zuordnung IMPORTIERT --
app-symbole-einbinden.mjs tut genau das. Beim ersten Lauf hat es
deshalb ungefragt einen Browser gestartet und ein Uebersichtsbild neu
gezeichnet, bevor es mit seiner eigentlichen Arbeit begann. Ein
Import darf nichts tun. */
const direktAufgerufen = process.argv[1]
&& fileURLToPath(import.meta.url) === process.argv[1];
if (direktAufgerufen) {
const befehl = process.argv[2] || "entwurf";
if (befehl === "entwurf") await entwerfen();
else if (befehl === "bauen") await bauen();
else { console.error(`Unbekannt: ${befehl}. Bekannt sind: entwurf, bauen`); process.exit(1); }
}