castle-web-cli 0.4.83 → 0.4.85

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/dist/agent-failures.d.ts +17 -0
  2. package/dist/agent-failures.js +151 -0
  3. package/dist/agent.d.ts +26 -0
  4. package/dist/agent.js +317 -74
  5. package/dist/ide.js +35 -13
  6. package/dist/native/loop.js +35 -0
  7. package/dist/native/openrouter.d.ts +7 -0
  8. package/dist/native/openrouter.js +25 -1
  9. package/dist/native/types.d.ts +2 -0
  10. package/dist/native/types.js +0 -38
  11. package/dist/openrouter-catalog.d.ts +28 -0
  12. package/dist/openrouter-catalog.js +299 -0
  13. package/dist/shell/assets/{index-C9Zhmien.js → index-BJLaUTJE.js} +60 -58
  14. package/dist/shell/assets/{index-BMkQt27u.css → index-DonnH--m.css} +1 -1
  15. package/dist/shell/index.html +2 -2
  16. package/kits/physics-2d/.prettierrc +8 -0
  17. package/kits/physics-2d/CLAUDE.md +329 -0
  18. package/kits/physics-2d/behaviors/Camera.jsx +43 -0
  19. package/kits/physics-2d/behaviors/Collider.jsx +199 -0
  20. package/kits/physics-2d/behaviors/Goal.jsx +29 -0
  21. package/kits/physics-2d/behaviors/Layout.jsx +53 -0
  22. package/kits/physics-2d/behaviors/Sprite.jsx +352 -0
  23. package/kits/physics-2d/behaviors/tint.js +47 -0
  24. package/kits/physics-2d/blueprints/ball.scene +14 -0
  25. package/kits/physics-2d/blueprints/block.scene +12 -0
  26. package/kits/physics-2d/blueprints/cauldron.scene +18 -0
  27. package/kits/physics-2d/blueprints/crate.scene +14 -0
  28. package/kits/physics-2d/blueprints/goal.scene +12 -0
  29. package/kits/physics-2d/castle.json +13 -0
  30. package/kits/physics-2d/docs/pxart-format.md +377 -0
  31. package/kits/physics-2d/drawings/block.pxart +25 -0
  32. package/kits/physics-2d/drawings/cauldron.pxart +113 -0
  33. package/kits/physics-2d/editors/BlueprintLibrary.jsx +247 -0
  34. package/kits/physics-2d/editors/ErrorBoundary.jsx +59 -0
  35. package/kits/physics-2d/editors/PlayOnly.jsx +31 -0
  36. package/kits/physics-2d/editors/PxArtEditor.jsx +954 -0
  37. package/kits/physics-2d/editors/SceneEditor.jsx +1681 -0
  38. package/kits/physics-2d/editors/SelectionOverlay.jsx +909 -0
  39. package/kits/physics-2d/editors/SingleEditor.jsx +122 -0
  40. package/kits/physics-2d/editors/behaviorRegistry.js +30 -0
  41. package/kits/physics-2d/editors/editorHistory.js +157 -0
  42. package/kits/physics-2d/editors/inspectorSheet.js +13 -0
  43. package/kits/physics-2d/editors/pixelCanvas.js +11 -0
  44. package/kits/physics-2d/editors/pixelEditorChrome.jsx +74 -0
  45. package/kits/physics-2d/editors/pixelGeometry.js +140 -0
  46. package/kits/physics-2d/editors/pixelInspector.jsx +633 -0
  47. package/kits/physics-2d/editors/pxArtEditorModel.js +732 -0
  48. package/kits/physics-2d/editors/pxArtPlayback.js +92 -0
  49. package/kits/physics-2d/editors/pxArtTimeline.jsx +752 -0
  50. package/kits/physics-2d/editors/pxArtTimeline.module.css +506 -0
  51. package/kits/physics-2d/editors/pxArtTools.js +232 -0
  52. package/kits/physics-2d/editors/useArtboardFit.js +102 -0
  53. package/kits/physics-2d/engine/ScenePlayer.jsx +196 -0
  54. package/kits/physics-2d/engine/SceneUI.jsx +59 -0
  55. package/kits/physics-2d/engine/assets.js +15 -0
  56. package/kits/physics-2d/engine/autoInspector.jsx +70 -0
  57. package/kits/physics-2d/engine/blueprint.js +521 -0
  58. package/kits/physics-2d/engine/collider.js +196 -0
  59. package/kits/physics-2d/engine/files.js +117 -0
  60. package/kits/physics-2d/engine/liveReload.js +88 -0
  61. package/kits/physics-2d/engine/pxart.js +1032 -0
  62. package/kits/physics-2d/engine/pxartSmooth.js +222 -0
  63. package/kits/physics-2d/engine/scene.js +686 -0
  64. package/kits/physics-2d/engine/spriteGeometry.js +32 -0
  65. package/kits/physics-2d/engine/ui.jsx +688 -0
  66. package/kits/physics-2d/engine/ui.module.css +2287 -0
  67. package/kits/physics-2d/eslint.config.js +71 -0
  68. package/kits/physics-2d/index.html +24 -0
  69. package/kits/physics-2d/main.jsx +24 -0
  70. package/kits/physics-2d/package-lock.json +2706 -0
  71. package/kits/physics-2d/package.json +42 -0
  72. package/kits/physics-2d/physics/PhysicsSystem.js +290 -0
  73. package/kits/physics-2d/physics/behaviors/AnalogStick.jsx +101 -0
  74. package/kits/physics-2d/physics/behaviors/Draggable.jsx +79 -0
  75. package/kits/physics-2d/physics/behaviors/RigidBody.jsx +55 -0
  76. package/kits/physics-2d/physics/behaviors/Slingshot.jsx +118 -0
  77. package/kits/physics-2d/physics/controls.js +79 -0
  78. package/kits/physics-2d/physics/index.js +26 -0
  79. package/kits/physics-2d/physics/matterBridge.js +126 -0
  80. package/kits/physics-2d/pnpm-lock.yaml +1761 -0
  81. package/kits/physics-2d/scenes/main.scene +12 -0
  82. package/kits/physics-2d/scenes/sandbox.scene +13 -0
  83. package/kits/physics-2d/scripts/draw.mjs +121 -0
  84. package/kits/physics-2d/vite.config.js +1 -0
  85. package/package.json +1 -1
