Skip to content

Commit afd46a0

Browse files
committed
Add Sprite Creator animation preview
1 parent eae8719 commit afd46a0

6 files changed

Lines changed: 326 additions & 241 deletions

File tree

assets/toolbox/sprites/js/index.js

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ const editorHistory = {
3636
redoStack: [],
3737
undoStack: [],
3838
};
39+
const animationPreview = {
40+
frameIndex: 0,
41+
timerId: null,
42+
};
3943

4044
function gridLabel(size) {
4145
return `Sprite Creator ${size} by ${size} pixel canvas`;
@@ -467,12 +471,12 @@ function renderPixelsToCanvas(canvas, paintedPixels, gridSize) {
467471
}
468472
}
469473

470-
function renderPreview() {
474+
function renderPreview(frame = currentFrame()) {
471475
const canvas = document.querySelector("[data-sprites-preview-canvas]");
472476
if (!canvas) {
473477
return;
474478
}
475-
renderPixelsToCanvas(canvas, editorState.paintedPixels, editorState.gridSize);
479+
renderPixelsToCanvas(canvas, frame.paintedPixels, editorState.gridSize);
476480
}
477481

478482
function renderFrameStrip() {
@@ -558,6 +562,63 @@ function deleteFrame() {
558562
updateHistoryControls();
559563
}
560564

565+
function updateAnimationControls(isPlaying) {
566+
const playButton = document.querySelector("[data-sprites-play-animation]");
567+
const stopButton = document.querySelector("[data-sprites-stop-animation]");
568+
if (playButton) {
569+
playButton.disabled = isPlaying;
570+
}
571+
if (stopButton) {
572+
stopButton.disabled = !isPlaying;
573+
}
574+
}
575+
576+
function animationStatusText(prefix, frameIndex = editorState.activeFrameIndex) {
577+
return `${prefix} Frame ${frameIndex + 1} of ${editorState.frames.length}. Unsaved preview only.`;
578+
}
579+
580+
function showAnimationFrame(frameIndex) {
581+
const normalizedIndex = frameIndex % editorState.frames.length;
582+
animationPreview.frameIndex = normalizedIndex;
583+
renderPreview(editorState.frames[normalizedIndex]);
584+
const status = document.querySelector("[data-sprites-animation-status]");
585+
if (status) {
586+
status.textContent = animationStatusText("Playing", normalizedIndex);
587+
}
588+
}
589+
590+
function stopAnimationPreview(message = "Animation preview stopped.") {
591+
if (animationPreview.timerId) {
592+
clearInterval(animationPreview.timerId);
593+
animationPreview.timerId = null;
594+
}
595+
renderPreview(currentFrame());
596+
updateAnimationControls(false);
597+
const status = document.querySelector("[data-sprites-animation-status]");
598+
if (status) {
599+
status.textContent = `${message} Selected Frame ${currentFrame().frameNumber} is shown.`;
600+
}
601+
}
602+
603+
function playAnimationPreview() {
604+
const status = document.querySelector("[data-sprites-animation-status]");
605+
if (editorState.frames.length < 2) {
606+
if (status) {
607+
status.textContent = "Animation preview needs at least two unsaved frames.";
608+
}
609+
return;
610+
}
611+
if (animationPreview.timerId) {
612+
clearInterval(animationPreview.timerId);
613+
}
614+
animationPreview.frameIndex = 0;
615+
updateAnimationControls(true);
616+
showAnimationFrame(animationPreview.frameIndex);
617+
animationPreview.timerId = setInterval(() => {
618+
showAnimationFrame((animationPreview.frameIndex + 1) % editorState.frames.length);
619+
}, DEFAULT_FRAME_DURATION_MS);
620+
}
621+
561622
function exportPreviewPng() {
562623
const canvas = document.querySelector("[data-sprites-preview-canvas]");
563624
const status = document.querySelector("[data-sprites-export-status]");
@@ -701,6 +762,18 @@ function wireFrameActions() {
701762
}
702763
}
703764

765+
function wireAnimationActions() {
766+
const playButton = document.querySelector("[data-sprites-play-animation]");
767+
if (playButton) {
768+
playButton.addEventListener("click", playAnimationPreview);
769+
}
770+
771+
const stopButton = document.querySelector("[data-sprites-stop-animation]");
772+
if (stopButton) {
773+
stopButton.addEventListener("click", () => stopAnimationPreview());
774+
}
775+
}
776+
704777
function wireHistoryActions() {
705778
const undoButton = document.querySelector("[data-sprites-undo]");
706779
if (undoButton) {
@@ -741,6 +814,7 @@ wireDrawingTools();
741814
wirePaletteButtons();
742815
wireCanvasActions();
743816
wireFrameActions();
817+
wireAnimationActions();
744818
wireHistoryActions();
745819
wireZoomControls();
746820
wireExportButton();
@@ -751,3 +825,4 @@ setZoomLevel(editorState.zoomLevel);
751825
updateHistoryControls();
752826
renderPreview();
753827
renderFrameStrip();
828+
updateAnimationControls(false);

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status
271271
await expect(page.getByRole("heading", { level: 2, name: "Sprite Details" })).toBeVisible();
272272
await expect(page.locator("[data-sprites-frame-strip]")).toBeVisible();
273273
await expect(page.locator("[data-sprites-frame-status]")).toContainText("1 unsaved frame");
274+
await expect(page.locator("[data-sprites-animation-status]")).toContainText("Animation preview is stopped");
274275
await expect(page.locator("[data-sprites-toolbar]")).toBeVisible();
275276
await expect(page.getByRole("button", { name: "Pencil tool" })).toBeEnabled();
276277
await expect(page.getByRole("button", { name: "Eraser tool" })).toBeEnabled();
@@ -535,3 +536,33 @@ test("Sprite Creator edits unsaved frame strip frames", async ({ page }) => {
535536
await server.close();
536537
}
537538
});
539+
540+
test("Sprite Creator previews unsaved animation frames", async ({ page }) => {
541+
const server = await startSpriteShellTestServer();
542+
const failures = collectPageFailures(page);
543+
544+
try {
545+
await page.goto(`${server.baseUrl}/toolbox/sprites/index.html`, { waitUntil: "networkidle" });
546+
547+
await page.getByRole("button", { name: "Gold editor color" }).click();
548+
await spriteCell(page, 1, 1).click();
549+
await page.getByRole("button", { name: "Add Frame" }).click();
550+
await page.getByRole("button", { name: "Blue editor color" }).click();
551+
await spriteCell(page, 1, 1).click();
552+
553+
await page.getByRole("button", { name: "Play Preview" }).click();
554+
await expect(page.getByRole("button", { name: "Stop Preview" })).toBeEnabled();
555+
await expect(page.locator("[data-sprites-animation-status]")).toContainText("Playing Frame 1");
556+
await expect(page.locator("[data-sprites-animation-status]")).toContainText("Playing Frame 2", { timeout: 1500 });
557+
558+
await page.getByRole("button", { name: "Stop Preview" }).click();
559+
await expect(page.locator("[data-sprites-animation-status]")).toContainText("Selected Frame 2 is shown");
560+
await expectCenterAndPreviewPainted(page, 1, 1, "sprite-canvas-cell--blue");
561+
562+
expect(failures.failedRequests).toEqual([]);
563+
expect(failures.pageErrors).toEqual([]);
564+
expect(failures.consoleErrors).toEqual([]);
565+
} finally {
566+
await server.close();
567+
}
568+
});
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# PR_26179_CHARLIE_036-sprites-animation-preview
2+
3+
Team: CHARLIE
4+
5+
Mode: Batch governance stacked feature workflow
6+
7+
Base branch: PR_26179_CHARLIE_035-sprites-frame-editing
8+
9+
Branch: PR_26179_CHARLIE_036-sprites-animation-preview
10+
11+
## Summary
12+
13+
Added unsaved animation preview playback for Sprite Creator. Play Preview cycles the right preview canvas through unsaved page-session frames, and Stop Preview returns the preview to the selected frame. No persistence, database, API, schema, save-to-library, or publishing behavior was introduced.
14+
15+
## Branch Validation
16+
17+
PASS
18+
19+
- Built from `PR_26179_CHARLIE_035-sprites-frame-editing`.
20+
- Scope stayed limited to unsaved animation preview.
21+
- No DB/API/schema changes.
22+
- No saved product data or browser-owned authoritative product data added.
23+
- No start_of_day files changed.
24+
- ZIP package created at the requested path.
25+
26+
## Requirement Checklist
27+
28+
PASS - Animation preview controls added.
29+
30+
PASS - Preview requires unsaved frame strip frames only.
31+
32+
PASS - Play cycles through unsaved frames in the right preview canvas.
33+
34+
PASS - Stop returns the preview to the selected frame.
35+
36+
PASS - No persistence added.
37+
38+
PASS - Targeted Playwright coverage updated.
39+
40+
## Validation Lane
41+
42+
PASS - `node --check assets/toolbox/sprites/js/index.js`
43+
44+
PASS - `node --check dev/tests/playwright/tools/SpritesToolShell.spec.mjs`
45+
46+
PASS - `git diff --check -- toolbox/sprites/index.html assets/toolbox/sprites/js/index.js dev/tests/playwright/tools/SpritesToolShell.spec.mjs`
47+
48+
PASS - Runtime guard scan found no prohibited inline style/script/event-handler or stale placeholder text in touched runtime files.
49+
50+
PASS - `npx playwright test dev/tests/playwright/tools/SpritesToolShell.spec.mjs --workers=1 --reporter=list`
51+
52+
## Manual Validation Notes
53+
54+
1. Open `toolbox/sprites/index.html`.
55+
2. Draw Frame 1.
56+
3. Add Frame 2 and draw a different draft.
57+
4. Click Play Preview and confirm the right preview cycles frames.
58+
5. Click Stop Preview and confirm the selected frame is shown.
59+
6. Confirm no save, publish, API, database, or library behavior is introduced.
60+
61+
## ZIP
62+
63+
`dev/workspace/zip/PR_26179_CHARLIE_036-sprites-animation-preview_delta.zip`
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
assets/toolbox/sprites/js/index.js
22
dev/tests/playwright/tools/SpritesToolShell.spec.mjs
3-
docs_build/dev/reports/PR_26179_CHARLIE_035-sprites-frame-editing.md
3+
docs_build/dev/reports/PR_26179_CHARLIE_036-sprites-animation-preview.md
44
docs_build/dev/reports/codex_changed_files.txt
55
docs_build/dev/reports/codex_review.diff
66
toolbox/sprites/index.html

0 commit comments

Comments
 (0)