nuts / files / scripts / generate-icons.ts
generate-icons.ts
| 1 | /** |
| 2 | * Generates all icon / favicon assets from acorn.png into public/. |
| 3 | * Run with: pnpm generate-icons |
| 4 | * |
| 5 | * Icon by Freepik – Flaticon |
| 6 | * https://www.flaticon.com/search?author_id=296&style_id=1001&type=standard |
| 7 | */ |
| 8 | |
| 9 | import sharp from 'sharp' |
| 10 | import { mkdir } from 'node:fs/promises' |
| 11 | import { resolve, dirname } from 'node:path' |
| 12 | import { fileURLToPath } from 'node:url' |
| 13 | |
| 14 | const __dirname = dirname(fileURLToPath(import.meta.url)) |
| 15 | const root = resolve(__dirname, '..') |
| 16 | const src = resolve(root, 'acorn.png') |
| 17 | const out = resolve(root, 'public') |
| 18 | |
| 19 | await mkdir(out, { recursive: true }) |
| 20 | |
| 21 | // ── Size definitions ─────────────────────────────────────────────────────── |
| 22 | interface Color { |
| 23 | r: number |
| 24 | g: number |
| 25 | b: number |
| 26 | alpha: number |
| 27 | } |
| 28 | |
| 29 | interface Task { |
| 30 | file: string |
| 31 | size: number |
| 32 | bg: Color |
| 33 | padFraction?: number |
| 34 | } |
| 35 | |
| 36 | const DARK_BG: Color = { r: 17, g: 17, b: 17, alpha: 1 } |
| 37 | const WHITE_BG: Color = { r: 255, g: 255, b: 255, alpha: 1 } |
| 38 | |
| 39 | const tasks: Task[] = [ |
| 40 | // Standard favicons (white background – legible at tiny sizes on all UIs) |
| 41 | { file: 'favicon-16x16.png', size: 16, bg: WHITE_BG }, |
| 42 | { file: 'favicon-32x32.png', size: 32, bg: WHITE_BG }, |
| 43 | // Apple Touch Icon (dark background to match the app) |
| 44 | { file: 'apple-touch-icon.png', size: 180, bg: DARK_BG }, |
| 45 | // PWA / Android icons |
| 46 | { file: 'icon-192.png', size: 192, bg: DARK_BG }, |
| 47 | { file: 'icon-512.png', size: 512, bg: DARK_BG }, |
| 48 | // OG / share image (larger, dark bg, the icon centred with padding) |
| 49 | { file: 'og-image.png', size: 512, bg: DARK_BG, padFraction: 0.15 }, |
| 50 | ] |
| 51 | |
| 52 | for (const { file, size, bg, padFraction = 0 } of tasks) { |
| 53 | const iconSize = Math.round(size * (1 - padFraction * 2)) |
| 54 | const dest = resolve(out, file) |
| 55 | const transparent = { r: 0, g: 0, b: 0, alpha: 0 } as const |
| 56 | |
| 57 | let pipeline = sharp(src).resize(iconSize, iconSize, { |
| 58 | fit: 'contain', |
| 59 | background: transparent, |
| 60 | }) |
| 61 | |
| 62 | if (padFraction > 0) { |
| 63 | const pad = Math.round(size * padFraction) |
| 64 | pipeline = pipeline.extend({ |
| 65 | top: pad, |
| 66 | bottom: pad, |
| 67 | left: pad, |
| 68 | right: pad, |
| 69 | background: transparent, |
| 70 | }) |
| 71 | } |
| 72 | |
| 73 | await pipeline.flatten({ background: bg }).png().toFile(dest) |
| 74 | console.log(` ✓ ${file} (${size}×${size})`) |
| 75 | } |
| 76 | |
| 77 | console.log('\nAll icons written to public/') |