From 54992a7d6f26e5ae1c79c49dfedc6e9b5e7d7676 Mon Sep 17 00:00:00 2001 From: William Thorsen Date: Tue, 10 Mar 2026 00:27:10 -0700 Subject: [PATCH 1/3] factory|feat: Add HTML preview page for sprite sheets Add preview-renderer.ts that generates a self-contained HTML page displaying all 12 labeled sprite frames in a 4x3 grid and 6 animated previews for each agent type. Integrate into generate-placeholder-pngs.ts to emit sprite-preview.html alongside the existing SVGs. --- .gitignore | 1 + packages/factory/package.json | 1 + .../scripts/generate-placeholder-pngs.ts | 13 ++ .../__tests__/preview-renderer.test.ts | 79 +++++++ .../scripts/sprites/preview-renderer.ts | 200 ++++++++++++++++++ 5 files changed, 294 insertions(+) create mode 100644 packages/factory/scripts/sprites/__tests__/preview-renderer.test.ts create mode 100644 packages/factory/scripts/sprites/preview-renderer.ts diff --git a/.gitignore b/.gitignore index 57d318f1..b630c704 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ out/ .vercel # Local-only +.local/ local/ tmp/ *.local diff --git a/packages/factory/package.json b/packages/factory/package.json index 8ba9a9be..d712cd22 100644 --- a/packages/factory/package.json +++ b/packages/factory/package.json @@ -9,6 +9,7 @@ "dev": "tsx src/server/index.ts & pnpm vite --host", "dev:client": "pnpm vite --host", "dev:server": "tsx --watch src/server/index.ts", + "generate-sprites": "tsx scripts/generate-placeholder-pngs.ts", "validate:run-index": "tsx src/scripts/validate-run-index.ts", "ws": "node --import tsx ../../scripts/run-workspace-script.ts" }, diff --git a/packages/factory/scripts/generate-placeholder-pngs.ts b/packages/factory/scripts/generate-placeholder-pngs.ts index 08ef7f8e..5e277297 100644 --- a/packages/factory/scripts/generate-placeholder-pngs.ts +++ b/packages/factory/scripts/generate-placeholder-pngs.ts @@ -12,6 +12,7 @@ import { join } from 'node:path'; import { ORCHESTRATOR_POSES } from './sprites/orchestrator-poses.ts'; import { ORCHESTRATOR_PALETTE, SUBAGENT_PALETTE } from './sprites/palettes.ts'; +import { renderPreviewHtml } from './sprites/preview-renderer.ts'; import { SUBAGENT_POSES } from './sprites/subagent-poses.ts'; import { renderSpriteSheet } from './sprites/svg-renderer.ts'; @@ -25,4 +26,16 @@ const orchestratorSvg = renderSpriteSheet(ORCHESTRATOR_POSES, ORCHESTRATOR_PALET writeFileSync(join(outDir, 'subagent.svg'), subagentSvg); writeFileSync(join(outDir, 'orchestrator.svg'), orchestratorSvg); +const previewHtml = renderPreviewHtml(subagentSvg, orchestratorSvg); +const previewDir = join(import.meta.dirname, '../../../.local/sprites'); +mkdirSync(previewDir, { recursive: true }); +const timestamp = new Date() + .toISOString() + .replace(/[-:]/g, '') + .replace('T', '-') + .replace(/\.\d+Z$/, 'Z'); +const previewPath = join(previewDir, `${timestamp}_sprite-preview.html`); +writeFileSync(previewPath, previewHtml); + console.info('Sprite sheets written to', outDir); +console.info('Preview written to', previewPath); diff --git a/packages/factory/scripts/sprites/__tests__/preview-renderer.test.ts b/packages/factory/scripts/sprites/__tests__/preview-renderer.test.ts new file mode 100644 index 00000000..984d06ec --- /dev/null +++ b/packages/factory/scripts/sprites/__tests__/preview-renderer.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, it } from 'vitest'; + +import { renderPreviewHtml } from '../preview-renderer.ts'; + +const STUB_SUBAGENT_SVG = 'subagent-stub'; +const STUB_ORCHESTRATOR_SVG = 'orchestrator-stub'; + +describe('renderPreviewHtml', () => { + const html = renderPreviewHtml(STUB_SUBAGENT_SVG, STUB_ORCHESTRATOR_SVG); + + // ── Document structure ────────────────────────────────────────────────── + + it('returns a string starting with ', () => { + expect(html).toMatch(/^/); + }); + + it('contains , and tags', () => { + expect(html).toContain(''); + expect(html).toContain(''); + }); + + // ── Styling ───────────────────────────────────────────────────────────── + + it('uses the midnight blue background color #1a1a2e', () => { + expect(html).toContain('#1a1a2e'); + }); + + // ── Section labels ────────────────────────────────────────────────────── + + it('contains a subagent section label', () => { + expect(html).toContain('subagent'); + }); + + it('contains an orchestrator section label', () => { + expect(html).toContain('orchestrator'); + }); + + // ── Frame labels ──────────────────────────────────────────────────────── + + const expectedLabels = [ + 'Idle 1', + 'Idle 2', + 'Walking', + 'Resting 1', + 'Working 1', + 'Working 2', + 'Working 3', + 'Resting 2', + 'Celebrating 1', + 'Celebrating 2', + 'Concerned', + 'Resting 3', + ]; + + for (const label of expectedLabels) { + it(`contains the frame label "${label}"`, () => { + expect(html).toContain(label); + }); + } + + // ── Animation script ─────────────────────────────────────────────────── + + it('contains a + +`; +} From 214dafefdfabc1d35603199093111f49c1cc9731 Mon Sep 17 00:00:00 2001 From: William Thorsen Date: Tue, 10 Mar 2026 00:40:21 -0700 Subject: [PATCH 2/3] factory|fix: Fix CSS vendor prefixes, test assertions, and JSDoc in preview renderer Replace invalid -webkit-image-rendering and -moz-image-rendering properties with the correct progressive-enhancement cascade (-moz-crisp-edges, crisp-edges, pixelated). Add JSDoc descriptions to renderFrameGridHtml and renderAnimationSectionsHtml. Tighten section-label test assertions to match

