diff --git a/apps/site/scripts/record-hero.tsx b/apps/site/scripts/record-hero.tsx new file mode 100644 index 0000000..a495909 --- /dev/null +++ b/apps/site/scripts/record-hero.tsx @@ -0,0 +1,166 @@ +/** + * Records `assets/hero-loop.webp` from the REAL playground. + * + * Why a recorder exists at all: the hero is the first thing a visitor sees, and it has to show a pointer. + * Visimer's claim is direct manipulation of the rendered diagram, so a clip with no cursor is + * indistinguishable from a source-pane screencast — the double-click that renames the node is the product, + * and an invisible double-click proves nothing. The previous asset was recorded without one, and there was + * no script to re-record it. This is that script. + * + * Everything in frame is the real app: real Mermaid dagre layout, a real double-click, real keystrokes, the + * editor's own caret, and the app's own source-sync highlight. The ONLY painted element is the pointer, + * because /playground paints none (/hero-loop does, but it is a different surface with different chrome). + * + * Determinism: the playground animates nothing on its own between inputs, so this owns the frame clock + * rather than sampling wall time. Each frame positions the overlay, fires at most one real input, then + * screenshots — so frame N is reproducible run to run. + * + * PREREQUISITES (deliberately not repo dependencies — this runs about twice a year, and neither belongs in + * every contributor's install): + * 1. playwright pnpm --filter site add -D playwright && pnpm --filter site exec playwright install chromium + * 2. img2webp brew install webp (libwebp; also provides webpmux for inspection) + * + * USAGE: + * pnpm dev:site # serves /playground on :5174 + * pnpm --filter site exec tsx scripts/record-hero.tsx + * # then follow the img2webp line it prints + */ +import { chromium } from 'playwright' +import { mkdirSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +const URL_BASE = process.env.HERO_URL ?? 'http://localhost:5174' +const OUT = join(tmpdir(), 'visimer-hero-frames') + +/** The diagram the hero renames a node in. Seeded through the playground's own `#code=` hash. */ +const SRC = `flowchart TD + A[Open a PR] --> B{Tests green?} + B -->|yes| C[Ship it] + B -->|no| D[Fix bugs] + D --> A` +const TARGET_LABEL = 'Ship it' +const NEW_LABEL = 'Deploy' + +/** + * The pointer, copied in from openstylus `openstyles/scene/Cursor.tsx` (declared `kind:"asset"` there, and + * that DS declares `distribution: "copy-in"`; its `openstyles/` tree is not in the published package, so + * copy-in is the sanctioned path rather than a dependency). + * + * An OS pointer is a faithful reproduction of fixed system chrome, so these two ink values are the + * reproduced artifact's own bytes — not themeable, not palette. The silhouette is drawn twice, a fattened + * outline pass beneath a fill pass, so the edge survives WebP/GIF quantization over a busy diagram canvas. + * + * HOTSPOTS are in the source's 24-unit box: the arrow's tip is (3,2), the I-beam's centre is (12,12). + * Position by hotspot, never by the SVG's corner, or the visible tip sits several px off the thing it is + * meant to be touching and the shot stops being evidence of anything. + */ +const ARROW_SVG = `` +const IBEAM_SVG = `` + +const ease = (t: number): number => 1 - (1 - t) ** 3 +const lerp = (a: number, b: number, t: number): number => a + (b - a) * t + +const encodeCode = (code: string): string => { + const bytes = new TextEncoder().encode(code) + let bin = '' + for (const b of bytes) bin += String.fromCharCode(b) + return btoa(bin).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '') +} + +rmSync(OUT, { recursive: true, force: true }) +mkdirSync(OUT, { recursive: true }) + +const browser = await chromium.launch() +// 1200x675 at deviceScaleFactor 2 — a 1200px layout shot at 2x, which is what the published 2400x1350 +// asset is. Rendering into a 2400px viewport instead reflows the layout and shrinks everything. +const page = await browser.newPage({ viewport: { width: 1200, height: 675 }, deviceScaleFactor: 2 }) +await page.goto(`${URL_BASE}/playground#code=${encodeCode(SRC)}`, { waitUntil: 'networkidle' }) +await page.waitForSelector('g.node', { timeout: 15_000 }) +await page.waitForTimeout(1200) + +await page.evaluate( + ({ arrow, ibeam }) => { + const host = document.createElement('div') + host.id = '__hero_cursor' + host.style.cssText = 'position:fixed;left:0;top:0;pointer-events:none;z-index:2147483647;' + host.innerHTML = `