Something went wrong. Try again.
ATProto-based AI Assistant Chat interface.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384// Draws the app's icons, which installing the app or adding it to a phone's home screen shows,// from src/logo.svg on a square tile, through headless Chrome. Run it with// `pnpm --filter @scn-chat/web icons` after changing the logo, and commit what it writes to// public/icons/. `CHROME` names the browser's binary where it isn't in the macOS default place.import { execFileSync } from 'node:child_process'import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'import { tmpdir } from 'node:os'import { join } from 'node:path'import { fileURLToPath } from 'node:url'
const browser = process.env.CHROME ?? '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
/** * The theme's tokens, taken from theme.css so the icons follow its colors. The tile and the logo * are drawn in the light scheme, as the sign-in page shows them there. */const tokens = Array.from( readFileSync(new URL('../src/styles/theme.css', import.meta.url), 'utf8') .replace(/\/\*[\s\S]*?\*\//g, '') .matchAll(/(--[\w-]+)\s*:\s*([^;{}]+);/g), (match) => `${match[1]}:${match[2]};`,).join('')
type Icon = { file: string size: number /** The logo's width, as a share of the icon's. */ logo: number /** The tile's corner radius, as a share of the icon's size. A platform that rounds it gets 0. */ radius: number}
const icons: Icon[] = [ { file: 'icon-192.png', size: 192, logo: 0.68, radius: 0.225 }, { file: 'icon-512.png', size: 512, logo: 0.68, radius: 0.225 }, // A launcher crops a maskable icon to its own shape, keeping only the middle 80% circle. { file: 'icon-maskable-512.png', size: 512, logo: 0.58, radius: 0 }, // iOS rounds the corners itself, and fills any transparency with black. { file: 'apple-touch-icon.png', size: 180, logo: 0.68, radius: 0 },]
const logo = readFileSync(new URL('../src/logo.svg', import.meta.url), 'utf8') // The logo's own colors follow the system's scheme, which an icon mustn't. .replace(/<style>[\s\S]*?<\/style>/, '') .replace(/<title>.*?<\/title>/, '') .replace('width="34" height="16"', 'width="100%"')
function page(icon: Icon) { return `<!doctype html><style>:root{color-scheme:light;${tokens}}html,body{margin:0;background:transparent}.tile{display:grid;place-items:center;width:${icon.size}px;height:${icon.size}px;background:var(--color-panel);border-radius:${icon.size * icon.radius}px}.logo{width:${icon.logo * 100}%;line-height:0}.face{fill:var(--color-logo-face)}path[mask]{fill:var(--color-logo-rim)}</style><div class="tile"><div class="logo">${logo}</div></div>`}
const out = fileURLToPath(new URL('../public/icons/', import.meta.url))const work = mkdtempSync(join(tmpdir(), 'scn-icons-'))try { for (const icon of icons) { const html = join(work, `${icon.file}.html`) writeFileSync(html, page(icon)) execFileSync( browser, [ '--headless', '--disable-gpu', '--hide-scrollbars', `--user-data-dir=${join(work, 'profile')}`, '--force-device-scale-factor=1', '--default-background-color=00000000', `--window-size=${icon.size},${icon.size}`, `--screenshot=${join(out, icon.file)}`, `file://${html}`, ], { stdio: 'ignore' }, ) console.log(`public/icons/${icon.file}`) }} finally { rmSync(work, { recursive: true, force: true })}