nuts / lava / files / scripts / generate-icons.ts
Branch lava · default branch · all branches
generate-icons.ts
44eb5832 2026-04-12 Pavel Safronov | 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/') |