Skip to content

Commit ce49ce7

Browse files
authored
Merge pull request #271 from ToolboxAid/PR_26179_CHARLIE_034-sprites-frame-strip
PR_26179_CHARLIE_034-sprites-frame-strip
2 parents 20700c9 + 67df8ee commit ce49ce7

7 files changed

Lines changed: 375 additions & 184 deletions

File tree

assets/theme-v2/css/gamefoundrystudio.css

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1248,6 +1248,46 @@ body.tool-focus-mode .tool-column:last-of-type {
12481248
box-shadow: 0 1px 4px var(--swatch-shadow-color)
12491249
}
12501250

1251+
.sprite-frame-strip {
1252+
display: flex;
1253+
gap: 10px;
1254+
overflow-x: auto
1255+
}
1256+
1257+
.sprite-frame-card {
1258+
display: grid;
1259+
gap: 6px;
1260+
min-width: 96px;
1261+
padding: 8px;
1262+
border: 1px solid var(--line);
1263+
border-radius: var(--radius-sm);
1264+
background: var(--panel);
1265+
color: var(--text);
1266+
text-align: left
1267+
}
1268+
1269+
.sprite-frame-card.is-active {
1270+
border-color: var(--accent);
1271+
box-shadow: var(--shadow-soft)
1272+
}
1273+
1274+
.sprite-frame-thumbnail {
1275+
width: 48px;
1276+
height: 48px;
1277+
border: 1px solid var(--line);
1278+
background: var(--card-background);
1279+
image-rendering: pixelated
1280+
}
1281+
1282+
.sprite-frame-card-title {
1283+
font-weight: 700
1284+
}
1285+
1286+
.sprite-frame-card-meta {
1287+
color: var(--muted);
1288+
font-size: .875rem
1289+
}
1290+
12511291
.sprite-preview-shell {
12521292
display: flex;
12531293
justify-content: center;

assets/toolbox/sprites/js/index.js

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ function updateDraftStatus() {
158158
status.textContent = draftStatusText();
159159
}
160160
renderPreview();
161+
renderFrameStrip();
161162
}
162163

163164
function updatePaletteStatus() {
@@ -220,6 +221,7 @@ function setZoomLevel(zoomLevel) {
220221
}
221222
applyPaintedPixelsToGrid();
222223
renderPreview();
224+
renderFrameStrip();
223225
}
224226

225227
function pickCellColor(cell) {
@@ -237,6 +239,7 @@ function pickCellColor(cell) {
237239
}
238240
applyPaintedPixelsToGrid();
239241
renderPreview();
242+
renderFrameStrip();
240243
}
241244

