'use strict'; // Slice the whole shipped icon set from the brand master (build-icon.mjs). Renders // with the ui package's Playwright chromium. See docs/ICONS.md for the target map. // // npm i opentype.js && node build-set.mjs // // Writes: ui/public/* (web), ui/assets/* (Capacitor layers), ./vk/* and ./store/* // (manual-upload art). Wiring (manifest, html, Android res) is done separately. import { execFileSync } from 'node:child_process'; import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, join } from 'node:path'; const DIR = dirname(fileURLToPath(import.meta.url)); const UI = join(DIR, '..', '..', '..', 'ui'); const PUB = join(UI, 'public'), ASSETS = join(UI, 'assets'); const VK = join(DIR, 'vk'), STORE = join(DIR, 'store'), TG = join(DIR, 'tg'); [VK, STORE, TG].forEach(d => mkdirSync(d, { recursive: true })); // one SVG string for a variant+layer from the reference generator const svgFor = (variant, layer) => execFileSync('node', [join(DIR, 'build-icon.mjs'), '--variant', variant, '--layer', layer], { encoding: 'utf8' }); const pw = await import(join(UI, 'node_modules', '@playwright', 'test', 'index.js')); const { chromium } = pw.default ?? pw; const browser = await chromium.launch(); const page = await browser.newPage(); async function pngFromSvg(svg, size, transparent) { await page.setViewportSize({ width: size, height: size }); await page.setContent(`${svg}`, { waitUntil: 'networkidle' }); return page.locator('svg').screenshot({ omitBackground: !!transparent }); } async function shootHtml(html, w, h) { await page.setViewportSize({ width: w, height: h }); await page.setContent(html, { waitUntil: 'networkidle' }); return page.screenshot(); } function icoFromPNG(png, sizePx) { const h = Buffer.alloc(22); h.writeUInt16LE(0, 0); h.writeUInt16LE(1, 2); h.writeUInt16LE(1, 4); h.writeUInt8(sizePx, 6); h.writeUInt8(sizePx, 7); h.writeUInt16LE(1, 10); h.writeUInt16LE(32, 12); h.writeUInt32LE(png.length, 14); h.writeUInt32LE(22, 18); return Buffer.concat([h, png]); } const write = (p, buf) => { writeFileSync(p, buf); console.log('wrote', p.replace(join(DIR, '..', '..', '..'), '.'), buf.length, 'b'); }; // ---- pre-render the SVG sources we need ---- const S = { fullLight: svgFor('light', 'full'), fullDark: svgFor('dark', 'full'), flatLight: svgFor('light', 'flat'), flatDark: svgFor('dark', 'flat'), maskLight: svgFor('light', 'maskable'), maskDark: svgFor('dark', 'maskable'), bgLight: svgFor('light', 'background'), fgLight: svgFor('light', 'foreground'), monoLight: svgFor('light', 'monochrome'), }; // ---- favicon.svg: light + dark in one file, toggled by prefers-color-scheme ---- const inner = svg => svg.replace(/^]*>/, '').replace(/<\/svg>\s*$/, ''); const faviconSvg = `` + `` + `${inner(S.fullLight)}${inner(S.fullDark)}\n`; write(join(PUB, 'favicon.svg'), Buffer.from(faviconSvg)); // ---- web rasters ---- write(join(PUB, 'favicon.ico'), icoFromPNG(await pngFromSvg(S.flatLight, 32, false), 32)); write(join(PUB, 'apple-touch-icon.png'), await pngFromSvg(S.flatLight, 180, false)); write(join(PUB, 'icon-192.png'), await pngFromSvg(S.flatLight, 192, false)); write(join(PUB, 'icon-512.png'), await pngFromSvg(S.flatLight, 512, false)); write(join(PUB, 'icon-192-dark.png'), await pngFromSvg(S.flatDark, 192, false)); write(join(PUB, 'icon-512-dark.png'), await pngFromSvg(S.flatDark, 512, false)); write(join(PUB, 'icon-maskable-512.png'), await pngFromSvg(S.maskLight, 512, false)); write(join(PUB, 'icon-maskable-512-dark.png'), await pngFromSvg(S.maskDark, 512, false)); // ---- Capacitor layers (ui/assets) ---- write(join(ASSETS, 'icon.png'), await pngFromSvg(S.flatLight, 1024, false)); // legacy single write(join(ASSETS, 'icon-foreground.png'), await pngFromSvg(S.fgLight, 1024, true)); // adaptive fg write(join(ASSETS, 'icon-background.png'), await pngFromSvg(S.bgLight, 1024, false)); // adaptive bg write(join(DIR, 'android-monochrome.png'), await pngFromSvg(S.monoLight, 1024, true)); // themed layer (wired manually) // ---- VK (no rounding — flat light) ---- for (const px of [576, 278, 150, 32]) write(join(VK, `vk-${px}.png`), await pngFromSvg(S.flatLight, px, false)); // ---- Telegram bot: square, no rounding (TG crops the avatar to a circle) ---- write(join(TG, 'tg-512.png'), await pngFromSvg(S.flatLight, 512, false)); // bot avatar + Mini App icon // ---- store art ---- write(join(STORE, 'rustore-512.png'), await pngFromSvg(S.flatLight, 512, false)); // ---- wordmark banner (board-green bg + master tile + «Эрудит» / Игра в слова) ---- const b64 = readFileSync(join(DIR, 'Spectral-Bold.ttf')).toString('base64'); const banner = (w, h, tile, t1, t2, gap, pad) => `
${S.fullLight}
«Эрудит»
Игра в слова
`; write(join(PUB, 'og-image.png'), await shootHtml(banner(1200, 630, 300, 150, 74, 64, 96), 1200, 630)); write(join(TG, 'tg-demo-640x360.png'), await shootHtml(banner(640, 360, 150, 72, 38, 30, 44), 640, 360)); await browser.close(); console.log('done');