/** * Generate PWA icons from the LogShip brand mark (public/icon.png, 512x512). * Uses sharp library to resize the PNG to the different PWA sizes, plus * "maskable" variants (mark scaled down on a white square) because the * full-bleed mark would be clipped by Android's circular icon mask. * * Run: node scripts/generate-icons.js */ import fs from 'fs' import path from 'path' import { fileURLToPath } from 'url' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) // Icon sizes needed for PWA const sizes = [72, 96, 128, 144, 152, 192, 384, 512] // Maskable sizes (referenced in server/routes/manifest.json.get.ts) const maskableSizes = [192, 512] const sourcePath = path.join(__dirname, '../public/icon.png') const publicDir = path.join(__dirname, '../public') async function main() { const sourceBuffer = fs.readFileSync(sourcePath) console.log('šŸ“± Generating PWA icons...') // Try to use sharp if available, otherwise create placeholder let sharp try { sharp = (await import('sharp')).default } catch (e) { console.warn('āš ļø Sharp not installed. Creating placeholder icons.') console.log(' To generate proper icons, run: npm install --save-dev sharp') console.log(' Then run this script again: node scripts/generate-icons.js') // Create simple placeholder text files sizes.forEach(size => { const filename = `icon-${size}x${size}.png` const filepath = path.join(publicDir, filename) // Create a simple placeholder (not a real PNG, but prevents 404 errors) fs.writeFileSync(filepath, `Placeholder for ${size}x${size} icon`) console.log(` Created placeholder: ${filename}`) }) console.log('\nāœ… Placeholder icons created') console.log(' Install sharp and regenerate for proper PNG icons') process.exit(0) } // Generate icons using sharp for (const size of sizes) { const filename = `icon-${size}x${size}.png` const filepath = path.join(publicDir, filename) try { await sharp(sourceBuffer) .resize(size, size) .png() .toFile(filepath) console.log(`āœ… Generated: ${filename}`) } catch (error) { console.error(`āŒ Failed to generate ${filename}:`, error.message) } } // Maskable variants: mark at ~66% centered on a white square so the OS // mask (circle/squircle) never clips the artwork. for (const size of maskableSizes) { const filename = `icon-maskable-${size}x${size}.png` const filepath = path.join(publicDir, filename) const markSize = Math.round(size * 0.66) try { const mark = await sharp(sourceBuffer) .resize(markSize, markSize) .png() .toBuffer() await sharp({ create: { width: size, height: size, channels: 4, background: { r: 255, g: 255, b: 255, alpha: 1 } } }) .composite([{ input: mark, gravity: 'center' }]) .png() .toFile(filepath) console.log(`āœ… Generated: ${filename}`) } catch (error) { console.error(`āŒ Failed to generate ${filename}:`, error.message) } } console.log('\nšŸŽ‰ All icons generated successfully!') } main().catch(error => { console.error('āŒ Error generating icons:', error) process.exit(1) })