castle-web-cli 0.4.128 → 0.4.130

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 (157) hide show
  1. package/dist/agent-prompts.js +4 -4
  2. package/dist/filesChanged.d.ts +2 -0
  3. package/dist/filesChanged.js +6 -2
  4. package/dist/ide.js +15 -4
  5. package/dist/importBrowse.js +1 -0
  6. package/dist/index.js +10 -4
  7. package/dist/init.js +63 -15
  8. package/dist/native/loop.js +1 -1
  9. package/dist/native/tools.js +1 -1
  10. package/dist/serve.d.ts +1 -0
  11. package/dist/serve.js +20 -1
  12. package/dist/shell/assets/index-BpOLUyVO.js +441 -0
  13. package/dist/shell/assets/{index-CAq6f9B5.css → index-D6K-0YDB.css} +1 -1
  14. package/dist/shell/index.html +2 -2
  15. package/kits/physics-2d/CLAUDE.md +40 -23
  16. package/kits/physics-2d/behaviors/Joints.jsx +4 -3
  17. package/kits/physics-2d/behaviors/SoundPlayer.jsx +266 -0
  18. package/kits/physics-2d/behaviors/Sprite.jsx +3 -3
  19. package/kits/physics-2d/behaviors/Video.jsx +4 -3
  20. package/kits/physics-2d/castle.json +1 -1
  21. package/kits/physics-2d/editors/PlayOnly.jsx +15 -6
  22. package/kits/physics-2d/editors/PxArtEditor.jsx +95 -31
  23. package/kits/physics-2d/editors/SceneEditor.jsx +5 -5
  24. package/kits/physics-2d/editors/SingleEditor.jsx +37 -8
  25. package/kits/physics-2d/editors/StyleEditor.jsx +7 -4
  26. package/kits/physics-2d/editors/pixelEditorChrome.jsx +7 -7
  27. package/kits/physics-2d/editors/pixelInspector.jsx +13 -7
  28. package/kits/physics-2d/editors/pxArtTimeline.jsx +29 -11
  29. package/kits/physics-2d/editors/pxArtTimeline.module.css +11 -0
  30. package/kits/physics-2d/engine/audioContext.js +41 -0
  31. package/kits/physics-2d/engine/autoInspector.jsx +50 -33
  32. package/kits/physics-2d/engine/blueprint.js +15 -3
  33. package/kits/physics-2d/engine/liveReload.js +33 -21
  34. package/kits/physics-2d/engine/media.js +29 -39
  35. package/kits/physics-2d/engine/sound.js +316 -0
  36. package/kits/physics-2d/engine/systemRegistry.js +1 -1
  37. package/kits/physics-2d/engine/ui.jsx +274 -5
  38. package/kits/physics-2d/engine/ui.module.css +137 -16
  39. package/kits/physics-2d/scripts/testsounds.mjs +84 -0
  40. package/kits/physics-2d/systems/media.js +31 -15
  41. package/kits/physics-3d/CLAUDE.md +109 -0
  42. package/kits/physics-3d/behaviors/Body.jsx +51 -0
  43. package/kits/physics-3d/behaviors/Door.jsx +31 -0
  44. package/kits/physics-3d/behaviors/Lookable.jsx +11 -0
  45. package/kits/physics-3d/behaviors/Model.jsx +14 -0
  46. package/kits/physics-3d/behaviors/Pickup.jsx +39 -0
  47. package/kits/physics-3d/behaviors/Player.jsx +172 -0
  48. package/kits/physics-3d/behaviors/Portal.jsx +19 -0
  49. package/kits/physics-3d/behaviors/Shape.jsx +75 -0
  50. package/kits/physics-3d/behaviors/Solid.jsx +11 -0
  51. package/kits/physics-3d/behaviors/Transform.jsx +24 -0
  52. package/kits/physics-3d/blueprints/barrel.scene +15 -0
  53. package/kits/physics-3d/blueprints/crate.scene +29 -0
  54. package/kits/physics-3d/blueprints/door.scene +35 -0
  55. package/kits/physics-3d/blueprints/gem.scene +26 -0
  56. package/kits/physics-3d/blueprints/pillar.scene +25 -0
  57. package/kits/physics-3d/blueprints/platform.scene +23 -0
  58. package/kits/physics-3d/blueprints/player.scene +29 -0
  59. package/kits/physics-3d/blueprints/portal.scene +21 -0
  60. package/kits/physics-3d/blueprints/rock.scene +14 -0
  61. package/kits/physics-3d/blueprints/statue.scene +26 -0
  62. package/kits/physics-3d/blueprints/tree.scene +14 -0
  63. package/kits/physics-3d/blueprints/wall.scene +26 -0
  64. package/kits/{basic-2d → physics-3d}/castle.json +23 -5
  65. package/kits/physics-3d/docs/pxmodel-format.md +111 -0
  66. package/kits/physics-3d/drawings/crate.pxart +26 -0
  67. package/kits/physics-3d/drawings/door-edge.pxart +26 -0
  68. package/kits/physics-3d/drawings/door.pxart +27 -0
  69. package/kits/physics-3d/drawings/face.pxart +26 -0
  70. package/kits/physics-3d/drawings/floor.pxart +27 -0
  71. package/kits/physics-3d/drawings/platform.pxart +27 -0
  72. package/kits/physics-3d/drawings/statue.pxart +27 -0
  73. package/kits/physics-3d/drawings/wall.pxart +27 -0
  74. package/kits/physics-3d/engine3d/PlayOnly3D.jsx +32 -0
  75. package/kits/physics-3d/engine3d/PxModelEditor.jsx +619 -0
  76. package/kits/physics-3d/engine3d/Scene3DEditor.jsx +618 -0
  77. package/kits/physics-3d/engine3d/Scene3DPlayer.jsx +309 -0
  78. package/kits/physics-3d/engine3d/editor3dData.js +114 -0
  79. package/kits/physics-3d/engine3d/editor3dInspector.jsx +404 -0
  80. package/kits/physics-3d/engine3d/editorChrome.jsx +218 -0
  81. package/kits/physics-3d/engine3d/editorWorld.js +249 -0
  82. package/kits/physics-3d/engine3d/materials.js +174 -0
  83. package/kits/physics-3d/engine3d/meshops.js +123 -0
  84. package/kits/physics-3d/engine3d/modelEditorWorld.js +439 -0
  85. package/kits/physics-3d/engine3d/modelFiles.js +139 -0
  86. package/kits/physics-3d/engine3d/pxmodel.js +265 -0
  87. package/kits/physics-3d/engine3d/thumbnails.js +137 -0
  88. package/kits/physics-3d/engine3d/world3d.js +216 -0
  89. package/kits/{basic-2d → physics-3d}/eslint.config.js +10 -25
  90. package/kits/physics-3d/index.html +20 -0
  91. package/kits/physics-3d/main.jsx +30 -0
  92. package/kits/physics-3d/models/barrel.pxmodel +10 -0
  93. package/kits/physics-3d/models/player.pxmodel +222 -0
  94. package/kits/physics-3d/models/rock.pxmodel +30 -0
  95. package/kits/physics-3d/models/tree.pxmodel +10 -0
  96. package/kits/{basic-2d → physics-3d}/package-lock.json +140 -119
  97. package/kits/{basic-2d → physics-3d}/package.json +11 -9
  98. package/kits/{basic-2d → physics-3d}/pnpm-lock.yaml +162 -137
  99. package/kits/physics-3d/scenes/main.scene +414 -0
  100. package/kits/physics-3d/scenes/model-lab.scene +127 -0
  101. package/kits/physics-3d/systems/physics3d.js +356 -0
  102. package/package.json +5 -2
  103. package/dist/shell/assets/index-DKu9ejyh.js +0 -436
  104. package/kits/basic-2d/CLAUDE.md +0 -221
  105. package/kits/basic-2d/behaviors/Camera.jsx +0 -43
  106. package/kits/basic-2d/behaviors/Collider.jsx +0 -213
  107. package/kits/basic-2d/behaviors/Layout.jsx +0 -53
  108. package/kits/basic-2d/behaviors/Sprite.jsx +0 -357
  109. package/kits/basic-2d/behaviors/tint.js +0 -47
  110. package/kits/basic-2d/blueprints/cauldron.scene +0 -20
  111. package/kits/basic-2d/docs/pxart-format.md +0 -377
  112. package/kits/basic-2d/drawings/cauldron.pxart +0 -113
  113. package/kits/basic-2d/editors/BlueprintLibrary.jsx +0 -270
  114. package/kits/basic-2d/editors/ErrorBoundary.jsx +0 -59
  115. package/kits/basic-2d/editors/PlayOnly.jsx +0 -31
  116. package/kits/basic-2d/editors/PxArtEditor.jsx +0 -1092
  117. package/kits/basic-2d/editors/SceneEditor.jsx +0 -1780
  118. package/kits/basic-2d/editors/SelectionOverlay.jsx +0 -909
  119. package/kits/basic-2d/editors/SingleEditor.jsx +0 -122
  120. package/kits/basic-2d/editors/behaviorRegistry.js +0 -34
  121. package/kits/basic-2d/editors/editorHistory.js +0 -157
  122. package/kits/basic-2d/editors/inspectorSheet.js +0 -13
  123. package/kits/basic-2d/editors/pixelCanvas.js +0 -11
  124. package/kits/basic-2d/editors/pixelEditorChrome.jsx +0 -74
  125. package/kits/basic-2d/editors/pixelGeometry.js +0 -140
  126. package/kits/basic-2d/editors/pixelInspector.jsx +0 -633
  127. package/kits/basic-2d/editors/pxArtEditorModel.js +0 -732
  128. package/kits/basic-2d/editors/pxArtPlayback.js +0 -92
  129. package/kits/basic-2d/editors/pxArtTimeline.jsx +0 -752
  130. package/kits/basic-2d/editors/pxArtTimeline.module.css +0 -506
  131. package/kits/basic-2d/editors/pxArtTools.js +0 -232
  132. package/kits/basic-2d/editors/useArtboardFit.js +0 -105
  133. package/kits/basic-2d/engine/ScenePlayer.jsx +0 -209
  134. package/kits/basic-2d/engine/SceneUI.jsx +0 -59
  135. package/kits/basic-2d/engine/assets.js +0 -15
  136. package/kits/basic-2d/engine/autoInspector.jsx +0 -70
  137. package/kits/basic-2d/engine/behaviorExtensions.js +0 -32
  138. package/kits/basic-2d/engine/blueprint.js +0 -557
  139. package/kits/basic-2d/engine/collider.js +0 -200
  140. package/kits/basic-2d/engine/files.js +0 -141
  141. package/kits/basic-2d/engine/liveReload.js +0 -88
  142. package/kits/basic-2d/engine/pxart.js +0 -1032
  143. package/kits/basic-2d/engine/pxartSmooth.js +0 -222
  144. package/kits/basic-2d/engine/scene.js +0 -696
  145. package/kits/basic-2d/engine/spriteGeometry.js +0 -32
  146. package/kits/basic-2d/engine/systemRegistry.js +0 -16
  147. package/kits/basic-2d/engine/ui.jsx +0 -695
  148. package/kits/basic-2d/engine/ui.module.css +0 -2287
  149. package/kits/basic-2d/index.html +0 -24
  150. package/kits/basic-2d/main.jsx +0 -24
  151. package/kits/basic-2d/scenes/main.scene +0 -16
  152. package/kits/basic-2d/scripts/draw.mjs +0 -121
  153. package/kits/physics-2d/behaviors/Sound.jsx +0 -163
  154. package/kits/physics-2d/behaviors/Tone.jsx +0 -95
  155. package/kits/physics-2d/engine/tone.js +0 -112
  156. /package/kits/{basic-2d → physics-3d}/.prettierrc +0 -0
  157. /package/kits/{basic-2d → physics-3d}/vite.config.js +0 -0
