castle-web-cli 0.4.115 → 0.4.116
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/agent-prompts.js +2 -2
- package/dist/agent.d.ts +9 -9
- package/dist/agent.js +590 -589
- package/dist/castleJson.d.ts +6 -0
- package/dist/castleJson.js +10 -0
- package/dist/editorConfig.js +27 -3
- package/dist/imports.d.ts +4 -0
- package/dist/imports.js +59 -10
- package/dist/init.d.ts +3 -0
- package/dist/init.js +99 -87
- package/dist/install.d.ts +1 -1
- package/dist/install.js +26 -24
- package/dist/shell/assets/{index-CUamb8rK.js → index-DiPlPGyg.js} +2 -2
- package/dist/shell/index.html +1 -1
- package/dist/vitePlugins.js +1 -1
- package/kits/physics-2d/CLAUDE.md +28 -16
- package/kits/physics-2d/behaviors/Joints.jsx +1 -1
- package/kits/physics-2d/behaviors/Sprite.jsx +17 -12
- package/kits/physics-2d/blueprints/ball.scene +1 -1
- package/kits/physics-2d/blueprints/block.scene +1 -1
- package/kits/physics-2d/blueprints/cauldron.scene +1 -1
- package/kits/physics-2d/blueprints/crate.scene +1 -1
- package/kits/physics-2d/castle.json +11 -3
- package/kits/physics-2d/docs/pxart-format.md +537 -51
- package/kits/physics-2d/editors/PxArtEditor.jsx +1794 -65
- package/kits/physics-2d/editors/brushFit.js +535 -0
- package/kits/physics-2d/editors/brushShapes.js +140 -0
- package/kits/physics-2d/editors/mediaFile.js +12 -1
- package/kits/physics-2d/editors/pathOverlay.js +340 -0
- package/kits/physics-2d/editors/pathTools.js +1906 -0
- package/kits/physics-2d/editors/pixelCanvas.js +13 -0
- package/kits/physics-2d/editors/pixelEditorChrome.jsx +2 -2
- package/kits/physics-2d/editors/pixelGeometry.js +4 -2
- package/kits/physics-2d/editors/pixelInspector.jsx +410 -37
- package/kits/physics-2d/editors/pxArtEditorModel.js +172 -16
- package/kits/physics-2d/editors/pxArtTimeline.jsx +163 -43
- package/kits/physics-2d/editors/pxArtTimeline.module.css +31 -5
- package/kits/physics-2d/engine/assets.js +1 -1
- package/kits/physics-2d/engine/blueprint.js +3 -3
- package/kits/physics-2d/engine/files.js +3 -1
- package/kits/physics-2d/engine/liveReload.js +1 -1
- package/kits/physics-2d/engine/physics/jointArt.js +3 -3
- package/kits/physics-2d/engine/pxart.js +153 -35
- package/kits/physics-2d/engine/pxartPath.js +1356 -0
- package/kits/physics-2d/engine/pxartSmooth.js +276 -125
- package/kits/physics-2d/engine/ui.jsx +22 -1
- package/kits/physics-2d/engine/ui.module.css +36 -12
- package/kits/physics-2d/package-lock.json +1 -1
- package/kits/physics-2d/scripts/draw.mjs +7 -7
- package/kits/physics-2d/scripts/import-svg.mjs +1231 -0
- package/kits/physics-2d/scripts/svg-emission-guide.md +92 -0
- package/package.json +1 -1
- /package/kits/physics-2d/drawings/{block.pxart → block.sprite} +0 -0
- /package/kits/physics-2d/drawings/{cauldron.pxart → cauldron.sprite} +0 -0
- /package/kits/physics-2d/drawings/{joint-rope.pxart → joint-rope.sprite} +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Sprite format (`.sprite`, physics-2d kit)
|
|
2
2
|
|
|
3
|
-
This documents the
|
|
3
|
+
This documents the Castle sprite format as integrated into the `physics-2d`
|
|
4
4
|
kit. The format and its parser/serializer/renderer live in the kit itself, in
|
|
5
5
|
`engine/pxart.js` (`parseFull`, `serializeFull`, `renderSpriteFrame`, plus
|
|
6
6
|
the compact-form helpers `parseCompact` / `serializeCompact` / `renderToCanvas`).
|
|
7
|
-
|
|
7
|
+
The on-disk JSON format is a pure kit-layer concern — the harness (`castle-web-sdk`, the CLI)
|
|
8
8
|
has no knowledge of it.
|
|
9
9
|
|
|
10
10
|
A sprite is modeled on Aseprite conventions: a `layers × frames` spreadsheet of
|
|
11
11
|
cells, one resolution per sprite, an ordered indexed palette, named animation
|
|
12
|
-
tags, and Aseprite-style linked cells. A
|
|
12
|
+
tags, and Aseprite-style linked cells. A sprite file is a single JSON object.
|
|
13
13
|
|
|
14
14
|
The format has two **coexisting on-disk forms** — capability tiers, *not*
|
|
15
15
|
temporal versions — that the kit treats as one in-memory model:
|
|
@@ -19,8 +19,9 @@ temporal versions — that the kit treats as one in-memory model:
|
|
|
19
19
|
absent). The parser upgrades it into a degenerate one-layer / one-frame
|
|
20
20
|
Sprite in memory.
|
|
21
21
|
|
|
22
|
-
> Files in this kit use the extension `.
|
|
23
|
-
> and
|
|
22
|
+
> Files in this kit use the extension `.sprite` under `drawings/`. The legacy
|
|
23
|
+
> extension `.pxart` names the same format and remains readable; create new art
|
|
24
|
+
> as `.sprite`.
|
|
24
25
|
|
|
25
26
|
---
|
|
26
27
|
|
|
@@ -104,7 +105,7 @@ only affects what the editor writes back.) The swatch UI renders as an 8-column
|
|
|
104
105
|
grid (8 rows for the 64 colors) that scrolls within the inspector.
|
|
105
106
|
|
|
106
107
|
**Agent subset — 16 colors (`AGENT_PALETTE_16`): what the LLM generates with.**
|
|
107
|
-
The agent generation path (the `draw` svg-rect → `.
|
|
108
|
+
The agent generation path (the `draw` svg-rect → `.sprite` quantizer and the
|
|
108
109
|
prompt the model receives) is constrained to a **fixed 16-color subset of
|
|
109
110
|
Endesga-64**, so generated art stays coherent. Every color below is a member of
|
|
110
111
|
`EDG64`. In order:
|
|
@@ -188,10 +189,386 @@ When the `playing` prop is `false`, the behavior holds frame 0.
|
|
|
188
189
|
- `layers`: an **ordered** array, **bottom → top** (`layers[0]` drawn first).
|
|
189
190
|
- Each layer: `id`, `name`, `visible` (skipped when hidden), `opacity`
|
|
190
191
|
(`0.0`–`1.0`), `blendMode` (**`"normal"` only** for now), `kind`
|
|
191
|
-
(**`"pixel"`
|
|
192
|
+
(**`"pixel"` or `"path"`**; missing/unknown values default to `"pixel"`),
|
|
193
|
+
and `cells` (its row of the spreadsheet).
|
|
192
194
|
|
|
193
195
|
`renderSpriteFrame` composites visible layers bottom→top, applying per-layer
|
|
194
|
-
opacity, at 1px/cell with `imageSmoothingEnabled = false`.
|
|
196
|
+
opacity, at 1px/cell with `imageSmoothingEnabled = false`. This render path is
|
|
197
|
+
kind-agnostic: every consumer can read the baked `grid`. **Pixels are truth.**
|
|
198
|
+
|
|
199
|
+
### 6.1 Path layers (stacked shapes)
|
|
200
|
+
|
|
201
|
+
A `"path"` layer may attach editable vector source to any grid cell. The vector
|
|
202
|
+
model is an **ordered stack of shapes** painted back-to-front (painter's
|
|
203
|
+
order).
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{
|
|
207
|
+
"grid": ["...."],
|
|
208
|
+
"path": {
|
|
209
|
+
"shapes": [
|
|
210
|
+
{
|
|
211
|
+
"fill": "a",
|
|
212
|
+
"stroke": null,
|
|
213
|
+
"subpaths": [
|
|
214
|
+
{
|
|
215
|
+
"closed": true,
|
|
216
|
+
"start": [4, 8],
|
|
217
|
+
"segs": [
|
|
218
|
+
{ "to": [12, 8], "c1": [2.2917, 2], "c2": [-2.2917, 2] },
|
|
219
|
+
{ "to": [4, 8], "c1": [-2.2917, -2], "c2": [2.2917, -2] }
|
|
220
|
+
]
|
|
221
|
+
}
|
|
222
|
+
]
|
|
223
|
+
}
|
|
224
|
+
]
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
- `shapes` is an ordered array (index 0 = back; later shapes overwrite earlier
|
|
230
|
+
pixels). Each shape has:
|
|
231
|
+
- `fill`: palette key or `null` (no fill).
|
|
232
|
+
- `stroke`: palette key or `null` (no stroke). Stroke is per-shape — whole
|
|
233
|
+
outline or none; there are no per-segment strokes.
|
|
234
|
+
- either `subpaths` — one or more rings/chains, described below — or a `prim`
|
|
235
|
+
key, whose geometry is generated from parameters instead
|
|
236
|
+
([§6.1.3](#613-primitive-shapes-normative)).
|
|
237
|
+
- A subpath is `{ closed, start, segs }`: one starting anchor and an ordered
|
|
238
|
+
list of segments leaving it. Its anchors are `start` followed by each `to`,
|
|
239
|
+
except that a curved closing segment's `to` restates `start` rather than
|
|
240
|
+
adding an anchor — see closure below.
|
|
241
|
+
- A segment is `{ to, c1?, c2? }`:
|
|
242
|
+
- `to` is the segment's end anchor, in absolute cell units.
|
|
243
|
+
- `c1` and `c2` are **cubic Bézier control offsets, stored relative** — `c1`
|
|
244
|
+
from the segment's start anchor, `c2` from `to`. Relative handles are what
|
|
245
|
+
let an anchor drag move a curve rigidly without rewriting its controls.
|
|
246
|
+
- A segment with neither is a straight line. There is no other curve form:
|
|
247
|
+
circular arcs are materialized into cubics on the way in.
|
|
248
|
+
- **Closure is implicit when it is straight, explicit when it is curved.** This
|
|
249
|
+
is SVG's `Z` with one addition, and it is the canonical form: a straight
|
|
250
|
+
closing segment is never stored.
|
|
251
|
+
- A four-corner rectangle stores **three** segments and reports **four**
|
|
252
|
+
anchors and **four** edges. The edge from the last anchor back to `start` is
|
|
253
|
+
synthesized by `subpathEdgeCount` / `subpathEdgeFrom` /
|
|
254
|
+
`edgeObjectsForSubpath`, so it is fully editable — bend it, split it, stroke
|
|
255
|
+
it — without being on disk.
|
|
256
|
+
- A **curved** close cannot be implied, because handles have nowhere to live.
|
|
257
|
+
It stays as a final segment whose `to` duplicates `start` — the second
|
|
258
|
+
segment in the example above (a lens is two curved edges between two
|
|
259
|
+
anchors).
|
|
260
|
+
- `subpathClosesOnStart` distinguishes the two. When it is true the duplicate
|
|
261
|
+
is **not a distinct anchor**; `subpathVertexCount` and `subpathPoints` omit
|
|
262
|
+
it, and anything that **rebuilds segments by index** must map that segment
|
|
263
|
+
back to vertex 0. Indexing it as `segIndex + 1` puts it past the last vertex,
|
|
264
|
+
so a drag strands it at the old coordinate: the ring gains a phantom anchor
|
|
265
|
+
and a spike back to where the shape used to be. Implicit closure removes this
|
|
266
|
+
hazard for straight closes by removing the duplicate, but curved closes still
|
|
267
|
+
have it.
|
|
268
|
+
- Canonicalization happens in `normalizeSubpath` and at the parse boundary, so
|
|
269
|
+
a redundant straight closing segment from an in-memory constructor is dropped
|
|
270
|
+
on read rather than tolerated. Two forms of the same ring never coexist and
|
|
271
|
+
disagree about edge count.
|
|
272
|
+
- Path data built in memory rather than loaded — a fixture, a generator — has
|
|
273
|
+
not been through that boundary, so anything consuming it must read through
|
|
274
|
+
`normalizeSubpath` or hand it to `parsePathData` first.
|
|
275
|
+
- Multi-subpath fills use **even-odd** winding (donuts / holes). Open subpaths
|
|
276
|
+
are skipped when filling, so a limb strokes but never bounds a region.
|
|
277
|
+
- An open subpath with stroke and no fill is a 1px polyline.
|
|
278
|
+
- **A shape may mix a closed outline with open subpaths, and that is how art
|
|
279
|
+
with internal divisions is written.** The closed ones bound the fill; the open
|
|
280
|
+
ones only stroke. One outline around the whole silhouette, plus a line for
|
|
281
|
+
each internal division:
|
|
282
|
+
|
|
283
|
+
```json
|
|
284
|
+
{
|
|
285
|
+
"fill": "b",
|
|
286
|
+
"stroke": "a",
|
|
287
|
+
"subpaths": [
|
|
288
|
+
{ "closed": true, "start": [5, 5], "segs": [
|
|
289
|
+
{ "to": [16, 5] }, { "to": [16, 9] }, { "to": [24, 9] },
|
|
290
|
+
{ "to": [24, 19] }, { "to": [16, 19] }, { "to": [16, 23] },
|
|
291
|
+
{ "to": [5, 23] } ] },
|
|
292
|
+
{ "closed": false, "start": [16, 9], "segs": [{ "to": [16, 19] }] }
|
|
293
|
+
]
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
The alternative — two abutting closed shapes — carries two fill keys that have
|
|
298
|
+
to be kept in step, and seams where they meet, because each strokes its own
|
|
299
|
+
side of the shared border. One shape with a divider has a single fill, one
|
|
300
|
+
stroke down the middle, and nothing to keep in step. Anchors shared between
|
|
301
|
+
subpaths are held together by coordinate: the editor moves and deletes every
|
|
302
|
+
anchor sitting on a site as one, so a divider stays welded to the outline it
|
|
303
|
+
divides.
|
|
304
|
+
|
|
305
|
+
The editor writes exactly this when a pen run that starts and ends on an
|
|
306
|
+
outline is spliced into it: the outline takes in the new run, and the stretch
|
|
307
|
+
it displaced is kept as an open subpath rather than discarded.
|
|
308
|
+
- **A boundary is stored once.** An open subpath that a closed subpath in the
|
|
309
|
+
same shape already walks in full is dropped rather than kept beside it
|
|
310
|
+
(`absorbRetracedSubpaths`). The match is a contiguous run of the loop's edges,
|
|
311
|
+
beginning anywhere on it and read either way round, comparing anchor positions
|
|
312
|
+
**and** curvature — so a divider counts as retraced only when it lies exactly
|
|
313
|
+
on the outline, not merely near it.
|
|
314
|
+
|
|
315
|
+
This is the dual of splice, and the pair is what stops editing from
|
|
316
|
+
ratcheting. Splice lifts a run out of a loop and keeps it because it has become
|
|
317
|
+
interior; absorb puts it back when the loop comes round to retrace it, which is
|
|
318
|
+
what deleting the vertex that made it interior does. Without both, every such
|
|
319
|
+
round trip leaves a second copy of an edge that already exists, and the copies
|
|
320
|
+
stroke twice and drift apart on the next drag. Dropping one is safe precisely
|
|
321
|
+
because `stroke` is per-shape, so coincident edges paint identically.
|
|
322
|
+
|
|
323
|
+
Unlike the closure rules above, this is **not** applied at the parse boundary.
|
|
324
|
+
A file keeps a retraced run until an edit commits, so it renders as authored
|
|
325
|
+
right up until the shape is first touched — which means **authored files must
|
|
326
|
+
not rely on one surviving**. Write a division only where it departs from the
|
|
327
|
+
outline; where it coincides, the outline's own edge is already the stroke.
|
|
328
|
+
- Because only closed subpaths bound the fill, **an outline that is opened stops
|
|
329
|
+
filling**. Removing an edge from a ring does exactly that — there is no
|
|
330
|
+
representation for a ring with a gap, so the survivors become an open subpath.
|
|
331
|
+
The shape keeps its `fill` key while open, so closing it again restores the
|
|
332
|
+
fill it had rather than leaving it to be picked a second time.
|
|
333
|
+
- Coordinates are finite floats in cell units, **framed to the canvas**
|
|
334
|
+
([§6.1.1](#611-coordinate-model-normative)) and rounded to four decimals on
|
|
335
|
+
serialize. The half-cell lattice is authoring guidance, not a storage
|
|
336
|
+
invariant ([§6.1.2](#612-the-half-cell-lattice-authoring-guidance)).
|
|
337
|
+
Off-canvas points are legal and clip during baking.
|
|
338
|
+
- Sagitta/bow ratio detents are an authoring **gesture** only (the bend tool
|
|
339
|
+
magnetizes toward bow/chord ratios at eighth-turn central angles). Storage is
|
|
340
|
+
always cubic segments.
|
|
341
|
+
- Shape objects may carry additional keys for forward-compatible variants. v1
|
|
342
|
+
only understands `fill` / `stroke` / `subpaths` / `prim`.
|
|
343
|
+
|
|
344
|
+
#### 6.1.1 Coordinate model (normative)
|
|
345
|
+
|
|
346
|
+
A sprite cell has **two** ways to say "where": the cel offset (`x`/`y` on the
|
|
347
|
+
cell, see [§7](#7-cells)) and the coordinates inside the cell's content. For a
|
|
348
|
+
pixel cell those compose — a small `grid` placed at an offset. For a path cell
|
|
349
|
+
they would double-count, so the format fixes it:
|
|
350
|
+
|
|
351
|
+
- **Path point coordinates are CANVAS-framed**, not cel-local: `[0, 0]` is the
|
|
352
|
+
canvas origin, and the bake output covers the full `resolution`, never a
|
|
353
|
+
tight bounding box.
|
|
354
|
+
- **A path cell's `x` and `y` MUST be `0`.** Serializers omit them. A nonzero
|
|
355
|
+
offset on a path cell is malformed, not a second positioning mechanism.
|
|
356
|
+
Rather than discard a malformed offset — which would jump the art to the
|
|
357
|
+
origin — the parser FOLDS it into the source, translating every point by
|
|
358
|
+
`(x, y)` and then clearing it. The cell lands in the canonical form with its
|
|
359
|
+
appearance unchanged, so an old or hand-edited file migrates silently and
|
|
360
|
+
correctly.
|
|
361
|
+
|
|
362
|
+
The offset mechanism exists so a pixel cel can retain content pushed outside
|
|
363
|
+
the canvas window without storing a canvas-sized grid of transparency. Path
|
|
364
|
+
cells don't need it: the vector source retains off-canvas geometry exactly (as
|
|
365
|
+
plain numbers) and clips only when baking, so nothing is lost without it.
|
|
366
|
+
|
|
367
|
+
Every cell on a path layer — including freshly converted layers with no
|
|
368
|
+
`path` key yet — MUST carry offset `(0, 0)`. Serializers omit `x`/`y`; nonzero
|
|
369
|
+
offsets on path layers are malformed.
|
|
370
|
+
|
|
371
|
+
**Resize** (`resizeSprite`) reframes and never crops. Pixel cells keep their
|
|
372
|
+
grid and shift their offset by the centered delta. Path cells have no offset
|
|
373
|
+
to shift, so they translate instead: every point moves by the same
|
|
374
|
+
`(dx, dy)` (via `translatePathData`) and the cell re-bakes at the new
|
|
375
|
+
resolution. Segment handles are stored relative and so are unaffected. A path layer
|
|
376
|
+
therefore survives any sequence of resizes losslessly — the source is
|
|
377
|
+
resolution-independent and only the derived `grid` changes.
|
|
378
|
+
|
|
379
|
+
#### 6.1.2 The half-cell lattice (authoring guidance)
|
|
380
|
+
|
|
381
|
+
The **half-cell lattice** — multiples of `0.5`, where integers are pixel
|
|
382
|
+
corners and half-integers are pixel centers — is the natural grid for pixel
|
|
383
|
+
art. Authoring tools **magnetize** toward it when the pointer is close (within
|
|
384
|
+
a screen-space radius, so the pull feels constant regardless of zoom). Alt
|
|
385
|
+
extends magnetism to pixel corners as well as centers.
|
|
386
|
+
|
|
387
|
+
**Storage does not enforce the lattice.** Coordinates round to four decimal
|
|
388
|
+
places on serialize; off-lattice values load faithfully. This lets imported SVG
|
|
389
|
+
and continuous ellipse parameters keep their geometry while still offering a
|
|
390
|
+
predictable snap feel in the editor.
|
|
391
|
+
|
|
392
|
+
For clean rasterization: stroked outlines degrade at exact integer coordinates;
|
|
393
|
+
filled regions are cleanest at integer corners. The SVG emission guide advises
|
|
394
|
+
agents on optimal phases — see `scripts/svg-emission-guide.md`.
|
|
395
|
+
|
|
396
|
+
`bows` are **not** stored — curves are cubic segments (`c1`/`c2` relative
|
|
397
|
+
offsets). The bend tool uses ratio detents as a **gesture** with magnetic pull.
|
|
398
|
+
|
|
399
|
+
Worth knowing before reaching for the curve vocabulary to fix a shape: it is
|
|
400
|
+
usually not what is limiting. Measured against a true ellipse, eight free arcs
|
|
401
|
+
on *exact* anchors land within 0.04 cells, but snapping those anchors to the
|
|
402
|
+
half-cell lattice costs 0.17 — several times more than the vocabulary ceiling.
|
|
403
|
+
The same measurement shows adding anchors can make a shape worse, not better:
|
|
404
|
+
a 4-anchor circle is exact to 0.006 cells, while an 8-anchor one is off by
|
|
405
|
+
0.343, because the intermediate anchors do not land on the lattice.
|
|
406
|
+
|
|
407
|
+
#### 6.1.3 Primitive shapes (normative)
|
|
408
|
+
|
|
409
|
+
A shape may carry a `prim` key instead of `subpaths`, in which case its geometry
|
|
410
|
+
is **generated from parameters** rather than authored. One primitive exists:
|
|
411
|
+
|
|
412
|
+
```json
|
|
413
|
+
{ "prim": "ellipse", "cx": 8, "cy": 8, "rx": 7, "ry": 4, "fill": "q", "stroke": null }
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
`cx`, `cy`, `rx`, `ry` are finite floats like any other coordinate, rounded to
|
|
417
|
+
four decimals on serialize; the radii are clamped to `MIN_ELLIPSE_RADIUS`.
|
|
418
|
+
Authoring magnetizes them toward the lattice the way it magnetizes points, but
|
|
419
|
+
storage does not enforce it, so a resize is continuous rather than stepped.
|
|
420
|
+
`fill` and `stroke` mean what they mean for any other shape.
|
|
421
|
+
|
|
422
|
+
**A primitive's `subpaths` are derived on parse and never serialized.** This is
|
|
423
|
+
the whole point of the construct, and it follows from the measurement in §6.1.2:
|
|
424
|
+
anchor placement — not the arc vocabulary — is what deforms an oval, so a
|
|
425
|
+
primitive stores only its *parameters* and lets the curve derived from them run
|
|
426
|
+
free. Writing that curve to disk would expose every intermediate anchor to
|
|
427
|
+
rounding and to any tool that quantizes points, undoing the gain. A 7×4 oval
|
|
428
|
+
derived this way sits within 0.005 cells of a true ellipse against 0.174 for the
|
|
429
|
+
same oval with hand-placed anchors.
|
|
430
|
+
|
|
431
|
+
The ring is traced by sixteen circular arcs whose sagittae are fitted to the
|
|
432
|
+
true ellipse. A circle comes out to a thousandth of a cell — every span really
|
|
433
|
+
is a circular arc — and an oval to about four times that, which is still far
|
|
434
|
+
below the lattice error it replaces.
|
|
435
|
+
|
|
436
|
+
Because the geometry is regenerated, **only the parameters are editable.** The
|
|
437
|
+
four anchors on the ellipse's own axes are exposed as radius handles; the rest
|
|
438
|
+
of the ring is not grabbable, and anchor insertion, deletion, bending, opening,
|
|
439
|
+
and hole-punching are all refused rather than allowed to appear to work and then
|
|
440
|
+
vanish on the next derive. **Detach-to-paths** (materialize the derived ring as
|
|
441
|
+
ordinary subpaths) is planned; until then the refusal is intentional.
|
|
442
|
+
Whole-shape operations — move, flip, quarter-turn,
|
|
443
|
+
duplicate, recolor, reorder — act on the parameters and behave normally.
|
|
444
|
+
|
|
445
|
+
Dragging a radius handle holds the opposite edge still, as in Figma and
|
|
446
|
+
Illustrator; Alt resizes about the centre instead. The difference is felt when
|
|
447
|
+
the pointer is near a lattice site and magnetism engages: anchoring the far edge
|
|
448
|
+
spends the whole snap at the handle, so it advances a cell at a time, while
|
|
449
|
+
resizing about the centre splits it across both sides and the handle tracks the
|
|
450
|
+
pointer twice as finely.
|
|
451
|
+
|
|
452
|
+
Consumers other than the editor need to know none of this: parse hands them an
|
|
453
|
+
ordinary shape with subpaths, so bake, stroke tracing, hit-testing, and the
|
|
454
|
+
vector renderer are unchanged.
|
|
455
|
+
|
|
456
|
+
#### 6.1.4 Stroke canonicality (normative)
|
|
457
|
+
|
|
458
|
+
A stroke exists in three forms, and it matters which one is authoritative for
|
|
459
|
+
what, because they are not cell-identical:
|
|
460
|
+
|
|
461
|
+
| Form | Where | Authoritative for |
|
|
462
|
+
| --- | --- | --- |
|
|
463
|
+
| **Source** — anchors + cubic segments | the `path` object | **all geometry queries**: hit-testing, snapping, selection, measurement |
|
|
464
|
+
| **Traced chain** — 8-connected 1px cells | `traceEdgePixels`, in the bake | the stored `grid` (what every consumer reads) |
|
|
465
|
+
| **Analytic stroke** — 1-cell round-cap/join | `renderer: "vector"` only | display, that frame only |
|
|
466
|
+
|
|
467
|
+
Both the traced chain and the analytic stroke are **derived** from the source;
|
|
468
|
+
neither derives from the other, and they are not required to agree cell for
|
|
469
|
+
cell. The traced chain quantizes to keep 1px stroke discipline; the analytic
|
|
470
|
+
stroke follows the true curve.
|
|
471
|
+
|
|
472
|
+
**Layer invariant:** anything answering "what does this look like / where is it"
|
|
473
|
+
(fill bake, stroke trace, hit-testing, marquee, vector render) flattens the
|
|
474
|
+
stored cubic segments. Sagitta/bow is a **gesture vocabulary only** — the bend
|
|
475
|
+
tool reads and writes it, but representation queries never re-derive a circular
|
|
476
|
+
arc from measured sagitta when true handles are present.
|
|
477
|
+
|
|
478
|
+
The rule that falls out: **never hit-test against baked pixels or against the
|
|
479
|
+
analytic stroke.** Editor queries measure distance to the flattened source
|
|
480
|
+
curve, so selection behaves identically at every finish setting.
|
|
481
|
+
`forEachStrokePixel` is a bake primitive and must not be used for hit-testing —
|
|
482
|
+
under `renderer: "vector"` it reports cells the user isn't looking at.
|
|
483
|
+
|
|
484
|
+
**Normative bake** (`bakePathCell` in `engine/pxartPath.js`):
|
|
485
|
+
|
|
486
|
+
This defines the bake's **output**, not its algorithm. Any implementation that
|
|
487
|
+
produces an identical grid is conformant — bounding-box culling, active-edge
|
|
488
|
+
scanline fills, and dirty-region re-bakes are all permitted and expected, since
|
|
489
|
+
the reference formulation below is superlinear in `resolution` and the format
|
|
490
|
+
allows canvases up to 512. The observable contract is the grid.
|
|
491
|
+
|
|
492
|
+
The kit takes that latitude: it culls each shape to its bounding box and fills
|
|
493
|
+
by scanline, computing edge crossings once per supersample row instead of
|
|
494
|
+
testing every subpixel against every edge. Read literally, step 2a costs
|
|
495
|
+
`area × supersample² × edges` per shape — and since the supersample is squared,
|
|
496
|
+
that grows with the FOURTH power of resolution. A stack that bakes in a
|
|
497
|
+
millisecond at 16×16 took **31 seconds** at 128×128 before the change and takes
|
|
498
|
+
under 100 ms after. Keep the reference formulation as the definition of the
|
|
499
|
+
output; do not implement it literally.
|
|
500
|
+
|
|
501
|
+
1. Conceptually, one supersampled composite raster covers the whole layer at 4×
|
|
502
|
+
(`FILL_SUPERSAMPLE`).
|
|
503
|
+
2. For each shape in stack order: (a) its fill covers every subpixel whose
|
|
504
|
+
center is inside the shape under even-odd winding over the flattened
|
|
505
|
+
subpaths (curves via the stored cubic segments), overwriting; (b) its 1px
|
|
506
|
+
stroke chain is computed at **native** resolution by flattening the same
|
|
507
|
+
cubic segments with symmetric quantization (`flattenEdge`), 8-connected
|
|
508
|
+
tracing (`traceEdgePixels`), and direction-invariant `cleanupCorners`, and
|
|
509
|
+
each stroke cell occupies a full `scale×scale` block of that key.
|
|
510
|
+
3. After all shapes: majority-downsample the composite once to native cells —
|
|
511
|
+
a cell paints only when painted subpixels cover ≥ half of it (ties paint);
|
|
512
|
+
color is the majority key among painted subpixels; on key-count ties prefer
|
|
513
|
+
the key painted **later** in stack order (painter's-order tiebreak).
|
|
514
|
+
|
|
515
|
+
The bake is a **pure function** of `(path, width, height)`: deterministic, with
|
|
516
|
+
no dependence on iteration order, floating-point accumulation across shapes, or
|
|
517
|
+
prior grid contents. Two bakes of equal input produce equal output.
|
|
518
|
+
|
|
519
|
+
Rationale: compositing fills at supersample before the single downsample means
|
|
520
|
+
two abutting shapes' subpixels union across the shared boundary, so boundary
|
|
521
|
+
cells go to whoever owns the majority — no 1px transparent gutters. Stroke
|
|
522
|
+
cells stamped as full blocks keep 1px stroke discipline while still letting a
|
|
523
|
+
later shape's fill cover an earlier shape's stroke.
|
|
524
|
+
|
|
525
|
+
Per-shape order within a shape is fill then stroke (stroke wins over that
|
|
526
|
+
shape's own fill). The bake has no anti-aliasing: every output cell remains an
|
|
527
|
+
exact palette key. Pixels remain the source of truth — `grid` is always baked
|
|
528
|
+
on serialization; runtime and non-path-aware consumers read only pixels.
|
|
529
|
+
|
|
530
|
+
**Finish semantics** (file-level `renderer` + `cornerRadius`, see
|
|
531
|
+
[§10](#10-finish)):
|
|
532
|
+
|
|
533
|
+
- `renderer: "grid"`, `cornerRadius: 0` (Pixel): render the baked `grid`
|
|
534
|
+
nearest-neighbor.
|
|
535
|
+
- `renderer: "grid"`, `cornerRadius > 0` (`¼` / `½`): corner-kernel smoothing on
|
|
536
|
+
the baked grid (same kernel as pixel layers — works for both kinds). The grid
|
|
537
|
+
itself is unchanged.
|
|
538
|
+
- `renderer: "vector"` (Smooth): paint the shape stack **analytically** into the
|
|
539
|
+
supersampled canvas — fills as even-odd canvas paths, strokes as 1-cell-wide
|
|
540
|
+
analytic strokes with round caps and round joins along the true curves,
|
|
541
|
+
shapes in stack order.
|
|
542
|
+
|
|
543
|
+
Only the last one departs from the baked grid, and only for display — see
|
|
544
|
+
[§6.1.4](#614-stroke-canonicality-normative) for which form is authoritative
|
|
545
|
+
for what.
|
|
546
|
+
|
|
547
|
+
Parser degradation is local and tolerant: a missing/non-array `shapes` drops
|
|
548
|
+
the path object while the baked grid remains valid; malformed shapes/subpaths
|
|
549
|
+
drop individually. `path` and `kind` round-trip through full parse/serialize.
|
|
550
|
+
Palette pruning includes shape fill/stroke keys.
|
|
551
|
+
|
|
552
|
+
Links share the whole resolved cell, including `path`. Each materialized cell
|
|
553
|
+
owns its own copy — resolving a link or re-linking after a frame op must deep
|
|
554
|
+
copy `path`, never alias one object across cells. A path layer always
|
|
555
|
+
serializes in full form (the `kind` mismatch fails compact eligibility on its
|
|
556
|
+
own; nothing hand-maintained enforces it).
|
|
557
|
+
|
|
558
|
+
**Layer-kind conversion is lossy in both directions**, and neither direction is
|
|
559
|
+
reversible except via Undo:
|
|
560
|
+
|
|
561
|
+
- **Pixel → Path** discards the layer's pixels on every frame. There is no
|
|
562
|
+
vectorization; the new path source starts empty and the grid re-bakes
|
|
563
|
+
transparent. The editor confirms first when the layer has opaque pixels.
|
|
564
|
+
- **Path → Pixel** discards the vector source and keeps the baked grid as
|
|
565
|
+
truth. The art survives; its editability does not.
|
|
566
|
+
|
|
567
|
+
Interactive shape editing is **shipped** (`editors/pathTools.js` — pen, select,
|
|
568
|
+
bend, punch, anchor edit, flip/rotate, z-order, recolor), alongside the render
|
|
569
|
+
path (bake + finishes + overlay). The path unit and check suites live in the
|
|
570
|
+
`shape-layers` lab deck, which is where this engine is developed; run them there
|
|
571
|
+
after format, bake, parser, serializer, or model changes.
|
|
195
572
|
|
|
196
573
|
---
|
|
197
574
|
|
|
@@ -213,6 +590,24 @@ cell index `f` is the layer's content for frame `f`. A cell is one of:
|
|
|
213
590
|
- **Faithful storage:** grids are stored as **packed strings** of palette keys.
|
|
214
591
|
Generation-only emission shapes (svg-rect, etc.) normalize into this packed
|
|
215
592
|
form and are never the thing of record.
|
|
593
|
+
- **Cel offset** (`x`/`y` on a `{ grid }` cell, omitted when `0`) places a grid
|
|
594
|
+
that is smaller than — or shifted out of — the canvas window, so a resize can
|
|
595
|
+
reframe without cropping. It applies to **pixel cells only**: a cell carrying
|
|
596
|
+
`path` must have `x`/`y` of `0`
|
|
597
|
+
([§6.1.1](#611-coordinate-model-normative)).
|
|
598
|
+
- Any helper that rebuilds a cell must **carry `path` through**, and each
|
|
599
|
+
rebuilt cell must own its own **deep copy** — resolving a link chain hands
|
|
600
|
+
several slots the same object, so materializing them without copying leaves
|
|
601
|
+
independent frames sharing one source. Rebuilding via a grid-only constructor
|
|
602
|
+
silently strips the vector source and leaves a stale baked grid behind it.
|
|
603
|
+
- A rebuild that MOVES a path cel (resize, cel-offset change) translates the
|
|
604
|
+
source and re-bakes rather than carrying the grid across unchanged, so the
|
|
605
|
+
grid never disagrees with the points it came from.
|
|
606
|
+
- A rebuild that would **write pixels directly into a path cel** — a paint
|
|
607
|
+
merge — must instead refuse. There is no correct outcome: keeping the source
|
|
608
|
+
desyncs it from the grid (the next path edit reverts the paint), and dropping
|
|
609
|
+
the source destroys the vector art. The pixel tools are gated off path layers
|
|
610
|
+
in the UI, so this is a guard, not a workflow.
|
|
216
611
|
|
|
217
612
|
---
|
|
218
613
|
|
|
@@ -224,6 +619,12 @@ cell index `f` is the layer's content for frame `f`. A cell is one of:
|
|
|
224
619
|
happen **outside** the file — in this kit, the actor's `Layout` box. The
|
|
225
620
|
`Sprite` behavior blits the native-resolution frame into the Layout rectangle
|
|
226
621
|
with smoothing disabled and an optional `tint` multiply.
|
|
622
|
+
- **Path source shares this space**, continuously rather than by cell index: a
|
|
623
|
+
point of `[3, 5]` is the corner where pixels `(2,4)`, `(3,4)`, `(2,5)` and
|
|
624
|
+
`(3,5)` meet, and `[3.5, 5.5]` is the center of pixel `(3,5)`. Both are on
|
|
625
|
+
the half-cell lattice ([§6.1.2](#612-the-half-cell-lattice-normative)).
|
|
626
|
+
Coordinates outside `[0, width] × [0, height]` are legal and retained; they
|
|
627
|
+
clip when baking.
|
|
227
628
|
|
|
228
629
|
---
|
|
229
630
|
|
|
@@ -282,51 +683,106 @@ case).
|
|
|
282
683
|
|
|
283
684
|
---
|
|
284
685
|
|
|
285
|
-
## 10.
|
|
686
|
+
## 10. Finish
|
|
286
687
|
|
|
287
688
|
```json
|
|
288
689
|
"cornerRadius": 0.25
|
|
289
690
|
```
|
|
290
691
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
692
|
+
```json
|
|
693
|
+
"renderer": "vector"
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
The finish is **two orthogonal fields**: *which renderer* draws the sprite, and
|
|
697
|
+
*how much* corner rounding the grid renderer applies.
|
|
698
|
+
|
|
699
|
+
- **`renderer`**: `"grid"` (default, omitted when default) or `"vector"`.
|
|
700
|
+
`"grid"` renders from the baked `grid`. `"vector"` renders each path layer
|
|
701
|
+
analytically from its shape stack and each pixel layer with the corner kernel
|
|
702
|
+
at `MAX_CORNER_RADIUS` — see [§6.1](#61-path-layers-stacked-shapes).
|
|
703
|
+
- **`cornerRadius`**: **always a number**, in native-pixel units, clamped to
|
|
704
|
+
`[0, MAX_CORNER_RADIUS]` on read. Default `0`, omitted when `0`. It is the
|
|
705
|
+
rounding amount the grid renderer applies; the vector renderer uses it for
|
|
706
|
+
its pixel layers.
|
|
707
|
+
|
|
708
|
+
Both are **file-level** — properties of the sprite asset itself, not
|
|
709
|
+
per-actor/per-placement overrides. There is no equivalent `Sprite` behavior
|
|
710
|
+
prop for either.
|
|
711
|
+
|
|
712
|
+
> **Why two fields.** These were briefly one: `cornerRadius` held either a
|
|
713
|
+
> number or the string `"smooth"`. That union is retired because a categorical
|
|
714
|
+
> mode and a continuous amount don't share a slot cleanly — every
|
|
715
|
+
> `cornerRadius > 0` test in the codebase silently read `"smooth"` as *false*
|
|
716
|
+
> (`'smooth' > 0` is `NaN`-comparison false), so a smooth sprite took the sharp
|
|
717
|
+
> branch anywhere the string wasn't special-cased, and `clampCornerRadius` had
|
|
718
|
+
> to return a string to stay lossless. Splitting them restores the invariant
|
|
719
|
+
> that **`cornerRadius` is a number everywhere**, which is what makes the
|
|
720
|
+
> "a value, not a flag" argument below actually hold.
|
|
721
|
+
|
|
297
722
|
- **A value, not a flag**, deliberately: the amount of rounding is itself
|
|
298
723
|
part of the portable file format, not a fixed code-side constant every
|
|
299
|
-
|
|
724
|
+
rounded sprite would otherwise be stuck with. `MAX_CORNER_RADIUS` (0.5, in
|
|
300
725
|
`engine/pxart.js`) is the ceiling — two corner cuts on the same
|
|
301
|
-
1-native-pixel edge must not overlap
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
726
|
+
1-native-pixel edge must not overlap. The `PxArtEditor` UI curates the pair
|
|
727
|
+
down to a 4-way segmented control (`Pixel` / `¼` / `½` / `Smooth`, see
|
|
728
|
+
`FinishBar` in `editors/pixelInspector.jsx`) so picking a finish never means
|
|
729
|
+
hunting a slider, but the format itself isn't limited to those four: any
|
|
730
|
+
in-range radius is valid under either renderer, e.g. from hand-edited JSON or
|
|
731
|
+
a future finer-grained UI.
|
|
732
|
+
|
|
733
|
+
| UI preset | `renderer` | `cornerRadius` |
|
|
734
|
+
| --- | --- | --- |
|
|
735
|
+
| Pixel | omitted (`"grid"`) | omitted (`0`) |
|
|
736
|
+
| ¼ | omitted (`"grid"`) | `0.25` |
|
|
737
|
+
| ½ | omitted (`"grid"`) | `0.5` |
|
|
738
|
+
| Smooth | `"vector"` | omitted (`0`) |
|
|
739
|
+
- **Defaults are never written to disk:** `serializeFull` and
|
|
740
|
+
`serializeCompact` both OMIT `cornerRadius` when it's `0` and `renderer` when
|
|
741
|
+
it's `"grid"`, so existing (and newly authored, unsmoothed) files stay
|
|
742
|
+
byte-identical to their pre-finish shape. A plain pixel sprite carries
|
|
743
|
+
neither field.
|
|
744
|
+
- **Present in BOTH on-disk forms.** `parseCompact` reads both fields
|
|
745
|
+
alongside `palette`/`grid`; `parseFull`'s native-full branch reads them
|
|
746
|
+
alongside `resolution`/`layers`; `upgradeCompactToFull` carries a compact
|
|
747
|
+
file's finish through into the upgraded Sprite. This means the kit's save
|
|
748
|
+
path — which picks compact vs. full by a serialize → re-parse round-trip
|
|
749
|
+
(`serializeModel` in `editors/pxArtEditorModel.js`, see
|
|
750
|
+
[§9](#9-compact-shorthand)) — never drops them: a single-layer/frame
|
|
318
751
|
rounded sprite still serializes compact, with `cornerRadius: 0.25` alongside
|
|
319
|
-
`palette`/`grid`.
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
752
|
+
`palette`/`grid`. The compact candidate must be built with BOTH fields and
|
|
753
|
+
the eligibility check must compare BOTH: that check compares an explicit list
|
|
754
|
+
of fields rather than the whole parsed object, so a field it does not name is
|
|
755
|
+
a field the compact form is free to silently drop. A mismatch on either one
|
|
756
|
+
forces the full form.
|
|
757
|
+
- **Parser tolerance:** `renderer` is the exact string `"vector"` or else
|
|
758
|
+
`"grid"` (missing, `null`, a typo — anything unrecognized reads as `"grid"`).
|
|
759
|
+
`cornerRadius` reads as a number clamped to `[0, MAX_CORNER_RADIUS]`, and
|
|
760
|
+
**any non-number defaults to `0`**; the parser never yields a non-numeric
|
|
761
|
+
`cornerRadius`, so downstream `cornerRadius > 0` tests are always meaningful.
|
|
762
|
+
**Legacy back-compat**, all migrating on read and none of them structural
|
|
763
|
+
signals (compact-vs-full detection, [§1](#1-form-discriminator), is
|
|
764
|
+
unaffected):
|
|
765
|
+
|
|
766
|
+
| Legacy on disk | Reads as |
|
|
767
|
+
| --- | --- |
|
|
768
|
+
| `cornerRadius: "smooth"` | `renderer: "vector"`, `cornerRadius: 0` |
|
|
769
|
+
| `cornerRadius: <number>` | `cornerRadius` clamped, `renderer: "grid"` |
|
|
770
|
+
| `render: "smooth"` (oldest enum) | `cornerRadius: 0.25`, `renderer: "grid"` |
|
|
771
|
+
| `render: <number>` (brief numeric phase) | that radius, `renderer: "grid"` |
|
|
772
|
+
| `render: "pixel"`, or anything else | `cornerRadius: 0`, `renderer: "grid"` |
|
|
773
|
+
|
|
774
|
+
Old files therefore keep rendering rounded rather than silently reverting to
|
|
775
|
+
sharp, and a file written before the split still selects the vector renderer.
|
|
776
|
+
- **Asking "is this sprite smoothed?"** Use `displayFinish(sprite)` from
|
|
777
|
+
`engine/pxart.js`, never a bare `cornerRadius` test. The vector renderer
|
|
778
|
+
carries `cornerRadius: 0`, so `cornerRadius === 0` reads a Smooth sprite as
|
|
779
|
+
Pixel. In `PxArtEditor` that predicate decides whether the native 1px/cell
|
|
780
|
+
canvas paints at all, and getting it wrong drew the blocky bake on top of the
|
|
781
|
+
analytic render — both finishes visible at once. `displayFinish` also returns
|
|
782
|
+
the radius to draw with, which is `MAX_CORNER_RADIUS` under the vector
|
|
783
|
+
renderer (pixel layers still go through the corner kernel there), not the
|
|
784
|
+
sprite's own field. `finish-check` guards this by asserting the interactive
|
|
785
|
+
canvas is empty at every non-Pixel finish.
|
|
330
786
|
- **Rendering.** `cornerRadius === 0` renders as before: 1px/cell,
|
|
331
787
|
nearest-neighbor (`renderSpriteFrame`, `imageSmoothingEnabled = false`).
|
|
332
788
|
`cornerRadius > 0` renders through `engine/pxartSmooth.js`'s
|
|
@@ -348,8 +804,29 @@ case).
|
|
|
348
804
|
a corner is always a same-block recoloring, never a shape that could leave
|
|
349
805
|
a gap against its neighbors. This suits small pixel-art sprites, not
|
|
350
806
|
general raster upscaling.
|
|
351
|
-
-
|
|
352
|
-
|
|
807
|
+
- **Rendering, `renderer: "vector"`.** `renderSmoothCompositeFrame` renders each
|
|
808
|
+
layer as smoothly as its data allows at one shared supersample scale, then
|
|
809
|
+
composites bottom-to-top with visibility and opacity. Path layers paint
|
|
810
|
+
their shape stack analytically (even-odd fills + 1-cell-wide round-cap
|
|
811
|
+
strokes in stack order) — see §6.1. Canvas anti-aliasing is intentional in
|
|
812
|
+
this display-only stage and never changes the stored grid. Pixel layers use
|
|
813
|
+
the corner kernel at `MAX_CORNER_RADIUS`. Under `renderer: "grid"` — at every
|
|
814
|
+
`cornerRadius`, including `¼` and `½` — path layers render the normative
|
|
815
|
+
`bakePathCell` grid unchanged.
|
|
816
|
+
- **Timeline thumbnails show the sprite's chosen finish**, via
|
|
817
|
+
`renderSmoothLayerCell` — a Smooth sprite reads as Smooth in the timeline
|
|
818
|
+
rather than as its bake. Only the plain Pixel finish takes the nearest-neighbor
|
|
819
|
+
blit. Measured at a 43px thumb, the analytic path render is *cheaper* than the
|
|
820
|
+
blit (0.06 ms vs 0.09 ms), because it draws shapes straight into the thumb
|
|
821
|
+
instead of baking a frame and upscaling it; the corner kernel behind `¼` / `½`
|
|
822
|
+
is the expensive one at ~0.4 ms, roughly 4-14x the blit. That cost is only
|
|
823
|
+
affordable because thumbs redraw on cel content and nothing else — see the
|
|
824
|
+
dependency list on `CellThumb`'s layout effect, which is load-bearing, not
|
|
825
|
+
incidental. It gates on the serialized `text`, since `deriveSprite` hands back
|
|
826
|
+
a fresh `sprite` object every render.
|
|
827
|
+
- `behaviors/Sprite.jsx` blits a smoothed sprite's cached canvas
|
|
828
|
+
with `imageSmoothingEnabled = true` (vs. `false` for the plain
|
|
829
|
+
`renderer: "grid"`, `cornerRadius === 0` case),
|
|
353
830
|
passing the sprite's own `cornerRadius` value through as
|
|
354
831
|
`renderSmoothSpriteFrame`'s `cornerRadius` option; the offscreen canvas
|
|
355
832
|
cache itself is unaffected (still one WeakMap entry per sprite object —
|
|
@@ -363,15 +840,24 @@ case).
|
|
|
363
840
|
## 11. Reserved / out of scope
|
|
364
841
|
|
|
365
842
|
Named so the format can grow without a breaking change, but **not built**:
|
|
366
|
-
tilemap/tileset layers (a future `kind`), slices, layer groups,
|
|
367
|
-
|
|
368
|
-
|
|
843
|
+
tilemap/tileset layers (a future `kind`), slices, layer groups, avatar layers,
|
|
844
|
+
and rich blend modes (beyond `"normal"`). Reserving the `kind` and `blendMode`
|
|
845
|
+
discriminators keeps these additive later, not breaking — `kind: "path"`
|
|
846
|
+
([§6.1](#61-path-layers-stacked-shapes)) is the first cash-in on that
|
|
847
|
+
reservation and shipped without a format break, which is the intended pattern
|
|
848
|
+
for the rest.
|
|
849
|
+
|
|
850
|
+
Within path layers specifically, **not built**: per-segment strokes, stroke
|
|
851
|
+
widths other than 1 cell, stroke alignment (inside/center/outside), gradients,
|
|
852
|
+
and non-circular curve types. The `shapes[]` entries tolerate unknown keys
|
|
853
|
+
precisely so variants like these stay additive; v1 understands only
|
|
854
|
+
`fill` / `stroke` / `subpaths`.
|
|
369
855
|
|
|
370
856
|
---
|
|
371
857
|
|
|
372
858
|
## Appendix — sample
|
|
373
859
|
|
|
374
|
-
`drawings/cauldron.
|
|
860
|
+
`drawings/cauldron.sprite` is a 16×16 multi-layer animated sprite referenced by
|
|
375
861
|
the `pig` actor in `scenes/main.scene` (via `blueprints/cauldron.scene`)
|
|
376
862
|
through the `Sprite` behavior — a working rendered example of the format in
|
|
377
863
|
this kit.
|