Skip to content

Commit af37823

Browse files
authored
Merge pull request #263 from ToolboxAid/PR_26179_CHARLIE_027-sprites-preview-export
PR_26179_CHARLIE_027-sprites-preview-export
2 parents 674fc88 + 51b0225 commit af37823

7 files changed

Lines changed: 336 additions & 199 deletions

File tree

assets/theme-v2/css/gamefoundrystudio.css

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1239,6 +1239,24 @@ body.tool-focus-mode .tool-column:last-of-type {
12391239
box-shadow: 0 1px 4px var(--swatch-shadow-color)
12401240
}
12411241

1242+
.sprite-preview-shell {
1243+
display: flex;
1244+
justify-content: center;
1245+
padding: 12px;
1246+
border: 1px solid var(--line);
1247+
border-radius: var(--radius-md);
1248+
background: var(--panel-soft)
1249+
}
1250+
1251+
.sprite-preview-canvas {
1252+
width: min(100%, 160px);
1253+
height: auto;
1254+
aspect-ratio: 1;
1255+
border: 1px solid var(--line);
1256+
background: var(--card-background);
1257+
image-rendering: pixelated
1258+
}
1259+
12421260
@media(max-width:980px) {
12431261

12441262
.grid.cols-4,

assets/toolbox/sprites/js/index.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ const DEFAULT_GRID_SIZE = 16;
22
const SUPPORTED_GRID_SIZES = Object.freeze([16, 32]);
33
const DRAWING_TOOLS = Object.freeze(["pencil", "eraser", "fill"]);
44
const EDITOR_COLOR_KEYS = Object.freeze(["ink", "orange", "gold", "green", "blue"]);
5+
const EDITOR_COLOR_CSS_VARIABLES = Object.freeze({
6+
blue: "--electric-blue",
7+
gold: "--forge-gold",
8+
green: "--green",
9+
ink: "--text",
10+
orange: "--molten-orange",
11+
});
512

613
const editorState = {
714
activeTool: "pencil",
@@ -51,6 +58,7 @@ function updateDraftStatus() {
5158
if (status) {
5259
status.textContent = draftStatusText();
5360
}
61+
renderPreview();
5462
}
5563

5664
function updatePaletteStatus() {
@@ -114,6 +122,64 @@ function fillGrid() {
114122
updateDraftStatus();
115123
}
116124

125+
function editorColorValue(colorKey) {
126+
const variableName = EDITOR_COLOR_CSS_VARIABLES[normalizeColorKey(colorKey)];
127+
const value = getComputedStyle(document.documentElement).getPropertyValue(variableName).trim();
128+
return value || "#111111";
129+
}
130+
131+
function renderPreview() {
132+
const canvas = document.querySelector("[data-sprites-preview-canvas]");
133+
if (!canvas) {
134+
return;
135+
}
136+
const context = canvas.getContext("2d");
137+
if (!context) {
138+
return;
139+
}
140+
const size = editorState.gridSize;
141+
const cellSize = canvas.width / size;
142+
context.clearRect(0, 0, canvas.width, canvas.height);
143+
for (const [key, colorKey] of editorState.paintedPixels.entries()) {
144+
const [rowText, columnText] = key.split(":");
145+
const row = Number(rowText);
146+
const column = Number(columnText);
147+
if (!Number.isFinite(row) || !Number.isFinite(column)) {
148+
continue;
149+
}
150+
context.fillStyle = editorColorValue(colorKey);
151+
context.fillRect((column - 1) * cellSize, (row - 1) * cellSize, cellSize, cellSize);
152+
}
153+
}
154+
155+
function exportPreviewPng() {
156+
const canvas = document.querySelector("[data-sprites-preview-canvas]");
157+
const status = document.querySelector("[data-sprites-export-status]");
158+
if (!canvas) {
159+
return;
160+
}
161+
canvas.toBlob((blob) => {
162+
if (!blob) {
163+
if (status) {
164+
status.textContent = "PNG export is unavailable in this browser session.";
165+
}
166+
return;
167+
}
168+
const objectUrl = URL.createObjectURL(blob);
169+
const link = document.createElement("a");
170+
link.href = objectUrl;
171+
link.download = "sprite-creator-draft.png";
172+
link.rel = "noopener";
173+
document.body.append(link);
174+
link.click();
175+
link.remove();
176+
URL.revokeObjectURL(objectUrl);
177+
if (status) {
178+
status.textContent = "PNG downloaded from unsaved editor draft.";
179+
}
180+
}, "image/png");
181+
}
182+
117183
function setGridSize(size) {
118184
const grid = document.querySelector("[data-sprites-pixel-grid]");
119185
const status = document.querySelector("[data-sprites-grid-status]");
@@ -186,9 +252,18 @@ function wirePaletteButtons() {
186252
});
187253
}
188254

255+
function wireExportButton() {
256+
const button = document.querySelector("[data-sprites-export-png]");
257+
if (button) {
258+
button.addEventListener("click", exportPreviewPng);
259+
}
260+
}
261+
189262
wireGridControls();
190263
wireDrawingTools();
191264
wirePaletteButtons();
265+
wireExportButton();
192266
setGridSize(DEFAULT_GRID_SIZE);
193267
setActiveTool(editorState.activeTool);
194268
setActiveColor(editorState.activeColor);
269+
renderPreview();

dev/tests/playwright/tools/SpritesToolShell.spec.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,18 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status
260260
await expect(page.locator("[data-sprites-pixel-grid] .is-painted")).toHaveCount(1024);
261261
await expect(page.locator("[data-sprites-pixel-grid] .sprite-canvas-cell--blue")).toHaveCount(1024);
262262
await expect(page.locator("[data-sprites-draft-status]")).toContainText("1024 draft pixels painted");
263+
await expect(page.locator("[data-sprites-preview-canvas]")).toBeVisible();
264+
const previewHasPaint = await page.locator("[data-sprites-preview-canvas]").evaluate((canvas) => {
265+
const context = canvas.getContext("2d");
266+
const pixel = context.getImageData(1, 1, 1, 1).data;
267+
return pixel[3] > 0;
268+
});
269+
expect(previewHasPaint).toBe(true);
270+
const downloadPromise = page.waitForEvent("download");
271+
await page.getByRole("button", { name: "Download PNG" }).click();
272+
const download = await downloadPromise;
273+
expect(download.suggestedFilename()).toBe("sprite-creator-draft.png");
274+
await expect(page.locator("[data-sprites-export-status]")).toContainText("PNG downloaded");
263275
await expect(page.locator("[data-sprites-shell-status]")).toContainText("Editor ready");
264276
await expect(page.locator("main")).toContainText("Palette/Colors remains the reusable color source");
265277
await expect(page.locator("main")).not.toContainText(/Not implemented yet|future rebuild work|Static wireframe only|Plan sprite creation/i);
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# PR_26179_CHARLIE_027-sprites-preview-export
2+
3+
Team: CHARLIE
4+
Workflow: stacked feature workflow
5+
Base branch: PR_26179_CHARLIE_026-sprites-palette-panel
6+
Canonical ZIP path: dev/workspace/zip/PR_26179_CHARLIE_027-sprites-preview-export_delta.zip
7+
8+
## Summary
9+
10+
Added live preview and PNG download/export for the unsaved Sprite Creator editor draft. Export uses canvas `toBlob` and browser download behavior. This PR does not save to library, publish, write browser storage, or add API/DB/schema changes.
11+
12+
## Branch Validation
13+
14+
PASS
15+
16+
- Current branch: PR_26179_CHARLIE_027-sprites-preview-export
17+
- Based on: PR_26179_CHARLIE_026-sprites-palette-panel
18+
- No start_of_day files changed
19+
- No DB/API/schema files changed
20+
- No stale PR #219-#228 code copied
21+
22+
## Requirement Checklist
23+
24+
| Requirement | Status | Notes |
25+
| --- | --- | --- |
26+
| Add live preview | PASS | Preview canvas reflects unsaved draft pixels. |
27+
| Add PNG export/download | PASS | Download button exports `sprite-creator-draft.png`. |
28+
| No save-to-library | PASS | No saved sprite library or product persistence added. |
29+
| No publishing | PASS | Export is local download only. |
30+
| Avoid persisted data URLs | PASS | Uses `canvas.toBlob`; no `imageDataUrl` or `toDataURL`. |
31+
| No DB/API/schema changes | PASS | Only UI/JS/CSS/test/report files changed. |
32+
33+
## Validation Lane Report
34+
35+
Commands:
36+
37+
```text
38+
node --check assets/toolbox/sprites/js/index.js
39+
node --check dev/tests/playwright/tools/SpritesToolShell.spec.mjs
40+
git diff --check -- toolbox/sprites/index.html assets/toolbox/sprites/js/index.js assets/theme-v2/css/gamefoundrystudio.css dev/tests/playwright/tools/SpritesToolShell.spec.mjs
41+
rg --pcre2 -n -i "localStorage|sessionStorage|indexedDB|imageDataUrl|toDataURL|<style|style=|<script(?![^>]+src=)|on(click|change|submit|input|load|error)=|local-mem|fake-login|MEM DB" toolbox/sprites/index.html assets/toolbox/sprites/js/index.js dev/tests/playwright/tools/SpritesToolShell.spec.mjs
42+
npx playwright test dev/tests/playwright/tools/SpritesToolShell.spec.mjs --workers=1 --reporter=list --output=<temp>
43+
```
44+
45+
Results:
46+
47+
- Node syntax checks: PASS
48+
- `git diff --check`: PASS
49+
- Guard scan: PASS, no matches
50+
- Targeted Playwright: PASS, 1 test passed
51+
52+
## Manual Validation Notes
53+
54+
1. Open `/toolbox/sprites/index.html` from the stacked branch.
55+
2. Draw or fill pixels.
56+
3. Confirm the preview canvas shows the draft.
57+
4. Click Download PNG and confirm a `sprite-creator-draft.png` download starts.
58+
5. Confirm no save-to-library or publishing controls are present.
59+
60+
## ZIP Path
61+
62+
`dev/workspace/zip/PR_26179_CHARLIE_027-sprites-preview-export_delta.zip`

docs_build/dev/reports/codex_changed_files.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
assets/toolbox/sprites/js/index.js
33
assets/theme-v2/css/gamefoundrystudio.css
44
dev/tests/playwright/tools/SpritesToolShell.spec.mjs
5-
docs_build/dev/reports/PR_26179_CHARLIE_026-sprites-palette-panel.md
5+
docs_build/dev/reports/PR_26179_CHARLIE_027-sprites-preview-export.md
66
docs_build/dev/reports/codex_changed_files.txt
77
docs_build/dev/reports/codex_review.diff

0 commit comments

Comments
 (0)