@@ -1,357 +0,0 @@
1
- import { resolveDeckFile } from 'castle-web-sdk';
2
- import React from 'react';
3
- import { frameCount, renderSpriteFrame } from '../engine/pxart';
4
- import { renderSmoothSpriteFrame } from '../engine/pxartSmooth';
5
- import { spriteDestRect } from '../engine/spriteGeometry';
6
- import { Panel, SelectField } from '../engine/ui';
7
- import { AutoFields, overrideProps } from '../engine/autoInspector';
8
- import { parseTint, tintImageData } from './tint';
9
-
10
- // Runtime behavior for the pixel-art Sprite format (.pxart). It looks up the
11
- // parsed Sprite from the scene's sprite map, composites the current animation
12
- // frame into a cached offscreen canvas (tinted), and blits it into the actor's
13
- // Layout box with smoothing disabled — unless the FILE itself opts into a
14
- // `cornerRadius` (a property of the .pxart asset, not a Sprite prop) greater
15
- // than 0, in which case the cached canvas holds a corner-rounded vector
16
- // render instead and gets blitted with smoothing on.
17
- //
18
- // Animation plays in `update` and is stored on `actor.runtime.spriteAnim` (the
19
- // behavior instance is recreated every frame, but the actor object persists in
20
- // the runtime). In edit-mode previews `update` never runs, so the actor holds
21
- // frame 0.
22
- const FALLBACK_FILE = 'drawings/cauldron.pxart';
23
-
24
- export class Sprite {
25
- static behaviorName = 'Sprite';
26
-
27
- static defaultProps = {
28
- file: FALLBACK_FILE,
29
- tint: '#ffffffff',
30
- playing: true,
31
- tag: '',
32
- mode: 'cover',
33
- tileSize: 50,
34
- };
35
-
36
- constructor(props) {
37
- this.props = props;
38
- }
39
-
40
- resolveSprite(scene) {
41
- // `drawings/x.pxart` is this deck's; `@imports/<alias>/drawings/x.pxart` is
42
- // an import's. (A value inherited from an imported blueprint arrives already
43
- // resolved against that blueprint's deck -- see engine/blueprint.js.)
44
- const file = resolveDeckFile(this.props.file);
45
- return scene.sprites?.[file] ?? scene.sprites?.[FALLBACK_FILE] ?? null;
46
- }
47
-
48
- update(actor, scene, dt) {
49
- if (!actor.runtime || actor.runtime.collected) return;
50
- const sprite = this.resolveSprite(scene);
51
- if (!sprite) return;
52
- const tag = resolveTag(sprite, this.props.tag);
53
- const sequence = frameSequence(sprite, tag);
54
- const state = ensureAnimState(actor, sprite, this.props, sequence);
55
- if (!this.props.playing) {
56
- resetAnimState(state, sequence);
57
- state.frameIndex = 0;
58
- return;
59
- }
60
- advanceAnim(state, sprite, sequence, tag, dt);
61
- }
62
-
63
- draw(actor, scene, ctx) {
64
- if (actor.runtime?.collected) return;
65
- const layout = actor.components.Layout;
66
- if (!layout) return;
67
- const sprite = this.resolveSprite(scene);
68
- if (!sprite) return;
69
- const total = frameCount(sprite);
70
- const frameIndex = Math.min(Math.max(actor.runtime?.spriteAnim?.frameIndex ?? 0, 0), total - 1);
71
- const canvas = getSpriteCanvas(sprite, frameIndex, this.props.tint);
72
- const prevSmoothing = ctx.imageSmoothingEnabled;
73
- // File-level `cornerRadius > 0` sprites are pre-rendered as supersampled,
74
- // corner-rounded vector fills (see engine/pxartSmooth.js); blit those with
75
- // smoothing on so downscaling to the Layout box stays antialiased. Plain
76
- // pixel sprites (`cornerRadius === 0`) keep nearest-neighbor blitting.
77
- const smoothing = sprite.cornerRadius > 0;
78
-
79
- // Snap the destination rect to whole device pixels so tiles that abut
80
- // exactly in card units (e.g. adjacent 100-unit-wide tiles) land on the
81
- // same device-pixel edge instead of each getting its own antialiased
82
- // partial-coverage edge, which otherwise leaves faint hairline seams
83
- // between them. Rounding each of the four device-space coordinates
84
- // independently (rather than rounding position+size) is what makes two
85
- // neighbors sharing a card-unit edge compute and round to the identical
86
- // device pixel. Skipped when the ctx is rotated/skewed (nonzero b or c,
87
- // set by the runtime for `Layout.rotation`), since a rotated rect has no
88
- // axis-aligned device edges to snap to.
89
- const transform = ctx.getTransform();
90
- const tiling = this.props.mode === 'tile';
91
- // `cover` scales the art to fill the box preserving aspect ratio, so `dest`
92
- // overflows the Layout box and the overflow must be clipped to the box.
93
- const covering = this.props.mode === 'cover';
94
- // Everything downstream (snapping, tiling's clip box) works off `dest`: the
95
- // Layout box itself for every mode except `fit`/`cover`, which resize to the
96
- // art's own aspect ratio (see `spriteDestRect`).
97
- const dest = spriteDestRect(this.props.mode, layout, canvas);
98
- if (transform.b === 0 && transform.c === 0) {
99
- const x0 = dest.x * transform.a + transform.e;
100
- const y0 = dest.y * transform.d + transform.f;
101
- const x1 = (dest.x + dest.width) * transform.a + transform.e;
102
- const y1 = (dest.y + dest.height) * transform.d + transform.f;
103
- const rx0 = Math.round(x0);
104
- const ry0 = Math.round(y0);
105
- const rx1 = Math.round(x1);
106
- const ry1 = Math.round(y1);
107
- ctx.save();
108
- ctx.setTransform(1, 0, 0, 1, 0, 0);
109
- ctx.imageSmoothingEnabled = smoothing;
110
- if (tiling) {
111
- drawTilesSnapped(ctx, canvas, this.props.tileSize, dest, transform, x0, y0, rx0, ry0, rx1, ry1);
112
- } else {
113
- // Clip cover's overflow to the snapped Layout box (undone by restore).
114
- if (covering) {
115
- const lx0 = Math.round(layout.x * transform.a + transform.e);
116
- const ly0 = Math.round(layout.y * transform.d + transform.f);
117
- const lx1 = Math.round((layout.x + layout.width) * transform.a + transform.e);
118
- const ly1 = Math.round((layout.y + layout.height) * transform.d + transform.f);
119
- ctx.beginPath();
120
- ctx.rect(lx0, ly0, lx1 - lx0, ly1 - ly0);
121
- ctx.clip();
122
- }
123
- ctx.drawImage(canvas, rx0, ry0, rx1 - rx0, ry1 - ry0);
124
- }
125
- ctx.restore();
126
- } else {
127
- ctx.imageSmoothingEnabled = smoothing;
128
- if (tiling) {
129
- drawTilesUnsnapped(ctx, canvas, this.props.tileSize, dest);
130
- } else if (covering) {
131
- // Rotated: clip cover's overflow to the Layout box in card units.
132
- ctx.save();
133
- ctx.beginPath();
134
- ctx.rect(layout.x, layout.y, layout.width, layout.height);
135
- ctx.clip();
136
- ctx.drawImage(canvas, dest.x, dest.y, dest.width, dest.height);
137
- ctx.restore();
138
- } else {
139
- ctx.drawImage(canvas, dest.x, dest.y, dest.width, dest.height);
140
- }
141
- ctx.imageSmoothingEnabled = prevSmoothing;
142
- }
143
- }
144
-
145
- static Inspector({ component, setComponent, files, override }) {
146
- const spriteFiles = getSpriteFiles(files);
147
- return (
148
- <Panel title="Sprite" overridden={override?.anyOverridden()}>
149
- <SelectField
150
- label="File"
151
- value={component.file}
152
- onChange={(file) => setComponent({ file })}
153
- options={spriteFiles}
154
- {...overrideProps(override, 'file')}
155
- />
156
- <SelectField
157
- label="Mode"
158
- value={component.mode}
159
- onChange={(mode) => setComponent({ mode })}
160
- options={['stretch', 'fit', 'cover', 'tile']}
161
- {...overrideProps(override, 'mode')}
162
- />
163
- <AutoFields
164
- defaultProps={Sprite.defaultProps}
165
- component={component}
166
- setComponent={setComponent}
167
- only={['playing', 'tint', 'tileSize']}
168
- override={override}
169
- />
170
- </Panel>
171
- );
172
- }
173
- }
174
-
175
- // Guard against non-finite or non-positive tileSize props (e.g. cleared
176
- // inspector fields), falling back to one 50-unit grid cell.
177
- function validTileSize(value) {
178
- return Number.isFinite(value) && value > 0 ? value : 50;
179
- }
180
-
181
- // Repeat the sprite in device space, clipped to the snapped box. Tile edges
182
- // are rounded independently from the box's unrounded device origin (x0/y0)
183
- // using the same per-edge-rounding discipline as the box snap in `draw`, so
184
- // neighboring tiles within this actor land on identical device pixels with
185
- // no hairline seams.
186
- function drawTilesSnapped(ctx, canvas, tileSize, layout, transform, x0, y0, rx0, ry0, rx1, ry1) {
187
- const cellHeight = validTileSize(tileSize);
188
- const cellWidth = cellHeight * (canvas.width / canvas.height);
189
- const cellDeviceWidth = cellWidth * transform.a;
190
- const cellDeviceHeight = cellHeight * transform.d;
191
- const cols = Math.ceil(layout.width / cellWidth);
192
- const rows = Math.ceil(layout.height / cellHeight);
193
- ctx.save();
194
- ctx.beginPath();
195
- ctx.rect(rx0, ry0, rx1 - rx0, ry1 - ry0);
196
- ctx.clip();
197
- for (let row = 0; row < rows; row++) {
198
- const edgeY0 = Math.round(y0 + row * cellDeviceHeight);
199
- const edgeY1 = Math.round(y0 + (row + 1) * cellDeviceHeight);
200
- for (let col = 0; col < cols; col++) {
201
- const edgeX0 = Math.round(x0 + col * cellDeviceWidth);
202
- const edgeX1 = Math.round(x0 + (col + 1) * cellDeviceWidth);
203
- ctx.drawImage(canvas, edgeX0, edgeY0, edgeX1 - edgeX0, edgeY1 - edgeY0);
204
- }
205
- }
206
- ctx.restore();
207
- }
208
-
209
- // Rotated actors have no axis-aligned device edges to snap to, so tile
210
- // unsnapped in card units, matching the unsnapped stretch fallback in `draw`.
211
- function drawTilesUnsnapped(ctx, canvas, tileSize, layout) {
212
- const cellHeight = validTileSize(tileSize);
213
- const cellWidth = cellHeight * (canvas.width / canvas.height);
214
- const cols = Math.ceil(layout.width / cellWidth);
215
- const rows = Math.ceil(layout.height / cellHeight);
216
- ctx.save();
217
- ctx.beginPath();
218
- ctx.rect(layout.x, layout.y, layout.width, layout.height);
219
- ctx.clip();
220
- for (let row = 0; row < rows; row++) {
221
- const ty = layout.y + row * cellHeight;
222
- for (let col = 0; col < cols; col++) {
223
- const tx = layout.x + col * cellWidth;
224
- ctx.drawImage(canvas, tx, ty, cellWidth, cellHeight);
225
- }
226
- }
227
- ctx.restore();
228
- }
229
-
230
- // Cache of native-resolution offscreen canvases keyed by sprite, then by
231
- // `${frameIndex}|${tint}`. A sprite edit produces a fresh Sprite object, so a
232
- // new object naturally misses the WeakMap and rebuilds.
233
- const spriteCanvasCache = new WeakMap();
234
-
235
- function getSpriteCanvas(sprite, frameIndex, tint) {
236
- const key = `${frameIndex}|${tint ?? ''}`;
237
- let byKey = spriteCanvasCache.get(sprite);
238
- if (!byKey) {
239
- byKey = new Map();
240
- spriteCanvasCache.set(sprite, byKey);
241
- }
242
- const cached = byKey.get(key);
243
- if (cached) return cached;
244
-
245
- const canvas = document.createElement('canvas');
246
- if (sprite.cornerRadius > 0) {
247
- renderSmoothSpriteFrame(sprite, frameIndex, canvas, { cornerRadius: sprite.cornerRadius });
248
- } else {
249
- renderSpriteFrame(sprite, frameIndex, canvas);
250
- }
251
- const tintRgba = parseTint(tint);
252
- if (tintRgba) {
253
- const octx = canvas.getContext('2d');
254
- if (octx && canvas.width > 0 && canvas.height > 0) {
255
- const image = octx.getImageData(0, 0, canvas.width, canvas.height);
256
- tintImageData(image.data, tintRgba);
257
- octx.putImageData(image, 0, 0);
258
- }
259
- }
260
- byKey.set(key, canvas);
261
- return canvas;
262
- }
263
-
264
- // Resolve the active tag: the `tag` prop when it names an existing tag,
265
- // otherwise the sprite's defaultTag, otherwise null (whole-timeline loop).
266
- function resolveTag(sprite, tagProp) {
267
- const name = tagProp || sprite.defaultTag;
268
- if (!name) return null;
269
- return sprite.tags.find((candidate) => candidate.name === name) ?? null;
270
- }
271
-
272
- // The ordered list of frame indices to step through, honoring the tag range and
273
- // direction. No tag => the whole timeline, forward.
274
- function frameSequence(sprite, tag) {
275
- const total = Math.max(frameCount(sprite), 1);
276
- let from = 0;
277
- let to = total - 1;
278
- let direction = 'forward';
279
- if (tag) {
280
- from = clampIndex(tag.from, total);
281
- to = clampIndex(tag.to, total);
282
- if (from > to) [from, to] = [to, from];
283
- direction = tag.direction;
284
- }
285
- if (from === to) return [from];
286
- const forward = [];
287
- for (let i = from; i <= to; i++) forward.push(i);
288
- if (direction === 'reverse') return forward.slice().reverse();
289
- if (direction === 'pingpong') {
290
- const back = [];
291
- for (let i = to - 1; i > from; i--) back.push(i);
292
- return [...forward, ...back];
293
- }
294
- return forward;
295
- }
296
-
297
- function clampIndex(value, total) {
298
- if (!Number.isInteger(value)) return 0;
299
- return Math.min(Math.max(value, 0), total - 1);
300
- }
301
-
302
- function animKey(props) {
303
- return `${props.file}|${props.tag ?? ''}`;
304
- }
305
-
306
- // Reset playback state when the sprite identity or active tag changes.
307
- function ensureAnimState(actor, sprite, props, sequence) {
308
- const key = animKey(props);
309
- let state = actor.runtime.spriteAnim;
310
- if (!state || state.sprite !== sprite || state.key !== key) {
311
- state = { sprite, key, pos: 0, elapsedMs: 0, loops: 0, done: false, frameIndex: sequence[0] };
312
- actor.runtime.spriteAnim = state;
313
- }
314
- return state;
315
- }
316
-
317
- function resetAnimState(state, sequence) {
318
- state.pos = 0;
319
- state.elapsedMs = 0;
320
- state.loops = 0;
321
- state.done = false;
322
- state.frameIndex = sequence[0];
323
- }
324
-
325
- // Advance the playhead by `dt` seconds, stepping frames by their durationMs and
326
- // honoring `repeat` (0 = loop forever; otherwise hold the last frame when done).
327
- function advanceAnim(state, sprite, sequence, tag, dt) {
328
- if (state.done) {
329
- state.frameIndex = sequence[state.pos];
330
- return;
331
- }
332
- const repeat = tag ? tag.repeat : 0;
333
- state.elapsedMs += dt * 1000;
334
- let guard = 0;
335
- while (guard++ < 1000) {
336
- const frame = sequence[state.pos];
337
- const duration = sprite.frames[frame]?.durationMs ?? sprite.defaultDurationMs;
338
- if (!(duration > 0) || state.elapsedMs < duration) break;
339
- state.elapsedMs -= duration;
340
- state.pos += 1;
341
- if (state.pos >= sequence.length) {
342
- state.loops += 1;
343
- if (repeat !== 0 && state.loops >= repeat) {
344
- state.pos = sequence.length - 1;
345
- state.done = true;
346
- state.elapsedMs = 0;
347
- break;
348
- }
349
- state.pos = 0;
350
- }
351
- }
352
- state.frameIndex = sequence[state.pos];
353
- }
354
-
355
- function getSpriteFiles(files) {
356
- return Object.keys(files).filter((path) => path.endsWith('.pxart'));
357
- }
@@ -1,47 +0,0 @@
1
- // Tint helpers for the pixel-art Sprite behavior. A tint is a hex color
2
- // (#rrggbb / #rrggbbaa) multiplied channel-wise into each source pixel. White
3
- // (#ffffffff) means "no change".
4
-
5
- // Parse a hex color into [r, g, b, a] channels (0-255). Returns null when the
6
- // string is not a hex color so callers can fall back to the raw color.
7
- export function parseHexColor(color) {
8
- const hex = String(color).trim().replace(/^#/, '');
9
- if (hex.length !== 6 && hex.length !== 8) return null;
10
- if (!/^[0-9a-fA-F]+$/.test(hex)) return null;
11
- const r = parseInt(hex.slice(0, 2), 16);
12
- const g = parseInt(hex.slice(2, 4), 16);
13
- const b = parseInt(hex.slice(4, 6), 16);
14
- const a = hex.length === 8 ? parseInt(hex.slice(6, 8), 16) : 255;
15
- return [r, g, b, a];
16
- }
17
-
18
- // Returns the tint as [r, g, b, a] channels, or null when there is no tint or
19
- // the tint is white (#ffffffff) -- both meaning "no change".
20
- export function parseTint(tint) {
21
- if (!tint) return null;
22
- const rgba = parseHexColor(tint);
23
- if (!rgba) return null;
24
- if (rgba[0] === 255 && rgba[1] === 255 && rgba[2] === 255 && rgba[3] === 255) {
25
- return null;
26
- }
27
- return rgba;
28
- }
29
-
30
- // Multiply each channel of a pixel color (hex string) by the tint (0-255).
31
- export function applyTint(color, tint) {
32
- const rgba = parseHexColor(color);
33
- if (!rgba) return color;
34
- const out = rgba.map((channel, i) => Math.round((channel * tint[i]) / 255));
35
- return '#' + out.map((channel) => channel.toString(16).padStart(2, '0')).join('');
36
- }
37
-
38
- // Multiply an ImageData buffer's RGBA channels in place by the tint (0-255).
39
- // Used to tint an already-rendered offscreen canvas (the Sprite path).
40
- export function tintImageData(data, tint) {
41
- for (let i = 0; i < data.length; i += 4) {
42
- data[i] = Math.round((data[i] * tint[0]) / 255);
43
- data[i + 1] = Math.round((data[i + 1] * tint[1]) / 255);
44
- data[i + 2] = Math.round((data[i + 2] * tint[2]) / 255);
45
- data[i + 3] = Math.round((data[i + 3] * tint[3]) / 255);
46
- }
47
- }
@@ -1,20 +0,0 @@
1
- {
2
- "name": "Cauldron",
3
- "actors": [
4
- {
5
- "components": {
6
- "Layout": {
7
- "width": 100,
8
- "height": 100,
9
- "z": 1
10
- },
11
- "Sprite": {
12
- "file": "drawings/cauldron.pxart"
13
- },
14
- "Collider": {
15
- "kind": "solid"
16
- }
17
- }
18
- }
19
- ]
20
- }