effect-motion 0.3.2 → 0.4.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/dist/CameraHelpers.d.ts +1 -1
- package/dist/CameraHelpers.js +7 -7
- package/dist/Renderer.js +154 -27
- package/dist/Runner.d.ts +100 -66
- package/dist/Runner.js +14 -11
- package/dist/Scene.d.ts +35 -13
- package/dist/Scene.js +64 -17
- package/dist/demo.js +9 -3
- package/dist/particles/simulate.js +2 -2
- package/dist/shapes/Group.d.ts +28 -18
- package/dist/shapes/Group.js +7 -0
- package/package.json +2 -2
package/dist/CameraHelpers.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import * as Entity from "./Entity.js";
|
|
|
5
5
|
import * as Instance from "./Instance.js";
|
|
6
6
|
import * as Runner from "./Runner.js";
|
|
7
7
|
import * as Timing from "./Timing.js";
|
|
8
|
+
export type { CameraState } from "./Camera.js";
|
|
8
9
|
/**
|
|
9
10
|
* The public Camera surface: the entity/identity from Camera.js plus the
|
|
10
11
|
* directing helpers. This module (not Camera.ts) is what index exports as
|
|
@@ -19,7 +20,6 @@ import * as Timing from "./Timing.js";
|
|
|
19
20
|
* `move`/`moveTo`.
|
|
20
21
|
*/
|
|
21
22
|
export { Camera, identity } from "./Camera.js";
|
|
22
|
-
export type { CameraState } from "./Camera.js";
|
|
23
23
|
type AnyInstance = Instance.Instance<any, any, any>;
|
|
24
24
|
/**
|
|
25
25
|
* A helper target: an Instance (position read live each frame), an Effect
|
package/dist/CameraHelpers.js
CHANGED
|
@@ -68,11 +68,11 @@ const targetReader = Effect.fnUntraced(function* (target, offset) {
|
|
|
68
68
|
const setPoi = (data, p) => Object.assign({}, data, { poiX: p.x, poiY: p.y, poiZ: p.z });
|
|
69
69
|
// the camera's WORLD position: x/y are pan-from-viewport-center
|
|
70
70
|
const worldPosition = Effect.fnUntraced(function* (cam) {
|
|
71
|
-
const {
|
|
71
|
+
const { comp } = yield* Runner.Runner;
|
|
72
72
|
const data = (yield* Scene.data(cam));
|
|
73
73
|
return {
|
|
74
|
-
x:
|
|
75
|
-
y:
|
|
74
|
+
x: comp.width / 2 + data.x,
|
|
75
|
+
y: comp.height / 2 + data.y,
|
|
76
76
|
z: data.z ?? 0,
|
|
77
77
|
};
|
|
78
78
|
});
|
|
@@ -162,8 +162,8 @@ const poiOrDie = (data) => {
|
|
|
162
162
|
};
|
|
163
163
|
const orbitImpl = Effect.fnUntraced(function* (camOrEffect, from, to, duration, timing) {
|
|
164
164
|
const cam = yield* Instance.flatten(camOrEffect);
|
|
165
|
-
const {
|
|
166
|
-
const origin = { x:
|
|
165
|
+
const { comp } = yield* Runner.Runner;
|
|
166
|
+
const origin = { x: comp.width / 2, y: comp.height / 2 };
|
|
167
167
|
const startData = (yield* Scene.data(cam));
|
|
168
168
|
const poi = poiOrDie(startData);
|
|
169
169
|
const world = {
|
|
@@ -197,8 +197,8 @@ export const orbitTo = dual((args) => Instance.isInstance(args[0]), ((cam, azimu
|
|
|
197
197
|
export const orbit = dual((args) => Instance.isInstance(args[0]), orbitImpl);
|
|
198
198
|
const dollyImpl = Effect.fnUntraced(function* (camOrEffect, from, to, duration, timing) {
|
|
199
199
|
const cam = yield* Instance.flatten(camOrEffect);
|
|
200
|
-
const {
|
|
201
|
-
const origin = { x:
|
|
200
|
+
const { comp } = yield* Runner.Runner;
|
|
201
|
+
const origin = { x: comp.width / 2, y: comp.height / 2 };
|
|
202
202
|
const startData = (yield* Scene.data(cam));
|
|
203
203
|
const poi = poiOrDie(startData);
|
|
204
204
|
const world = {
|
package/dist/Renderer.js
CHANGED
|
@@ -5,6 +5,7 @@ import * as Color from "./Color.js";
|
|
|
5
5
|
import * as Projection from "./Projection.js";
|
|
6
6
|
import { circleOfConfusion, quantizeSigma } from "./render/dof.js";
|
|
7
7
|
import { builtinPaints } from "./render/shapes.js";
|
|
8
|
+
import * as Group from "./shapes/Group.js";
|
|
8
9
|
import { Hud } from "./shapes/Hud.js";
|
|
9
10
|
// in frame data, a `children: string[]` field means child instance ids
|
|
10
11
|
const childIdsOf = (data) => {
|
|
@@ -92,7 +93,10 @@ dpr = 1) => Effect.gen(function* () {
|
|
|
92
93
|
// top tier. `inWorldContainer` marks any ordinary container above —
|
|
93
94
|
// a Hud there would compose world offsets into screen coordinates,
|
|
94
95
|
// which is incoherent and dies loudly.
|
|
95
|
-
const flatten = (id, offset, hud, inWorldContainer
|
|
96
|
+
const flatten = (id, offset, hud, inWorldContainer,
|
|
97
|
+
// where this node's paintables land: the top-level list, or an
|
|
98
|
+
// enclosing sub-composition's own sub-list
|
|
99
|
+
sink) => Effect.gen(function* () {
|
|
96
100
|
if (visited.has(id)) {
|
|
97
101
|
return yield* Effect.die(new Error(`Renderer: instance "${id}" is referenced more than once (duplicate parent or cycle)`));
|
|
98
102
|
}
|
|
@@ -115,12 +119,50 @@ dpr = 1) => Effect.gen(function* () {
|
|
|
115
119
|
z: offset.z + (data.z ?? 0),
|
|
116
120
|
};
|
|
117
121
|
const childIds = childIdsOf(entry.data).filter((childId) => isVisible(frame, childId));
|
|
122
|
+
// a SIZED container is a sub-composition (Scene.play's mount
|
|
123
|
+
// group, or any Group given bounds): its subtree collects into
|
|
124
|
+
// its own unit at the group's depth — clipped, backed, and
|
|
125
|
+
// transformed as one layer, AE-precomp-style
|
|
126
|
+
const isComp = Array.isArray(entry.data.children) &&
|
|
127
|
+
typeof data.width === "number" &&
|
|
128
|
+
typeof data.height === "number";
|
|
129
|
+
if (isComp) {
|
|
130
|
+
const proj = Projection.project(effectiveCamera, world, origin);
|
|
131
|
+
const subs = [];
|
|
132
|
+
yield* Effect.all(childIds.map((childId) => flatten(childId, world, subtreeHud, inWorldContainer || !subtreeHud, subs)));
|
|
133
|
+
const groupData = entry.data;
|
|
134
|
+
sink.push({
|
|
135
|
+
id,
|
|
136
|
+
entry,
|
|
137
|
+
projection: {
|
|
138
|
+
screen: Projection.billboardAffine(proj, {
|
|
139
|
+
x: data.x ?? 0,
|
|
140
|
+
y: data.y ?? 0,
|
|
141
|
+
}),
|
|
142
|
+
depth: proj.depth,
|
|
143
|
+
scale: proj.scale,
|
|
144
|
+
},
|
|
145
|
+
hud: subtreeHud,
|
|
146
|
+
comp: {
|
|
147
|
+
width: data.width,
|
|
148
|
+
height: data.height,
|
|
149
|
+
...(groupData.backgroundColor !== undefined
|
|
150
|
+
? { backgroundColor: groupData.backgroundColor }
|
|
151
|
+
: {}),
|
|
152
|
+
opacity: groupData.opacity ?? 1,
|
|
153
|
+
transform: groupData.transform ?? Group.identityTransform,
|
|
154
|
+
anchor: { x: proj.x, y: proj.y },
|
|
155
|
+
subs,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
118
160
|
if (childIds.length > 0) {
|
|
119
161
|
// a pure container: contribute position, recurse, paint
|
|
120
162
|
// nothing itself (the root, Groups, Huds). ponytail: only
|
|
121
163
|
// translation composes down — a Group's 2D affine transform
|
|
122
164
|
// is not yet threaded into child world coords.
|
|
123
|
-
yield* Effect.all(childIds.map((childId) => flatten(childId, world, subtreeHud, inWorldContainer || !subtreeHud)));
|
|
165
|
+
yield* Effect.all(childIds.map((childId) => flatten(childId, world, subtreeHud, inWorldContainer || !subtreeHud, sink)));
|
|
124
166
|
return;
|
|
125
167
|
}
|
|
126
168
|
// a skeletal path leaf (Path): every command point is an
|
|
@@ -140,7 +182,7 @@ dpr = 1) => Effect.gen(function* () {
|
|
|
140
182
|
// the segment branch does — path paints ignore it (their
|
|
141
183
|
// geometry is already screen-space) but the field stays coherent
|
|
142
184
|
const first = projected.subpaths[0]?.points[0] ?? origin;
|
|
143
|
-
|
|
185
|
+
sink.push({
|
|
144
186
|
id,
|
|
145
187
|
entry,
|
|
146
188
|
projection: {
|
|
@@ -194,7 +236,7 @@ dpr = 1) => Effect.gen(function* () {
|
|
|
194
236
|
// Upgrade: split the segment where its (linear) view depth
|
|
195
237
|
// crosses DoF bucket boundaries → gradient blur along the
|
|
196
238
|
// line plus per-piece sort keys.
|
|
197
|
-
|
|
239
|
+
sink.push({
|
|
198
240
|
id,
|
|
199
241
|
entry,
|
|
200
242
|
projection: {
|
|
@@ -235,7 +277,7 @@ dpr = 1) => Effect.gen(function* () {
|
|
|
235
277
|
// the tilted plane is entirely behind the near plane — cull
|
|
236
278
|
return;
|
|
237
279
|
}
|
|
238
|
-
|
|
280
|
+
sink.push({
|
|
239
281
|
id,
|
|
240
282
|
entry,
|
|
241
283
|
projection: {
|
|
@@ -257,15 +299,16 @@ dpr = 1) => Effect.gen(function* () {
|
|
|
257
299
|
visited.add(frame.root);
|
|
258
300
|
yield* Effect.all(childIdsOf(rootEntry.data)
|
|
259
301
|
.filter((childId) => isVisible(frame, childId))
|
|
260
|
-
.map((childId) => flatten(childId, { x: 0, y: 0, z: 0 }, false, false)));
|
|
302
|
+
.map((childId) => flatten(childId, { x: 0, y: 0, z: 0 }, false, false, paintables)));
|
|
261
303
|
// painter's order: two tiers — world content by depth (farthest
|
|
262
304
|
// first), then HUD content by depth among itself, each with a stable
|
|
263
305
|
// id tie-break so equal-depth paintables paint deterministically.
|
|
264
306
|
// ponytail: naive O(n log n) per frame — swap for a spatial structure
|
|
265
307
|
// only if a scene with thousands of objects proves it.
|
|
266
|
-
|
|
308
|
+
const byPaintOrder = (a, b) => (a.hud ? 1 : 0) - (b.hud ? 1 : 0) ||
|
|
267
309
|
b.projection.depth - a.projection.depth ||
|
|
268
|
-
(a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
|
|
310
|
+
(a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
311
|
+
paintables.sort(byPaintOrder);
|
|
269
312
|
const meta = {
|
|
270
313
|
frameRate: frame.frameRate,
|
|
271
314
|
width: frame.width,
|
|
@@ -292,12 +335,110 @@ dpr = 1) => Effect.gen(function* () {
|
|
|
292
335
|
yield* Tvg.Scene.add(scene, bucketScene);
|
|
293
336
|
}
|
|
294
337
|
});
|
|
338
|
+
// paint one paintable into `target`: a leaf via its entity's paint fn,
|
|
339
|
+
// a sub-composition as one clipped nested scene (below)
|
|
340
|
+
const paintOne = (p, target) => Effect.gen(function* () {
|
|
341
|
+
if (p.comp !== undefined) {
|
|
342
|
+
return yield* paintComp(p, p.comp, target);
|
|
343
|
+
}
|
|
344
|
+
// the concrete paint fn for this entity name; the map is exhaustive
|
|
345
|
+
// over Entities by construction (PaintFunctions<Entities>). The
|
|
346
|
+
// specific member depends on the instance's entity, known only at
|
|
347
|
+
// runtime — hence the cast to the erased paint-fn type.
|
|
348
|
+
const paint = builtinPaints[p.entry.entity.name];
|
|
349
|
+
if (paint === undefined) {
|
|
350
|
+
return yield* Effect.die(new Error(`Renderer: no paint function for entity "${p.entry.entity.name}"`));
|
|
351
|
+
}
|
|
352
|
+
yield* paint({
|
|
353
|
+
entity: p.entry.entity,
|
|
354
|
+
id: p.id,
|
|
355
|
+
data: p.entry.data,
|
|
356
|
+
projection: p.projection,
|
|
357
|
+
canvas,
|
|
358
|
+
scene: target,
|
|
359
|
+
meta,
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
// A sub-composition paints as ONE layer: inner scene clipped to the
|
|
363
|
+
// bounds (background rect first, then the subtree in painter's order),
|
|
364
|
+
// outer scene carrying the group's transform and opacity — the clip
|
|
365
|
+
// lives inside, so the bounds move/scale WITH the unit, as an AE
|
|
366
|
+
// precomp layer's frame does.
|
|
367
|
+
const paintComp = (p, comp, target) => Effect.gen(function* () {
|
|
368
|
+
const scale = p.projection.scale;
|
|
369
|
+
// bounds start at the group's anchor: a comp's local space is
|
|
370
|
+
// top-left-anchored like every shape (world = screen at rest)
|
|
371
|
+
const w = comp.width * scale;
|
|
372
|
+
const h = comp.height * scale;
|
|
373
|
+
const x = comp.anchor.x;
|
|
374
|
+
const y = comp.anchor.y;
|
|
375
|
+
const outer = yield* Tvg.Scene.make();
|
|
376
|
+
const clipShape = yield* Tvg.Shape.make();
|
|
377
|
+
yield* Tvg.Shape.appendRect(clipShape, x, y, w, h);
|
|
378
|
+
yield* Tvg.Paint.clip(outer, clipShape);
|
|
379
|
+
if (comp.backgroundColor !== undefined) {
|
|
380
|
+
const { r, g, b, a } = Color.bytes(comp.backgroundColor);
|
|
381
|
+
if (a > 0) {
|
|
382
|
+
const bg = yield* Tvg.Shape.make();
|
|
383
|
+
yield* Tvg.Shape.appendRect(bg, x, y, w, h);
|
|
384
|
+
yield* Tvg.Shape.setFillColor(bg, r, g, b, a);
|
|
385
|
+
yield* Tvg.Scene.add(outer, bg);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// ponytail: the unit's content paints sharp — depth of field
|
|
389
|
+
// treats the comp as one layer at its anchor depth (like a
|
|
390
|
+
// tilted plane's quad); bucket inside the unit if a deep comp
|
|
391
|
+
// ever needs per-child blur.
|
|
392
|
+
for (const sub of [...comp.subs].sort(byPaintOrder)) {
|
|
393
|
+
if (sub.projection.scale <= 0 && sub.projection.quad === undefined) {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
yield* paintOne(sub, outer);
|
|
397
|
+
}
|
|
398
|
+
if (!Group.isIdentityTransform(comp.transform)) {
|
|
399
|
+
// the group's local matrix conjugated into screen space about
|
|
400
|
+
// the BOUNDS CENTER (AE scales a precomp layer about its
|
|
401
|
+
// anchor, default the layer center):
|
|
402
|
+
// screen = T(c)·S(scale)·M·S(1/scale)·T(-c), c = bounds center
|
|
403
|
+
const cx = x + w / 2;
|
|
404
|
+
const cy = y + h / 2;
|
|
405
|
+
const t = (e, f) => ({
|
|
406
|
+
a: 1,
|
|
407
|
+
b: 0,
|
|
408
|
+
c: 0,
|
|
409
|
+
d: 1,
|
|
410
|
+
e,
|
|
411
|
+
f,
|
|
412
|
+
});
|
|
413
|
+
const s = (k) => ({
|
|
414
|
+
a: k,
|
|
415
|
+
b: 0,
|
|
416
|
+
c: 0,
|
|
417
|
+
d: k,
|
|
418
|
+
e: 0,
|
|
419
|
+
f: 0,
|
|
420
|
+
});
|
|
421
|
+
const m = [
|
|
422
|
+
t(cx, cy),
|
|
423
|
+
s(scale),
|
|
424
|
+
comp.transform,
|
|
425
|
+
s(1 / scale),
|
|
426
|
+
t(-cx, -cy),
|
|
427
|
+
].reduce(Group.multiplyTransforms);
|
|
428
|
+
yield* Tvg.Paint.setTransform(outer, m);
|
|
429
|
+
}
|
|
430
|
+
if (comp.opacity < 1) {
|
|
431
|
+
yield* Tvg.Paint.setOpacity(outer, Math.round(Math.max(0, Math.min(1, comp.opacity)) * 255));
|
|
432
|
+
}
|
|
433
|
+
yield* Tvg.Scene.add(target, outer);
|
|
434
|
+
});
|
|
295
435
|
// paint far→near. A paintable whose anchor is behind the camera
|
|
296
436
|
// (scale <= 0) is culled here so paint functions never see an invalid
|
|
297
437
|
// placement — EXCEPT a tilted plane carrying a quad: its polygon is
|
|
298
438
|
// already near-plane-clipped, and it can be visible (near part in
|
|
299
439
|
// front) while its anchor corner is behind.
|
|
300
|
-
for (const
|
|
440
|
+
for (const p of paintables) {
|
|
441
|
+
const { projection, hud } = p;
|
|
301
442
|
if (projection.scale <= 0 && projection.quad === undefined) {
|
|
302
443
|
continue;
|
|
303
444
|
}
|
|
@@ -311,23 +452,7 @@ dpr = 1) => Effect.gen(function* () {
|
|
|
311
452
|
bucketScene = sigma === 0 ? scene : yield* Tvg.Scene.make();
|
|
312
453
|
bucketSigma = sigma;
|
|
313
454
|
}
|
|
314
|
-
|
|
315
|
-
// over Entities by construction (PaintFunctions<Entities>). The
|
|
316
|
-
// specific member depends on the instance's entity, known only at
|
|
317
|
-
// runtime — hence the cast to the erased paint-fn type.
|
|
318
|
-
const paint = builtinPaints[entry.entity.name];
|
|
319
|
-
if (paint === undefined) {
|
|
320
|
-
return yield* Effect.die(new Error(`Renderer: no paint function for entity "${entry.entity.name}"`));
|
|
321
|
-
}
|
|
322
|
-
yield* paint({
|
|
323
|
-
entity: entry.entity,
|
|
324
|
-
id,
|
|
325
|
-
data: entry.data,
|
|
326
|
-
projection,
|
|
327
|
-
canvas,
|
|
328
|
-
scene: bucketScene,
|
|
329
|
-
meta,
|
|
330
|
-
});
|
|
455
|
+
yield* paintOne(p, bucketScene);
|
|
331
456
|
}
|
|
332
457
|
yield* closeBucket;
|
|
333
458
|
});
|
|
@@ -368,7 +493,9 @@ export const render = (frame, options) => Effect.gen(function* () {
|
|
|
368
493
|
}
|
|
369
494
|
yield* Tvg.Canvas.add(canvas, scene);
|
|
370
495
|
yield* Tvg.Canvas.update(canvas);
|
|
371
|
-
|
|
496
|
+
// clear the target buffer: with a transparent background nothing else
|
|
497
|
+
// overwrites the previous frame's (or uninitialized) pixels
|
|
498
|
+
yield* Tvg.Canvas.draw(canvas, true);
|
|
372
499
|
yield* Tvg.Canvas.sync(canvas);
|
|
373
500
|
const buffer = yield* Tvg.Canvas.render(canvas);
|
|
374
501
|
return {
|
package/dist/Runner.d.ts
CHANGED
|
@@ -15,13 +15,12 @@ export declare const ROOT_ID = "root";
|
|
|
15
15
|
export type Seed = number | string;
|
|
16
16
|
/** the fixed default: scenes are deterministic even with no seed set */
|
|
17
17
|
export declare const defaultSeed: Seed;
|
|
18
|
+
/**
|
|
19
|
+
* Playback settings — how the movie RUNS. What the movie IS (resolution,
|
|
20
|
+
* background) lives on the root scene as its composition config.
|
|
21
|
+
*/
|
|
18
22
|
export type Settings = {
|
|
19
23
|
frameRate: number;
|
|
20
|
-
/** output resolution — carried on every frame so renderers can size themselves */
|
|
21
|
-
width: number;
|
|
22
|
-
height: number;
|
|
23
|
-
/** canvas background — carried on every frame so renderers can paint it (default a near-black, not pure #000) */
|
|
24
|
-
backgroundColor: Color.Color;
|
|
25
24
|
/**
|
|
26
25
|
* seeds the scene's pseudo-random service (effect's Random via
|
|
27
26
|
* withSeed); the fixed default keeps default-constructed scenes
|
|
@@ -37,6 +36,18 @@ export type Settings = {
|
|
|
37
36
|
*/
|
|
38
37
|
maxFrames: number;
|
|
39
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* A scene's composition config, After Effects–style: what the comp IS.
|
|
41
|
+
* The runner inherits the ROOT scene's config; a nested scene keeps its
|
|
42
|
+
* own as its bounds (see Scene.play).
|
|
43
|
+
*/
|
|
44
|
+
export type CompConfig = {
|
|
45
|
+
width: number;
|
|
46
|
+
height: number;
|
|
47
|
+
/** carried on every frame so renderers can paint it; transparent = nothing painted */
|
|
48
|
+
backgroundColor: Color.Color;
|
|
49
|
+
};
|
|
50
|
+
export declare const defaultComp: CompConfig;
|
|
40
51
|
export type GroupInstance = Instance.Of<typeof Group>;
|
|
41
52
|
/**
|
|
42
53
|
* A child in a polymorphic `children` list: a plain string (→ a `Text`),
|
|
@@ -79,38 +90,47 @@ export declare const CurrentParent: Context.Reference<Instance.Instance<"shapes/
|
|
|
79
90
|
readonly e: Schema.Number;
|
|
80
91
|
readonly f: Schema.Number;
|
|
81
92
|
}>>;
|
|
93
|
+
width: Schema.optionalKey<Schema.Number>;
|
|
94
|
+
height: Schema.optionalKey<Schema.Number>;
|
|
95
|
+
backgroundColor: Schema.optionalKey<typeof Color.Color>;
|
|
82
96
|
children: Schema.withConstructorDefault<Schema.$Array<Schema.String>>;
|
|
83
97
|
}>, {
|
|
84
|
-
"~position": Entity.TraitLens<
|
|
85
|
-
x:
|
|
86
|
-
y:
|
|
87
|
-
z:
|
|
88
|
-
opacity:
|
|
89
|
-
transform: Schema.
|
|
98
|
+
"~position": Entity.TraitLens<{
|
|
99
|
+
readonly x: number;
|
|
100
|
+
readonly y: number;
|
|
101
|
+
readonly z: number;
|
|
102
|
+
readonly opacity: number;
|
|
103
|
+
readonly transform: Schema.Struct.ReadonlySide<{
|
|
90
104
|
readonly a: Schema.Number;
|
|
91
105
|
readonly b: Schema.Number;
|
|
92
106
|
readonly c: Schema.Number;
|
|
93
107
|
readonly d: Schema.Number;
|
|
94
108
|
readonly e: Schema.Number;
|
|
95
109
|
readonly f: Schema.Number;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
}, "Type">;
|
|
111
|
+
readonly width?: number;
|
|
112
|
+
readonly height?: number;
|
|
113
|
+
readonly backgroundColor?: Color.Color;
|
|
114
|
+
readonly children: readonly string[];
|
|
115
|
+
}, Entity.Position>;
|
|
116
|
+
"~opacity": Entity.TraitLens<{
|
|
117
|
+
readonly x: number;
|
|
118
|
+
readonly y: number;
|
|
119
|
+
readonly z: number;
|
|
120
|
+
readonly opacity: number;
|
|
121
|
+
readonly transform: Schema.Struct.ReadonlySide<{
|
|
105
122
|
readonly a: Schema.Number;
|
|
106
123
|
readonly b: Schema.Number;
|
|
107
124
|
readonly c: Schema.Number;
|
|
108
125
|
readonly d: Schema.Number;
|
|
109
126
|
readonly e: Schema.Number;
|
|
110
127
|
readonly f: Schema.Number;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
128
|
+
}, "Type">;
|
|
129
|
+
readonly width?: number;
|
|
130
|
+
readonly height?: number;
|
|
131
|
+
readonly backgroundColor?: Color.Color;
|
|
132
|
+
readonly children: readonly string[];
|
|
133
|
+
}, number>;
|
|
114
134
|
}> | null>;
|
|
115
135
|
/**
|
|
116
136
|
* A branch of animation as the runner tracks it: its fiber and its
|
|
@@ -135,50 +155,57 @@ declare const Runner_base: Context.ServiceClass<Runner, "Runner", {
|
|
|
135
155
|
readonly e: Schema.Number;
|
|
136
156
|
readonly f: Schema.Number;
|
|
137
157
|
}>>;
|
|
158
|
+
width: Schema.optionalKey<Schema.Number>;
|
|
159
|
+
height: Schema.optionalKey<Schema.Number>;
|
|
160
|
+
backgroundColor: Schema.optionalKey<typeof Color.Color>;
|
|
138
161
|
children: Schema.withConstructorDefault<Schema.$Array<Schema.String>>;
|
|
139
162
|
}>, {
|
|
140
|
-
"~position": Entity.TraitLens<
|
|
141
|
-
x:
|
|
142
|
-
y:
|
|
143
|
-
z:
|
|
144
|
-
opacity:
|
|
145
|
-
transform: Schema.
|
|
163
|
+
"~position": Entity.TraitLens<{
|
|
164
|
+
readonly x: number;
|
|
165
|
+
readonly y: number;
|
|
166
|
+
readonly z: number;
|
|
167
|
+
readonly opacity: number;
|
|
168
|
+
readonly transform: Schema.Struct.ReadonlySide<{
|
|
146
169
|
readonly a: Schema.Number;
|
|
147
170
|
readonly b: Schema.Number;
|
|
148
171
|
readonly c: Schema.Number;
|
|
149
172
|
readonly d: Schema.Number;
|
|
150
173
|
readonly e: Schema.Number;
|
|
151
174
|
readonly f: Schema.Number;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
175
|
+
}, "Type">;
|
|
176
|
+
readonly width?: number;
|
|
177
|
+
readonly height?: number;
|
|
178
|
+
readonly backgroundColor?: Color.Color;
|
|
179
|
+
readonly children: readonly string[];
|
|
180
|
+
}, Entity.Position>;
|
|
181
|
+
"~opacity": Entity.TraitLens<{
|
|
182
|
+
readonly x: number;
|
|
183
|
+
readonly y: number;
|
|
184
|
+
readonly z: number;
|
|
185
|
+
readonly opacity: number;
|
|
186
|
+
readonly transform: Schema.Struct.ReadonlySide<{
|
|
161
187
|
readonly a: Schema.Number;
|
|
162
188
|
readonly b: Schema.Number;
|
|
163
189
|
readonly c: Schema.Number;
|
|
164
190
|
readonly d: Schema.Number;
|
|
165
191
|
readonly e: Schema.Number;
|
|
166
192
|
readonly f: Schema.Number;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
193
|
+
}, "Type">;
|
|
194
|
+
readonly width?: number;
|
|
195
|
+
readonly height?: number;
|
|
196
|
+
readonly backgroundColor?: Color.Color;
|
|
197
|
+
readonly children: readonly string[];
|
|
198
|
+
}, number>;
|
|
170
199
|
}>;
|
|
171
200
|
instantiate: <Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>>>(entity: Entity.Entity<Name, Data, Traits>, props: InstantiateProps<Data["~type.make.in"]>) => Effect.Effect<Instance.Instance<Name, Data, Traits>, never, Runner>;
|
|
172
201
|
appendChild: (parent: GroupInstance, child: Instance.Instance) => void;
|
|
173
202
|
removeChild: (parent: GroupInstance, child: Instance.Instance) => void;
|
|
174
203
|
settings: {
|
|
175
204
|
frameRate: number;
|
|
176
|
-
width: number;
|
|
177
|
-
height: number;
|
|
178
|
-
backgroundColor: Color.Color;
|
|
179
205
|
seed: Seed;
|
|
180
206
|
maxFrames: number;
|
|
181
207
|
};
|
|
208
|
+
comp: CompConfig;
|
|
182
209
|
getDataUnsafe: <Name extends string, Data extends Schema.Top>(instance: Instance.Instance<Name, Data>) => Data["Type"] | null;
|
|
183
210
|
setDataUnsafe: <Name extends string, Data extends Schema.Top>(instance: Instance.Instance<Name, Data>, data: unknown) => void;
|
|
184
211
|
state: Effect.Effect<{
|
|
@@ -279,7 +306,7 @@ declare const Runner_base: Context.ServiceClass<Runner, "Runner", {
|
|
|
279
306
|
recordFailure: (cause: Cause.Cause<unknown>) => void;
|
|
280
307
|
failureCause: () => Cause.Cause<unknown> | undefined;
|
|
281
308
|
}> & {
|
|
282
|
-
readonly make: (settings?: Partial<Settings> | undefined) => Effect.Effect<{
|
|
309
|
+
readonly make: (settings?: Partial<Settings> | undefined, comp?: CompConfig | undefined) => Effect.Effect<{
|
|
283
310
|
root: Instance.Instance<"shapes/Group", Schema.Struct<{
|
|
284
311
|
x: Schema.withConstructorDefault<Schema.Number>;
|
|
285
312
|
y: Schema.withConstructorDefault<Schema.Number>;
|
|
@@ -293,50 +320,57 @@ declare const Runner_base: Context.ServiceClass<Runner, "Runner", {
|
|
|
293
320
|
readonly e: Schema.Number;
|
|
294
321
|
readonly f: Schema.Number;
|
|
295
322
|
}>>;
|
|
323
|
+
width: Schema.optionalKey<Schema.Number>;
|
|
324
|
+
height: Schema.optionalKey<Schema.Number>;
|
|
325
|
+
backgroundColor: Schema.optionalKey<typeof Color.Color>;
|
|
296
326
|
children: Schema.withConstructorDefault<Schema.$Array<Schema.String>>;
|
|
297
327
|
}>, {
|
|
298
|
-
"~position": Entity.TraitLens<
|
|
299
|
-
x:
|
|
300
|
-
y:
|
|
301
|
-
z:
|
|
302
|
-
opacity:
|
|
303
|
-
transform: Schema.
|
|
328
|
+
"~position": Entity.TraitLens<{
|
|
329
|
+
readonly x: number;
|
|
330
|
+
readonly y: number;
|
|
331
|
+
readonly z: number;
|
|
332
|
+
readonly opacity: number;
|
|
333
|
+
readonly transform: Schema.Struct.ReadonlySide<{
|
|
304
334
|
readonly a: Schema.Number;
|
|
305
335
|
readonly b: Schema.Number;
|
|
306
336
|
readonly c: Schema.Number;
|
|
307
337
|
readonly d: Schema.Number;
|
|
308
338
|
readonly e: Schema.Number;
|
|
309
339
|
readonly f: Schema.Number;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
340
|
+
}, "Type">;
|
|
341
|
+
readonly width?: number;
|
|
342
|
+
readonly height?: number;
|
|
343
|
+
readonly backgroundColor?: Color.Color;
|
|
344
|
+
readonly children: readonly string[];
|
|
345
|
+
}, Entity.Position>;
|
|
346
|
+
"~opacity": Entity.TraitLens<{
|
|
347
|
+
readonly x: number;
|
|
348
|
+
readonly y: number;
|
|
349
|
+
readonly z: number;
|
|
350
|
+
readonly opacity: number;
|
|
351
|
+
readonly transform: Schema.Struct.ReadonlySide<{
|
|
319
352
|
readonly a: Schema.Number;
|
|
320
353
|
readonly b: Schema.Number;
|
|
321
354
|
readonly c: Schema.Number;
|
|
322
355
|
readonly d: Schema.Number;
|
|
323
356
|
readonly e: Schema.Number;
|
|
324
357
|
readonly f: Schema.Number;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
|
|
358
|
+
}, "Type">;
|
|
359
|
+
readonly width?: number;
|
|
360
|
+
readonly height?: number;
|
|
361
|
+
readonly backgroundColor?: Color.Color;
|
|
362
|
+
readonly children: readonly string[];
|
|
363
|
+
}, number>;
|
|
328
364
|
}>;
|
|
329
365
|
instantiate: <Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>>>(entity: Entity.Entity<Name, Data, Traits>, props: InstantiateProps<Data["~type.make.in"]>) => Effect.Effect<Instance.Instance<Name, Data, Traits>, never, Runner>;
|
|
330
366
|
appendChild: (parent: GroupInstance, child: Instance.Instance) => void;
|
|
331
367
|
removeChild: (parent: GroupInstance, child: Instance.Instance) => void;
|
|
332
368
|
settings: {
|
|
333
369
|
frameRate: number;
|
|
334
|
-
width: number;
|
|
335
|
-
height: number;
|
|
336
|
-
backgroundColor: Color.Color;
|
|
337
370
|
seed: Seed;
|
|
338
371
|
maxFrames: number;
|
|
339
372
|
};
|
|
373
|
+
comp: CompConfig;
|
|
340
374
|
getDataUnsafe: <Name extends string, Data extends Schema.Top>(instance: Instance.Instance<Name, Data>) => Data["Type"] | null;
|
|
341
375
|
setDataUnsafe: <Name extends string, Data extends Schema.Top>(instance: Instance.Instance<Name, Data>, data: unknown) => void;
|
|
342
376
|
state: Effect.Effect<{
|
package/dist/Runner.js
CHANGED
|
@@ -13,13 +13,18 @@ export const TypeId = "~motion/SceneRunner";
|
|
|
13
13
|
export const ROOT_ID = "root";
|
|
14
14
|
/** the fixed default: scenes are deterministic even with no seed set */
|
|
15
15
|
export const defaultSeed = "effect-motion";
|
|
16
|
+
export const defaultComp = {
|
|
17
|
+
width: 1920,
|
|
18
|
+
height: 1080,
|
|
19
|
+
backgroundColor: Color.transparent,
|
|
20
|
+
};
|
|
16
21
|
/**
|
|
17
22
|
* The ambient mount parent for `instantiate` — provided per scene
|
|
18
23
|
* evaluation (`Scene.play({ parent })`); `null` means the runner root.
|
|
19
24
|
*/
|
|
20
25
|
export const CurrentParent = Context.Reference("motion/Runner/CurrentParent", { defaultValue: () => null });
|
|
21
26
|
export class Runner extends Context.Service()("Runner", {
|
|
22
|
-
make: Effect.fnUntraced(function* (settings = {}) {
|
|
27
|
+
make: Effect.fnUntraced(function* (settings = {}, comp = defaultComp) {
|
|
23
28
|
const instances = {};
|
|
24
29
|
// each instance's current parent group id (or null = detached / root).
|
|
25
30
|
// Tracked so appendChild detaches from the old parent in O(1) rather
|
|
@@ -66,9 +71,6 @@ export class Runner extends Context.Service()("Runner", {
|
|
|
66
71
|
const resolvedSettings = {
|
|
67
72
|
...settings,
|
|
68
73
|
frameRate: settings.frameRate ?? 60,
|
|
69
|
-
width: settings.width ?? 500,
|
|
70
|
-
height: settings.height ?? 300,
|
|
71
|
-
backgroundColor: settings.backgroundColor ?? Color.rgba(22, 22, 29),
|
|
72
74
|
seed: settings.seed ?? defaultSeed,
|
|
73
75
|
maxFrames: settings.maxFrames ?? 36_000,
|
|
74
76
|
};
|
|
@@ -80,13 +82,13 @@ export class Runner extends Context.Service()("Runner", {
|
|
|
80
82
|
// camera is present from the start, so `depth`/zoom work with no author
|
|
81
83
|
// ceremony; `setCamera` swaps which instance is active.
|
|
82
84
|
const camera = Instance.make(Camera, "camera");
|
|
83
|
-
setDataUnsafe(camera, identity(
|
|
85
|
+
setDataUnsafe(camera, identity(comp.width));
|
|
84
86
|
let activeCameraId = camera.id;
|
|
85
87
|
const cameraState = () => {
|
|
86
88
|
const data = instances[activeCameraId]?.data;
|
|
87
89
|
// a destroyed active camera falls back to identity rather than dying:
|
|
88
90
|
// the view is not scene-critical state
|
|
89
|
-
return data ?? identity(
|
|
91
|
+
return data ?? identity(comp.width);
|
|
90
92
|
};
|
|
91
93
|
// append `id` to a group's children and record it as the child's parent
|
|
92
94
|
const attach = (parent, id) => {
|
|
@@ -172,8 +174,7 @@ export class Runner extends Context.Service()("Runner", {
|
|
|
172
174
|
return undefined;
|
|
173
175
|
}
|
|
174
176
|
const p = props;
|
|
175
|
-
const focalLength = p.focalLength ??
|
|
176
|
-
Projection.defaultFocalLength(resolvedSettings.width);
|
|
177
|
+
const focalLength = p.focalLength ?? Projection.defaultFocalLength(comp.width);
|
|
177
178
|
return {
|
|
178
179
|
focalLength,
|
|
179
180
|
z: p.z ?? focalLength,
|
|
@@ -218,6 +219,8 @@ export class Runner extends Context.Service()("Runner", {
|
|
|
218
219
|
// leaving it detached from the tree (still alive, just unmounted)
|
|
219
220
|
removeChild,
|
|
220
221
|
settings: resolvedSettings,
|
|
222
|
+
// the root scene's composition config (resolution + background)
|
|
223
|
+
comp,
|
|
221
224
|
getDataUnsafe,
|
|
222
225
|
setDataUnsafe,
|
|
223
226
|
state: Effect.sync(() => {
|
|
@@ -229,9 +232,9 @@ export class Runner extends Context.Service()("Runner", {
|
|
|
229
232
|
instances: renderable,
|
|
230
233
|
root: ROOT_ID,
|
|
231
234
|
frameRate: resolvedSettings.frameRate,
|
|
232
|
-
width:
|
|
233
|
-
height:
|
|
234
|
-
backgroundColor:
|
|
235
|
+
width: comp.width,
|
|
236
|
+
height: comp.height,
|
|
237
|
+
backgroundColor: comp.backgroundColor,
|
|
235
238
|
camera: cameraState(),
|
|
236
239
|
};
|
|
237
240
|
}),
|
package/dist/Scene.d.ts
CHANGED
|
@@ -17,13 +17,21 @@ export declare const TypeId: "~motion/Scene";
|
|
|
17
17
|
export interface Scene<E = never, R = never> {
|
|
18
18
|
readonly [TypeId]: typeof TypeId;
|
|
19
19
|
readonly runner: Effect.Effect<void, E, R | Scope.Scope>;
|
|
20
|
+
/**
|
|
21
|
+
* composition config, After Effects–style: what this comp IS. The
|
|
22
|
+
* runner inherits the ROOT scene's; a played scene keeps its own as
|
|
23
|
+
* its bounds (see {@link play}). Unlike annotations, read by the runtime.
|
|
24
|
+
*/
|
|
25
|
+
readonly width: number;
|
|
26
|
+
readonly height: number;
|
|
27
|
+
readonly backgroundColor: Color.Color;
|
|
20
28
|
/** tooling-facing metadata; never read by the runtime */
|
|
21
29
|
readonly annotations: Context.Context<never>;
|
|
22
30
|
annotate<I, S>(key: Context.Key<I, S>, value: S): Scene<E, R>;
|
|
23
31
|
annotateMerge(context: Context.Context<never>): Scene<E, R>;
|
|
24
32
|
}
|
|
25
33
|
export type AnyScene = Scene<never, never>;
|
|
26
|
-
export declare const make: <const Eff extends Effect.Effect<any, any, any>, const AEff>(f: () => Generator<Eff, AEff, never>) => Scene<[Eff] extends [never] ? never : [Eff] extends [Effect.Effect<infer _A, infer E, infer _R>] ? E : never, [Eff] extends [never] ? never : [Eff] extends [Effect.Effect<infer _A, infer _E, infer R>] ? R : never>;
|
|
34
|
+
export declare const make: <const Eff extends Effect.Effect<any, any, any>, const AEff>(f: () => Generator<Eff, AEff, never>, meta?: Partial<Runner.CompConfig>) => Scene<[Eff] extends [never] ? never : [Eff] extends [Effect.Effect<infer _A, infer E, infer _R>] ? E : never, [Eff] extends [never] ? never : [Eff] extends [Effect.Effect<infer _A, infer _E, infer R>] ? R : never>;
|
|
27
35
|
export declare const instantiate: <Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>>>(entity: Entity.Entity<Name, Data, Traits>, props: Runner.InstantiateProps<Data["~type.make.in"]>) => Effect.Effect<Instance.Instance<Name, Data, Traits>, never, Runner.Runner>;
|
|
28
36
|
export declare const tick: Effect.Effect<void, never, Runner.Runner>;
|
|
29
37
|
/**
|
|
@@ -49,7 +57,8 @@ export interface Frame<Entities extends Entity.AnyEntity = Entity.AnyEntity> {
|
|
|
49
57
|
instances: Record<string, EntriesFromEntities<Entities>>;
|
|
50
58
|
/** id of the root group (conventionally "root"); never rendered itself */
|
|
51
59
|
root: string;
|
|
52
|
-
/** render metadata from the runner settings
|
|
60
|
+
/** render metadata — frameRate from the runner settings, resolution and
|
|
61
|
+
* background from the ROOT scene's comp config; a frame is self-describing */
|
|
53
62
|
frameRate: number;
|
|
54
63
|
width: number;
|
|
55
64
|
height: number;
|
|
@@ -82,12 +91,11 @@ export declare const appendChild: (parent: Runner.GroupInstance, child: Instance
|
|
|
82
91
|
export declare const removeChild: (parent: Runner.GroupInstance, child: Instance.Instance) => Effect.Effect<void, never, Runner.Runner>;
|
|
83
92
|
export declare const settings: () => Effect.Effect<{
|
|
84
93
|
frameRate: number;
|
|
85
|
-
width: number;
|
|
86
|
-
height: number;
|
|
87
|
-
backgroundColor: Color.Color;
|
|
88
94
|
seed: Runner.Seed;
|
|
89
95
|
maxFrames: number;
|
|
90
96
|
}, never, Runner.Runner>;
|
|
97
|
+
/** the movie's composition config — the ROOT scene's width/height/background */
|
|
98
|
+
export declare const comp: () => Effect.Effect<Runner.CompConfig, never, Runner.Runner>;
|
|
91
99
|
/**
|
|
92
100
|
* The active camera instance — an ordinary instance carrying `~position`
|
|
93
101
|
* (world x/y/z), Euler orientation (`rotX`/`rotY`/`rotZ`), and
|
|
@@ -217,21 +225,35 @@ export declare const fork: <A, E = never, R = never>(effect: Effect.Effect<A, E,
|
|
|
217
225
|
*/
|
|
218
226
|
export declare const background: <A, E = never, R = never>(effect: Effect.Effect<A, E, R>) => Effect.Effect<BranchHandle<A, E>, never, Runner.Runner | Exclude<Exclude<R, never>, Phaser.Phaser>>;
|
|
219
227
|
export interface PlayOptions {
|
|
220
|
-
/** group to mount the
|
|
228
|
+
/** group to mount the child's bounds group under (default: the ambient parent) */
|
|
221
229
|
readonly parent?: Runner.GroupInstance;
|
|
222
230
|
/** seed for this evaluation (default: the movie's seed) */
|
|
223
231
|
readonly seed?: Runner.Seed;
|
|
224
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* A played scene's branch handle plus its mount group — the child comp as
|
|
235
|
+
* one unit. Move/fade the group (trait lenses) or scale it (transform
|
|
236
|
+
* operations) to transform the whole nested scene, bounds included.
|
|
237
|
+
*/
|
|
238
|
+
export interface PlayHandle<A = void, E = never> extends BranchHandle<A, E> {
|
|
239
|
+
readonly group: Runner.GroupInstance;
|
|
240
|
+
}
|
|
225
241
|
/**
|
|
226
242
|
* Play a scene as a branch of the current scene — the explicit door to
|
|
227
|
-
* nesting. The child shares the movie's
|
|
228
|
-
* frame cap, and gets its own scope,
|
|
229
|
-
* FRESH seeded Random stream: `play(scene)` inside a
|
|
230
|
-
* animates exactly like `run(scene, { seed: S })`
|
|
231
|
-
*
|
|
232
|
-
*
|
|
243
|
+
* nesting, After Effects–precomp-style. The child shares the movie's
|
|
244
|
+
* runner, phaser, frame rate, and frame cap, and gets its own scope,
|
|
245
|
+
* branch handle, and a FRESH seeded Random stream: `play(scene)` inside a
|
|
246
|
+
* movie seeded `S` animates exactly like `run(scene, { seed: S })`
|
|
247
|
+
* standalone. Each evaluation mounts the child under an implicit group
|
|
248
|
+
* carrying the child scene's bounds (width/height/backgroundColor):
|
|
249
|
+
* content clips to them, a non-transparent background paints within them,
|
|
250
|
+
* and the group is placed so the child's bounds CENTER in the enclosing
|
|
251
|
+
* comp (the movie, or the enclosing played scene) — a child smaller or
|
|
252
|
+
* bigger than the movie renders centered. Awaited like a fork —
|
|
253
|
+
* `yield* handle.finished` for sequential nesting, or don't await for
|
|
254
|
+
* concurrent scenes.
|
|
233
255
|
*/
|
|
234
|
-
export declare const play: <E, R>(scene: Scene<E, R>, options?: PlayOptions) => Effect.Effect<
|
|
256
|
+
export declare const play: <E, R>(scene: Scene<E, R>, options?: PlayOptions) => Effect.Effect<PlayHandle<void, E>, never, Runner.Runner | Exclude<R, Scope.Scope>>;
|
|
235
257
|
/**
|
|
236
258
|
* Run effects in lockstep parallel, sharing frame phases — the public
|
|
237
259
|
* counterpart to the low-level `Phaser.all`. Takes no schedule: pacing a
|
package/dist/Scene.js
CHANGED
|
@@ -7,18 +7,22 @@ import * as Random from "effect/Random";
|
|
|
7
7
|
import * as Stream from "effect/Stream";
|
|
8
8
|
import * as Phaser from "./Phaser.js";
|
|
9
9
|
import * as Runner from "./Runner.js";
|
|
10
|
+
import { Group } from "./shapes/Group.js";
|
|
10
11
|
import * as Time from "./Time.js";
|
|
11
12
|
export const TypeId = "~motion/Scene";
|
|
12
|
-
export const make = (f) => {
|
|
13
|
-
return makeScene(Effect.scoped(Effect.gen(f)), Context.empty());
|
|
13
|
+
export const make = (f, meta = {}) => {
|
|
14
|
+
return makeScene(Effect.scoped(Effect.gen(f)), Context.empty(), meta);
|
|
14
15
|
};
|
|
15
16
|
// annotate/annotateMerge return new scene values sharing the same body
|
|
16
|
-
const makeScene = (runnerEffect, annotations) => ({
|
|
17
|
+
const makeScene = (runnerEffect, annotations, meta) => ({
|
|
17
18
|
[TypeId]: TypeId,
|
|
18
19
|
runner: runnerEffect,
|
|
19
20
|
annotations,
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
width: meta.width ?? Runner.defaultComp.width,
|
|
22
|
+
height: meta.height ?? Runner.defaultComp.height,
|
|
23
|
+
backgroundColor: meta.backgroundColor ?? Runner.defaultComp.backgroundColor,
|
|
24
|
+
annotate: (key, value) => makeScene(runnerEffect, Context.add(annotations, key, value), meta),
|
|
25
|
+
annotateMerge: (context) => makeScene(runnerEffect, Context.merge(annotations, context), meta),
|
|
22
26
|
});
|
|
23
27
|
export const instantiate = Effect.fnUntraced(function* (entity, props) {
|
|
24
28
|
const runner = yield* Runner.Runner;
|
|
@@ -76,7 +80,12 @@ export const step = (runningScene) => Effect.gen(function* () {
|
|
|
76
80
|
return (yield* runningScene.runner.state);
|
|
77
81
|
});
|
|
78
82
|
export const run = (scene, settings = {}) => Effect.gen(function* () {
|
|
79
|
-
|
|
83
|
+
// the runner inherits the ROOT scene's composition config
|
|
84
|
+
const runner = yield* Runner.Runner.make(settings, {
|
|
85
|
+
width: scene.width,
|
|
86
|
+
height: scene.height,
|
|
87
|
+
backgroundColor: scene.backgroundColor,
|
|
88
|
+
});
|
|
80
89
|
let done = false;
|
|
81
90
|
// the body is itself a branch: Scene.finish inside it demotes the
|
|
82
91
|
// root (count--) while the body keeps ticking as a tail
|
|
@@ -189,6 +198,11 @@ export const settings = Effect.fnUntraced(function* () {
|
|
|
189
198
|
const runner = yield* Runner.Runner;
|
|
190
199
|
return runner.settings;
|
|
191
200
|
});
|
|
201
|
+
/** the movie's composition config — the ROOT scene's width/height/background */
|
|
202
|
+
export const comp = Effect.fnUntraced(function* () {
|
|
203
|
+
const runner = yield* Runner.Runner;
|
|
204
|
+
return runner.comp;
|
|
205
|
+
});
|
|
192
206
|
/**
|
|
193
207
|
* The active camera instance — an ordinary instance carrying `~position`
|
|
194
208
|
* (world x/y/z), Euler orientation (`rotX`/`rotY`/`rotZ`), and
|
|
@@ -353,24 +367,57 @@ export const background = (effect) => Effect.gen(function* () {
|
|
|
353
367
|
});
|
|
354
368
|
/**
|
|
355
369
|
* Play a scene as a branch of the current scene — the explicit door to
|
|
356
|
-
* nesting. The child shares the movie's
|
|
357
|
-
* frame cap, and gets its own scope,
|
|
358
|
-
* FRESH seeded Random stream: `play(scene)` inside a
|
|
359
|
-
* animates exactly like `run(scene, { seed: S })`
|
|
360
|
-
*
|
|
361
|
-
*
|
|
370
|
+
* nesting, After Effects–precomp-style. The child shares the movie's
|
|
371
|
+
* runner, phaser, frame rate, and frame cap, and gets its own scope,
|
|
372
|
+
* branch handle, and a FRESH seeded Random stream: `play(scene)` inside a
|
|
373
|
+
* movie seeded `S` animates exactly like `run(scene, { seed: S })`
|
|
374
|
+
* standalone. Each evaluation mounts the child under an implicit group
|
|
375
|
+
* carrying the child scene's bounds (width/height/backgroundColor):
|
|
376
|
+
* content clips to them, a non-transparent background paints within them,
|
|
377
|
+
* and the group is placed so the child's bounds CENTER in the enclosing
|
|
378
|
+
* comp (the movie, or the enclosing played scene) — a child smaller or
|
|
379
|
+
* bigger than the movie renders centered. Awaited like a fork —
|
|
380
|
+
* `yield* handle.finished` for sequential nesting, or don't await for
|
|
381
|
+
* concurrent scenes.
|
|
362
382
|
*/
|
|
363
383
|
export const play = (scene, options) => Effect.gen(function* () {
|
|
364
384
|
const runner = yield* Runner.Runner;
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
385
|
+
// default placement: the child's bounds centered in the enclosing
|
|
386
|
+
// comp — the ambient (or explicit) parent's bounds when it is a
|
|
387
|
+
// sized group, the movie's comp at the root. An unsized parent
|
|
388
|
+
// group has no bounds to center in; the child mounts at its origin.
|
|
389
|
+
const ambient = options?.parent ?? (yield* Runner.CurrentParent);
|
|
390
|
+
const enclosing = (() => {
|
|
391
|
+
if (ambient === null) {
|
|
392
|
+
return runner.comp;
|
|
393
|
+
}
|
|
394
|
+
const data = runner.getDataUnsafe(ambient);
|
|
395
|
+
return data !== null &&
|
|
396
|
+
typeof data.width === "number" &&
|
|
397
|
+
typeof data.height === "number"
|
|
398
|
+
? { width: data.width, height: data.height }
|
|
399
|
+
: null;
|
|
400
|
+
})();
|
|
401
|
+
// the child's comp bounds ride on the mount group: the renderer clips
|
|
402
|
+
// the subtree to them and paints the child's background within them
|
|
403
|
+
const group = yield* runner
|
|
404
|
+
.instantiate(Group, {
|
|
405
|
+
x: enclosing === null ? 0 : (enclosing.width - scene.width) / 2,
|
|
406
|
+
y: enclosing === null ? 0 : (enclosing.height - scene.height) / 2,
|
|
407
|
+
width: scene.width,
|
|
408
|
+
height: scene.height,
|
|
409
|
+
backgroundColor: scene.backgroundColor,
|
|
410
|
+
})
|
|
411
|
+
.pipe(Effect.provideService(Runner.CurrentParent, ambient));
|
|
412
|
+
const body = scene.runner.pipe(Effect.scoped,
|
|
413
|
+
// the child's instances mount under its bounds group
|
|
414
|
+
Effect.provideService(Runner.CurrentParent, group),
|
|
369
415
|
// fresh stream per evaluation: nested playback must equal a
|
|
370
416
|
// standalone run with the same seed, never inherit the parent's
|
|
371
417
|
// stream position
|
|
372
418
|
Random.withSeed(options?.seed ?? runner.settings.seed));
|
|
373
|
-
|
|
419
|
+
const handle = (yield* forkBranch(runner, body, "fork"));
|
|
420
|
+
return { ...handle, group };
|
|
374
421
|
});
|
|
375
422
|
/**
|
|
376
423
|
* Run effects in lockstep parallel, sharing frame phases — the public
|
package/dist/demo.js
CHANGED
|
@@ -9,6 +9,12 @@ import * as PngExporter from "./PngExporter.js";
|
|
|
9
9
|
import * as Renderer from "./Renderer.js";
|
|
10
10
|
import * as Scene from "./Scene.js";
|
|
11
11
|
import * as Shapes from "./Shapes.js";
|
|
12
|
+
// the demo's comp config — the old runner defaults, now explicit on the scene
|
|
13
|
+
const demoComp = {
|
|
14
|
+
width: 500,
|
|
15
|
+
height: 300,
|
|
16
|
+
backgroundColor: Color.rgba(22, 22, 29),
|
|
17
|
+
};
|
|
12
18
|
// children live in the group's local coordinates: one motion moves them all
|
|
13
19
|
export const scene = Scene.make(function* () {
|
|
14
20
|
const duo = yield* Scene.instantiate(Shapes.Group, {
|
|
@@ -31,7 +37,7 @@ export const scene = Scene.make(function* () {
|
|
|
31
37
|
});
|
|
32
38
|
yield* Motion.wait("1.5 seconds");
|
|
33
39
|
yield* duo.pipe(Motion.moveTo({ x: 380 }, "1.5 seconds", "easeInOutCubic"), Motion.moveTo({ x: 70 }, "1.5 seconds", "easeInOutCubic"), Motion.wait("1.5 seconds"), Physics.springTo({ y: 80 }, "jump"), Motion.fadeTo(0.15, "1 second"));
|
|
34
|
-
});
|
|
40
|
+
}, demoComp);
|
|
35
41
|
// schedule-driven composition: a background pulse loops for the scene's
|
|
36
42
|
// duration while three staggered dots define its actual length
|
|
37
43
|
export const staggered = Scene.make(function* () {
|
|
@@ -49,11 +55,11 @@ export const staggered = Scene.make(function* () {
|
|
|
49
55
|
yield* Motion.tweenTo(circle, { x: x + 300 }, "1 second", "easeInOutCubic");
|
|
50
56
|
});
|
|
51
57
|
yield* Scene.stagger([dot(0), dot(25), dot(50)], Schedule.spaced("250 millis"));
|
|
52
|
-
});
|
|
58
|
+
}, demoComp);
|
|
53
59
|
// render the middle frame of the duo scene to a PNG through the single ThorVG
|
|
54
60
|
// renderer (Node adapter) — the end-to-end path: Scene.stream → renderToPng.
|
|
55
61
|
const movie = Effect.gen(function* () {
|
|
56
|
-
const frames = yield* Scene.stream(scene
|
|
62
|
+
const frames = yield* Scene.stream(scene).pipe(Stream.runCollect);
|
|
57
63
|
const list = [...frames];
|
|
58
64
|
const mid = list[Math.floor(list.length / 2)];
|
|
59
65
|
const framebuffer = yield* Renderer.render(mid);
|
|
@@ -51,8 +51,8 @@ const run = Effect.fnUntraced(function* (instance, duration, emission) {
|
|
|
51
51
|
// fill spreads across a region: the field's own if set, else the frame
|
|
52
52
|
const current = yield* Scene.data(instance);
|
|
53
53
|
const region = {
|
|
54
|
-
w: current.region?.w ?? runner.
|
|
55
|
-
h: current.region?.h ?? runner.
|
|
54
|
+
w: current.region?.w ?? runner.comp.width,
|
|
55
|
+
h: current.region?.h ?? runner.comp.height,
|
|
56
56
|
};
|
|
57
57
|
for (let i = 1; i <= frames; i++) {
|
|
58
58
|
const n = birthsForFrame(emission, i, fps);
|
package/dist/shapes/Group.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as Schema from "effect/Schema";
|
|
2
|
+
import * as Color from "../Color.js";
|
|
2
3
|
import * as Entity from "../Entity.js";
|
|
3
4
|
export declare const TransformMatrix: Schema.Struct<{
|
|
4
5
|
readonly a: Schema.Number;
|
|
@@ -74,38 +75,47 @@ export declare const Group: Entity.Entity<"shapes/Group", Schema.Struct<{
|
|
|
74
75
|
readonly e: Schema.Number;
|
|
75
76
|
readonly f: Schema.Number;
|
|
76
77
|
}>>;
|
|
78
|
+
width: Schema.optionalKey<Schema.Number>;
|
|
79
|
+
height: Schema.optionalKey<Schema.Number>;
|
|
80
|
+
backgroundColor: Schema.optionalKey<typeof Color.Color>;
|
|
77
81
|
children: Schema.withConstructorDefault<Schema.$Array<Schema.String>>;
|
|
78
82
|
}>, {
|
|
79
|
-
"~position": Entity.TraitLens<
|
|
80
|
-
x:
|
|
81
|
-
y:
|
|
82
|
-
z:
|
|
83
|
-
opacity:
|
|
84
|
-
transform: Schema.
|
|
83
|
+
"~position": Entity.TraitLens<{
|
|
84
|
+
readonly x: number;
|
|
85
|
+
readonly y: number;
|
|
86
|
+
readonly z: number;
|
|
87
|
+
readonly opacity: number;
|
|
88
|
+
readonly transform: Schema.Struct.ReadonlySide<{
|
|
85
89
|
readonly a: Schema.Number;
|
|
86
90
|
readonly b: Schema.Number;
|
|
87
91
|
readonly c: Schema.Number;
|
|
88
92
|
readonly d: Schema.Number;
|
|
89
93
|
readonly e: Schema.Number;
|
|
90
94
|
readonly f: Schema.Number;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
}, "Type">;
|
|
96
|
+
readonly width?: number;
|
|
97
|
+
readonly height?: number;
|
|
98
|
+
readonly backgroundColor?: Color.Color;
|
|
99
|
+
readonly children: readonly string[];
|
|
100
|
+
}, Entity.Position>;
|
|
101
|
+
"~opacity": Entity.TraitLens<{
|
|
102
|
+
readonly x: number;
|
|
103
|
+
readonly y: number;
|
|
104
|
+
readonly z: number;
|
|
105
|
+
readonly opacity: number;
|
|
106
|
+
readonly transform: Schema.Struct.ReadonlySide<{
|
|
100
107
|
readonly a: Schema.Number;
|
|
101
108
|
readonly b: Schema.Number;
|
|
102
109
|
readonly c: Schema.Number;
|
|
103
110
|
readonly d: Schema.Number;
|
|
104
111
|
readonly e: Schema.Number;
|
|
105
112
|
readonly f: Schema.Number;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
113
|
+
}, "Type">;
|
|
114
|
+
readonly width?: number;
|
|
115
|
+
readonly height?: number;
|
|
116
|
+
readonly backgroundColor?: Color.Color;
|
|
117
|
+
readonly children: readonly string[];
|
|
118
|
+
}, number>;
|
|
109
119
|
}>;
|
|
110
120
|
/** The group's x/y position composed outside its normalized local transform. */
|
|
111
121
|
export declare const resolvedTransform: (data: {
|
package/dist/shapes/Group.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
2
|
import * as Schema from "effect/Schema";
|
|
3
3
|
import * as SchemaGetter from "effect/SchemaGetter";
|
|
4
|
+
import * as Color from "../Color.js";
|
|
4
5
|
import * as Entity from "../Entity.js";
|
|
5
6
|
import * as Shape2D from "./Shape2D.js";
|
|
6
7
|
export const TransformMatrix = Schema.Struct({
|
|
@@ -57,6 +58,12 @@ const fields = {
|
|
|
57
58
|
...Shape2D.position,
|
|
58
59
|
...Shape2D.opacity,
|
|
59
60
|
transform: TransformMatrix.pipe(Schema.withConstructorDefault(Effect.succeed(identityTransform))),
|
|
61
|
+
// comp bounds (Scene.play mount groups carry the child comp's): a SIZED
|
|
62
|
+
// group clips its subtree to them, paints a non-transparent
|
|
63
|
+
// backgroundColor within them, and renders as one unit (AE precomp)
|
|
64
|
+
width: Schema.optionalKey(Schema.Number),
|
|
65
|
+
height: Schema.optionalKey(Schema.Number),
|
|
66
|
+
backgroundColor: Schema.optionalKey(Color.Color),
|
|
60
67
|
children: Schema.Array(Schema.String).pipe(Schema.withConstructorDefault(Effect.sync(() => []))),
|
|
61
68
|
};
|
|
62
69
|
const decodeTransform = Schema.decodeUnknownSync(Transform);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effect-motion",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Deterministic, frame-exact motion graphics in code, composed with Effect",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@effect/platform-node": "4.0.0-beta.98",
|
|
52
52
|
"chroma-js": "^3.2.0",
|
|
53
|
-
"@effect-motion/thorvg": "^0.
|
|
53
|
+
"@effect-motion/thorvg": "^0.2.0"
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"build": "tsc -p tsconfig.build.json",
|