castle-web-cli 0.4.84 → 0.4.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ide.js +35 -13
- package/dist/shell/assets/{index-BOgm5T3W.js → index-BJLaUTJE.js} +21 -21
- package/dist/shell/index.html +1 -1
- package/kits/physics-2d/.prettierrc +8 -0
- package/kits/physics-2d/CLAUDE.md +329 -0
- package/kits/physics-2d/behaviors/Camera.jsx +43 -0
- package/kits/physics-2d/behaviors/Collider.jsx +199 -0
- package/kits/physics-2d/behaviors/Goal.jsx +29 -0
- package/kits/physics-2d/behaviors/Layout.jsx +53 -0
- package/kits/physics-2d/behaviors/Sprite.jsx +352 -0
- package/kits/physics-2d/behaviors/tint.js +47 -0
- package/kits/physics-2d/blueprints/ball.scene +14 -0
- package/kits/physics-2d/blueprints/block.scene +12 -0
- package/kits/physics-2d/blueprints/cauldron.scene +18 -0
- package/kits/physics-2d/blueprints/crate.scene +14 -0
- package/kits/physics-2d/blueprints/goal.scene +12 -0
- package/kits/physics-2d/castle.json +13 -0
- package/kits/physics-2d/docs/pxart-format.md +377 -0
- package/kits/physics-2d/drawings/block.pxart +25 -0
- package/kits/physics-2d/drawings/cauldron.pxart +113 -0
- package/kits/physics-2d/editors/BlueprintLibrary.jsx +247 -0
- package/kits/physics-2d/editors/ErrorBoundary.jsx +59 -0
- package/kits/physics-2d/editors/PlayOnly.jsx +31 -0
- package/kits/physics-2d/editors/PxArtEditor.jsx +954 -0
- package/kits/physics-2d/editors/SceneEditor.jsx +1681 -0
- package/kits/physics-2d/editors/SelectionOverlay.jsx +909 -0
- package/kits/physics-2d/editors/SingleEditor.jsx +122 -0
- package/kits/physics-2d/editors/behaviorRegistry.js +30 -0
- package/kits/physics-2d/editors/editorHistory.js +157 -0
- package/kits/physics-2d/editors/inspectorSheet.js +13 -0
- package/kits/physics-2d/editors/pixelCanvas.js +11 -0
- package/kits/physics-2d/editors/pixelEditorChrome.jsx +74 -0
- package/kits/physics-2d/editors/pixelGeometry.js +140 -0
- package/kits/physics-2d/editors/pixelInspector.jsx +633 -0
- package/kits/physics-2d/editors/pxArtEditorModel.js +732 -0
- package/kits/physics-2d/editors/pxArtPlayback.js +92 -0
- package/kits/physics-2d/editors/pxArtTimeline.jsx +752 -0
- package/kits/physics-2d/editors/pxArtTimeline.module.css +506 -0
- package/kits/physics-2d/editors/pxArtTools.js +232 -0
- package/kits/physics-2d/editors/useArtboardFit.js +102 -0
- package/kits/physics-2d/engine/ScenePlayer.jsx +196 -0
- package/kits/physics-2d/engine/SceneUI.jsx +59 -0
- package/kits/physics-2d/engine/assets.js +15 -0
- package/kits/physics-2d/engine/autoInspector.jsx +70 -0
- package/kits/physics-2d/engine/blueprint.js +521 -0
- package/kits/physics-2d/engine/collider.js +196 -0
- package/kits/physics-2d/engine/files.js +117 -0
- package/kits/physics-2d/engine/liveReload.js +88 -0
- package/kits/physics-2d/engine/pxart.js +1032 -0
- package/kits/physics-2d/engine/pxartSmooth.js +222 -0
- package/kits/physics-2d/engine/scene.js +686 -0
- package/kits/physics-2d/engine/spriteGeometry.js +32 -0
- package/kits/physics-2d/engine/ui.jsx +688 -0
- package/kits/physics-2d/engine/ui.module.css +2287 -0
- package/kits/physics-2d/eslint.config.js +71 -0
- package/kits/physics-2d/index.html +24 -0
- package/kits/physics-2d/main.jsx +24 -0
- package/kits/physics-2d/package-lock.json +2706 -0
- package/kits/physics-2d/package.json +42 -0
- package/kits/physics-2d/physics/PhysicsSystem.js +290 -0
- package/kits/physics-2d/physics/behaviors/AnalogStick.jsx +101 -0
- package/kits/physics-2d/physics/behaviors/Draggable.jsx +79 -0
- package/kits/physics-2d/physics/behaviors/RigidBody.jsx +55 -0
- package/kits/physics-2d/physics/behaviors/Slingshot.jsx +118 -0
- package/kits/physics-2d/physics/controls.js +79 -0
- package/kits/physics-2d/physics/index.js +26 -0
- package/kits/physics-2d/physics/matterBridge.js +126 -0
- package/kits/physics-2d/pnpm-lock.yaml +1761 -0
- package/kits/physics-2d/scenes/main.scene +12 -0
- package/kits/physics-2d/scenes/sandbox.scene +13 -0
- package/kits/physics-2d/scripts/draw.mjs +121 -0
- package/kits/physics-2d/vite.config.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# `.pxart` Pixel-Art Format (basic-2d kit)
|
|
2
|
+
|
|
3
|
+
This documents the `.pxart` sprite format as integrated into the `basic-2d`
|
|
4
|
+
kit. The format and its parser/serializer/renderer live in the kit itself, in
|
|
5
|
+
`engine/pxart.js` (`parseFull`, `serializeFull`, `renderSpriteFrame`, plus
|
|
6
|
+
the compact-form helpers `parseCompact` / `serializeCompact` / `renderToCanvas`).
|
|
7
|
+
`.pxart` is a pure kit-layer concern — the harness (`castle-web-sdk`, the CLI)
|
|
8
|
+
has no knowledge of it.
|
|
9
|
+
|
|
10
|
+
A sprite is modeled on Aseprite conventions: a `layers × frames` spreadsheet of
|
|
11
|
+
cells, one resolution per sprite, an ordered indexed palette, named animation
|
|
12
|
+
tags, and Aseprite-style linked cells. A `.pxart` file is a single JSON object.
|
|
13
|
+
|
|
14
|
+
The format has two **coexisting on-disk forms** — capability tiers, *not*
|
|
15
|
+
temporal versions — that the kit treats as one in-memory model:
|
|
16
|
+
|
|
17
|
+
- **full** — the full layered/animated `Sprite` (`"format": "full"`).
|
|
18
|
+
- **compact** — a flat `{ palette, grid }` object (`"format": "compact"`, or
|
|
19
|
+
absent). The parser upgrades it into a degenerate one-layer / one-frame
|
|
20
|
+
Sprite in memory.
|
|
21
|
+
|
|
22
|
+
> Files in this kit use the extension `.pxart`. They live under `drawings/`
|
|
23
|
+
> and are discovered by `engine/files.js`.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 1. Form discriminator
|
|
28
|
+
|
|
29
|
+
- Every canonical file carries a top-level string `format`.
|
|
30
|
+
- `"format": "full"` — the layered/animated sprite.
|
|
31
|
+
- `"format": "compact"` (or absent) — the flat-grid shorthand (see
|
|
32
|
+
[§9](#9-compact-shorthand)). The parser upgrades it in memory.
|
|
33
|
+
- `serializeFull` always stamps `"format": "full"`. `serializeCompact` (the
|
|
34
|
+
compact path the kit's `PxArtEditor` and the `draw` generator use) always
|
|
35
|
+
stamps `"format": "compact"`.
|
|
36
|
+
- **The two forms are capability tiers, not versions** — `full` is a superset of
|
|
37
|
+
`compact`. Files of both kinds coexist; a sprite is written in whichever form
|
|
38
|
+
loses nothing (see [§9](#9-compact-shorthand)).
|
|
39
|
+
- **Detection is by STRUCTURE, not the field.** The parser recognizes the
|
|
40
|
+
compact form by an object `palette` + a `grid`, and the full form by a
|
|
41
|
+
`resolution` / array `palette` / `frames` / `layers`. The `format` field is a
|
|
42
|
+
hint used only to disambiguate genuinely ambiguous shapes; clear structure
|
|
43
|
+
always wins, so a file with a missing or even mislabeled discriminator still
|
|
44
|
+
parses (without silent mis-detection). Unknown top-level fields are ignored
|
|
45
|
+
(forward compatibility).
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 2. Resolution
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
"resolution": { "width": 16, "height": 16 }
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- **Default resolution is 16×16** (`DEFAULT_RESOLUTION`). New/empty sprites and
|
|
56
|
+
generated art start at 16×16, and the kit's `PxArtEditor` opens a blank canvas
|
|
57
|
+
at this size.
|
|
58
|
+
- **Power-of-two only**, from **16 to 512**: one of `16, 32, 64, 128, 256, 512`.
|
|
59
|
+
- **One resolution per sprite.** There is no per-layer or per-frame resolution.
|
|
60
|
+
- **Parser policy:**
|
|
61
|
+
- **Explicit full-form `resolution`:** each dimension is **snapped** to the
|
|
62
|
+
valid set (clamped to `[16, 512]`, then rounded to the nearest power of two,
|
|
63
|
+
ties round up). Out-of-range values snap rather than reject, keeping the
|
|
64
|
+
parser tolerant.
|
|
65
|
+
- **compact shorthand upgrade:** resolution is inferred from the grid's
|
|
66
|
+
**native dimensions verbatim** and is **not** snapped, so arbitrary-size
|
|
67
|
+
compact grids round-trip faithfully.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 3. Palette
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
"palette": [
|
|
75
|
+
{ "key": "o", "hex": "#e8b800" },
|
|
76
|
+
{ "key": "y", "hex": "#ffe11a" }
|
|
77
|
+
]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
- An **ORDERED array** of `{ key, hex }` entries (indexed-with-hex color mode).
|
|
81
|
+
- `key`: a **single character** used in packed grid strings.
|
|
82
|
+
- `hex`: a normalized `#rrggbb` or `#rrggbbaa`.
|
|
83
|
+
- **Ordering is meaningful** — it is the stable index, preserved on parse and
|
|
84
|
+
serialize.
|
|
85
|
+
- **`.` is reserved for transparent** and is **never** a palette entry. A grid
|
|
86
|
+
character of `.` (or any char with no matching palette key) renders
|
|
87
|
+
transparent.
|
|
88
|
+
- Indexed-with-hex only: **not** RGBA-per-pixel, **not** grayscale.
|
|
89
|
+
|
|
90
|
+
### Two-tier palette system (Endesga-64)
|
|
91
|
+
|
|
92
|
+
The kit uses a **two-tier** fixed palette, both tiers drawn from Lospec's
|
|
93
|
+
[Endesga 64](https://lospec.com/palette-list/endesga-64). There is no free color
|
|
94
|
+
picking at either tier.
|
|
95
|
+
|
|
96
|
+
**Full palette — Endesga 64 (`EDG64`, all 64 colors): what USERS paint with.**
|
|
97
|
+
The kit's `PxArtEditor` exposes the **full 64-color swatch** (canonical Lospec
|
|
98
|
+
order). The editor assigns each color a stable single-char key (`KEY_ALPHABET`,
|
|
99
|
+
64 distinct keys) plus `.` for transparent. When an existing sprite is opened for
|
|
100
|
+
editing, each cell's resolved color is **snapped to the nearest Endesga-64
|
|
101
|
+
color** by RGB distance, so editing always stays on the fixed swatch. (Sprites
|
|
102
|
+
still *render* with their own stored palette via `renderSpriteFrame`; the snap
|
|
103
|
+
only affects what the editor writes back.) The swatch UI renders as an 8-column
|
|
104
|
+
grid (8 rows for the 64 colors) that scrolls within the inspector.
|
|
105
|
+
|
|
106
|
+
**Agent subset — 16 colors (`AGENT_PALETTE_16`): what the LLM generates with.**
|
|
107
|
+
The agent generation path (the `draw` svg-rect → `.pxart` quantizer and the
|
|
108
|
+
prompt the model receives) is constrained to a **fixed 16-color subset of
|
|
109
|
+
Endesga-64**, so generated art stays coherent. Every color below is a member of
|
|
110
|
+
`EDG64`. In order:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
#e69c69 #bf6f4a #8a4836 #391f21 #891e2b #ea323c #ffa214 #ffeb57
|
|
114
|
+
#5ac54f #1e6f50 #134c4c #657392 #c7cfdd #ffffff #0cf1ff #0098dc
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
So: the editor offers all 64 to people; the agent generation path snaps to these
|
|
118
|
+
16. Both constants (`EDG64`, `AGENT_PALETTE_16`) live in the kit's
|
|
119
|
+
`engine/pxart.js`.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 4. Frames & timing
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
"frames": [{ "durationMs": 120 }, { "durationMs": 120 }, { "durationMs": 240 }],
|
|
127
|
+
"defaultDurationMs": 120
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- `frames`: an array of `{ durationMs }`. Its length is the **frame count**.
|
|
131
|
+
Frame indices are 0-based.
|
|
132
|
+
- `defaultDurationMs`: applied to any frame missing an explicit `durationMs`
|
|
133
|
+
(the kit default is 100ms).
|
|
134
|
+
- **No global `fps` field** (matches Aseprite). Holding a frame longer is just a
|
|
135
|
+
larger `durationMs`.
|
|
136
|
+
|
|
137
|
+
The kit's `Sprite` behavior advances frames in its `update(dt)` using each
|
|
138
|
+
frame's `durationMs`.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## 5. Tags
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
"tags": [
|
|
146
|
+
{ "name": "idle", "from": 0, "to": 1, "direction": "pingpong", "repeat": 0 },
|
|
147
|
+
{ "name": "blink", "from": 2, "to": 2, "direction": "forward", "repeat": 1 }
|
|
148
|
+
],
|
|
149
|
+
"defaultTag": "idle"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- `tags`: named, inclusive 0-based frame ranges. Each tag has:
|
|
153
|
+
- `name`: stable handle.
|
|
154
|
+
- `from`, `to`: inclusive frame indices.
|
|
155
|
+
- `direction`: `"forward" | "reverse" | "pingpong"`.
|
|
156
|
+
- `repeat`: play count; **`0` = loop forever**.
|
|
157
|
+
- `defaultTag`: name of the tag that plays by default.
|
|
158
|
+
|
|
159
|
+
**Animation lives in the file** — ranges and timing are the asset's intrinsic
|
|
160
|
+
property. The placement/runtime layer may override *which* tag plays:
|
|
161
|
+
|
|
162
|
+
### Actor-side tag override (this kit)
|
|
163
|
+
|
|
164
|
+
The `Sprite` behavior exposes a `tag` prop. Resolution order:
|
|
165
|
+
|
|
166
|
+
1. If `tag` is a non-empty string naming a tag in the file, that tag plays.
|
|
167
|
+
2. Otherwise the file's `defaultTag` plays.
|
|
168
|
+
3. Otherwise the whole timeline plays forward, looping.
|
|
169
|
+
|
|
170
|
+
`direction` and `repeat` come from the tag (a whole-timeline fallback loops
|
|
171
|
+
forever, forward). When `repeat` is exhausted the behavior holds the last frame.
|
|
172
|
+
When the `playing` prop is `false`, the behavior holds frame 0.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## 6. Layers
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
"layers": [
|
|
180
|
+
{
|
|
181
|
+
"id": "body", "name": "Body", "visible": true, "opacity": 1.0,
|
|
182
|
+
"blendMode": "normal", "kind": "pixel",
|
|
183
|
+
"cells": [ ... ]
|
|
184
|
+
}
|
|
185
|
+
]
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
- `layers`: an **ordered** array, **bottom → top** (`layers[0]` drawn first).
|
|
189
|
+
- Each layer: `id`, `name`, `visible` (skipped when hidden), `opacity`
|
|
190
|
+
(`0.0`–`1.0`), `blendMode` (**`"normal"` only** for now), `kind`
|
|
191
|
+
(**`"pixel"` only** for now), and `cells` (its row of the spreadsheet).
|
|
192
|
+
|
|
193
|
+
`renderSpriteFrame` composites visible layers bottom→top, applying per-layer
|
|
194
|
+
opacity, at 1px/cell with `imageSmoothingEnabled = false`.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## 7. Cells
|
|
199
|
+
|
|
200
|
+
`cells` is **dense-with-`null`**: `layer.cells.length == frames.length`, and
|
|
201
|
+
cell index `f` is the layer's content for frame `f`. A cell is one of:
|
|
202
|
+
|
|
203
|
+
| Form | Meaning |
|
|
204
|
+
| --- | --- |
|
|
205
|
+
| `{ "grid": ["...", "..."] }` | An image: packed strings using palette keys, one per row. `.` = transparent. |
|
|
206
|
+
| `{ "link": <frameIndex> }` | Aseprite-style **same-layer** reference; shares the image of `cells[frameIndex]`. Edits to the source propagate. |
|
|
207
|
+
| `null` | Empty / fully transparent for this (layer, frame). |
|
|
208
|
+
|
|
209
|
+
- A `link` must point at a cell index on the **same layer**; the resolver
|
|
210
|
+
(`resolveCellGrid`) follows the chain to the underlying `{ grid }`, guarding
|
|
211
|
+
against cycles and invalid indices (resolving to `null` if it can't reach a
|
|
212
|
+
grid).
|
|
213
|
+
- **Faithful storage:** grids are stored as **packed strings** of palette keys.
|
|
214
|
+
Generation-only emission shapes (svg-rect, etc.) normalize into this packed
|
|
215
|
+
form and are never the thing of record.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 8. Coordinates
|
|
220
|
+
|
|
221
|
+
- **Origin is top-left.** `grid[0]` is the top row; char index 0 is the leftmost
|
|
222
|
+
column.
|
|
223
|
+
- Units are **pixels** within the sprite's `resolution`. Scaling/placement
|
|
224
|
+
happen **outside** the file — in this kit, the actor's `Layout` box. The
|
|
225
|
+
`Sprite` behavior blits the native-resolution frame into the Layout rectangle
|
|
226
|
+
with smoothing disabled and an optional `tint` multiply.
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## 9. compact shorthand
|
|
231
|
+
|
|
232
|
+
A file with a top-level `palette` (an **object** map) + `grid` (no `layers` /
|
|
233
|
+
`frames`) is the **compact flat-grid shorthand**:
|
|
234
|
+
|
|
235
|
+
```json
|
|
236
|
+
{
|
|
237
|
+
"format": "compact",
|
|
238
|
+
"palette": { "y": "#ffe11a", "o": "#e8b800", ".": null },
|
|
239
|
+
"grid": ["..ooyyoo..", ".oyyyyyyo.", "oyyyyyyyyo"]
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
It is **sugar for one layer / one frame**, upgraded in memory by `parseFull`
|
|
244
|
+
(via `upgradeCompactToFull`):
|
|
245
|
+
|
|
246
|
+
- `resolution` ← the grid's native `{ width, height }` (not snapped).
|
|
247
|
+
- `palette` ← the compact record as an ordered array (insertion order), dropping
|
|
248
|
+
the `.`/null transparent entry.
|
|
249
|
+
- one `frame` using `defaultDurationMs`.
|
|
250
|
+
- one `pixel` layer whose single cell is `{ grid: <the grid> }`.
|
|
251
|
+
|
|
252
|
+
This keeps cold-start generation tiny: emit just `{ palette, grid }` and the
|
|
253
|
+
system treats it as a full (degenerate) Sprite.
|
|
254
|
+
|
|
255
|
+
### Single-layer / single-frame generation contract
|
|
256
|
+
|
|
257
|
+
The **generation contract** for this kit is to emit the **compact shorthand**: a
|
|
258
|
+
single 16×16 (default) `{ palette, grid }` sprite over the agent 16-color subset
|
|
259
|
+
(`AGENT_PALETTE_16`).
|
|
260
|
+
The CLI generation worker and the kit's `PxArtEditor` both produce/store this
|
|
261
|
+
compact form when nothing is lost by it. Multi-layer / multi-frame structure is
|
|
262
|
+
authored by tools later, not hand-generated — and the minimal `PxArtEditor`
|
|
263
|
+
opens such files **read-only** (it only edits the single-layer / single-frame
|
|
264
|
+
case).
|
|
265
|
+
|
|
266
|
+
### compact ↔ full relationship
|
|
267
|
+
|
|
268
|
+
- `full` is a **superset** of `compact`; the compact functions keep their exact
|
|
269
|
+
behavior.
|
|
270
|
+
- A compact file is equivalent to a full sprite with one `pixel` layer and one
|
|
271
|
+
frame.
|
|
272
|
+
- `parseFull` accepts **both** forms and always yields a full `Sprite`.
|
|
273
|
+
- On save, the kit picks the form **structurally**: it serializes the working
|
|
274
|
+
sprite to the candidate compact form, re-parses it, and keeps the compact
|
|
275
|
+
output only when that round-trip reproduces the sprite exactly (`serializeModel`
|
|
276
|
+
in `editors/pxArtEditorModel.js`). A single fully-opaque visible layer / single
|
|
277
|
+
inheriting frame / no tags / all on-canvas content round-trips losslessly, so
|
|
278
|
+
it is stored compact. Anything the compact form can't carry — extra
|
|
279
|
+
layers/frames/tags, per-layer opacity or visibility, cel offsets, or off-canvas
|
|
280
|
+
content — forces `serializeFull`. This is a derived fact, not a hand-maintained
|
|
281
|
+
checklist, so it stays correct as new full-form features are added.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## 10. Corner rounding
|
|
286
|
+
|
|
287
|
+
```json
|
|
288
|
+
"cornerRadius": 0.25
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
- A top-level **number**: a corner-rounding radius, in native-pixel units.
|
|
292
|
+
`0` (the default) renders sharp/nearest-neighbor, same as before this field
|
|
293
|
+
existed. Any value `> 0` renders rounded, by that amount — see "Rendering"
|
|
294
|
+
below. **File-level** — a property of the sprite asset itself, not a
|
|
295
|
+
per-actor/per-placement override. There is no equivalent `Sprite` behavior
|
|
296
|
+
prop.
|
|
297
|
+
- **A value, not a flag**, deliberately: the amount of rounding is itself
|
|
298
|
+
part of the portable file format, not a fixed code-side constant every
|
|
299
|
+
smooth sprite would otherwise be stuck with. `MAX_CORNER_RADIUS` (0.5, in
|
|
300
|
+
`engine/pxart.js`) is the ceiling — two corner cuts on the same
|
|
301
|
+
1-native-pixel edge must not overlap — and any value is clamped to
|
|
302
|
+
`[0, MAX_CORNER_RADIUS]` on read. The `PxArtEditor` UI curates this down to
|
|
303
|
+
a 3-way segmented control (`0` / `¼` / `½`, see `CornerRadiusBar` in
|
|
304
|
+
`editors/pixelInspector.jsx`) so picking a value never means hunting a
|
|
305
|
+
slider, but the format itself isn't limited to those three — any in-range
|
|
306
|
+
number is valid, e.g. from hand-edited JSON or a future finer-grained UI.
|
|
307
|
+
- **`0` is the default** and is never written to disk: `serializeFull` and
|
|
308
|
+
`serializeCompact` both OMIT the field when it's `0`, so existing (and
|
|
309
|
+
newly authored, unsmoothed) files stay byte-identical to their
|
|
310
|
+
pre-`cornerRadius`-field shape.
|
|
311
|
+
- **Present in BOTH on-disk forms.** `parseCompact` reads a top-level
|
|
312
|
+
`cornerRadius` alongside `palette`/`grid`; `parseFull`'s native-full branch
|
|
313
|
+
reads it alongside `resolution`/`layers`; `upgradeCompactToFull` carries a
|
|
314
|
+
compact file's `cornerRadius` through into the upgraded Sprite. This means
|
|
315
|
+
the kit's save path — which picks compact vs. full by a serialize →
|
|
316
|
+
re-parse round-trip (`serializeModel` in `editors/pxArtEditorModel.js`, see
|
|
317
|
+
[§9](#9-compact-shorthand)) — never drops the field: a single-layer/frame
|
|
318
|
+
rounded sprite still serializes compact, with `cornerRadius: 0.25` alongside
|
|
319
|
+
`palette`/`grid`.
|
|
320
|
+
- **Parser tolerance:** any non-finite-number value (missing field, `null`, a
|
|
321
|
+
typo) defaults to `0`. Numbers are clamped to `[0, MAX_CORNER_RADIUS]`.
|
|
322
|
+
**Legacy back-compat:** this field used to be called `render` — first a
|
|
323
|
+
`"pixel"` | `"smooth"` string enum, then (briefly) a bare numeric radius
|
|
324
|
+
under that same key. Both migrate on read: a legacy `render: "smooth"`
|
|
325
|
+
becomes a fixed radius (`0.25`); a legacy numeric `render` value is read
|
|
326
|
+
as-is; anything else (including the original `render: "pixel"`) defaults to
|
|
327
|
+
`0`. This keeps old files rendering rounded rather than silently reverting
|
|
328
|
+
to sharp. Detection of compact vs. full form ([§1](#1-form-discriminator))
|
|
329
|
+
is unaffected — neither key is ever used as a structural signal.
|
|
330
|
+
- **Rendering.** `cornerRadius === 0` renders as before: 1px/cell,
|
|
331
|
+
nearest-neighbor (`renderSpriteFrame`, `imageSmoothingEnabled = false`).
|
|
332
|
+
`cornerRadius > 0` renders through `engine/pxartSmooth.js`'s
|
|
333
|
+
`renderSmoothSpriteFrame`, using a LOCAL per-pixel kernel — the same shape
|
|
334
|
+
of algorithm as Animal Crossing's actual smoothing (xBRZ-style template
|
|
335
|
+
matching), not a global vectorization pass: it composites the frame
|
|
336
|
+
normally (so layer visibility/opacity/blend keep working), then for each
|
|
337
|
+
pixel independently, classifies each of its 4 corners against only the 3
|
|
338
|
+
pixels touching that corner (its two edge-adjacent neighbors and their
|
|
339
|
+
shared diagonal neighbor). A corner rounds only when it's a genuine convex
|
|
340
|
+
corner of that pixel's own color region — a diagonal touch between two
|
|
341
|
+
same-colored pixels is deliberately left sharp so it doesn't get visually
|
|
342
|
+
pinched off, and a corner already matched by an adjacent same-color
|
|
343
|
+
neighbor is left for that neighbor's own (independent) classification to
|
|
344
|
+
handle. Because the kernel never looks past a pixel's immediate
|
|
345
|
+
neighborhood, there's no notion of a "run" or "line" to (mis)detect across
|
|
346
|
+
a whole shape — every pixel supersamples into its own fixed block of the
|
|
347
|
+
output canvas (by default 8x the sprite's native resolution), so rounding
|
|
348
|
+
a corner is always a same-block recoloring, never a shape that could leave
|
|
349
|
+
a gap against its neighbors. This suits small pixel-art sprites, not
|
|
350
|
+
general raster upscaling.
|
|
351
|
+
- `behaviors/Sprite.jsx` blits a `cornerRadius > 0` sprite's cached canvas
|
|
352
|
+
with `imageSmoothingEnabled = true` (vs. `false` for `cornerRadius === 0`),
|
|
353
|
+
passing the sprite's own `cornerRadius` value through as
|
|
354
|
+
`renderSmoothSpriteFrame`'s `cornerRadius` option; the offscreen canvas
|
|
355
|
+
cache itself is unaffected (still one WeakMap entry per sprite object —
|
|
356
|
+
smoothing is a property of the sprite, not an extra cache dimension). The
|
|
357
|
+
`PxArtEditor` artboard previews rounded sprites live (a display-resolution
|
|
358
|
+
canvas behind the pixel-grid editing surface) while keeping normal
|
|
359
|
+
pixel-grid editing/selection interactions on the grid underneath.
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## 11. Reserved / out of scope
|
|
364
|
+
|
|
365
|
+
Named so the format can grow without a breaking change, but **not built**:
|
|
366
|
+
tilemap/tileset layers (a future `kind`), slices, layer groups, vector/avatar
|
|
367
|
+
layers, and rich blend modes (beyond `"normal"`). Reserving the `kind` and
|
|
368
|
+
`blendMode` discriminators keeps these additive later, not breaking.
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Appendix — sample
|
|
373
|
+
|
|
374
|
+
`drawings/cauldron.pxart` is a 16×16 multi-layer animated sprite referenced by
|
|
375
|
+
the `pig` actor in `scenes/main.scene` (via `blueprints/cauldron.scene`)
|
|
376
|
+
through the `Sprite` behavior — a working rendered example of the format in
|
|
377
|
+
this kit.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "compact",
|
|
3
|
+
"palette": {
|
|
4
|
+
"a": "#c7cfdd",
|
|
5
|
+
"b": "#657392"
|
|
6
|
+
},
|
|
7
|
+
"grid": [
|
|
8
|
+
"aaaaaaaaaaaaaaaa",
|
|
9
|
+
"bbbbbbbbbbbbbbbb",
|
|
10
|
+
"bbbbbbbbbbbbbbbb",
|
|
11
|
+
"bbbbbbbbbbbbbbbb",
|
|
12
|
+
"bbbbbbbbbbbbbbbb",
|
|
13
|
+
"bbbbbbbbbbbbbbbb",
|
|
14
|
+
"bbbbbbbbbbbbbbbb",
|
|
15
|
+
"bbbbbbbbbbbbbbbb",
|
|
16
|
+
"bbbbbbbbbbbbbbbb",
|
|
17
|
+
"bbbbbbbbbbbbbbbb",
|
|
18
|
+
"bbbbbbbbbbbbbbbb",
|
|
19
|
+
"bbbbbbbbbbbbbbbb",
|
|
20
|
+
"bbbbbbbbbbbbbbbb",
|
|
21
|
+
"bbbbbbbbbbbbbbbb",
|
|
22
|
+
"bbbbbbbbbbbbbbbb",
|
|
23
|
+
"bbbbbbbbbbbbbbbb"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": "full",
|
|
3
|
+
"resolution": {
|
|
4
|
+
"width": 16,
|
|
5
|
+
"height": 16
|
|
6
|
+
},
|
|
7
|
+
"palette": [
|
|
8
|
+
{
|
|
9
|
+
"key": "I",
|
|
10
|
+
"hex": "#5ac54f"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"key": "1",
|
|
14
|
+
"hex": "#622461"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"key": "2",
|
|
18
|
+
"hex": "#93388f"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"frames": [
|
|
22
|
+
{},
|
|
23
|
+
{}
|
|
24
|
+
],
|
|
25
|
+
"defaultDurationMs": 500,
|
|
26
|
+
"tags": [],
|
|
27
|
+
"cornerRadius": 0.25,
|
|
28
|
+
"layers": [
|
|
29
|
+
{
|
|
30
|
+
"id": "layer-0",
|
|
31
|
+
"name": "Layer 1",
|
|
32
|
+
"visible": true,
|
|
33
|
+
"opacity": 1,
|
|
34
|
+
"blendMode": "normal",
|
|
35
|
+
"kind": "pixel",
|
|
36
|
+
"cells": [
|
|
37
|
+
{
|
|
38
|
+
"grid": [
|
|
39
|
+
"................",
|
|
40
|
+
"................",
|
|
41
|
+
"................",
|
|
42
|
+
"...11111111111..",
|
|
43
|
+
".211IIIIIIIII11.",
|
|
44
|
+
".22IIIIIIIII221.",
|
|
45
|
+
"..22222222222...",
|
|
46
|
+
"...1111111111...",
|
|
47
|
+
"..222222221111..",
|
|
48
|
+
".22222222221111.",
|
|
49
|
+
".22222222221111.",
|
|
50
|
+
".22222222221111.",
|
|
51
|
+
".22222222211111.",
|
|
52
|
+
"..222222211111..",
|
|
53
|
+
".11222211111111.",
|
|
54
|
+
".11.11111111.11."
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"link": 0
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"id": "layer-1",
|
|
64
|
+
"name": "Layer 1 copy",
|
|
65
|
+
"visible": true,
|
|
66
|
+
"opacity": 1,
|
|
67
|
+
"blendMode": "normal",
|
|
68
|
+
"kind": "pixel",
|
|
69
|
+
"cells": [
|
|
70
|
+
{
|
|
71
|
+
"grid": [
|
|
72
|
+
"...........I....",
|
|
73
|
+
"....I...........",
|
|
74
|
+
".......I........",
|
|
75
|
+
"................",
|
|
76
|
+
"................",
|
|
77
|
+
"................",
|
|
78
|
+
"................",
|
|
79
|
+
"................",
|
|
80
|
+
"................",
|
|
81
|
+
"................",
|
|
82
|
+
"................",
|
|
83
|
+
"................",
|
|
84
|
+
"................",
|
|
85
|
+
"................",
|
|
86
|
+
"................",
|
|
87
|
+
"................"
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"grid": [
|
|
92
|
+
"...I............",
|
|
93
|
+
"........I.......",
|
|
94
|
+
"............I...",
|
|
95
|
+
"................",
|
|
96
|
+
"................",
|
|
97
|
+
"................",
|
|
98
|
+
"................",
|
|
99
|
+
"................",
|
|
100
|
+
"................",
|
|
101
|
+
"................",
|
|
102
|
+
"................",
|
|
103
|
+
"................",
|
|
104
|
+
"................",
|
|
105
|
+
"................",
|
|
106
|
+
"................",
|
|
107
|
+
"................"
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
}
|