242245
function setPixel(row, column, colorKey) {
@@ -414,19 +417,15 @@ function editorColorValue(colorKey) {
414417
return value || "#111111";
415418
}
416419

417-
function renderPreview() {
418-
const canvas = document.querySelector("[data-sprites-preview-canvas]");
419-
if (!canvas) {
420-
return;
421-
}
420+
function renderPixelsToCanvas(canvas, paintedPixels, gridSize) {
422421
const context = canvas.getContext("2d");
423422
if (!context) {
424423
return;
425424
}
426-
const size = editorState.gridSize;
425+
const size = gridSize || DEFAULT_GRID_SIZE;
427426
const cellSize = canvas.width / size;
428427
context.clearRect(0, 0, canvas.width, canvas.height);
429-
for (const [key, colorKey] of editorState.paintedPixels.entries()) {
428+
for (const [key, colorKey] of paintedPixels.entries()) {
430429
const [rowText, columnText] = key.split(":");
431430
const row = Number(rowText);
432431
const column = Number(columnText);
@@ -438,6 +437,25 @@ function renderPreview() {
438437
}
439438
}
440439

440+
function renderPreview() {
441+
const canvas = document.querySelector("[data-sprites-preview-canvas]");
442+
if (!canvas) {
443+
return;
444+
}
445+
renderPixelsToCanvas(canvas, editorState.paintedPixels, editorState.gridSize);
446+
}
447+
448+
function renderFrameStrip() {
449+
const thumbnail = document.querySelector("[data-sprites-frame-thumbnail='0']");
450+
const status = document.querySelector("[data-sprites-frame-status]");
451+
if (thumbnail) {
452+
renderPixelsToCanvas(thumbnail, editorState.paintedPixels, editorState.gridSize);
453+
}
454+
if (status) {
455+
status.textContent = `Frame strip: 1 unsaved frame. Current editor draft has ${editorState.paintedPixels.size} painted pixel${editorState.paintedPixels.size === 1 ? "" : "s"}.`;
456+
}
457+
}
458+
441459
function exportPreviewPng() {
442460
const canvas = document.querySelector("[data-sprites-preview-canvas]");
443461
const status = document.querySelector("[data-sprites-export-status]");
@@ -609,3 +627,4 @@ setActiveColor(editorState.activeColor);
609627
setZoomLevel(editorState.zoomLevel);
610628
updateHistoryControls();
611629
renderPreview();
630+
renderFrameStrip();

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,22 @@ async function previewCellHasPaint(page, row, column) {
219219
}, { column, gridSize, row });
220220
}
221221

222+
async function canvasHasAnyPaint(locator) {
223+
return locator.evaluate((canvas) => {
224+
const context = canvas.getContext("2d");
225+
if (!context) {
226+
return false;
227+
}
228+
const imageData = context.getImageData(0, 0, canvas.width, canvas.height).data;
229+
for (let index = 3; index < imageData.length; index += 4) {
230+
if (imageData[index] > 0) {
231+
return true;
232+
}
233+
}
234+
return false;
235+
});
236+
}
237+
222238
async function expectCenterAndPreviewPainted(page, row, column, colorClass = null) {
223239
const cell = spriteCell(page, row, column);
224240
await expect(cell).toHaveClass(/is-painted/);
@@ -253,6 +269,8 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status
253269
await expect(page.getByText("Canvas Setup", { exact: true })).toBeVisible();
254270
await expect(page.getByRole("heading", { level: 2, name: "Pixel Work Area" })).toBeVisible();
255271
await expect(page.getByRole("heading", { level: 2, name: "Sprite Details" })).toBeVisible();
272+
await expect(page.locator("[data-sprites-frame-strip]")).toBeVisible();
273+
await expect(page.locator("[data-sprites-frame-status]")).toContainText("1 unsaved frame");
256274
await expect(page.locator("[data-sprites-toolbar]")).toBeVisible();
257275
await expect(page.getByRole("button", { name: "Pencil tool" })).toBeEnabled();
258276
await expect(page.getByRole("button", { name: "Eraser tool" })).toBeEnabled();
@@ -408,6 +426,8 @@ test("Sprite Creator keeps center canvas and right preview in sync", async ({ pa
408426

409427
await spriteCell(page, 1, 1).click();
410428
await expectCenterAndPreviewPainted(page, 1, 1, "sprite-canvas-cell--ink");
429+
await expect(page.locator("[data-sprites-frame-status]")).toContainText("1 painted pixel");
430+
expect(await canvasHasAnyPaint(page.locator("[data-sprites-frame-thumbnail='0']"))).toBe(true);
411431

412432
await page.getByRole("button", { name: "Eraser tool" }).click();
413433
await spriteCell(page, 1, 1).click();
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# PR_26179_CHARLIE_034-sprites-frame-strip
2+
3+
Team: CHARLIE
4+
5+
Mode: Batch governance stacked feature workflow
6+
7+
Base branch: PR_26179_CHARLIE_033-sprites-canvas-preview-sync
8+
9+
Branch: PR_26179_CHARLIE_034-sprites-frame-strip
10+
11+
## Summary
12+
13+
Added a Theme V2-compliant Sprite Creator frame strip for the current unsaved editor frame. The Frame 1 thumbnail mirrors the same page-session editor state as the center canvas and right preview. Frame editing remains deferred to the next scoped PR.
14+
15+
## Branch Validation
16+
17+
PASS
18+
19+
- Built from `PR_26179_CHARLIE_033-sprites-canvas-preview-sync`.
20+
- Scope stayed limited to frame strip UI and synced thumbnail rendering.
21+
- No DB/API/schema changes.
22+
- No product persistence 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 - Added visible Sprite Creator frame strip.
29+
30+
PASS - Frame strip shows the current unsaved frame.
31+
32+
PASS - Frame thumbnail renders from page-session editor state.
33+
34+
PASS - No frame editing behavior added in this slice.
35+
36+
PASS - Theme V2 CSS used.
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 assets/theme-v2/css/gamefoundrystudio.css 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. Confirm the Animation panel shows a Frame 1 strip card.
56+
3. Draw a pixel on the center canvas.
57+
4. Confirm Frame 1 status updates with painted pixel count.
58+
5. Confirm the Frame 1 thumbnail shows the current unsaved draft.
59+
60+
## ZIP
61+
62+
`dev/workspace/zip/PR_26179_CHARLIE_034-sprites-frame-strip_delta.zip`
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
assets/theme-v2/css/gamefoundrystudio.css
12
assets/toolbox/sprites/js/index.js
23
dev/tests/playwright/tools/SpritesToolShell.spec.mjs
3-
docs_build/dev/reports/PR_26179_CHARLIE_033-sprites-canvas-preview-sync.md
4+
docs_build/dev/reports/PR_26179_CHARLIE_034-sprites-frame-strip.md
45
docs_build/dev/reports/codex_changed_files.txt
56
docs_build/dev/reports/codex_review.diff
7+
toolbox/sprites/index.html

0 commit comments

Comments
 (0)