@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.
@@ -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 };