// Renders og image, favicons, PWA icons, and .kody/icon.png with headless Chrome. // Usage: node scripts/render-art.mjs (needs playwright-core + a Chrome; set CHROME_PATH if not /usr/bin/google-chrome) import { chromium } from 'playwright-core' import { writeFileSync, mkdirSync } from 'node:fs' const BG = '#05070d', INK = '#e8f1ff', GLOW = '#7fb4ff' // Deterministic pseudo-random so renders are reproducible. let seed = 7; const rnd = () => ((seed = (seed * 16807) % 2147483647) / 2147483647) const rockPath = (cx, cy, r, n = 11) => { const pts = []; for (let i = 0; i < n; i++) { const a = (i / n) * Math.PI * 2, k = 0.72 + rnd() * 0.36; pts.push([cx + Math.cos(a) * r * k, cy + Math.sin(a) * r * k]) } return 'M' + pts.map((p) => p.map((v) => v.toFixed(1)).join(' ')).join(' L') + ' Z' } const ship = (x, y, s, rot, px = 3) => `` const flame = (x, y, s, rot, px = 3) => `` const glowDefs = (b) => ` ` function ogSvg() { seed = 11 const stars = Array.from({ length: 140 }, () => ``).join('') const rocks = [[150, 500, 95], [1060, 480, 105], [1110, 70, 42], [70, 70, 34], [340, 440, 22], [1165, 300, 18], [960, 395, 22], [250, 400, 18]] .map(([x, y, r]) => ``).join('') const bullets = [[720, 438], [762, 408], [804, 378]].map(([x, y]) => ``).join('') const debris = Array.from({ length: 18 }, () => { const a = rnd() * 6.28, d = 20 + rnd() * 60; return `` }).join('') return `${glowDefs(5)} ${stars} ${rocks}${bullets}${debris}${flame(640, 480, 4.5, -35, 3.2)}${ship(640, 480, 4.5, -35, 3.2)} ASTEROIDS VECTOR ARCADE ยท KEYBOARD OR TOUCH ASTEROIDS.KENTCDODDS.COM` } // Icon: glowing ship flying past an asteroid. `pad` shrinks content into the maskable safe zone. function iconSvg(size, { pad = 0, round = true } = {}) { seed = 3 const k = 1 - pad * 2, o = 256 * pad * 2 const inner = ` ${flame(175, 190, 6.5, -40, 12)}${ship(175, 190, 6.5, -40, 13)}` return `${glowDefs(9)} ${inner}` } // Tiny favicon: just the ship, thicker strokes, readable at 16-32px. function faviconSvg() { return `` } const out = { 'public/og.jpg': [ogSvg(), 1200, 630], 'public/icons/icon-192.png': [iconSvg(192), 192, 192], 'public/icons/icon-512.png': [iconSvg(512), 512, 512], 'public/icons/icon-maskable-512.png': [iconSvg(512, { pad: 0.1, round: false }), 512, 512], 'public/icons/apple-touch-icon.png': [iconSvg(180, { round: false }), 180, 180], 'public/icons/favicon-32.png': [faviconSvg(), 32, 32], '.kody/icon.png': [iconSvg(512), 512, 512], } mkdirSync('public/icons', { recursive: true }) writeFileSync('public/icons/favicon.svg', faviconSvg()) writeFileSync('.kody/icon.svg', iconSvg(512)) const browser = await chromium.launch({ executablePath: process.env.CHROME_PATH || '/usr/bin/google-chrome' }) for (const [file, [svg, w, h]] of Object.entries(out)) { const page = await browser.newPage({ viewport: { width: w, height: h } }) await page.setContent(`${svg.replace(/width="\d+" height="\d+" viewBox/, `width="${w}" height="${h}" viewBox`)}`) const jpeg = file.endsWith('.jpg') await page.screenshot({ path: file, type: jpeg ? 'jpeg' : 'png', quality: jpeg ? 86 : undefined, omitBackground: !jpeg, clip: { x: 0, y: 0, width: w, height: h } }) await page.close(); console.log('wrote', file) } await browser.close()