Files
flimmer/scripts/generate-icons.ts
T
Florian Wilde 488b99f7a1 feat: Flimmer-Branding mit Logo, Icons und Design-Tokens
Logo als SVG in drei Varianten (Icon, Icon+Wortmarke horizontal, monochrom);
die Wortmarke ist als Pfad konstruiert und braucht daher keine installierte
Schrift. Daraus generiert scripts/generate-icons.ts Favicon (SVG + ICO),
PWA-Icons, Maskable-Icons, Apple-Touch-Icon und OG-Bild.

Die Farbwelt liegt als CSS-Variablen in globals.css und wird ueber
@theme inline an Tailwind durchgereicht — Dark/Light und spaetere Themes
bauen darauf auf. Alle Kombinationen erfuellen WCAG AA.
2026-09-01 18:26:21 +02:00

130 lines
4.1 KiB
TypeScript

/**
* Erzeugt Favicon und PWA-Icons aus den SVG-Quellen unter public/brand/.
*
* npm run icons
*
* Quelle ist immer das Vektor-Logo — die Rasterdateien sind reine Ableitungen
* und werden bei Bedarf neu erzeugt.
*/
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { join } from "node:path";
import sharp from "sharp";
const ROOT = process.cwd();
const BRAND = join(ROOT, "public", "brand");
const ICONS = join(ROOT, "public", "icons");
const ICON_SVG = join(BRAND, "flimmer-icon.svg");
const MASKABLE_SVG = join(BRAND, "flimmer-icon-maskable.svg");
/** Rendert ein SVG in ein quadratisches PNG. */
async function png(svgPath: string, size: number): Promise<Buffer> {
const svg = readFileSync(svgPath);
return sharp(svg, { density: Math.max(72, Math.ceil((size / 128) * 96) * 4) })
.resize(size, size, { fit: "contain", background: { r: 0, g: 0, b: 0, alpha: 0 } })
.png({ compressionLevel: 9 })
.toBuffer();
}
/**
* Minimaler ICO-Container mit eingebetteten PNGs.
* Alle Browser ab IE11 verstehen PNG-in-ICO; das spart eine Abhängigkeit.
*/
function buildIco(images: { size: number; data: Buffer }[]): Buffer {
const header = Buffer.alloc(6);
header.writeUInt16LE(0, 0); // reserviert
header.writeUInt16LE(1, 2); // Typ 1 = Icon
header.writeUInt16LE(images.length, 4);
const entries: Buffer[] = [];
let offset = 6 + images.length * 16;
for (const img of images) {
const entry = Buffer.alloc(16);
entry.writeUInt8(img.size >= 256 ? 0 : img.size, 0); // Breite (0 = 256)
entry.writeUInt8(img.size >= 256 ? 0 : img.size, 1); // Höhe
entry.writeUInt8(0, 2); // Farbpalette
entry.writeUInt8(0, 3); // reserviert
entry.writeUInt16LE(1, 4); // Farbebenen
entry.writeUInt16LE(32, 6); // Bits pro Pixel
entry.writeUInt32LE(img.data.length, 8);
entry.writeUInt32LE(offset, 12);
entries.push(entry);
offset += img.data.length;
}
return Buffer.concat([header, ...entries, ...images.map((i) => i.data)]);
}
const PWA_SIZES = [64, 96, 128, 192, 256, 384, 512];
const FAVICON_SIZES = [16, 32, 48];
async function main() {
mkdirSync(ICONS, { recursive: true });
// PWA-Icons (transparenter Rand, „any"-Purpose)
for (const size of PWA_SIZES) {
const data = await png(ICON_SVG, size);
writeFileSync(join(ICONS, `icon-${size}.png`), data);
console.log(` icons/icon-${size}.png`);
}
// Maskable (randlose Fläche, Motiv in der Sicherheitszone)
for (const size of [192, 512]) {
const data = await png(MASKABLE_SVG, size);
writeFileSync(join(ICONS, `maskable-${size}.png`), data);
console.log(` icons/maskable-${size}.png`);
}
// Apple Touch Icon — iOS mag keine Transparenz, deshalb deckend
const apple = await sharp(readFileSync(MASKABLE_SVG), { density: 576 })
.resize(180, 180)
.flatten({ background: "#12101d" })
.png({ compressionLevel: 9 })
.toBuffer();
writeFileSync(join(ICONS, "apple-touch-icon.png"), apple);
console.log(" icons/apple-touch-icon.png");
// Favicon: SVG (scharf in jeder Größe) + ICO (Fallback, Lesezeichenleisten)
writeFileSync(join(ROOT, "public", "favicon.svg"), readFileSync(ICON_SVG));
console.log(" favicon.svg");
const icoImages = [];
for (const size of FAVICON_SIZES) {
icoImages.push({ size, data: await png(ICON_SVG, size) });
}
writeFileSync(join(ROOT, "public", "favicon.ico"), buildIco(icoImages));
console.log(" favicon.ico");
// Open-Graph-Bild für geteilte Links
const og = await sharp({
create: {
width: 1200,
height: 630,
channels: 4,
background: { r: 12, g: 10, b: 20, alpha: 1 },
},
})
.composite([
{
input: await sharp(readFileSync(join(BRAND, "flimmer-logo.svg")), { density: 300 })
.resize({ width: 760 })
.png()
.toBuffer(),
gravity: "centre",
},
])
.png({ compressionLevel: 9 })
.toBuffer();
writeFileSync(join(ROOT, "public", "og-image.png"), og);
console.log(" og-image.png");
}
main().then(
() => console.log("Icons erzeugt."),
(err) => {
console.error(err);
process.exit(1);
},
);