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.
130 lines
4.1 KiB
TypeScript
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);
|
|
},
|
|
);
|