effect-motion 0.4.0 → 0.5.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 (91) hide show
  1. package/README.md +2 -2
  2. package/dist/Camera.d.ts +186 -49
  3. package/dist/Camera.js +343 -76
  4. package/dist/Color.d.ts +101 -1
  5. package/dist/Color.js +101 -1
  6. package/dist/EffectMotionError.d.ts +17 -0
  7. package/dist/EffectMotionError.js +17 -0
  8. package/dist/Entity.d.ts +682 -38
  9. package/dist/Entity.js +271 -27
  10. package/dist/Font.d.ts +108 -0
  11. package/dist/Font.js +95 -0
  12. package/dist/Image.d.ts +71 -0
  13. package/dist/Image.js +50 -0
  14. package/dist/Instance.d.ts +73 -11
  15. package/dist/Instance.js +44 -11
  16. package/dist/Motion.d.ts +328 -53
  17. package/dist/Motion.js +278 -46
  18. package/dist/Physics.d.ts +154 -20
  19. package/dist/Physics.js +92 -11
  20. package/dist/Projection.d.ts +37 -132
  21. package/dist/Projection.js +33 -292
  22. package/dist/Resource.d.ts +26 -0
  23. package/dist/Resource.js +41 -0
  24. package/dist/Runner.d.ts +639 -350
  25. package/dist/Runner.js +197 -150
  26. package/dist/Scene.d.ts +603 -175
  27. package/dist/Scene.js +596 -112
  28. package/dist/Timing.d.ts +170 -13
  29. package/dist/Timing.js +124 -6
  30. package/dist/Tree.d.ts +39 -0
  31. package/dist/Tree.js +127 -0
  32. package/dist/index.d.ts +54 -5
  33. package/dist/index.js +56 -5
  34. package/dist/particles/Particle.d.ts +2 -2
  35. package/dist/particles/ParticleField.d.ts +8 -6
  36. package/dist/particles/ParticleField.js +8 -9
  37. package/dist/particles/constructors.d.ts +5 -6
  38. package/dist/particles/constructors.js +8 -3
  39. package/dist/particles/legacy.d.ts +58 -0
  40. package/dist/particles/legacy.js +46 -0
  41. package/dist/particles/simulate.js +11 -3
  42. package/dist/particles/step.js +14 -10
  43. package/dist/types.d.ts +5 -0
  44. package/dist/types.js +1 -0
  45. package/package.json +2 -4
  46. package/dist/CameraHelpers.d.ts +0 -70
  47. package/dist/CameraHelpers.js +0 -239
  48. package/dist/CanvasExporter.d.ts +0 -12
  49. package/dist/CanvasExporter.js +0 -40
  50. package/dist/Fonts.d.ts +0 -41
  51. package/dist/Fonts.js +0 -27
  52. package/dist/Images.d.ts +0 -33
  53. package/dist/Images.js +0 -24
  54. package/dist/PngExporter.d.ts +0 -6
  55. package/dist/PngExporter.js +0 -85
  56. package/dist/Renderer.d.ts +0 -118
  57. package/dist/Renderer.js +0 -508
  58. package/dist/Shapes.d.ts +0 -11
  59. package/dist/Shapes.js +0 -11
  60. package/dist/demo.d.ts +0 -3
  61. package/dist/demo.js +0 -71
  62. package/dist/render/dof.d.ts +0 -27
  63. package/dist/render/dof.js +0 -37
  64. package/dist/render/paint.d.ts +0 -30
  65. package/dist/render/paint.js +0 -36
  66. package/dist/render/shapes.d.ts +0 -42
  67. package/dist/render/shapes.js +0 -310
  68. package/dist/shapes/Circle.d.ts +0 -32
  69. package/dist/shapes/Circle.js +0 -9
  70. package/dist/shapes/Ellipse.d.ts +0 -35
  71. package/dist/shapes/Ellipse.js +0 -10
  72. package/dist/shapes/Group.d.ts +0 -126
  73. package/dist/shapes/Group.js +0 -89
  74. package/dist/shapes/Hud.d.ts +0 -38
  75. package/dist/shapes/Hud.js +0 -35
  76. package/dist/shapes/Image.d.ts +0 -45
  77. package/dist/shapes/Image.js +0 -28
  78. package/dist/shapes/Line.d.ts +0 -47
  79. package/dist/shapes/Line.js +0 -35
  80. package/dist/shapes/Path.d.ts +0 -107
  81. package/dist/shapes/Path.js +0 -32
  82. package/dist/shapes/Rect.d.ts +0 -51
  83. package/dist/shapes/Rect.js +0 -22
  84. package/dist/shapes/Shape2D.d.ts +0 -49
  85. package/dist/shapes/Shape2D.js +0 -52
  86. package/dist/shapes/Shapes.d.ts +0 -11
  87. package/dist/shapes/Shapes.js +0 -11
  88. package/dist/shapes/Square.d.ts +0 -32
  89. package/dist/shapes/Square.js +0 -11
  90. package/dist/shapes/Text.d.ts +0 -51
  91. package/dist/shapes/Text.js +0 -24
