castle-web-cli 0.4.114 → 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 +7 -0
- package/dist/castleJson.js +10 -0
- package/dist/editorConfig.d.ts +34 -0
- package/dist/editorConfig.js +125 -0
- package/dist/ide.js +245 -120
- package/dist/imports.d.ts +10 -0
- package/dist/imports.js +149 -17
- package/dist/init.d.ts +3 -0
- package/dist/init.js +113 -88
- package/dist/install.d.ts +1 -1
- package/dist/install.js +26 -24
- package/dist/serve.js +16 -0
- package/dist/shell/assets/index-DiPlPGyg.js +144 -0
- package/dist/shell/assets/index-Y6cJRRCX.css +1 -0
- package/dist/shell/index.html +2 -2
- package/dist/unsupportedMedia.d.ts +1 -0
- package/dist/unsupportedMedia.js +54 -0
- package/dist/vitePlugins.js +13 -20
- package/kits/basic-2d/castle.json +1 -1
- package/kits/basic-2d/editors/behaviorRegistry.js +5 -7
- package/kits/physics-2d/CLAUDE.md +65 -34
- package/kits/physics-2d/{physics/behaviors → behaviors}/AnalogStick.jsx +3 -3
- package/kits/physics-2d/behaviors/Collider.jsx +1 -1
- package/kits/physics-2d/{physics/behaviors → behaviors}/Draggable.jsx +3 -3
- package/kits/physics-2d/{physics/behaviors → behaviors}/Joints.jsx +5 -5
- package/kits/physics-2d/{physics/behaviors → behaviors}/RigidBody.jsx +2 -2
- package/kits/physics-2d/{physics/behaviors → behaviors}/Slingshot.jsx +3 -3
- package/kits/physics-2d/behaviors/Sound.jsx +152 -0
- package/kits/physics-2d/behaviors/Sprite.jsx +112 -42
- package/kits/physics-2d/behaviors/Tone.jsx +83 -0
- package/kits/physics-2d/behaviors/Video.jsx +111 -0
- package/kits/physics-2d/behaviors/tint.js +24 -9
- 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 +74 -4
- package/kits/physics-2d/docs/pxart-format.md +537 -51
- package/kits/physics-2d/editors/BlueprintLibrary.jsx +7 -3
- package/kits/physics-2d/editors/ImageViewer.jsx +206 -0
- package/kits/physics-2d/editors/MediaPlayer.jsx +57 -0
- package/kits/physics-2d/editors/PxArtEditor.jsx +1794 -65
- package/kits/physics-2d/editors/SceneEditor.jsx +7 -3
- package/kits/physics-2d/editors/SingleEditor.jsx +8 -0
- package/kits/physics-2d/editors/behaviorRegistry.js +5 -7
- 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 +31 -0
- package/kits/physics-2d/editors/mediaViewer.module.css +128 -0
- 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/ScenePlayer.jsx +1 -0
- package/kits/physics-2d/engine/art.js +105 -0
- package/kits/physics-2d/engine/assets.js +12 -4
- package/kits/physics-2d/engine/audioContext.js +34 -0
- package/kits/physics-2d/engine/blueprint.js +3 -3
- package/kits/physics-2d/engine/collider.js +28 -17
- package/kits/physics-2d/engine/files.js +77 -1
- package/kits/physics-2d/engine/liveReload.js +1 -1
- package/kits/physics-2d/engine/media.js +212 -0
- package/kits/physics-2d/{physics → engine/physics}/PhysicsSystem.js +1 -1
- package/kits/physics-2d/{physics → engine/physics}/jointArt.js +3 -4
- package/kits/physics-2d/{physics → engine/physics}/matterBridge.js +2 -9
- 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/scene.js +12 -0
- package/kits/physics-2d/engine/tone.js +112 -0
- 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/kits/physics-2d/systems/media.js +34 -0
- package/kits/physics-2d/systems/physics.js +12 -3
- package/package.json +1 -1
- package/dist/shell/assets/index-CqpY1xZR.js +0 -144
- package/dist/shell/assets/index-DCwVFL5u.css +0 -1
- package/kits/physics-2d/physics/index.js +0 -26
- /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
- /package/kits/physics-2d/{physics → engine/physics}/controls.js +0 -0
- /package/kits/physics-2d/{physics → engine/physics}/joints.js +0 -0
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
import { resolveDeckFile } from 'castle-web-sdk';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { artFrameCount, isImageArt, renderArtFrame } from '../engine/art';
|
|
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';
|
|
8
|
-
import { parseTint,
|
|
9
|
+
import { parseTint, tintCanvas } from './tint';
|
|
9
10
|
|
|
10
|
-
// Runtime behavior for the pixel-art
|
|
11
|
-
//
|
|
12
|
-
//
|
|
11
|
+
// Runtime behavior for a deck's 2D art: the pixel-art `.pxart` format, or an
|
|
12
|
+
// image file (png / jpg / gif / webp / svg) the deck uploaded. Both come out of
|
|
13
|
+
// the scene's sprite map the same way (see engine/art.js), so everything below
|
|
14
|
+
// -- draw modes, tint, tiling, the collider's auto-fit -- works on either.
|
|
15
|
+
//
|
|
16
|
+
// It looks up the art, composites the current animation frame into a cached
|
|
17
|
+
// offscreen canvas (tinted), and blits it into the actor's
|
|
13
18
|
// Layout box with smoothing disabled — unless the FILE itself opts into a
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// render
|
|
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.
|
|
17
22
|
//
|
|
18
23
|
// Animation plays in `update` and is stored on `actor.runtime.spriteAnim` (the
|
|
19
24
|
// behavior instance is recreated every frame, but the actor object persists in
|
|
20
25
|
// the runtime). In edit-mode previews `update` never runs, so the actor holds
|
|
21
26
|
// frame 0.
|
|
22
|
-
const FALLBACK_FILE = 'drawings/cauldron.
|
|
27
|
+
const FALLBACK_FILE = 'drawings/cauldron.sprite';
|
|
23
28
|
|
|
24
29
|
export class Sprite {
|
|
25
30
|
static behaviorName = 'Sprite';
|
|
@@ -66,15 +71,21 @@ export class Sprite {
|
|
|
66
71
|
if (!layout) return;
|
|
67
72
|
const sprite = this.resolveSprite(scene);
|
|
68
73
|
if (!sprite) return;
|
|
69
|
-
const total =
|
|
74
|
+
const total = artFrameCount(sprite);
|
|
70
75
|
const frameIndex = Math.min(Math.max(actor.runtime?.spriteAnim?.frameIndex ?? 0, 0), total - 1);
|
|
71
76
|
const canvas = getSpriteCanvas(sprite, frameIndex, this.props.tint);
|
|
77
|
+
if (!canvas) return;
|
|
78
|
+
// `canvas` is a <canvas> for pixel art and may be the <img> itself for an
|
|
79
|
+
// untinted image, so size comes from the art rather than the element.
|
|
80
|
+
const artSize = sprite.resolution;
|
|
72
81
|
const prevSmoothing = ctx.imageSmoothingEnabled;
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
|
|
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);
|
|
78
89
|
|
|
79
90
|
// Snap the destination rect to whole device pixels so tiles that abut
|
|
80
91
|
// exactly in card units (e.g. adjacent 100-unit-wide tiles) land on the
|
|
@@ -94,7 +105,7 @@ export class Sprite {
|
|
|
94
105
|
// Everything downstream (snapping, tiling's clip box) works off `dest`: the
|
|
95
106
|
// Layout box itself for every mode except `fit`/`cover`, which resize to the
|
|
96
107
|
// art's own aspect ratio (see `spriteDestRect`).
|
|
97
|
-
const dest = spriteDestRect(this.props.mode, layout,
|
|
108
|
+
const dest = spriteDestRect(this.props.mode, layout, artSize);
|
|
98
109
|
if (transform.b === 0 && transform.c === 0) {
|
|
99
110
|
const x0 = dest.x * transform.a + transform.e;
|
|
100
111
|
const y0 = dest.y * transform.d + transform.f;
|
|
@@ -108,7 +119,7 @@ export class Sprite {
|
|
|
108
119
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
109
120
|
ctx.imageSmoothingEnabled = smoothing;
|
|
110
121
|
if (tiling) {
|
|
111
|
-
drawTilesSnapped(ctx, canvas, this.props.tileSize, dest, transform, x0, y0, rx0, ry0, rx1, ry1);
|
|
122
|
+
drawTilesSnapped(ctx, canvas, artSize, this.props.tileSize, dest, transform, x0, y0, rx0, ry0, rx1, ry1);
|
|
112
123
|
} else {
|
|
113
124
|
// Clip cover's overflow to the snapped Layout box (undone by restore).
|
|
114
125
|
if (covering) {
|
|
@@ -126,7 +137,7 @@ export class Sprite {
|
|
|
126
137
|
} else {
|
|
127
138
|
ctx.imageSmoothingEnabled = smoothing;
|
|
128
139
|
if (tiling) {
|
|
129
|
-
drawTilesUnsnapped(ctx, canvas, this.props.tileSize, dest);
|
|
140
|
+
drawTilesUnsnapped(ctx, canvas, artSize, this.props.tileSize, dest);
|
|
130
141
|
} else if (covering) {
|
|
131
142
|
// Rotated: clip cover's overflow to the Layout box in card units.
|
|
132
143
|
ctx.save();
|
|
@@ -142,8 +153,8 @@ export class Sprite {
|
|
|
142
153
|
}
|
|
143
154
|
}
|
|
144
155
|
|
|
145
|
-
static Inspector({ component, setComponent,
|
|
146
|
-
const spriteFiles = getSpriteFiles(
|
|
156
|
+
static Inspector({ component, setComponent, sprites, override }) {
|
|
157
|
+
const spriteFiles = getSpriteFiles(sprites);
|
|
147
158
|
return (
|
|
148
159
|
<Panel title="Sprite" overridden={override?.anyOverridden()}>
|
|
149
160
|
<SelectField
|
|
@@ -183,9 +194,9 @@ function validTileSize(value) {
|
|
|
183
194
|
// using the same per-edge-rounding discipline as the box snap in `draw`, so
|
|
184
195
|
// neighboring tiles within this actor land on identical device pixels with
|
|
185
196
|
// no hairline seams.
|
|
186
|
-
function drawTilesSnapped(ctx, canvas, tileSize, layout, transform, x0, y0, rx0, ry0, rx1, ry1) {
|
|
197
|
+
function drawTilesSnapped(ctx, canvas, artSize, tileSize, layout, transform, x0, y0, rx0, ry0, rx1, ry1) {
|
|
187
198
|
const cellHeight = validTileSize(tileSize);
|
|
188
|
-
const cellWidth = cellHeight * (
|
|
199
|
+
const cellWidth = cellHeight * (artSize.width / artSize.height);
|
|
189
200
|
const cellDeviceWidth = cellWidth * transform.a;
|
|
190
201
|
const cellDeviceHeight = cellHeight * transform.d;
|
|
191
202
|
const cols = Math.ceil(layout.width / cellWidth);
|
|
@@ -208,9 +219,9 @@ function drawTilesSnapped(ctx, canvas, tileSize, layout, transform, x0, y0, rx0,
|
|
|
208
219
|
|
|
209
220
|
// Rotated actors have no axis-aligned device edges to snap to, so tile
|
|
210
221
|
// unsnapped in card units, matching the unsnapped stretch fallback in `draw`.
|
|
211
|
-
function drawTilesUnsnapped(ctx, canvas, tileSize, layout) {
|
|
222
|
+
function drawTilesUnsnapped(ctx, canvas, artSize, tileSize, layout) {
|
|
212
223
|
const cellHeight = validTileSize(tileSize);
|
|
213
|
-
const cellWidth = cellHeight * (
|
|
224
|
+
const cellWidth = cellHeight * (artSize.width / artSize.height);
|
|
214
225
|
const cols = Math.ceil(layout.width / cellWidth);
|
|
215
226
|
const rows = Math.ceil(layout.height / cellHeight);
|
|
216
227
|
ctx.save();
|
|
@@ -232,7 +243,61 @@ function drawTilesUnsnapped(ctx, canvas, tileSize, layout) {
|
|
|
232
243
|
// new object naturally misses the WeakMap and rebuilds.
|
|
233
244
|
const spriteCanvasCache = new WeakMap();
|
|
234
245
|
|
|
246
|
+
// Ceiling on a cached sprite bitmap's long side. Nothing is ever rasterized
|
|
247
|
+
// larger than the card itself -- 500x700 card units, times the display's pixel
|
|
248
|
+
// ratio -- so this is roughly the card at 3x, and a copy bigger than it holds
|
|
249
|
+
// detail that no draw can show. It only ever bites uploaded photos: `.pxart`
|
|
250
|
+
// caps at 512 (RESOLUTION_MAX), and a 4000x3000 photo goes from a 46 MB cached
|
|
251
|
+
// canvas to 12 MB with nothing visibly different.
|
|
252
|
+
const MAX_CACHE_LONG_SIDE = 2048;
|
|
253
|
+
|
|
254
|
+
// The raster this frame is drawn from, at its own natural size: the decoded
|
|
255
|
+
// <img> for an image, a freshly rendered canvas for pixel art. Null while an
|
|
256
|
+
// image is still downloading.
|
|
257
|
+
function frameSource(sprite, frameIndex) {
|
|
258
|
+
if (isImageArt(sprite)) return sprite.ready ? sprite.image : null;
|
|
259
|
+
const canvas = document.createElement('canvas');
|
|
260
|
+
if (isVectorRenderer(sprite.renderer)) {
|
|
261
|
+
renderSmoothCompositeFrame(sprite, frameIndex, canvas);
|
|
262
|
+
return canvas;
|
|
263
|
+
}
|
|
264
|
+
if (sprite.cornerRadius > 0) {
|
|
265
|
+
renderSmoothSpriteFrame(sprite, frameIndex, canvas, { cornerRadius: sprite.cornerRadius });
|
|
266
|
+
return canvas;
|
|
267
|
+
}
|
|
268
|
+
return renderArtFrame(sprite, frameIndex, canvas) ? canvas : null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function sourceSize(source) {
|
|
272
|
+
return {
|
|
273
|
+
width: source.naturalWidth || source.width,
|
|
274
|
+
height: source.naturalHeight || source.height,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// `size` shrunk to fit MAX_CACHE_LONG_SIDE, keeping the aspect ratio. Returned
|
|
279
|
+
// unchanged when it already fits, which is the case for everything but a large
|
|
280
|
+
// uploaded image (so no pixel art is ever resampled by this).
|
|
281
|
+
function cappedSize({ width, height }) {
|
|
282
|
+
const longest = Math.max(width, height);
|
|
283
|
+
if (longest <= MAX_CACHE_LONG_SIDE) return { width, height };
|
|
284
|
+
const scale = MAX_CACHE_LONG_SIDE / longest;
|
|
285
|
+
return {
|
|
286
|
+
width: Math.max(1, Math.round(width * scale)),
|
|
287
|
+
height: Math.max(1, Math.round(height * scale)),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// The drawable source for one frame: a cached offscreen canvas, or -- for an
|
|
292
|
+
// untinted image -- the decoded <img> itself, since copying it into a canvas
|
|
293
|
+
// would buy nothing and cost a full-resolution bitmap. Null while an image is
|
|
294
|
+
// still downloading; the caller skips the draw and picks it up next frame.
|
|
235
295
|
function getSpriteCanvas(sprite, frameIndex, tint) {
|
|
296
|
+
const tintRgba = parseTint(tint);
|
|
297
|
+
if (isImageArt(sprite)) {
|
|
298
|
+
if (!sprite.ready) return null;
|
|
299
|
+
if (!tintRgba) return sprite.image;
|
|
300
|
+
}
|
|
236
301
|
const key = `${frameIndex}|${tint ?? ''}`;
|
|
237
302
|
let byKey = spriteCanvasCache.get(sprite);
|
|
238
303
|
if (!byKey) {
|
|
@@ -242,21 +307,24 @@ function getSpriteCanvas(sprite, frameIndex, tint) {
|
|
|
242
307
|
const cached = byKey.get(key);
|
|
243
308
|
if (cached) return cached;
|
|
244
309
|
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (tintRgba) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const image = octx.getImageData(0, 0, canvas.width, canvas.height);
|
|
256
|
-
tintImageData(image.data, tintRgba);
|
|
257
|
-
octx.putImageData(image, 0, 0);
|
|
258
|
-
}
|
|
310
|
+
const source = frameSource(sprite, frameIndex);
|
|
311
|
+
// Nothing to render yet -- don't cache the blank.
|
|
312
|
+
if (!source) return null;
|
|
313
|
+
const natural = sourceSize(source);
|
|
314
|
+
const size = cappedSize(natural);
|
|
315
|
+
// A copy is only worth making when it changes something: a tint to apply, or
|
|
316
|
+
// a bitmap too big to keep at full size. Otherwise the render IS the cache.
|
|
317
|
+
if (!tintRgba && size.width === natural.width) {
|
|
318
|
+
byKey.set(key, source);
|
|
319
|
+
return source;
|
|
259
320
|
}
|
|
321
|
+
const canvas = document.createElement('canvas');
|
|
322
|
+
canvas.width = size.width;
|
|
323
|
+
canvas.height = size.height;
|
|
324
|
+
const ctx = canvas.getContext('2d');
|
|
325
|
+
if (!ctx) return null;
|
|
326
|
+
ctx.drawImage(source, 0, 0, size.width, size.height);
|
|
327
|
+
if (tintRgba) tintCanvas(canvas, source, tintRgba);
|
|
260
328
|
byKey.set(key, canvas);
|
|
261
329
|
return canvas;
|
|
262
330
|
}
|
|
@@ -272,7 +340,7 @@ function resolveTag(sprite, tagProp) {
|
|
|
272
340
|
// The ordered list of frame indices to step through, honoring the tag range and
|
|
273
341
|
// direction. No tag => the whole timeline, forward.
|
|
274
342
|
function frameSequence(sprite, tag) {
|
|
275
|
-
const total = Math.max(
|
|
343
|
+
const total = Math.max(artFrameCount(sprite), 1);
|
|
276
344
|
let from = 0;
|
|
277
345
|
let to = total - 1;
|
|
278
346
|
let direction = 'forward';
|
|
@@ -352,6 +420,8 @@ function advanceAnim(state, sprite, sequence, tag, dt) {
|
|
|
352
420
|
state.frameIndex = sequence[state.pos];
|
|
353
421
|
}
|
|
354
422
|
|
|
355
|
-
|
|
356
|
-
|
|
423
|
+
// Everything the deck can point a Sprite at: `.pxart` drawings and image files
|
|
424
|
+
// alike, which is exactly what the resolved art map holds.
|
|
425
|
+
function getSpriteFiles(sprites) {
|
|
426
|
+
return Object.keys(sprites ?? {}).sort();
|
|
357
427
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { playTone, WAVEFORMS } from '../engine/tone';
|
|
3
|
+
import { Panel, SelectField } from '../engine/ui';
|
|
4
|
+
import { AutoFields, overrideProps } from '../engine/autoInspector';
|
|
5
|
+
|
|
6
|
+
// Plays a synthesized note -- no audio file involved. This is the cheapest sound
|
|
7
|
+
// a deck can make: a blip on a pickup, a thud on a miss, a rising scale as a
|
|
8
|
+
// counter climbs. Nothing is downloaded and nothing is stored.
|
|
9
|
+
//
|
|
10
|
+
// actor.runtime.tone.play(); // the note as configured
|
|
11
|
+
// actor.runtime.tone.play({ note: 72 }); // ...or override it per play
|
|
12
|
+
// await actor.runtime.tone.play(); // wait for the release to finish
|
|
13
|
+
//
|
|
14
|
+
// `note` is a MIDI number: 60 is middle C, 72 an octave up, +1 a semitone. The
|
|
15
|
+
// envelope is attack (silence up to full) then release (full down to silence),
|
|
16
|
+
// so the note's whole length is attack + release seconds.
|
|
17
|
+
export class Tone {
|
|
18
|
+
static behaviorName = 'Tone';
|
|
19
|
+
|
|
20
|
+
static defaultProps = {
|
|
21
|
+
note: 60,
|
|
22
|
+
waveform: 'square',
|
|
23
|
+
attack: 0,
|
|
24
|
+
release: 0.3,
|
|
25
|
+
volume: 0.5,
|
|
26
|
+
pan: 0,
|
|
27
|
+
playOnStart: false,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
constructor(props) {
|
|
31
|
+
this.props = props;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
update(actor) {
|
|
35
|
+
if (!actor.runtime || actor.runtime.collected) return;
|
|
36
|
+
const handle = actor.runtime.tone;
|
|
37
|
+
if (!handle) {
|
|
38
|
+
actor.runtime.tone = makeHandle(() => this.props);
|
|
39
|
+
if (this.props.playOnStart) void actor.runtime.tone.play();
|
|
40
|
+
} else {
|
|
41
|
+
handle.props = this.props;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static Inspector({ component, setComponent, override }) {
|
|
46
|
+
return (
|
|
47
|
+
<Panel title="Tone" overridden={override?.anyOverridden()}>
|
|
48
|
+
<SelectField
|
|
49
|
+
label="Waveform"
|
|
50
|
+
value={component.waveform}
|
|
51
|
+
onChange={(waveform) => setComponent({ waveform })}
|
|
52
|
+
options={WAVEFORMS}
|
|
53
|
+
{...overrideProps(override, 'waveform')}
|
|
54
|
+
/>
|
|
55
|
+
<AutoFields
|
|
56
|
+
defaultProps={Tone.defaultProps}
|
|
57
|
+
component={component}
|
|
58
|
+
setComponent={setComponent}
|
|
59
|
+
only={['note', 'attack', 'release', 'volume', 'pan', 'playOnStart']}
|
|
60
|
+
override={override}
|
|
61
|
+
/>
|
|
62
|
+
</Panel>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// `actor.runtime.tone`. `play(overrides)` takes the same fields as the props, so
|
|
68
|
+
// one Tone actor can cover a whole scale without a component per note.
|
|
69
|
+
function makeHandle(currentProps) {
|
|
70
|
+
const handle = {
|
|
71
|
+
props: currentProps(),
|
|
72
|
+
voice: null,
|
|
73
|
+
play(overrides) {
|
|
74
|
+
handle.voice = playTone({ ...handle.props, ...overrides });
|
|
75
|
+
return handle.voice.finished;
|
|
76
|
+
},
|
|
77
|
+
stop() {
|
|
78
|
+
handle.voice?.stop();
|
|
79
|
+
handle.voice = null;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
return handle;
|
|
83
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { resolveDeckFile } from 'castle-web-sdk';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { mediaElement, playMedia, stopMedia } from '../engine/media';
|
|
4
|
+
import { mediaFilesOfKind } from '../engine/files';
|
|
5
|
+
import { spriteDestRect } from '../engine/spriteGeometry';
|
|
6
|
+
import { Panel, SelectField } from '../engine/ui';
|
|
7
|
+
import { AutoFields, overrideProps } from '../engine/autoInspector';
|
|
8
|
+
|
|
9
|
+
// Plays a video file (.mp4 / .webm / .mov / .m4v) inside the actor's Layout box.
|
|
10
|
+
//
|
|
11
|
+
// The video is decoded by the browser and blitted into the scene canvas every
|
|
12
|
+
// frame, so it sits in the scene like any other actor: it moves with Layout,
|
|
13
|
+
// rotates, draws in `z` order, and can have a Collider. `mode` frames it in the
|
|
14
|
+
// box the same way a Sprite does (cover / fit / stretch).
|
|
15
|
+
//
|
|
16
|
+
// In the editor it holds its first frame -- `update` is what starts playback and
|
|
17
|
+
// only play mode runs it -- so you can position it against the real picture.
|
|
18
|
+
// Sound is muted by default: an autoplaying video with sound is blocked by
|
|
19
|
+
// browsers, and a muted one isn't.
|
|
20
|
+
export class Video {
|
|
21
|
+
static behaviorName = 'Video';
|
|
22
|
+
|
|
23
|
+
static defaultProps = {
|
|
24
|
+
file: '',
|
|
25
|
+
mode: 'cover',
|
|
26
|
+
playing: true,
|
|
27
|
+
loop: true,
|
|
28
|
+
muted: true,
|
|
29
|
+
volume: 1,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
constructor(props) {
|
|
33
|
+
this.props = props;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
element(actor) {
|
|
37
|
+
return mediaElement(actor.id, 'video', resolveDeckFile(this.props.file));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
update(actor) {
|
|
41
|
+
if (!actor.runtime || actor.runtime.collected) return;
|
|
42
|
+
const element = this.element(actor);
|
|
43
|
+
if (!element) return;
|
|
44
|
+
element.loop = Boolean(this.props.loop);
|
|
45
|
+
element.muted = Boolean(this.props.muted);
|
|
46
|
+
element.volume = Number.isFinite(this.props.volume)
|
|
47
|
+
? Math.min(1, Math.max(0, this.props.volume))
|
|
48
|
+
: 1;
|
|
49
|
+
if (this.props.playing) playMedia(element);
|
|
50
|
+
else if (!element.paused) element.pause();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
draw(actor, scene, ctx) {
|
|
54
|
+
if (actor.runtime?.collected) return;
|
|
55
|
+
const layout = actor.components.Layout;
|
|
56
|
+
if (!layout) return;
|
|
57
|
+
const element = this.element(actor);
|
|
58
|
+
// HAVE_CURRENT_DATA: there is a frame to draw. Below it the element has
|
|
59
|
+
// nothing decoded yet and drawImage would throw.
|
|
60
|
+
if (!element || element.readyState < 2) return;
|
|
61
|
+
const size = { width: element.videoWidth, height: element.videoHeight };
|
|
62
|
+
if (!size.width || !size.height) return;
|
|
63
|
+
const dest = spriteDestRect(this.props.mode, layout, size);
|
|
64
|
+
const covering = this.props.mode === 'cover';
|
|
65
|
+
if (covering) {
|
|
66
|
+
// `cover` fills the box by overflowing it; the overflow is cropped.
|
|
67
|
+
ctx.save();
|
|
68
|
+
ctx.beginPath();
|
|
69
|
+
ctx.rect(layout.x, layout.y, layout.width, layout.height);
|
|
70
|
+
ctx.clip();
|
|
71
|
+
}
|
|
72
|
+
ctx.drawImage(element, dest.x, dest.y, dest.width, dest.height);
|
|
73
|
+
if (covering) ctx.restore();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Used by the inspector's "stop" affordance and by game code that wants the
|
|
77
|
+
// element itself (`scene.getActor('clip').components.Video` is props; this is
|
|
78
|
+
// the live element).
|
|
79
|
+
static stop(actor) {
|
|
80
|
+
const element = mediaElement(actor.id, 'video', actor.components?.Video?.file);
|
|
81
|
+
if (element) stopMedia(element);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static Inspector({ component, setComponent, override }) {
|
|
85
|
+
return (
|
|
86
|
+
<Panel title="Video" overridden={override?.anyOverridden()}>
|
|
87
|
+
<SelectField
|
|
88
|
+
label="File"
|
|
89
|
+
value={component.file}
|
|
90
|
+
onChange={(file) => setComponent({ file })}
|
|
91
|
+
options={mediaFilesOfKind('video')}
|
|
92
|
+
{...overrideProps(override, 'file')}
|
|
93
|
+
/>
|
|
94
|
+
<SelectField
|
|
95
|
+
label="Mode"
|
|
96
|
+
value={component.mode}
|
|
97
|
+
onChange={(mode) => setComponent({ mode })}
|
|
98
|
+
options={['stretch', 'fit', 'cover']}
|
|
99
|
+
{...overrideProps(override, 'mode')}
|
|
100
|
+
/>
|
|
101
|
+
<AutoFields
|
|
102
|
+
defaultProps={Video.defaultProps}
|
|
103
|
+
component={component}
|
|
104
|
+
setComponent={setComponent}
|
|
105
|
+
only={['playing', 'loop', 'muted', 'volume']}
|
|
106
|
+
override={override}
|
|
107
|
+
/>
|
|
108
|
+
</Panel>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -35,13 +35,28 @@ export function applyTint(color, tint) {
|
|
|
35
35
|
return '#' + out.map((channel) => channel.toString(16).padStart(2, '0')).join('');
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// Multiply
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
38
|
+
// Multiply a tint into an already-drawn canvas, using the compositor rather
|
|
39
|
+
// than a pixel loop. `source` is the untinted art the canvas was drawn from
|
|
40
|
+
// (an <img> or a canvas), redrawn at the end to restore the alpha channel.
|
|
41
|
+
//
|
|
42
|
+
// Two steps, and the second is not optional: `multiply` blends alpha as well as
|
|
43
|
+
// color, so the fill lands on the transparent pixels too and the sprite comes
|
|
44
|
+
// out as a solid rectangle. `destination-in` with the original puts the shape
|
|
45
|
+
// back. The tint's own alpha rides in on that same pass.
|
|
46
|
+
//
|
|
47
|
+
// This replaced a getImageData / multiply-in-JS / putImageData round trip, which
|
|
48
|
+
// costs the same on a 16x16 pixel sprite and 3-5x more on an uploaded photo --
|
|
49
|
+
// the bitmap has to leave the GPU and come back. Output is byte-identical,
|
|
50
|
+
// checked at both opaque and translucent tints.
|
|
51
|
+
export function tintCanvas(canvas, source, tint) {
|
|
52
|
+
const ctx = canvas.getContext('2d');
|
|
53
|
+
if (!ctx || canvas.width === 0 || canvas.height === 0) return;
|
|
54
|
+
ctx.globalCompositeOperation = 'multiply';
|
|
55
|
+
ctx.fillStyle = `rgb(${tint[0]},${tint[1]},${tint[2]})`;
|
|
56
|
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
57
|
+
ctx.globalCompositeOperation = 'destination-in';
|
|
58
|
+
ctx.globalAlpha = tint[3] / 255;
|
|
59
|
+
ctx.drawImage(source, 0, 0, canvas.width, canvas.height);
|
|
60
|
+
ctx.globalAlpha = 1;
|
|
61
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
47
62
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
{
|
|
5
5
|
"components": {
|
|
6
6
|
"Layout": { "width": 44, "height": 44, "z": 2 },
|
|
7
|
-
"Sprite": { "file": "drawings/cauldron.
|
|
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.
|
|
7
|
+
"Sprite": { "file": "drawings/block.sprite", "mode": "tile", "tileSize": 20 },
|
|
8
8
|
"Collider": { "bounciness": 0.4, "friction": 0.2 }
|
|
9
9
|
}
|
|
10
10
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
{
|
|
5
5
|
"components": {
|
|
6
6
|
"Layout": { "width": 48, "height": 48, "z": 2 },
|
|
7
|
-
"Sprite": { "file": "drawings/block.
|
|
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": {}
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"drawings/**",
|
|
23
23
|
"scenes/**",
|
|
24
24
|
"blueprints/**",
|
|
25
|
-
"behaviors/**"
|
|
25
|
+
"behaviors/**",
|
|
26
|
+
"assets/**"
|
|
26
27
|
],
|
|
27
28
|
"fileTypes": [
|
|
28
29
|
{
|
|
@@ -33,10 +34,17 @@
|
|
|
33
34
|
"editor": "kit",
|
|
34
35
|
"data": true
|
|
35
36
|
},
|
|
37
|
+
{
|
|
38
|
+
"ext": ".sprite",
|
|
39
|
+
"label": "Sprite",
|
|
40
|
+
"new": "New sprite",
|
|
41
|
+
"icon": "layer-group",
|
|
42
|
+
"editor": "kit",
|
|
43
|
+
"data": true
|
|
44
|
+
},
|
|
36
45
|
{
|
|
37
46
|
"ext": ".pxart",
|
|
38
|
-
"label": "
|
|
39
|
-
"new": "New drawing",
|
|
47
|
+
"label": "Sprite",
|
|
40
48
|
"icon": "layer-group",
|
|
41
49
|
"editor": "kit",
|
|
42
50
|
"data": true
|
|
@@ -46,12 +54,74 @@
|
|
|
46
54
|
"label": "Behavior",
|
|
47
55
|
"new": "New behavior",
|
|
48
56
|
"icon": "code"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"ext": ".png",
|
|
60
|
+
"label": "Image",
|
|
61
|
+
"icon": "image",
|
|
62
|
+
"editor": "kit"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"ext": ".jpg",
|
|
66
|
+
"label": "Image",
|
|
67
|
+
"icon": "image",
|
|
68
|
+
"editor": "kit"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"ext": ".jpeg",
|
|
72
|
+
"label": "Image",
|
|
73
|
+
"icon": "image",
|
|
74
|
+
"editor": "kit"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"ext": ".gif",
|
|
78
|
+
"label": "Image",
|
|
79
|
+
"icon": "image",
|
|
80
|
+
"editor": "kit"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"ext": ".webp",
|
|
84
|
+
"label": "Image",
|
|
85
|
+
"icon": "image",
|
|
86
|
+
"editor": "kit"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"ext": ".svg",
|
|
90
|
+
"label": "Image",
|
|
91
|
+
"icon": "image",
|
|
92
|
+
"editor": "kit"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"ext": ".mp3",
|
|
96
|
+
"label": "Audio",
|
|
97
|
+
"icon": "music",
|
|
98
|
+
"editor": "kit"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"ext": ".wav",
|
|
102
|
+
"label": "Audio",
|
|
103
|
+
"icon": "music",
|
|
104
|
+
"editor": "kit"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"ext": ".m4a",
|
|
108
|
+
"label": "Audio",
|
|
109
|
+
"icon": "music",
|
|
110
|
+
"editor": "kit"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"ext": ".mp4",
|
|
114
|
+
"label": "Video",
|
|
115
|
+
"icon": "film",
|
|
116
|
+
"editor": "kit"
|
|
49
117
|
}
|
|
50
118
|
],
|
|
51
119
|
"defaultPlayFile": "scenes/main.scene"
|
|
52
120
|
},
|
|
53
121
|
"deckId": "ckRZGFW4iPrx",
|
|
54
122
|
"cardId": "_oBFbAW6DxsO",
|
|
123
|
+
"main": "main.jsx",
|
|
124
|
+
"autoUpdateWhenImported": true,
|
|
55
125
|
"title": "physics-2d",
|
|
56
|
-
"publishedVersion": "2026-08-
|
|
126
|
+
"publishedVersion": "2026-08-08T19:00:43.040Z"
|
|
57
127
|
}
|