Skip to content

Commit 19abdfd

Browse files
committed
Add Sprite Creator basic drawing
1 parent 07f81af commit 19abdfd

7 files changed

Lines changed: 409 additions & 171 deletions

File tree

assets/theme-v2/css/gamefoundrystudio.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,10 @@ body.tool-focus-mode .tool-column:last-of-type {
11951195
opacity: 1
11961196
}
11971197

1198+
.sprite-canvas-cell.is-painted {
1199+
background: var(--text)
1200+
}
1201+
11981202
@media(max-width:980px) {
11991203

12001204
.grid.cols-4,

assets/toolbox/sprites/js/index.js

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
const DEFAULT_GRID_SIZE = 16;
22
const SUPPORTED_GRID_SIZES = Object.freeze([16, 32]);
3+
const DRAWING_TOOLS = Object.freeze(["pencil", "eraser", "fill"]);
4+
5+
const editorState = {
6+
activeTool: "pencil",
7+
gridSize: DEFAULT_GRID_SIZE,
8+
paintedPixels: new Set(),
9+
};
310

411
function gridLabel(size) {
512
return `Sprite Creator ${size} by ${size} pixel canvas`;
@@ -9,6 +16,67 @@ function buttonForSize(size) {
916
return document.querySelector(`[data-sprites-grid-size="${size}"]`);
1017
}
1118

19+
function pixelKey(row, column) {
20+
return `${row}:${column}`;
21+
}
22+
23+
function draftStatusText() {
24+
const count = editorState.paintedPixels.size;
25+
if (count === 0) {
26+
return "Unsaved editor state: empty draft.";
27+
}
28+
return `Unsaved editor state: ${count} draft pixel${count === 1 ? "" : "s"} painted.`;
29+
}
30+
31+
function updateDraftStatus() {
32+
const status = document.querySelector("[data-sprites-draft-status]");
33+
if (status) {
34+
status.textContent = draftStatusText();
35+
}
36+
}
37+
38+
function setActiveTool(toolName) {
39+
if (!DRAWING_TOOLS.includes(toolName)) {
40+
return;
41+
}
42+
editorState.activeTool = toolName;
43+
document.querySelectorAll("[data-sprite-tool-button]").forEach((button) => {
44+
const isActive = button.dataset.spriteToolButton === toolName;
45+
button.classList.toggle("primary", isActive);
46+
button.setAttribute("aria-pressed", String(isActive));
47+
});
48+
const status = document.querySelector("[data-sprites-tool-status]");
49+
if (status) {
50+
status.textContent = `${toolName[0].toUpperCase()}${toolName.slice(1)} is active. Drawing stays in unsaved editor state for this page session only.`;
51+
}
52+
}
53+
54+
function paintCell(cell) {
55+
const key = pixelKey(cell.dataset.spritePixelRow, cell.dataset.spritePixelColumn);
56+
if (editorState.activeTool === "eraser") {
57+
editorState.paintedPixels.delete(key);
58+
cell.classList.remove("is-painted");
59+
} else {
60+
editorState.paintedPixels.add(key);
61+
cell.classList.add("is-painted");
62+
}
63+
updateDraftStatus();
64+
}
65+
66+
function fillGrid() {
67+
const grid = document.querySelector("[data-sprites-pixel-grid]");
68+
if (!grid) {
69+
return;
70+
}
71+
editorState.paintedPixels.clear();
72+
grid.querySelectorAll("[data-sprite-pixel-row]").forEach((cell) => {
73+
const key = pixelKey(cell.dataset.spritePixelRow, cell.dataset.spritePixelColumn);
74+
editorState.paintedPixels.add(key);
75+
cell.classList.add("is-painted");
76+
});
77+
updateDraftStatus();
78+
}
79+
1280
function setGridSize(size) {
1381
const grid = document.querySelector("[data-sprites-pixel-grid]");
1482
const status = document.querySelector("[data-sprites-grid-status]");
@@ -17,6 +85,8 @@ function setGridSize(size) {
1785
}
1886

1987
grid.replaceChildren();
88+
editorState.gridSize = size;
89+
editorState.paintedPixels.clear();
2090
grid.dataset.spritesGridSize = String(size);
2191
grid.setAttribute("aria-label", gridLabel(size));
2292

@@ -26,11 +96,11 @@ function setGridSize(size) {
2696
const cell = document.createElement("button");
2797
cell.className = "sprite-canvas-cell";
2898
cell.type = "button";
29-
cell.disabled = true;
3099
cell.setAttribute("role", "gridcell");
31100
cell.setAttribute("aria-label", `Pixel row ${row}, column ${column}`);
32101
cell.dataset.spritePixelRow = String(row);
33102
cell.dataset.spritePixelColumn = String(column);
103+
cell.addEventListener("click", () => paintCell(cell));
34104
grid.append(cell);
35105
}
36106

@@ -43,6 +113,7 @@ function setGridSize(size) {
43113
if (status) {
44114
status.textContent = `Canvas display mode: ${size}x${size}. No pixel data is saved.`;
45115
}
116+
updateDraftStatus();
46117
}
47118

48119
function wireGridControls() {
@@ -55,5 +126,22 @@ function wireGridControls() {
55126
});
56127
}
57128

129+
function wireDrawingTools() {
130+
document.querySelectorAll("[data-sprite-tool-button]").forEach((button) => {
131+
const toolName = button.dataset.spriteToolButton;
132+
if (!DRAWING_TOOLS.includes(toolName) || button.disabled) {
133+
return;
134+
}
135+
button.addEventListener("click", () => {
136+
setActiveTool(toolName);
137+
if (toolName === "fill") {
138+
fillGrid();
139+
}
140+
});
141+
});
142+
}
143+
58144
wireGridControls();
145+
wireDrawingTools();
59146
setGridSize(DEFAULT_GRID_SIZE);
147+
setActiveTool(editorState.activeTool);

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@ const __filename = fileURLToPath(import.meta.url);
1010
const __dirname = path.dirname(__filename);
1111
const repoRoot = path.resolve(__dirname, "..", "..", "..", "..");
1212
const SPRITE_TOOLBAR_PLACEHOLDERS = [
13-
"Pencil",
14-
"Eraser",
15-
"Fill",
1613
"Line",
1714
"Rectangle",
1815
"Circle",
@@ -228,10 +225,13 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status
228225
await expect(page.getByRole("heading", { level: 2, name: "Pixel Work Area" })).toBeVisible();
229226
await expect(page.getByRole("heading", { level: 2, name: "Sprite Details" })).toBeVisible();
230227
await expect(page.locator("[data-sprites-toolbar]")).toBeVisible();
228+
await expect(page.getByRole("button", { name: "Pencil tool" })).toBeEnabled();
229+
await expect(page.getByRole("button", { name: "Eraser tool" })).toBeEnabled();
230+
await expect(page.getByRole("button", { name: "Fill tool" })).toBeEnabled();
231231
for (const toolName of SPRITE_TOOLBAR_PLACEHOLDERS) {
232232
await expect(page.getByRole("button", { name: `${toolName} tool placeholder` })).toBeDisabled();
233233
}
234-
await expect(page.locator("main")).toContainText("Toolbar placeholders only");
234+
await expect(page.locator("[data-sprites-tool-status]")).toContainText("Pencil is active");
235235
await expect(page.locator("[data-sprites-pixel-grid]")).toBeVisible();
236236
await expect(page.locator("[data-sprites-pixel-grid] [role='gridcell']")).toHaveCount(256);
237237
await expect(page.locator("[data-sprites-grid-status]")).toContainText("Canvas display mode: 16x16");
@@ -240,7 +240,18 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status
240240
await expect(page.locator("[data-sprites-pixel-grid] [role='gridcell']")).toHaveCount(1024);
241241
await expect(page.locator("[data-sprites-grid-status]")).toContainText("Canvas display mode: 32x32");
242242
await expect(page.getByRole("button", { name: "32x32" })).toHaveAttribute("aria-pressed", "true");
243-
await expect(page.locator("[data-sprites-shell-status]")).toContainText("Shell ready");
243+
const firstPixel = page.locator("[data-sprites-pixel-grid] [role='gridcell']").first();
244+
await firstPixel.click();
245+
await expect(firstPixel).toHaveClass(/is-painted/);
246+
await expect(page.locator("[data-sprites-draft-status]")).toContainText("1 draft pixel painted");
247+
await page.getByRole("button", { name: "Eraser tool" }).click();
248+
await firstPixel.click();
249+
await expect(firstPixel).not.toHaveClass(/is-painted/);
250+
await expect(page.locator("[data-sprites-draft-status]")).toContainText("empty draft");
251+
await page.getByRole("button", { name: "Fill tool" }).click();
252+
await expect(page.locator("[data-sprites-pixel-grid] .is-painted")).toHaveCount(1024);
253+
await expect(page.locator("[data-sprites-draft-status]")).toContainText("1024 draft pixels painted");
254+
await expect(page.locator("[data-sprites-shell-status]")).toContainText("Editor ready");
244255
await expect(page.locator("main")).toContainText("Palette/Colors keys only");
245256
await expect(page.locator("main")).not.toContainText(/Not implemented yet|future rebuild work|Static wireframe only|Plan sprite creation/i);
246257
await expect(page.locator("style, [style], script:not([src])")).toHaveCount(0);
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# PR_26179_CHARLIE_025-sprites-basic-drawing
2+
3+
Team: CHARLIE
4+
Workflow: stacked feature workflow
5+
Base branch: PR_26179_CHARLIE_024-sprites-canvas-grid
6+
Canonical ZIP path: dev/workspace/zip/PR_26179_CHARLIE_025-sprites-basic-drawing_delta.zip
7+
8+
## Summary
9+
10+
Implemented basic Sprite Creator drawing for Pencil, Eraser, and Fill on the visible pixel canvas. Drawing state is page-session editor state only and is clearly labeled as unsaved. No product save, browser storage, API, DB, schema, or publishing behavior was added.
11+
12+
## Branch Validation
13+
14+
PASS
15+
16+
- Current branch: PR_26179_CHARLIE_025-sprites-basic-drawing
17+
- Based on: PR_26179_CHARLIE_024-sprites-canvas-grid
18+
- No start_of_day files changed
19+
- No DB/API/schema files changed
20+
- No stale PR #219-#228 code copied
21+
22+
## Requirement Checklist
23+
24+
| Requirement | Status | Notes |
25+
| --- | --- | --- |
26+
| Implement Pencil | PASS | Clicking a pixel paints it in unsaved editor state. |
27+
| Implement Eraser | PASS | Clicking a painted pixel clears it. |
28+
| Implement Fill | PASS | Fill paints the current grid. |
29+
| Page-session editor state only | PASS | State lives in the module runtime only. |
30+
| Clearly marked unsaved | PASS | Status copy labels unsaved editor state. |
31+
| No product save | PASS | No save/load/publishing contract added. |
32+
| No browser-owned authoritative product data | PASS | No browser storage or persistence added. |
33+
| No DB/API/schema changes | PASS | Only UI/JS/CSS/test/report files changed. |
34+
35+
## Validation Lane Report
36+
37+
Commands:
38+
39+
```text
40+
node --check assets/toolbox/sprites/js/index.js
41+
node --check dev/tests/playwright/tools/SpritesToolShell.spec.mjs
42+
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
43+
rg --pcre2 -n -i "localStorage|sessionStorage|indexedDB|<style|style=|<script(?![^>]+src=)|on(click|change|submit|input|load|error)=|imageDataUrl|local-mem|fake-login|MEM DB" toolbox/sprites/index.html assets/toolbox/sprites/js/index.js dev/tests/playwright/tools/SpritesToolShell.spec.mjs
44+
npx playwright test dev/tests/playwright/tools/SpritesToolShell.spec.mjs --workers=1 --reporter=list --output=<temp>
45+
```
46+
47+
Results:
48+
49+
- Node syntax checks: PASS
50+
- `git diff --check`: PASS
51+
- Guard scan: PASS, no matches
52+
- Targeted Playwright: PASS, 1 test passed
53+
54+
## Manual Validation Notes
55+
56+
1. Open `/toolbox/sprites/index.html` from the stacked branch.
57+
2. Click a pixel with Pencil active and confirm it paints.
58+
3. Choose Eraser, click the painted pixel, and confirm it clears.
59+
4. Choose Fill and confirm the visible grid fills.
60+
5. Confirm copy says the state is unsaved editor state.
61+
62+
## ZIP Path
63+
64+
`dev/workspace/zip/PR_26179_CHARLIE_025-sprites-basic-drawing_delta.zip`

docs_build/dev/reports/codex_changed_files.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
assets/toolbox/sprites/js/index.js
33
assets/theme-v2/css/gamefoundrystudio.css
44
dev/tests/playwright/tools/SpritesToolShell.spec.mjs
5-
docs_build/dev/reports/PR_26179_CHARLIE_024-sprites-canvas-grid.md
5+
docs_build/dev/reports/PR_26179_CHARLIE_025-sprites-basic-drawing.md
66
docs_build/dev/reports/codex_changed_files.txt
77
docs_build/dev/reports/codex_review.diff

0 commit comments

Comments
 (0)