Skip to content

Commit 8f35e64

Browse files
committed
Add simple sprite editor (#2)
Add simple editor
1 parent 017f31a commit 8f35e64

8 files changed

Lines changed: 1154 additions & 5 deletions

File tree

README.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ No install required — use it instantly in the browser via GitHub Pages, or run
2323
|:----------------------------------------------:|:----------------------------------------:|
2424
| ![BW icons view](docs/screenshot-bw-icons.png) | ![Fonts view](docs/screenshot-fonts.png) |
2525

26+
| Sprite Editor ||
27+
|:---------------------------------------------:|:---------------------------------------------:|
28+
| ![Sprite editor](docs/Screenshot-sprite-editor.png) ||
29+
2630
---
2731

2832
## Features
@@ -41,6 +45,7 @@ No install required — use it instantly in the browser via GitHub Pages, or run
4145
| **Multi-file** | Select or drop multiple `.h` files / a whole folder — prompted once to **Add** or **Replace**, then all files are loaded in batch |
4246
| **Export ZIP** | Original source files + per-sprite split `.h` + `.png` preview + root `assets.h` |
4347
| **Save PNG** | Per-sprite **PNG N×** (current zoom) and **PNG 1×** (native size) buttons on every sprite card |
48+
| **Sprite Editor** | Open a dedicated editor in a new tab, draw with Pen/Eraser/Bucket/Picker, and export clean sprite headers |
4449
| **Fonts** | Parse and preview Adafruit GFX font files — glyph charmap + live text preview with colour picker |
4550
| **Font subsetting** | Subset any loaded font to only the characters you need and download a ready-to-use `.h` file |
4651

@@ -122,14 +127,32 @@ Use the **BG** selector to choose how transparent pixels are displayed:
122127

123128
---
124129

130+
## Standalone Sprite Editor
131+
132+
Use **Open Sprite Editor** in the top toolbar, or click **Edit** on any sprite card/frame to open the editor in a new tab with sprite data preloaded.
133+
134+
The editor is designed for quick pixel-level touchups and header generation:
135+
136+
- **Tool bar above canvas** with Pen, Eraser, Bucket Fill, and Picker
137+
- **Checkerboard transparency background** aligned with the main app rendering (one checker cell per sprite pixel at active zoom)
138+
- **Transparent paint mode** to paint `0xFEFE` directly
139+
- **Smart parser** for pasted C arrays, including named constants, `TRANSPARENT`, and `_SIZE[2]` dimensions
140+
- **Export options**: Generate/copy/download `.h`, plus **Save PNG (zoom)** and **Save PNG (1x)**
141+
- **Header visibility logic**: generated header output stays hidden until generated and is auto-invalidated after edits
142+
143+
---
144+
125145
## Project Structure
126146

127147
```
128148
spriteanvil/
129149
├── web/
130150
│ ├── index.html # UI
131151
│ ├── app.js # Parser, renderer, animation, export
132-
│ └── styles.css # Dark-theme styles
152+
│ ├── styles.css # Main app styles
153+
│ ├── editor.html # Standalone sprite editor UI
154+
│ ├── editor.js # Standalone editor logic (draw, parse, export)
155+
│ └── editor.css # Standalone editor styles
133156
├── Dockerfile # nginx:alpine serving web/
134157
├── docker-compose.yml # Port 8080
135158
└── examples/

docs/Screenshot-sprite-editor.png

202 KB
Loading

web/app.js

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,21 @@ let fonts = [];
77
let currentTab = 'sprites';
88
const animState = new Map(); // baseName → intervalId
99

10+
function openEditorWithSprite(sprite, suggestedName = null) {
11+
const payload = {
12+
name: suggestedName || sprite.name,
13+
width: sprite.width,
14+
height: sprite.height,
15+
data: sprite.data
16+
};
17+
try {
18+
localStorage.setItem('spriteanvil.editorPayload', JSON.stringify(payload));
19+
} catch (err) {
20+
console.error('Failed to store sprite payload for editor', err);
21+
}
22+
globalThis.open('editor.html', '_blank', 'noopener');
23+
}
24+
1025
// ─── Tab switching ────────────────────────────────────────────────────────────
1126
function switchTab(name) {
1227
currentTab = name;
@@ -374,19 +389,20 @@ function renderSprites() {
374389
});
375390

376391
singles.forEach(s => {
377-
const { card, zoomBtn, nativeBtn, exportBtn } = mkCard(`${s.name} <span class="dim">${s.width}&times;${s.height}</span>`);
392+
const { card, zoomBtn, nativeBtn, editBtn, exportBtn } = mkCard(`${s.name} <span class="dim">${s.width}&times;${s.height}</span>`);
378393
const canvas = drawSprite(s, z, grid);
379394
card.appendChild(canvas);
380395
if (s.isMono) card.appendChild(mkMonoPickers(s, canvas));
381396
zoomBtn.onclick = () => downloadCanvas(drawSprite(s, getZoom(), false), s.name, getZoom());
382397
nativeBtn.onclick = () => downloadCanvas(drawSprite(s, 1, false), s.name);
398+
editBtn.onclick = () => openEditorWithSprite(s, s.name);
383399
exportBtn.onclick = () => exportSpriteHeader(s);
384400
container.appendChild(card);
385401
});
386402

387403
Object.entries(frameGroups).forEach(([base, frames]) => {
388404
const { width: w, height: h } = frames[0];
389-
const { card, zoomBtn, nativeBtn, exportBtn } = mkCard(
405+
const { card, zoomBtn, nativeBtn, editBtn, exportBtn } = mkCard(
390406
`${base} <span class="dim">${frames.length} frames &times; ${w}&times;${h}</span>`);
391407

392408
// ── animated preview ─────────────────────────────────────────────────
@@ -423,9 +439,22 @@ function renderSprites() {
423439
const getCurrentFrame = setupAnim(base, frames, animCanvas, fpsInput, playBtn);
424440
attachPixelInspector(animCanvas, getCurrentFrame);
425441

442+
editBtn.onclick = () => openEditorWithSprite(frames[0], `${base}_frame0`);
443+
426444
// ── strip or individual frames ───────────────────────────────────────
427445
if (useStrip && frames.length > 1) {
428446
card.appendChild(drawStrip(frames, z, grid));
447+
const stripEditRow = document.createElement('div');
448+
stripEditRow.className = 'frames-row';
449+
frames.forEach((f, i) => {
450+
const btn = document.createElement('button');
451+
btn.className = 'save-png-btn';
452+
btn.textContent = `Edit Frame ${i}`;
453+
btn.title = `Open Frame ${i} in the sprite editor`;
454+
btn.onclick = () => openEditorWithSprite(f, `${base}_frame${i}`);
455+
stripEditRow.appendChild(btn);
456+
});
457+
card.appendChild(stripEditRow);
429458
} else {
430459
const row = document.createElement('div');
431460
row.className = 'frames-row';
@@ -441,6 +470,12 @@ function renderSprites() {
441470
framePngBtn.title = `Save Frame ${i} as PNG at current zoom (${z}×)`;
442471
framePngBtn.onclick = () => downloadCanvas(drawSprite(f, z, false), `${base}_frame${i}`, z);
443472
wrap.appendChild(framePngBtn);
473+
const frameEditBtn = document.createElement('button');
474+
frameEditBtn.className = 'save-png-btn';
475+
frameEditBtn.textContent = 'Edit';
476+
frameEditBtn.title = `Open Frame ${i} in the sprite editor`;
477+
frameEditBtn.onclick = () => openEditorWithSprite(f, `${base}_frame${i}`);
478+
wrap.appendChild(frameEditBtn);
444479
row.appendChild(wrap);
445480
});
446481
card.appendChild(row);
@@ -554,6 +589,11 @@ function mkCard(labelHtml) {
554589
nativeBtn.title = 'Save PNG at native size (1 screen pixel per sprite pixel)';
555590
btns.appendChild(zoomBtn);
556591
btns.appendChild(nativeBtn);
592+
const editBtn = document.createElement('button');
593+
editBtn.className = 'save-png-btn';
594+
editBtn.textContent = 'Edit';
595+
editBtn.title = 'Open this sprite in the editor';
596+
btns.appendChild(editBtn);
557597
// Add Export RGB565 .h button
558598
const exportBtn = document.createElement('button');
559599
exportBtn.className = 'save-png-btn';
@@ -562,7 +602,7 @@ function mkCard(labelHtml) {
562602
btns.appendChild(exportBtn);
563603
lbl.appendChild(btns);
564604
card.appendChild(lbl);
565-
return { card, zoomBtn, nativeBtn, exportBtn };
605+
return { card, zoomBtn, nativeBtn, editBtn, exportBtn };
566606
}
567607

568608
function drawBg(ctx, w, h, z, offX = 0, bg = null) {

0 commit comments

Comments
 (0)