Skip to content

Commit cc4d22d

Browse files
authored
Merge pull request #267 from ToolboxAid/PR_26179_CHARLIE_031-sprites-picker-zoom
PR_26179_CHARLIE_031-sprites-picker-zoom
2 parents 8eb58d1 + c5803b0 commit cc4d22d

7 files changed

Lines changed: 412 additions & 293 deletions

File tree

assets/theme-v2/css/gamefoundrystudio.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,6 +1156,7 @@ body.tool-focus-mode .tool-column:last-of-type {
11561156
.sprite-canvas-shell {
11571157
display: flex;
11581158
justify-content: center;
1159+
overflow: auto;
11591160
padding: 16px;
11601161
border: 1px solid var(--line);
11611162
border-radius: var(--radius-md);
@@ -1180,6 +1181,14 @@ body.tool-focus-mode .tool-column:last-of-type {
11801181
--sprite-grid-size: 32
11811182
}
11821183

1184+
.sprite-canvas-shell[data-sprites-zoom-level="2"] .sprite-canvas-grid {
1185+
width: min(720px, 160%)
1186+
}
1187+
1188+
.sprite-canvas-shell[data-sprites-zoom-level="4"] .sprite-canvas-grid {
1189+
width: min(960px, 220%)
1190+
}
1191+
11831192
.sprite-canvas-cell {
11841193
min-width: 0;
11851194
min-height: 0;

assets/toolbox/sprites/js/index.js

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const DEFAULT_GRID_SIZE = 16;
22
const SUPPORTED_GRID_SIZES = Object.freeze([16, 32]);
3-
const DRAWING_TOOLS = Object.freeze(["pencil", "eraser", "fill"]);
3+
const SUPPORTED_ZOOM_LEVELS = Object.freeze([1, 2, 4]);
4+
const EDITOR_TOOLS = Object.freeze(["pencil", "eraser", "fill", "picker", "zoom"]);
45
const EDITOR_COLOR_KEYS = Object.freeze(["ink", "orange", "gold", "green", "blue"]);
56
const EDITOR_COLOR_CSS_VARIABLES = Object.freeze({
67
blue: "--electric-blue",
@@ -15,6 +16,7 @@ const editorState = {
1516
activeColor: "ink",
1617
gridSize: DEFAULT_GRID_SIZE,
1718
paintedPixels: new Map(),
19+
zoomLevel: 1,
1820
};
1921
const editorHistory = {
2022
redoStack: [],
@@ -88,6 +90,16 @@ function normalizeColorKey(colorKey) {
8890
return EDITOR_COLOR_KEYS.includes(colorKey) ? colorKey : "ink";
8991
}
9092

93+
function colorLabel(colorKey) {
94+
const normalizedColorKey = normalizeColorKey(colorKey);
95+
return `${normalizedColorKey[0].toUpperCase()}${normalizedColorKey.slice(1)}`;
96+
}
97+
98+
function normalizeZoomLevel(zoomLevel) {
99+
const value = Number(zoomLevel);
100+
return SUPPORTED_ZOOM_LEVELS.includes(value) ? value : 1;
101+
}
102+
91103
function clearCellColor(cell) {
92104
cell.classList.remove("is-painted");
93105
cell.classList.remove(...EDITOR_COLOR_KEYS.map((colorKey) => `sprite-canvas-cell--${colorKey}`));
@@ -136,7 +148,7 @@ function updateDraftStatus() {
136148
function updatePaletteStatus() {
137149
const status = document.querySelector("[data-sprites-palette-status]");
138150
if (status) {
139-
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.`;
151+
status.textContent = `Active editor color: ${colorLabel(editorState.activeColor)}. Palette/Colors remains the reusable color source for future saved sprite records.`;
140152
}
141153
}
142154

@@ -152,7 +164,7 @@ function setActiveColor(colorKey) {
152164
}
153165

154166
function setActiveTool(toolName) {
155-
if (!DRAWING_TOOLS.includes(toolName)) {
167+
if (!EDITOR_TOOLS.includes(toolName)) {
156168
return;
157169
}
158170
editorState.activeTool = toolName;
@@ -167,8 +179,51 @@ function setActiveTool(toolName) {
167179
}
168180
}
169181

182+
function setZoomLevel(zoomLevel) {
183+
const normalizedZoomLevel = normalizeZoomLevel(zoomLevel);
184+
const shell = document.querySelector("[data-sprites-grid-shell]");
185+
const status = document.querySelector("[data-sprites-zoom-status]");
186+
editorState.zoomLevel = normalizedZoomLevel;
187+
188+
if (shell) {
189+
shell.dataset.spritesZoomLevel = String(normalizedZoomLevel);
190+
}
191+
192+
document.querySelectorAll("[data-sprites-zoom-level]").forEach((button) => {
193+
const isActive = button.dataset.spritesZoomLevel === String(normalizedZoomLevel);
194+
button.classList.toggle("primary", isActive);
195+
button.setAttribute("aria-pressed", String(isActive));
196+
});
197+
198+
if (status) {
199+
status.textContent = `Canvas zoom display: ${normalizedZoomLevel * 100}%.`;
200+
}
201+
}
202+
203+
function pickCellColor(cell) {
204+
const colorKey = cell.dataset.spriteColorKey;
205+
const status = document.querySelector("[data-sprites-tool-status]");
206+
if (!colorKey) {
207+
if (status) {
208+
status.textContent = "Picker found an empty pixel. Active color was not changed.";
209+
}
210+
return;
211+
}
212+
setActiveColor(colorKey);
213+
if (status) {
214+
status.textContent = `Picker selected ${colorLabel(colorKey)} from the unsaved editor canvas.`;
215+
}
216+
}
217+
170218
function paintCell(cell) {
171219
const key = pixelKey(cell.dataset.spritePixelRow, cell.dataset.spritePixelColumn);
220+
if (editorState.activeTool === "picker") {
221+
pickCellColor(cell);
222+
return;
223+
}
224+
if (editorState.activeTool === "zoom") {
225+
return;
226+
}
172227
if (editorState.activeTool === "eraser") {
173228
if (!editorState.paintedPixels.has(key)) {
174229
return;
@@ -334,7 +389,7 @@ function wireGridControls() {
334389
function wireDrawingTools() {
335390
document.querySelectorAll("[data-sprite-tool-button]").forEach((button) => {
336391
const toolName = button.dataset.spriteToolButton;
337-
if (!DRAWING_TOOLS.includes(toolName) || button.disabled) {
392+
if (!EDITOR_TOOLS.includes(toolName) || button.disabled) {
338393
return;
339394
}
340395
button.addEventListener("click", () => {
@@ -346,6 +401,12 @@ function wireDrawingTools() {
346401
});
347402
}
348403

404+
function wireZoomControls() {
405+
document.querySelectorAll("[data-sprites-zoom-level]").forEach((button) => {
406+
button.addEventListener("click", () => setZoomLevel(button.dataset.spritesZoomLevel));
407+
});
408+
}
409+
349410
function wirePaletteButtons() {
350411
document.querySelectorAll("[data-sprite-color-button]").forEach((button) => {
351412
button.addEventListener("click", () => {
@@ -406,9 +467,11 @@ wireDrawingTools();
406467
wirePaletteButtons();
407468
wireCanvasActions();
408469
wireHistoryActions();
470+
wireZoomControls();
409471
wireExportButton();
410472
setGridSize(DEFAULT_GRID_SIZE);
411473
setActiveTool(editorState.activeTool);
412474
setActiveColor(editorState.activeColor);
475+
setZoomLevel(editorState.zoomLevel);
413476
updateHistoryControls();
414477
renderPreview();

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@ const SPRITE_TOOLBAR_PLACEHOLDERS = [
1313
"Line",
1414
"Rectangle",
1515
"Circle",
16-
"Picker",
1716
"Move",
18-
"Zoom",
1917
];
2018

2119
function contentTypeForPath(filePath) {
@@ -230,6 +228,8 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status
230228
await expect(page.getByRole("button", { name: "Pencil tool" })).toBeEnabled();
231229
await expect(page.getByRole("button", { name: "Eraser tool" })).toBeEnabled();
232230
await expect(page.getByRole("button", { name: "Fill tool" })).toBeEnabled();
231+
await expect(page.getByRole("button", { name: "Picker tool" })).toBeEnabled();
232+
await expect(page.getByRole("button", { name: "Zoom tool" })).toBeEnabled();
233233
for (const toolName of SPRITE_TOOLBAR_PLACEHOLDERS) {
234234
await expect(page.getByRole("button", { name: `${toolName} tool placeholder` })).toBeDisabled();
235235
}
@@ -239,6 +239,7 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status
239239
await expect(page.locator("[data-sprites-pixel-grid]")).toBeVisible();
240240
await expect(page.locator("[data-sprites-pixel-grid] [role='gridcell']")).toHaveCount(256);
241241
await expect(page.locator("[data-sprites-grid-status]")).toContainText("Canvas display mode: 16x16");
242+
await expect(page.locator("[data-sprites-zoom-status]")).toContainText("100%");
242243
await page.getByRole("button", { name: "32x32" }).click();
243244
await expect(page.locator("[data-sprites-pixel-grid]")).toHaveAttribute("aria-label", "Sprite Creator 32 by 32 pixel canvas");
244245
await expect(page.locator("[data-sprites-pixel-grid] [role='gridcell']")).toHaveCount(1024);
@@ -269,6 +270,11 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status
269270
await firstPixel.click();
270271
await expect(firstPixel).toHaveClass(/sprite-canvas-cell--gold/);
271272
await page.getByRole("button", { name: "Blue editor color" }).click();
273+
await page.getByRole("button", { name: "Picker tool" }).click();
274+
await firstPixel.click();
275+
await expect(page.locator("[data-sprites-palette-status]")).toContainText("Active editor color: Gold");
276+
await expect(page.locator("[data-sprites-tool-status]")).toContainText("Picker selected Gold");
277+
await page.getByRole("button", { name: "Blue editor color" }).click();
272278
await page.getByRole("button", { name: "Fill tool" }).click();
273279
await expect(page.locator("[data-sprites-pixel-grid] .is-painted")).toHaveCount(1024);
274280
await expect(page.locator("[data-sprites-pixel-grid] .sprite-canvas-cell--blue")).toHaveCount(1024);
@@ -300,6 +306,15 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status
300306
await page.getByRole("button", { name: "Blue editor color" }).click();
301307
await page.getByRole("button", { name: "Fill tool" }).click();
302308
await expect(page.locator("[data-sprites-pixel-grid] .is-painted")).toHaveCount(256);
309+
await page.getByRole("button", { name: "Zoom tool" }).click();
310+
await page.getByRole("button", { name: "200%" }).click();
311+
await expect(page.locator("[data-sprites-grid-shell]")).toHaveAttribute("data-sprites-zoom-level", "2");
312+
await expect(page.locator("[data-sprites-zoom-status]")).toContainText("200%");
313+
await page.getByRole("button", { name: "400%" }).click();
314+
await expect(page.locator("[data-sprites-grid-shell]")).toHaveAttribute("data-sprites-zoom-level", "4");
315+
await expect(page.locator("[data-sprites-zoom-status]")).toContainText("400%");
316+
await page.getByRole("button", { name: "100%" }).click();
317+
await expect(page.locator("[data-sprites-grid-shell]")).toHaveAttribute("data-sprites-zoom-level", "1");
303318
await expect(page.locator("[data-sprites-preview-canvas]")).toBeVisible();
304319
const previewHasPaint = await page.locator("[data-sprites-preview-canvas]").evaluate((canvas) => {
305320
const context = canvas.getContext("2d");
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# PR_26179_CHARLIE_031-sprites-picker-zoom
2+
3+
Team: CHARLIE
4+
5+
Mode: Batch governance stacked feature workflow
6+
7+
Base branch: PR_26179_CHARLIE_030-sprites-undo-redo
8+
9+
Branch: PR_26179_CHARLIE_031-sprites-picker-zoom
10+
11+
## Summary
12+
13+
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.
14+
15+
## Branch Validation
16+
17+
PASS
18+
19+
- Built on the previous stacked Sprite branch.
20+
- Scope stayed limited to Picker and Zoom display controls.
21+
- Move remains disabled/deferred.
22+
- No DB/API/schema changes.
23+
- No product data persistence was added.
24+
- No stale PR #219-#228 code was copied.
25+
- ZIP package created at the user-requested batch path.
26+
27+
## Requirement Checklist
28+
29+
PASS - Picker implemented for existing painted pixels.
30+
31+
PASS - Picker updates the active editor color without mutating the canvas.
32+
33+
PASS - Zoom display controls added for 100%, 200%, and 400%.
34+
35+
PASS - Zoom is display-only and does not persist data.
36+
37+
PASS - Move remains disabled/deferred.
38+
39+
PASS - Theme V2 CSS used for zoom sizing.
40+
41+
PASS - No inline CSS, script blocks, style blocks, or inline event handlers added.
42+
43+
PASS - Targeted Playwright coverage updated.
44+
45+
## Validation Lane
46+
47+
PASS - `node --check assets/toolbox/sprites/js/index.js`
48+
49+
PASS - `node --check dev/tests/playwright/tools/SpritesToolShell.spec.mjs`
50+
51+
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`
52+
53+
PASS - Runtime guard scan found no prohibited inline style/script/event-handler or stale placeholder text in touched runtime files.
54+
55+
PASS - `npx playwright test dev/tests/playwright/tools/SpritesToolShell.spec.mjs --workers=1 --reporter=list`
56+
57+
## Manual Validation Notes
58+
59+
1. Open `toolbox/sprites/index.html`.
60+
2. Paint a pixel with Gold.
61+
3. Change the active editor color to Blue.
62+
4. Select Picker and click the Gold pixel.
63+
5. Confirm the active editor color returns to Gold and the canvas does not change.
64+
6. Select Zoom and click 200%, 400%, and 100%.
65+
7. Confirm the canvas display scale updates and no sprite data is saved.
66+
67+
## ZIP
68+
69+
`dev/workspace/zip/PR_26179_CHARLIE_031-sprites-picker-zoom_delta.zip`
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +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_030-sprites-undo-redo.md
4+
docs_build/dev/reports/PR_26179_CHARLIE_031-sprites-picker-zoom.md
45
docs_build/dev/reports/codex_changed_files.txt
56
docs_build/dev/reports/codex_review.diff
67
toolbox/sprites/index.html

0 commit comments

Comments
 (0)