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.
Files changed (55) hide show
  1. package/dist/agent-prompts.js +2 -2
  2. package/dist/agent.d.ts +9 -9
  3. package/dist/agent.js +590 -589
  4. package/dist/castleJson.d.ts +6 -0
  5. package/dist/castleJson.js +10 -0
  6. package/dist/editorConfig.js +27 -3
  7. package/dist/imports.d.ts +4 -0
  8. package/dist/imports.js +59 -10
  9. package/dist/init.d.ts +3 -0
  10. package/dist/init.js +99 -87
  11. package/dist/install.d.ts +1 -1
  12. package/dist/install.js +26 -24
  13. package/dist/shell/assets/{index-CUamb8rK.js → index-DiPlPGyg.js} +2 -2
  14. package/dist/shell/index.html +1 -1
  15. package/dist/vitePlugins.js +1 -1
  16. package/kits/physics-2d/CLAUDE.md +28 -16
  17. package/kits/physics-2d/behaviors/Joints.jsx +1 -1
  18. package/kits/physics-2d/behaviors/Sprite.jsx +17 -12
  19. package/kits/physics-2d/blueprints/ball.scene +1 -1
  20. package/kits/physics-2d/blueprints/block.scene +1 -1
  21. package/kits/physics-2d/blueprints/cauldron.scene +1 -1
  22. package/kits/physics-2d/blueprints/crate.scene +1 -1
  23. package/kits/physics-2d/castle.json +11 -3
  24. package/kits/physics-2d/docs/pxart-format.md +537 -51
  25. package/kits/physics-2d/editors/PxArtEditor.jsx +1794 -65
  26. package/kits/physics-2d/editors/brushFit.js +535 -0
  27. package/kits/physics-2d/editors/brushShapes.js +140 -0
  28. package/kits/physics-2d/editors/mediaFile.js +12 -1
  29. package/kits/physics-2d/editors/pathOverlay.js +340 -0
  30. package/kits/physics-2d/editors/pathTools.js +1906 -0
  31. package/kits/physics-2d/editors/pixelCanvas.js +13 -0
  32. package/kits/physics-2d/editors/pixelEditorChrome.jsx +2 -2
  33. package/kits/physics-2d/editors/pixelGeometry.js +4 -2
  34. package/kits/physics-2d/editors/pixelInspector.jsx +410 -37
  35. package/kits/physics-2d/editors/pxArtEditorModel.js +172 -16
  36. package/kits/physics-2d/editors/pxArtTimeline.jsx +163 -43
  37. package/kits/physics-2d/editors/pxArtTimeline.module.css +31 -5
  38. package/kits/physics-2d/engine/assets.js +1 -1
  39. package/kits/physics-2d/engine/blueprint.js +3 -3
  40. package/kits/physics-2d/engine/files.js +3 -1
  41. package/kits/physics-2d/engine/liveReload.js +1 -1
  42. package/kits/physics-2d/engine/physics/jointArt.js +3 -3
  43. package/kits/physics-2d/engine/pxart.js +153 -35
  44. package/kits/physics-2d/engine/pxartPath.js +1356 -0
  45. package/kits/physics-2d/engine/pxartSmooth.js +276 -125
  46. package/kits/physics-2d/engine/ui.jsx +22 -1
  47. package/kits/physics-2d/engine/ui.module.css +36 -12
  48. package/kits/physics-2d/package-lock.json +1 -1
  49. package/kits/physics-2d/scripts/draw.mjs +7 -7
  50. package/kits/physics-2d/scripts/import-svg.mjs +1231 -0
  51. package/kits/physics-2d/scripts/svg-emission-guide.md +92 -0
  52. package/package.json +1 -1
  53. /package/kits/physics-2d/drawings/{block.pxart → block.sprite} +0 -0
  54. /package/kits/physics-2d/drawings/{cauldron.pxart → cauldron.sprite} +0 -0
  55. /package/kits/physics-2d/drawings/{joint-rope.pxart → joint-rope.sprite} +0 -0