package/dist/Runner.js CHANGED
@@ -1,17 +1,22 @@
1
1
  import { Layer } from "effect";
2
2
  import * as Context from "effect/Context";
3
3
  import * as Effect from "effect/Effect";
4
- import { Camera, identity } from "./Camera.js";
5
4
  import * as Color from "./Color.js";
5
+ import * as Entity from "./Entity.js";
6
6
  import * as Instance from "./Instance.js";
7
7
  import * as Phaser from "./Phaser.js";
8
8
  import * as Projection from "./Projection.js";
9
- import { Group } from "./shapes/Group.js";
10
- import { Text } from "./shapes/Text.js";
9
+ import { ROOT_ID, Tree } from "./Tree.js";
11
10
  export const TypeId = "~motion/SceneRunner";
12
- /** conventional id of the implicit root group every instance attaches to */
13
- export const ROOT_ID = "root";
14
- /** the fixed default: scenes are deterministic even with no seed set */
11
+ export { ROOT_ID };
12
+ /**
13
+ * The seed used when none is given.
14
+ *
15
+ * @remarks
16
+ * Fixed rather than random on purpose: a scene using randomness still
17
+ * produces identical frames on every run unless you deliberately vary the
18
+ * seed.
19
+ */
15
20
  export const defaultSeed = "effect-motion";
