22// SVG used as the app wordmark. Run with: node scripts/generate-icons.js
33import sharp from 'sharp'
44import { join , dirname } from 'path'
5+ import { writeFileSync } from 'fs'
56import { fileURLToPath } from 'url'
67
78const __dirname = dirname ( fileURLToPath ( import . meta. url ) )
8- const outDir = join ( __dirname , '..' , 'public' , 'icons' )
9+ const publicDir = join ( __dirname , '..' , 'public' )
10+ const outDir = join ( publicDir , 'icons' )
11+
12+ // Build an ICO file from one or more PNG buffers.
13+ // ICO format: 6-byte header, 16-byte directory entry per image, then raw PNG data.
14+ function buildIco ( pngBuffers ) {
15+ const count = pngBuffers . length
16+ const headerSize = 6
17+ const dirEntrySize = 16
18+ const dataOffset = headerSize + dirEntrySize * count
19+
20+ const header = Buffer . alloc ( headerSize )
21+ header . writeUInt16LE ( 0 , 0 ) // reserved
22+ header . writeUInt16LE ( 1 , 2 ) // type: 1 = ICO
23+ header . writeUInt16LE ( count , 4 ) // image count
24+
25+ const dirEntries = [ ]
26+ let offset = dataOffset
27+ for ( const png of pngBuffers ) {
28+ const entry = Buffer . alloc ( dirEntrySize )
29+ // width/height: 0 means 256 in ICO spec; for <=255 use actual value
30+ const meta = sharp ( png )
31+ // We already know the sizes we're passing in, but we encode from the PNG header
32+ // For simplicity, we set width/height to 0 (works for all sizes up to 256)
33+ entry . writeUInt8 ( 0 , 0 ) // width (0 = 256 or "read from data")
34+ entry . writeUInt8 ( 0 , 1 ) // height
35+ entry . writeUInt8 ( 0 , 2 ) // color palette
36+ entry . writeUInt8 ( 0 , 3 ) // reserved
37+ entry . writeUInt16LE ( 1 , 4 ) // color planes
38+ entry . writeUInt16LE ( 32 , 6 ) // bits per pixel
39+ entry . writeUInt32LE ( png . length , 8 ) // data size
40+ entry . writeUInt32LE ( offset , 12 ) // data offset
41+ dirEntries . push ( entry )
42+ offset += png . length
43+ }
44+
45+ return Buffer . concat ( [ header , ...dirEntries , ...pngBuffers ] )
46+ }
947
1048// Uses the same circle layout as the HomeView wordmark-icon, but with
1149// icon-specific explicit colours and stroke/opacity values for PNG output.
@@ -25,9 +63,53 @@ const svgTemplate = (size) => {
2563</svg>`
2664}
2765
66+ // Maskable icons need a safe zone (inner 80% circle). Add extra padding
67+ // by scaling the viewBox content down so the design sits within the safe area.
68+ const maskableSvgTemplate = ( size ) => {
69+ const bg = '#0d0d1a'
70+ const fg = '#00e676'
71+ const rx = Math . round ( size * 0.22 )
72+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${ size } " height="${ size } " viewBox="0 0 32 32" fill="none">
73+ <rect width="32" height="32" rx="${ rx / ( size / 32 ) } " fill="${ bg } "/>
74+ <g transform="translate(16,16) scale(0.75) translate(-16,-16)">
75+ <circle cx="16" cy="16" r="14" stroke="${ fg } " stroke-width="1.2" opacity="0.35"/>
76+ <circle cx="16" cy="16" r="9" stroke="${ fg } " stroke-width="1.2" opacity="0.65"/>
77+ <circle cx="16" cy="16" r="4" stroke="${ fg } " stroke-width="1.2"/>
78+ <circle cx="16" cy="16" r="1.5" fill="${ fg } "/>
79+ </g>
80+ </svg>`
81+ }
82+
2883for ( const size of [ 192 , 512 ] ) {
2984 const svg = Buffer . from ( svgTemplate ( size ) )
3085 const dest = join ( outDir , `icon-${ size } .png` )
3186 await sharp ( svg ) . resize ( size , size ) . png ( ) . toFile ( dest )
3287 console . log ( `✓ icon-${ size } .png` )
3388}
89+
90+ // Apple touch icon (180×180)
91+ {
92+ const svg = Buffer . from ( svgTemplate ( 180 ) )
93+ const dest = join ( outDir , `apple-touch-icon.png` )
94+ await sharp ( svg ) . resize ( 180 , 180 ) . png ( ) . toFile ( dest )
95+ console . log ( `✓ apple-touch-icon.png` )
96+ }
97+
98+ // Maskable icon (512×512) — design scaled to 75% to fit within the safe zone
99+ {
100+ const svg = Buffer . from ( maskableSvgTemplate ( 512 ) )
101+ const dest = join ( outDir , `icon-maskable-512.png` )
102+ await sharp ( svg ) . resize ( 512 , 512 ) . png ( ) . toFile ( dest )
103+ console . log ( `✓ icon-maskable-512.png` )
104+ }
105+
106+ // Favicon (ICO with 32×32 and 16×16)
107+ {
108+ const sizes = [ 32 , 16 ]
109+ const pngBuffers = await Promise . all (
110+ sizes . map ( s => sharp ( Buffer . from ( svgTemplate ( s ) ) ) . resize ( s , s ) . png ( ) . toBuffer ( ) )
111+ )
112+ const ico = buildIco ( pngBuffers )
113+ writeFileSync ( join ( publicDir , 'favicon.ico' ) , ico )
114+ console . log ( `✓ favicon.ico` )
115+ }
0 commit comments