#!/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, `
${phosphor(icon)}
`, `.t{width:128px;height:128px;display:grid;place-items:center;background:linear-gradient(135deg,${a},${b});color:#fff}.t svg{width:72px;height:72px}`)); } for (const [i, [icon, a, b, c]] of SCENES.entries()) { for (const [shape, w, h] of SHAPES) { const land = w > h; const phone = land ? { w: h * 0.42, h: h * 0.86 } : { w: w * 0.62, h: Math.min(h * 0.72, w * 0.62 * 2.05) }; await snap(`scene${i + 1}-${shape}`, w, h, page(w, h, `
${phosphor(icon)}
${phosphor(icon)}
`, `.bg{position:relative;width:${w}px;height:${h}px;background:linear-gradient(135deg,${a},${b});color:#fff;overflow:hidden} .glow{position:absolute;inset:0;background:radial-gradient(circle at 50% 30%,rgba(255,255,255,.35),transparent 70%)} .big{position:absolute;${land ? `left:${w * 0.1}px;top:${h * 0.22}px` : `left:50%;top:${h * 0.05}px;transform:translateX(-50%)`};color:${c}} .big svg{width:${land ? h * 0.5 : w * 0.26}px;height:${land ? h * 0.5 : w * 0.26}px} .ph{position:absolute;width:${phone.w}px;height:${phone.h}px;${land ? `right:${w * 0.1}px;top:${h * 0.07}px` : `left:50%;bottom:-${phone.w * 0.12}px;transform:translateX(-50%)`}; border-radius:${phone.w * 0.14}px;background:#10142a;border:${Math.max(2, phone.w * 0.03)}px solid rgba(255,255,255,.85);padding:${phone.w * 0.06}px} .scr{width:100%;height:100%;border-radius:${phone.w * 0.08}px;background:#1d2350;display:grid;place-items:center;color:${c}} .scr svg{width:45%;height:45%}`)); } } await browser.close();