@cyberart-io/engine 0.0.1 → 0.0.3
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 +193 -11
- package/dist/headless.d.ts +637 -0
- package/dist/headless.js +8 -0
- package/dist/index.d.ts +753 -8
- package/dist/index.js +1 -1
- package/docs/asset-resolver.md +155 -0
- package/docs/deterministic-mode.md +132 -0
- package/docs/events.md +272 -0
- package/docs/headless-harness.md +186 -0
- package/docs/presentation-adapter.md +150 -0
- package/docs/presentation-cue.md +82 -0
- package/package.json +15 -3
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ This guide is the path from `npm i` to a piece on screen, then a short map of th
|
|
|
24
24
|
npm i @cyberart-io/engine
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
The package is minified JS plus
|
|
27
|
+
The package is minified JS plus rolled-up `.d.ts` for two entrypoints: the browser runtime (`.`) and Node/jsdom helpers (`./headless`). It does not include animation carts. Tone.js is an optional peer — skip it until a cart declares `audio: 'tone'`.
|
|
28
28
|
|
|
29
29
|
## Quick start
|
|
30
30
|
|
|
@@ -83,6 +83,17 @@ window.addEventListener('pagehide', () => {
|
|
|
83
83
|
|
|
84
84
|
`createRuntime` and `mount` are synchronous. `mount` **prepares** the cart (`getDefaultState`) but does not start the loop — that is `start()`. If prepare fails, `mount` throws.
|
|
85
85
|
|
|
86
|
+
## Feature guides
|
|
87
|
+
|
|
88
|
+
Full API for the event router, deterministic replay, and CI harness (so agents can use every export):
|
|
89
|
+
|
|
90
|
+
- [Events and router](docs/events.md) — mailbox, envelope, typed contracts, `createEventRouter`, hops, idempotency, rejections
|
|
91
|
+
- [Deterministic mode](docs/deterministic-mode.md) — `step` / `schedule`, clocks, `ScriptedAction`, replay diffs
|
|
92
|
+
- [Headless harness](docs/headless-harness.md) — `createHeadlessHarness` from `@cyberart-io/engine/headless`, jsdom canvas, inspect / screenshot
|
|
93
|
+
- [Presentation adapter](docs/presentation-adapter.md) — host-owned render model, intents, loading / error / unsupported
|
|
94
|
+
- [Asset resolver](docs/asset-resolver.md) — host-pluggable images/audio/fonts/spritesheets, cache, preload, fallbacks
|
|
95
|
+
- [Presentation cue](docs/presentation-cue.md) — deterministic `createPresentationTimeline`, duplicate policy, reduced-motion, lifecycle events
|
|
96
|
+
|
|
86
97
|
## Write a cart
|
|
87
98
|
|
|
88
99
|
A cart is an `AnimationCart`. Required:
|
|
@@ -122,14 +133,20 @@ const cart = runtime.mount(artProject, {
|
|
|
122
133
|
});
|
|
123
134
|
```
|
|
124
135
|
|
|
136
|
+
`runtime.hostChannel` is the mailbox for this instance. Attach it to `createEventRouter` from the host; carts never receive the router.
|
|
137
|
+
|
|
138
|
+
`runtime.assets` is the preloader when `createRuntime({ assets: { resolver } })` was set. Logical refs and fallbacks: [asset resolver](docs/asset-resolver.md).
|
|
139
|
+
|
|
125
140
|
`createRuntime` options:
|
|
126
141
|
|
|
127
142
|
| Option | Default | Meaning |
|
|
128
143
|
|---|---|---|
|
|
129
144
|
| `container` | required | Element that will hold the canvas. |
|
|
130
|
-
| `seed` | generated | Token hash for `Random`. Instance-local; does not clobber a global cache. |
|
|
131
|
-
| `captureKeyboard` | `false` | When true, the cart listens for `window` keydown. Full-page players pass `true`. |
|
|
145
|
+
| `seed` | generated | Token hash for `Random`. Instance-local; does not clobber a global cache. 64-hex hashes pass through; other seeds are mixed via `canonicalizeSeed`. |
|
|
146
|
+
| `captureKeyboard` | `false` | When true, the cart listens for `window` keydown. Full-page players pass `true`. Off in deterministic mode. |
|
|
132
147
|
| `audio` | none | Libraries to unlock if `unlockAudio()` runs before `mount`. After mount, the cart’s `metadata.audio` wins. |
|
|
148
|
+
| `deterministic` | off | Host-controlled clock, `step`/`advance`, and scripted input/assets. Leave unset for live kaleidoscope / Art Blocks. |
|
|
149
|
+
| `assets` | off | Host `AssetResolver` plus engine cache/preload. Carts keep logical refs. Leave unset when the piece has no media. |
|
|
133
150
|
|
|
134
151
|
`CartHandle` (what `mount` returns):
|
|
135
152
|
|
|
@@ -139,6 +156,9 @@ const cart = runtime.mount(artProject, {
|
|
|
139
156
|
| `pause()` / `resume()` | Freeze / continue the loop. |
|
|
140
157
|
| `snapshot()` | PNG data URL + seed + metadata. Not live `state`. |
|
|
141
158
|
| `dispatch(event)` | Queue an inbound host event for the cart. |
|
|
159
|
+
| `step(frames)` / `advance(ms)` | Deterministic ticks only. Throw if `deterministic` was not set. |
|
|
160
|
+
| `schedule(action)` | Queue a pointer/key/host-event/asset for a future frame. |
|
|
161
|
+
| `getClock()` / `getRandomState()` / `getReplayMetadata()` | Replay inspection. |
|
|
142
162
|
| `exportState()` / `exportStateJSON()` | Pause-safe serializable bundle. |
|
|
143
163
|
| `importState(bundle)` | Restore a bundle (or JSON string). |
|
|
144
164
|
| `destroy()` | Unload this cart. Idempotent. |
|
|
@@ -187,12 +207,58 @@ const runtime = createRuntime({
|
|
|
187
207
|
|
|
188
208
|
`Random` in `getDefaultState` / `update` / `render` follows that hash. Mark `metadata.generative: true` when the piece is a function of the seed — saves then refuse to load onto a different hash.
|
|
189
209
|
|
|
190
|
-
|
|
210
|
+
`Random.getState()` / `setState()` snapshot the dual sfc32 generators (post-warmup). Same hash always yields the same sequence; do not use `Math.random()` or `Date.now()` in cart logic. `setState` throws if the snapshot seed does not match.
|
|
211
|
+
|
|
212
|
+
A 64-hex Art Blocks hash is used as-is (optional `0x`, lowercased). `canonicalizeSeed` mixes any other value — including a number such as `42` — into a 64-hex hash. `createRuntime` uses `resolveRuntimeSeed`: live mode keeps a short `?hash=` string as `0x…` so kaleidoscope local hashes do not change; deterministic mode always canonicalizes.
|
|
213
|
+
|
|
214
|
+
## Deterministic mode
|
|
215
|
+
|
|
216
|
+
Seeded `Random` is not enough to reproduce an interactive scene. Opt in with `deterministic` so the host owns time and input. **Leave this unset for kaleidoscope / Art Blocks.**
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
const runtime = createRuntime({
|
|
220
|
+
container,
|
|
221
|
+
seed: 42,
|
|
222
|
+
deterministic: {
|
|
223
|
+
origin: 0,
|
|
224
|
+
actions: [
|
|
225
|
+
{ type: 'asset', atFrame: 2, id: 'room', status: 'ready' },
|
|
226
|
+
{ type: 'pointer', atFrame: 7, pointer: { kind: 'down', x: 40, y: 20 } },
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
const cart = runtime.mount(artProject);
|
|
231
|
+
await cart.step(10);
|
|
232
|
+
const replay = await cart.getReplayMetadata();
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
`step` / `advance` / `schedule` / `getClock` / `getReplayMetadata` / `describeReplayMismatch` are documented in [deterministic mode](docs/deterministic-mode.md) (`ScriptedAction` includes `key` and `event`; asset types are `ASSET_READY_EVENT` / `ASSET_FAILED_EVENT`; clocks are `createVirtualClock` / `createWallClock`).
|
|
236
|
+
|
|
237
|
+
## Headless harness
|
|
238
|
+
|
|
239
|
+
CI and agents should drive the **same** `createRuntime({ deterministic })` path. Import the harness from **`@cyberart-io/engine/headless`** so browser production builds never walk `node:fs/promises`.
|
|
240
|
+
|
|
241
|
+
- Carts and production hosts: `import { createRuntime } from '@cyberart-io/engine'`
|
|
242
|
+
- Tests, CI, and frame capture: `import { createHeadlessHarness } from '@cyberart-io/engine/headless'`
|
|
243
|
+
|
|
244
|
+
`@cyberart-io/engine` does not re-export the harness. `installHeadlessCanvas` is the documented jsdom install (test-only). `createHeadlessHarness` sizes a container, mounts, and wraps step / input / inspect / snapshot.
|
|
245
|
+
|
|
246
|
+
Full options, `click` clock rule, Node-only `captureFrame`, remount, and the reproduce command: [headless harness](docs/headless-harness.md).
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
import { createHeadlessHarness } from '@cyberart-io/engine/headless';
|
|
250
|
+
|
|
251
|
+
const harness = createHeadlessHarness({ cart: artProject, seed: 42 });
|
|
252
|
+
await harness.step(5);
|
|
253
|
+
harness.click(x, y); // pointer-down, canvas pixels, next frame
|
|
254
|
+
await harness.step(1);
|
|
255
|
+
const { state, events } = await harness.inspect();
|
|
256
|
+
harness.destroy();
|
|
257
|
+
```
|
|
191
258
|
|
|
192
|
-
|
|
259
|
+
## Events
|
|
193
260
|
|
|
194
|
-
|
|
195
|
-
- Cart → host: `hostChannel.emit({ type, payload })`. Pass `onEvent` at `mount` to hear them.
|
|
261
|
+
Default path: per-runtime **mailbox** (`dispatch` / `consume` / `emit`). Multi-cart hosts attach each `HostChannel` to `createEventRouter` — carts never see the router.
|
|
196
262
|
|
|
197
263
|
```ts
|
|
198
264
|
const cart = runtime.mount(artProject, {
|
|
@@ -202,11 +268,127 @@ const cart = runtime.mount(artProject, {
|
|
|
202
268
|
}
|
|
203
269
|
},
|
|
204
270
|
});
|
|
205
|
-
|
|
206
271
|
cart.dispatch({ type: 'art-project.theme', payload: 'dusk' });
|
|
207
272
|
```
|
|
208
273
|
|
|
209
|
-
|
|
274
|
+
Envelope, contracts (`defineIntent` / `defineStateEvent` / `defineDiagnostic`), `attach` / `detach` / `publish` / `turn`, hops, idempotency, budgets, and rejection reasons: [events and router](docs/events.md).
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
import {
|
|
278
|
+
defineIntent,
|
|
279
|
+
defineStateEvent,
|
|
280
|
+
createContractRegistry,
|
|
281
|
+
deriveAttachOptions,
|
|
282
|
+
createEventRouter,
|
|
283
|
+
} from '@cyberart-io/engine';
|
|
284
|
+
|
|
285
|
+
const exit = defineIntent('adventure.intent.exit-requested', {
|
|
286
|
+
version: 1,
|
|
287
|
+
fields: { exitId: { type: 'string' } },
|
|
288
|
+
});
|
|
289
|
+
const room = defineStateEvent('adventure.state.room-changed', {
|
|
290
|
+
version: 1,
|
|
291
|
+
fields: { roomId: { type: 'string' } },
|
|
292
|
+
});
|
|
293
|
+
if (!exit.ok || !room.ok) throw new Error('contracts');
|
|
294
|
+
const contracts = [exit.contract, room.contract];
|
|
295
|
+
const registry = createContractRegistry(contracts);
|
|
296
|
+
const router = createEventRouter({
|
|
297
|
+
validate: registry.asRouterValidate,
|
|
298
|
+
});
|
|
299
|
+
router.attach('presentation', runtime.hostChannel, deriveAttachOptions(contracts, 'cart'));
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Presentation cue
|
|
303
|
+
|
|
304
|
+
Frame-stepped effects (checkmarks, ripples, room fades). No `setTimeout` / rAF. Drive `step` from the same clock as deterministic `cart.step`.
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
import { createPresentationTimeline } from '@cyberart-io/engine';
|
|
308
|
+
|
|
309
|
+
const timeline = createPresentationTimeline({ originFrame: 0, reducedMotion: false });
|
|
310
|
+
timeline.play({
|
|
311
|
+
name: 'checkmark',
|
|
312
|
+
idempotencyKey: 'gold',
|
|
313
|
+
durationFrames: 90,
|
|
314
|
+
easing: 'ease-out',
|
|
315
|
+
onDuplicate: 'replace',
|
|
316
|
+
});
|
|
317
|
+
timeline.step(90);
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Lifecycle names `cue.started` / `cue.completed` / `cue.cancelled` / `cue.replaced` are local to the timeline. They are not router envelopes unless you define a contract. Duplicate policy, late-play catch-up, and reduced-motion: [presentation cue](docs/presentation-cue.md).
|
|
321
|
+
|
|
322
|
+
## Presentation adapter
|
|
323
|
+
|
|
324
|
+
Host-owned render model in, interaction intents out. Cyberart does not mutate the canonical world. `gameManager` is not this contract.
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
import {
|
|
328
|
+
attachPresentationAdapter,
|
|
329
|
+
createReferencePresentationCart,
|
|
330
|
+
} from '@cyberart-io/engine';
|
|
331
|
+
import { createHeadlessHarness } from '@cyberart-io/engine/headless';
|
|
332
|
+
|
|
333
|
+
const harness = createHeadlessHarness({
|
|
334
|
+
cart: createReferencePresentationCart(),
|
|
335
|
+
seed: 42,
|
|
336
|
+
onEvent: (event) => hostReduce(event),
|
|
337
|
+
});
|
|
338
|
+
const adapter = attachPresentationAdapter(harness);
|
|
339
|
+
adapter.present({
|
|
340
|
+
contractVersion: 1,
|
|
341
|
+
phase: 'ready',
|
|
342
|
+
view: { title: 'Joiner Brook', regions: [] },
|
|
343
|
+
});
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
`start()` maps to `cart.start()`. Phases: `loading` / `ready` / `error` / `unsupported`. Full contract, router attach (`runtime.hostChannel`), and the reproduce command: [presentation adapter](docs/presentation-adapter.md).
|
|
347
|
+
|
|
348
|
+
## Assets
|
|
349
|
+
|
|
350
|
+
Hosts resolve logical refs (Library IDs, Moltazine posts, `world:asset/…`, ordinary URLs). The engine caches, dedupes, reports progress, and applies timeouts / CORS-shaped failures / silent fallbacks. Policy stays out of authored cart code.
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
import {
|
|
354
|
+
createRuntime,
|
|
355
|
+
createFixtureAssetResolver,
|
|
356
|
+
createHostedAssetResolver,
|
|
357
|
+
} from '@cyberart-io/engine';
|
|
358
|
+
|
|
359
|
+
const roomAssets = [
|
|
360
|
+
{ id: 'room-bg', ref: 'moltazine:post/porch-1#primary-image', type: 'image' as const },
|
|
361
|
+
{
|
|
362
|
+
id: 'ambience',
|
|
363
|
+
ref: 'world:asset/stream-loop',
|
|
364
|
+
type: 'audio' as const,
|
|
365
|
+
fallback: 'silent' as const,
|
|
366
|
+
},
|
|
367
|
+
];
|
|
368
|
+
|
|
369
|
+
const local = createRuntime({
|
|
370
|
+
container,
|
|
371
|
+
assets: {
|
|
372
|
+
resolver: createFixtureAssetResolver({
|
|
373
|
+
'moltazine:post/porch-1#primary-image': { url: 'fixture://images/porch.png' },
|
|
374
|
+
'world:asset/stream-loop': { url: 'fixture://audio/stream-loop.ogg' },
|
|
375
|
+
}),
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
await local.assets!.preload(roomAssets);
|
|
379
|
+
|
|
380
|
+
const hosted = createRuntime({
|
|
381
|
+
container,
|
|
382
|
+
assets: {
|
|
383
|
+
resolver: createHostedAssetResolver({
|
|
384
|
+
cdnBase: 'https://cdn.example/library/',
|
|
385
|
+
failures: { 'world:asset/stream-loop': 'cors' },
|
|
386
|
+
}),
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
The same declarations produce fixture URLs locally and CDN URLs (plus a typed CORS failure and silent audio fallback) when hosted. Live preload dispatches `ASSET_READY_EVENT` / `ASSET_FAILED_EVENT`. Deterministic mode leaves those events to scripted `{ type: 'asset' }` actions so tests do not wait on wall-clock fetch. Full API and the reproduce command: [asset resolver](docs/asset-resolver.md).
|
|
210
392
|
|
|
211
393
|
## Save and load
|
|
212
394
|
|
|
@@ -258,7 +440,7 @@ const cart = runtime.mount(artProject, {
|
|
|
258
440
|
});
|
|
259
441
|
```
|
|
260
442
|
|
|
261
|
-
Pass `captureKeyboard: true` on `createRuntime` or the keys never reach the cart. Without `persist: 'localStorage'`, the host adapter ignores the events.
|
|
443
|
+
Pass `captureKeyboard: true` on `createRuntime` or the keys never reach the cart. Without `persist: 'localStorage'`, the host adapter ignores the events. W/E emits `cyberart.state.save` / `cyberart.state.load`; those names are historical — they are intents, not authoritative state events.
|
|
262
444
|
|
|
263
445
|
Slots are `localStorage['cyberart.state.' + cartId]`, or per-hash when the cart is generative. JSON can persist even if a framebuffer write fails.
|
|
264
446
|
|
|
@@ -281,7 +463,7 @@ A canvas the host adopted is left in place on destroy; a canvas the engine creat
|
|
|
281
463
|
|
|
282
464
|
## Publishing this package (maintainers)
|
|
283
465
|
|
|
284
|
-
Not part of writing a cart. The npm tarball is built from `packages/engine/src/index.ts` and contains minified `dist/index.js`, rolled-up
|
|
466
|
+
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/` (event / deterministic / harness / presentation-adapter / asset-resolver / presentation-cue API), and `package.json`. Site code that still imports `src/ui/lib/...` hits thin re-export shims so those paths keep working.
|
|
285
467
|
|
|
286
468
|
```bash
|
|
287
469
|
pnpm run pack:engine
|