effect-motion 0.3.2 → 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.
- package/README.md +2 -2
- package/dist/Camera.d.ts +186 -49
- package/dist/Camera.js +343 -76
- package/dist/Color.d.ts +101 -1
- package/dist/Color.js +101 -1
- package/dist/EffectMotionError.d.ts +17 -0
- package/dist/EffectMotionError.js +17 -0
- package/dist/Entity.d.ts +682 -38
- package/dist/Entity.js +271 -27
- package/dist/Font.d.ts +108 -0
- package/dist/Font.js +95 -0
- package/dist/Image.d.ts +71 -0
- package/dist/Image.js +50 -0
- package/dist/Instance.d.ts +73 -11
- package/dist/Instance.js +44 -11
- package/dist/Motion.d.ts +328 -53
- package/dist/Motion.js +278 -46
- package/dist/Physics.d.ts +154 -20
- package/dist/Physics.js +92 -11
- package/dist/Projection.d.ts +37 -132
- package/dist/Projection.js +33 -292
- package/dist/Resource.d.ts +26 -0
- package/dist/Resource.js +41 -0
- package/dist/Runner.d.ts +653 -330
- package/dist/Runner.js +208 -158
- package/dist/Scene.d.ts +621 -171
- package/dist/Scene.js +620 -89
- package/dist/Timing.d.ts +170 -13
- package/dist/Timing.js +124 -6
- package/dist/Tree.d.ts +39 -0
- package/dist/Tree.js +127 -0
- package/dist/index.d.ts +54 -5
- package/dist/index.js +56 -5
- package/dist/particles/Particle.d.ts +2 -2
- package/dist/particles/ParticleField.d.ts +8 -6
- package/dist/particles/ParticleField.js +8 -9
- package/dist/particles/constructors.d.ts +5 -6
- package/dist/particles/constructors.js +8 -3
- package/dist/particles/legacy.d.ts +58 -0
- package/dist/particles/legacy.js +46 -0
- package/dist/particles/simulate.js +13 -5
- package/dist/particles/step.js +14 -10
- package/dist/types.d.ts +5 -0
- package/dist/types.js +1 -0
- package/package.json +2 -4
- package/dist/CameraHelpers.d.ts +0 -70
- package/dist/CameraHelpers.js +0 -239
- package/dist/CanvasExporter.d.ts +0 -12
- package/dist/CanvasExporter.js +0 -40
- package/dist/Fonts.d.ts +0 -41
- package/dist/Fonts.js +0 -27
- package/dist/Images.d.ts +0 -33
- package/dist/Images.js +0 -24
- package/dist/PngExporter.d.ts +0 -6
- package/dist/PngExporter.js +0 -85
- package/dist/Renderer.d.ts +0 -118
- package/dist/Renderer.js +0 -381
- package/dist/Shapes.d.ts +0 -11
- package/dist/Shapes.js +0 -11
- package/dist/demo.d.ts +0 -3
- package/dist/demo.js +0 -65
- package/dist/render/dof.d.ts +0 -27
- package/dist/render/dof.js +0 -37
- package/dist/render/paint.d.ts +0 -30
- package/dist/render/paint.js +0 -36
- package/dist/render/shapes.d.ts +0 -42
- package/dist/render/shapes.js +0 -310
- package/dist/shapes/Circle.d.ts +0 -32
- package/dist/shapes/Circle.js +0 -9
- package/dist/shapes/Ellipse.d.ts +0 -35
- package/dist/shapes/Ellipse.js +0 -10
- package/dist/shapes/Group.d.ts +0 -116
- package/dist/shapes/Group.js +0 -82
- package/dist/shapes/Hud.d.ts +0 -38
- package/dist/shapes/Hud.js +0 -35
- package/dist/shapes/Image.d.ts +0 -45
- package/dist/shapes/Image.js +0 -28
- package/dist/shapes/Line.d.ts +0 -47
- package/dist/shapes/Line.js +0 -35
- package/dist/shapes/Path.d.ts +0 -107
- package/dist/shapes/Path.js +0 -32
- package/dist/shapes/Rect.d.ts +0 -51
- package/dist/shapes/Rect.js +0 -22
- package/dist/shapes/Shape2D.d.ts +0 -49
- package/dist/shapes/Shape2D.js +0 -52
- package/dist/shapes/Shapes.d.ts +0 -11
- package/dist/shapes/Shapes.js +0 -11
- package/dist/shapes/Square.d.ts +0 -32
- package/dist/shapes/Square.js +0 -11
- package/dist/shapes/Text.d.ts +0 -51
- package/dist/shapes/Text.js +0 -24
package/dist/Runner.js
CHANGED
|
@@ -1,30 +1,75 @@
|
|
|
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 {
|
|
10
|
-
import { Text } from "./shapes/Text.js";
|
|
9
|
+
import { ROOT_ID, Tree } from "./Tree.js";
|
|
11
10
|
export const TypeId = "~motion/SceneRunner";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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";
|
|
21
|
+
export const defaultComp = {
|
|
22
|
+
width: 1920,
|
|
23
|
+
height: 1080,
|
|
24
|
+
backgroundColor: Color.transparent,
|
|
25
|
+
};
|
|
16
26
|
/**
|
|
17
27
|
* The ambient mount parent for `instantiate` — provided per scene
|
|
18
28
|
* evaluation (`Scene.play({ parent })`); `null` means the runner root.
|
|
19
29
|
*/
|
|
20
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
|
+
};
|
|
21
70
|
export class Runner extends Context.Service()("Runner", {
|
|
22
|
-
make: Effect.fnUntraced(function* (settings = {}) {
|
|
23
|
-
const
|
|
24
|
-
// each instance's current parent group id (or null = detached / root).
|
|
25
|
-
// Tracked so appendChild detaches from the old parent in O(1) rather
|
|
26
|
-
// than scanning the tree. The root is not tracked (it has no parent).
|
|
27
|
-
const parentOf = {};
|
|
71
|
+
make: Effect.fnUntraced(function* (settings = {}, comp = defaultComp) {
|
|
72
|
+
const tree = new Tree();
|
|
28
73
|
const phaser = yield* Phaser.Phaser.make;
|
|
29
74
|
// concurrent branches spawned by Scene.fork / Scene.play /
|
|
30
75
|
// Scene.background. `forks` hold the scene's end hostage until their
|
|
@@ -40,93 +85,67 @@ export class Runner extends Context.Service()("Runner", {
|
|
|
40
85
|
// first NON-finished branch failure; failures in a tail (after
|
|
41
86
|
// Scene.finish) are deliberately not reported
|
|
42
87
|
let failure;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
};
|
|
57
|
-
const setVisibleUnsafe = (id, visible) => {
|
|
58
|
-
const entry = instances[id];
|
|
59
|
-
if (entry !== undefined) {
|
|
60
|
-
instances[id] = { ...entry, $visible: visible };
|
|
61
|
-
}
|
|
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;
|
|
62
101
|
};
|
|
63
102
|
const getDataUnsafe = (instance) => {
|
|
64
|
-
|
|
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;
|
|
65
110
|
};
|
|
66
111
|
const resolvedSettings = {
|
|
67
112
|
...settings,
|
|
68
113
|
frameRate: settings.frameRate ?? 60,
|
|
69
|
-
width: settings.width ?? 500,
|
|
70
|
-
height: settings.height ?? 300,
|
|
71
|
-
backgroundColor: settings.backgroundColor ?? Color.rgba(22, 22, 29),
|
|
72
114
|
seed: settings.seed ?? defaultSeed,
|
|
73
115
|
maxFrames: settings.maxFrames ?? 36_000,
|
|
74
116
|
};
|
|
75
|
-
// the root group: never rendered itself, holds the top level
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
// the active camera: an ordinary
|
|
79
|
-
// never
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
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");
|
|
84
126
|
let activeCameraId = camera.id;
|
|
85
127
|
const cameraState = () => {
|
|
86
|
-
const
|
|
87
|
-
// a destroyed active camera falls back to
|
|
88
|
-
// the view is not scene-critical
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// append `id` to a group's children and record it as the child's parent
|
|
92
|
-
const attach = (parent, id) => {
|
|
93
|
-
const data = getDataUnsafe(parent);
|
|
94
|
-
if (data === null) {
|
|
95
|
-
throw new Error(`Runner: parent group "${parent.id}" was destroyed`);
|
|
96
|
-
}
|
|
97
|
-
setDataUnsafe(parent, { ...data, children: [...data.children, id] });
|
|
98
|
-
parentOf[id] = parent.id;
|
|
99
|
-
};
|
|
100
|
-
// remove `id` from its current parent's children (O(1) via parentOf),
|
|
101
|
-
// leaving it detached. No-op if already detached or parent is gone.
|
|
102
|
-
const detach = (id) => {
|
|
103
|
-
const parentId = parentOf[id];
|
|
104
|
-
if (parentId == null) {
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
const parentEntry = instances[parentId];
|
|
108
|
-
const children = parentEntry?.data
|
|
109
|
-
?.children;
|
|
110
|
-
if (parentEntry !== undefined && Array.isArray(children)) {
|
|
111
|
-
setDataUnsafe({ id: parentId, entity: parentEntry.entity }, {
|
|
112
|
-
...parentEntry.data,
|
|
113
|
-
children: children.filter((c) => c !== id),
|
|
114
|
-
});
|
|
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));
|
|
115
133
|
}
|
|
116
|
-
|
|
134
|
+
return toCameraView(entry.state);
|
|
117
135
|
};
|
|
118
|
-
// move `child` under `parent`:
|
|
119
|
-
//
|
|
136
|
+
// move `child` under `parent`: the tree detaches it from its current
|
|
137
|
+
// parent first, so it is never double-referenced.
|
|
120
138
|
const appendChild = (parent, child) => {
|
|
121
|
-
|
|
139
|
+
const childEntry = tree.getEntry(child.id);
|
|
140
|
+
if (childEntry === null) {
|
|
122
141
|
throw new Error(`Runner: child "${child.id}" was destroyed`);
|
|
123
142
|
}
|
|
124
|
-
|
|
125
|
-
attach(parent, child.id);
|
|
143
|
+
tree.appendChild(parent.id, childEntry);
|
|
126
144
|
};
|
|
127
145
|
const removeChild = (parent, child) => {
|
|
128
|
-
|
|
129
|
-
|
|
146
|
+
const entry = tree.getEntry(child.id);
|
|
147
|
+
if (entry !== null && entry.parentId === parent.id) {
|
|
148
|
+
tree.removeFromParent(entry);
|
|
130
149
|
}
|
|
131
150
|
};
|
|
132
151
|
// normalize a polymorphic children list into stored child ids, in
|
|
@@ -136,8 +155,8 @@ export class Runner extends Context.Service()("Runner", {
|
|
|
136
155
|
const ids = [];
|
|
137
156
|
for (const child of children) {
|
|
138
157
|
if (typeof child === "string") {
|
|
139
|
-
const
|
|
140
|
-
ids.push(
|
|
158
|
+
const text = yield* self.instantiate("Text", { text: child });
|
|
159
|
+
ids.push(text.id);
|
|
141
160
|
}
|
|
142
161
|
else if (Instance.isInstance(child)) {
|
|
143
162
|
ids.push(child.id);
|
|
@@ -151,61 +170,39 @@ export class Runner extends Context.Service()("Runner", {
|
|
|
151
170
|
});
|
|
152
171
|
const self = {
|
|
153
172
|
root,
|
|
154
|
-
instantiate: Effect.fnUntraced(function* (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const childIds = "children" in props && props.children
|
|
161
|
-
? yield* normalizeChildren(props.children)
|
|
162
|
-
: undefined;
|
|
163
|
-
const id = generateId(entity.name);
|
|
164
|
-
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;
|
|
165
179
|
// cameras get width-relative z/focalLength defaults filled here
|
|
166
|
-
// (AE's 50mm equivalent
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
return {
|
|
178
|
-
focalLength,
|
|
179
|
-
z: p.z ?? focalLength,
|
|
180
|
-
// depth of field: focus at the resting distance by default,
|
|
181
|
-
// so the z=0 plane is sharp for an untouched camera
|
|
182
|
-
focusDistance: p.focusDistance ?? focalLength,
|
|
183
|
-
};
|
|
184
|
-
})();
|
|
185
|
-
setDataUnsafe(instance, {
|
|
186
|
-
...props,
|
|
187
|
-
...cameraDefaults,
|
|
188
|
-
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,
|
|
189
191
|
});
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
// the unknown entity). Everything else mounts under the ambient
|
|
197
|
-
// parent (Scene.play), defaulting to root.
|
|
198
|
-
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") {
|
|
199
198
|
const ambient = yield* CurrentParent;
|
|
200
|
-
|
|
199
|
+
tree.appendChild((ambient ?? root).id, entry);
|
|
201
200
|
}
|
|
202
201
|
// adopt listed children: they were attached to the ambient parent
|
|
203
|
-
// at birth;
|
|
204
|
-
// (their ids already live in this instance's `children` data)
|
|
202
|
+
// at birth; appendChild moves each under this instance in order
|
|
205
203
|
if (childIds !== undefined) {
|
|
206
204
|
for (const childId of childIds) {
|
|
207
|
-
|
|
208
|
-
parentOf[childId] = id;
|
|
205
|
+
tree.appendChild(entry, childId);
|
|
209
206
|
}
|
|
210
207
|
}
|
|
211
208
|
return instance;
|
|
@@ -218,24 +215,53 @@ export class Runner extends Context.Service()("Runner", {
|
|
|
218
215
|
// leaving it detached from the tree (still alive, just unmounted)
|
|
219
216
|
removeChild,
|
|
220
217
|
settings: resolvedSettings,
|
|
218
|
+
// the root scene's composition config (resolution + background)
|
|
219
|
+
comp,
|
|
221
220
|
getDataUnsafe,
|
|
222
221
|
setDataUnsafe,
|
|
223
222
|
state: Effect.sync(() => {
|
|
224
|
-
// the active camera lives in
|
|
225
|
-
// but it is view state, not a renderable instance — omit it from
|
|
226
|
-
// frame's instance map (its data is surfaced
|
|
227
|
-
const
|
|
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
|
+
}
|
|
228
233
|
return {
|
|
229
|
-
instances
|
|
234
|
+
instances,
|
|
230
235
|
root: ROOT_ID,
|
|
231
236
|
frameRate: resolvedSettings.frameRate,
|
|
232
|
-
width:
|
|
233
|
-
height:
|
|
234
|
-
backgroundColor:
|
|
237
|
+
width: comp.width,
|
|
238
|
+
height: comp.height,
|
|
239
|
+
backgroundColor: comp.backgroundColor,
|
|
235
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),
|
|
236
245
|
};
|
|
237
246
|
}),
|
|
238
|
-
|
|
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)
|
|
239
265
|
camera,
|
|
240
266
|
// swap the active camera to another instance; its live data becomes
|
|
241
267
|
// the view on every subsequent frame
|
|
@@ -243,25 +269,11 @@ export class Runner extends Context.Service()("Runner", {
|
|
|
243
269
|
activeCameraId = instance.id;
|
|
244
270
|
},
|
|
245
271
|
destroy: (instance) => {
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
delete parentOf[instance.id];
|
|
250
|
-
// backstop scan: stays correct even after manual reparenting via
|
|
251
|
-
// raw data updates (which bypass parentOf tracking)
|
|
252
|
-
for (const [id, entry] of Object.entries(instances)) {
|
|
253
|
-
const children = entry.data.children;
|
|
254
|
-
if (Array.isArray(children) && children.includes(instance.id)) {
|
|
255
|
-
instances[id] = {
|
|
256
|
-
entity: entry.entity,
|
|
257
|
-
$visible: entry.$visible,
|
|
258
|
-
data: entry.entity.data.make({
|
|
259
|
-
...entry.data,
|
|
260
|
-
children: children.filter((child) => child !== instance.id),
|
|
261
|
-
}),
|
|
262
|
-
};
|
|
263
|
-
}
|
|
272
|
+
// double-destroy is a no-op, like the old map-based delete
|
|
273
|
+
if (tree.getEntry(instance.id) === null) {
|
|
274
|
+
return;
|
|
264
275
|
}
|
|
276
|
+
tree.remove(instance.id);
|
|
265
277
|
},
|
|
266
278
|
phaser,
|
|
267
279
|
forks,
|
|
@@ -279,4 +291,42 @@ export class Runner extends Context.Service()("Runner", {
|
|
|
279
291
|
}),
|
|
280
292
|
}) {
|
|
281
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
|
+
};
|
|
282
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);
|