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
40 changes: 40 additions & 0 deletions assets/theme-v2/css/gamefoundrystudio.css
Original file line number Diff line number Diff line change
Expand Up @@ -1248,6 +1248,46 @@ body.tool-focus-mode .tool-column:last-of-type {
box-shadow: 0 1px 4px var(--swatch-shadow-color)
}

.sprite-frame-strip {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Put frame strip styles in the loaded theme bundle

The Sprite page only loads assets/theme-v2/css/theme.css, and that entrypoint does not import gamefoundrystudio.css (a repo-wide search also finds no active link/import for this file). Because the new .sprite-frame-* rules live only here, the added frame strip/card/thumbnail render with browser defaults instead of the intended Theme V2 strip layout. Please move these rules into an imported stylesheet or wire this stylesheet into the page/theme.

Useful? React with 👍 / 👎.

display: flex;
gap: 10px;
overflow-x: auto
}

.sprite-frame-card {
display: grid;
gap: 6px;
min-width: 96px;
padding: 8px;
border: 1px solid var(--line);
border-radius: var(--radius-sm);
background: var(--panel);
color: var(--text);
text-align: left
}

.sprite-frame-card.is-active {
border-color: var(--accent);
box-shadow: var(--shadow-soft)
}

.sprite-frame-thumbnail {
width: 48px;
height: 48px;
border: 1px solid var(--line);
background: var(--card-background);
image-rendering: pixelated
}

.sprite-frame-card-title {
font-weight: 700
}

.sprite-frame-card-meta {
color: var(--muted);
font-size: .875rem
}

.sprite-preview-shell {
display: flex;
justify-content: center;
Expand Down
33 changes: 26 additions & 7 deletions assets/toolbox/sprites/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ function updateDraftStatus() {
status.textContent = draftStatusText();
}
renderPreview();
renderFrameStrip();
}

function updatePaletteStatus() {
Expand Down Expand Up @@ -220,6 +221,7 @@ function setZoomLevel(zoomLevel) {
}
applyPaintedPixelsToGrid();
renderPreview();
renderFrameStrip();
}

function pickCellColor(cell) {
Expand All @@ -237,6 +239,7 @@ function pickCellColor(cell) {
}
applyPaintedPixelsToGrid();
renderPreview();
renderFrameStrip();
}