@@ -10,7 +10,7 @@
10
10
  <link rel="icon" type="image/png" sizes="32x32" href="/__castle/ide/favicon-32x32.png" />
11
11
  <link rel="icon" type="image/png" sizes="16x16" href="/__castle/ide/favicon-16x16.png" />
12
12
  <link rel="icon" href="/__castle/ide/favicon.ico" sizes="any" />
13
- <script type="module" crossorigin src="/__castle/ide/assets/index-CUamb8rK.js"></script>
13
+ <script type="module" crossorigin src="/__castle/ide/assets/index-DiPlPGyg.js"></script>
14
14
  <link rel="stylesheet" crossorigin href="/__castle/ide/assets/index-Y6cJRRCX.css">
15
15
  </head>
16
16
  <body>
@@ -24,7 +24,7 @@ const IMPORTS_PREFIX = '@imports/';
24
24
  // own to define. This list is only the fallback for a deck where nothing
25
25
  // declares anything, kept so decks written before the declaration existed behave
26
26
  // as they did.
27
- const FALLBACK_DATA_FILE_EXTS = ['.scene', '.drawing', '.pxart'];
27
+ const FALLBACK_DATA_FILE_EXTS = ['.scene', '.drawing', '.pxart', '.sprite'];
28
28
  function dataFileExts(root) {
29
29
  const resolved = resolveFileTypes(root);
30
30
  // Declared file types with no data entries mean exactly that: a deck whose
@@ -9,7 +9,7 @@ See `## Physics` below.
9
9
 
10
10
  ## Welcome message
11
11
 
12
- Welcome to an early test of Castle's new engine, with real 2D physics! You're starting with Castle's art and scene editors. Open `cauldron.pxart` or `main.scene` to try them out, or I can start building something for you — a physics toy, a launcher game, a drag-and-fling puzzle, whatever you like.
12
+ Welcome to an early test of Castle's new engine, with real 2D physics! You're starting with Castle's art and scene editors. Open `cauldron.sprite` or `main.scene` to try them out, or I can start building something for you — a physics toy, a launcher game, a drag-and-fling puzzle, whatever you like.
13
13
 
14
14
  Do you already know what you want to make, or do you want to figure it out together?
15
15
 
@@ -21,8 +21,8 @@ Do you already know what you want to make, or do you want to figure it out toget
21
21
  - Game logic lives in `behaviors/*.jsx` classes. A behavior's `static behaviorName` must match the component key used in scene JSON.
22
22
  - Editable screens live in `scenes/*.scene` files (plain JSON). Use a separate scene file per distinct screen, and switch with `scene.loadFromFile('name.scene')`.
23
23
  - Every actor is an instance of a **blueprint** (`blueprints/*.scene`) — see `## Blueprints` below. Always author the blueprint file yourself and reference it via `"blueprint"`. Don't write inline actors (full `components`, no `blueprint` field): the editor auto-migrates each one into its own new blueprint file on open, one per actor with no dedup, which litters the deck with junk blueprints.
24
- - Real game objects and scenery should usually be editable sprites: generate `.pxart` with `npm run draw -- name`, then place it via a `Sprite` component pointing at `drawings/name.pxart`. Dynamic UI/effects stay procedural.
25
- - A deck can also hold uploaded media (the Files panel's Upload button): `Sprite` draws image files as well as `.pxart`, `Sound` and `Video` play audio and video files, and `Tone` synthesizes a note with no file at all. Point their `file` at the deck path (`assets/logo.png`).
24
+ - Real game objects and scenery should usually be editable sprites: generate `.sprite` with `npm run draw -- name`, then place it via a `Sprite` component pointing at `drawings/name.sprite`. Dynamic UI/effects stay procedural.
25
+ - A deck can also hold uploaded media (the Files panel's Upload button): `Sprite` draws image files as well as `.sprite`, `Sound` and `Video` play audio and video files, and `Tone` synthesizes a note with no file at all. Point their `file` at the deck path (`assets/logo.png`).
26
26
  - **Supported media formats are exactly**: `.png` `.jpg` `.jpeg` `.gif` `.webp` `.svg` images, `.mp3` `.wav` `.m4a` audio, `.mp4` (H.264) video. Nothing else — a published deck inlines every asset as a `data:` URI, where the browser trusts the declared type instead of sniffing the bytes, so a format that merely works while serving (`.mov` is the classic case) can be dead once published. Convert, don't improvise.
27
27
  - This kit is plain JavaScript. Use `.jsx` for files with JSX, `.js` otherwise; do not add TypeScript files or a new build step.
28
28
  - After any code, scene, or drawing edit, run `npm run restart`.
@@ -35,11 +35,11 @@ Do you already know what you want to make, or do you want to figure it out toget
35
35
  One rule wherever a file is named -- JS imports, `"blueprint"` refs, `Sprite.file`,
36
36
  a joint's `sprite`, anything a behavior invents:
37
37
 
38
- - `drawings/ship.pxart` -- a file of the deck the reference is WRITTEN IN. In this
38
+ - `drawings/ship.sprite` -- a file of the deck the reference is WRITTEN IN. In this
39
39
  deck's own files that means this deck; in a file belonging to an import, that
40
- import. So a kit's blueprint saying `drawings/cauldron.pxart` keeps meaning the
40
+ import. So a kit's blueprint saying `drawings/cauldron.sprite` keeps meaning the
41
41
  kit's drawing once the kit is imported by someone else.
42
- - `@imports/<alias>/drawings/ship.pxart` -- a file of the deck imported under
42
+ - `@imports/<alias>/drawings/ship.sprite` -- a file of the deck imported under
43
43
  `<alias>`. This is the only way to name another deck's file, so cross-deck
44
44
  references are visible as such, and it means the same thing from any file at
45
45
  any depth.
@@ -59,7 +59,7 @@ The deck is already serving when you start (`castle-web init` set that up; see `
59
59
 
60
60
  1. **Build incrementally.** Start with the smallest playable thing (one mechanic, one scene change), `npm run restart`, then add the next piece. Do NOT write the whole game in one shot.
61
61
  2. **After every edit:** `npm run restart` (no hot reload). The served page refreshes and the user sees the change.
62
- 3. **Prefer real, editable assets.** For game objects, characters, and scenery, make actual pixel-art sprites and place them as real actors in `scenes/*.scene` — not shapes drawn in code. Real assets let the creator move and re-skin things in the editor and let other creators remix the deck. Make art as `.pxart` via the `draw` command (see **Creating pixel art** below). Data-driven UI (health bars, score/text, HUD gauges) and dynamic things (bullets, particles, effects) are correctly procedural/code — don't force those into sprites.
62
+ 3. **Prefer real, editable assets.** For game objects, characters, and scenery, make actual pixel-art sprites and place them as real actors in `scenes/*.scene` — not shapes drawn in code. Real assets let the creator move and re-skin things in the editor and let other creators remix the deck. Make art as `.sprite` via the `draw` command (see **Creating pixel art** below). Data-driven UI (health bars, score/text, HUD gauges) and dynamic things (bullets, particles, effects) are correctly procedural/code — don't force those into sprites.
63
63
  4. **Separate scenes per screen.** Use a separate `scenes/*.scene` file for each distinct screen — menu/title, each level, game-over, etc. — not one mega-scene. Each stays independently editable in the editor. Switch at runtime with `scene.loadFromFile('gameover.scene')` (reads the file and transitions) on play / win / level change. NEVER `import` a `.scene` file as a module — scene files are data, not modules; use `scene.readFromFile` / `scene.loadFromFile`.
64
64
 
65
65
  Card size is **500 wide × 700 tall** (origin top-left, +y is down).
@@ -126,7 +126,7 @@ A **blueprint** is a `.scene` file under `blueprints/` whose single actor (`acto
126
126
  {
127
127
  "components": {
128
128
  "Layout": { "width": 50, "height": 25 },
129
- "Sprite": { "file": "drawings/brick.pxart" },
129
+ "Sprite": { "file": "drawings/brick.sprite" },
130
130
  "Collider": { "kind": "solid" },
131
131
  "Brick": {}
132
132
  }
@@ -145,8 +145,8 @@ Template `components` keys are behavior names (the `static behaviorName`); add a
145
145
  ## Built-in behaviors
146
146
 
147
147
  - **Layout** — `{ x, y, width, height, z?, rotation? }`. Every actor needs one. `z` orders draw (low first). `rotation` is degrees about the center.
148
- - **Sprite** — `{ file: "drawings/foo.pxart", tint?: "#rrggbbaa", playing?: true, tag?: "", mode?: 'cover'|'fit'|'stretch'|'tile', tileSize?: 50 }`. Renders the deck's 2D art into the Layout box. `file` is either a `.pxart` pixel-art sprite or an uploaded **image file** (`.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.svg`) — every prop below works the same on both, except that an image is a single still (no frames or tags) and is drawn smoothed rather than nearest-neighbour, which is what you want for a photo and not for pixel art. Draw modes: scaled up preserving the art's aspect ratio to fill the whole box, cropping whatever overflows a mismatched box (`mode: 'cover'`, the default -- CSS object-fit: cover), scaled preserving the art's aspect ratio and centered so the whole sprite stays visible, letterboxing a mismatched box instead of cropping (`mode: 'fit'` -- CSS object-fit: contain), scaled to fill the box exactly and distorting the art when the aspect ratios don't match (`mode: 'stretch'`), or repeated (`mode: 'tile'`). `tint` multiplies; use white (`#ffffffff`) or omit for the original colors. Animated sprites play automatically; set `playing: false` to hold the first frame, or `tag` to play a named animation tag. Tile mode repeats the art at a fixed cell size (`tileSize` card units tall, width scaled by the art's aspect) instead of stretching it across the Layout box.
149
- - **Missing sprites fall back to a placeholder.** If `file` names a sprite that doesn't exist yet, it renders the fallback `drawings/cauldron.pxart` — so you can give an actor its REAL intended sprite name (`drawings/paddle.pxart`) right away and it shows the placeholder until that file is created. Reference real names from the start; don't wait for the art.
148
+ - **Sprite** — `{ file: "drawings/foo.sprite", tint?: "#rrggbbaa", playing?: true, tag?: "", mode?: 'cover'|'fit'|'stretch'|'tile', tileSize?: 50 }`. Renders the deck's 2D art into the Layout box. `file` is either a `.sprite` pixel-art sprite or an uploaded **image file** (`.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.svg`) — every prop below works the same on both, except that an image is a single still (no frames or tags) and is drawn smoothed rather than nearest-neighbour, which is what you want for a photo and not for pixel art. Draw modes: scaled up preserving the art's aspect ratio to fill the whole box, cropping whatever overflows a mismatched box (`mode: 'cover'`, the default -- CSS object-fit: cover), scaled preserving the art's aspect ratio and centered so the whole sprite stays visible, letterboxing a mismatched box instead of cropping (`mode: 'fit'` -- CSS object-fit: contain), scaled to fill the box exactly and distorting the art when the aspect ratios don't match (`mode: 'stretch'`), or repeated (`mode: 'tile'`). `tint` multiplies; use white (`#ffffffff`) or omit for the original colors. Animated sprites play automatically; set `playing: false` to hold the first frame, or `tag` to play a named animation tag. Tile mode repeats the art at a fixed cell size (`tileSize` card units tall, width scaled by the art's aspect) instead of stretching it across the Layout box.
149
+ - **Missing sprites fall back to a placeholder.** If `file` names a sprite that doesn't exist yet, it renders the fallback `drawings/cauldron.sprite` — so you can give an actor its REAL intended sprite name (`drawings/paddle.sprite`) right away and it shows the placeholder until that file is created. Reference real names from the start; don't wait for the art.
150
150
  - **Fill the box, don't distort.** The default `mode: 'cover'` fills the Layout box with the art undistorted, cropping whatever overflows when the box aspect doesn't match the art's — so treat the Layout box like an image frame in a design tool: size it to frame what matters and keep the sprite's important content toward the center, since the edges may be cropped. Keeping an actor's Layout aspect ratio close to its sprite's native aspect ratio minimizes how much gets cropped. When the whole sprite must stay visible (nothing is safe to crop), use `mode: 'fit'`, which preserves the art and letterboxes the mismatched box instead. Avoid `mode: 'stretch'` for pixel art — filling a long/tall box by distorting wrecks the pixels (a brick sprite stretched into a wall ruins the bricks). For long surfaces (walls, floors, platforms), use `mode: 'tile'` on one actor to repeat the art at a fixed cell size instead of stretching or cropping a single sprite.
151
151
  - **Collider** — `{ kind: 'solid'|'pickup', mode?: 'auto'|'manual', width, height, offsetX?, offsetY?, debug? }`. Just a rect; `offsetX`/`offsetY` nudge the collider from its default position (so `0,0` is the default). `mode: 'auto'` (the default) fits the rect to the actor's Sprite's opaque pixels, mapped through the Sprite's draw mode (`fit`'s letterboxed dest rect, `stretch`'s full Layout box, `cover`'s filled box clipped to the Layout box; `tile` mode and a missing/unresolved Sprite use the whole Layout box instead). `width`/`height` only apply in `mode: 'manual'` (a rect of that size, centered in the Layout box); they're ignored in `auto`. The framework does NOT auto-resolve collisions for you. Use it as data:
152
152
 
@@ -328,7 +328,7 @@ anchors.
328
328
  prototyping; you see your joints working.
329
329
  - `hidden` — nothing drawn in play (the joint is pure mechanics). It still shows
330
330
  as a faint line in the *editor* so you don't lose track of it.
331
- - `sprite` — draw art along the joint. Set `sprite` to a `.pxart`
331
+ - `sprite` — draw art along the joint. Set `sprite` to a `.sprite`
332
332
  (defaults to a built-in rope), `thickness` (px), and `fit`: `tile` (repeat a
333
333
  segment — rope, chain) or `stretch` (one image end-to-end — stick, beam). The
334
334
  art rotates to the joint's angle and scales to its live length, so a rope
@@ -390,9 +390,9 @@ That last seam is the generic one: any kit can add a per-frame system by droppin
390
390
  a file in `systems/` that exports `installSystem(runtime)`, with no engine edits.
391
391
  Physics is just this kit's use of it.
392
392
 
393
- ## Creating pixel art (`.pxart`)
393
+ ## Creating pixel art (`.sprite`)
394
394
 
395
- Don't hand-write pixel grids. Generate sprites with the `draw` command: **emit a terse svg-rect** — a tiny `<svg viewBox="0 0 16 16">` with **one `<rect>` per pixel** — and pipe it to `npm run draw`. It quantizes every fill to the agent palette and writes a single-frame `drawings/<name>.pxart`.
395
+ Don't hand-write pixel grids. Generate sprites with the `draw` command: **emit a terse svg-rect** — a tiny `<svg viewBox="0 0 16 16">` with **one `<rect>` per pixel** — and pipe it to `npm run draw`. It quantizes every fill to the agent palette and writes a single-frame `drawings/<name>.sprite`.
396
396
 
397
397
  - **Resolution is 16×16.** Use `viewBox="0 0 16 16"` and **one 1×1 `<rect>` per filled pixel** (`width="1" height="1"` at integer `x`/`y`). Leave background pixels out (omitted = transparent). Keep every rect 1×1 — the decoder infers cell size from the smallest rect, so mixing in larger blocks shrinks the whole grid.
398
398
  - **Palette is the agent 16-color subset** (a fixed subset of the editor's full Endesga-64 painting palette) — every `fill` must be one of:
@@ -406,10 +406,22 @@ Don't hand-write pixel grids. Generate sprites with the `draw` command: **emit a
406
406
  printf '<svg viewBox="0 0 16 16"><rect x="7" y="4" width="1" height="1" fill="#ffa214"/><rect x="8" y="4" width="1" height="1" fill="#ffa214"/><rect x="7" y="5" width="1" height="1" fill="#0098dc"/><rect x="8" y="5" width="1" height="1" fill="#0098dc"/></svg>' | npm run draw -- ship
407
407
  ```
408
408
 
409
- writes `drawings/ship.pxart`. You can also pass `--from file.svg` instead of stdin. An undecodable svg (no usable rects) errors with a nonzero exit and writes nothing.
410
- - **After writing, `npm run restart`.** Newly created files aren't in the kit's static glob until a restart; until then the missing-sprite fallback renders the placeholder (so an actor pointing at a not-yet-drawn `.pxart` is still playable).
409
+ writes `drawings/ship.sprite`. You can also pass `--from file.svg` instead of stdin. An undecodable svg (no usable rects) errors with a nonzero exit and writes nothing.
410
+ - **After writing, `npm run restart`.** Newly created files aren't in the kit's static glob until a restart; until then the missing-sprite fallback renders the placeholder (so an actor pointing at a not-yet-drawn `.sprite` is still playable).
411
411
 
412
- Point a `Sprite` component's `file` at the generated `drawings/<name>.pxart` to put the art on an actor.
412
+ Point a `Sprite` component's `file` at the generated `drawings/<name>.sprite` to put the art on an actor.
413
+
414
+ Older decks may still use the `.pxart` extension for the same format; the editor opens and saves both, but **create new art as `.sprite`**.
415
+
416
+ ## Path layers (vector source in `.sprite`)
417
+
418
+ Layers come in two kinds. A **pixel** layer is the grid of palette keys described above. A **path** layer also carries vector source: an ordered list of shapes, each a fill plus an optional stroke over subpaths of cubic segments. Shapes composite in painter's order, so a later shape overwrites an earlier one's pixels, and a shape's own multi-subpath fill resolves even-odd (which is how holes are punched).
419
+
420
+ Only **closed** subpaths bound a fill; an open one only strokes. That is how a shape draws its internal divisions — one outline around the silhouette plus an open subpath per division — rather than as two abutting shapes, which would carry two fill keys to keep in step and a seam where each strokes its own side of the shared border. A division that lies exactly on the outline is redundant, and the editor drops it on the next edit, so draw one only where it departs from the outline.
421
+
422
+ The editor always bakes a path layer's shapes down into the cell's `grid`, so **pixels stay the truth** for the runtime and for anything that doesn't know about paths. Path layers are fully editable in the app — pen, select, bend, punch, flip/rotate, z-order, recolor — and pixel layers behave exactly as they did. Converting a layer's kind is lossy in both directions: Pixel → Path throws away the pixels, Path → Pixel throws away the vector source and keeps the baked grid.
423
+
424
+ To generate path art, emit an svg of **shapes** rather than per-pixel rects and pipe it to `node scripts/import-svg.mjs <name>`; `scripts/svg-emission-guide.md` covers what the importer accepts. `docs/pxart-format.md` is normative for the on-disk form, the half-cell lattice path points are authored onto (guidance for placement, not a storage rule — coordinates are plain floats), the bake, and the sprite's finish fields.
413
425
 
414
426
  ## SceneRuntime API (what `scene` exposes to behaviors)
415
427
 
@@ -72,7 +72,7 @@ export class Joints {
72
72
 
73
73
  static Inspector({ component, setComponent, override, beginPick, pickActive, files }) {
74
74
  const list = Array.isArray(component.list) ? component.list : [];
75
- const spriteFiles = Object.keys(files ?? {}).filter((f) => f.endsWith('.pxart'));
75
+ const spriteFiles = Object.keys(files ?? {}).filter((f) => f.endsWith('.sprite') || f.endsWith('.pxart'));
76
76
  // Which entry's target-pick is live. Combined with the editor's global
77
77
  // `pickActive` so a cancel (Esc/empty click) clears every entry's state.
78
78
  const [pickingIndex, setPickingIndex] = useState(null);
@@ -1,7 +1,8 @@
1
1
  import { resolveDeckFile } from 'castle-web-sdk';
2
2
  import React from 'react';
3
3
  import { artFrameCount, isImageArt, renderArtFrame } from '../engine/art';
4
- import { renderSmoothSpriteFrame } from '../engine/pxartSmooth';
4
+ import { isVectorRenderer } from '../engine/pxart';
5
+ import { renderSmoothCompositeFrame, renderSmoothSpriteFrame } from '../engine/pxartSmooth';
5
6
  import { spriteDestRect } from '../engine/spriteGeometry';
6
7
  import { Panel, SelectField } from '../engine/ui';
7
8
  import { AutoFields, overrideProps } from '../engine/autoInspector';
@@ -15,15 +16,15 @@ import { parseTint, tintCanvas } from './tint';
15
16
  // It looks up the art, composites the current animation frame into a cached
16
17
  // offscreen canvas (tinted), and blits it into the actor's
17
18
  // Layout box with smoothing disabled — unless the FILE itself opts into a
18
- // `cornerRadius` (a property of the .pxart asset, not a Sprite prop) greater
19
- // than 0, in which case the cached canvas holds a corner-rounded vector
20
- // render instead and gets blitted with smoothing on.
19
+ // non-default finish (vector renderer, or `cornerRadius` > 0 on a .pxart
20
+ // asset — not a Sprite prop), in which case the cached canvas holds a
21
+ // supersampled render and gets blitted with smoothing on.
21
22
  //
22
23
  // Animation plays in `update` and is stored on `actor.runtime.spriteAnim` (the
23
24
  // behavior instance is recreated every frame, but the actor object persists in
24
25
  // the runtime). In edit-mode previews `update` never runs, so the actor holds
25
26
  // frame 0.
26
- const FALLBACK_FILE = 'drawings/cauldron.pxart';
27
+ const FALLBACK_FILE = 'drawings/cauldron.sprite';
27
28
 
28
29
  export class Sprite {
29
30
  static behaviorName = 'Sprite';
@@ -78,13 +79,13 @@ export class Sprite {
78
79
  // untinted image, so size comes from the art rather than the element.
79
80
  const artSize = sprite.resolution;
80
81
  const prevSmoothing = ctx.imageSmoothingEnabled;
81
- // File-level `cornerRadius > 0` sprites are pre-rendered as supersampled,
82
- // corner-rounded vector fills (see engine/pxartSmooth.js); blit those with
83
- // smoothing on so downscaling to the Layout box stays antialiased. Plain
84
- // pixel sprites (`cornerRadius === 0`) keep nearest-neighbor blitting.
85
- // Image files are resampled too -- nearest neighbour is right for pixel art
86
- // and wrong for a photograph.
87
- const smoothing = sprite.cornerRadius > 0 || isImageArt(sprite);
82
+ // Sprites with a non-default finish are pre-rendered supersampled (see
83
+ // engine/pxartSmooth.js); blit those with smoothing on so downscaling to
84
+ // the Layout box stays antialiased. Plain pixel sprites (grid renderer at
85
+ // radius 0) keep nearest-neighbor blitting. Image files are resampled too
86
+ // -- nearest neighbour is right for pixel art and wrong for a photograph.
87
+ const smoothing =
88
+ isVectorRenderer(sprite.renderer) || sprite.cornerRadius > 0 || isImageArt(sprite);
88
89
 
89
90
  // Snap the destination rect to whole device pixels so tiles that abut
90
91
  // exactly in card units (e.g. adjacent 100-unit-wide tiles) land on the
@@ -256,6 +257,10 @@ const MAX_CACHE_LONG_SIDE = 2048;
256
257
  function frameSource(sprite, frameIndex) {
257
258
  if (isImageArt(sprite)) return sprite.ready ? sprite.image : null;
258
259
  const canvas = document.createElement('canvas');
260
+ if (isVectorRenderer(sprite.renderer)) {
261
+ renderSmoothCompositeFrame(sprite, frameIndex, canvas);
262
+ return canvas;
263
+ }
259
264
  if (sprite.cornerRadius > 0) {
260
265
  renderSmoothSpriteFrame(sprite, frameIndex, canvas, { cornerRadius: sprite.cornerRadius });
261
266
  return canvas;
@@ -4,7 +4,7 @@
4
4
  {
5
5
  "components": {
6
6
  "Layout": { "width": 44, "height": 44, "z": 2 },
7
- "Sprite": { "file": "drawings/cauldron.pxart", "mode": "fit" },
7
+ "Sprite": { "file": "drawings/cauldron.sprite", "mode": "fit" },
8
8
  "Collider": { "shape": "circle", "width": 40, "height": 40, "bounciness": 0.6, "friction": 0.05 },
9
9
  "RigidBody": { "bodyType": "dynamic", "gravityScale": 1, "drag": 0.005 },
10
10
  "Slingshot": { "speed": 0.1, "maxDrag": 200 }
@@ -4,7 +4,7 @@
4
4
  {
5
5
  "components": {
6
6
  "Layout": { "width": 100, "height": 40, "z": 1 },
7
- "Sprite": { "file": "drawings/block.pxart", "mode": "tile", "tileSize": 20 },
7
+ "Sprite": { "file": "drawings/block.sprite", "mode": "tile", "tileSize": 20 },
8
8
  "Collider": { "bounciness": 0.4, "friction": 0.2 }
9
9
  }
10
10
  }
@@ -9,7 +9,7 @@
9
9
  "z": 1
10
10
  },
11
11
  "Sprite": {
12
- "file": "drawings/cauldron.pxart"
12
+ "file": "drawings/cauldron.sprite"
13
13
  },
14
14
  "Collider": {}
15
15
  }
@@ -4,7 +4,7 @@
4
4
  {
5
5
  "components": {
6
6
  "Layout": { "width": 48, "height": 48, "z": 2 },
7
- "Sprite": { "file": "drawings/block.pxart", "mode": "fit" },
7
+ "Sprite": { "file": "drawings/block.sprite", "mode": "fit" },
8
8
  "Collider": { "shape": "box", "width": 46, "height": 46, "bounciness": 0.2, "friction": 0.3 },
9
9
  "RigidBody": { "bodyType": "dynamic", "gravityScale": 1, "drag": 0.02 },
10
10
  "Draggable": {}
@@ -34,10 +34,17 @@
34
34
  "editor": "kit",
35
35
  "data": true
36
36
  },
37
+ {
38
+ "ext": ".sprite",
39
+ "label": "Sprite",
40
+ "new": "New sprite",
41
+ "icon": "layer-group",
42
+ "editor": "kit",
43
+ "data": true
44
+ },
37
45
  {
38
46
  "ext": ".pxart",
39
- "label": "Pixel Art",
40
- "new": "New drawing",
47
+ "label": "Sprite",
41
48
  "icon": "layer-group",
42
49
  "editor": "kit",
43
50
  "data": true
@@ -113,7 +120,8 @@
113
120
  },
114
121
  "deckId": "ckRZGFW4iPrx",
115
122
  "cardId": "_oBFbAW6DxsO",
123
+ "main": "main.jsx",
116
124
  "autoUpdateWhenImported": true,
117
125
  "title": "physics-2d",
118
- "publishedVersion": "2026-08-06T19:53:55.080Z"
126
+ "publishedVersion": "2026-08-08T19:00:43.040Z"
119
127
  }