@cyberart-io/engine 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,186 @@
1
+ CyberArt Engine License
2
+ Copyright (c) 2026 Aaron Boyarsky
3
+
4
+ This is a source-available license, not an OSI open-source license.
5
+ It is intended to let people make and sell art, games, simulations,
6
+ and similar standalone works, and to keep the Engine from being
7
+ redistributed or used as a competing product or platform.
8
+
9
+ By installing, copying, modifying, or using the Engine, you agree to
10
+ this license. If you do not agree, you may not use the Engine.
11
+
12
+ This license applies only to this version of the Engine. The Licensor
13
+ may license other versions under different terms.
14
+
15
+ 1. Definitions
16
+
17
+ "Licensor" means Aaron Boyarsky, and any successor or assignee of
18
+ his copyright in the Engine. A written assignment may substitute a
19
+ company or other person as Licensor for this license and for later
20
+ versions.
21
+
22
+ "You" (or "your") means the individual or legal entity exercising
23
+ rights under this license.
24
+
25
+ "Affiliate" means any entity that directly or indirectly controls,
26
+ is controlled by, or is under common control with you. "Control"
27
+ means the power to direct management, whether through ownership of
28
+ more than fifty percent (50%) of voting securities, by contract, or
29
+ otherwise.
30
+
31
+ "Engine" means the @cyberart-io/engine software, including its
32
+ source, object, and bundled forms, and any modification, fork, or
33
+ derivative of that software (a "Derivative Engine").
34
+
35
+ "Creative Work" means an original standalone work that you create
36
+ using the Engine, whose primary purpose is to be experienced,
37
+ played, or observed as art, a game, a simulation, or another work
38
+ of the same kind (for example a cart, sketch, animation,
39
+ installation, toy, visualizer, exhibit, or playable piece). The
40
+ Engine may be bundled only as the runtime for that work.
41
+
42
+ A Creative Work is not: a general-purpose host, runtime, SDK,
43
+ template, marketplace, catalog, CMS, widget, multi-artist gallery,
44
+ or other platform that exists mainly to run, sell, or host works by
45
+ others (including a collection or site whose purpose is to mount
46
+ third-party carts or similar works); a competing product to the
47
+ Engine; or general application or infrastructure software that is
48
+ not itself art, a game, a simulation, or a similar standalone work.
49
+ A site or exhibit of only your own Creative Works is not, by itself,
50
+ excluded.
51
+
52
+ "Competing Use" means you or an Affiliate using the Engine or a
53
+ Derivative Engine to offer, operate, or sell a product or service
54
+ that (a) provides the Engine (or equivalent host/runtime APIs) to
55
+ third parties, (b) hosts, mounts, or distributes third-party carts
56
+ or similar works as a platform, or (c) substitutes for the Engine
57
+ as a framework others would otherwise obtain from the Licensor.
58
+
59
+ 2. Grant
60
+
61
+ Subject to this license, the Licensor grants you a non-exclusive,
62
+ non-sublicensable, royalty-free license to:
63
+
64
+ (a) use, copy, and modify the Engine, solely to create, develop,
65
+ perform, exhibit, play, and archive Creative Works;
66
+ (b) distribute and sell your Creative Works, including a copy of
67
+ the Engine bundled with a Creative Work, but only as reasonably
68
+ necessary for that work to run for its audience (for example a
69
+ webpage, file, token, or build that runs the piece); and
70
+ (c) keep private copies and modifications of the Engine for the
71
+ purposes in (a) and (b).
72
+
73
+ This license is personal to you and is not transferable, except
74
+ that you may assign it to a successor of your business solely
75
+ together with your Creative Work(s). The successor is bound by this
76
+ license. You may not assign or transfer rights in the Engine apart
77
+ from a Creative Work.
78
+
79
+ You may charge for the Creative Work. You may not charge for the
80
+ Engine as a separate product, library, or service.
81
+
82
+ Each distribution of a Creative Work under Section 2(b) grants the
83
+ recipient, directly from the Licensor (not as a sublicense from
84
+ you), a license solely to run that copy to experience that Creative
85
+ Work, and not to extract or reuse the Engine for any other purpose.
86
+
87
+ Any distribution under Section 2(b) must keep copyright and license
88
+ notices on the Engine, or include a prominent pointer to this
89
+ license, with the bundled copy.
90
+
91
+ 3. Restrictions
92
+
93
+ You may not, except under a separate written license from the
94
+ Licensor:
95
+
96
+ (a) publish, distribute, license, sublicense, or sell the Engine or
97
+ a Derivative Engine on its own, as an SDK, npm package,
98
+ template, or competing framework;
99
+ (b) make a Competing Use of the Engine or a Derivative Engine, or
100
+ permit an Affiliate to do so;
101
+ (c) use the Engine for a primary purpose other than creating
102
+ Creative Works (including general application software or
103
+ infrastructure that is not itself a Creative Work);
104
+ (d) remove or obscure copyright or license notices on the Engine;
105
+ (e) represent that the Licensor endorses you or your Creative Work;
106
+ or
107
+ (f) use the Engine (including its source, APIs, or runtime
108
+ behavior) to train a machine-learning model, or otherwise to
109
+ generate a substitute engine or a Competing Use.
110
+
111
+ Creating a Creative Work that itself uses machine learning is not,
112
+ by itself, a violation of Section 3(f). Using output of your own
113
+ Creative Work to train a model for your own Creative Works is not,
114
+ by itself, a violation of Section 3(f).
115
+
116
+ Bundling under Section 2(b) does not allow you to offer the Engine
117
+ as a reusable library, document it as a third-party SDK, or invite
118
+ others to build unrelated works on your copy.
119
+
120
+ 4. Patents
121
+
122
+ The Licensor grants you a non-exclusive, royalty-free patent
123
+ license under the Licensor's patents that are necessarily infringed
124
+ by the Engine as provided, or by modifications you make under
125
+ Section 2, solely to the extent required to exercise the rights in
126
+ Section 2. All other patent rights are reserved.
127
+
128
+ This patent license ends if you or an Affiliate commence a claim
129
+ that the Engine or the Licensor infringes a patent.
130
+
131
+ 5. Ownership
132
+
133
+ The Licensor retains all right, title, and interest in the Engine.
134
+ You retain all right, title, and interest in your Creative Works,
135
+ excluding the Engine itself. Assignment of copyright to a successor
136
+ Licensor does not expand this grant. This license does not grant
137
+ trademark rights in "CyberArt", "@cyberart-io", or related marks.
138
+
139
+ 6. Contributions
140
+
141
+ If you submit a contribution to the Engine by pull request, patch,
142
+ or another submission you designate as a contribution, you assign
143
+ to the Licensor all right, title, and interest in that
144
+ contribution, and you grant the Licensor a matching patent license
145
+ in the contribution. Informal comments are not contributions unless
146
+ you later submit them as above. If that assignment is ineffective,
147
+ you grant the Licensor an irrevocable, perpetual, worldwide,
148
+ royalty-free, sublicensable license to use, modify, and relicense
149
+ the contribution as part of the Engine, including under other
150
+ terms.
151
+
152
+ 7. No warranty; limitation of liability
153
+
154
+ THE ENGINE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
155
+ EXPRESS OR IMPLIED, INCLUDING MERCHANTABILITY, FITNESS FOR A
156
+ PARTICULAR PURPOSE, AND NONINFRINGEMENT.
157
+
158
+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE LICENSOR IS NOT LIABLE
159
+ FOR ANY DAMAGES ARISING FROM THE ENGINE OR THIS LICENSE, WHETHER IN
160
+ CONTRACT, TORT, OR OTHERWISE, INCLUDING LOST PROFITS, LOST DATA, OR
161
+ CONSEQUENTIAL, INCIDENTAL, INDIRECT, OR SPECIAL DAMAGES.
162
+
163
+ IF A COURT HOLDS THE EXCLUSION OF DAMAGES UNENFORCEABLE, THE
164
+ LICENSOR'S TOTAL LIABILITY IS LIMITED TO THE AMOUNTS YOU PAID THE
165
+ LICENSOR FOR THE ENGINE (IF ANY), AND IS US$0 IF YOU PAID NOTHING.
166
+
167
+ 8. Termination
168
+
169
+ This license ends automatically if you breach it. On termination
170
+ you must stop using and distributing the Engine, except that
171
+ recipients who already received a Creative Work bundled under
172
+ Section 2(b) may keep that copy solely to run that work. Sections
173
+ 1, 3, 4, 5, 6, 7, 9, and 10 survive.
174
+
175
+ 9. Governing law
176
+
177
+ Except where prohibited by applicable law, this license is governed
178
+ by the laws of the Commonwealth of Virginia, without regard to
179
+ conflict-of-law rules, and the exclusive venue for disputes is the
180
+ state or federal courts located in Virginia.
181
+
182
+ 10. Entire agreement
183
+
184
+ This is the entire license for this version of the Engine. Rights
185
+ not expressly granted are reserved. If a court finds a term
186
+ unenforceable, the rest remains in effect.
package/README.md ADDED
@@ -0,0 +1,291 @@
1
+ # `@cyberart-io/engine`
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). That is a state machine in a loop that paints to the screen. The engine mounts it into a DOM element you own, runs the loop, and gives you pause, snapshot, events, and save/load.
4
+
5
+ ## License
6
+
7
+ You may use this to make and sell art, games, simulations, or similar standalone works — including bundling the runtime so the piece can run. You may not redistribute or sell the engine itself, or use it to build a competing platform. This is not an OSI open-source license. Full terms: [LICENSE](LICENSE).
8
+
9
+ The published `@cyberart-io/engine` package, and the source files that comprise it, are under that license. The rest of this repository (site, carts, API, infrastructure) is proprietary and has no public license grant.
10
+
11
+ A bundled copy must keep the copyright and license notice, or a pointer to it. The minified `dist/index.js` does not embed the text, so put a pointer next to the build, for example in HTML:
12
+
13
+ ```html
14
+ <!-- @cyberart-io/engine — CyberArt Engine License: see ./LICENSE -->
15
+ ```
16
+
17
+ or ship a copy of `LICENSE` beside the files you distribute.
18
+
19
+ Tone.js is an optional peer under *its* license. Bundling Tone with a piece does not relicense Tone.
20
+
21
+ This guide is the path from `npm i` to a piece on screen, then a short map of the API. Copy the examples as-is; swap in your own drawing.
22
+
23
+ ```bash
24
+ npm i @cyberart-io/engine
25
+ ```
26
+
27
+ The package is minified JS plus a rolled-up `.d.ts`. It does not include animation carts. Tone.js is an optional peer — skip it until a cart declares `audio: 'tone'`.
28
+
29
+ ## Quick start
30
+
31
+ A silent cart can `start()` immediately (no click). This `art-project` picks a hue from the token seed and drifts it each frame.
32
+
33
+ `art-project.ts`:
34
+
35
+ ```ts
36
+ import type { AnimationCart } from '@cyberart-io/engine';
37
+
38
+ type State = { hue: number; t: number };
39
+
40
+ export const artProject: AnimationCart<State> = {
41
+ metadata: {
42
+ id: 'art-project',
43
+ name: 'art-project',
44
+ frameRate: 30,
45
+ },
46
+ getDefaultState: (R) => ({
47
+ hue: R.dec(0, 360),
48
+ t: 0,
49
+ }),
50
+ update: (_R, _frames, _params, _dim, state) => ({
51
+ ...state,
52
+ t: state.t + 1,
53
+ }),
54
+ render: (_R, _frames, _params, dim, state, ctx) => {
55
+ const hue = (state.hue + state.t * 0.4) % 360;
56
+ ctx.fillStyle = `hsl(${hue} 70% 45%)`;
57
+ ctx.fillRect(0, 0, dim.width, dim.height);
58
+ },
59
+ };
60
+ ```
61
+
62
+ `main.ts`:
63
+
64
+ ```ts
65
+ import { createRuntime } from '@cyberart-io/engine';
66
+ import { artProject } from './art-project';
67
+
68
+ const container = document.getElementById('stage')!;
69
+ const runtime = createRuntime({ container });
70
+ const cart = runtime.mount(artProject);
71
+
72
+ await cart.start();
73
+
74
+ window.addEventListener('pagehide', () => {
75
+ cart.destroy();
76
+ runtime.destroy();
77
+ });
78
+ ```
79
+
80
+ ```html
81
+ <div id="stage"></div>
82
+ ```
83
+
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
+
86
+ ## Write a cart
87
+
88
+ A cart is an `AnimationCart`. Required:
89
+
90
+ | Piece | Role |
91
+ |---|---|
92
+ | `getDefaultState` | Build the initial `state`. Use `R` (`Random`) for anything that should follow the seed. |
93
+ | `update` | Advance `state` each tick. Return the next state. |
94
+ | `render` | Draw into the 2D context (and optional `ImageData`). |
95
+ | `metadata.id` / `name` / `frameRate` | Identity and loop rate. |
96
+
97
+ Useful optionals:
98
+
99
+ | Piece | Role |
100
+ |---|---|
101
+ | `teardown` | Dispose long-lived resources (Tone nodes, listeners) when the cart unloads. |
102
+ | `metadata.audio` | `'tone'` if the piece needs Web Audio. Omit for a silent cart. |
103
+ | `metadata.generative` | `true` when output is a function of the token hash. Saves then only load on the same seed. |
104
+
105
+ 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`.
106
+
107
+ `Random` is seed-stable: `R.dec(min, max)`, `R.int(min, max)`, `R.bool()`, `R.choose(list)`.
108
+
109
+ ## Host API
110
+
111
+ ```ts
112
+ const runtime = createRuntime({
113
+ container, // required mount point
114
+ seed, // optional `0x` + 64 hex; drives Random
115
+ captureKeyboard, // default false — embeds should leave this off
116
+ audio, // hint to unlock Tone if a click beats mount
117
+ });
118
+
119
+ const cart = runtime.mount(artProject, {
120
+ initialState, // boot overrides into getDefaultState (not a save replay)
121
+ onEvent, // outbound events from the cart
122
+ });
123
+ ```
124
+
125
+ `createRuntime` options:
126
+
127
+ | Option | Default | Meaning |
128
+ |---|---|---|
129
+ | `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`. |
132
+ | `audio` | none | Libraries to unlock if `unlockAudio()` runs before `mount`. After mount, the cart’s `metadata.audio` wins. |
133
+
134
+ `CartHandle` (what `mount` returns):
135
+
136
+ | Member | Meaning |
137
+ |---|---|
138
+ | `start()` | Unlock declared audio (if any) and begin the loop. |
139
+ | `pause()` / `resume()` | Freeze / continue the loop. |
140
+ | `snapshot()` | PNG data URL + seed + metadata. Not live `state`. |
141
+ | `dispatch(event)` | Queue an inbound host event for the cart. |
142
+ | `exportState()` / `exportStateJSON()` | Pause-safe serializable bundle. |
143
+ | `importState(bundle)` | Restore a bundle (or JSON string). |
144
+ | `destroy()` | Unload this cart. Idempotent. |
145
+ | `needsAudio` | True when the cart declared audio libraries. Use this to show a click overlay. |
146
+ | `paused` / `isPrepared` / `isLoopRunning` | Loop flags. |
147
+ | `canvas` | The canvas in use, if any. |
148
+ | `tokenData` | Live hash and token id. |
149
+ | `getCartState()` | Live object for debug UI. Not JSON-safe — use export/import for saves. |
150
+
151
+ `runtime.destroy()` tears down the runtime. `runtime.unlockAudio()` is for an early click before `mount`. `runtime.onError` receives frame errors (`phase`, `consecutive`, `stopped`).
152
+
153
+ One cart per runtime. A second `mount` unloads the first. Several pieces on a page means several `createRuntime()` calls.
154
+
155
+ ## Audio
156
+
157
+ Silent carts (no `metadata.audio`) never import Tone. Call `start()` whenever you want.
158
+
159
+ Carts that need Web Audio declare it:
160
+
161
+ ```ts
162
+ metadata: {
163
+ id: 'art-project',
164
+ name: 'art-project',
165
+ frameRate: 30,
166
+ audio: 'tone',
167
+ }
168
+ ```
169
+
170
+ Install the peer: `npm i tone@^14.8.15`. Call `cart.start()` **inside a click or tap**. Browsers often leave `Tone.start()` pending without a gesture, so the loop never begins. `needsAudio` is true so the host can show a click overlay.
171
+
172
+ `start()` only unlocks libraries the cart (or `createRuntime({ audio })`) declared.
173
+
174
+ 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.
175
+
176
+ ## Seed
177
+
178
+ Pass `seed` when you want a deterministic output (for example from `?hash=`):
179
+
180
+ ```ts
181
+ const hash = new URLSearchParams(window.location.search).get('hash');
182
+ const runtime = createRuntime({
183
+ container,
184
+ seed: hash ?? undefined,
185
+ });
186
+ ```
187
+
188
+ `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
+
190
+ ## Events
191
+
192
+ The runtime does not ship an event schema. Types are yours.
193
+
194
+ - Host → cart: `cart.dispatch({ type, payload })`. The cart opts in by calling `hostChannel.consume()` (typically in `update`). Unconsumed events keep the newest 32.
195
+ - Cart → host: `hostChannel.emit({ type, payload })`. Pass `onEvent` at `mount` to hear them.
196
+
197
+ ```ts
198
+ const cart = runtime.mount(artProject, {
199
+ onEvent: (event) => {
200
+ if (event.type === 'art-project.theme') {
201
+ document.body.dataset.theme = String(event.payload);
202
+ }
203
+ },
204
+ });
205
+
206
+ cart.dispatch({ type: 'art-project.theme', payload: 'dusk' });
207
+ ```
208
+
209
+ Carts that never mention `HostChannel` ignore both directions.
210
+
211
+ ## Save and load
212
+
213
+ 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:
214
+
215
+ ```ts
216
+ const bundle = await cart.exportState();
217
+ const json = await cart.exportStateJSON();
218
+
219
+ await cart.importState(bundle);
220
+ await cart.importState(json);
221
+ ```
222
+
223
+ 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`).
224
+
225
+ Compatibility is against the **serializable subset** of live state:
226
+
227
+ - If both sides have a `cartId` (`metadata.id`), they must match.
228
+ - If either the live cart or the bundle is generative, `bundle.seed` must match the live hash (case-insensitive, optional `0x`).
229
+ - The snapshot must share at least one key with the live serializable shape.
230
+ - Extra snapshot keys are ignored. Keys missing from the snapshot keep scaffold values.
231
+ - A type clash on an overlapping key throws. An empty `state` object is rejected.
232
+ - The bundle records canvas buffer size. Load pins the simulation to that size and letterboxes if the window is smaller. A later user resize to a different size clears the pin and restarts from the hash.
233
+
234
+ Not restored: Tone / AudioContext graph (re-init audio on the next `update`), the `Random` sequence, canvas pixels, or hash-derived `featureState`. Typed arrays round-trip; class instances and functions are omitted.
235
+
236
+ ### Optional W/E helpers
237
+
238
+ `exportState` / `importState` do not touch `localStorage`. Two helpers wire **W** / **E** if you want that:
239
+
240
+ ```ts
241
+ // in the cart, once you have keyboardManager + hostChannel
242
+ import { registerCartStateHotkeys } from '@cyberart-io/engine';
243
+
244
+ registerCartStateHotkeys(keyboardManager, hostChannel, {
245
+ persist: 'localStorage',
246
+ });
247
+ ```
248
+
249
+ ```ts
250
+ // in the host
251
+ import { attachCartStatePersistence } from '@cyberart-io/engine';
252
+
253
+ const cart = runtime.mount(artProject, {
254
+ onEvent: attachCartStatePersistence(
255
+ () => cart,
256
+ () => artProject.metadata?.id,
257
+ ),
258
+ });
259
+ ```
260
+
261
+ Pass `captureKeyboard: true` on `createRuntime` or the keys never reach the cart. Without `persist: 'localStorage'`, the host adapter ignores the events.
262
+
263
+ Slots are `localStorage['cyberart.state.' + cartId]`, or per-hash when the cart is generative. JSON can persist even if a framebuffer write fails.
264
+
265
+ ## Canvas
266
+
267
+ The engine prefers, in order:
268
+
269
+ 1. A canvas in the container marked `data-cyberart-canvas` (`CYBERART_CANVAS_ATTR`)
270
+ 2. The container’s first `<canvas>`
271
+ 3. A canvas it creates (no `id="canvas"`)
272
+
273
+ 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.
274
+
275
+ ## Limits
276
+
277
+ - **One audio graph per page.** Tone / master effects are process-global. Destroying a cart disposes cart-owned nodes, not the shared context.
278
+ - **No catalog.** There is no `mount({ id, version })` network fetch. You pass the cart object.
279
+ - **`getCartState()` is live**, not a snapshot. Use export/import for pause-safe state. `snapshot()` is still PNG + seed.
280
+ - **Keyboard defaults off** in embeds (`captureKeyboard: false`). Full-page players pass `true`.
281
+
282
+ ## Publishing this package (maintainers)
283
+
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 `dist/index.d.ts`, `LICENSE`, `README.md`, and `package.json`.
285
+
286
+ ```bash
287
+ pnpm run pack:engine
288
+ cd packages/engine && npm publish --access public
289
+ ```
290
+
291
+ `pack:engine` builds, then runs the same `npm pack` membership check `npm publish` uses. `prepublishOnly` rebuilds immediately before publish. `publishConfig.access` is `public`. You must be `npm login`’d to the `@cyberart-io` org. Do not `npm i github:cyberart-io/cyberart.io` — that downloads this repo’s TypeScript. Version independently of the site. Do not publish from the repo root (`@cyberart.io/source` is private).
@@ -0,0 +1,377 @@
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
+ /** Marker on canvases this runtime created or that a host offered for adoption. */
8
+ declare const CYBERART_CANVAS_ATTR = "data-cyberart-canvas";
9
+
10
+ /**
11
+ * Copyright (c) 2026 Aaron Boyarsky
12
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
13
+ * See packages/engine/LICENSE
14
+ *
15
+ * Audio libraries the animation manager knows how to load and unlock.
16
+ * Carts declare which they need via `metadata.audio`; the manager looks them
17
+ * up here. Add a new adapter when a second library is supported.
18
+ */
19
+ type AudioLibraryId = 'tone';
20
+ type AudioLibrarySpec = AudioLibraryId | AudioLibraryId[];
21
+
22
+ /**
23
+ * Copyright (c) 2026 Aaron Boyarsky
24
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
25
+ * See packages/engine/LICENSE
26
+ */
27
+ type ExternalAssetDependency = {
28
+ cid: string;
29
+ [k: string]: unknown;
30
+ };
31
+ type TokenData = {
32
+ hash: string;
33
+ tokenId: string;
34
+ externalAssetDependencies?: ExternalAssetDependency[];
35
+ /**
36
+ * Per AB Engine Flex generator spec: gateway URL the script is supposed to
37
+ * combine with each `externalAssetDependencies[i].cid` of type `"IPFS"`.
38
+ * AB defaults this to `https://ipfs.artblocks.io/ipfs/` and lets partners
39
+ * override per-contract via support. Optional because standard (non-Flex)
40
+ * projects don't get it.
41
+ */
42
+ preferredIPFSGateway?: string;
43
+ /** Same idea for Arweave-typed external assets. We don't currently use it. */
44
+ preferredArweaveGateway?: string;
45
+ };
46
+
47
+ /**
48
+ * Copyright (c) 2026 Aaron Boyarsky
49
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
50
+ * See packages/engine/LICENSE
51
+ */
52
+
53
+ declare class Random {
54
+ private useA;
55
+ private prngA;
56
+ private prngB;
57
+ constructor(tokenData: TokenData);
58
+ r_zero_one(): number;
59
+ dec(min?: number, max?: number): number;
60
+ int(min: number, max?: number): number;
61
+ bool(p?: number): boolean;
62
+ sign(): 1 | -1;
63
+ choose<T = unknown>(list: T[]): T;
64
+ }
65
+
66
+ type DimensionContext = {
67
+ width: number;
68
+ height: number;
69
+ iWidth: number;
70
+ iHeight: number;
71
+ smallDim: number;
72
+ largeDim: number;
73
+ area: number;
74
+ aspectRatio: number;
75
+ };
76
+
77
+ /**
78
+ * Copyright (c) 2026 Aaron Boyarsky
79
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
80
+ * See packages/engine/LICENSE
81
+ */
82
+ type KeypressHandler = () => void;
83
+ /**
84
+ * Handler for keyboard inputs.
85
+ */
86
+ declare class KeyboardManager {
87
+ private actionMap;
88
+ private debugMode;
89
+ constructor(debugMode?: boolean, captureKeyboard?: boolean);
90
+ /**
91
+ * When a key is pressed, check if there's a corresponding action, and execute it.
92
+ * @param e
93
+ */
94
+ checkKeypress({ key }: KeyboardEvent): void;
95
+ /**
96
+ * Register a certain action to be performed when a given key is pressed.
97
+ * @param key
98
+ * @param action
99
+ * @param overwrite
100
+ */
101
+ registerAction(key: string, action: KeypressHandler, overwrite?: boolean): void;
102
+ /**
103
+ * Remove the global keydown listener and clear registered actions. Call this
104
+ * when the owning animation is torn down so listeners don't accumulate across
105
+ * cart reloads (e.g. on window resize).
106
+ */
107
+ destroy(): void;
108
+ }
109
+
110
+ /**
111
+ * Copyright (c) 2026 Aaron Boyarsky
112
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
113
+ * See packages/engine/LICENSE
114
+ */
115
+ type PointerClick = {
116
+ x: number;
117
+ y: number;
118
+ };
119
+ declare class PointerManager {
120
+ x: number;
121
+ y: number;
122
+ isDown: boolean;
123
+ private clicks;
124
+ private canvas;
125
+ constructor(canvas: HTMLCanvasElement);
126
+ private toCanvasCoords;
127
+ private onPointerDown;
128
+ private onPointerMove;
129
+ private onPointerUp;
130
+ hasClick(): boolean;
131
+ consumeClick(): PointerClick | null;
132
+ destroy(): void;
133
+ }
134
+
135
+ /**
136
+ * Copyright (c) 2026 Aaron Boyarsky
137
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
138
+ * See packages/engine/LICENSE
139
+ *
140
+ * Host ↔ cart mailbox. Inbound events are queued by the host via `dispatch`
141
+ * and drained by cart code that opts in via `consume()`. Outbound events are
142
+ * `emit`ted by the cart and forwarded to host `onEvent` listeners.
143
+ *
144
+ * Carts that never mention this object behave as they do today.
145
+ */
146
+ type HostEvent = {
147
+ type: string;
148
+ payload?: unknown;
149
+ };
150
+ type HostEventListener = (event: HostEvent) => void;
151
+ declare class HostChannel {
152
+ private inbound;
153
+ private listeners;
154
+ dispatch(event: HostEvent): void;
155
+ consume(): HostEvent[];
156
+ emit(event: HostEvent): void;
157
+ onEvent(listener: HostEventListener): () => void;
158
+ clear(): void;
159
+ }
160
+
161
+ /**
162
+ * Copyright (c) 2026 Aaron Boyarsky
163
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
164
+ * See packages/engine/LICENSE
165
+ */
166
+
167
+ type AnimationTiming = {
168
+ now: number;
169
+ startTime: number;
170
+ elapsedSinceStart: number;
171
+ deltaSinceLastUpdate: number;
172
+ deltaSinceLastRender: number;
173
+ };
174
+ type AnimationCart<T = unknown, TFeatureState = undefined> = {
175
+ getDefaultFeatureState?: (R: Random, dimensionContext: DimensionContext, rawParams: number[], keyboardManager: KeyboardManager, customState?: Partial<T>, pointerManager?: PointerManager, gameManager?: unknown, hostChannel?: HostChannel) => TFeatureState;
176
+ getDefaultState: (R: Random, dimensionContext: DimensionContext, rawParams: number[], keyboardManager: KeyboardManager, customState?: Partial<T>, pointerManager?: PointerManager, gameManager?: unknown, featureState?: Readonly<TFeatureState>, hostChannel?: HostChannel) => T;
177
+ 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;
178
+ 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;
179
+ adjust?: Record<string, {
180
+ type: 'switch';
181
+ immediate?: boolean;
182
+ label: string;
183
+ description: string;
184
+ } | {
185
+ type: 'slider';
186
+ immediate?: boolean;
187
+ description: string;
188
+ min: number;
189
+ max: number;
190
+ step: number;
191
+ label: string;
192
+ mapToOutput: (value: number) => number;
193
+ mapToInput: (value: number) => number;
194
+ }>;
195
+ /**
196
+ * Optional cleanup hook invoked when the cart is unloaded (e.g. on window
197
+ * resize, which fully rebuilds the animation). Carts that allocate long-lived
198
+ * resources such as Tone.js audio nodes should dispose them here to avoid
199
+ * leaks accumulating across reloads.
200
+ */
201
+ teardown?: (state: T, featureState?: Readonly<TFeatureState>) => void;
202
+ metadata?: {
203
+ id: string;
204
+ name: string;
205
+ description?: string;
206
+ frameRate: number;
207
+ /**
208
+ * Optional per-visualMode display FPS overrides (e.g. mycelium at 60
209
+ * while the cart baseline stays lower). Keys match cart `visualMode`.
210
+ */
211
+ frameRateByVisualMode?: Partial<Record<string, number>>;
212
+ /**
213
+ * Audio libraries this cart needs (currently `'tone'`). The animation
214
+ * manager loads and unlocks them; omit or leave unset for a silent cart.
215
+ */
216
+ audio?: AudioLibrarySpec;
217
+ /**
218
+ * When true, outputs are a function of the token hash. Saved state may
219
+ * only be loaded back onto the same hash.
220
+ */
221
+ generative?: boolean;
222
+ };
223
+ };
224
+
225
+ type CartStateDimensions = {
226
+ /** Canvas buffer pixels (device pixels). */
227
+ width: number;
228
+ height: number;
229
+ /** CSS layout size of the window/container at save. */
230
+ cssWidth?: number;
231
+ cssHeight?: number;
232
+ dpr?: number;
233
+ };
234
+ type CartStateBundle = {
235
+ version: number;
236
+ cartId?: string;
237
+ /** True when the cart is hash-seeded; load requires the same seed. */
238
+ generative?: boolean;
239
+ seed: string;
240
+ framesElapsed: number;
241
+ /** Pause-aware animation clock; restored so time-based visuals match the save. */
242
+ elapsedSinceStart?: number;
243
+ /** Output size the simulation was running at. Load pins the canvas to this. */
244
+ dimensions?: CartStateDimensions;
245
+ state: unknown;
246
+ };
247
+ declare class IncompatibleCartStateError extends Error {
248
+ constructor(message: string);
249
+ }
250
+
251
+ /**
252
+ * Copyright (c) 2026 Aaron Boyarsky
253
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
254
+ * See packages/engine/LICENSE
255
+ */
256
+
257
+ type FrameErrorInfo = {
258
+ phase: 'update' | 'render' | 'draw';
259
+ consecutive: number;
260
+ stopped: boolean;
261
+ };
262
+ type CreateRuntimeOptions = {
263
+ /** Required mount point. The runtime creates or adopts a canvas inside this element. */
264
+ container: HTMLElement;
265
+ /** Optional token hash (`0x` + 64 hex). Instance-local; does not clobber an existing global cache. */
266
+ seed?: string;
267
+ /**
268
+ * When true, the cart listens for window keydown (full-page players).
269
+ * Defaults to false so an embed does not steal keys from the host.
270
+ */
271
+ captureKeyboard?: boolean;
272
+ /**
273
+ * Audio libraries to unlock if the host calls `unlockAudio()` before `mount`
274
+ * (click can beat idle prebuild). After mount, `cart.metadata.audio` wins.
275
+ */
276
+ audio?: AudioLibrarySpec;
277
+ };
278
+ type MountOptions<T = unknown> = {
279
+ /** Boot overrides passed as `customState` into `getDefaultState`. Not a live-state replay. */
280
+ initialState?: Partial<T>;
281
+ /** Outbound events emitted by cart code via `hostChannel.emit`. */
282
+ onEvent?: HostEventListener;
283
+ /** Site-only. Opaque to the published engine; pass a GameManager from the app. */
284
+ gameManager?: unknown;
285
+ };
286
+ type CartSnapshot = {
287
+ seed: string;
288
+ metadata?: AnimationCart['metadata'];
289
+ pngDataUrl: string;
290
+ };
291
+ type CartHandle = {
292
+ start(): Promise<void>;
293
+ pause(): void;
294
+ resume(): void;
295
+ dispatch(event: HostEvent): void;
296
+ snapshot(): CartSnapshot;
297
+ destroy(): void;
298
+ reload(): void;
299
+ reinit(event?: Event): void;
300
+ /**
301
+ * Live cart state for site chrome (e.g. `/art` debug sliders). Not a public
302
+ * snapshot API — grids and audio nodes are not structured-cloneable.
303
+ */
304
+ getCartState(): unknown;
305
+ exportState(): Promise<CartStateBundle>;
306
+ exportStateJSON(): Promise<string>;
307
+ importState(bundle: CartStateBundle | string, extras?: {
308
+ framebuffer?: ImageData | null;
309
+ }): Promise<void>;
310
+ peekExportedFramebuffer(): ImageData | null;
311
+ peekSeed(): string | undefined;
312
+ isGenerative(): boolean;
313
+ readonly canvas: HTMLCanvasElement | undefined;
314
+ paused: boolean;
315
+ readonly tokenData: TokenData;
316
+ readonly isPrepared: boolean;
317
+ readonly isLoopRunning: boolean;
318
+ readonly needsAudio: boolean;
319
+ readonly audioLibraries: AudioLibraryId[];
320
+ };
321
+ type CyberArtRuntime = {
322
+ mount<T>(cart: AnimationCart<T, any>, options?: MountOptions<T>): CartHandle;
323
+ unlockAudio(): Promise<void>;
324
+ destroy(): void;
325
+ readonly tokenData: TokenData;
326
+ onError?: (error: unknown, info: FrameErrorInfo) => void;
327
+ };
328
+ declare function createRuntime(options: CreateRuntimeOptions): CyberArtRuntime;
329
+
330
+ /**
331
+ * Copyright (c) 2026 Aaron Boyarsky
332
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
333
+ * See packages/engine/LICENSE
334
+ */
335
+
336
+ type CartStateMessageHandler = (message: string, mode: 'persistent' | 'transient') => void;
337
+ type ImportCartStateExtras = {
338
+ /** CPU pixels captured at save. Not part of cartState. */
339
+ framebuffer?: ImageData | null;
340
+ };
341
+ type CartStatePersister = {
342
+ exportState: () => Promise<CartStateBundle>;
343
+ importState: (bundle: CartStateBundle | string, extras?: ImportCartStateExtras) => Promise<void>;
344
+ peekExportedFramebuffer?: () => ImageData | null;
345
+ /** Live token hash; used to look up a generative cart's per-hash save. */
346
+ peekSeed?: () => string | undefined;
347
+ /** Live cart `metadata.generative`. */
348
+ isGenerative?: () => boolean;
349
+ };
350
+ /**
351
+ * Chain `exportState` / `importState` onto localStorage. No-ops unless the
352
+ * cart's W/E event asks for `persist: 'localStorage'`.
353
+ */
354
+ declare function attachCartStatePersistence(getTarget: () => CartStatePersister | null | undefined, getCartId?: () => string | undefined, onMessage?: CartStateMessageHandler): HostEventListener;
355
+
356
+ /**
357
+ * Copyright (c) 2026 Aaron Boyarsky
358
+ * SPDX-License-Identifier: LicenseRef-CyberArt-Engine
359
+ * See packages/engine/LICENSE
360
+ */
361
+
362
+ type CartStatePersistBackend = 'localStorage';
363
+ type CartStateHotkeyOptions = {
364
+ /**
365
+ * Host persistence to chain onto W/E. `exportState` / `importState` stay
366
+ * on the runtime; the host adapter writes and reads this backend.
367
+ */
368
+ persist?: CartStatePersistBackend;
369
+ };
370
+ /**
371
+ * Cart-facing W/E bindings. Emits save/load events; does not touch
372
+ * localStorage. Pass `persist: 'localStorage'` so a host adapter chained
373
+ * with `attachCartStatePersistence` will store and restore the bundle.
374
+ */
375
+ declare function registerCartStateHotkeys(keyboardManager: KeyboardManager, hostChannel: HostChannel, options?: CartStateHotkeyOptions): void;
376
+
377
+ export { type AnimationCart, type AnimationTiming, type AudioLibraryId, type AudioLibrarySpec, CYBERART_CANVAS_ATTR, type CartHandle, type CartSnapshot, type CartStateBundle, type CartStateHotkeyOptions, type CartStateMessageHandler, type CartStatePersister, type CreateRuntimeOptions, type CyberArtRuntime, type DimensionContext, type FrameErrorInfo, HostChannel, type HostEvent, type HostEventListener, type ImportCartStateExtras, IncompatibleCartStateError, KeyboardManager, type MountOptions, type PointerClick, PointerManager, Random, type TokenData, attachCartStatePersistence, createRuntime, registerCartStateHotkeys };
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
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
+ var Ft=(e,t)=>()=>(e&&(t=e(e=0)),t);var jt={};function Kt(e,t){let n=e.indexOf(yt);if(n===-1)return null;let r=n+yt.length,i=`const __cyberSkip=new Set(${JSON.stringify(t)});const __cyberOrig=registerProcessor;registerProcessor=(n,p)=>__cyberSkip.has(n)?undefined:__cyberOrig(n,p);`;return e.slice(0,r)+i+e.slice(r)}var Y,U,Ut,_,yt,wt=Ft(()=>{"use strict";Y=/AudioWorkletProcessor with name:\s*["'`][^"'`]+["'`]\s+is already registered/i,U="__cyberartWorkletPatchApplied";typeof window<"u"&&!window[U]&&(window.addEventListener("error",e=>{let t=e?.message??"";Y.test(t)&&(e.preventDefault(),e.stopImmediatePropagation())},!0),window.addEventListener("unhandledrejection",e=>{let t=e?.reason,n=typeof t=="string"?t:t?.message??"";Y.test(n)&&e.preventDefault()}),window[U]=!0);Ut=!1,_=(...e)=>{Ut&&console.log("[workletPatch]",...e)},yt="((AudioWorkletProcessor,registerProcessor)=>{";if(typeof AudioWorklet<"u"&&!AudioWorklet.prototype[U]){let e=new WeakMap,t=/registerProcessor\s*\(\s*['"`]([^'"`]+)['"`]/g,n=AudioWorklet.prototype.addModule,r=0;AudioWorklet.prototype.addModule=async function(i,s){let o=++r,u=e.get(this);u||(u=new Set,e.set(this,u)),_(`#${o} addModule`,{url:i.slice(0,64),isBlob:i.startsWith("blob:"),knownBefore:[...u]});let l=i,c=null;if(i.startsWith("blob:")){let h=null;try{h=await(await fetch(i)).text()}catch(d){_(`#${o} blob fetch failed`,d)}if(h!==null){let d=new Set;for(let m of h.matchAll(t))d.add(m[1]);if(d.size>0){let m=[...d].filter(C=>u.has(C)),S=[...d].filter(C=>!u.has(C));if(m.length>0&&S.length===0){_(`#${o} short-circuit \u2014 all processors already registered`,m);return}if(m.length>0&&S.length>0){let C=Kt(h,m);if(C!==null){let f=new Blob([C],{type:"application/javascript"});c=URL.createObjectURL(f),l=c,_(`#${o} rewrote blob; skipping`,m,"allowing",S)}else _(`#${o} blob rewrite failed; passing original through`)}S.forEach(C=>u.add(C))}}}return n.call(this,l,s).then(()=>{c&&URL.revokeObjectURL(c)},h=>{c&&URL.revokeObjectURL(c);let d=(h&&h.message)??"";if(!(Y.test(d)||/already registered/i.test(d)))throw h})},AudioWorklet.prototype[U]=!0}});var dt=()=>{let e=["ms","moz","webkit","o"];for(let t=0;t<e.length&&!window.requestAnimationFrame;++t)window.requestAnimationFrame=window[e[t]+"RequestAnimationFrame"],window.cancelAnimationFrame=window[e[t]+"CancelAnimationFrame"]||window[e[t]+"CancelRequestAnimationFrame"]},b=window.devicePixelRatio||1,k=/Headless/i.test(navigator.userAgent),Ae=/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent)||/Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.platform);function ut(){return Math.min(window.innerWidth,window.innerHeight)}var O="data-cyberart-canvas",T=class{constructor(t,n,r=1,a={}){this.width=t,this.height=n;let i=a.container,s=a.adoptFromDocument??!i,o=i?i.querySelector(`canvas[${O}]`)??i.querySelector("canvas"):null;if(o)this.canvas=o,this.adopted=!0;else if(s){let u=document.getElementById("canvas")??document.querySelector("canvas");u?(this.canvas=u,this.adopted=!0):(this.canvas=document.createElement("canvas"),this.canvas.setAttribute("id","canvas"),this.adopted=!1)}else this.canvas=document.createElement("canvas"),this.adopted=!1;this.canvas.hasAttribute(O)||this.canvas.setAttribute(O,""),this.canvas.style.display="block",this.canvas.style.margin="auto",this.ctx=this.canvas.getContext("2d",{colorSpace:"display-p3",alpha:!1}),this.ctx.globalAlpha=r,this.ctx.imageSmoothingEnabled=!1,this.setSize(t,n)}setSize(t,n){this.width=t,this.height=n,this.canvas.width=t,this.canvas.height=n,this.canvas.style.width=`${t/b}px`,this.canvas.style.height=`${n/b}px`,this.imageData=this.ctx.createImageData(t,n),this.ctx.putImageData(this.imageData,0,0)}_blank(t=1){let n=t?Math.max(t,.03):1;this.ctx.fillStyle=`rgba(0,0,0,${n})`,this.ctx.fillRect(0,0,this.width,this.height)}clearAll(){this.ctx.save(),this.ctx.globalAlpha=1,this._blank(),this.ctx.restore()}};function ct(e,t){let n=e/t,r,a;return n>1?(r=e,a=~~(e/n)):(a=t,r=~~(t*n)),{width:r,height:a,iWidth:1/r,iHeight:1/a,area:r*a,largeDim:Math.max(r,a),smallDim:Math.min(r,a),aspectRatio:n}}function ht(e,t=window.innerWidth,n=window.innerHeight){let r=ut(),a,i;return n<t?e>1?t>=r*e?(a=r*e,i=r):(a=t,i=a/e):(a=r*e,i=r):e>1?(a=r,i=r/e):n<r/e?(i=n,a=i*e):(a=r,i=r/e),[a,i]}var H=class{constructor(t=!1,n=!0){this.debugMode=t,this.actionMap={},this.checkKeypress=this.checkKeypress.bind(this),this.registerAction=this.registerAction.bind(this),n&&window.addEventListener("keydown",this.checkKeypress,!1)}checkKeypress({key:t}){this.debugMode&&console.log(`Pressed ${t}`),t in this.actionMap&&this.actionMap[t]()}registerAction(t,n,r=!1){if(t.length!==1)throw new Error(`Only single-character actions may be registered. Cannot register: ${t}`);if(t in this.actionMap&&!r)throw new Error(`Action already exists for code: ${t}`);this.actionMap[t]=n}destroy(){window.removeEventListener("keydown",this.checkKeypress,!1),this.actionMap={}}},lt=H;var B=class{constructor(t){this.x=-1;this.y=-1;this.isDown=!1;this.clicks=[];this.canvas=t,this.onPointerDown=this.onPointerDown.bind(this),this.onPointerMove=this.onPointerMove.bind(this),this.onPointerUp=this.onPointerUp.bind(this),t.addEventListener("pointerdown",this.onPointerDown),t.addEventListener("pointermove",this.onPointerMove),t.addEventListener("pointerup",this.onPointerUp)}toCanvasCoords(t){let n=this.canvas.getBoundingClientRect();return{x:(t.clientX-n.left)*(this.canvas.width/n.width),y:(t.clientY-n.top)*(this.canvas.height/n.height)}}onPointerDown(t){let{x:n,y:r}=this.toCanvasCoords(t);this.x=n,this.y=r,this.isDown=!0,this.clicks.push({x:n,y:r})}onPointerMove(t){let{x:n,y:r}=this.toCanvasCoords(t);this.x=n,this.y=r}onPointerUp(t){let{x:n,y:r}=this.toCanvasCoords(t);this.x=n,this.y=r,this.isDown=!1}hasClick(){return this.clicks.length>0}consumeClick(){return this.clicks.shift()??null}destroy(){this.canvas.removeEventListener("pointerdown",this.onPointerDown),this.canvas.removeEventListener("pointermove",this.onPointerMove),this.canvas.removeEventListener("pointerup",this.onPointerUp)}},pt=B;var mt=(e,t,n,r)=>({now:e,startTime:t,elapsedSinceStart:e-t,deltaSinceLastUpdate:n===null?0:e-n,deltaSinceLastRender:r===null?0:e-r}),N=class{constructor(t,n,r,a,i,s={}){this.startTime=0;this.lastUpdateAt=null;this.lastRenderAt=null;this.clockOffsetMs=0;this.pauseStartedAt=null;this.renderAnimation=this.renderAnimation.bind(this),this.dimensionContext=ct(t,n),this.R=r,this.gameManager=s.gameManager,this.hostChannel=s.hostChannel,this.canvas=new T(this.dimensionContext.width,this.dimensionContext.height,1,s.canvasMount),this.drawingContext=this.canvas.ctx,this.keyboardManager=new lt(!1,s.captureKeyboard??!0),this.pointerManager=new pt(this.canvas.canvas),this.animationCart=i;let o=s.customState;this.featureState=i.getDefaultFeatureState?.(this.R,this.dimensionContext,a,this.keyboardManager,o,this.pointerManager,this.gameManager,this.hostChannel),this.cartState=i.getDefaultState(this.R,this.dimensionContext,a,this.keyboardManager,o,this.pointerManager,this.gameManager,this.featureState,this.hostChannel),this.startTime=performance.now(),this.canvas.clearAll()}captureFramebuffer(){let t=this.canvas.imageData,n=new ImageData(t.width,t.height);return n.data.set(t.data),n}restoreFramebuffer(t){let n=this.canvas.imageData;return n.width!==t.width||n.height!==t.height||n.data.length!==t.data.length?!1:(n.data.set(t.data),this.drawingContext.putImageData(n,0,0),!0)}setPaused(t){let n=performance.now();if(t){this.pauseStartedAt===null&&(this.pauseStartedAt=n);return}this.pauseStartedAt!==null&&(this.clockOffsetMs+=n-this.pauseStartedAt,this.pauseStartedAt=null)}resolveNow(t){let n=this.pauseStartedAt!==null?t-this.pauseStartedAt:0;return t-this.clockOffsetMs-n}getElapsedSinceStart(t=performance.now()){return Math.max(0,this.resolveNow(t)-this.startTime)}restoreClock(t,n){this.pauseStartedAt=null,this.lastUpdateAt=null,this.lastRenderAt=null;let r=Math.max(0,t);if(typeof n=="number"&&Number.isFinite(n)){this.startTime=n,this.clockOffsetMs=performance.now()-n-r;return}this.clockOffsetMs=0,this.startTime=performance.now()-r}async updateAnimationState(t,n,r=performance.now()){let a=this.resolveNow(r),i=mt(a,this.startTime,this.lastUpdateAt,this.lastRenderAt);this.cartState=this.animationCart.update(this.R,t,n,this.dimensionContext,this.cartState,this.keyboardManager,this.pointerManager,this.gameManager,i,this.featureState,this.hostChannel),this.lastUpdateAt=a}async renderAnimation(t,n,r=performance.now()){let a=this.resolveNow(r),i=mt(a,this.startTime,this.lastUpdateAt,this.lastRenderAt);this.animationCart.render(this.R,t,n,this.dimensionContext,this.cartState,this.drawingContext,this.canvas.imageData,this.pointerManager,this.gameManager,i,this.featureState,this.hostChannel),this.lastRenderAt=a}getCanvas(){return this.canvas.canvas}getDrawDetails(){return[this.canvas.canvas,0,0,this.dimensionContext.width,this.dimensionContext.height]}teardown(){try{this.animationCart.teardown?.(this.cartState,this.featureState)}catch(t){console.error("Error during cart teardown",t)}this.keyboardManager.destroy(),this.pointerManager.destroy()}};var W=class extends T{constructor(t,n,r=1,a=!0,i={}){if(super(t,n,r,i),this.adopted)return;let s=i.container;if(s){this.canvas.parentNode!==s&&s.appendChild(this.canvas);return}if(a){let o=document.getElementById("canvas");o&&o!==this.canvas&&o.parentNode===document.body&&document.body.removeChild(o),document.body.appendChild(this.canvas)}}};var I=class{constructor(t){this.useA=!1;let n=function(r){let a=parseInt(r.substring(0,8),16),i=parseInt(r.substring(8,8),16),s=parseInt(r.substring(16,8),16),o=parseInt(r.substring(24,8),16);return function(){a|=0,i|=0,s|=0,o|=0;let u=(a+i|0)+o|0;return o=o+1|0,a=i^i>>>9,i=s+(s<<3)|0,s=s<<21|s>>>11,s=s+u|0,(u>>>0)/4294967296}};this.prngA=n(t.hash.substring(2,32)),this.prngB=n(t.hash.substring(34,32));for(let r=0;r<1e6;r+=2)this.prngA(),this.prngB()}r_zero_one(){return this.useA=!this.useA,this.useA?this.prngA():this.prngB()}dec(t,n){return t===void 0?this.r_zero_one():n===void 0?t*this.r_zero_one():t+(n-t)*this.r_zero_one()}int(t,n){return n===void 0&&(n=t,t=0),Math.floor(this.dec(t,n))}bool(t=.5){return this.r_zero_one()<t}sign(){return this.bool()?1:-1}choose(t){return t[this.int(t.length)]}};var gt="bafybeicjwcq5lxxtfnyj4p2ugcr7ctyb5wl62cbcechj6tsabdq5whcs7u",ft="https://ipfs.io/ipfs/";function Ot(e){return e.endsWith("/")?e:`${e}/`}var L=null;function Ht(e){return[...Array(e)].map(()=>Math.floor(Math.random()*16).toString(16)).join("")}function Bt(e){return e&&e.length>0?e:[{cid:gt}]}function Nt(e){return e.startsWith("0x")?e:`0x${e}`}function Wt(e){if(!e&&typeof tokenData<"u"&&tokenData?.hash)return{hash:tokenData.hash,tokenId:String(tokenData.tokenId),externalAssetDependencies:Bt(tokenData.externalAssetDependencies),preferredIPFSGateway:Ot(tokenData.preferredIPFSGateway||ft),preferredArweaveGateway:tokenData.preferredArweaveGateway};let t=typeof window<"u"?new URLSearchParams(window.location.search):new URLSearchParams;return{hash:e?Nt(e):t.get("hash")||`0x${Ht(64)}`,tokenId:"18009999",externalAssetDependencies:[{cid:gt}],preferredIPFSGateway:ft}}function q(e){let t=e?.useCache!==!1;if(t&&L&&!e?.hash)return L;let n=Wt(e?.hash);return t?L=n:L||(L=n),n}function bt(e){return new Array(32).fill(null).map((t,n)=>parseInt(e.hash.slice(2+n*2,4+n*2),16))}async function J(){let e=await import("tone");if(typeof e.start=="function")return e;let t=e.default;return t&&typeof t.start=="function"?t:e}var St={id:"tone",async load(){await Promise.resolve().then(()=>(wt(),jt))},async unlock(){await St.load(),await(await J()).start()},async resumeIfSuspended(){let e=await J();e.context.state==="suspended"&&await e.context.resume()},async suspendIfRunning(){let t=(await J()).context.rawContext;"suspend"in t&&t.state==="running"&&await t.suspend()}},Ct={tone:St};function X(e){if(!e)return[];let t=Array.isArray(e)?e:[e],n=[];for(let r of t){if(!(r in Ct)){console.warn(`Unknown audio library "${r}"`);continue}let a=r;n.includes(a)||n.push(a)}return n}function Q(e){return e.map(t=>Ct[t])}var K="$cyberartTa",g=class extends Error{constructor(t){super(t),this.name="IncompatibleCartStateError"}},Z={Float32Array,Float64Array,Int8Array,Int16Array,Int32Array,Uint8Array,Uint8ClampedArray,Uint16Array,Uint32Array};function Vt(e){for(let t of Object.keys(Z))if(e instanceof Z[t])return t}function At(e){return ArrayBuffer.isView(e)&&!(e instanceof DataView)}function y(e){if(e===null||typeof e!="object"||Array.isArray(e))return!1;let t=Object.getPrototypeOf(e);return t===Object.prototype||t===null}function tt(e){return y(e)?typeof e[K]=="string"&&Array.isArray(e.d):!1}function $t(e){return e==null?!1:typeof e=="function"?!0:!(typeof e!="object"||Array.isArray(e)||At(e)||y(e))}var Gt=new Set(["audioContextStarted","midiSynths","keepRatioPrimed","audioInitStarted","musicSeekApplied"]);function vt(e){return e===null?"null":tt(e)?String(e[K]):Array.isArray(e)?"array":y(e)?"object":typeof e}function zt(e,t){return e===t||e==="null"||t==="null"}function A(e){return e.trim().toLowerCase().replace(/^0x/,"")}function Et(e,t,n,r){if(!(!n&&!r)&&A(e)!==A(t))throw new g(`Generative cart state is for seed "${t}", but the live token is "${e}"`)}function P(e){return typeof e=="number"&&Number.isFinite(e)&&e>0}function xt(e,t){let n=e.dimensions;if(n&&P(n.width)&&P(n.height))return n;if(t&&P(t.width)&&P(t.height))return{width:t.width,height:t.height};if(y(e.state)){let r=e.state.containerWidth,a=e.state.containerHeight;if(P(r)&&P(a))return{width:r,height:a}}return null}function D(e,t=new WeakSet){if(e===null||typeof e=="string"||typeof e=="boolean")return e;if(typeof e=="number")return Number.isFinite(e)?e:null;if(!(typeof e=="function"||typeof e>"u"||typeof e=="bigint"||typeof e=="symbol")){if(At(e)){let n=Vt(e);return n?{[K]:n,d:Array.from(e)}:void 0}if(typeof e=="object"&&!t.has(e)){t.add(e);try{if(Array.isArray(e)){let a=[],i=0;for(let s of e){let o=D(s,t);o!==void 0&&(a.push(o),i++)}return e.length>0&&i===0?void 0:a}if(!y(e)||$t(e))return;let n=Object.keys(e),r={};for(let a of n){if(Gt.has(a))continue;let i=D(e[a],t);i!==void 0&&(r[a]=i)}return n.length>0&&Object.keys(r).length===0?void 0:r}finally{t.delete(e)}}}}function F(e){if(tt(e)){let t=Z[e[K]];return t?new t(e.d.map(n=>Number(n))):e}if(Array.isArray(e))return e.map(t=>F(t));if(y(e)){let t={};for(let n of Object.keys(e))t[n]=F(e[n]);return t}return e}function et(e,t,n,r,a=!0){if(a&&n&&r&&n!==r)throw new g(`Cart state is for "${r}", but the live cart is "${n}"`);if(!y(t)){if(a)throw new g("Cart state snapshot is empty");return}let i=Object.keys(t);if(i.length===0){if(a)throw new g("Cart state snapshot is empty");return}let s=D(e);if(!y(s)||Object.keys(s).length===0){if(a)throw new g("Live cart has no serializable state");return}let o=i.filter(u=>u in s);if(o.length===0){if(a)throw new g("Cart state snapshot does not overlap the live cart shape");return}for(let u of o){let l=vt(s[u]),c=vt(t[u]);if(!zt(l,c))throw new g(`Cart state field "${u}" has type ${c}, live cart expects ${l}`);if(l==="object"){let h=t[u],d=e[u];y(h)&&!tt(h)&&et(d,h,void 0,void 0,!1)}}}function kt(e,t){if(!y(e)||!y(t))return;let n=D(e);if(y(n))for(let r of Object.keys(t))r in n&&(e[r]=F(t[r]))}function Tt(e){let t=typeof e=="string"?JSON.parse(e):e;if(!t||typeof t!="object")throw new g("Cart state bundle is not an object");if(t.version!==1)throw new g(`Unsupported cart state version ${String(t.version)}`);if(typeof t.seed!="string"||typeof t.framesElapsed!="number")throw new g("Cart state bundle is missing seed or framesElapsed");return t}function Yt(e){if(!e||typeof e!="object")return 0;let t=e;return typeof t.startTime=="number"&&typeof t.lastSimulationUpdateAt=="number"?Math.max(0,t.lastSimulationUpdateAt-t.startTime):0}function Jt(e,t){if(typeof window.resizeTo!="function")return!1;try{if(window.top!==window)return!1}catch{return!1}let n=Math.max(1,Math.round(e)),r=Math.max(1,Math.round(t)),a=Math.max(0,window.outerWidth-window.innerWidth),i=Math.max(0,window.outerHeight-window.innerHeight);try{window.resizeTo(n+a,r+i)}catch{return!1}return Math.abs(window.innerWidth-n)<=8&&Math.abs(window.innerHeight-r)<=8}var Xt=[[0,0],[16,9],[9,16],[4,3],[3,4],[2,1],[1,2],[3,1],[1,3],[1,1]],rt=Xt[0],Qt=rt[0]/rt[1],nt=30,Zt=120,te=3840/2160,ee=2160/3840,j=class{constructor(t=nt,n=!1,r=window.innerWidth,a=window.innerHeight,i=!0,s,o={}){this.framesElapsed=0;this.frameRate=nt;this._paused=!1;this.savedToken=!1;this.prepared=!1;this.loopRunning=!1;this.fallbackAudio=[];this.audioLibrariesInternal=[];this.consecutiveErrorCount=0;this.lastErrorLogAt=0;this.reinitStayPaused=!1;this.reinitPending=!1;this.lastExportedFramebuffer=null;this.pinnedOutput=null;this.pinViewportAtImport=null;this.importing=!1;this.drawLoop=this.drawLoop.bind(this),this.loadCart=this.loadCart.bind(this),this.prepareCart=this.prepareCart.bind(this),this.beginPlayback=this.beginPlayback.bind(this),this.reinit=this.reinit.bind(this),this.tuneFramerate=this.tuneFramerate.bind(this),this.container=o.container,this.seed=o.seed,this.captureKeyboard=o.captureKeyboard??!0,this.customState=o.customState,this.hostChannel=o.hostChannel,this.fallbackAudio=X(o.audio),this.audioLibrariesInternal=this.fallbackAudio,this.isBodyCanvas=this.container?!1:i,this.autoMode=n,this.aspectRatio=Qt,this.selectedAsp=rt,this.isFullscreen=!!document.fullscreenElement,this.updateFrameRate(t),this.tempUnpause=!1,this.synchronous=k,this.containerWidth=r,this.containerHeight=a,this.gameManager=s,this.init(!0)}setGameManager(t){this.gameManager=t}reinit(t){if(!this.importing){if(this.shouldKeepPinnedOutput()){this.fitPinnedCanvas();return}this.reinitPending||(this.reinitStayPaused=this._paused,this.reinitPending=!0),this.paused=!0,clearTimeout(this.reinitTimeout),this.reinitTimeout=setTimeout(()=>{if(this.importing){this.paused=this.reinitStayPaused,this.reinitPending=!1;return}this.updateInProgress?this.reinit(t):(this.requestedAnimationFrame&&cancelAnimationFrame(this.requestedAnimationFrame),this.clearPinnedOutput(),this.init(),this.reloadCart(),this.paused=this.reinitStayPaused,this.reinitPending=!1)},150)}}initSeed(){this.tokenData=this.seed?q({hash:this.seed,useCache:!1}):q(),this.rawParams=bt(this.tokenData),this.mintNo=parseInt(this.tokenData.tokenId)%1e6,console.log("token",this.mintNo,this.tokenData.hash)}init(t=!1){t&&this.initSeed(),this.pureAspectUpdate(this.selectedAsp);let n=this.aspectRatio,[r,a]=ht(n,this.containerWidth,this.containerHeight);r*=b,a*=b,this.width=~~r,this.height=~~a,this.atCenterX=this.width/2,this.atCenterY=this.height/2,this.mainCanvas?this.mainCanvas.setSize(this.width,this.height):this.mainCanvas=new W(this.width,this.height,1,this.isBodyCanvas,this.canvasMountOptions())}loadCart(t){this.prepareCart(t)&&this.beginPlayback()}prepareCart(t){if(this.prepared)return!0;try{let n=~~(this.width/1),r=~~(this.height/this.width*n),a=new I(this.tokenData),i=new N(n,r,a,this.rawParams,t,{gameManager:this.gameManager,canvasMount:this.canvasMountOptions(),customState:this.customState,captureKeyboard:this.captureKeyboard,hostChannel:this.hostChannel});return this.animation=i,this.animationCart=t,this.paused=!1,this.framesElapsed=0,this.R=a,this.applyCartFrameRate(t,i),this.resolveAudioLibraries(t),this.prepared=!0,!0}catch(n){return console.error(n),!1}}applyCartFrameRate(t,n){let r=t.metadata,a=r?.frameRate??nt,i=n.cartState?.visualMode??n.featureState?.visualMode,s=i&&r?.frameRateByVisualMode?r.frameRateByVisualMode[i]:void 0;this.updateFrameRate(typeof s=="number"&&s>0?s:a)}get audioLibraries(){return this.audioLibrariesInternal}get needsAudio(){return this.audioLibrariesInternal.length>0}resolveAudioLibraries(t){this.audioLibrariesInternal=X(t.metadata?.audio)}async unlockAudio(){if(this.audioLibrariesInternal.length===0)return;let t=Q(this.audioLibrariesInternal);if(k){for(let n of t)await n.load();return}for(let n of t)await n.unlock()}async resumeAudioLibraries(){if(!(this.paused||k))for(let t of Q(this.audioLibrariesInternal))try{await t.resumeIfSuspended()}catch(n){console.error(`Failed to resume audio library "${t.id}" after reload`,n)}}beginPlayback(){!this.prepared||this.loopRunning||(this.prepared=!1,this.loopRunning=!0,this.consecutiveErrorCount=0,this.requestedAnimationFrame=requestAnimationFrame(this.drawLoop))}get isPrepared(){return this.prepared}get isLoopRunning(){return this.loopRunning}get paused(){return this._paused}set paused(t){this._paused!==t&&(this._paused=t,this.animation?.setPaused(t))}async waitUntilUpdateIdle(){for(;this.updateInProgress;)await new Promise(t=>{typeof requestAnimationFrame=="function"?requestAnimationFrame(()=>t()):setTimeout(t,0)})}requirePreparedAnimation(){if(!this.animation)throw new Error("No prepared cart");return this.animation}async exportState(){let t=this.requirePreparedAnimation(),n=this._paused;this.paused=!0,await this.waitUntilUpdateIdle();try{this.lastExportedFramebuffer=t.captureFramebuffer();let r=t.getCanvas();return{version:1,cartId:this.animationCart?.metadata?.id,generative:this.animationCart?.metadata?.generative,seed:this.tokenData.hash,framesElapsed:this.framesElapsed,elapsedSinceStart:t.getElapsedSinceStart(),dimensions:{width:r.width,height:r.height,cssWidth:this.containerWidth,cssHeight:this.containerHeight,dpr:b},state:D(t.cartState)}}finally{this.paused=n}}async exportStateJSON(){return JSON.stringify(await this.exportState())}peekExportedFramebuffer(){return this.lastExportedFramebuffer}peekSeed(){return this.tokenData?.hash}isGenerative(){return this.animationCart?.metadata?.generative===!0}async importState(t,n){let r=this.requirePreparedAnimation(),a=Tt(t),i=this._paused,s=this.loopRunning;this.paused=!0,await this.waitUntilUpdateIdle(),et(r.cartState,a.state,this.animationCart?.metadata?.id,a.cartId),Et(this.tokenData.hash,a.seed,this.animationCart?.metadata?.generative,a.generative),clearTimeout(this.reinitTimeout),this.reinitPending=!1,this.importing=!0;let o=xt(a,n?.framebuffer??this.lastExportedFramebuffer);if(o){this.pinOutputSize(o);let c=o.cssWidth??o.width/b,h=o.cssHeight??o.height/b;Jt(c,h),this.syncContainerFromDom(),this.pinViewportAtImport={w:this.containerWidth,h:this.containerHeight}}let u=this.customState,l=!1;try{if(this.customState=F(a.state),this.unloadCart(),!this.prepareCart(this.animationCart))throw new Error("Failed to prepare cart while loading state");this.animation.cartState!==this.customState&&kt(this.animation.cartState,a.state),this.framesElapsed=a.framesElapsed;let c=this.animation,h=c.cartState,d=typeof a.elapsedSinceStart=="number"?a.elapsedSinceStart:Yt(h);c.restoreClock(d,typeof h?.startTime=="number"?h.startTime:void 0),i&&c.setPaused(!0);let m=n?.framebuffer??this.lastExportedFramebuffer;m&&c.restoreFramebuffer(m)?h.keepRatioPrimed=!0:await c.renderAnimation(this.framesElapsed,this.rawParams),this.fitPinnedCanvas(),l=!0}catch(c){throw this.clearPinnedOutput(),this.customState=u,this.paused=i,this.animationCart&&(this.syncContainerFromDom(),this.init(),this.reloadCart()),c}finally{this.importing=!1,l&&(this.customState=u,this.paused=i,s&&this.beginPlayback())}}unloadCart(){this.loopRunning=!1,this.requestedAnimationFrame&&cancelAnimationFrame(this.requestedAnimationFrame),this.drawLoopTimeout&&clearTimeout(this.drawLoopTimeout),this.animation?.teardown(),this.animation=void 0,this.prepared=!1,this.audioLibrariesInternal=this.fallbackAudio,this.mainCanvas?.clearAll()}reloadCart(t=!1){let n=this.animationCart;if(!n)return;this.pinnedOutput&&(this.clearPinnedOutput(),this.syncContainerFromDom(),this.init());let r=this.reinitPending?this.reinitStayPaused:this._paused;this.unloadCart(),t&&this.initSeed(),this.loadCart(n),this.paused=r,!this.paused&&!k&&this.resumeAudioLibraries()}getFrameRate(){return this.frameRate}updateFrameRate(t){if(this.frameRate=t,t>=1)this.targetDrawTime=1e3/t;else{let r=1/(2-t);this.targetDrawTime=1e3/r}this.drawTime=this.targetDrawTime}canvasMountOptions(){return{container:this.container,adoptFromDocument:!this.container}}syncContainerFromDom(){if(this.container){this.containerWidth=this.container.clientWidth||this.containerWidth,this.containerHeight=this.container.clientHeight||this.containerHeight;return}this.containerWidth=window.innerWidth,this.containerHeight=window.innerHeight}pinOutputSize(t){this.pinnedOutput=t,this.width=~~t.width,this.height=~~t.height,this.atCenterX=this.width/2,this.atCenterY=this.height/2,this.aspectRatio=this.width/this.height,this.mainCanvas&&this.mainCanvas.setSize(this.width,this.height)}shouldKeepPinnedOutput(){return this.pinnedOutput?(this.syncContainerFromDom(),this.viewportMatches(this.pinnedCssSize())?!0:!!(this.pinViewportAtImport&&this.viewportMatches(this.pinViewportAtImport))):!1}pinnedCssSize(){let t=this.pinnedOutput;return{w:t.cssWidth??t.width/b,h:t.cssHeight??t.height/b}}viewportMatches(t){return Math.abs(this.containerWidth-t.w)<8&&Math.abs(this.containerHeight-t.h)<8}fitPinnedCanvas(){let t=this.pinnedOutput,n=this.canvas;if(!t||!n)return;this.container&&!this.container.style.position&&(this.container.style.position="relative");let r=this.containerWidth||window.innerWidth,a=this.containerHeight||window.innerHeight,i=t.width/b,s=t.height/b;if(i<1||s<1||r<1||a<1)return;let o=Math.min(r/i,a/s),u=i*o,l=s*o;n.style.position="absolute",n.style.left="50%",n.style.top="50%",n.style.transform="translate(-50%, -50%)",n.style.margin="0",n.style.width=`${u}px`,n.style.height=`${l}px`,n.style.imageRendering=Math.abs(o-1)<.01?"auto":"pixelated"}clearPinnedOutput(){let t=this.canvas;this.pinnedOutput=null,this.pinViewportAtImport=null,t&&(t.style.position="",t.style.left="",t.style.top="",t.style.transform="",t.style.imageRendering="",t.style.width="",t.style.height="",t.style.margin="auto")}get canvas(){return this.animation?.getCanvas()??this.mainCanvas?.canvas}destroy(){this.clearPinnedOutput(),this.unloadCart(),clearTimeout(this.reinitTimeout),this.mainCanvas&&!this.mainCanvas.adopted&&this.mainCanvas.canvas.remove()}drawIt(){}tuneFramerate(t){let n=this.lastCall||t;this.lastCall=t,this.drawTime=Math.max(0,this.drawTime+this.targetDrawTime-this.lastCall+n)}async drawLoop(t){if(!this.loopRunning)return;let n=!1,r,a="update";try{if(!this.paused||this.tempUnpause){if(k||this.tuneFramerate(t),!this.animation)return;if(a="update",this.updateInProgress=!0,await this.animation.updateAnimationState(this.framesElapsed,this.rawParams,t),this.updateInProgress=!1,a="render",await this.animation.renderAnimation(this.framesElapsed,this.rawParams,t),this.autoMode&&this.animation.cartState.saveToken&&!this.savedToken&&!new URLSearchParams(window.location.search).get("hash")){let o=localStorage.getItem("goodones"),u=JSON.stringify(o?[...JSON.parse(o),this.tokenData.hash]:[this.tokenData.hash]);localStorage.setItem("goodones",u),this.savedToken=!0,localStorage.setItem(this.tokenData.hash,this.mainCanvas.canvas.toDataURL("image/png")),window.location.reload(),this.loopRunning=!1;return}this.framesElapsed++,this.tempUnpause&&(this.tempUnpause=!1)}a="draw",this.drawIt()}catch(i){n=!0,r=i,this.handleFrameError(i,a)}finally{this.updateInProgress=!1}n?(this.consecutiveErrorCount++,this.consecutiveErrorCount>=Zt&&(this.loopRunning=!1,this.onError?.(r,{phase:a,consecutive:this.consecutiveErrorCount,stopped:!0}))):this.consecutiveErrorCount=0,this.loopRunning&&(this.drawLoopTimeout=setTimeout(()=>{this.requestedAnimationFrame=requestAnimationFrame(this.drawLoop)},this.drawTime))}handleFrameError(t,n){let r=performance.now();r-this.lastErrorLogAt>=1e3&&(this.lastErrorLogAt=r,console.error(`Animation ${n} error:`,t)),this.consecutiveErrorCount===0&&this.onError?.(t,{phase:n,consecutive:1,stopped:!1})}pureAspectUpdate(t){if(t[0]===0&&t[1]===0){let r=this.containerWidth||window.innerWidth,a=this.containerHeight||window.innerHeight,i=r/a;this.aspectRatio=Math.min(te,Math.max(ee,i))}else this.aspectRatio=t[0]/t[1]}};var M=class{constructor(){this.inbound=[];this.listeners=[]}dispatch(t){this.inbound.push(t),this.inbound.length>32&&this.inbound.splice(0,this.inbound.length-32)}consume(){if(this.inbound.length===0)return[];let t=this.inbound;return this.inbound=[],t}emit(t){for(let n of this.listeners)n(t)}onEvent(t){return this.listeners.push(t),()=>{this.listeners=this.listeners.filter(n=>n!==t)}}clear(){this.inbound=[],this.listeners=[]}};function ne(e){dt();let{container:t,seed:n,captureKeyboard:r=!1,audio:a}=e,i=new M,s=!1,o=null,u,l=!1,c=t.clientWidth||window.innerWidth,h=t.clientHeight||window.innerHeight,d=new j(30,!1,c,h,!1,void 0,{container:t,seed:n,captureKeyboard:r,hostChannel:i,audio:a}),m=()=>{let f=t.clientWidth||window.innerWidth,v=t.clientHeight||window.innerHeight;return f===d.containerWidth&&v===d.containerHeight?!1:(d.containerWidth=f,d.containerHeight=v,!0)},S=null;return typeof ResizeObserver<"u"&&(S=new ResizeObserver(()=>{if(s||!o)return;m()&&l&&d.reinit()}),S.observe(t)),d.onError=(f,v)=>{u?.(f,v)},{get tokenData(){return d.tokenData},get onError(){return u},set onError(f){u=f},mount(f,v={}){if(s)throw new Error("createRuntime: cannot mount on a destroyed runtime");o?.destroy(),l=!1,d.customState=v.initialState,d.setGameManager(v.gameManager);let ot=v.onEvent?i.onEvent(v.onEvent):()=>{};if(!d.prepareCart(f))throw ot(),new Error("createRuntime: failed to prepare cart");let p=!1,z={async start(){if(!p){if(m()&&d.isPrepared&&(d.init(),d.unloadCart(),!d.prepareCart(f)))throw new Error("createRuntime: failed to prepare cart");d.needsAudio&&(await d.unlockAudio(),p)||(l=!0,d.beginPlayback())}},pause(){p||(d.paused=!0)},resume(){p||(d.paused=!1)},dispatch(w){p||i.dispatch(w)},snapshot(){if(p)return{seed:"",pngDataUrl:""};let w=d.canvas;return{seed:d.tokenData.hash,metadata:f.metadata,pngDataUrl:w?w.toDataURL("image/png"):""}},destroy(){p||(p=!0,l=!1,ot(),i.clear(),d.unloadCart(),o===z&&(o=null))},reload(){if(!p){if(!l){if(d.isPrepared&&(d.unloadCart(),!d.prepareCart(f)))throw new Error("createRuntime: failed to prepare cart");return}d.reloadCart()}},reinit(w){if(!p){if(!l){m();return}d.reinit(w)}},getCartState(){return d.animation?.cartState},exportState(){return p?Promise.reject(new Error("Cart handle has been destroyed")):d.exportState()},exportStateJSON(){return p?Promise.reject(new Error("Cart handle has been destroyed")):d.exportStateJSON()},async importState(w,_t){if(p)throw new Error("Cart handle has been destroyed");await d.importState(w,_t)},peekExportedFramebuffer(){return p?null:d.peekExportedFramebuffer()},peekSeed(){if(!p)return d.peekSeed()},isGenerative(){return p?!1:d.isGenerative()},get canvas(){return d.canvas},get paused(){return d.paused},set paused(w){p||(d.paused=w)},get tokenData(){return d.tokenData},get isPrepared(){return d.isPrepared},get isLoopRunning(){return d.isLoopRunning},get needsAudio(){return d.needsAudio},get audioLibraries(){return d.audioLibraries}};return o=z,z},async unlockAudio(){await d.unlockAudio()},destroy(){s||(s=!0,o?.destroy(),o=null,S?.disconnect(),S=null,i.clear(),d.destroy())}}}var V="cyberart.state.save",$="cyberart.state.load";function re(e,t,n={}){let r=n.persist,a=r?{persist:r}:void 0,i=()=>{t.emit({type:V,payload:a})},s=()=>{t.emit({type:$,payload:a})};e.registerAction("w",i,!0),e.registerAction("W",i,!0),e.registerAction("e",s,!0),e.registerAction("E",s,!0)}function Pt(e){if(e.type!==V&&e.type!==$)return;let t=e.payload?.persist;return t==="localStorage"?t:void 0}var ae="cyberart-io",ie=1,E="framebuffers";function at(e,t){return`${e||"unknown"}:${t?A(t):"none"}`}function Mt(e,t){return`${e||"unknown"}:${t||"none"}`}function it(){return new Promise((e,t)=>{let n=indexedDB.open(ae,ie);n.onupgradeneeded=()=>{let r=n.result;r.objectStoreNames.contains(E)||r.createObjectStore(E)},n.onsuccess=()=>e(n.result),n.onerror=()=>t(n.error??new Error("indexedDB open failed"))})}function se(e){let t=e.data;return t.buffer.slice(t.byteOffset,t.byteOffset+t.byteLength)}function Dt(e,t){return new Promise((n,r)=>{let i=e.transaction(E,"readonly").objectStore(E).get(t);i.onsuccess=()=>n(i.result),i.onerror=()=>r(i.error??new Error("indexedDB read failed"))})}async function Rt(e,t,n){if(typeof indexedDB>"u")return;let r=await it(),a={width:n.width,height:n.height,data:se(n)};try{await new Promise((i,s)=>{let o=r.transaction(E,"readwrite");o.oncomplete=()=>i(),o.onerror=()=>s(o.error??new Error("indexedDB write failed")),o.objectStore(E).put(a,at(e,t))})}finally{r.close()}}async function It(e,t){if(typeof indexedDB>"u")return null;let n=await it();try{let r=at(e,t),a=Mt(e,t),i=await Dt(n,r)??(r===a?void 0:await Dt(n,a));if(!i||!i.data||i.width<1||i.height<1)return null;let s=new ImageData(i.width,i.height);return s.data.set(new Uint8ClampedArray(i.data)),s}finally{n.close()}}async function Lt(e,t){if(typeof indexedDB>"u")return;let n=await it();try{await new Promise((r,a)=>{let i=n.transaction(E,"readwrite");i.oncomplete=()=>r(),i.onerror=()=>a(i.error??new Error("indexedDB delete failed"));let s=i.objectStore(E);s.delete(at(e,t)),s.delete(Mt(e,t))})}finally{n.close()}}var st="cyberart.state.",oe="Saved state",de="Loaded state",ue="Failed to save state",ce="Failed to load state",he="No saved state";function x(e,t){let n=e||"unknown";return t?st+n+"."+A(t):st+n}function le(e,t,n){return r=>{if(Pt(r)==="localStorage"){if(r.type===V){pe(e,t,n);return}r.type===$&&me(e,t,n)}}}function R(e){try{let t=JSON.parse(e);if(!t||typeof t!="object")return null;let n=t;if(typeof n.savedAt=="number"&&n.bundle&&typeof n.bundle=="object")return{savedAt:n.savedAt,bundle:n.bundle};let r=t;return typeof r.seed=="string"&&typeof r.version=="number"?{savedAt:0,bundle:r}:null}catch{return null}}async function pe(e,t,n){let r=e();if(r)try{let a=await r.exportState(),i=a.cartId??t?.();a.generative===!0?(Ce(i),G(x(i,a.seed),a)):G(x(i),a);let s=r.peekExportedFramebuffer?.();if(s)try{await Rt(i,a.seed,s)}catch(o){console.warn("Failed to save cart framebuffer",o)}n?.(oe,"transient")}catch(a){console.error("Failed to save cart state",a),n?.(ue,"transient")}}async function me(e,t,n){let r=e();if(!r)return;let a=t?.(),i=r.peekSeed?.(),s=r.isGenerative?.()===!0,o=we(a,s,i);if(!o){console.warn("No saved cart state for",a||"unknown",s&&i?`(hash ${i})`:""),n?.(he,"transient");return}try{let u=await It(a,o.bundle.seed).catch(()=>null);await r.importState(o.bundle,{framebuffer:u}),n?.(de,"transient")}catch(u){console.error("Failed to load cart state",u),n?.(ce,"transient")}}function G(e,t){let n=JSON.stringify({savedAt:Date.now(),bundle:t});fe(e,n)}function fe(e,t){for(;;)try{localStorage.setItem(e,t);return}catch(n){if(!ge(n)||!be(e))throw n}}function ge(e){if(!e||typeof e!="object")return!1;let t=e.name,n=e.code;return t==="QuotaExceededError"||t==="NS_ERROR_DOM_QUOTA_REACHED"||n===22||n===1014}function be(e){let t=null,n=1/0;for(let i of ye()){if(i===e)continue;let s=localStorage.getItem(i);if(!s)continue;let u=R(s)?.savedAt??0;u<n&&(n=u,t=i)}if(!t)return!1;let r=localStorage.getItem(t),a=r?R(r):null;return localStorage.removeItem(t),a?.bundle&&Lt(a.bundle.cartId,a.bundle.seed).catch(()=>{}),!0}function ye(){let e=[];for(let t=0;t<localStorage.length;t++){let n=localStorage.key(t);n&&n.startsWith(st)&&e.push(n)}return e}function we(e,t,n){if(t&&n){let a=localStorage.getItem(x(e,n));if(a)return R(a);let i=localStorage.getItem(x(e));if(!i)return null;let s=R(i);if(!s||!Se(s.bundle,n))return null;try{G(x(e,n),s.bundle)}catch{}return s}let r=localStorage.getItem(x(e));return r?R(r):null}function Se(e,t){return typeof e.seed=="string"&&A(e.seed)===A(t)}function Ce(e){let t=x(e),n=localStorage.getItem(t);if(!n)return;let r=R(n);if(r&&typeof r.bundle.seed=="string"){let a=x(e,r.bundle.seed);if(!localStorage.getItem(a))try{G(a,r.bundle)}catch{return}}localStorage.removeItem(t)}export{O as CYBERART_CANVAS_ATTR,M as HostChannel,g as IncompatibleCartStateError,H as KeyboardManager,B as PointerManager,I as Random,le as attachCartStatePersistence,ne as createRuntime,re as registerCartStateHotkeys};
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@cyberart-io/engine",
3
+ "version": "0.0.1",
4
+ "description": "Embeddable CyberArt host engine: mount a cart, pause, snapshot, and save/load state.",
5
+ "type": "module",
6
+ "license": "SEE LICENSE IN LICENSE",
7
+ "files": [
8
+ "dist/index.js",
9
+ "dist/index.d.ts"
10
+ ],
11
+ "main": "./dist/index.js",
12
+ "module": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js"
18
+ }
19
+ },
20
+ "sideEffects": false,
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/cyberart-io/cyberart.io.git",
24
+ "directory": "packages/engine"
25
+ },
26
+ "homepage": "https://github.com/cyberart-io/cyberart.io/tree/main/packages/engine",
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "keywords": [
31
+ "cyberart",
32
+ "generative-art",
33
+ "canvas",
34
+ "animation"
35
+ ],
36
+ "peerDependencies": {
37
+ "tone": "^14.8.15"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "tone": {
41
+ "optional": true
42
+ }
43
+ },
44
+ "scripts": {
45
+ "build": "node ../../scripts/build-engine.mjs",
46
+ "prepublishOnly": "node ../../scripts/build-engine.mjs"
47
+ }
48
+ }