function setPixel(row, column, colorKey) {
Expand Down Expand Up @@ -414,19 +417,15 @@ function editorColorValue(colorKey) {
return value || "#111111";
}

function renderPreview() {
const canvas = document.querySelector("[data-sprites-preview-canvas]");
if (!canvas) {
return;
}
function renderPixelsToCanvas(canvas, paintedPixels, gridSize) {
const context = canvas.getContext("2d");
if (!context) {
return;
}
const size = editorState.gridSize;
const size = gridSize || DEFAULT_GRID_SIZE;
const cellSize = canvas.width / size;
context.clearRect(0, 0, canvas.width, canvas.height);
for (const [key, colorKey] of editorState.paintedPixels.entries()) {
for (const [key, colorKey] of paintedPixels.entries()) {
const [rowText, columnText] = key.split(":");
const row = Number(rowText);
const column = Number(columnText);
Expand All @@ -438,6 +437,25 @@ function renderPreview() {
}
}

function renderPreview() {
const canvas = document.querySelector("[data-sprites-preview-canvas]");
if (!canvas) {
return;
}
renderPixelsToCanvas(canvas, editorState.paintedPixels, editorState.gridSize);
}

function renderFrameStrip() {
const thumbnail = document.querySelector("[data-sprites-frame-thumbnail='0']");
const status = document.querySelector("[data-sprites-frame-status]");
if (thumbnail) {
renderPixelsToCanvas(thumbnail, editorState.paintedPixels, editorState.gridSize);
}
if (status) {
status.textContent = `Frame strip: 1 unsaved frame. Current editor draft has ${editorState.paintedPixels.size} painted pixel${editorState.paintedPixels.size === 1 ? "" : "s"}.`;
}
}

function exportPreviewPng() {
const canvas = document.querySelector("[data-sprites-preview-canvas]");
const status = document.querySelector("[data-sprites-export-status]");
Expand Down Expand Up @@ -609,3 +627,4 @@ setActiveColor(editorState.activeColor);
setZoomLevel(editorState.zoomLevel);
updateHistoryControls();
renderPreview();
renderFrameStrip();
20 changes: 20 additions & 0 deletions dev/tests/playwright/tools/SpritesToolShell.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,22 @@ async function previewCellHasPaint(page, row, column) {
}, { column, gridSize, row });
}

async function canvasHasAnyPaint(locator) {
return locator.evaluate((canvas) => {
const context = canvas.getContext("2d");
if (!context) {
return false;
}
const imageData = context.getImageData(0, 0, canvas.width, canvas.height).data;
for (let index = 3; index < imageData.length; index += 4) {
if (imageData[index] > 0) {
return true;
}
}
return false;
});
}

async function expectCenterAndPreviewPainted(page, row, column, colorClass = null) {
const cell = spriteCell(page, row, column);
await expect(cell).toHaveClass(/is-painted/);
Expand Down Expand Up @@ -253,6 +269,8 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status
await expect(page.getByText("Canvas Setup", { exact: true })).toBeVisible();
await expect(page.getByRole("heading", { level: 2, name: "Pixel Work Area" })).toBeVisible();
await expect(page.getByRole("heading", { level: 2, name: "Sprite Details" })).toBeVisible();
await expect(page.locator("[data-sprites-frame-strip]")).toBeVisible();
await expect(page.locator("[data-sprites-frame-status]")).toContainText("1 unsaved frame");
await expect(page.locator("[data-sprites-toolbar]")).toBeVisible();
await expect(page.getByRole("button", { name: "Pencil tool" })).toBeEnabled();
await expect(page.getByRole("button", { name: "Eraser tool" })).toBeEnabled();
Expand Down Expand Up @@ -408,6 +426,8 @@ test("Sprite Creator keeps center canvas and right preview in sync", async ({ pa

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

await page.getByRole("button", { name: "Eraser tool" }).click();
await spriteCell(page, 1, 1).click();
Expand Down
62 changes: 62 additions & 0 deletions docs_build/dev/reports/PR_26179_CHARLIE_034-sprites-frame-strip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# PR_26179_CHARLIE_034-sprites-frame-strip

Team: CHARLIE

Mode: Batch governance stacked feature workflow

Base branch: PR_26179_CHARLIE_033-sprites-canvas-preview-sync

Branch: PR_26179_CHARLIE_034-sprites-frame-strip

## Summary

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.

## Branch Validation

PASS

- Built from `PR_26179_CHARLIE_033-sprites-canvas-preview-sync`.
- Scope stayed limited to frame strip UI and synced thumbnail rendering.
- No DB/API/schema changes.
- No product persistence or browser-owned authoritative product data added.
- No start_of_day files changed.
- ZIP package created at the requested path.

## Requirement Checklist

PASS - Added visible Sprite Creator frame strip.

PASS - Frame strip shows the current unsaved frame.

PASS - Frame thumbnail renders from page-session editor state.

PASS - No frame editing behavior added in this slice.

PASS - Theme V2 CSS used.

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 assets/theme-v2/css/gamefoundrystudio.css 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. Confirm the Animation panel shows a Frame 1 strip card.
3. Draw a pixel on the center canvas.
4. Confirm Frame 1 status updates with painted pixel count.
5. Confirm the Frame 1 thumbnail shows the current unsaved draft.

## ZIP

`dev/workspace/zip/PR_26179_CHARLIE_034-sprites-frame-strip_delta.zip`
4 changes: 3 additions & 1 deletion docs_build/dev/reports/codex_changed_files.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
assets/theme-v2/css/gamefoundrystudio.css
assets/toolbox/sprites/js/index.js
dev/tests/playwright/tools/SpritesToolShell.spec.mjs
docs_build/dev/reports/PR_26179_CHARLIE_033-sprites-canvas-preview-sync.md
docs_build/dev/reports/PR_26179_CHARLIE_034-sprites-frame-strip.md
docs_build/dev/reports/codex_changed_files.txt
docs_build/dev/reports/codex_review.diff
toolbox/sprites/index.html
Loading
Loading