effect-motion 0.2.0

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.
Files changed (84) hide show
  1. package/README.md +40 -0
  2. package/dist/Camera.d.ts +49 -0
  3. package/dist/Camera.js +33 -0
  4. package/dist/Entity.d.ts +40 -0
  5. package/dist/Entity.js +32 -0
  6. package/dist/Fonts.d.ts +31 -0
  7. package/dist/Fonts.js +11 -0
  8. package/dist/Instance.d.ts +16 -0
  9. package/dist/Instance.js +18 -0
  10. package/dist/Motion.d.ts +74 -0
  11. package/dist/Motion.js +125 -0
  12. package/dist/Phaser.d.ts +65 -0
  13. package/dist/Phaser.js +170 -0
  14. package/dist/Physics.d.ts +78 -0
  15. package/dist/Physics.js +117 -0
  16. package/dist/Renderer.d.ts +69 -0
  17. package/dist/Renderer.js +90 -0
  18. package/dist/Runner.d.ts +360 -0
  19. package/dist/Runner.js +257 -0
  20. package/dist/Scene.d.ts +241 -0
  21. package/dist/Scene.js +454 -0
  22. package/dist/Time.d.ts +38 -0
  23. package/dist/Time.js +43 -0
  24. package/dist/Timing.d.ts +96 -0
  25. package/dist/Timing.js +151 -0
  26. package/dist/demo.d.ts +186 -0
  27. package/dist/demo.js +76 -0
  28. package/dist/index.d.ts +13 -0
  29. package/dist/index.js +13 -0
  30. package/dist/particles/Particle.d.ts +91 -0
  31. package/dist/particles/Particle.js +1 -0
  32. package/dist/particles/ParticleField.d.ts +170 -0
  33. package/dist/particles/ParticleField.js +69 -0
  34. package/dist/particles/Prng.d.ts +28 -0
  35. package/dist/particles/Prng.js +43 -0
  36. package/dist/particles/constructors.d.ts +78 -0
  37. package/dist/particles/constructors.js +15 -0
  38. package/dist/particles/index.d.ts +6 -0
  39. package/dist/particles/index.js +6 -0
  40. package/dist/particles/overLife.d.ts +16 -0
  41. package/dist/particles/overLife.js +21 -0
  42. package/dist/particles/render.d.ts +13 -0
  43. package/dist/particles/render.js +33 -0
  44. package/dist/particles/simulate.d.ts +39 -0
  45. package/dist/particles/simulate.js +81 -0
  46. package/dist/particles/step.d.ts +24 -0
  47. package/dist/particles/step.js +167 -0
  48. package/dist/shapes/Circle.d.ts +37 -0
  49. package/dist/shapes/Circle.js +9 -0
  50. package/dist/shapes/Ellipse.d.ts +41 -0
  51. package/dist/shapes/Ellipse.js +10 -0
  52. package/dist/shapes/Group.d.ts +150 -0
  53. package/dist/shapes/Group.js +82 -0
  54. package/dist/shapes/Layer.d.ts +9 -0
  55. package/dist/shapes/Layer.js +23 -0
  56. package/dist/shapes/Line.d.ts +47 -0
  57. package/dist/shapes/Line.js +30 -0
  58. package/dist/shapes/Path.d.ts +38 -0
  59. package/dist/shapes/Path.js +13 -0
  60. package/dist/shapes/Rect.d.ts +41 -0
  61. package/dist/shapes/Rect.js +10 -0
  62. package/dist/shapes/Shape2D.d.ts +40 -0
  63. package/dist/shapes/Shape2D.js +41 -0
  64. package/dist/shapes/Square.d.ts +37 -0
  65. package/dist/shapes/Square.js +11 -0
  66. package/dist/shapes/Text.d.ts +60 -0
  67. package/dist/shapes/Text.js +24 -0
  68. package/dist/shapes/index.d.ts +10 -0
  69. package/dist/shapes/index.js +10 -0
  70. package/dist/svg/SvgDomRenderer.d.ts +28 -0
  71. package/dist/svg/SvgDomRenderer.js +50 -0
  72. package/dist/svg/SvgNode.d.ts +13 -0
  73. package/dist/svg/SvgNode.js +18 -0
  74. package/dist/svg/SvgRenderer.d.ts +22 -0
  75. package/dist/svg/SvgRenderer.js +20 -0
  76. package/dist/svg/camera.d.ts +21 -0
  77. package/dist/svg/camera.js +43 -0
  78. package/dist/svg/index.d.ts +6 -0
  79. package/dist/svg/index.js +6 -0
  80. package/dist/svg/layers.d.ts +18 -0
  81. package/dist/svg/layers.js +13 -0
  82. package/dist/svg/shapes.d.ts +1089 -0
  83. package/dist/svg/shapes.js +119 -0
  84. package/package.json +51 -0