16
21
  export const defaultComp = {
17
22
  width: 1920,
@@ -23,13 +28,48 @@ export const defaultComp = {
23
28
  * evaluation (`Scene.play({ parent })`); `null` means the runner root.
24
29
  */
25
30
  export const CurrentParent = Context.Reference("motion/Runner/CurrentParent", { defaultValue: () => null });
31
+ const nodeNotFound = (id) => {
32
+ throw new Error(`Runner: node "${id}" not found`);
33
+ };
34
+ /**
35
+ * The entity → view bridge. `Projection.CameraView` is flat (x/y/z,
36
+ * rotX/rotY/rotZ) while the Camera entity nests position/rotation, so the
37
+ * conversion happens HERE, once, at the frame boundary — the renderer and
38
+ * the projection math keep the contract they already had.
39
+ */
40
+ const toCameraView = (camera) => ({
41
+ x: camera.position.x,
42
+ y: camera.position.y,
43
+ z: camera.position.z,
44
+ rotX: camera.rotation.x,
45
+ rotY: camera.rotation.y,
46
+ rotZ: camera.rotation.z,
47
+ focalLength: camera.focalLength,
48
+ focusDistance: camera.focusDistance,
49
+ aperture: camera.aperture,
50
+ ...(camera.poi === null
51
+ ? {}
52
+ : { poiX: camera.poi.x, poiY: camera.poi.y, poiZ: camera.poi.z }),
53
+ });
54
+ /**
55
+ * The resting camera for a comp of the given width: width-relative focal
56
+ * length (AE's 50mm equivalent), positioned so the z=0 plane projects at
57
+ * scale 1 and is in focus.
58
+ */
59
+ export const identityCameraView = (width) => toCameraView(identityCamera(width));
60
+ export const identityCamera = (width) => {
61
+ const focalLength = Projection.defaultFocalLength(width);
62
+ const z = Projection.defaultCameraZ(focalLength);
63
+ return Entity.Camera.make({
64
+ position: Entity.vec3({ x: 0, y: 0, z }),
65
+ focalLength,
66
+ focusDistance: z,
67
+ aperture: 0,
68
+ });
69
+ };
26
70
  export class Runner extends Context.Service()("Runner", {
27
71
  make: Effect.fnUntraced(function* (settings = {}, comp = defaultComp) {
28
- const instances = {};
29
- // each instance's current parent group id (or null = detached / root).
30
- // Tracked so appendChild detaches from the old parent in O(1) rather
31
- // than scanning the tree. The root is not tracked (it has no parent).
32
- const parentOf = {};
72
+ const tree = new Tree();
33
73
  const phaser = yield* Phaser.Phaser.make;
34
74
  // concurrent branches spawned by Scene.fork / Scene.play /
35
75
  // Scene.background. `forks` hold the scene's end hostage until their
@@ -45,28 +85,28 @@ export class Runner extends Context.Service()("Runner", {
45
85
  // first NON-finished branch failure; failures in a tail (after
46
86
  // Scene.finish) are deliberately not reported
47
87
  let failure;
48
- let idCounter = 0;
49
- const generateId = (name) => {
50
- return `${name}_${idCounter++}`;
51
- };
52
- const setDataUnsafe = (instance, data) => {
53
- // preserve $visible across data updates; new instances default visible
54
- // ($visible is set explicitly by instantiate when overridden)
55
- const prev = instances[instance.id];
56
- instances[instance.id] = {
57
- data: instance.entity.data.make(data),
58
- entity: instance.entity,
59
- $visible: prev?.$visible ?? true,
60
- };
61
- };
62
- const setVisibleUnsafe = (id, visible) => {
63
- const entry = instances[id];
64
- if (entry !== undefined) {
65
- instances[id] = { ...entry, $visible: visible };
66
- }
88
+ /**
89
+ * Mounted scenes, by the id of the group they mount under.
90
+ *
91
+ * A comp is a render-to-texture boundary — its own scene, render
92
+ * target, and identity camera — created ONLY by `Scene.play`. It used
93
+ * to be inferred from a Group carrying width/height, which duplicated
94
+ * what the child Scene already owned and made "is this a comp" a
95
+ * question about field presence. It is now declared here explicitly.
96
+ */
97
+ const comps = new Map();
98
+ const setDataUnsafe = (instance, state) => {
99
+ const entry = tree.getEntry(instance.id) ?? nodeNotFound(instance.id);
100
+ entry.state = state;
67
101
  };
68
102
  const getDataUnsafe = (instance) => {
69
- return instances[instance.id]?.data ?? null;
103
+ const entry = tree.getEntry(instance.id);
104
+ if (entry === null) {
105
+ return null;
106
+ }
107
+ // the instance's tag names the entry's state; the tree stores mixed
108
+ // tags, so this is the one place the two are reconciled
109
+ return entry.state;
70
110
  };
71
111
  const resolvedSettings = {
72
112
  ...settings,
@@ -74,61 +114,38 @@ export class Runner extends Context.Service()("Runner", {
74
114
  seed: settings.seed ?? defaultSeed,
75
115
  maxFrames: settings.maxFrames ?? 36_000,
76
116
  };
77
- // the root group: never rendered itself, holds the top level
78
- const root = Instance.make(Group, ROOT_ID);
79
- setDataUnsafe(root, {});
80
- // the active camera: an ordinary instance (so the animators drive it),
81
- // never registered with a sink so it never draws. A default identity
82
- // camera is present from the start, so `depth`/zoom work with no author
83
- // ceremony; `setCamera` swaps which instance is active.
84
- const camera = Instance.make(Camera, "camera");
85
- setDataUnsafe(camera, identity(comp.width));
117
+ // the root group: never rendered itself, holds the top level. Its
118
+ // entry is created by the tree itself.
119
+ const root = Instance.makeInstance(ROOT_ID, "Group");
120
+ // the active camera: an ordinary tree node (so the animators drive it),
121
+ // never rendered. A default resting camera is present from the start,
122
+ // so depth/zoom work with no author ceremony; `setCamera` swaps which
123
+ // instance is active.
124
+ tree.createNode(identityCamera(comp.width), "camera");
125
+ const camera = Instance.makeInstance("camera", "Camera");
86
126
  let activeCameraId = camera.id;
87
127
  const cameraState = () => {
88
- const data = instances[activeCameraId]?.data;
89
- // a destroyed active camera falls back to identity rather than dying:
90
- // the view is not scene-critical state
91
- return data ?? identity(comp.width);
92
- };
93
- // append `id` to a group's children and record it as the child's parent
94
- const attach = (parent, id) => {
95
- const data = getDataUnsafe(parent);
96
- if (data === null) {
97
- throw new Error(`Runner: parent group "${parent.id}" was destroyed`);
128
+ const entry = tree.getEntry(activeCameraId);
129
+ // a destroyed (or swapped-to-non-camera) active camera falls back to
130
+ // the resting view rather than dying: the view is not scene-critical
131
+ if (entry === null || entry.state._tag !== "Camera") {
132
+ return toCameraView(identityCamera(comp.width));
98
133
  }
99
- setDataUnsafe(parent, { ...data, children: [...data.children, id] });
100
- parentOf[id] = parent.id;
134
+ return toCameraView(entry.state);
101
135
  };
102
- // remove `id` from its current parent's children (O(1) via parentOf),
103
- // leaving it detached. No-op if already detached or parent is gone.
104
- const detach = (id) => {
105
- const parentId = parentOf[id];
106
- if (parentId == null) {
107
- return;
108
- }
109
- const parentEntry = instances[parentId];
110
- const children = parentEntry?.data
111
- ?.children;
112
- if (parentEntry !== undefined && Array.isArray(children)) {
113
- setDataUnsafe({ id: parentId, entity: parentEntry.entity }, {
114
- ...parentEntry.data,
115
- children: children.filter((c) => c !== id),
116
- });
117
- }
118
- parentOf[id] = null;
119
- };
120
- // move `child` under `parent`: detach from its current parent first
121
- // (so it is never double-referenced), then attach.
136
+ // move `child` under `parent`: the tree detaches it from its current
137
+ // parent first, so it is never double-referenced.
122
138
  const appendChild = (parent, child) => {
123
- if (instances[child.id] === undefined) {
139
+ const childEntry = tree.getEntry(child.id);
140
+ if (childEntry === null) {
124
141
  throw new Error(`Runner: child "${child.id}" was destroyed`);
125
142
  }
126
- detach(child.id);
127
- attach(parent, child.id);
143
+ tree.appendChild(parent.id, childEntry);
128
144
  };
129
145
  const removeChild = (parent, child) => {
130
- if (parentOf[child.id] === parent.id) {
131
- detach(child.id);
146
+ const entry = tree.getEntry(child.id);
147
+ if (entry !== null && entry.parentId === parent.id) {
148
+ tree.removeFromParent(entry);
132
149
  }
133
150
  };
134
151
  // normalize a polymorphic children list into stored child ids, in
@@ -138,8 +155,8 @@ export class Runner extends Context.Service()("Runner", {
138
155
  const ids = [];
139
156
  for (const child of children) {
140
157
  if (typeof child === "string") {
141
- const child$ = yield* self.instantiate(Text, { text: child });
142
- ids.push(child$.id);
158
+ const text = yield* self.instantiate("Text", { text: child });
159
+ ids.push(text.id);
143
160
  }
144
161
  else if (Instance.isInstance(child)) {
145
162
  ids.push(child.id);
@@ -153,60 +170,39 @@ export class Runner extends Context.Service()("Runner", {
153
170
  });
154
171
  const self = {
155
172
  root,
156
- instantiate: Effect.fnUntraced(function* (entity, props) {
157
- // peel off builtin ($visible) and polymorphic children before the
158
- // schema constructs the data — neither is an entity-data field
159
- // = props
160
- // children are born (via normalizeChildren) attached to their
161
- // ambient parent — reparent them into THIS instance below
162
- const childIds = "children" in props && props.children
163
- ? yield* normalizeChildren(props.children)
164
- : undefined;
165
- const id = generateId(entity.name);
166
- const instance = Instance.make(entity, id);
173
+ instantiate: Effect.fnUntraced(function* (kind, props) {
174
+ const raw = props;
175
+ const children = Array.isArray(raw.children)
176
+ ? raw.children
177
+ : [];
178
+ const childIds = children.length > 0 ? yield* normalizeChildren(children) : undefined;
167
179
  // cameras get width-relative z/focalLength defaults filled here
168
- // (AE's 50mm equivalent — see Camera.ts): the schema can't default
169
- // them because only the Runner knows the scene width. Filling for
170
- // EVERY Camera instance (not just the built-in one) keeps a
171
- // setCamera swap from jumping zoom.
172
- const cameraDefaults = (() => {
173
- if (entity.name !== Camera.name) {
174
- return undefined;
175
- }
176
- const p = props;
177
- const focalLength = p.focalLength ?? Projection.defaultFocalLength(comp.width);
178
- return {
179
- focalLength,
180
- z: p.z ?? focalLength,
181
- // depth of field: focus at the resting distance by default,
182
- // so the z=0 plane is sharp for an untouched camera
183
- focusDistance: p.focusDistance ?? focalLength,
184
- };
185
- })();
186
- setDataUnsafe(instance, {
187
- ...props,
188
- ...cameraDefaults,
189
- children: childIds,
180
+ // (AE's 50mm equivalent): the schema cannot default them because
181
+ // only the Runner knows the comp width. Filled for EVERY Camera,
182
+ // not just the built-in one, so a setCamera swap never jumps zoom.
183
+ const defaults = kind === "Camera" ? cameraDefaults(raw, comp.width) : undefined;
184
+ // raw children never reach stored data: normalized ids are
185
+ // appended through the tree below, in list order
186
+ const { children: _children, ...rest } = raw;
187
+ const definition = Entity.getEntityDefinitionByTag(kind);
188
+ const state = definition.make({
189
+ ...rest,
190
+ ...defaults,
190
191
  });
191
- if (props.$visible === false) {
192
- setVisibleUnsafe(id, false);
193
- }
194
- // cameras are view state, not tree nodes: they live in `instances`
195
- // so the animators drive them, but must NOT be mounted into the
196
- // render tree (no sink renders a Camera — the renderer would die on
197
- // the unknown entity). Everything else mounts under the ambient
198
- // parent (Scene.play), defaulting to root.
199
- if (entity.name !== Camera.name) {
192
+ const entry = tree.createNode(state);
193
+ const instance = Instance.makeInstance(entry.id, kind);
194
+ // cameras are view state, not scene content: they live in the tree
195
+ // so the animators drive them, but must NOT be mounted under a
196
+ // group. Everything else mounts under the ambient parent.
197
+ if (kind !== "Camera") {
200
198
  const ambient = yield* CurrentParent;
201
- attach(ambient ?? root, id);
199
+ tree.appendChild((ambient ?? root).id, entry);
202
200
  }
203
201
  // adopt listed children: they were attached to the ambient parent
204
- // at birth; detach them there and record this instance as parent
205
- // (their ids already live in this instance's `children` data)
202
+ // at birth; appendChild moves each under this instance in order
206
203
  if (childIds !== undefined) {
207
204
  for (const childId of childIds) {
208
- detach(childId);
209
- parentOf[childId] = id;
205
+ tree.appendChild(entry, childId);
210
206
  }
211
207
  }
212
208
  return instance;
@@ -224,21 +220,48 @@ export class Runner extends Context.Service()("Runner", {
224
220
  getDataUnsafe,
225
221
  setDataUnsafe,
226
222
  state: Effect.sync(() => {
227
- // the active camera lives in `instances` so the animators drive it,
228
- // but it is view state, not a renderable instance — omit it from the
229
- // frame's instance map (its data is surfaced separately as `camera`)
230
- const { [activeCameraId]: _camera, ...renderable } = instances;
223
+ // the active camera lives in the tree so the animators drive it,
224
+ // but it is view state, not a renderable instance — omit it from
225
+ // the frame's instance map (its data is surfaced as `camera`)
226
+ const instances = {};
227
+ for (const [id, entry] of Object.entries(tree.map)) {
228
+ if (id === activeCameraId) {
229
+ continue;
230
+ }
231
+ instances[id] = { data: entry.state };
232
+ }
231
233
  return {
232
- instances: renderable,
234
+ instances,
233
235
  root: ROOT_ID,
234
236
  frameRate: resolvedSettings.frameRate,
235
237
  width: comp.width,
236
238
  height: comp.height,
237
239
  backgroundColor: comp.backgroundColor,
238
240
  camera: cameraState(),
241
+ // mounted scenes, by mount-group id: the renderer reads this
242
+ // to know a subtree is a render-to-texture boundary, instead
243
+ // of inferring it from a group carrying a size
244
+ comps: Object.fromEntries(comps),
239
245
  };
240
246
  }),
241
- // the default identity camera (animate it, or swap via setCamera)
247
+ /**
248
+ * ponytail: create a tree node for an entity that is NOT in the
249
+ * union — the particle system only (design D10). Mounts under the
250
+ * ambient parent like any instance. Delete with the particles
251
+ * rewrite; see `particlesEscapeInstantiate`.
252
+ */
253
+ instantiateEscape: Effect.fnUntraced(function* (state) {
254
+ const entry = tree.createNode(state);
255
+ const ambient = yield* CurrentParent;
256
+ tree.appendChild((ambient ?? root).id, entry);
257
+ return Instance.makeInstance(entry.id, state._tag);
258
+ }),
259
+ // declare the group at `id` to be a mounted scene with these bounds
260
+ // (see `comps`); called by Scene.play, never by authors
261
+ registerComp: (id, config) => {
262
+ comps.set(id, config);
263
+ },
264
+ // the default resting camera (animate it, or swap via setCamera)
242
265
  camera,
243
266
  // swap the active camera to another instance; its live data becomes
244
267
  // the view on every subsequent frame
@@ -246,25 +269,11 @@ export class Runner extends Context.Service()("Runner", {
246
269
  activeCameraId = instance.id;
247
270
  },
248
271
  destroy: (instance) => {
249
- // O(1) detach from the tracked parent, then drop the instance
250
- detach(instance.id);
251
- delete instances[instance.id];
252
- delete parentOf[instance.id];
253
- // backstop scan: stays correct even after manual reparenting via
254
- // raw data updates (which bypass parentOf tracking)
255
- for (const [id, entry] of Object.entries(instances)) {
256
- const children = entry.data.children;
257
- if (Array.isArray(children) && children.includes(instance.id)) {
258
- instances[id] = {
259
- entity: entry.entity,
260
- $visible: entry.$visible,
261
- data: entry.entity.data.make({
262
- ...entry.data,
263
- children: children.filter((child) => child !== instance.id),
264
- }),
265
- };
266
- }
272
+ // double-destroy is a no-op, like the old map-based delete
273
+ if (tree.getEntry(instance.id) === null) {
274
+ return;
267
275
  }
276
+ tree.remove(instance.id);
268
277
  },
269
278
  phaser,
270
279
  forks,
@@ -282,4 +291,42 @@ export class Runner extends Context.Service()("Runner", {
282
291
  }),
283
292
  }) {
284
293
  }
294
+ /**
295
+ * Width-relative camera defaults the schema cannot express, since only the
296
+ * Runner knows the comp width. Applied to any Camera at instantiate.
297
+ */
298
+ const cameraDefaults = (props, width) => {
299
+ const position = props.position;
300
+ const focalLength = typeof props.focalLength === "number" && props.focalLength !== 0
301
+ ? props.focalLength
302
+ : Projection.defaultFocalLength(width);
303
+ const restingZ = Projection.defaultCameraZ(focalLength);
304
+ return {
305
+ focalLength,
306
+ position: Entity.vec3({
307
+ x: position?.x ?? 0,
308
+ y: position?.y ?? 0,
309
+ z: position?.z ?? restingZ,
310
+ }),
311
+ // depth of field: focus at the resting distance by default, so the
312
+ // z=0 plane is sharp for an untouched camera
313
+ focusDistance: typeof props.focusDistance === "number" && props.focusDistance !== 0
314
+ ? props.focusDistance
315
+ : restingZ,
316
+ };
317
+ };
285
318
  export const layer = Layer.effect(Runner, Runner.make());
319
+ /** the tag particle fields are stored under; never a union member */
320
+ export const PARTICLE_FIELD_TAG = "particles/ParticleField";
321
+ /** instantiate a non-union entity. Particles only. */
322
+ export const particlesEscapeInstantiate = (runner, state) =>
323
+ // bypasses the tag→EntityMap lookup, which by construction has no
324
+ // ParticleField. The state is stored verbatim; only particles read it.
325
+ runner.instantiateEscape({
326
+ ...state,
327
+ _tag: PARTICLE_FIELD_TAG,
328
+ });
329
+ /** read a non-union entity's state. Particles only. */
330
+ export const particlesEscapeRead = (runner, instance) => runner.getDataUnsafe(instance);
331
+ /** write a non-union entity's state. Particles only. */
332
+ export const particlesEscapeWrite = (runner, instance, state) => runner.setDataUnsafe(instance, state);