Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 56 additions & 2 deletions assets/toolbox/sprites/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -457,8 +457,12 @@ function renderPixelsToCanvas(canvas, paintedPixels, gridSize) {
return;
}
const size = gridSize || DEFAULT_GRID_SIZE;
const cellSize = canvas.width / size;
context.clearRect(0, 0, canvas.width, canvas.height);
drawPixelsToContext(context, paintedPixels, size, 0, 0, canvas.width);
}

function drawPixelsToContext(context, paintedPixels, gridSize, offsetX, offsetY, outputSize) {
const cellSize = outputSize / gridSize;
for (const [key, colorKey] of paintedPixels.entries()) {
const [rowText, columnText] = key.split(":");
const row = Number(rowText);
Expand All @@ -467,7 +471,7 @@ function renderPixelsToCanvas(canvas, paintedPixels, gridSize) {
continue;
}
context.fillStyle = editorColorValue(colorKey);
context.fillRect((column - 1) * cellSize, (row - 1) * cellSize, cellSize, cellSize);
context.fillRect(offsetX + (column - 1) * cellSize, offsetY + (row - 1) * cellSize, cellSize, cellSize);
}
}

Expand Down Expand Up @@ -647,6 +651,52 @@ function exportPreviewPng() {
}, "image/png");
}

function exportAnimationStripPng() {
const status = document.querySelector("[data-sprites-export-status]");
if (editorState.frames.length < 2) {
if (status) {
status.textContent = "Animation strip export needs at least two unsaved frames.";
}
return;
}
stopAnimationPreview("Animation preview stopped before export.");
const frameSize = 160;
const canvas = document.createElement("canvas");
canvas.width = frameSize * editorState.frames.length;
canvas.height = frameSize;
const context = canvas.getContext("2d");
if (!context) {
if (status) {
status.textContent = "Animation strip export is unavailable in this browser session.";
}
return;
}
context.clearRect(0, 0, canvas.width, canvas.height);
editorState.frames.forEach((frame, index) => {
drawPixelsToContext(context, frame.paintedPixels, editorState.gridSize, index * frameSize, 0, frameSize);
});
canvas.toBlob((blob) => {
if (!blob) {
if (status) {
status.textContent = "Animation strip export is unavailable in this browser session.";
}
return;
}
const objectUrl = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = objectUrl;
link.download = "sprite-creator-animation-strip.png";
link.rel = "noopener";
document.body.append(link);
link.click();
link.remove();
URL.revokeObjectURL(objectUrl);
if (status) {
status.textContent = `Animation strip PNG downloaded from ${editorState.frames.length} unsaved frames.`;
}
}, "image/png");
}

function setGridSize(size, options = {}) {
const grid = document.querySelector("[data-sprites-pixel-grid]");
const status = document.querySelector("[data-sprites-grid-status]");
Expand Down Expand Up @@ -807,6 +857,10 @@ function wireExportButton() {
if (button) {
button.addEventListener("click", exportPreviewPng);
}
const animationButton = document.querySelector("[data-sprites-export-animation]");
if (animationButton) {
animationButton.addEventListener("click", exportAnimationStripPng);
}
}

wireGridControls();
Expand Down
5 changes: 5 additions & 0 deletions dev/tests/playwright/tools/SpritesToolShell.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,11 @@ test("Sprite Creator previews unsaved animation frames", async ({ page }) => {
await page.getByRole("button", { name: "Stop Preview" }).click();
await expect(page.locator("[data-sprites-animation-status]")).toContainText("Selected Frame 2 is shown");
await expectCenterAndPreviewPainted(page, 1, 1, "sprite-canvas-cell--blue");
const stripDownloadPromise = page.waitForEvent("download");
await page.getByRole("button", { name: "Download Animation Strip" }).click();
const stripDownload = await stripDownloadPromise;
expect(stripDownload.suggestedFilename()).toBe("sprite-creator-animation-strip.png");
await expect(page.locator("[data-sprites-export-status]")).toContainText("Animation strip PNG downloaded from 2 unsaved frames");

expect(failures.failedRequests).toEqual([]);
expect(failures.pageErrors).toEqual([]);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# PR_26179_CHARLIE_037-sprites-animation-export

Team: CHARLIE

Mode: Batch governance stacked feature workflow

Base branch: PR_26179_CHARLIE_036-sprites-animation-preview

Branch: PR_26179_CHARLIE_037-sprites-animation-export

## Summary

Added local animation strip export for Sprite Creator. Download Animation Strip generates a PNG strip from unsaved page-session frames only. The export does not save to the sprite library, publish an asset, call an API, create database records, or introduce authoritative browser-owned product data.

## Branch Validation

PASS

- Built from `PR_26179_CHARLIE_036-sprites-animation-preview`.
- Scope stayed limited to unsaved animation export.
- No DB/API/schema changes.
- No saved product data or browser-owned authoritative product data added.
- No start_of_day files changed.
- ZIP package created at the requested path.

## Requirement Checklist

PASS - Animation strip export button added.

PASS - Export requires at least two unsaved frames.

PASS - Export creates a local PNG strip download.

PASS - Export stops animation preview before generating the strip.

PASS - No persistence, publishing, API, database, or storage behavior added.

PASS - Targeted Playwright coverage updated.

## Validation Lane

PASS - `node --check assets/toolbox/sprites/js/index.js`

PASS - `node --check dev/tests/playwright/tools/SpritesToolShell.spec.mjs`

PASS - `git diff --check -- toolbox/sprites/index.html assets/toolbox/sprites/js/index.js dev/tests/playwright/tools/SpritesToolShell.spec.mjs`

PASS - Runtime guard scan found no prohibited inline style/script/event-handler or stale placeholder text in touched runtime files.

PASS - `npx playwright test dev/tests/playwright/tools/SpritesToolShell.spec.mjs --workers=1 --reporter=list`

## Manual Validation Notes

1. Open `toolbox/sprites/index.html`.
2. Draw Frame 1.
3. Add Frame 2 and draw a different draft.
4. Click Download Animation Strip.
5. Confirm `sprite-creator-animation-strip.png` downloads.
6. Confirm the export status says the PNG came from unsaved frames.
7. Confirm no save, publish, API, database, or library workflow is introduced.

## ZIP

`dev/workspace/zip/PR_26179_CHARLIE_037-sprites-animation-export_delta.zip`
2 changes: 1 addition & 1 deletion docs_build/dev/reports/codex_changed_files.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
assets/toolbox/sprites/js/index.js
dev/tests/playwright/tools/SpritesToolShell.spec.mjs
docs_build/dev/reports/PR_26179_CHARLIE_036-sprites-animation-preview.md
docs_build/dev/reports/PR_26179_CHARLIE_037-sprites-animation-export.md
docs_build/dev/reports/codex_changed_files.txt
docs_build/dev/reports/codex_review.diff
toolbox/sprites/index.html
Loading
Loading