/** * Renders the app icons from one SVG source. * * Run with `node scripts/generate-icons.mjs` after changing the mark; the * results are committed so neither the build nor the container needs to draw * anything at runtime. */ import fs from 'node:fs'; import path from 'node:path'; import sharp from 'sharp'; const OUT_DIR = path.resolve(process.cwd(), 'public/icons'); // Four studs on a rounded plate - readable down to 32px, which a detailed // brick drawing would not be. const mark = (background, foreground) => ` `; /** * A maskable icon has to survive being cropped to a circle, so the mark sits * on a full-bleed background with generous padding. */ const maskable = (background, foreground) => ` `; const DARK = '#0a0a0a'; const LIGHT = '#fafafa'; const targets = [ { file: 'icon-192.png', size: 192, svg: mark(DARK, LIGHT) }, { file: 'icon-512.png', size: 512, svg: mark(DARK, LIGHT) }, { file: 'icon-maskable-512.png', size: 512, svg: maskable(DARK, LIGHT) }, { file: 'apple-touch-icon.png', size: 180, svg: mark(DARK, LIGHT) }, ]; fs.mkdirSync(OUT_DIR, { recursive: true }); for (const target of targets) { await sharp(Buffer.from(target.svg)) .resize(target.size, target.size) .png({ compressionLevel: 9 }) .toFile(path.join(OUT_DIR, target.file)); console.log(`[icons] ${target.file} (${target.size}px)`); } // The favicon keeps the same mark so the browser tab matches the home screen. await sharp(Buffer.from(mark(DARK, LIGHT))) .resize(32, 32) .png({ compressionLevel: 9 }) .toFile(path.resolve(process.cwd(), 'public/favicon.png')); console.log('[icons] favicon.png (32px)');