@@ -0,0 +1,360 @@
1
+ import { Layer } from "effect";
2
+ import type * as Cause from "effect/Cause";
3
+ import * as Context from "effect/Context";
4
+ import * as Effect from "effect/Effect";
5
+ import type * as Fiber from "effect/Fiber";
6
+ import type * as Schema from "effect/Schema";
7
+ import { type CameraState } from "./Camera";
8
+ import type * as Entity from "./Entity";
9
+ import * as Instance from "./Instance";
10
+ import { Group } from "./shapes/Group";
11
+ export declare const TypeId: "~motion/SceneRunner";
12
+ /** conventional id of the implicit root group every instance attaches to */
13
+ export declare const ROOT_ID = "root";
14
+ export type Seed = number | string;
15
+ /** the fixed default: scenes are deterministic even with no seed set */
16
+ export declare const defaultSeed: Seed;
17
+ export type Settings = {
18
+ frameRate: number;
19
+ /** output resolution — carried on every frame so renderers can size themselves */
20
+ width: number;
21
+ height: number;
22
+ /** canvas background — carried on every frame so renderers can paint it (default a near-black, not pure #000) */
23
+ backgroundColor: string;
24
+ /**
25
+ * seeds the scene's pseudo-random service (effect's Random via
26
+ * withSeed); the fixed default keeps default-constructed scenes
27
+ * byte-identical across runs. Note: the generator algorithm belongs
28
+ * to effect, so upgrading effect may change seeded sequences.
29
+ */
30
+ seed: Seed;
31
+ /**
32
+ * hard cap on frames a scene may produce (default 36_000 — 10 minutes
33
+ * at 60fps): a scene that exceeds it dies instead of hanging the
34
+ * consumer. Set to `Infinity` to declare an intentionally infinite
35
+ * scene.
36
+ */
37
+ maxFrames: number;
38
+ };
39
+ export type GroupInstance = Instance.Of<typeof Group>;
40
+ /**
41
+ * A child in a polymorphic `children` list: a plain string (→ a `Text`),
42
+ * an already-created `Instance`, or an `Effect` that resolves to one (a
43
+ * not-yet-yielded `instantiate` — yielded internally so JSX children need
44
+ * no `yield*`). Normalized to a stored child id in list order.
45
+ */
46
+ export type Child = string | Instance.Instance | Effect.Effect<Instance.Instance, unknown, unknown>;
47
+ /**
48
+ * Builtin, engine-owned instance properties — namespaced with `$` and
49
+ * held BESIDE entity data, never in the entity schema. Every entity gets
50
+ * them uniformly. `$visible` defaults to `true`.
51
+ */
52
+ export interface BuiltinProps {
53
+ readonly $visible?: boolean;
54
+ }
55
+ /**
56
+ * The input accepted by `instantiate`: an entity's own make-input, plus
57
+ * builtin props ($visible), plus — when the entity has a `children` field
58
+ * — a polymorphic `children` list (strings/instances/effects) in place of
59
+ * the stored `Array<string>` of ids.
60
+ */
61
+ export type InstantiateProps<MakeInput> = Omit<MakeInput, "children"> & BuiltinProps & ("children" extends keyof MakeInput ? {
62
+ readonly children?: ReadonlyArray<Child>;
63
+ } : Record<never, never>);
64
+ /**
65
+ * The ambient mount parent for `instantiate` — provided per scene
66
+ * evaluation (`Scene.play({ parent })`); `null` means the runner root.
67
+ */
68
+ export declare const CurrentParent: Context.Reference<Instance.Instance<"shapes/Group", Schema.Struct<{
69
+ x: Schema.withConstructorDefault<Schema.Number>;
70
+ y: Schema.withConstructorDefault<Schema.Number>;
71
+ opacity: Schema.withConstructorDefault<Schema.Number>;
72
+ transform: Schema.withConstructorDefault<Schema.Struct<{
73
+ readonly a: Schema.Number;
74
+ readonly b: Schema.Number;
75
+ readonly c: Schema.Number;
76
+ readonly d: Schema.Number;
77
+ readonly e: Schema.Number;
78
+ readonly f: Schema.Number;
79
+ }>>;
80
+ children: Schema.withConstructorDefault<Schema.$Array<Schema.String>>;
81
+ }>, {
82
+ "~position": Entity.TraitLens<Schema.Struct.ReadonlySide<{
83
+ x: Schema.withConstructorDefault<Schema.Number>;
84
+ y: Schema.withConstructorDefault<Schema.Number>;
85
+ opacity: Schema.withConstructorDefault<Schema.Number>;
86
+ transform: Schema.withConstructorDefault<Schema.Struct<{
87
+ readonly a: Schema.Number;
88
+ readonly b: Schema.Number;
89
+ readonly c: Schema.Number;
90
+ readonly d: Schema.Number;
91
+ readonly e: Schema.Number;
92
+ readonly f: Schema.Number;
93
+ }>>;
94
+ children: Schema.withConstructorDefault<Schema.$Array<Schema.String>>;
95
+ }, "Type">, Entity.Position>;
96
+ "~opacity": Entity.TraitLens<Schema.Struct.ReadonlySide<{
97
+ x: Schema.withConstructorDefault<Schema.Number>;
98
+ y: Schema.withConstructorDefault<Schema.Number>;
99
+ opacity: Schema.withConstructorDefault<Schema.Number>;
100
+ transform: Schema.withConstructorDefault<Schema.Struct<{
101
+ readonly a: Schema.Number;
102
+ readonly b: Schema.Number;
103
+ readonly c: Schema.Number;
104
+ readonly d: Schema.Number;
105
+ readonly e: Schema.Number;
106
+ readonly f: Schema.Number;
107
+ }>>;
108
+ children: Schema.withConstructorDefault<Schema.$Array<Schema.String>>;
109
+ }, "Type">, number>;
110
+ }> | null>;
111
+ /**
112
+ * A branch of animation as the runner tracks it: its fiber and its
113
+ * SEMANTIC end (`finished` resolves at `Scene.finish` or completion,
114
+ * whichever comes first).
115
+ */
116
+ export interface BranchEntry {
117
+ readonly fiber: Fiber.Fiber<unknown, unknown>;
118
+ readonly finished: Effect.Effect<void>;
119
+ }
120
+ declare const Runner_base: Context.ServiceClass<Runner, "Runner", {
121
+ root: Instance.Instance<"shapes/Group", Schema.Struct<{
122
+ x: Schema.withConstructorDefault<Schema.Number>;
123
+ y: Schema.withConstructorDefault<Schema.Number>;
124
+ opacity: Schema.withConstructorDefault<Schema.Number>;
125
+ transform: Schema.withConstructorDefault<Schema.Struct<{
126
+ readonly a: Schema.Number;
127
+ readonly b: Schema.Number;
128
+ readonly c: Schema.Number;
129
+ readonly d: Schema.Number;
130
+ readonly e: Schema.Number;
131
+ readonly f: Schema.Number;
132
+ }>>;
133
+ children: Schema.withConstructorDefault<Schema.$Array<Schema.String>>;
134
+ }>, {
135
+ "~position": Entity.TraitLens<Schema.Struct.ReadonlySide<{
136
+ x: Schema.withConstructorDefault<Schema.Number>;
137
+ y: Schema.withConstructorDefault<Schema.Number>;
138
+ opacity: Schema.withConstructorDefault<Schema.Number>;
139
+ transform: Schema.withConstructorDefault<Schema.Struct<{
140
+ readonly a: Schema.Number;
141
+ readonly b: Schema.Number;
142
+ readonly c: Schema.Number;
143
+ readonly d: Schema.Number;
144
+ readonly e: Schema.Number;
145
+ readonly f: Schema.Number;
146
+ }>>;
147
+ children: Schema.withConstructorDefault<Schema.$Array<Schema.String>>;
148
+ }, "Type">, Entity.Position>;
149
+ "~opacity": Entity.TraitLens<Schema.Struct.ReadonlySide<{
150
+ x: Schema.withConstructorDefault<Schema.Number>;
151
+ y: Schema.withConstructorDefault<Schema.Number>;
152
+ opacity: Schema.withConstructorDefault<Schema.Number>;
153
+ transform: Schema.withConstructorDefault<Schema.Struct<{
154
+ readonly a: Schema.Number;
155
+ readonly b: Schema.Number;
156
+ readonly c: Schema.Number;
157
+ readonly d: Schema.Number;
158
+ readonly e: Schema.Number;
159
+ readonly f: Schema.Number;
160
+ }>>;
161
+ children: Schema.withConstructorDefault<Schema.$Array<Schema.String>>;
162
+ }, "Type">, number>;
163
+ }>;
164
+ instantiate: <Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>>, MakeInput>(entity: Entity.Entity<Name, Data, Traits, MakeInput>, props: InstantiateProps<MakeInput>) => Effect.Effect<Instance.Instance<Name, Data, Traits>, unknown, Entity.Entity<Name, Data, Traits, MakeInput> | Runner>;
165
+ appendChild: (parent: GroupInstance, child: Instance.Instance) => void;
166
+ removeChild: (parent: GroupInstance, child: Instance.Instance) => void;
167
+ settings: {
168
+ frameRate: number;
169
+ width: number;
170
+ height: number;
171
+ backgroundColor: string;
172
+ seed: Seed;
173
+ maxFrames: number;
174
+ };
175
+ getDataUnsafe: <Name extends string, Data extends Schema.Top>(instance: Instance.Instance<Name, Data>) => Data["Type"] | null;
176
+ setDataUnsafe: <Name extends string, Data extends Schema.Top>(instance: Instance.Instance<Name, Data>, data: unknown) => void;
177
+ state: Effect.Effect<{
178
+ instances: {
179
+ [x: string]: {
180
+ data: unknown;
181
+ entity: Entity.AnyEntity;
182
+ $visible: boolean;
183
+ };
184
+ };
185
+ root: string;
186
+ frameRate: number;
187
+ width: number;
188
+ height: number;
189
+ backgroundColor: string;
190
+ camera: CameraState;
191
+ }, never, never>;
192
+ camera: Instance.Instance<"Camera", Schema.Struct<{
193
+ x: Schema.withConstructorDefault<Schema.Number>;
194
+ y: Schema.withConstructorDefault<Schema.Number>;
195
+ zoom: Schema.withConstructorDefault<Schema.Number>;
196
+ }>, {
197
+ readonly "~position": {
198
+ get: (data: Schema.Struct.ReadonlySide<{
199
+ x: Schema.withConstructorDefault<Schema.Number>;
200
+ y: Schema.withConstructorDefault<Schema.Number>;
201
+ zoom: Schema.withConstructorDefault<Schema.Number>;
202
+ }, "Type">) => {
203
+ x: number;
204
+ y: number;
205
+ };
206
+ set: (data: Schema.Struct.ReadonlySide<{
207
+ x: Schema.withConstructorDefault<Schema.Number>;
208
+ y: Schema.withConstructorDefault<Schema.Number>;
209
+ zoom: Schema.withConstructorDefault<Schema.Number>;
210
+ }, "Type">, value: Entity.Position) => Schema.Struct.ReadonlySide<{
211
+ x: Schema.withConstructorDefault<Schema.Number>;
212
+ y: Schema.withConstructorDefault<Schema.Number>;
213
+ zoom: Schema.withConstructorDefault<Schema.Number>;
214
+ }, "Type">;
215
+ };
216
+ }>;
217
+ setCamera: (instance: Instance.Instance) => void;
218
+ destroy: <Name extends string, Data extends Schema.Top>(instance: Instance.Instance<Name, Data>) => void;
219
+ phaser: {
220
+ register: (n: number) => void;
221
+ deregister: (n: number) => void;
222
+ arriveAndAwaitAdvance: Effect.Effect<void, never, never>;
223
+ awaitAdvance: Effect.Effect<number, never, never>;
224
+ snapshotUnsafe: () => {
225
+ phase: number;
226
+ parties: number;
227
+ arrived: number;
228
+ state: "idle" | "pending" | "running";
229
+ };
230
+ };
231
+ forks: Set<BranchEntry>;
232
+ backgrounds: Set<BranchEntry>;
233
+ countAwaited: (n: number) => void;
234
+ awaitedCount: () => number;
235
+ recordFailure: (cause: Cause.Cause<unknown>) => void;
236
+ failureCause: () => Cause.Cause<unknown> | undefined;
237
+ }> & {
238
+ readonly make: (settings?: Partial<Settings> | undefined) => Effect.Effect<{
239
+ root: Instance.Instance<"shapes/Group", Schema.Struct<{
240
+ x: Schema.withConstructorDefault<Schema.Number>;
241
+ y: Schema.withConstructorDefault<Schema.Number>;
242
+ opacity: Schema.withConstructorDefault<Schema.Number>;
243
+ transform: Schema.withConstructorDefault<Schema.Struct<{
244
+ readonly a: Schema.Number;
245
+ readonly b: Schema.Number;
246
+ readonly c: Schema.Number;
247
+ readonly d: Schema.Number;
248
+ readonly e: Schema.Number;
249
+ readonly f: Schema.Number;
250
+ }>>;
251
+ children: Schema.withConstructorDefault<Schema.$Array<Schema.String>>;
252
+ }>, {
253
+ "~position": Entity.TraitLens<Schema.Struct.ReadonlySide<{
254
+ x: Schema.withConstructorDefault<Schema.Number>;
255
+ y: Schema.withConstructorDefault<Schema.Number>;
256
+ opacity: Schema.withConstructorDefault<Schema.Number>;
257
+ transform: Schema.withConstructorDefault<Schema.Struct<{
258
+ readonly a: Schema.Number;
259
+ readonly b: Schema.Number;
260
+ readonly c: Schema.Number;
261
+ readonly d: Schema.Number;
262
+ readonly e: Schema.Number;
263
+ readonly f: Schema.Number;
264
+ }>>;
265
+ children: Schema.withConstructorDefault<Schema.$Array<Schema.String>>;
266
+ }, "Type">, Entity.Position>;
267
+ "~opacity": Entity.TraitLens<Schema.Struct.ReadonlySide<{
268
+ x: Schema.withConstructorDefault<Schema.Number>;
269
+ y: Schema.withConstructorDefault<Schema.Number>;
270
+ opacity: Schema.withConstructorDefault<Schema.Number>;
271
+ transform: Schema.withConstructorDefault<Schema.Struct<{
272
+ readonly a: Schema.Number;
273
+ readonly b: Schema.Number;
274
+ readonly c: Schema.Number;
275
+ readonly d: Schema.Number;
276
+ readonly e: Schema.Number;
277
+ readonly f: Schema.Number;
278
+ }>>;
279
+ children: Schema.withConstructorDefault<Schema.$Array<Schema.String>>;
280
+ }, "Type">, number>;
281
+ }>;
282
+ instantiate: <Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>>, MakeInput>(entity: Entity.Entity<Name, Data, Traits, MakeInput>, props: InstantiateProps<MakeInput>) => Effect.Effect<Instance.Instance<Name, Data, Traits>, unknown, Entity.Entity<Name, Data, Traits, MakeInput> | Runner>;
283
+ appendChild: (parent: GroupInstance, child: Instance.Instance) => void;
284
+ removeChild: (parent: GroupInstance, child: Instance.Instance) => void;
285
+ settings: {
286
+ frameRate: number;
287
+ width: number;
288
+ height: number;
289
+ backgroundColor: string;
290
+ seed: Seed;
291
+ maxFrames: number;
292
+ };
293
+ getDataUnsafe: <Name extends string, Data extends Schema.Top>(instance: Instance.Instance<Name, Data>) => Data["Type"] | null;
294
+ setDataUnsafe: <Name extends string, Data extends Schema.Top>(instance: Instance.Instance<Name, Data>, data: unknown) => void;
295
+ state: Effect.Effect<{
296
+ instances: {
297
+ [x: string]: {
298
+ data: unknown;
299
+ entity: Entity.AnyEntity;
300
+ $visible: boolean;
301
+ };
302
+ };
303
+ root: string;
304
+ frameRate: number;
305
+ width: number;
306
+ height: number;
307
+ backgroundColor: string;
308
+ camera: CameraState;
309
+ }, never, never>;
310
+ camera: Instance.Instance<"Camera", Schema.Struct<{
311
+ x: Schema.withConstructorDefault<Schema.Number>;
312
+ y: Schema.withConstructorDefault<Schema.Number>;
313
+ zoom: Schema.withConstructorDefault<Schema.Number>;
314
+ }>, {
315
+ readonly "~position": {
316
+ get: (data: Schema.Struct.ReadonlySide<{
317
+ x: Schema.withConstructorDefault<Schema.Number>;
318
+ y: Schema.withConstructorDefault<Schema.Number>;
319
+ zoom: Schema.withConstructorDefault<Schema.Number>;
320
+ }, "Type">) => {
321
+ x: number;
322
+ y: number;
323
+ };
324
+ set: (data: Schema.Struct.ReadonlySide<{
325
+ x: Schema.withConstructorDefault<Schema.Number>;
326
+ y: Schema.withConstructorDefault<Schema.Number>;
327
+ zoom: Schema.withConstructorDefault<Schema.Number>;
328
+ }, "Type">, value: Entity.Position) => Schema.Struct.ReadonlySide<{
329
+ x: Schema.withConstructorDefault<Schema.Number>;
330
+ y: Schema.withConstructorDefault<Schema.Number>;
331
+ zoom: Schema.withConstructorDefault<Schema.Number>;
332
+ }, "Type">;
333
+ };
334
+ }>;
335
+ setCamera: (instance: Instance.Instance) => void;
336
+ destroy: <Name extends string, Data extends Schema.Top>(instance: Instance.Instance<Name, Data>) => void;
337
+ phaser: {
338
+ register: (n: number) => void;
339
+ deregister: (n: number) => void;
340
+ arriveAndAwaitAdvance: Effect.Effect<void, never, never>;
341
+ awaitAdvance: Effect.Effect<number, never, never>;
342
+ snapshotUnsafe: () => {
343
+ phase: number;
344
+ parties: number;
345
+ arrived: number;
346
+ state: "idle" | "pending" | "running";
347
+ };
348
+ };
349
+ forks: Set<BranchEntry>;
350
+ backgrounds: Set<BranchEntry>;
351
+ countAwaited: (n: number) => void;
352
+ awaitedCount: () => number;
353
+ recordFailure: (cause: Cause.Cause<unknown>) => void;
354
+ failureCause: () => Cause.Cause<unknown> | undefined;
355
+ }, never, never>;
356
+ };
357
+ export declare class Runner extends Runner_base {
358
+ }
359
+ export declare const layer: Layer.Layer<Runner, never, never>;
360
+ export {};
package/dist/Runner.js ADDED
@@ -0,0 +1,257 @@
1
+ import { Layer } from "effect";
2
+ import * as Context from "effect/Context";
3
+ import * as Effect from "effect/Effect";
4
+ import { Camera, IDENTITY } from "./Camera";
5
+ import * as Instance from "./Instance";
6
+ import * as Phaser from "./Phaser";
7
+ import { Group } from "./shapes/Group";
8
+ import { Text } from "./shapes/Text";
9
+ export const TypeId = "~motion/SceneRunner";
10
+ /** conventional id of the implicit root group every instance attaches to */
11
+ export const ROOT_ID = "root";
12
+ /** the fixed default: scenes are deterministic even with no seed set */
13
+ export const defaultSeed = "effect-motion";
14
+ /**
15
+ * The ambient mount parent for `instantiate` — provided per scene
16
+ * evaluation (`Scene.play({ parent })`); `null` means the runner root.
17
+ */
18
+ export const CurrentParent = Context.Reference("motion/Runner/CurrentParent", { defaultValue: () => null });
19
+ export class Runner extends Context.Service()("Runner", {
20
+ make: Effect.fnUntraced(function* (settings = {}) {
21
+ const instances = {};
22
+ // each instance's current parent group id (or null = detached / root).
23
+ // Tracked so appendChild detaches from the old parent in O(1) rather
24
+ // than scanning the tree. The root is not tracked (it has no parent).
25
+ const parentOf = {};
26
+ const phaser = yield* Phaser.Phaser.make;
27
+ // concurrent branches spawned by Scene.fork / Scene.play /
28
+ // Scene.background. `forks` hold the scene's end hostage until their
29
+ // SEMANTIC end; a branch that finishes (or completes) is demoted to
30
+ // `backgrounds`, which are interrupted at scene end.
31
+ const forks = new Set();
32
+ const backgrounds = new Set();
33
+ // root party + un-finished forks — the work the scene's end must wait
34
+ // for. Kept as a synchronous counter (updated in the same finalizers
35
+ // that release phaser parties) so the frame consumer can decide
36
+ // "scene over" without depending on the scene fiber being scheduled.
37
+ let awaited = 0;
38
+ // first NON-finished branch failure; failures in a tail (after
39
+ // Scene.finish) are deliberately not reported
40
+ let failure;
41
+ let idCounter = 0;
42
+ const generateId = (name) => {
43
+ return `${name}_${idCounter++}`;
44
+ };
45
+ const setDataUnsafe = (instance, data) => {
46
+ // preserve $visible across data updates; new instances default visible
47
+ // ($visible is set explicitly by instantiate when overridden)
48
+ const prev = instances[instance.id];
49
+ instances[instance.id] = {
50
+ data: instance.entity.make(data),
51
+ entity: instance.entity,
52
+ $visible: prev?.$visible ?? true,
53
+ };
54
+ };
55
+ const setVisibleUnsafe = (id, visible) => {
56
+ const entry = instances[id];
57
+ if (entry !== undefined) {
58
+ instances[id] = { ...entry, $visible: visible };
59
+ }
60
+ };
61
+ const getDataUnsafe = (instance) => {
62
+ return instances[instance.id]?.data ?? null;
63
+ };
64
+ const resolvedSettings = {
65
+ ...settings,
66
+ frameRate: settings.frameRate ?? 60,
67
+ width: settings.width ?? 500,
68
+ height: settings.height ?? 300,
69
+ backgroundColor: settings.backgroundColor ?? "#16161d",
70
+ seed: settings.seed ?? defaultSeed,
71
+ maxFrames: settings.maxFrames ?? 36_000,
72
+ };
73
+ // the root group: never rendered itself, holds the top level
74
+ const root = Instance.make(Group, ROOT_ID);
75
+ setDataUnsafe(root, {});
76
+ // the active camera: an ordinary instance (so the animators drive it),
77
+ // never registered with a sink so it never draws. A default identity
78
+ // camera is present from the start, so `depth`/zoom work with no author
79
+ // ceremony; `setCamera` swaps which instance is active.
80
+ const camera = Instance.make(Camera, "camera");
81
+ setDataUnsafe(camera, {});
82
+ let activeCameraId = camera.id;
83
+ const cameraState = () => {
84
+ const data = instances[activeCameraId]?.data;
85
+ // a destroyed active camera falls back to identity rather than dying:
86
+ // the view is not scene-critical state
87
+ return data ?? IDENTITY;
88
+ };
89
+ // append `id` to a group's children and record it as the child's parent
90
+ const attach = (parent, id) => {
91
+ const data = getDataUnsafe(parent);
92
+ if (data === null) {
93
+ throw new Error(`Runner: parent group "${parent.id}" was destroyed`);
94
+ }
95
+ setDataUnsafe(parent, { ...data, children: [...data.children, id] });
96
+ parentOf[id] = parent.id;
97
+ };
98
+ // remove `id` from its current parent's children (O(1) via parentOf),
99
+ // leaving it detached. No-op if already detached or parent is gone.
100
+ const detach = (id) => {
101
+ const parentId = parentOf[id];
102
+ if (parentId == null) {
103
+ return;
104
+ }
105
+ const parentEntry = instances[parentId];
106
+ const children = parentEntry?.data
107
+ ?.children;
108
+ if (parentEntry !== undefined && Array.isArray(children)) {
109
+ setDataUnsafe({ id: parentId, entity: parentEntry.entity }, {
110
+ ...parentEntry.data,
111
+ children: children.filter((c) => c !== id),
112
+ });
113
+ }
114
+ parentOf[id] = null;
115
+ };
116
+ // move `child` under `parent`: detach from its current parent first
117
+ // (so it is never double-referenced), then attach.
118
+ const appendChild = (parent, child) => {
119
+ if (instances[child.id] === undefined) {
120
+ throw new Error(`Runner: child "${child.id}" was destroyed`);
121
+ }
122
+ detach(child.id);
123
+ attach(parent, child.id);
124
+ };
125
+ const removeChild = (parent, child) => {
126
+ if (parentOf[child.id] === parent.id) {
127
+ detach(child.id);
128
+ }
129
+ };
130
+ // normalize a polymorphic children list into stored child ids, in
131
+ // order: a string → a Text; an Instance → its id; otherwise an
132
+ // Effect<Instance> yielded here (JSX children need no yield*).
133
+ const normalizeChildren = (children) => Effect.gen(function* () {
134
+ const ids = [];
135
+ for (const child of children) {
136
+ if (typeof child === "string") {
137
+ const child$ = yield* self.instantiate(Text, { text: child });
138
+ ids.push(child$.id);
139
+ }
140
+ else if (Instance.isInstance(child)) {
141
+ ids.push(child.id);
142
+ }
143
+ else {
144
+ const resolved = yield* child;
145
+ ids.push(resolved.id);
146
+ }
147
+ }
148
+ return ids;
149
+ });
150
+ const self = {
151
+ root,
152
+ instantiate: Effect.fnUntraced(function* (entity, props) {
153
+ // peel off builtin ($visible) and polymorphic children before the
154
+ // schema constructs the data — neither is an entity-data field
155
+ const { $visible, children, ...rest } = props;
156
+ // children are born (via normalizeChildren) attached to their
157
+ // ambient parent — reparent them into THIS instance below
158
+ const childIds = children === undefined
159
+ ? undefined
160
+ : yield* normalizeChildren(children);
161
+ const dataInput = childIds === undefined ? rest : { ...rest, children: childIds };
162
+ const id = generateId(entity.name);
163
+ const instance = Instance.make(entity, id);
164
+ setDataUnsafe(instance, dataInput);
165
+ if ($visible === false) {
166
+ setVisibleUnsafe(id, false);
167
+ }
168
+ // cameras are view state, not tree nodes: they live in `instances`
169
+ // so the animators drive them, but must NOT be mounted into the
170
+ // render tree (no sink renders a Camera — the renderer would die on
171
+ // the unknown entity). Everything else mounts under the ambient
172
+ // parent (Scene.play), defaulting to root.
173
+ if (entity.name !== Camera.name) {
174
+ const ambient = yield* CurrentParent;
175
+ attach(ambient ?? root, id);
176
+ }
177
+ // adopt listed children: they were attached to the ambient parent
178
+ // at birth; detach them there and record this instance as parent
179
+ // (their ids already live in this instance's `children` data)
180
+ if (childIds !== undefined) {
181
+ for (const childId of childIds) {
182
+ detach(childId);
183
+ parentOf[childId] = id;
184
+ }
185
+ }
186
+ return instance;
187
+ }),
188
+ // move `child` under `parent` (detaching from its current parent
189
+ // first, so it is never double-referenced). Instances are born
190
+ // attached to the ambient parent; this reparents them.
191
+ appendChild,
192
+ // detach `child` from `parent` (no-op unless currently its child),
193
+ // leaving it detached from the tree (still alive, just unmounted)
194
+ removeChild,
195
+ settings: resolvedSettings,
196
+ getDataUnsafe,
197
+ setDataUnsafe,
198
+ state: Effect.sync(() => {
199
+ // the active camera lives in `instances` so the animators drive it,
200
+ // but it is view state, not a renderable instance — omit it from the
201
+ // frame's instance map (its data is surfaced separately as `camera`)
202
+ const { [activeCameraId]: _camera, ...renderable } = instances;
203
+ return {
204
+ instances: renderable,
205
+ root: ROOT_ID,
206
+ frameRate: resolvedSettings.frameRate,
207
+ width: resolvedSettings.width,
208
+ height: resolvedSettings.height,
209
+ backgroundColor: resolvedSettings.backgroundColor,
210
+ camera: cameraState(),
211
+ };
212
+ }),
213
+ // the default identity camera (animate it, or swap via setCamera)
214
+ camera,
215
+ // swap the active camera to another instance; its live data becomes
216
+ // the view on every subsequent frame
217
+ setCamera: (instance) => {
218
+ activeCameraId = instance.id;
219
+ },
220
+ destroy: (instance) => {
221
+ // O(1) detach from the tracked parent, then drop the instance
222
+ detach(instance.id);
223
+ delete instances[instance.id];
224
+ delete parentOf[instance.id];
225
+ // backstop scan: stays correct even after manual reparenting via
226
+ // raw data updates (which bypass parentOf tracking)
227
+ for (const [id, entry] of Object.entries(instances)) {
228
+ const children = entry.data.children;
229
+ if (Array.isArray(children) && children.includes(instance.id)) {
230
+ instances[id] = {
231
+ entity: entry.entity,
232
+ $visible: entry.$visible,
233
+ data: entry.entity.make({
234
+ ...entry.data,
235
+ children: children.filter((child) => child !== instance.id),
236
+ }),
237
+ };
238
+ }
239
+ }
240
+ },
241
+ phaser,
242
+ forks,
243
+ backgrounds,
244
+ countAwaited: (n) => {
245
+ awaited += n;
246
+ },
247
+ awaitedCount: () => awaited,
248
+ recordFailure: (cause) => {
249
+ failure = failure ?? cause;
250
+ },
251
+ failureCause: () => failure,
252
+ };
253
+ return self;
254
+ }),
255
+ }) {
256
+ }
257
+ export const layer = Layer.effect(Runner, Runner.make());