Audit des Creator Workspace, App-Symbole fuer alle Apps der Domain
AUDIT (Auftrag: vollstaendiger Durchgang, Fehler direkt beheben)
Ausgangslage waren 40 Pruefungen mit 1616 Einzelpunkten, alle gruen.
Acht neue Pruefungen kamen dazu; sie haben gefunden, was die alten nicht
sehen konnten.
Der schwerste Fund: Die Zugangsschranke verglich req.path EXAKT gegen
eine Liste. Express raeumt Punkt-Segmente selbst weg, mehrfache
Schraegstriche aber nicht. Damit kam //workspace/start.html OHNE
Anmeldung mit HTTP 200, und ein Creator bekam ueber
/workspace//personen.html die Verwaltungsseite. Die DATEN waren nie
betroffen (nachgemessen: 404 bzw. 401). Behoben durch Normalisierung
UND eine Umkehr der Logik -- jetzt ist jede .html geschuetzt ausser der
Anmeldeseite, statt nur die in der Liste. Eine vergessene neue Seite
steht damit nicht mehr versehentlich offen.
Weiter behoben:
* Kaputter/abgebrochener Rumpf ergab 500 in HTML statt 400 in JSON --
die Oberflaeche ruft ueberall a.json() und lief in einen zweiten
Fehler; der Knopf hing ohne Meldung.
* POST /zustand/sichern war der einzige von 60 schreibenden Wegen
ohne Herkunftspruefung.
* workspace-sicherung.js gab interne Pfade in Fehlermeldungen nach
aussen; alle 23 anderen Module antworten neutral.
* admin_notiz war als einziges von 14 Feldern ohne <label>.
* Der aktive Filter hatte keinen sichtbaren Fokus (CSS-Spezifitaet
0,3,0 schlug 0,2,0) -- genau der Knopf, auf dem man steht.
* h1 -> h3 ohne Zwischenstufe auf zwei Seiten.
* HSTS ging auch ueber http mit (RFC 6797, 7.2 verbietet das).
* upgrade-insecure-requests galt auch auf 127.0.0.1 -- dadurch war
WebKit/Safari ueberhaupt nicht pruefbar, also der Browser, den
jedes iPhone benutzt.
* pruef-grosscheck las readdirSync(".") und pruefte aus server/
gestartet NULL oeffentliche Seiten -- meldete aber "ok".
Neue Pruefungen: struktur, schranke, haerte, alle-wege,
barrierefrei-workspace, breiten, tempo-workspace, browser.
Jede mit Gegenprobe und mit der geprueften Anzahl in der Bedingung.
Vier davon sind beim Bauen durch die eigene Gegenprobe aufgeflogen und
haetten sonst dauerhaft gruen gemeldet, ohne etwas zu messen.
APP-SYMBOLE (Wunsch: alle Apps der Domain, jede anders, ausser
safeaddress)
Zehn Apps, zehn Stile, zehn in OKLCH gerechnete Farben. Zusammen haelt
sie dasselbe Logo, dieselbe Eckenrundung und eine gemeinsame gedeckte
Farbreihe. Beim Bauen wird gemessen, ob sich das Logo vom Grund abhebt
(19 bis 58 Helligkeitsstufen).
Dabei aufgefallen: Das Kundenportal hatte kein eigenes Manifest und
trug Namen und Symbol der Webdesign-Seite. Der Workspace hatte gar
keins und war als App nicht installierbar. Beide haben jetzt eins.
Geaendert wurde AUSSCHLIESSLICH das Symbol. Ein Zwischenstand hatte
auch die Themenfarben gesetzt; das war mehr als bestellt und wurde
zurueckgenommen.
Werkzeuge: tools/logo-freistellen.mjs, tools/app-symbole.mjs,
tools/app-symbole-einbinden.mjs -- alles im Browser gerechnet, kein
Bildprogramm, keine neue Abhaengigkeit.
Gitea und Nextcloud sind bereits live und nachgeprueft.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
/* ===================================================================
|
||||
Bindet die gebauten App-Symbole überall ein.
|
||||
|
||||
Getrennt von tools/app-symbole.mjs, weil es zwei verschiedene Dinge
|
||||
sind: dort werden Bilder ERZEUGT, hier werden Dateien VERÄNDERT. Wer
|
||||
nur den Stil nachjustieren will, soll das tun können, ohne dass dabei
|
||||
vierzig HTML-Dateien angefasst werden.
|
||||
|
||||
WAS DIESES WERKZEUG ANFASST:
|
||||
* die Manifeste (vorhandene ändern, fehlende anlegen)
|
||||
* in jeder HTML-Seite: das Reiter-Symbol (favicon), das
|
||||
Startbildschirm-Symbol (apple-touch-icon) und der Verweis aufs
|
||||
richtige Manifest
|
||||
|
||||
MEHRFACH AUFRUFBAR. Jede Änderung prüft erst, ob sie schon da ist.
|
||||
Ein zweiter Lauf ändert deshalb nichts mehr -- wichtig, weil dieses
|
||||
Werkzeug nach jeder Stiländerung erneut laufen soll.
|
||||
|
||||
NICHT ANGEFASST werden Name, Beschreibung, Farben und Startadresse
|
||||
der vorhandenen Manifeste. Die sind gewachsen und gehören nicht zu
|
||||
dieser Aufgabe.
|
||||
|
||||
Aufrufen mit: node tools/app-symbole-einbinden.mjs
|
||||
=================================================================== */
|
||||
|
||||
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, copyFileSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { APPS, FARBEN, STIL_JE_APP, STILE } from "./app-symbole.mjs";
|
||||
|
||||
const WURZEL = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const SYM = join(WURZEL, "assets", "img", "app-symbole");
|
||||
|
||||
if (!existsSync(SYM) || readdirSync(SYM).length < 60) {
|
||||
console.error("Die Symbole fehlen. Erst node tools/app-symbole.mjs bauen laufen lassen.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const farbeVon = Object.fromEntries(FARBEN.map((f) => [f.schluessel, f]));
|
||||
let geaendert = 0;
|
||||
const berichte = [];
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
1. Die Manifeste
|
||||
------------------------------------------------------------------ */
|
||||
|
||||
/** Der Symbolblock, wie ihn ein Manifest braucht. */
|
||||
const symbolBlock = (schluessel, pfadVorn) => [
|
||||
{ src: `${pfadVorn}${schluessel}-192.png`, sizes: "192x192", type: "image/png", purpose: "any" },
|
||||
{ src: `${pfadVorn}${schluessel}-512.png`, sizes: "512x512", type: "image/png", purpose: "any" },
|
||||
{ src: `${pfadVorn}${schluessel}-192-maskable.png`, sizes: "192x192", type: "image/png", purpose: "maskable" },
|
||||
{ src: `${pfadVorn}${schluessel}-512-maskable.png`, sizes: "512x512", type: "image/png", purpose: "maskable" },
|
||||
];
|
||||
|
||||
const PFAD_VORN = "/assets/img/app-symbole/";
|
||||
|
||||
/* Vorhandene Manifeste: nur die Symbole und die Themenfarbe tauschen. */
|
||||
const MANIFESTE_VORHANDEN = [
|
||||
["manifest.json", "universe"],
|
||||
["manifest-verwaltung.json", "verwaltung"],
|
||||
["webdesign/app.webmanifest", "webdesign"],
|
||||
["webdesign/verwaltung.webmanifest", "wdverwalt"],
|
||||
];
|
||||
|
||||
for (const [datei, schluessel] of MANIFESTE_VORHANDEN) {
|
||||
const pfad = join(WURZEL, datei);
|
||||
if (!existsSync(pfad)) { berichte.push(` ? ${datei} gibt es nicht — übersprungen`); continue; }
|
||||
const inhalt = JSON.parse(readFileSync(pfad, "utf8"));
|
||||
const neu = symbolBlock(schluessel, PFAD_VORN);
|
||||
const f = farbeVon[schluessel];
|
||||
|
||||
/* NUR DIE SYMBOLE. Themenfarbe und Hintergrundfarbe bleiben, wie sie
|
||||
sind.
|
||||
|
||||
Ein Zwischenstand vom 05.09.2026 hatte sie mitgesetzt -- der
|
||||
Gedanke war, dass die Statusleiste der installierten App zur
|
||||
Symbolfarbe passen sollte. Filipe dazu: *"wieso cloud ich will doch
|
||||
nur dass die symbole anders aussehen wenn ich sie installiere mehr
|
||||
nicht."* Das war eindeutig und richtig: Bestellt waren die Symbole.
|
||||
Alles andere anzufassen ist Eigenmacht, auch wenn es gut gemeint
|
||||
ist und gut aussieht.
|
||||
|
||||
Die Farben wurden deshalb auf ihre alten Werte zurückgesetzt. Wer
|
||||
sie später doch angleichen will, macht das als eigene Entscheidung
|
||||
-- nicht als Nebenwirkung eines Symboltauschs. */
|
||||
const vorher = JSON.stringify(inhalt);
|
||||
inhalt.icons = neu;
|
||||
if (JSON.stringify(inhalt) === vorher) {
|
||||
berichte.push(` = ${datei} war schon richtig`);
|
||||
continue;
|
||||
}
|
||||
writeFileSync(pfad, JSON.stringify(inhalt, null, 2) + "\n");
|
||||
geaendert++;
|
||||
berichte.push(` ✓ ${datei} → ${schluessel}`);
|
||||
}
|
||||
|
||||
/* Fehlende Manifeste: Kundenportal und Workspace hatten keins.
|
||||
Das Portal lief bisher unter dem Manifest der Webdesign-Seite -- damit
|
||||
trug die Verknüpfung deren Namen und deren Symbol. Der Workspace hatte
|
||||
überhaupt keins; er war als App gar nicht installierbar. */
|
||||
const MANIFESTE_NEU = [
|
||||
{
|
||||
datei: "webdesign/portal.webmanifest", schluessel: "portal",
|
||||
seite: "webdesign/portal.html",
|
||||
inhalt: {
|
||||
name: "Kundenportal — Dogfather Webdesign",
|
||||
short_name: "Kundenportal",
|
||||
description: "Der eigene Bereich für Kundinnen und Kunden: Projekte, Anfragen, "
|
||||
+ "Zahlungen und Postfach an einem Ort.",
|
||||
start_url: "/webdesign/portal.html",
|
||||
id: "/webdesign/portal.html",
|
||||
scope: "/webdesign/",
|
||||
display: "standalone", orientation: "any", lang: "de", dir: "ltr",
|
||||
},
|
||||
},
|
||||
{
|
||||
datei: "workspace/app.webmanifest", schluessel: "workspace",
|
||||
seite: "workspace/start.html",
|
||||
inhalt: {
|
||||
name: "Creator Workspace — Dogfather Universe",
|
||||
short_name: "Workspace",
|
||||
description: "Betreuung an einem Ort: Aufgaben, Termine, Calls, Bereiche, "
|
||||
+ "Dateien und Reports. Nur mit persönlichem Zugangscode.",
|
||||
start_url: "/workspace/start.html",
|
||||
id: "/workspace/start.html",
|
||||
scope: "/workspace/",
|
||||
display: "standalone", orientation: "any", lang: "de", dir: "ltr",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const { datei, schluessel, inhalt, seite } of MANIFESTE_NEU) {
|
||||
const pfad = join(WURZEL, datei);
|
||||
|
||||
/* Diese beiden Manifeste sind neu, es gibt also keinen alten Wert,
|
||||
auf den man zurueckfallen koennte. Genommen wird deshalb die Farbe,
|
||||
die die SEITE schon hat -- nicht die Farbe des neuen Symbols. Auch
|
||||
hier gilt: geaendert wird nur das Symbol; ein neues Manifest darf
|
||||
nicht durch die Hintertuer ein neues Aussehen mitbringen. */
|
||||
let seitenFarbe = "#05070b";
|
||||
try {
|
||||
const html = readFileSync(join(WURZEL, seite), "utf8");
|
||||
const m = html.match(/<meta[^>]*name="theme-color"[^>]*content="([^"]*)"/i);
|
||||
if (m) seitenFarbe = m[1];
|
||||
} catch { /* Seite nicht lesbar -- dann der dunkle Vorgabewert */ }
|
||||
|
||||
const fertig = {
|
||||
...inhalt,
|
||||
background_color: seitenFarbe,
|
||||
theme_color: seitenFarbe,
|
||||
icons: symbolBlock(schluessel, PFAD_VORN),
|
||||
};
|
||||
const alt = existsSync(pfad) ? readFileSync(pfad, "utf8") : "";
|
||||
const neu = JSON.stringify(fertig, null, 2) + "\n";
|
||||
if (alt === neu) { berichte.push(` = ${datei} war schon richtig`); continue; }
|
||||
mkdirSync(dirname(pfad), { recursive: true });
|
||||
writeFileSync(pfad, neu);
|
||||
geaendert++;
|
||||
berichte.push(` ${alt ? "✓" : "+"} ${datei} → ${schluessel}${alt ? "" : " (neu angelegt)"}`);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
2. Die HTML-Seiten
|
||||
------------------------------------------------------------------ */
|
||||
|
||||
/** Welche Seite gehört zu welcher App? Die genaueste Regel gewinnt,
|
||||
* deshalb steht das Besondere vor dem Allgemeinen. */
|
||||
const ZUORDNUNG = [
|
||||
{ treffer: (p) => p === "webdesign/portal.html", app: "portal", manifest: "/webdesign/portal.webmanifest" },
|
||||
{ treffer: (p) => p === "webdesign/verwaltung.html", app: "wdverwalt", manifest: "/webdesign/verwaltung.webmanifest" },
|
||||
{ treffer: (p) => p.startsWith("webdesign/"), app: "webdesign", manifest: "/webdesign/app.webmanifest" },
|
||||
{ treffer: (p) => p.startsWith("workspace/"), app: "workspace", manifest: "/workspace/app.webmanifest" },
|
||||
{ treffer: (p) => p === "verwaltung.html", app: "verwaltung", manifest: "/manifest-verwaltung.json" },
|
||||
{ treffer: (p) => !p.includes("/"), app: "universe", manifest: "/manifest.json" },
|
||||
];
|
||||
|
||||
function htmlSeiten() {
|
||||
const raus = [];
|
||||
const gehen = (ordner, vorn = "") => {
|
||||
for (const n of readdirSync(ordner, { withFileTypes: true })) {
|
||||
if (n.name === "node_modules" || n.name.startsWith(".")) continue;
|
||||
const p = join(ordner, n.name);
|
||||
const rel = vorn ? `${vorn}/${n.name}` : n.name;
|
||||
if (n.isDirectory()) {
|
||||
if (["webdesign", "workspace"].includes(rel)) gehen(p, rel);
|
||||
continue;
|
||||
}
|
||||
if (n.name.endsWith(".html")) raus.push(rel);
|
||||
}
|
||||
};
|
||||
gehen(WURZEL);
|
||||
return raus;
|
||||
}
|
||||
|
||||
for (const rel of htmlSeiten()) {
|
||||
const regel = ZUORDNUNG.find((z) => z.treffer(rel));
|
||||
if (!regel) continue;
|
||||
const pfad = join(WURZEL, rel);
|
||||
let text = readFileSync(pfad, "utf8");
|
||||
const vorher = text;
|
||||
const s = regel.app;
|
||||
|
||||
/* Reiter-Symbol. Ersetzt jeden vorhandenen icon-Verweis; gibt es
|
||||
keinen, kommt er hinter <head>. */
|
||||
const favicon = `<link rel="icon" type="image/png" href="${PFAD_VORN}${s}-32.png" />`;
|
||||
if (/<link[^>]*rel="icon"[^>]*>/i.test(text)) {
|
||||
text = text.replace(/<link[^>]*rel="icon"[^>]*>/i, favicon);
|
||||
} else {
|
||||
text = text.replace(/<head>/i, `<head>\n${favicon}`);
|
||||
}
|
||||
|
||||
/* Startbildschirm-Symbol. */
|
||||
const apple = `<link rel="apple-touch-icon" href="${PFAD_VORN}${s}-180.png" />`;
|
||||
if (/<link[^>]*rel="apple-touch-icon"[^>]*>/i.test(text)) {
|
||||
text = text.replace(/<link[^>]*rel="apple-touch-icon"[^>]*>/i, apple);
|
||||
} else {
|
||||
text = text.replace(favicon, `${favicon}\n${apple}`);
|
||||
}
|
||||
|
||||
/* Manifest -- nur ersetzen, nicht neu erfinden: Seiten ohne Manifest
|
||||
sollen auch keins bekommen (etwa Diagnoseseiten). */
|
||||
if (/<link[^>]*rel="manifest"[^>]*>/i.test(text)) {
|
||||
text = text.replace(/<link[^>]*rel="manifest"[^>]*href="[^"]*"/i,
|
||||
`<link rel="manifest" href="${regel.manifest}"`);
|
||||
}
|
||||
|
||||
/* Die Themenfarbe der Seite bleibt unangetastet -- siehe die
|
||||
Begründung oben bei den Manifesten. Bestellt waren die Symbole. */
|
||||
|
||||
if (text !== vorher) { writeFileSync(pfad, text); geaendert++; }
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
3. Die zwei Apps in eigenen Ordnern
|
||||
------------------------------------------------------------------ */
|
||||
const AUSWAERTS = [
|
||||
{ name: "Dogi's ZockerAnstalt", schluessel: "zocker",
|
||||
ziel: "C:/Users/qciga/Documents/Obelix/Spielehalle/public/icons" },
|
||||
{ name: "DogFather Analyse", schluessel: "analyse",
|
||||
ziel: "C:/Users/qciga/Documents/Obelix/Analyse/logos" },
|
||||
];
|
||||
|
||||
for (const { name, schluessel, ziel } of AUSWAERTS) {
|
||||
if (!existsSync(ziel)) { berichte.push(` ? ${name}: ${ziel} gibt es nicht`); continue; }
|
||||
for (const g of ["192", "512", "192-maskable", "512-maskable", "180", "32"]) {
|
||||
copyFileSync(join(SYM, `${schluessel}-${g}.png`), join(ziel, `dogfather-${schluessel}-${g}.png`));
|
||||
}
|
||||
berichte.push(` ✓ ${name}: 6 Symbole nach ${ziel.split("/").slice(-3).join("/")} kopiert`);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------
|
||||
Bericht
|
||||
------------------------------------------------------------------ */
|
||||
console.log("Manifeste und Ordner:");
|
||||
for (const z of berichte) console.log(z);
|
||||
console.log(`\nGeänderte Dateien insgesamt: ${geaendert}`);
|
||||
console.log("\nZuordnung:");
|
||||
for (const app of APPS) {
|
||||
const f = farbeVon[app.schluessel];
|
||||
console.log(` ${app.kurz.padEnd(12)} ${STILE[STIL_JE_APP[app.schluessel]].name.padEnd(18)}`
|
||||
+ ` ${f.mittel} ${app.wo}`);
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
/* ===================================================================
|
||||
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" },
|
||||
{ schluessel: "workspace", name: "Creator Workspace", kurz: "Workspace", wo: "/workspace" },
|
||||
{ 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, logoDaten } = {}) {
|
||||
const stil = STILE[stilName];
|
||||
const logoFilter = typeof stil.logo === "function" ? stil.logo(f) : stil.logo;
|
||||
/* Maskable: Android schneidet bis zu 20 % ringsum weg. Das Logo muss
|
||||
deshalb kleiner sitzen, sonst fehlen ihm die Ohren. */
|
||||
const logoBreite = maskable ? 52 : 68;
|
||||
const radius = maskable ? 0 : 22.4; // 22,4 % ist die Apple-Rundung
|
||||
const k = `s-${stilName}-${f.schluessel}${maskable ? "-m" : ""}`;
|
||||
|
||||
const css = `
|
||||
.${k} {
|
||||
position: relative; width: ${n}px; height: ${n}px; overflow: hidden;
|
||||
border-radius: ${radius}%; flex: none;
|
||||
--r2: ${n * 0.006}px; --r3: ${n * 0.012}px; --r4: ${n * 0.016}px;
|
||||
--r5: ${n * 0.022}px; --r6: ${n * 0.03}px; --r8: ${n * 0.05}px;
|
||||
--r10: ${n * 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 markup = `<div class="${k}">
|
||||
<div class="unterlage"></div>
|
||||
<img class="logo" src="${logoDaten}" alt="">
|
||||
<div class="glanz"></div>
|
||||
<div class="rand"></div>
|
||||
</div>`;
|
||||
return { css, markup, klasse: 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, 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, 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); }
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
/* ===================================================================
|
||||
Stellt den DogFather-Logokopf frei — einmal, als Vorlage für alle
|
||||
App-Symbole.
|
||||
|
||||
WOZU. Jede App an dogfather-universe.com soll dasselbe Logo tragen,
|
||||
aber in einer eigenen Farbe (Wunsch Filipe, 04.09.2026: "alle sollen
|
||||
das Logo haben, aber durch Farben anders aussehen"). Dafür braucht es
|
||||
den Kopf OHNE Hintergrund — sonst klebt an jedem Symbol das alte
|
||||
Hellblau.
|
||||
|
||||
DAS VORHANDENE MATERIAL taugt so nicht:
|
||||
assets/img/icon-512.png Kopf, aber auf hellblauem Grund
|
||||
assets/img/logo-dogfather-*.png Kopf MIT Schriftzug, auf Weiss
|
||||
assets/img/marke/dogfather-husky.png die Maskottchen-Figur im
|
||||
Hoodie, kein Logo
|
||||
Gebraucht wird der Kopf aus dem ersten, ohne dessen Hintergrund.
|
||||
|
||||
WARUM FLUTFUELLUNG UND KEIN FARBFILTER. Der naheliegende Weg — "alle
|
||||
hellblauen Bildpunkte durchsichtig machen" — zerstört das Logo: Im
|
||||
Gesicht liegen dieselben hellen Töne (Wange, Schnauze, Ohrinnenseite).
|
||||
Die Füllung startet stattdessen an den vier Ecken und frisst sich nur
|
||||
so weit nach innen, wie die Farbe zusammenhängt. Was vom Rand nicht
|
||||
erreichbar ist, bleibt stehen — auch wenn es dieselbe Farbe hat.
|
||||
|
||||
WEICHE KANTE. Ein harter Schnitt ergibt Treppenstufen, die man bei
|
||||
192 px deutlich sieht. Deshalb wird der Rand über die Farbnähe
|
||||
abgestuft: fast gleich = ganz durchsichtig, nah dran = teilweise.
|
||||
|
||||
Gerechnet im Browser, der ohnehin für die Prüfungen da ist — kein
|
||||
Bildprogramm, keine neue Abhängigkeit, 0 EUR. Derselbe Weg wie bei
|
||||
tools/buehne-bauen.mjs.
|
||||
|
||||
Aufrufen mit: node tools/logo-freistellen.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 QUELLE = join(WURZEL, "assets", "img", "icon-512.png");
|
||||
const ZIEL_ORDNER = join(WURZEL, "assets", "img", "marke");
|
||||
const ZIEL = join(ZIEL_ORDNER, "logo-kopf-frei.png");
|
||||
|
||||
if (!existsSync(QUELLE)) {
|
||||
console.error(`Quelle fehlt: ${QUELLE}`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (!existsSync(ZIEL_ORDNER)) mkdirSync(ZIEL_ORDNER, { recursive: true });
|
||||
|
||||
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 alsDatenAdresse = (pfad) =>
|
||||
"data:image/png;base64," + readFileSync(pfad).toString("base64");
|
||||
|
||||
const ergebnis = await seite.evaluate(async (quelle) => {
|
||||
const bild = new Image();
|
||||
bild.src = quelle;
|
||||
await bild.decode();
|
||||
|
||||
const n = bild.naturalWidth;
|
||||
const leinwand = document.createElement("canvas");
|
||||
leinwand.width = n; leinwand.height = n;
|
||||
const stift = leinwand.getContext("2d", { willReadFrequently: true });
|
||||
stift.drawImage(bild, 0, 0);
|
||||
const daten = stift.getImageData(0, 0, n, n);
|
||||
const p = daten.data;
|
||||
|
||||
/* DEN GRUNDTON AUS DEM HAEUFIGSTEN RANDTON LESEN, nicht aus den Ecken.
|
||||
|
||||
Erster Versuch: die vier Ecken mitteln. Ergebnis war rgb(0,0,0) und
|
||||
ein Bild, aus dem nichts entfernt wurde. Der Grund ist die Form des
|
||||
Symbols: `icon-512.png` hat ABGERUNDETE Ecken, dort ist es
|
||||
durchsichtig -- und durchsichtige Punkte tragen die Farbwerte
|
||||
0,0,0. Gemittelt wurde also viermal Schwarz, und gesucht wurde
|
||||
danach nach schwarzem Hintergrund, den es nicht gibt.
|
||||
|
||||
Jetzt wird der ganze Rand abgelaufen, undurchsichtige Punkte werden
|
||||
grob nach Farbe gebuendelt, und der groesste Haufen gewinnt. Das
|
||||
ueberlebt runde Ecken, einen Rahmen und einzelne Ausreisser. */
|
||||
const haufen = new Map();
|
||||
const merke = (x, y) => {
|
||||
const i = (y * n + x) * 4;
|
||||
if (p[i + 3] < 200) return; // durchsichtig zaehlt nicht mit
|
||||
const schluessel = `${p[i] >> 3},${p[i + 1] >> 3},${p[i + 2] >> 3}`;
|
||||
const e = haufen.get(schluessel) || { n: 0, r: 0, g: 0, b: 0 };
|
||||
e.n++; e.r += p[i]; e.g += p[i + 1]; e.b += p[i + 2];
|
||||
haufen.set(schluessel, e);
|
||||
};
|
||||
for (let x = 0; x < n; x++) { merke(x, 0); merke(x, n - 1); }
|
||||
for (let y = 0; y < n; y++) { merke(0, y); merke(n - 1, y); }
|
||||
|
||||
if (!haufen.size) return { fehler: "Der ganze Rand ist durchsichtig — nichts abzulesen." };
|
||||
const groesster = [...haufen.values()].sort((a, b) => b.n - a.n)[0];
|
||||
const randPunkte = [...haufen.values()].reduce((s, e) => s + e.n, 0);
|
||||
if (groesster.n / randPunkte < 0.5) {
|
||||
return { fehler: `Der Rand ist nicht einfarbig genug `
|
||||
+ `(haeufigster Ton nur ${Math.round(groesster.n / randPunkte * 100)} % des Randes).` };
|
||||
}
|
||||
const grund = [
|
||||
Math.round(groesster.r / groesster.n),
|
||||
Math.round(groesster.g / groesster.n),
|
||||
Math.round(groesster.b / 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 gilt er als Logo; dazwischen wird weich abgestuft */
|
||||
const NAH = 42, WEIT = 96;
|
||||
|
||||
/* Flutfuellung von den Raendern. Eine eigene Liste statt Rekursion --
|
||||
bei 512x512 liefe der Aufrufstapel sonst über. */
|
||||
const besucht = new Uint8Array(n * n);
|
||||
const liste = [];
|
||||
for (let x = 0; x < n; x++) { liste.push(x, x + (n - 1) * n); }
|
||||
for (let y = 0; y < n; y++) { liste.push(y * n, n - 1 + y * n); }
|
||||
|
||||
let entfernt = 0, weich = 0;
|
||||
while (liste.length) {
|
||||
const feld = liste.pop();
|
||||
if (besucht[feld]) continue;
|
||||
besucht[feld] = 1;
|
||||
const i = feld * 4;
|
||||
|
||||
/* Schon durchsichtige Punkte (die runden Ecken) sind bereits
|
||||
Hintergrund -- durch sie hindurch muss die Fuellung weiterlaufen,
|
||||
sonst kommt sie gar nicht erst an den hellblauen Grund heran. */
|
||||
if (p[i + 3] < 40) {
|
||||
const x0 = feld % n, y0 = (feld - x0) / n;
|
||||
if (x0 > 0) liste.push(feld - 1);
|
||||
if (x0 < n - 1) liste.push(feld + 1);
|
||||
if (y0 > 0) liste.push(feld - n);
|
||||
if (y0 < n - 1) liste.push(feld + n);
|
||||
continue;
|
||||
}
|
||||
|
||||
const a = abstand(i);
|
||||
if (a > WEIT) continue; // gehört zum Logo, hier ist Schluss
|
||||
|
||||
if (a <= NAH) { p[i + 3] = 0; entfernt++; }
|
||||
else {
|
||||
/* Zwischenzone: je näher am Grundton, desto durchsichtiger. */
|
||||
p[i + 3] = Math.round(255 * ((a - NAH) / (WEIT - NAH)));
|
||||
weich++;
|
||||
}
|
||||
|
||||
const x = feld % n, y = (feld - x) / n;
|
||||
if (x > 0) liste.push(feld - 1);
|
||||
if (x < n - 1) liste.push(feld + 1);
|
||||
if (y > 0) liste.push(feld - n);
|
||||
if (y < n - 1) liste.push(feld + n);
|
||||
}
|
||||
|
||||
/* ---- Zweiter Durchgang: die EINGESCHLOSSENEN Reste --------------------
|
||||
|
||||
Die Flutfuellung kommt nur an das heran, was mit dem Rand
|
||||
zusammenhaengt. In den Ohren, am Auge und an der Schnauze liegen
|
||||
aber Flaechen desselben Hellblau, die ringsum von schwarzer Kontur
|
||||
umschlossen sind -- die Fuellung erreicht sie nie. Im ersten
|
||||
Ergebnis blieben sie als blaue Flecken stehen und haetten auf jedem
|
||||
eingefaerbten Symbol als Fremdkoerper geleuchtet.
|
||||
|
||||
Im Original-Logo (assets/img/marke/logo.png, auf Weiss) sind genau
|
||||
diese Stellen WEISS -- sie waren also nie blau, dort schien beim
|
||||
Bau des Symbols der Hintergrund durch. Sie gehoeren damit ebenfalls
|
||||
weg.
|
||||
|
||||
Hier ist ein reiner Farbvergleich gefahrlos, und das laesst sich
|
||||
nachrechnen statt hoffen: Der Abstand von Weiss (255,255,255) zum
|
||||
Grundton betraegt rund 120 -- deutlich mehr als die Schwelle NAH
|
||||
(42). Das Fell kann also nicht versehentlich mitgeloescht werden.
|
||||
Genau diese Zahl steht unten in der Ausgabe, damit sie beim
|
||||
naechsten Logo nachgeprueft wird, statt geglaubt zu werden. */
|
||||
const abstandWeiss = Math.sqrt(
|
||||
(255 - grund[0]) ** 2 + (255 - grund[1]) ** 2 + (255 - grund[2]) ** 2);
|
||||
let eingeschlossen = 0;
|
||||
if (abstandWeiss > NAH * 1.8) {
|
||||
for (let i = 0; i < p.length; i += 4) {
|
||||
if (p[i + 3] === 0) continue;
|
||||
if (abstand(i) <= NAH) { p[i + 3] = 0; eingeschlossen++; }
|
||||
}
|
||||
}
|
||||
|
||||
stift.putImageData(daten, 0, 0);
|
||||
|
||||
/* Wie viel Bild ist übrig? Bleibt fast nichts stehen, hat die Füllung
|
||||
das Logo mitgefressen -- dann ist das Ergebnis wertlos und darf
|
||||
nicht gespeichert werden. */
|
||||
let sichtbar = 0;
|
||||
for (let i = 3; i < p.length; i += 4) if (p[i] > 40) sichtbar++;
|
||||
|
||||
return {
|
||||
png: leinwand.toDataURL("image/png"),
|
||||
grund, entfernt, weich, sichtbar, gesamt: n * n, groesse: n,
|
||||
eingeschlossen, abstandWeiss: Math.round(abstandWeiss),
|
||||
};
|
||||
}, alsDatenAdresse(QUELLE));
|
||||
|
||||
await browser.close();
|
||||
|
||||
if (ergebnis.fehler) {
|
||||
console.error("Abgebrochen:", ergebnis.fehler);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const anteil = ergebnis.sichtbar / ergebnis.gesamt;
|
||||
console.log(`Grundton erkannt: rgb(${ergebnis.grund.join(", ")})`);
|
||||
console.log(`ganz entfernt: ${ergebnis.entfernt} Punkte`);
|
||||
console.log(`weiche Kante: ${ergebnis.weich} Punkte`);
|
||||
console.log(`eingeschlossen: ${ergebnis.eingeschlossen} Punkte `
|
||||
+ `(Abstand Weiss-zu-Grundton: ${ergebnis.abstandWeiss}, Schwelle 42 — Fell bleibt sicher)`);
|
||||
console.log(`Logo übrig: ${ergebnis.sichtbar} Punkte (${(anteil * 100).toFixed(1)} %)`);
|
||||
|
||||
/* Die Anzahl gehört in die Bedingung: Ein leeres Bild wäre sonst das
|
||||
sauberste Ergebnis von allen. */
|
||||
if (anteil < 0.12) {
|
||||
console.error(`\nFEHLER: Nur ${(anteil * 100).toFixed(1)} % übrig — die Füllung hat das Logo`
|
||||
+ ` mitgenommen. Nichts gespeichert.`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (anteil > 0.92) {
|
||||
console.error(`\nFEHLER: ${(anteil * 100).toFixed(1)} % übrig — es wurde praktisch nichts`
|
||||
+ ` entfernt. Nichts gespeichert.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
writeFileSync(ZIEL, Buffer.from(ergebnis.png.split(",")[1], "base64"));
|
||||
console.log(`\nGespeichert: ${ZIEL.replace(WURZEL, ".")} (${ergebnis.groesse}x${ergebnis.groesse})`);
|
||||
Reference in New Issue
Block a user