@@ -0,0 +1,352 @@
1
+ import React from 'react';
2
+ import { frameCount, renderSpriteFrame } from '../engine/pxart';
3
+ import { renderSmoothSpriteFrame } from '../engine/pxartSmooth';
4
+ import { spriteDestRect } from '../engine/spriteGeometry';
5
+ import { Panel, SelectField } from '../engine/ui';
6
+ import { AutoFields, overrideProps } from '../engine/autoInspector';
7
+ import { parseTint, tintImageData } from './tint';
8
+
9
+ // Runtime behavior for the pixel-art Sprite format (.pxart). It looks up the
10
+ // parsed Sprite from the scene's sprite map, composites the current animation
11
+ // frame into a cached offscreen canvas (tinted), and blits it into the actor's
12
+ // Layout box with smoothing disabled — unless the FILE itself opts into a
13
+ // `cornerRadius` (a property of the .pxart asset, not a Sprite prop) greater
14
+ // than 0, in which case the cached canvas holds a corner-rounded vector
15
+ // render instead and gets blitted with smoothing on.
16
+ //
17
+ // Animation plays in `update` and is stored on `actor.runtime.spriteAnim` (the
18
+ // behavior instance is recreated every frame, but the actor object persists in
19
+ // the runtime). In edit-mode previews `update` never runs, so the actor holds
20
+ // frame 0.
21
+ const FALLBACK_FILE = 'drawings/cauldron.pxart';
22
+
23
+ export class Sprite {
24
+ static behaviorName = 'Sprite';
25
+
26
+ static defaultProps = {
27
+ file: FALLBACK_FILE,
28
+ tint: '#ffffffff',
29
+ playing: true,
30
+ tag: '',
31
+ mode: 'cover',
32
+ tileSize: 50,
33
+ };
34
+
35
+ constructor(props) {
36
+ this.props = props;
37
+ }
38
+
39
+ resolveSprite(scene) {
40
+ return scene.sprites?.[this.props.file] ?? scene.sprites?.[FALLBACK_FILE] ?? null;
41
+ }
42
+
43
+ update(actor, scene, dt) {
44
+ if (!actor.runtime || actor.runtime.collected) return;
45
+ const sprite = this.resolveSprite(scene);
46
+ if (!sprite) return;
47
+ const tag = resolveTag(sprite, this.props.tag);
48
+ const sequence = frameSequence(sprite, tag);
49
+ const state = ensureAnimState(actor, sprite, this.props, sequence);
50
+ if (!this.props.playing) {
51
+ resetAnimState(state, sequence);
52
+ state.frameIndex = 0;
53
+ return;
54
+ }
55
+ advanceAnim(state, sprite, sequence, tag, dt);
56
+ }
57
+
58
+ draw(actor, scene, ctx) {
59
+ if (actor.runtime?.collected) return;
60
+ const layout = actor.components.Layout;
61
+ if (!layout) return;
62
+ const sprite = this.resolveSprite(scene);
63
+ if (!sprite) return;
64
+ const total = frameCount(sprite);
65
+ const frameIndex = Math.min(Math.max(actor.runtime?.spriteAnim?.frameIndex ?? 0, 0), total - 1);
66
+ const canvas = getSpriteCanvas(sprite, frameIndex, this.props.tint);
67
+ const prevSmoothing = ctx.imageSmoothingEnabled;
68
+ // File-level `cornerRadius > 0` sprites are pre-rendered as supersampled,
69
+ // corner-rounded vector fills (see engine/pxartSmooth.js); blit those with
70
+ // smoothing on so downscaling to the Layout box stays antialiased. Plain
71
+ // pixel sprites (`cornerRadius === 0`) keep nearest-neighbor blitting.
72
+ const smoothing = sprite.cornerRadius > 0;
73
+
74
+ // Snap the destination rect to whole device pixels so tiles that abut
75
+ // exactly in card units (e.g. adjacent 100-unit-wide tiles) land on the
76
+ // same device-pixel edge instead of each getting its own antialiased
77
+ // partial-coverage edge, which otherwise leaves faint hairline seams
78
+ // between them. Rounding each of the four device-space coordinates
79
+ // independently (rather than rounding position+size) is what makes two
80
+ // neighbors sharing a card-unit edge compute and round to the identical
81
+ // device pixel. Skipped when the ctx is rotated/skewed (nonzero b or c,
82
+ // set by the runtime for `Layout.rotation`), since a rotated rect has no
83
+ // axis-aligned device edges to snap to.
84
+ const transform = ctx.getTransform();
85
+ const tiling = this.props.mode === 'tile';
86
+ // `cover` scales the art to fill the box preserving aspect ratio, so `dest`
87
+ // overflows the Layout box and the overflow must be clipped to the box.
88
+ const covering = this.props.mode === 'cover';
89
+ // Everything downstream (snapping, tiling's clip box) works off `dest`: the
90
+ // Layout box itself for every mode except `fit`/`cover`, which resize to the
91
+ // art's own aspect ratio (see `spriteDestRect`).
92
+ const dest = spriteDestRect(this.props.mode, layout, canvas);
93
+ if (transform.b === 0 && transform.c === 0) {
94
+ const x0 = dest.x * transform.a + transform.e;
95
+ const y0 = dest.y * transform.d + transform.f;
96
+ const x1 = (dest.x + dest.width) * transform.a + transform.e;
97
+ const y1 = (dest.y + dest.height) * transform.d + transform.f;
98
+ const rx0 = Math.round(x0);
99
+ const ry0 = Math.round(y0);
100
+ const rx1 = Math.round(x1);
101
+ const ry1 = Math.round(y1);
102
+ ctx.save();
103
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
104
+ ctx.imageSmoothingEnabled = smoothing;
105
+ if (tiling) {
106
+ drawTilesSnapped(ctx, canvas, this.props.tileSize, dest, transform, x0, y0, rx0, ry0, rx1, ry1);
107
+ } else {
108
+ // Clip cover's overflow to the snapped Layout box (undone by restore).
109
+ if (covering) {
110
+ const lx0 = Math.round(layout.x * transform.a + transform.e);
111
+ const ly0 = Math.round(layout.y * transform.d + transform.f);
112
+ const lx1 = Math.round((layout.x + layout.width) * transform.a + transform.e);
113
+ const ly1 = Math.round((layout.y + layout.height) * transform.d + transform.f);
114
+ ctx.beginPath();
115
+ ctx.rect(lx0, ly0, lx1 - lx0, ly1 - ly0);
116
+ ctx.clip();
117
+ }
118
+ ctx.drawImage(canvas, rx0, ry0, rx1 - rx0, ry1 - ry0);
119
+ }
120
+ ctx.restore();
121
+ } else {
122
+ ctx.imageSmoothingEnabled = smoothing;
123
+ if (tiling) {
124
+ drawTilesUnsnapped(ctx, canvas, this.props.tileSize, dest);
125
+ } else if (covering) {
126
+ // Rotated: clip cover's overflow to the Layout box in card units.
127
+ ctx.save();
128
+ ctx.beginPath();
129
+ ctx.rect(layout.x, layout.y, layout.width, layout.height);
130
+ ctx.clip();
131
+ ctx.drawImage(canvas, dest.x, dest.y, dest.width, dest.height);
132
+ ctx.restore();
133
+ } else {
134
+ ctx.drawImage(canvas, dest.x, dest.y, dest.width, dest.height);
135
+ }
136
+ ctx.imageSmoothingEnabled = prevSmoothing;
137
+ }
138
+ }
139
+
140
+ static Inspector({ component, setComponent, files, override }) {
141
+ const spriteFiles = getSpriteFiles(files);
142
+ return (
143
+ <Panel title="Sprite" overridden={override?.anyOverridden()}>
144
+ <SelectField
145
+ label="File"
146
+ value={component.file}
147
+ onChange={(file) => setComponent({ file })}
148
+ options={spriteFiles}
149
+ {...overrideProps(override, 'file')}
150
+ />
151
+ <SelectField
152
+ label="Mode"
153
+ value={component.mode}
154
+ onChange={(mode) => setComponent({ mode })}
155
+ options={['stretch', 'fit', 'cover', 'tile']}
156
+ {...overrideProps(override, 'mode')}
157
+ />
158
+ <AutoFields
159
+ defaultProps={Sprite.defaultProps}
160
+ component={component}
161
+ setComponent={setComponent}
162
+ only={['playing', 'tint', 'tileSize']}
163
+ override={override}
164
+ />
165
+ </Panel>
166
+ );
167
+ }
168
+ }
169
+
170
+ // Guard against non-finite or non-positive tileSize props (e.g. cleared
171
+ // inspector fields), falling back to one 50-unit grid cell.
172
+ function validTileSize(value) {
173
+ return Number.isFinite(value) && value > 0 ? value : 50;
174
+ }
175
+
176
+ // Repeat the sprite in device space, clipped to the snapped box. Tile edges
177
+ // are rounded independently from the box's unrounded device origin (x0/y0)
178
+ // using the same per-edge-rounding discipline as the box snap in `draw`, so
179
+ // neighboring tiles within this actor land on identical device pixels with
180
+ // no hairline seams.
181
+ function drawTilesSnapped(ctx, canvas, tileSize, layout, transform, x0, y0, rx0, ry0, rx1, ry1) {
182
+ const cellHeight = validTileSize(tileSize);
183
+ const cellWidth = cellHeight * (canvas.width / canvas.height);
184
+ const cellDeviceWidth = cellWidth * transform.a;
185
+ const cellDeviceHeight = cellHeight * transform.d;
186
+ const cols = Math.ceil(layout.width / cellWidth);
187
+ const rows = Math.ceil(layout.height / cellHeight);
188
+ ctx.save();
189
+ ctx.beginPath();
190
+ ctx.rect(rx0, ry0, rx1 - rx0, ry1 - ry0);
191
+ ctx.clip();
192
+ for (let row = 0; row < rows; row++) {
193
+ const edgeY0 = Math.round(y0 + row * cellDeviceHeight);
194
+ const edgeY1 = Math.round(y0 + (row + 1) * cellDeviceHeight);
195
+ for (let col = 0; col < cols; col++) {
196
+ const edgeX0 = Math.round(x0 + col * cellDeviceWidth);
197
+ const edgeX1 = Math.round(x0 + (col + 1) * cellDeviceWidth);
198
+ ctx.drawImage(canvas, edgeX0, edgeY0, edgeX1 - edgeX0, edgeY1 - edgeY0);
199
+ }
200
+ }
201
+ ctx.restore();
202
+ }
203
+
204
+ // Rotated actors have no axis-aligned device edges to snap to, so tile
205
+ // unsnapped in card units, matching the unsnapped stretch fallback in `draw`.
206
+ function drawTilesUnsnapped(ctx, canvas, tileSize, layout) {
207
+ const cellHeight = validTileSize(tileSize);
208
+ const cellWidth = cellHeight * (canvas.width / canvas.height);
209
+ const cols = Math.ceil(layout.width / cellWidth);
210
+ const rows = Math.ceil(layout.height / cellHeight);
211
+ ctx.save();
212
+ ctx.beginPath();
213
+ ctx.rect(layout.x, layout.y, layout.width, layout.height);
214
+ ctx.clip();
215
+ for (let row = 0; row < rows; row++) {
216
+ const ty = layout.y + row * cellHeight;
217
+ for (let col = 0; col < cols; col++) {
218
+ const tx = layout.x + col * cellWidth;
219
+ ctx.drawImage(canvas, tx, ty, cellWidth, cellHeight);
220
+ }
221
+ }
222
+ ctx.restore();
223
+ }
224
+
225
+ // Cache of native-resolution offscreen canvases keyed by sprite, then by
226
+ // `${frameIndex}|${tint}`. A sprite edit produces a fresh Sprite object, so a
227
+ // new object naturally misses the WeakMap and rebuilds.
228
+ const spriteCanvasCache = new WeakMap();
229
+
230
+ function getSpriteCanvas(sprite, frameIndex, tint) {
231
+ const key = `${frameIndex}|${tint ?? ''}`;
232
+ let byKey = spriteCanvasCache.get(sprite);
233
+ if (!byKey) {
234
+ byKey = new Map();
235
+ spriteCanvasCache.set(sprite, byKey);
236
+ }
237
+ const cached = byKey.get(key);
238
+ if (cached) return cached;
239
+
240
+ const canvas = document.createElement('canvas');
241
+ if (sprite.cornerRadius > 0) {
242
+ renderSmoothSpriteFrame(sprite, frameIndex, canvas, { cornerRadius: sprite.cornerRadius });
243
+ } else {
244
+ renderSpriteFrame(sprite, frameIndex, canvas);
245
+ }
246
+ const tintRgba = parseTint(tint);
247
+ if (tintRgba) {
248
+ const octx = canvas.getContext('2d');
249
+ if (octx && canvas.width > 0 && canvas.height > 0) {
250
+ const image = octx.getImageData(0, 0, canvas.width, canvas.height);
251
+ tintImageData(image.data, tintRgba);
252
+ octx.putImageData(image, 0, 0);
253
+ }
254
+ }
255
+ byKey.set(key, canvas);
256
+ return canvas;
257
+ }
258
+
259
+ // Resolve the active tag: the `tag` prop when it names an existing tag,
260
+ // otherwise the sprite's defaultTag, otherwise null (whole-timeline loop).
261
+ function resolveTag(sprite, tagProp) {
262
+ const name = tagProp || sprite.defaultTag;
263
+ if (!name) return null;
264
+ return sprite.tags.find((candidate) => candidate.name === name) ?? null;
265
+ }
266
+
267
+ // The ordered list of frame indices to step through, honoring the tag range and
268
+ // direction. No tag => the whole timeline, forward.
269
+ function frameSequence(sprite, tag) {
270
+ const total = Math.max(frameCount(sprite), 1);
271
+ let from = 0;
272
+ let to = total - 1;
273
+ let direction = 'forward';
274
+ if (tag) {
275
+ from = clampIndex(tag.from, total);
276
+ to = clampIndex(tag.to, total);
277
+ if (from > to) [from, to] = [to, from];
278
+ direction = tag.direction;
279
+ }
280
+ if (from === to) return [from];
281
+ const forward = [];
282
+ for (let i = from; i <= to; i++) forward.push(i);
283
+ if (direction === 'reverse') return forward.slice().reverse();
284
+ if (direction === 'pingpong') {
285
+ const back = [];
286
+ for (let i = to - 1; i > from; i--) back.push(i);
287
+ return [...forward, ...back];
288
+ }
289
+ return forward;
290
+ }
291
+
292
+ function clampIndex(value, total) {
293
+ if (!Number.isInteger(value)) return 0;
294
+ return Math.min(Math.max(value, 0), total - 1);
295
+ }
296
+
297
+ function animKey(props) {
298
+ return `${props.file}|${props.tag ?? ''}`;
299
+ }
300
+
301
+ // Reset playback state when the sprite identity or active tag changes.
302
+ function ensureAnimState(actor, sprite, props, sequence) {
303
+ const key = animKey(props);
304
+ let state = actor.runtime.spriteAnim;
305
+ if (!state || state.sprite !== sprite || state.key !== key) {
306
+ state = { sprite, key, pos: 0, elapsedMs: 0, loops: 0, done: false, frameIndex: sequence[0] };
307
+ actor.runtime.spriteAnim = state;
308
+ }
309
+ return state;
310
+ }
311
+
312
+ function resetAnimState(state, sequence) {
313
+ state.pos = 0;
314
+ state.elapsedMs = 0;
315
+ state.loops = 0;
316
+ state.done = false;
317
+ state.frameIndex = sequence[0];
318
+ }
319
+
320
+ // Advance the playhead by `dt` seconds, stepping frames by their durationMs and
321
+ // honoring `repeat` (0 = loop forever; otherwise hold the last frame when done).
322
+ function advanceAnim(state, sprite, sequence, tag, dt) {
323
+ if (state.done) {
324
+ state.frameIndex = sequence[state.pos];
325
+ return;
326
+ }
327
+ const repeat = tag ? tag.repeat : 0;
328
+ state.elapsedMs += dt * 1000;
329
+ let guard = 0;
330
+ while (guard++ < 1000) {
331
+ const frame = sequence[state.pos];
332
+ const duration = sprite.frames[frame]?.durationMs ?? sprite.defaultDurationMs;
333
+ if (!(duration > 0) || state.elapsedMs < duration) break;
334
+ state.elapsedMs -= duration;
335
+ state.pos += 1;
336
+ if (state.pos >= sequence.length) {
337
+ state.loops += 1;
338
+ if (repeat !== 0 && state.loops >= repeat) {
339
+ state.pos = sequence.length - 1;
340
+ state.done = true;
341
+ state.elapsedMs = 0;
342
+ break;
343
+ }
344
+ state.pos = 0;
345
+ }
346
+ }
347
+ state.frameIndex = sequence[state.pos];
348
+ }
349
+
350
+ function getSpriteFiles(files) {
351
+ return Object.keys(files).filter((path) => path.endsWith('.pxart'));
352
+ }
@@ -0,0 +1,47 @@
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
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "Ball",
3
+ "actors": [
4
+ {
5
+ "components": {
6
+ "Layout": { "width": 44, "height": 44, "z": 2 },
7
+ "Sprite": { "file": "drawings/cauldron.pxart", "mode": "fit" },
8
+ "Collider": { "shape": "circle", "width": 40, "height": 40, "bounciness": 0.6, "friction": 0.05 },
9
+ "RigidBody": { "bodyType": "dynamic", "gravityScale": 1, "drag": 0.005 },
10
+ "Slingshot": { "speed": 0.1, "maxDrag": 200 }
11
+ }
12
+ }
13
+ ]
14
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "Block",
3
+ "actors": [
4
+ {
5
+ "components": {
6
+ "Layout": { "width": 100, "height": 40, "z": 1 },
7
+ "Sprite": { "file": "drawings/block.pxart", "mode": "tile", "tileSize": 20 },
8
+ "Collider": { "bounciness": 0.4, "friction": 0.2 }
9
+ }
10
+ }
11
+ ]
12
+ }
@@ -0,0 +1,18 @@
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
+ }
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "Crate",
3
+ "actors": [
4
+ {
5
+ "components": {
6
+ "Layout": { "width": 48, "height": 48, "z": 2 },
7
+ "Sprite": { "file": "drawings/block.pxart", "mode": "fit" },
8
+ "Collider": { "shape": "box", "width": 46, "height": 46, "bounciness": 0.2, "friction": 0.3 },
9
+ "RigidBody": { "bodyType": "dynamic", "gravityScale": 1, "drag": 0.02 },
10
+ "Draggable": {}
11
+ }
12
+ }
13
+ ]
14
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "Goal",
3
+ "actors": [
4
+ {
5
+ "components": {
6
+ "Layout": { "width": 110, "height": 34, "z": 3 },
7
+ "Collider": { "width": 110, "height": 34, "isTrigger": true },
8
+ "Goal": {}
9
+ }
10
+ }
11
+ ]
12
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "editor": {
3
+ "initialPanels": [
4
+ {
5
+ "width": 400,
6
+ "column": [{ "type": "playtest" }, { "type": "files" }]
7
+ },
8
+ { "type": "editor", "file": "scenes/main.scene" }
9
+ ],
10
+ "hiddenPaths": [],
11
+ "visiblePaths": ["drawings/**", "scenes/**", "blueprints/**", "behaviors/**"]
12
+ }
13
+ }