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.
@@ -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
@@ -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 { settings } = yield* Runner.Runner;
71
+ const { comp } = yield* Runner.Runner;
72
72
  const data = (yield* Scene.data(cam));
73
73
  return {
74
- x: settings.width / 2 + data.x,
75
- y: settings.height / 2 + data.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 { settings } = yield* Runner.Runner;
166
- const origin = { x: settings.width / 2, y: settings.height / 2 };
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 { settings } = yield* Runner.Runner;
201
- const origin = { x: settings.width / 2, y: settings.height / 2 };
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) => Effect.gen(function* () {
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
- paintables.push({
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
- paintables.push({
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
- paintables.push({
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
- paintables.sort((a, b) => (a.hud ? 1 : 0) - (b.hud ? 1 : 0) ||
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 { id, entry, projection, hud } of paintables) {
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
- // the concrete paint fn for this entity name; the map is exhaustive
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
- yield* Tvg.Canvas.draw(canvas);
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<Schema.Struct.ReadonlySide<{
85
- x: Schema.withConstructorDefault<Schema.Number>;
86
- y: Schema.withConstructorDefault<Schema.Number>;
87
- z: Schema.withConstructorDefault<Schema.Number>;
88
- opacity: Schema.withConstructorDefault<Schema.Number>;
89
- transform: Schema.withConstructorDefault<Schema.Struct<{
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
- children: Schema.withConstructorDefault<Schema.$Array<Schema.String>>;
98
- }, "Type">, Entity.Position>;
99
- "~opacity": Entity.TraitLens<Schema.Struct.ReadonlySide<{
100
- x: Schema.withConstructorDefault<Schema.Number>;
101
- y: Schema.withConstructorDefault<Schema.Number>;
102
- z: Schema.withConstructorDefault<Schema.Number>;
103
- opacity: Schema.withConstructorDefault<Schema.Number>;
104
- transform: Schema.withConstructorDefault<Schema.Struct<{
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
- children: Schema.withConstructorDefault<Schema.$Array<Schema.String>>;
113
- }, "Type">, number>;
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<Schema.Struct.ReadonlySide<{
141
- x: Schema.withConstructorDefault<Schema.Number>;
142
- y: Schema.withConstructorDefault<Schema.Number>;
143
- z: Schema.withConstructorDefault<Schema.Number>;
144
- opacity: Schema.withConstructorDefault<Schema.Number>;
145
- transform: Schema.withConstructorDefault<Schema.Struct<{
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
- children: Schema.withConstructorDefault<Schema.$Array<Schema.String>>;
154
- }, "Type">, Entity.Position>;
155
- "~opacity": Entity.TraitLens<Schema.Struct.ReadonlySide<{
156
- x: Schema.withConstructorDefault<Schema.Number>;
157
- y: Schema.withConstructorDefault<Schema.Number>;
158
- z: Schema.withConstructorDefault<Schema.Number>;
159
- opacity: Schema.withConstructorDefault<Schema.Number>;
160
- transform: Schema.withConstructorDefault<Schema.Struct<{
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
- children: Schema.withConstructorDefault<Schema.$Array<Schema.String>>;
169
- }, "Type">, number>;
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<Schema.Struct.ReadonlySide<{
299
- x: Schema.withConstructorDefault<Schema.Number>;
300
- y: Schema.withConstructorDefault<Schema.Number>;
301
- z: Schema.withConstructorDefault<Schema.Number>;
302
- opacity: Schema.withConstructorDefault<Schema.Number>;
303
- transform: Schema.withConstructorDefault<Schema.Struct<{
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
- children: Schema.withConstructorDefault<Schema.$Array<Schema.String>>;
312
- }, "Type">, Entity.Position>;
313
- "~opacity": Entity.TraitLens<Schema.Struct.ReadonlySide<{
314
- x: Schema.withConstructorDefault<Schema.Number>;
315
- y: Schema.withConstructorDefault<Schema.Number>;
316
- z: Schema.withConstructorDefault<Schema.Number>;
317
- opacity: Schema.withConstructorDefault<Schema.Number>;
318
- transform: Schema.withConstructorDefault<Schema.Struct<{
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
- children: Schema.withConstructorDefault<Schema.$Array<Schema.String>>;
327
- }, "Type">, number>;
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(resolvedSettings.width));
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(resolvedSettings.width);
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: resolvedSettings.width,
233
- height: resolvedSettings.height,
234
- backgroundColor: resolvedSettings.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 — a frame is self-describing */
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 scene's instances under (default: the root) */
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 runner, phaser, frame rate, and
228
- * frame cap, and gets its own scope, branch handle, mount parent, and a
229
- * FRESH seeded Random stream: `play(scene)` inside a movie seeded `S`
230
- * animates exactly like `run(scene, { seed: S })` standalone. Awaited
231
- * like a fork — `yield* handle.finished` for sequential nesting, or
232
- * don't await for concurrent scenes.
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<BranchHandle<void, E>, never, Runner.Runner | Exclude<R, Scope.Scope>>;
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
- annotate: (key, value) => makeScene(runnerEffect, Context.add(annotations, key, value)),
21
- annotateMerge: (context) => makeScene(runnerEffect, Context.merge(annotations, context)),
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
- const runner = yield* Runner.Runner.make(settings);
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 runner, phaser, frame rate, and
357
- * frame cap, and gets its own scope, branch handle, mount parent, and a
358
- * FRESH seeded Random stream: `play(scene)` inside a movie seeded `S`
359
- * animates exactly like `run(scene, { seed: S })` standalone. Awaited
360
- * like a fork — `yield* handle.finished` for sequential nesting, or
361
- * don't await for concurrent scenes.
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
- const mounted = options?.parent === undefined
366
- ? scene.runner.pipe(Effect.scoped)
367
- : scene.runner.pipe(Effect.scoped, Effect.provideService(Runner.CurrentParent, options.parent));
368
- const body = mounted.pipe(
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
- return (yield* forkBranch(runner, body, "fork"));
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, { width: 500, height: 300 }).pipe(Stream.runCollect);
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.settings.width,
55
- h: current.region?.h ?? runner.settings.height,
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);
@@ -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<Schema.Struct.ReadonlySide<{
80
- x: Schema.withConstructorDefault<Schema.Number>;
81
- y: Schema.withConstructorDefault<Schema.Number>;
82
- z: Schema.withConstructorDefault<Schema.Number>;
83
- opacity: Schema.withConstructorDefault<Schema.Number>;
84
- transform: Schema.withConstructorDefault<Schema.Struct<{
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
- children: Schema.withConstructorDefault<Schema.$Array<Schema.String>>;
93
- }, "Type">, Entity.Position>;
94
- "~opacity": Entity.TraitLens<Schema.Struct.ReadonlySide<{
95
- x: Schema.withConstructorDefault<Schema.Number>;
96
- y: Schema.withConstructorDefault<Schema.Number>;
97
- z: Schema.withConstructorDefault<Schema.Number>;
98
- opacity: Schema.withConstructorDefault<Schema.Number>;
99
- transform: Schema.withConstructorDefault<Schema.Struct<{
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
- children: Schema.withConstructorDefault<Schema.$Array<Schema.String>>;
108
- }, "Type">, number>;
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: {
@@ -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.2",
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.1.0"
53
+ "@effect-motion/thorvg": "^0.2.0"
54
54
  },
55
55
  "scripts": {
56
56
  "build": "tsc -p tsconfig.build.json",