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
9 changes: 9 additions & 0 deletions assets/theme-v2/css/gamefoundrystudio.css
Original file line number Diff line number Diff line change
Expand Up @@ -1156,6 +1156,7 @@ body.tool-focus-mode .tool-column:last-of-type {
.sprite-canvas-shell {
display: flex;
justify-content: center;
overflow: auto;
padding: 16px;
border: 1px solid var(--line);
border-radius: var(--radius-md);
Expand All @@ -1180,6 +1181,14 @@ body.tool-focus-mode .tool-column:last-of-type {
--sprite-grid-size: 32
}

.sprite-canvas-shell[data-sprites-zoom-level="2"] .sprite-canvas-grid {
width: min(720px, 160%)

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 Match canvas zoom to the selected percentage

When the center panel is wide enough for the normal grid to reach its 520px cap, the 200% control only grows the grid to 720px (about 138%), and the adjacent 400% rule caps at 960px (about 185%), even though the UI/status reports 200% and 400%. This makes the new zoom controls visibly under-scale the canvas in the common desktop layout; tie these widths to the base canvas size or use a real scale so the displayed zoom matches the selected percentage.

Useful? React with 👍 / 👎.

}

.sprite-canvas-shell[data-sprites-zoom-level="4"] .sprite-canvas-grid {
width: min(960px, 220%)
}

.sprite-canvas-cell {
min-width: 0;
min-height: 0;
Expand Down
71 changes: 67 additions & 4 deletions assets/toolbox/sprites/js/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const DEFAULT_GRID_SIZE = 16;
const SUPPORTED_GRID_SIZES = Object.freeze([16, 32]);
const DRAWING_TOOLS = Object.freeze(["pencil", "eraser", "fill"]);
const SUPPORTED_ZOOM_LEVELS = Object.freeze([1, 2, 4]);
const EDITOR_TOOLS = Object.freeze(["pencil", "eraser", "fill", "picker", "zoom"]);
const EDITOR_COLOR_KEYS = Object.freeze(["ink", "orange", "gold", "green", "blue"]);
const EDITOR_COLOR_CSS_VARIABLES = Object.freeze({
blue: "--electric-blue",
Expand All @@ -15,6 +16,7 @@ const editorState = {
activeColor: "ink",
gridSize: DEFAULT_GRID_SIZE,
paintedPixels: new Map(),
zoomLevel: 1,
};
const editorHistory = {
redoStack: [],
Expand Down Expand Up @@ -88,6 +90,16 @@ function normalizeColorKey(colorKey) {
return EDITOR_COLOR_KEYS.includes(colorKey) ? colorKey : "ink";
}

function colorLabel(colorKey) {
const normalizedColorKey = normalizeColorKey(colorKey);
return `${normalizedColorKey[0].toUpperCase()}${normalizedColorKey.slice(1)}`;
}

function normalizeZoomLevel(zoomLevel) {
const value = Number(zoomLevel);
return SUPPORTED_ZOOM_LEVELS.includes(value) ? value : 1;
}

function clearCellColor(cell) {
cell.classList.remove("is-painted");
cell.classList.remove(...EDITOR_COLOR_KEYS.map((colorKey) => `sprite-canvas-cell--${colorKey}`));
Expand Down Expand Up @@ -136,7 +148,7 @@ function updateDraftStatus() {
function updatePaletteStatus() {
const status = document.querySelector("[data-sprites-palette-status]");
if (status) {
status.textContent = `Active editor color: ${editorState.activeColor[0].toUpperCase()}${editorState.activeColor.slice(1)}. Palette/Colors remains the reusable color source for future saved sprite records.`;
status.textContent = `Active editor color: ${colorLabel(editorState.activeColor)}. Palette/Colors remains the reusable color source for future saved sprite records.`;
}
}

Expand All @@ -152,7 +164,7 @@ function setActiveColor(colorKey) {
}

function setActiveTool(toolName) {
if (!DRAWING_TOOLS.includes(toolName)) {
if (!EDITOR_TOOLS.includes(toolName)) {
return;
}
editorState.activeTool = toolName;
Expand All @@ -167,8 +179,51 @@ function setActiveTool(toolName) {
}
}

function setZoomLevel(zoomLevel) {
const normalizedZoomLevel = normalizeZoomLevel(zoomLevel);
const shell = document.querySelector("[data-sprites-grid-shell]");
const status = document.querySelector("[data-sprites-zoom-status]");
editorState.zoomLevel = normalizedZoomLevel;

if (shell) {
shell.dataset.spritesZoomLevel = String(normalizedZoomLevel);
}

document.querySelectorAll("[data-sprites-zoom-level]").forEach((button) => {
const isActive = button.dataset.spritesZoomLevel === String(normalizedZoomLevel);
button.classList.toggle("primary", isActive);
button.setAttribute("aria-pressed", String(isActive));
});

if (status) {
status.textContent = `Canvas zoom display: ${normalizedZoomLevel * 100}%.`;
}
}

function pickCellColor(cell) {
const colorKey = cell.dataset.spriteColorKey;
const status = document.querySelector("[data-sprites-tool-status]");
if (!colorKey) {
if (status) {
status.textContent = "Picker found an empty pixel. Active color was not changed.";
}
return;
}
setActiveColor(colorKey);
if (status) {
status.textContent = `Picker selected ${colorLabel(colorKey)} from the unsaved editor canvas.`;
}
}

function paintCell(cell) {
const key = pixelKey(cell.dataset.spritePixelRow, cell.dataset.spritePixelColumn);
if (editorState.activeTool === "picker") {
pickCellColor(cell);
return;
}
if (editorState.activeTool === "zoom") {
return;
}
if (editorState.activeTool === "eraser") {
if (!editorState.paintedPixels.has(key)) {
return;
Expand Down Expand Up @@ -334,7 +389,7 @@ function wireGridControls() {
function wireDrawingTools() {
document.querySelectorAll("[data-sprite-tool-button]").forEach((button) => {
const toolName = button.dataset.spriteToolButton;
if (!DRAWING_TOOLS.includes(toolName) || button.disabled) {
if (!EDITOR_TOOLS.includes(toolName) || button.disabled) {
return;
}
button.addEventListener("click", () => {
Expand All @@ -346,6 +401,12 @@ function wireDrawingTools() {
});
}

function wireZoomControls() {
document.querySelectorAll("[data-sprites-zoom-level]").forEach((button) => {
button.addEventListener("click", () => setZoomLevel(button.dataset.spritesZoomLevel));
});
}

function wirePaletteButtons() {
document.querySelectorAll("[data-sprite-color-button]").forEach((button) => {
button.addEventListener("click", () => {
Expand Down Expand Up @@ -406,9 +467,11 @@ wireDrawingTools();
wirePaletteButtons();
wireCanvasActions();
wireHistoryActions();
wireZoomControls();
wireExportButton();
setGridSize(DEFAULT_GRID_SIZE);
setActiveTool(editorState.activeTool);
setActiveColor(editorState.activeColor);
setZoomLevel(editorState.zoomLevel);
updateHistoryControls();
renderPreview();
19 changes: 17 additions & 2 deletions dev/tests/playwright/tools/SpritesToolShell.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ const SPRITE_TOOLBAR_PLACEHOLDERS = [
"Line",
"Rectangle",
"Circle",
"Picker",
"Move",
"Zoom",
];

function contentTypeForPath(filePath) {
Expand Down Expand Up @@ -230,6 +228,8 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status
await expect(page.getByRole("button", { name: "Pencil tool" })).toBeEnabled();
await expect(page.getByRole("button", { name: "Eraser tool" })).toBeEnabled();
await expect(page.getByRole("button", { name: "Fill tool" })).toBeEnabled();
await expect(page.getByRole("button", { name: "Picker tool" })).toBeEnabled();
await expect(page.getByRole("button", { name: "Zoom tool" })).toBeEnabled();
for (const toolName of SPRITE_TOOLBAR_PLACEHOLDERS) {
await expect(page.getByRole("button", { name: `${toolName} tool placeholder` })).toBeDisabled();
}
Expand All @@ -239,6 +239,7 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status
await expect(page.locator("[data-sprites-pixel-grid]")).toBeVisible();
await expect(page.locator("[data-sprites-pixel-grid] [role='gridcell']")).toHaveCount(256);
await expect(page.locator("[data-sprites-grid-status]")).toContainText("Canvas display mode: 16x16");
await expect(page.locator("[data-sprites-zoom-status]")).toContainText("100%");
await page.getByRole("button", { name: "32x32" }).click();
await expect(page.locator("[data-sprites-pixel-grid]")).toHaveAttribute("aria-label", "Sprite Creator 32 by 32 pixel canvas");
await expect(page.locator("[data-sprites-pixel-grid] [role='gridcell']")).toHaveCount(1024);
Expand Down Expand Up @@ -269,6 +270,11 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status
await firstPixel.click();
await expect(firstPixel).toHaveClass(/sprite-canvas-cell--gold/);
await page.getByRole("button", { name: "Blue editor color" }).click();
await page.getByRole("button", { name: "Picker tool" }).click();
await firstPixel.click();
await expect(page.locator("[data-sprites-palette-status]")).toContainText("Active editor color: Gold");
await expect(page.locator("[data-sprites-tool-status]")).toContainText("Picker selected Gold");
await page.getByRole("button", { name: "Blue editor color" }).click();
await page.getByRole("button", { name: "Fill tool" }).click();
await expect(page.locator("[data-sprites-pixel-grid] .is-painted")).toHaveCount(1024);
await expect(page.locator("[data-sprites-pixel-grid] .sprite-canvas-cell--blue")).toHaveCount(1024);
Expand Down Expand Up @@ -300,6 +306,15 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status
await page.getByRole("button", { name: "Blue editor color" }).click();
await page.getByRole("button", { name: "Fill tool" }).click();
await expect(page.locator("[data-sprites-pixel-grid] .is-painted")).toHaveCount(256);
await page.getByRole("button", { name: "Zoom tool" }).click();
await page.getByRole("button", { name: "200%" }).click();
await expect(page.locator("[data-sprites-grid-shell]")).toHaveAttribute("data-sprites-zoom-level", "2");
await expect(page.locator("[data-sprites-zoom-status]")).toContainText("200%");
await page.getByRole("button", { name: "400%" }).click();
await expect(page.locator("[data-sprites-grid-shell]")).toHaveAttribute("data-sprites-zoom-level", "4");
await expect(page.locator("[data-sprites-zoom-status]")).toContainText("400%");
await page.getByRole("button", { name: "100%" }).click();
await expect(page.locator("[data-sprites-grid-shell]")).toHaveAttribute("data-sprites-zoom-level", "1");
await expect(page.locator("[data-sprites-preview-canvas]")).toBeVisible();
const previewHasPaint = await page.locator("[data-sprites-preview-canvas]").evaluate((canvas) => {
const context = canvas.getContext("2d");
Expand Down
69 changes: 69 additions & 0 deletions docs_build/dev/reports/PR_26179_CHARLIE_031-sprites-picker-zoom.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# PR_26179_CHARLIE_031-sprites-picker-zoom

Team: CHARLIE

Mode: Batch governance stacked feature workflow

Base branch: PR_26179_CHARLIE_030-sprites-undo-redo

Branch: PR_26179_CHARLIE_031-sprites-picker-zoom

## Summary

Implemented Sprite Creator picker and zoom display controls. Picker reads an existing unsaved draft pixel color and updates the active editor color. Zoom changes only the canvas display scale through Theme V2 CSS and page-session UI state. No persistence, database, API, schema, browser-owned authoritative product data, or start_of_day files were changed.

## Branch Validation

PASS

- Built on the previous stacked Sprite branch.
- Scope stayed limited to Picker and Zoom display controls.
- Move remains disabled/deferred.
- No DB/API/schema changes.
- No product data persistence was added.
- No stale PR #219-#228 code was copied.
- ZIP package created at the user-requested batch path.

## Requirement Checklist

PASS - Picker implemented for existing painted pixels.

PASS - Picker updates the active editor color without mutating the canvas.

PASS - Zoom display controls added for 100%, 200%, and 400%.

PASS - Zoom is display-only and does not persist data.

PASS - Move remains disabled/deferred.

PASS - Theme V2 CSS used for zoom sizing.

PASS - No inline CSS, script blocks, style blocks, or inline event handlers 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 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. Paint a pixel with Gold.
3. Change the active editor color to Blue.
4. Select Picker and click the Gold pixel.
5. Confirm the active editor color returns to Gold and the canvas does not change.
6. Select Zoom and click 200%, 400%, and 100%.
7. Confirm the canvas display scale updates and no sprite data is saved.

## ZIP

`dev/workspace/zip/PR_26179_CHARLIE_031-sprites-picker-zoom_delta.zip`
3 changes: 2 additions & 1 deletion docs_build/dev/reports/codex_changed_files.txt
Original file line number Diff line number Diff line change
@@ -1,6 +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_030-sprites-undo-redo.md
docs_build/dev/reports/PR_26179_CHARLIE_031-sprites-picker-zoom.md
docs_build/dev/reports/codex_changed_files.txt
docs_build/dev/reports/codex_review.diff
toolbox/sprites/index.html
Loading
Loading