Die Bildmarke liegt als SVG in drei Varianten (dunkel, hell, mono) vor. Rasterdateien erzeugt scripts/icons.mjs mit sharp und sie werden eingecheckt, damit Build und Laufzeitabbild kein sharp brauchen. Unter 32 px dient eine reduzierte Marke ohne Zahlenring und Bolzen, weil die Details dort zu Rauschen verschwimmen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
100 lines
3.7 KiB
JavaScript
100 lines
3.7 KiB
JavaScript
// Erzeugt alle Raster-Icons aus den SVG-Vorlagen in public/brand/ (#002).
|
|
//
|
|
// Die PNGs und das favicon.ico werden eingecheckt, damit Build und Docker-Abbild
|
|
// kein sharp zur Laufzeit brauchen. Nach jeder Logo-Aenderung: `pnpm icons`.
|
|
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import sharp from "sharp";
|
|
|
|
const WURZEL = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
const BRAND = join(WURZEL, "public/brand");
|
|
const ICONS = join(WURZEL, "public/icons");
|
|
const APP = join(WURZEL, "src/app");
|
|
|
|
// Seitenhintergrund (--bg); App-Icons stehen auf dieser Flaeche statt transparent,
|
|
// weil iOS und Android transparente Bereiche sonst schwarz oder weiss fuellen.
|
|
const HINTERGRUND = "#0D1013";
|
|
|
|
/** Rendert ein SVG scharf in der Zielgroesse (density skaliert die Vektoren statt Pixel). */
|
|
async function rendern(svg, groesse) {
|
|
return sharp(svg, { density: Math.ceil((72 * groesse) / 32) * 2 })
|
|
.resize(groesse, groesse)
|
|
.png()
|
|
.toBuffer();
|
|
}
|
|
|
|
/** Logo mittig auf --bg mit Rand, damit es auf Homescreens nicht an die Kante stoesst. */
|
|
async function aufFlaeche(svg, groesse, anteil) {
|
|
const innen = Math.round(groesse * anteil);
|
|
const logo = await rendern(svg, innen);
|
|
return sharp({
|
|
create: { width: groesse, height: groesse, channels: 4, background: HINTERGRUND },
|
|
})
|
|
.composite([{ input: logo, gravity: "center" }])
|
|
.png()
|
|
.toBuffer();
|
|
}
|
|
|
|
/** Minimaler ICO-Container mit PNG-Eintraegen (seit Windows Vista von allen Browsern gelesen). */
|
|
function icoBauen(bilder) {
|
|
const kopf = Buffer.alloc(6);
|
|
kopf.writeUInt16LE(0, 0);
|
|
kopf.writeUInt16LE(1, 2);
|
|
kopf.writeUInt16LE(bilder.length, 4);
|
|
const eintraege = [];
|
|
let versatz = 6 + 16 * bilder.length;
|
|
for (const { groesse, daten } of bilder) {
|
|
const e = Buffer.alloc(16);
|
|
e.writeUInt8(groesse >= 256 ? 0 : groesse, 0);
|
|
e.writeUInt8(groesse >= 256 ? 0 : groesse, 1);
|
|
e.writeUInt8(0, 2);
|
|
e.writeUInt8(0, 3);
|
|
e.writeUInt16LE(1, 4);
|
|
e.writeUInt16LE(32, 6);
|
|
e.writeUInt32LE(daten.length, 8);
|
|
e.writeUInt32LE(versatz, 12);
|
|
versatz += daten.length;
|
|
eintraege.push(e);
|
|
}
|
|
return Buffer.concat([kopf, ...eintraege, ...bilder.map((b) => b.daten)]);
|
|
}
|
|
|
|
async function main() {
|
|
await mkdir(ICONS, { recursive: true });
|
|
const logo = await readFile(join(BRAND, "logo.svg"));
|
|
// Unter 32 px wird die reduzierte Variante ohne Zahlenring und Bolzen genutzt,
|
|
// sonst verschwimmen die Details zu grauem Rauschen.
|
|
const favicon = await readFile(join(BRAND, "favicon.svg"));
|
|
|
|
const icon192 = await aufFlaeche(logo, 192, 0.72);
|
|
const icon512 = await aufFlaeche(logo, 512, 0.72);
|
|
const apple = await aufFlaeche(logo, 180, 0.72);
|
|
const fav32 = await rendern(favicon, 32);
|
|
const fav16 = await rendern(favicon, 16);
|
|
const fav48 = await rendern(favicon, 48);
|
|
|
|
await writeFile(join(ICONS, "icon-192.png"), icon192);
|
|
await writeFile(join(ICONS, "icon-512.png"), icon512);
|
|
await writeFile(join(ICONS, "apple-touch-icon.png"), apple);
|
|
await writeFile(join(ICONS, "favicon-32.png"), fav32);
|
|
await writeFile(join(ICONS, "favicon-16.png"), fav16);
|
|
|
|
// Next-Metadaten-Dateien: favicon.ico, icon.svg und apple-icon.png in src/app
|
|
// erzeugen die <link>-Tags automatisch.
|
|
await writeFile(
|
|
join(APP, "favicon.ico"),
|
|
icoBauen([
|
|
{ groesse: 16, daten: fav16 },
|
|
{ groesse: 32, daten: fav32 },
|
|
{ groesse: 48, daten: fav48 },
|
|
]),
|
|
);
|
|
await copyFile(join(BRAND, "favicon.svg"), join(APP, "icon.svg"));
|
|
await writeFile(join(APP, "apple-icon.png"), apple);
|
|
|
|
console.log("Icons erzeugt: public/icons/*, src/app/favicon.ico, src/app/icon.svg, src/app/apple-icon.png");
|
|
}
|
|
|
|
await main();
|