// Regenerates the favicon set in public/ from the Tabler antenna-tower icon. // Usage: npm run icons import { writeFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import sharp from 'sharp'; const require = createRequire(import.meta.url); const { icons } = require('@iconify-json/tabler/icons.json'); const RED = '#c95748'; const INK = '#111012'; const tower = (stroke) => icons['building-broadcast-tower'].body .replaceAll('currentColor', INK) .replaceAll('stroke-width="2"', `stroke-width="${stroke}"`); /** * Square icon: red background, ink tower. * `pad` = icon inset (0..1), `radius` in viewBox units, `stroke` = Tabler stroke width. */ const svg = ({ pad = 0.14, radius = 6, stroke = 2.4 } = {}) => { const size = 32; const inner = size * (1 - pad * 2); const offset = size * pad; const scale = inner / 24; return `${tower(stroke)}`; }; const png = (source, px) => sharp(Buffer.from(source), { density: 72 * (px / 32) * 4 }) .resize(px, px) .png() .toBuffer(); /** ICO container with embedded PNG images. */ function ico(images) { const header = Buffer.alloc(6 + images.length * 16); header.writeUInt16LE(0, 0); header.writeUInt16LE(1, 2); header.writeUInt16LE(images.length, 4); let offset = header.length; images.forEach(({ size, data }, i) => { const e = 6 + i * 16; header.writeUInt8(size >= 256 ? 0 : size, e); header.writeUInt8(size >= 256 ? 0 : size, e + 1); header.writeUInt16LE(1, e + 4); // colour planes header.writeUInt16LE(32, e + 6); // bits per pixel header.writeUInt32LE(data.length, e + 8); header.writeUInt32LE(offset, e + 12); offset += data.length; }); return Buffer.concat([header, ...images.map((i) => i.data)]); } const rounded = svg(); const tiny = svg({ pad: 0.06, radius: 4, stroke: 3.2 }); // 16 px tab icon: bigger, bolder const square = svg({ radius: 0 }); // platforms apply their own mask const maskable = svg({ pad: 0.24, radius: 0 }); // keep the icon inside the 80% safe zone writeFileSync('public/favicon.svg', rounded); writeFileSync( 'public/favicon.ico', ico( await Promise.all( [16, 32, 48].map(async (size) => ({ size, data: await png(size === 16 ? tiny : rounded, size), })), ), ), ); writeFileSync('public/apple-touch-icon.png', await png(svg({ pad: 0.18, radius: 0 }), 180)); writeFileSync('public/icon-192.png', await png(square, 192)); writeFileSync('public/icon-512.png', await png(square, 512)); writeFileSync('public/icon-maskable-512.png', await png(maskable, 512)); console.log('Icons written to public/');