markup instead of vacuously matching base64 content. Remove unused placeholder background-image style from animation cell divs. --- .../scripts/sprites/__tests__/preview-renderer.test.ts | 4 ++-- packages/factory/scripts/sprites/preview-renderer.ts | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/factory/scripts/sprites/__tests__/preview-renderer.test.ts b/packages/factory/scripts/sprites/__tests__/preview-renderer.test.ts index 984d06ec..b6fd8f7e 100644 --- a/packages/factory/scripts/sprites/__tests__/preview-renderer.test.ts +++ b/packages/factory/scripts/sprites/__tests__/preview-renderer.test.ts @@ -29,11 +29,11 @@ describe('renderPreviewHtml', () => { // ── Section labels ────────────────────────────────────────────────────── it('contains a subagent section label', () => { - expect(html).toContain('subagent'); + expect(html).toContain('

subagent

'); }); it('contains an orchestrator section label', () => { - expect(html).toContain('orchestrator'); + expect(html).toContain('

orchestrator

'); }); // ── Frame labels ──────────────────────────────────────────────────────── diff --git a/packages/factory/scripts/sprites/preview-renderer.ts b/packages/factory/scripts/sprites/preview-renderer.ts index e873c762..79f6f593 100644 --- a/packages/factory/scripts/sprites/preview-renderer.ts +++ b/packages/factory/scripts/sprites/preview-renderer.ts @@ -19,6 +19,7 @@ const FRAME_LABELS = [ const ANIMATION_NAMES = ['Idle', 'Walking', 'Working', 'Celebrating', 'Concerned', 'Resting']; +/** Renders the 4x3 grid of individually labeled sprite frames as HTML. */ function renderFrameGridHtml(dataUri: string): string { return FRAME_LABELS.map((label, i) => { const col = i % 4; @@ -32,13 +33,14 @@ function renderFrameGridHtml(dataUri: string): string { }).join('\n'); } +/** Renders animation preview cells with element IDs for JS-driven playback. */ // Animation definitions duplicated from sprite-definitions.ts (cannot import due to excalibur dependency) // Strategies: PingPong = forward then backward, Loop = forward-only, Freeze = static function renderAnimationSectionsHtml(idPrefix: string): string { return ANIMATION_NAMES.map((name) => { const id = `${idPrefix}-${name.toLowerCase()}`; return `
-
+
${name}
`; }).join('\n'); @@ -90,10 +92,9 @@ export function renderPreviewHtml(subagentSvg: string, orchestratorSvg: string): height: 128px; background-repeat: no-repeat; background-size: ${4 * 128}px ${3 * 128}px; + image-rendering: -moz-crisp-edges; + image-rendering: crisp-edges; image-rendering: pixelated; - image-rendering: -webkit-optimize-contrast; - -webkit-image-rendering: crisp-edges; - -moz-image-rendering: crisp-edges; border: 1px solid #333; margin: 0 auto; } From 792720f8e5db955399f853a7b0b7c0be9e208275 Mon Sep 17 00:00:00 2001 From: William Thorsen Date: Tue, 10 Mar 2026 00:47:06 -0700 Subject: [PATCH 3/3] factory|refactor: Simplify preview renderer template and fix PingPong 2-frame stutter Replace array-join HTML construction with a single template literal, remove the PingPong 2-frame special case that caused a double-frame stutter at wrap point, and remove a misplaced duplication comment already present at the animations array. --- .../factory/scripts/sprites/preview-renderer.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/factory/scripts/sprites/preview-renderer.ts b/packages/factory/scripts/sprites/preview-renderer.ts index 79f6f593..c9027469 100644 --- a/packages/factory/scripts/sprites/preview-renderer.ts +++ b/packages/factory/scripts/sprites/preview-renderer.ts @@ -24,18 +24,14 @@ function renderFrameGridHtml(dataUri: string): string { return FRAME_LABELS.map((label, i) => { const col = i % 4; const row = Math.floor(i / 4); - return [ - `
`, - `
`, - `
${label}
`, - `
`, - ].join('\n'); + return `
+
+
${label}
+
`; }).join('\n'); } /** Renders animation preview cells with element IDs for JS-driven playback. */ -// Animation definitions duplicated from sprite-definitions.ts (cannot import due to excalibur dependency) -// Strategies: PingPong = forward then backward, Loop = forward-only, Freeze = static function renderAnimationSectionsHtml(idPrefix: string): string { return ANIMATION_NAMES.map((name) => { const id = `${idPrefix}-${name.toLowerCase()}`; @@ -178,8 +174,6 @@ ${renderAnimationSectionsHtml('orchestrator')} if (anim.strategy === PING_PONG && sequence.length > 2) { var reversed = sequence.slice(1, -1).reverse(); sequence = sequence.concat(reversed); - } else if (anim.strategy === PING_PONG && sequence.length === 2) { - sequence = sequence.concat(sequence.slice(0, 1)); } var idx = 0;