@cyberart-io/engine 0.0.5 → 0.0.7
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/README.md +117 -14
- package/dist/headless.d.ts +483 -6
- package/dist/headless.js +1 -1
- package/dist/index.d.ts +883 -6
- package/dist/index.js +1 -1
- package/docs/audio.md +152 -0
- package/docs/calculation-carts.md +134 -0
- package/docs/capability-manifest.md +5 -2
- package/docs/compositor.md +8 -3
- package/docs/executable-modules.md +112 -0
- package/docs/midi.md +128 -0
- package/docs/runtime-group.md +3 -3
- package/docs/snapshots.md +102 -0
- package/docs/visual-layers.md +151 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# `@cyberart-io/engine`
|
|
2
2
|
|
|
3
|
-
An engine for building animated, interactive, generative programs for the web. You write a **cart** — a small object with `getDefaultState` (initial state), `update` (advance that state each tick), and `render` (draw it).
|
|
3
|
+
An engine for building animated, interactive, generative programs for the web. You write a **cart** — a small object with `getDefaultState` (initial state), `update` (advance that state each tick), and optional `render` (draw it). A render cart is a state machine in a loop that paints to the screen. A calculation cart runs the same state machine without a canvas, and talks to a sibling that does paint. The engine mounts it into a DOM element you own, runs the loop, and gives you pause, snapshot, events, and save/load.
|
|
4
4
|
|
|
5
5
|
## License
|
|
6
6
|
|
|
@@ -94,11 +94,17 @@ Full API for the event router, deterministic replay, and CI harness (so agents c
|
|
|
94
94
|
- [Asset resolver](docs/asset-resolver.md) — host-pluggable images/audio/fonts/spritesheets, cache, preload, fallbacks
|
|
95
95
|
- [Presentation cue](docs/presentation-cue.md) — deterministic `createPresentationTimeline`, duplicate policy, reduced-motion, lifecycle events
|
|
96
96
|
- [Capability manifest](docs/capability-manifest.md) — versioned JSON for runtime features, phases, managers, assets, events, permissions, integrations
|
|
97
|
+
- [Executable modules](docs/executable-modules.md) — trusted versioned factories, host allowlists, isolation, per-module failures
|
|
97
98
|
- [Normalized geometry](docs/normalized-geometry.md) — coordinate spaces, contain/cover/crop layout, landmarks, hit regions, debug overlay
|
|
98
99
|
- [Runtime group](docs/runtime-group.md) — `createRuntimeGroup`, shared router attach, lockstep clock; `createHeadlessMultiCartHarness` from `@cyberart-io/engine/headless`
|
|
100
|
+
- [Calculation carts](docs/calculation-carts.md) — `kind: 'calculation'`, update + events without paint; sibling render carts still paint
|
|
99
101
|
- [Compositor](docs/compositor.md) — `createCompositor`, transparent stacking, `screen` blend, `writeComposedFrame`
|
|
102
|
+
- [Visual layers](docs/visual-layers.md) — `createVisualLayerController`, versioned overlay `v1`/`v2`, deterministic swaps, `hostState` JSON
|
|
100
103
|
- [Browser harness](docs/browser-harness.md) — `createBrowserHarness`, DOM clicks, viewport/DPR, composed screenshots
|
|
101
104
|
- [Replay inspector](docs/replay-inspector.md) — `createReplayInspector`, causation trees, redacted export/import, headless replay
|
|
105
|
+
- [MIDI](docs/midi.md) — host-owned `MidiManager`, note/CC/pitch in and out, `inject` / fake port, structured `requestAccess`
|
|
106
|
+
- [Snapshots](docs/snapshots.md) — versioned envelope, `defineSnapshot` / `applySnapshotMigrations`, host-owned `hostState`
|
|
107
|
+
- [Audio](docs/audio.md) — `createAudioBroker`, `createAudioCueTimeline`, headless cue traces (PCM is not deterministic)
|
|
102
108
|
|
|
103
109
|
## Write a cart
|
|
104
110
|
|
|
@@ -108,18 +114,20 @@ A cart is an `AnimationCart`. Required:
|
|
|
108
114
|
|---|---|
|
|
109
115
|
| `getDefaultState` | Build the initial `state`. Use `R` (`Random`) for anything that should follow the seed. |
|
|
110
116
|
| `update` | Advance `state` each tick. Return the next state. |
|
|
111
|
-
| `render` | Draw into the 2D context (and optional `ImageData`). |
|
|
112
117
|
| `metadata.id` / `name` / `frameRate` | Identity and loop rate. |
|
|
113
118
|
|
|
119
|
+
`render` draws into the 2D context (and optional `ImageData`). Omit it on calculation carts (`createRuntime({ kind: 'calculation' })` or a group participant with `kind: 'calculation'`). The runtime does not call `render` and does not create a canvas for that kind. Render carts (the default) still paint as before.
|
|
120
|
+
|
|
114
121
|
Useful optionals:
|
|
115
122
|
|
|
116
123
|
| Piece | Role |
|
|
117
124
|
|---|---|
|
|
125
|
+
| `render` | Draw into the 2D context. Omit on calculation carts; the runtime will not call a no-op either. |
|
|
118
126
|
| `teardown` | Dispose long-lived resources (Tone nodes, listeners) when the cart unloads. |
|
|
119
127
|
| `metadata.audio` | `'tone'` if the piece needs Web Audio. Omit for a silent cart. |
|
|
120
128
|
| `metadata.generative` | `true` when output is a function of the token hash. Saves then only load on the same seed. |
|
|
121
129
|
|
|
122
|
-
Callbacks also receive `DimensionContext` (`width` / `height` and derived size fields), `KeyboardManager`, `PointerManager`, and an optional `HostChannel`. You do not have to use them. Types live on the package: `AnimationCart`, `Random`, `DimensionContext`, `KeyboardManager`, `PointerManager`, `HostChannel`.
|
|
130
|
+
Callbacks also receive `DimensionContext` (`width` / `height` and derived size fields), `KeyboardManager`, `PointerManager`, and an optional `HostChannel`. You do not have to use them. Types live on the package: `AnimationCart`, `CartKind`, `Random`, `DimensionContext`, `KeyboardManager`, `PointerManager`, `HostChannel`.
|
|
123
131
|
|
|
124
132
|
`Random` is seed-stable: `R.dec(min, max)`, `R.int(min, max)`, `R.bool()`, `R.choose(list)`.
|
|
125
133
|
|
|
@@ -153,6 +161,9 @@ const cart = runtime.mount(artProject, {
|
|
|
153
161
|
| `audio` | none | Libraries to unlock if `unlockAudio()` runs before `mount`. After mount, the cart’s `metadata.audio` wins. |
|
|
154
162
|
| `deterministic` | off | Host-controlled clock, `step`/`advance`, and scripted input/assets. Leave unset for live kaleidoscope / Art Blocks. |
|
|
155
163
|
| `assets` | off | Host `AssetResolver` plus engine cache/preload. Carts keep logical refs. Leave unset when the piece has no media. |
|
|
164
|
+
| `audioBroker` | off | Shared `createAudioBroker` instance. Optional. Omit when the cart only uses `metadata.audio: 'tone'`. |
|
|
165
|
+
| `audioParticipantId` | none | Group participant id to authorize / teardown on this runtime. |
|
|
166
|
+
| `kind` | `'render'` | `'calculation'` skips canvas construction and paint. `getDefaultState`, `update`, and host-channel events still run. |
|
|
156
167
|
|
|
157
168
|
`CartHandle` (what `mount` returns):
|
|
158
169
|
|
|
@@ -165,8 +176,10 @@ const cart = runtime.mount(artProject, {
|
|
|
165
176
|
| `step(frames)` / `advance(ms)` | Deterministic ticks only. Throw if `deterministic` was not set. |
|
|
166
177
|
| `schedule(action)` | Queue a pointer/key/host-event/asset for a future frame. |
|
|
167
178
|
| `getClock()` / `getRandomState()` / `getReplayMetadata()` | Replay inspection. |
|
|
168
|
-
| `exportState()` / `exportStateJSON()` | Pause-safe serializable
|
|
169
|
-
| `importState(bundle)` | Restore a bundle
|
|
179
|
+
| `exportState()` / `exportStateJSON()` | Pause-safe serializable `CartStateBundle` (schema 1 blob). |
|
|
180
|
+
| `importState(bundle)` | Restore a bundle, JSON string, or snapshot envelope (migrates then loads `engineState`). |
|
|
181
|
+
| `exportSnapshot()` / `exportSnapshotJSON()` | Schema 2 envelope wrapping the bundle plus clock, rng, optional `hostState`. |
|
|
182
|
+
| `importSnapshot(envelope)` | Migrate to the current schema and restore `engineState`. Hosts reapply `hostState`. |
|
|
170
183
|
| `destroy()` | Unload this cart. Idempotent. |
|
|
171
184
|
| `needsAudio` | True when the cart declared audio libraries. Use this to show a click overlay. |
|
|
172
185
|
| `paused` / `isPrepared` / `isLoopRunning` | Loop flags. |
|
|
@@ -174,9 +187,9 @@ const cart = runtime.mount(artProject, {
|
|
|
174
187
|
| `tokenData` | Live hash and token id. |
|
|
175
188
|
| `getCartState()` | Live object for debug UI. Not JSON-safe — use export/import for saves. |
|
|
176
189
|
|
|
177
|
-
`runtime.destroy()` tears down the runtime. `runtime.unlockAudio()` is for an early click before `mount`. `runtime.onError` receives frame errors (`phase`, `consecutive`, `stopped`).
|
|
190
|
+
`runtime.destroy()` tears down the runtime. `runtime.unlockAudio()` is for an early click before `mount`. `runtime.onError` receives frame errors (`phase`, `consecutive`, `stopped`). `runtime.kind` is `'render'` (default) or `'calculation'`.
|
|
178
191
|
|
|
179
|
-
One cart per runtime. A second `mount` unloads the first. Several pieces on a page means several `createRuntime()` calls, or one `createRuntimeGroup()` that attaches each mailbox to a shared router and locksteps a deterministic clock.
|
|
192
|
+
One cart per runtime. A second `mount` unloads the first. Several pieces on a page means several `createRuntime()` calls, or one `createRuntimeGroup()` that attaches each mailbox to a shared router and locksteps a deterministic clock. Calculation participants (`kind: 'calculation'`) share that group without a canvas; see [calculation carts](docs/calculation-carts.md).
|
|
180
193
|
|
|
181
194
|
## Audio
|
|
182
195
|
|
|
@@ -199,6 +212,14 @@ Install the peer: `npm i tone@^14.8.15`. Call `cart.start()` **inside a click or
|
|
|
199
212
|
|
|
200
213
|
If a click can happen before `mount` finishes, pass the same `audio` into `createRuntime` and/or call `runtime.unlockAudio()` from that click. After mount, the cart’s metadata wins.
|
|
201
214
|
|
|
215
|
+
Several carts on one page should share **one** `createAudioBroker` (pass it to `createRuntime` / `createRuntimeGroup`). One `unlock()` covers authorized participants. Channel gain, mute, duck, and priority live on the broker; cue timing is `createAudioCueTimeline` / `scheduleAudioCue` (presentation-frame clock). Headless CI records cue events with `createHeadlessAudioAdapter` from `@cyberart-io/engine/headless` — the event trace is deterministic, PCM is not. Full API: [audio](docs/audio.md).
|
|
216
|
+
|
|
217
|
+
## MIDI
|
|
218
|
+
|
|
219
|
+
Host-owned `MidiManager` — carts and hosts construct it. The runtime does not pass it into `getDefaultState`. Subscribe to note / CC / pitch, `inject` for tests without hardware, and `send` note-on/off, CC, or raw bytes through an injectable port. `requestAccess()` wraps `navigator.requestMIDIAccess` when present; a missing API or denied permission is a structured result, so silent carts keep running. Headless/jsdom has no Web MIDI; inject and send still work via a test port.
|
|
220
|
+
|
|
221
|
+
Full API and the reproduce command: [MIDI](docs/midi.md). Capability manifests already list `midi` as an integration.
|
|
222
|
+
|
|
202
223
|
## Seed
|
|
203
224
|
|
|
204
225
|
Pass `seed` when you want a deterministic output (for example from `?hash=`):
|
|
@@ -358,6 +379,40 @@ validateCapabilityManifest(defined.manifest, {
|
|
|
358
379
|
|
|
359
380
|
Fields, host allowlists, and diagnostic codes: [capability manifest](docs/capability-manifest.md).
|
|
360
381
|
|
|
382
|
+
## Executable modules
|
|
383
|
+
|
|
384
|
+
Trusted factories keyed by exact `id` + `version`. The host allowlists which refs may load. There is no `eval` / `new Function` path for cart-supplied strings.
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
import { createExecutableModuleHost } from '@cyberart-io/engine';
|
|
388
|
+
|
|
389
|
+
const host = createExecutableModuleHost({
|
|
390
|
+
allowlist: [
|
|
391
|
+
{ id: 'overlay-fx', version: '1.0.0' },
|
|
392
|
+
{ id: 'host.module', version: '1.0.0' },
|
|
393
|
+
],
|
|
394
|
+
limits: { maxInvokeMs: 16, maxInvokesPerTurn: 4 },
|
|
395
|
+
modules: [
|
|
396
|
+
{
|
|
397
|
+
id: 'overlay-fx',
|
|
398
|
+
version: '1.0.0',
|
|
399
|
+
create: (capabilities) => ({
|
|
400
|
+
invoke: (input, { signal }) => {
|
|
401
|
+
if (signal.aborted) return;
|
|
402
|
+
return { input, seed: capabilities.seed };
|
|
403
|
+
},
|
|
404
|
+
}),
|
|
405
|
+
},
|
|
406
|
+
],
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
const allowed = await host.invoke({ id: 'overlay-fx', version: '1.0.0' });
|
|
410
|
+
await host.invoke({ id: 'overlay-fx', version: '2.0.0' }); // version-mismatch, not invoked
|
|
411
|
+
host.destroy();
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Unknown ids, wrong versions, and refs missing from the allowlist fail closed. A throwing module returns `{ ok: false, error }` and the next allowlisted invoke still runs. Isolation is in-process (trusted factories), not a worker or iframe. Carts may declare required refs as optional `modules.refs` on a [capability manifest](docs/capability-manifest.md). Full API and the reproduce command: [executable modules](docs/executable-modules.md).
|
|
415
|
+
|
|
361
416
|
## Runtime group
|
|
362
417
|
|
|
363
418
|
Several production carts, one router, one lockstep clock. Use this instead of intercepting each `onEvent` and republishing by hand.
|
|
@@ -368,17 +423,57 @@ import { createRuntimeGroup } from '@cyberart-io/engine';
|
|
|
368
423
|
const group = createRuntimeGroup({
|
|
369
424
|
origin: 0,
|
|
370
425
|
participants: [
|
|
371
|
-
{ id: '
|
|
372
|
-
{ id: '
|
|
426
|
+
{ id: 'world', cart: worldCart, kind: 'calculation', emit: ['host.intent.*'], subscribe: ['host.state.*'] },
|
|
427
|
+
{ id: 'overlay', cart: overlayCart, subscribe: ['host.intent.*'] },
|
|
373
428
|
],
|
|
374
429
|
});
|
|
375
|
-
group.publish({ type: 'host.state.
|
|
430
|
+
group.publish({ type: 'host.state.scene-changed', kind: 'state', payload: { sceneId: 'alpha' } });
|
|
376
431
|
await group.step(2);
|
|
377
432
|
const { trace } = await group.inspect();
|
|
378
433
|
group.destroy();
|
|
379
434
|
```
|
|
380
435
|
|
|
381
|
-
Headless / CI: `createHeadlessMultiCartHarness` from `@cyberart-io/engine/headless` is the same handle after `installHeadlessCanvas()`. Full options: [runtime group](docs/runtime-group.md). Causation trees, redaction, and tape replay: [replay inspector](docs/replay-inspector.md).
|
|
436
|
+
Headless / CI: `createHeadlessMultiCartHarness` from `@cyberart-io/engine/headless` is the same handle after `installHeadlessCanvas()`. Full options: [runtime group](docs/runtime-group.md). Calculation carts (no canvas / paint): [calculation carts](docs/calculation-carts.md). Causation trees, redaction, and tape replay: [replay inspector](docs/replay-inspector.md).
|
|
437
|
+
|
|
438
|
+
## Visual layers
|
|
439
|
+
|
|
440
|
+
Durable overlay / mask / sprite versions (`v1`, `v2`) with deterministic show, hide, replace, and crossfade. An accepted `host.state.accepted` (`sceneId: 'alpha'`) runs a presentation cue; the compositor reveals a **preloaded** alternate source in one `setLayer`. Failed assets keep the prior valid frame and emit `visual.layer.failed`. Persist `controller.snapshot()` in envelope `hostState` — do not put pixels in the envelope.
|
|
441
|
+
|
|
442
|
+
```ts
|
|
443
|
+
import {
|
|
444
|
+
ASSET_READY_EVENT,
|
|
445
|
+
HOST_STATE_ACCEPTED_EVENT,
|
|
446
|
+
createVisualLayerController,
|
|
447
|
+
} from '@cyberart-io/engine';
|
|
448
|
+
|
|
449
|
+
const layers = createVisualLayerController({
|
|
450
|
+
compositor,
|
|
451
|
+
sceneId: 'alpha',
|
|
452
|
+
layers: [
|
|
453
|
+
{
|
|
454
|
+
id: 'overlay',
|
|
455
|
+
kind: 'layer',
|
|
456
|
+
initialVersion: 'v1',
|
|
457
|
+
versions: [
|
|
458
|
+
{ id: 'v1', assetId: 'overlay.v1' },
|
|
459
|
+
{ id: 'v2', assetId: 'overlay.v2' },
|
|
460
|
+
],
|
|
461
|
+
},
|
|
462
|
+
],
|
|
463
|
+
onAccepted: [{ layerId: 'overlay', toVersion: 'v2', kind: 'replace', durationFrames: 2 }],
|
|
464
|
+
});
|
|
465
|
+
layers.registerSource('overlay.v1', imageV1);
|
|
466
|
+
layers.registerSource('overlay.v2', imageV2);
|
|
467
|
+
layers.handleHostEvent({ type: ASSET_READY_EVENT, kind: 'state', payload: { id: 'overlay.v2' } });
|
|
468
|
+
layers.handleHostEvent({
|
|
469
|
+
type: HOST_STATE_ACCEPTED_EVENT,
|
|
470
|
+
kind: 'state',
|
|
471
|
+
payload: { sceneId: 'alpha' },
|
|
472
|
+
});
|
|
473
|
+
layers.step(2);
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Headless capture: `captureVisualLayers(layers)` from `@cyberart-io/engine` or `@cyberart-io/engine/headless`. Full API: [visual layers](docs/visual-layers.md).
|
|
382
477
|
|
|
383
478
|
## Normalized geometry
|
|
384
479
|
|
|
@@ -476,14 +571,22 @@ The same declarations produce fixture URLs locally and CDN URLs (plus a typed CO
|
|
|
476
571
|
|
|
477
572
|
## Save and load
|
|
478
573
|
|
|
479
|
-
Live `state` is not JSON-safe (audio nodes, managers, typed arrays, possible cycles). `snapshot()` is PNG + seed. Use export/import when you want the simulation itself
|
|
574
|
+
Live `state` is not JSON-safe (audio nodes, managers, typed arrays, possible cycles). `snapshot()` is PNG + seed. Use export/import when you want the simulation itself.
|
|
575
|
+
|
|
576
|
+
`exportState` still returns the engine-owned `CartStateBundle` (`CART_STATE_BUNDLE_VERSION = 1`) so existing carts keep working. The versioned envelope (`SNAPSHOT_SCHEMA_VERSION = 2`) wraps that blob in `engineState` and keeps host-owned JSON in `hostState` (or a `hostStateRef`). Hosts persist the envelope; Cyberart is not the database. Full field list, migration registry, and diagnostic codes: [versioned snapshots](docs/snapshots.md).
|
|
480
577
|
|
|
481
578
|
```ts
|
|
482
579
|
const bundle = await cart.exportState();
|
|
483
580
|
const json = await cart.exportStateJSON();
|
|
581
|
+
const envelope = await cart.exportSnapshot({
|
|
582
|
+
cartVersion: '3',
|
|
583
|
+
hostState: { sceneId: 'alpha' },
|
|
584
|
+
});
|
|
484
585
|
|
|
485
586
|
await cart.importState(bundle);
|
|
486
587
|
await cart.importState(json);
|
|
588
|
+
await cart.importState(envelope);
|
|
589
|
+
await cart.importSnapshot(envelope);
|
|
487
590
|
```
|
|
488
591
|
|
|
489
592
|
Both pause, wait until any in-flight `update` finishes, then restore the previous pause flag. Import tears down, runs `getDefaultState` again (fresh managers, empty audio graph), overlays the save onto that scaffold unless `getDefaultState` already returned the revived `customState`, and restores `framesElapsed`. Works on a prepared cart before `start()`. Throws if nothing is prepared, or if the bundle is incompatible (`IncompatibleCartStateError`).
|
|
@@ -536,7 +639,7 @@ The engine prefers, in order:
|
|
|
536
639
|
2. The container’s first `<canvas>`
|
|
537
640
|
3. A canvas it creates (no `id="canvas"`)
|
|
538
641
|
|
|
539
|
-
A canvas the host adopted is left in place on destroy; a canvas the engine created is removed. Both `cart.destroy()` and `runtime.destroy()` are idempotent.
|
|
642
|
+
A canvas the host adopted is left in place on destroy; a canvas the engine created is removed. Both `cart.destroy()` and `runtime.destroy()` are idempotent. `createRuntime({ kind: 'calculation' })` never creates or adopts a canvas (`cart.canvas` is `undefined`).
|
|
540
643
|
|
|
541
644
|
## Limits
|
|
542
645
|
|
|
@@ -547,7 +650,7 @@ A canvas the host adopted is left in place on destroy; a canvas the engine creat
|
|
|
547
650
|
|
|
548
651
|
## Publishing this package (maintainers)
|
|
549
652
|
|
|
550
|
-
Not part of writing a cart. Engine source lives in `packages/engine/src/` (not mixed into the site). The npm tarball is built from `packages/engine/src/index.ts` and `packages/engine/src/headless.ts` and contains minified `dist/index.js` + `dist/headless.js`, rolled-up `.d.ts` for both, `LICENSE`, `README.md`, `docs/` (including compositor, browser harness, and
|
|
653
|
+
Not part of writing a cart. Engine source lives in `packages/engine/src/` (not mixed into the site). The npm tarball is built from `packages/engine/src/index.ts` and `packages/engine/src/headless.ts` and contains minified `dist/index.js` + `dist/headless.js`, rolled-up `.d.ts` for both, `LICENSE`, `README.md`, `docs/` (including compositor, visual layers, browser harness, replay inspector, MIDI, executable modules, snapshots, audio, and calculation carts), and `package.json`. Site code that still imports `src/ui/lib/...` hits thin re-export shims so those paths keep working.
|
|
551
654
|
|
|
552
655
|
```bash
|
|
553
656
|
pnpm run pack:engine
|