@cyberart-io/engine 0.0.2 → 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 +137 -7
- package/dist/headless.d.ts +637 -0
- package/dist/headless.js +8 -0
- package/dist/index.d.ts +467 -64
- package/dist/index.js +1 -1
- package/docs/asset-resolver.md +155 -0
- package/docs/deterministic-mode.md +1 -1
- package/docs/events.md +79 -10
- package/docs/headless-harness.md +58 -16
- package/docs/presentation-adapter.md +150 -0
- package/docs/presentation-cue.md +82 -0
- package/package.json +13 -2
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* @cyberart-io/engine
|
|
3
|
+
* Copyright (c) 2026 Aaron Boyarsky
|
|
4
|
+
* Licensed under the CyberArt Engine License. See LICENSE.
|
|
5
|
+
* Not an OSI open-source license.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Copyright (c) 2026 Aaron Boyarsky
|
|
9
|
+
* SPDX-License-Identifier: LicenseRef-CyberArt-Engine
|
|
10
|
+
* See packages/engine/LICENSE
|
|
11
|
+
*
|
|
12
|
+
* Audio libraries the animation manager knows how to load and unlock.
|
|
13
|
+
* Carts declare which they need via `metadata.audio`; the manager looks them
|
|
14
|
+
* up here. Add a new adapter when a second library is supported.
|
|
15
|
+
*/
|
|
16
|
+
type AudioLibraryId = 'tone';
|
|
17
|
+
type AudioLibrarySpec = AudioLibraryId | AudioLibraryId[];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Copyright (c) 2026 Aaron Boyarsky
|
|
21
|
+
* SPDX-License-Identifier: LicenseRef-CyberArt-Engine
|
|
22
|
+
* See packages/engine/LICENSE
|
|
23
|
+
*/
|
|
24
|
+
type ExternalAssetDependency = {
|
|
25
|
+
cid: string;
|
|
26
|
+
[k: string]: unknown;
|
|
27
|
+
};
|
|
28
|
+
type TokenData = {
|
|
29
|
+
hash: string;
|
|
30
|
+
tokenId: string;
|
|
31
|
+
externalAssetDependencies?: ExternalAssetDependency[];
|
|
32
|
+
/**
|
|
33
|
+
* Per AB Engine Flex generator spec: gateway URL the script is supposed to
|
|
34
|
+
* combine with each `externalAssetDependencies[i].cid` of type `"IPFS"`.
|
|
35
|
+
* AB defaults this to `https://ipfs.artblocks.io/ipfs/` and lets partners
|
|
36
|
+
* override per-contract via support. Optional because standard (non-Flex)
|
|
37
|
+
* projects don't get it.
|
|
38
|
+
*/
|
|
39
|
+
preferredIPFSGateway?: string;
|
|
40
|
+
/** Same idea for Arweave-typed external assets. We don't currently use it. */
|
|
41
|
+
preferredArweaveGateway?: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Copyright (c) 2026 Aaron Boyarsky
|
|
46
|
+
* SPDX-License-Identifier: LicenseRef-CyberArt-Engine
|
|
47
|
+
* See packages/engine/LICENSE
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
type Sfc32Regs = {
|
|
51
|
+
a: number;
|
|
52
|
+
b: number;
|
|
53
|
+
c: number;
|
|
54
|
+
d: number;
|
|
55
|
+
};
|
|
56
|
+
/** Snapshot of the dual sfc32 generators after warmup (or after `setState`). */
|
|
57
|
+
type RandomState = {
|
|
58
|
+
seed: string;
|
|
59
|
+
useA: boolean;
|
|
60
|
+
prngA: Sfc32Regs;
|
|
61
|
+
prngB: Sfc32Regs;
|
|
62
|
+
};
|
|
63
|
+
declare class Random {
|
|
64
|
+
readonly seed: string;
|
|
65
|
+
private useA;
|
|
66
|
+
private prngA;
|
|
67
|
+
private prngB;
|
|
68
|
+
private genA;
|
|
69
|
+
private genB;
|
|
70
|
+
constructor(tokenData: TokenData);
|
|
71
|
+
getState(): RandomState;
|
|
72
|
+
setState(state: RandomState): void;
|
|
73
|
+
r_zero_one(): number;
|
|
74
|
+
dec(min?: number, max?: number): number;
|
|
75
|
+
int(min: number, max?: number): number;
|
|
76
|
+
bool(p?: number): boolean;
|
|
77
|
+
sign(): 1 | -1;
|
|
78
|
+
choose<T = unknown>(list: T[]): T;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type DimensionContext = {
|
|
82
|
+
width: number;
|
|
83
|
+
height: number;
|
|
84
|
+
iWidth: number;
|
|
85
|
+
iHeight: number;
|
|
86
|
+
smallDim: number;
|
|
87
|
+
largeDim: number;
|
|
88
|
+
area: number;
|
|
89
|
+
aspectRatio: number;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Copyright (c) 2026 Aaron Boyarsky
|
|
94
|
+
* SPDX-License-Identifier: LicenseRef-CyberArt-Engine
|
|
95
|
+
* See packages/engine/LICENSE
|
|
96
|
+
*/
|
|
97
|
+
type KeypressHandler = () => void;
|
|
98
|
+
/**
|
|
99
|
+
* Handler for keyboard inputs.
|
|
100
|
+
*/
|
|
101
|
+
declare class KeyboardManager {
|
|
102
|
+
private actionMap;
|
|
103
|
+
private debugMode;
|
|
104
|
+
private listening;
|
|
105
|
+
constructor(debugMode?: boolean, captureKeyboard?: boolean);
|
|
106
|
+
/**
|
|
107
|
+
* When a key is pressed, check if there's a corresponding action, and execute it.
|
|
108
|
+
* @param e
|
|
109
|
+
*/
|
|
110
|
+
checkKeypress({ key }: KeyboardEvent): void;
|
|
111
|
+
/**
|
|
112
|
+
* Inject a key without a DOM event. Deterministic hosts call this at a
|
|
113
|
+
* chosen frame instead of listening on `window`.
|
|
114
|
+
*/
|
|
115
|
+
inject(key: string): void;
|
|
116
|
+
/**
|
|
117
|
+
* Register a certain action to be performed when a given key is pressed.
|
|
118
|
+
* @param key
|
|
119
|
+
* @param action
|
|
120
|
+
* @param overwrite
|
|
121
|
+
*/
|
|
122
|
+
registerAction(key: string, action: KeypressHandler, overwrite?: boolean): void;
|
|
123
|
+
/**
|
|
124
|
+
* Remove the global keydown listener and clear registered actions. Call this
|
|
125
|
+
* when the owning animation is torn down so listeners don't accumulate across
|
|
126
|
+
* cart reloads (e.g. on window resize).
|
|
127
|
+
*/
|
|
128
|
+
destroy(): void;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Copyright (c) 2026 Aaron Boyarsky
|
|
133
|
+
* SPDX-License-Identifier: LicenseRef-CyberArt-Engine
|
|
134
|
+
* See packages/engine/LICENSE
|
|
135
|
+
*/
|
|
136
|
+
type PointerClick = {
|
|
137
|
+
x: number;
|
|
138
|
+
y: number;
|
|
139
|
+
};
|
|
140
|
+
type PointerManagerOptions = {
|
|
141
|
+
/**
|
|
142
|
+
* When false, do not attach canvas pointer listeners. Deterministic mode
|
|
143
|
+
* injects coordinates instead of reading the live pointer.
|
|
144
|
+
*/
|
|
145
|
+
listen?: boolean;
|
|
146
|
+
};
|
|
147
|
+
declare class PointerManager {
|
|
148
|
+
x: number;
|
|
149
|
+
y: number;
|
|
150
|
+
isDown: boolean;
|
|
151
|
+
private clicks;
|
|
152
|
+
private canvas;
|
|
153
|
+
private listening;
|
|
154
|
+
constructor(canvas: HTMLCanvasElement, options?: PointerManagerOptions);
|
|
155
|
+
private toCanvasCoords;
|
|
156
|
+
private onPointerDown;
|
|
157
|
+
private onPointerMove;
|
|
158
|
+
private onPointerUp;
|
|
159
|
+
hasClick(): boolean;
|
|
160
|
+
consumeClick(): PointerClick | null;
|
|
161
|
+
/**
|
|
162
|
+
* Inject a pointer sample in canvas pixel space. Deterministic hosts call
|
|
163
|
+
* this at a chosen frame instead of waiting on DOM pointer events.
|
|
164
|
+
*/
|
|
165
|
+
inject(kind: 'down' | 'move' | 'up', x: number, y: number): void;
|
|
166
|
+
destroy(): void;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
type EventKind = 'intent' | 'state' | 'diagnostic';
|
|
170
|
+
type EventInput = {
|
|
171
|
+
type: string;
|
|
172
|
+
payload?: unknown;
|
|
173
|
+
schemaVersion?: number;
|
|
174
|
+
kind?: EventKind;
|
|
175
|
+
source?: string;
|
|
176
|
+
target?: string;
|
|
177
|
+
id?: string;
|
|
178
|
+
correlationId?: string;
|
|
179
|
+
causationId?: string;
|
|
180
|
+
seq?: number;
|
|
181
|
+
hops?: number;
|
|
182
|
+
idempotencyKey?: string;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Copyright (c) 2026 Aaron Boyarsky
|
|
187
|
+
* SPDX-License-Identifier: LicenseRef-CyberArt-Engine
|
|
188
|
+
* See packages/engine/LICENSE
|
|
189
|
+
*
|
|
190
|
+
* Host ↔ cart mailbox. Inbound events are queued by the host via `dispatch`
|
|
191
|
+
* and drained by cart code that opts in via `consume()`. Outbound events are
|
|
192
|
+
* `emit`ted by the cart and forwarded to host `onEvent` listeners.
|
|
193
|
+
*
|
|
194
|
+
* Carts that never mention this object behave as they do today.
|
|
195
|
+
*
|
|
196
|
+
* Routed traffic is a full EventEnvelope; thin `{ type, payload }` remains
|
|
197
|
+
* valid on an unattached mailbox. Extra envelope fields are optional here.
|
|
198
|
+
*/
|
|
199
|
+
|
|
200
|
+
type HostEvent = EventInput;
|
|
201
|
+
type HostEventListener = (event: HostEvent) => void;
|
|
202
|
+
declare class HostChannel {
|
|
203
|
+
private inbound;
|
|
204
|
+
private listeners;
|
|
205
|
+
private closed;
|
|
206
|
+
dispatch(event: HostEvent): void;
|
|
207
|
+
consume(): HostEvent[];
|
|
208
|
+
emit(event: HostEvent): void;
|
|
209
|
+
onEvent(listener: HostEventListener): () => void;
|
|
210
|
+
/** Drop queued inbound events. Does not remove `onEvent` listeners. */
|
|
211
|
+
clearInbound(): void;
|
|
212
|
+
/** Drop inbound events and listeners. Runtime teardown; not cart unload. */
|
|
213
|
+
clear(): void;
|
|
214
|
+
/** Permanent close. Further dispatch / emit / consume / onEvent throw. */
|
|
215
|
+
close(): void;
|
|
216
|
+
private requireOpen;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Copyright (c) 2026 Aaron Boyarsky
|
|
221
|
+
* SPDX-License-Identifier: LicenseRef-CyberArt-Engine
|
|
222
|
+
* See packages/engine/LICENSE
|
|
223
|
+
*/
|
|
224
|
+
|
|
225
|
+
type AnimationTiming = {
|
|
226
|
+
now: number;
|
|
227
|
+
startTime: number;
|
|
228
|
+
elapsedSinceStart: number;
|
|
229
|
+
deltaSinceLastUpdate: number;
|
|
230
|
+
deltaSinceLastRender: number;
|
|
231
|
+
};
|
|
232
|
+
type AnimationCart<T = unknown, TFeatureState = undefined> = {
|
|
233
|
+
getDefaultFeatureState?: (R: Random, dimensionContext: DimensionContext, rawParams: number[], keyboardManager: KeyboardManager, customState?: Partial<T>, pointerManager?: PointerManager, gameManager?: unknown, hostChannel?: HostChannel) => TFeatureState;
|
|
234
|
+
getDefaultState: (R: Random, dimensionContext: DimensionContext, rawParams: number[], keyboardManager: KeyboardManager, customState?: Partial<T>, pointerManager?: PointerManager, gameManager?: unknown, featureState?: Readonly<TFeatureState>, hostChannel?: HostChannel) => T;
|
|
235
|
+
update: (R: Random, framesElapsed: number, rawParams: number[], dimensionContext: DimensionContext, state: T, keyboardManager: KeyboardManager, pointerManager?: PointerManager, gameManager?: unknown, timing?: AnimationTiming, featureState?: Readonly<TFeatureState>, hostChannel?: HostChannel) => T;
|
|
236
|
+
render: (R: Random, framesElapsed: number, rawParams: number[], dimensionContext: DimensionContext, state: T, drawingContext: CanvasRenderingContext2D, imageData: ImageData, pointerManager?: PointerManager, gameManager?: unknown, timing?: AnimationTiming, featureState?: Readonly<TFeatureState>, hostChannel?: HostChannel) => void;
|
|
237
|
+
adjust?: Record<string, {
|
|
238
|
+
type: 'switch';
|
|
239
|
+
immediate?: boolean;
|
|
240
|
+
label: string;
|
|
241
|
+
description: string;
|
|
242
|
+
} | {
|
|
243
|
+
type: 'slider';
|
|
244
|
+
immediate?: boolean;
|
|
245
|
+
description: string;
|
|
246
|
+
min: number;
|
|
247
|
+
max: number;
|
|
248
|
+
step: number;
|
|
249
|
+
label: string;
|
|
250
|
+
mapToOutput: (value: number) => number;
|
|
251
|
+
mapToInput: (value: number) => number;
|
|
252
|
+
}>;
|
|
253
|
+
/**
|
|
254
|
+
* Optional cleanup hook invoked when the cart is unloaded (e.g. on window
|
|
255
|
+
* resize, which fully rebuilds the animation). Carts that allocate long-lived
|
|
256
|
+
* resources such as Tone.js audio nodes should dispose them here to avoid
|
|
257
|
+
* leaks accumulating across reloads.
|
|
258
|
+
*/
|
|
259
|
+
teardown?: (state: T, featureState?: Readonly<TFeatureState>) => void;
|
|
260
|
+
metadata?: {
|
|
261
|
+
id: string;
|
|
262
|
+
name: string;
|
|
263
|
+
description?: string;
|
|
264
|
+
frameRate: number;
|
|
265
|
+
/**
|
|
266
|
+
* Optional per-visualMode display FPS overrides (e.g. mycelium at 60
|
|
267
|
+
* while the cart baseline stays lower). Keys match cart `visualMode`.
|
|
268
|
+
*/
|
|
269
|
+
frameRateByVisualMode?: Partial<Record<string, number>>;
|
|
270
|
+
/**
|
|
271
|
+
* Audio libraries this cart needs (currently `'tone'`). The animation
|
|
272
|
+
* manager loads and unlocks them; omit or leave unset for a silent cart.
|
|
273
|
+
*/
|
|
274
|
+
audio?: AudioLibrarySpec;
|
|
275
|
+
/**
|
|
276
|
+
* When true, outputs are a function of the token hash. Saved state may
|
|
277
|
+
* only be loaded back onto the same hash.
|
|
278
|
+
*/
|
|
279
|
+
generative?: boolean;
|
|
280
|
+
};
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
type CartStateDimensions = {
|
|
284
|
+
/** Canvas buffer pixels (device pixels). */
|
|
285
|
+
width: number;
|
|
286
|
+
height: number;
|
|
287
|
+
/** CSS layout size of the window/container at save. */
|
|
288
|
+
cssWidth?: number;
|
|
289
|
+
cssHeight?: number;
|
|
290
|
+
dpr?: number;
|
|
291
|
+
};
|
|
292
|
+
type CartStateBundle = {
|
|
293
|
+
version: number;
|
|
294
|
+
cartId?: string;
|
|
295
|
+
/** True when the cart is hash-seeded; load requires the same seed. */
|
|
296
|
+
generative?: boolean;
|
|
297
|
+
seed: string;
|
|
298
|
+
framesElapsed: number;
|
|
299
|
+
/** Pause-aware animation clock; restored so time-based visuals match the save. */
|
|
300
|
+
elapsedSinceStart?: number;
|
|
301
|
+
/** Output size the simulation was running at. Load pins the canvas to this. */
|
|
302
|
+
dimensions?: CartStateDimensions;
|
|
303
|
+
state: unknown;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Copyright (c) 2026 Aaron Boyarsky
|
|
308
|
+
* SPDX-License-Identifier: LicenseRef-CyberArt-Engine
|
|
309
|
+
* See packages/engine/LICENSE
|
|
310
|
+
*
|
|
311
|
+
* Host-controlled time, input, and asset completion for deterministic replays.
|
|
312
|
+
* Production kaleidoscope / Art Blocks playback does not enable this mode.
|
|
313
|
+
* Logical asset URLs are resolved by the host preloader (`assetResolver.ts`);
|
|
314
|
+
* this module only times `ASSET_READY_EVENT` / `ASSET_FAILED_EVENT` delivery.
|
|
315
|
+
*/
|
|
316
|
+
|
|
317
|
+
type PointerKind = 'down' | 'move' | 'up';
|
|
318
|
+
type ScriptedAction = {
|
|
319
|
+
atFrame: number;
|
|
320
|
+
} & ({
|
|
321
|
+
type: 'pointer';
|
|
322
|
+
pointer: {
|
|
323
|
+
kind: PointerKind;
|
|
324
|
+
x: number;
|
|
325
|
+
y: number;
|
|
326
|
+
};
|
|
327
|
+
} | {
|
|
328
|
+
type: 'key';
|
|
329
|
+
key: string;
|
|
330
|
+
} | {
|
|
331
|
+
type: 'event';
|
|
332
|
+
event: HostEvent;
|
|
333
|
+
} | {
|
|
334
|
+
type: 'asset';
|
|
335
|
+
id: string;
|
|
336
|
+
status: 'ready' | 'failed';
|
|
337
|
+
/** Optional structured failure or resolved resource. Envelope is unchanged. */
|
|
338
|
+
detail?: unknown;
|
|
339
|
+
});
|
|
340
|
+
type DeterministicRuntimeOptions = {
|
|
341
|
+
/** Virtual clock origin in ms. Default 0. */
|
|
342
|
+
origin?: number;
|
|
343
|
+
/** Actions applied at the start of `atFrame`, before `update`. */
|
|
344
|
+
actions?: ScriptedAction[];
|
|
345
|
+
};
|
|
346
|
+
type ClockSnapshot = {
|
|
347
|
+
now: number;
|
|
348
|
+
framesElapsed: number;
|
|
349
|
+
frameRate: number;
|
|
350
|
+
};
|
|
351
|
+
type ReplayMetadata = {
|
|
352
|
+
seed: string;
|
|
353
|
+
clock: ClockSnapshot;
|
|
354
|
+
rng: RandomState;
|
|
355
|
+
actions: ScriptedAction[];
|
|
356
|
+
applied: AppliedAction[];
|
|
357
|
+
events: HostEvent[];
|
|
358
|
+
state: unknown;
|
|
359
|
+
};
|
|
360
|
+
type AppliedAction = {
|
|
361
|
+
frame: number;
|
|
362
|
+
action: ScriptedAction;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
declare const ASSET_KINDS: readonly ["image", "audio", "font", "spritesheet"];
|
|
366
|
+
type AssetKind = (typeof ASSET_KINDS)[number];
|
|
367
|
+
declare const ASSET_FAILURE_CODES: readonly ["timeout", "cors", "not-found", "invalid", "aborted", "resolver"];
|
|
368
|
+
type AssetFailureCode = (typeof ASSET_FAILURE_CODES)[number];
|
|
369
|
+
type AssetCorsMode = 'anonymous' | 'use-credentials' | 'omit';
|
|
370
|
+
type AssetProvenance = {
|
|
371
|
+
readonly [key: string]: string | number | boolean | null | undefined;
|
|
372
|
+
};
|
|
373
|
+
type AssetDeclaration = {
|
|
374
|
+
/** Cache key and `ASSET_*_EVENT` payload `id`. */
|
|
375
|
+
id: string;
|
|
376
|
+
/** Logical URI: `https://…`, `moltazine:post/<id>#fragment`, `world:asset/…`, `library:…`. */
|
|
377
|
+
ref: string;
|
|
378
|
+
type: AssetKind;
|
|
379
|
+
integrity?: string;
|
|
380
|
+
provenance?: AssetProvenance;
|
|
381
|
+
/** Per-asset timeout. Ignored when wall-clock timeouts are off (deterministic). */
|
|
382
|
+
timeoutMs?: number;
|
|
383
|
+
/**
|
|
384
|
+
* `'silent'` synthesizes `SILENT_ASSET_FALLBACK_REF`. A string is another
|
|
385
|
+
* logical ref of the same type. A declaration is resolved as-is.
|
|
386
|
+
*/
|
|
387
|
+
fallback?: 'silent' | string | AssetDeclaration;
|
|
388
|
+
};
|
|
389
|
+
type AssetResolveRequest = {
|
|
390
|
+
id: string;
|
|
391
|
+
ref: string;
|
|
392
|
+
type: AssetKind;
|
|
393
|
+
integrity?: string;
|
|
394
|
+
provenance?: AssetProvenance;
|
|
395
|
+
};
|
|
396
|
+
type ResolvedAsset = {
|
|
397
|
+
id: string;
|
|
398
|
+
ref: string;
|
|
399
|
+
type: AssetKind;
|
|
400
|
+
url: string;
|
|
401
|
+
integrity?: string;
|
|
402
|
+
provenance?: AssetProvenance;
|
|
403
|
+
cors?: AssetCorsMode;
|
|
404
|
+
usedFallback?: boolean;
|
|
405
|
+
/** When true, `dispose` / `forget` call `URL.revokeObjectURL`. */
|
|
406
|
+
managed?: boolean;
|
|
407
|
+
};
|
|
408
|
+
type AssetFailure = {
|
|
409
|
+
id: string;
|
|
410
|
+
ref: string;
|
|
411
|
+
code: AssetFailureCode;
|
|
412
|
+
message: string;
|
|
413
|
+
};
|
|
414
|
+
type AssetItemStatus = {
|
|
415
|
+
state: 'pending';
|
|
416
|
+
} | {
|
|
417
|
+
state: 'loading';
|
|
418
|
+
} | {
|
|
419
|
+
state: 'ready';
|
|
420
|
+
resource: ResolvedAsset;
|
|
421
|
+
failure?: AssetFailure;
|
|
422
|
+
} | {
|
|
423
|
+
state: 'failed';
|
|
424
|
+
failure: AssetFailure;
|
|
425
|
+
};
|
|
426
|
+
type AssetPreloadSnapshot = {
|
|
427
|
+
total: number;
|
|
428
|
+
pending: number;
|
|
429
|
+
ready: number;
|
|
430
|
+
failed: number;
|
|
431
|
+
fallbacks: number;
|
|
432
|
+
items: Record<string, AssetItemStatus>;
|
|
433
|
+
failures: AssetFailure[];
|
|
434
|
+
};
|
|
435
|
+
type AssetResolver = {
|
|
436
|
+
resolve(request: AssetResolveRequest, signal?: AbortSignal): Promise<ResolvedAsset>;
|
|
437
|
+
};
|
|
438
|
+
type AssetRuntimeOptions = {
|
|
439
|
+
resolver: AssetResolver;
|
|
440
|
+
timeoutMs?: number;
|
|
441
|
+
emitEvents?: boolean;
|
|
442
|
+
};
|
|
443
|
+
type AssetPreloader = {
|
|
444
|
+
preload(declarations: readonly AssetDeclaration[]): Promise<AssetPreloadSnapshot>;
|
|
445
|
+
get(id: string): ResolvedAsset | undefined;
|
|
446
|
+
getProgress(): AssetPreloadSnapshot;
|
|
447
|
+
onProgress(listener: (snapshot: AssetPreloadSnapshot) => void): () => void;
|
|
448
|
+
abort(id?: string): void;
|
|
449
|
+
forget(id?: string): void;
|
|
450
|
+
dispose(): void;
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Copyright (c) 2026 Aaron Boyarsky
|
|
455
|
+
* SPDX-License-Identifier: LicenseRef-CyberArt-Engine
|
|
456
|
+
* See packages/engine/LICENSE
|
|
457
|
+
*/
|
|
458
|
+
|
|
459
|
+
type FrameErrorInfo = {
|
|
460
|
+
phase: 'update' | 'render' | 'draw';
|
|
461
|
+
consecutive: number;
|
|
462
|
+
stopped: boolean;
|
|
463
|
+
};
|
|
464
|
+
type CreateRuntimeOptions = {
|
|
465
|
+
/** Required mount point. The runtime creates or adopts a canvas inside this element. */
|
|
466
|
+
container: HTMLElement;
|
|
467
|
+
/**
|
|
468
|
+
* Token hash (`0x` + 64 hex) or any seed mixed into one. Instance-local;
|
|
469
|
+
* does not clobber an existing global cache. Kaleidoscope / Art Blocks
|
|
470
|
+
* pass the platform hash unchanged.
|
|
471
|
+
*/
|
|
472
|
+
seed?: string | number;
|
|
473
|
+
/**
|
|
474
|
+
* When true, the cart listens for window keydown (full-page players).
|
|
475
|
+
* Defaults to false so an embed does not steal keys from the host.
|
|
476
|
+
* Ignored when `deterministic` is set — input is injected per frame.
|
|
477
|
+
*/
|
|
478
|
+
captureKeyboard?: boolean;
|
|
479
|
+
/**
|
|
480
|
+
* Audio libraries to unlock if the host calls `unlockAudio()` before `mount`
|
|
481
|
+
* (click can beat idle prebuild). After mount, `cart.metadata.audio` wins.
|
|
482
|
+
*/
|
|
483
|
+
audio?: AudioLibrarySpec;
|
|
484
|
+
/**
|
|
485
|
+
* Host-controlled time, input, and asset events. Production playback
|
|
486
|
+
* (kaleidoscope locally and `build:art`) leaves this unset so rAF and the
|
|
487
|
+
* token hash drive the piece as they do today.
|
|
488
|
+
*/
|
|
489
|
+
deterministic?: boolean | DeterministicRuntimeOptions;
|
|
490
|
+
/**
|
|
491
|
+
* Host-pluggable asset resolver/preloader. Carts request logical refs;
|
|
492
|
+
* the host maps them to loadable URLs or blobs. Leave unset when unused.
|
|
493
|
+
* In deterministic mode, preload does not dispatch `ASSET_*` events —
|
|
494
|
+
* scripted `{ type: 'asset' }` actions own delivery timing.
|
|
495
|
+
*/
|
|
496
|
+
assets?: AssetRuntimeOptions;
|
|
497
|
+
};
|
|
498
|
+
type MountOptions<T = unknown> = {
|
|
499
|
+
/** Boot overrides passed as `customState` into `getDefaultState`. Not a live-state replay. */
|
|
500
|
+
initialState?: Partial<T>;
|
|
501
|
+
/** Outbound events emitted by cart code via `hostChannel.emit`. */
|
|
502
|
+
onEvent?: HostEventListener;
|
|
503
|
+
/** Site-only. Opaque to the published engine; pass a GameManager from the app. */
|
|
504
|
+
gameManager?: unknown;
|
|
505
|
+
};
|
|
506
|
+
type CartSnapshot = {
|
|
507
|
+
seed: string;
|
|
508
|
+
metadata?: AnimationCart['metadata'];
|
|
509
|
+
pngDataUrl: string;
|
|
510
|
+
};
|
|
511
|
+
type CartHandle = {
|
|
512
|
+
start(): Promise<void>;
|
|
513
|
+
pause(): void;
|
|
514
|
+
resume(): void;
|
|
515
|
+
dispatch(event: HostEvent): void;
|
|
516
|
+
snapshot(): CartSnapshot;
|
|
517
|
+
destroy(): void;
|
|
518
|
+
reload(): void;
|
|
519
|
+
reinit(event?: Event): void;
|
|
520
|
+
/**
|
|
521
|
+
* Live cart state for site chrome (e.g. `/art` debug sliders). Not a public
|
|
522
|
+
* snapshot API — grids and audio nodes are not structured-cloneable.
|
|
523
|
+
*/
|
|
524
|
+
getCartState(): unknown;
|
|
525
|
+
exportState(): Promise<CartStateBundle>;
|
|
526
|
+
exportStateJSON(): Promise<string>;
|
|
527
|
+
importState(bundle: CartStateBundle | string, extras?: {
|
|
528
|
+
framebuffer?: ImageData | null;
|
|
529
|
+
}): Promise<void>;
|
|
530
|
+
peekExportedFramebuffer(): ImageData | null;
|
|
531
|
+
peekSeed(): string | undefined;
|
|
532
|
+
isGenerative(): boolean;
|
|
533
|
+
/**
|
|
534
|
+
* Run `frames` ticks on the virtual clock. Requires `deterministic`.
|
|
535
|
+
* Does not use rAF; live kaleidoscope playback never calls this.
|
|
536
|
+
*/
|
|
537
|
+
step(frames?: number): Promise<void>;
|
|
538
|
+
/** Run the frames that span `ms` at the cart frame rate. Requires `deterministic`. */
|
|
539
|
+
advance(ms: number): Promise<void>;
|
|
540
|
+
/** Queue a scripted input/asset/host event for a future frame. */
|
|
541
|
+
schedule(action: ScriptedAction): void;
|
|
542
|
+
getClock(): ClockSnapshot;
|
|
543
|
+
getRandomState(): RandomState;
|
|
544
|
+
getReplayMetadata(): Promise<ReplayMetadata>;
|
|
545
|
+
readonly canvas: HTMLCanvasElement | undefined;
|
|
546
|
+
paused: boolean;
|
|
547
|
+
readonly tokenData: TokenData;
|
|
548
|
+
readonly isPrepared: boolean;
|
|
549
|
+
readonly isLoopRunning: boolean;
|
|
550
|
+
readonly needsAudio: boolean;
|
|
551
|
+
readonly audioLibraries: AudioLibraryId[];
|
|
552
|
+
};
|
|
553
|
+
type CyberArtRuntime = {
|
|
554
|
+
mount<T>(cart: AnimationCart<T, any>, options?: MountOptions<T>): CartHandle;
|
|
555
|
+
unlockAudio(): Promise<void>;
|
|
556
|
+
destroy(): void;
|
|
557
|
+
readonly tokenData: TokenData;
|
|
558
|
+
/**
|
|
559
|
+
* This runtime's mailbox. Attach it to `createEventRouter` from the host;
|
|
560
|
+
* carts never receive the router. Survives cart remount; `destroy()` clears it.
|
|
561
|
+
*/
|
|
562
|
+
readonly hostChannel: HostChannel;
|
|
563
|
+
/**
|
|
564
|
+
* Preloader for this runtime. Undefined when `assets` was omitted.
|
|
565
|
+
* Survives cart remount; `destroy()` disposes it.
|
|
566
|
+
*/
|
|
567
|
+
readonly assets: AssetPreloader | undefined;
|
|
568
|
+
onError?: (error: unknown, info: FrameErrorInfo) => void;
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Copyright (c) 2026 Aaron Boyarsky
|
|
573
|
+
* SPDX-License-Identifier: LicenseRef-CyberArt-Engine
|
|
574
|
+
* See packages/engine/LICENSE
|
|
575
|
+
*
|
|
576
|
+
* CI / agent harness around production `createRuntime({ deterministic })`.
|
|
577
|
+
* `installHeadlessCanvas` is test-only — do not call it from Player or kaleidoscope.
|
|
578
|
+
*/
|
|
579
|
+
|
|
580
|
+
/** 1×1 PNG so `captureFrame(path)` writes a file that actually opens. */
|
|
581
|
+
declare const HEADLESS_PNG_DATA_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
|
|
582
|
+
declare const DEFAULT_HEADLESS_WIDTH = 320;
|
|
583
|
+
declare const DEFAULT_HEADLESS_HEIGHT = 180;
|
|
584
|
+
/**
|
|
585
|
+
* Documented jsdom canvas install. Mutates `HTMLCanvasElement.prototype`.
|
|
586
|
+
* Idempotent. Not for production playback.
|
|
587
|
+
*/
|
|
588
|
+
declare function installHeadlessCanvas(): void;
|
|
589
|
+
type HeadlessFrameError = {
|
|
590
|
+
error: unknown;
|
|
591
|
+
info: FrameErrorInfo;
|
|
592
|
+
};
|
|
593
|
+
type HeadlessInspect = {
|
|
594
|
+
state: unknown;
|
|
595
|
+
events: HostEvent[];
|
|
596
|
+
errors: HeadlessFrameError[];
|
|
597
|
+
replay: ReplayMetadata;
|
|
598
|
+
clock: ClockSnapshot;
|
|
599
|
+
};
|
|
600
|
+
type CreateHeadlessHarnessOptions<T = unknown> = {
|
|
601
|
+
cart: AnimationCart<T>;
|
|
602
|
+
seed?: CreateRuntimeOptions['seed'];
|
|
603
|
+
width?: number;
|
|
604
|
+
height?: number;
|
|
605
|
+
/** Virtual clock origin in ms. Default 0. */
|
|
606
|
+
origin?: number;
|
|
607
|
+
actions?: ScriptedAction[];
|
|
608
|
+
initialState?: Partial<T>;
|
|
609
|
+
gameManager?: unknown;
|
|
610
|
+
onEvent?: HostEventListener;
|
|
611
|
+
onError?: (error: unknown, info: FrameErrorInfo) => void;
|
|
612
|
+
};
|
|
613
|
+
type HeadlessHarness<T = unknown> = {
|
|
614
|
+
readonly runtime: CyberArtRuntime;
|
|
615
|
+
readonly container: HTMLElement;
|
|
616
|
+
readonly events: readonly HostEvent[];
|
|
617
|
+
readonly errors: readonly HeadlessFrameError[];
|
|
618
|
+
readonly cart: CartHandle;
|
|
619
|
+
step(frames?: number): Promise<void>;
|
|
620
|
+
advance(ms: number): Promise<void>;
|
|
621
|
+
schedule(action: ScriptedAction): void;
|
|
622
|
+
dispatch(event: HostEvent): void;
|
|
623
|
+
start(): Promise<void>;
|
|
624
|
+
pause(): void;
|
|
625
|
+
resume(): void;
|
|
626
|
+
readonly paused: boolean;
|
|
627
|
+
key(key: string): void;
|
|
628
|
+
/** Pointer-down at the next frame. Use `schedule` for move/up. Canvas pixels, not CSS. */
|
|
629
|
+
click(x: number, y: number): void;
|
|
630
|
+
inspect(): Promise<HeadlessInspect>;
|
|
631
|
+
captureFrame(path?: string): Promise<CartSnapshot>;
|
|
632
|
+
remount(options?: MountOptions<T>): CartHandle;
|
|
633
|
+
destroy(): void;
|
|
634
|
+
};
|
|
635
|
+
declare function createHeadlessHarness<T>(options: CreateHeadlessHarnessOptions<T>): HeadlessHarness<T>;
|
|
636
|
+
|
|
637
|
+
export { type CreateHeadlessHarnessOptions, DEFAULT_HEADLESS_HEIGHT, DEFAULT_HEADLESS_WIDTH, HEADLESS_PNG_DATA_URL, type HeadlessFrameError, type HeadlessHarness, type HeadlessInspect, createHeadlessHarness, installHeadlessCanvas };
|