#!/usr/bin/env bun // Renders the desktop dev fixtures' art (apps/desktop/src/dev/art/*.png): app icons and store screenshot stand-ins, // each a Phosphor icon (@phosphor-icons/core, MIT, "fill" weight) on a CSS gradient, photographed by Playwright's // Chromium. No drawn SVG: the icons are Phosphor's own files. Dev only (the fixtures never ship in the release bundle). // bun tools/fixture-art.ts (Playwright's Chromium, or an installed Google Chrome; same launcher as storebound shots) import { mkdirSync, readFileSync, renameSync } from 'node:fs'; import { join } from 'node:path'; import { launch } from '../cli/shots/browser'; const root = new URL('..', import.meta.url).pathname; const out = join(root, 'apps/desktop/src/dev/art'); const phosphor = (name: string) => readFileSync(join(root, 'node_modules/@phosphor-icons/core/assets/fill', `${name}-fill.svg`), 'utf8'); /** [file, Phosphor icon, gradient from, gradient to] */ const ICONS: [string, string, string, string][] = [ ['icon-prism', 'drop', '#7c5cff', '#ff5fa2'], ['icon-tidy', 'list-checks', '#2fd18b', '#0f9d74'], ['icon-orbit', 'planet', '#0ea5e9', '#6366f1'], ['icon-block', 'cube', '#22c55e', '#15803d'], ['icon-lantern', 'lamp', '#f59e0b', '#b45309'], ['icon-tycoon', 'coins', '#a855f7', '#6d28d9'], ['icon-racer', 'car', '#ef4444', '#991b1b'], ['icon-habit', 'egg', '#14b8a6', '#0f766e'], ['icon-focus', 'timer', '#f43f5e', '#be123c'], ['icon-sketch', 'star', '#fb923c', '#ea580c'], ['icon-night', 'moon', '#1e293b', '#4338ca'], ['icon-mood', 'grid-four', '#ec4899', '#8b5cf6'], ]; /** Four scenes of the demo game: [Phosphor icon, from, to, accent] */ const SCENES: [string, string, string, string][] = [ ['drop', '#7c5cff', '#ff5fa2', '#ffd36e'], ['lightning', '#ff7a59', '#ff3d7f', '#ffe08a'], ['calendar-dots', '#2fb5ff', '#5b6cff', '#b8f3ff'], ['trophy', '#ffb03b', '#ff7a1a', '#fff1b8'], ]; /** Screenshot shapes at a small preview size, in the stores' aspect ratios. */ const SHAPES: [string, number, number][] = [['phone', 240, 521], ['play', 240, 427], ['ipad', 300, 400], ['feature', 512, 250]]; const page = (w: number, h: number, body: string, css: string) => `
${body}`; mkdirSync(out, { recursive: true }); const browser = await launch(root, {}); const tab = await browser.newPage({ deviceScaleFactor: 1 }); async function snap(file: string, w: number, h: number, html: string) { await tab.setViewportSize({ width: w, height: h }); await tab.setContent(html); const png = join(out, `${file}.png`); await tab.screenshot({ path: png, clip: { x: 0, y: 0, width: w, height: h } }); // 8-bit palette PNG (ffmpeg, already needed for storebound icons): about a quarter of the bytes; kept as is without it if (Bun.which('ffmpeg')) { const tmp = `${png}.q.png`; const r = Bun.spawnSync(['ffmpeg', '-v', 'error', '-y', '-i', png, '-vf', 'split[a][b];[a]palettegen=max_colors=96:stats_mode=full[p];[b][p]paletteuse=dither=bayer:bayer_scale=3', tmp]); if (r.exitCode === 0) renameSync(tmp, png); } console.log(`fixture-art: ${file}.png ${w}x${h}`); } for (const [file, icon, a, b] of ICONS) { await snap(file, 128, 128, page(128, 128, `