effect-motion 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/Camera.d.ts +186 -49
- package/dist/Camera.js +343 -76
- package/dist/Color.d.ts +101 -1
- package/dist/Color.js +101 -1
- package/dist/EffectMotionError.d.ts +17 -0
- package/dist/EffectMotionError.js +17 -0
- package/dist/Entity.d.ts +684 -30
- package/dist/Entity.js +281 -27
- package/dist/Font.d.ts +108 -0
- package/dist/Font.js +95 -0
- package/dist/Image.d.ts +71 -0
- package/dist/Image.js +50 -0
- package/dist/Instance.d.ts +73 -11
- package/dist/Instance.js +44 -11
- package/dist/Motion.d.ts +328 -53
- package/dist/Motion.js +278 -46
- package/dist/Physics.d.ts +154 -20
- package/dist/Physics.js +92 -11
- package/dist/Projection.d.ts +36 -132
- package/dist/Projection.js +33 -292
- package/dist/Resource.d.ts +26 -0
- package/dist/Resource.js +41 -0
- package/dist/Runner.d.ts +714 -349
- package/dist/Runner.js +197 -150
- package/dist/Scene.d.ts +603 -175
- package/dist/Scene.js +596 -112
- package/dist/Timing.d.ts +170 -13
- package/dist/Timing.js +124 -6
- package/dist/Tree.d.ts +39 -0
- package/dist/Tree.js +127 -0
- package/dist/index.d.ts +54 -5
- package/dist/index.js +56 -5
- package/dist/particles/Particle.d.ts +2 -2
- package/dist/particles/ParticleField.d.ts +44 -42
- package/dist/particles/ParticleField.js +8 -9
- package/dist/particles/constructors.d.ts +5 -6
- package/dist/particles/constructors.js +8 -3
- package/dist/particles/legacy.d.ts +58 -0
- package/dist/particles/legacy.js +46 -0
- package/dist/particles/simulate.js +11 -3
- package/dist/particles/step.js +14 -10
- package/dist/types.d.ts +5 -0
- package/dist/types.js +1 -0
- package/package.json +58 -60
- package/dist/CameraHelpers.d.ts +0 -70
- package/dist/CameraHelpers.js +0 -239
- package/dist/CanvasExporter.d.ts +0 -12
- package/dist/CanvasExporter.js +0 -40
- package/dist/Fonts.d.ts +0 -41
- package/dist/Fonts.js +0 -27
- package/dist/Images.d.ts +0 -33
- package/dist/Images.js +0 -24
- package/dist/PngExporter.d.ts +0 -6
- package/dist/PngExporter.js +0 -85
- package/dist/Renderer.d.ts +0 -118
- package/dist/Renderer.js +0 -508
- package/dist/Shapes.d.ts +0 -11
- package/dist/Shapes.js +0 -11
- package/dist/demo.d.ts +0 -3
- package/dist/demo.js +0 -71
- package/dist/render/dof.d.ts +0 -27
- package/dist/render/dof.js +0 -37
- package/dist/render/paint.d.ts +0 -30
- package/dist/render/paint.js +0 -36
- package/dist/render/shapes.d.ts +0 -42
- package/dist/render/shapes.js +0 -310
- package/dist/shapes/Circle.d.ts +0 -32
- package/dist/shapes/Circle.js +0 -9
- package/dist/shapes/Ellipse.d.ts +0 -35
- package/dist/shapes/Ellipse.js +0 -10
- package/dist/shapes/Group.d.ts +0 -126
- package/dist/shapes/Group.js +0 -89
- package/dist/shapes/Hud.d.ts +0 -38
- package/dist/shapes/Hud.js +0 -35
- package/dist/shapes/Image.d.ts +0 -45
- package/dist/shapes/Image.js +0 -28
- package/dist/shapes/Line.d.ts +0 -47
- package/dist/shapes/Line.js +0 -35
- package/dist/shapes/Path.d.ts +0 -107
- package/dist/shapes/Path.js +0 -32
- package/dist/shapes/Rect.d.ts +0 -51
- package/dist/shapes/Rect.js +0 -22
- package/dist/shapes/Shape2D.d.ts +0 -49
- package/dist/shapes/Shape2D.js +0 -52
- package/dist/shapes/Shapes.d.ts +0 -11
- package/dist/shapes/Shapes.js +0 -11
- package/dist/shapes/Square.d.ts +0 -32
- package/dist/shapes/Square.js +0 -11
- package/dist/shapes/Text.d.ts +0 -51
- package/dist/shapes/Text.js +0 -24
package/dist/Scene.js
CHANGED
|
@@ -1,41 +1,187 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Authoring and running scenes — the module you start from.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* A scene is a generator function that creates entities and yields
|
|
6
|
+
* animations. It is a pure description: running it twice produces the same
|
|
7
|
+
* frames, because scene time is counted in FRAMES rather than read from a
|
|
8
|
+
* clock, and randomness comes from a seeded generator.
|
|
9
|
+
*
|
|
10
|
+
* The surface divides into three jobs:
|
|
11
|
+
*
|
|
12
|
+
* - **Authoring** — {@link make} to declare a scene, {@link instantiate} to
|
|
13
|
+
* put something in it, {@link sleep} to hold, {@link data} / {@link update}
|
|
14
|
+
* to read and write entity state directly.
|
|
15
|
+
* - **Composition** — {@link all} (together), {@link chain} (one after
|
|
16
|
+
* another), {@link stagger} (overlapping), {@link repeat} (again, on a
|
|
17
|
+
* schedule), {@link fork} / {@link background} (alongside), {@link play}
|
|
18
|
+
* (a whole scene nested inside another).
|
|
19
|
+
* - **Consumption** — {@link run} to start one, {@link stream} to pull its
|
|
20
|
+
* frames lazily, {@link step} to advance it a frame at a time.
|
|
21
|
+
*
|
|
22
|
+
* Concurrency is frame-synchronized. Animations running "at the same time"
|
|
23
|
+
* all advance exactly one frame per tick and wait for each other at a
|
|
24
|
+
* barrier, so adding concurrency never changes the frames a scene produces —
|
|
25
|
+
* only how they are written.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* A complete scene: two shapes, one moving after the other.
|
|
29
|
+
* ```typescript
|
|
30
|
+
* import * as Motion from "effect-motion/Motion";
|
|
31
|
+
* import * as Scene from "effect-motion/Scene";
|
|
32
|
+
*
|
|
33
|
+
* const scene = Scene.make(
|
|
34
|
+
* function* () {
|
|
35
|
+
* const dot = yield* Scene.instantiate("Circle", { radius: 20 });
|
|
36
|
+
* yield* dot.pipe(Motion.moveTo({ x: 400 }, "1 second"));
|
|
37
|
+
* },
|
|
38
|
+
* { width: 500, height: 300 },
|
|
39
|
+
* );
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
import { Latch, Layer } from "effect";
|
|
2
43
|
import * as Cause from "effect/Cause";
|
|
44
|
+
import * as Context from "effect/Context";
|
|
3
45
|
import * as Effect from "effect/Effect";
|
|
4
46
|
import * as Exit from "effect/Exit";
|
|
5
47
|
import * as Fiber from "effect/Fiber";
|
|
6
48
|
import * as Random from "effect/Random";
|
|
7
49
|
import * as Stream from "effect/Stream";
|
|
50
|
+
import * as Entity from "./Entity.js";
|
|
8
51
|
import * as Phaser from "./Phaser.js";
|
|
9
52
|
import * as Runner from "./Runner.js";
|
|
10
|
-
import { Group } from "./shapes/Group.js";
|
|
11
53
|
import * as Time from "./Time.js";
|
|
12
54
|
export const TypeId = "~motion/Scene";
|
|
13
|
-
|
|
14
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Declare a scene from a generator body.
|
|
57
|
+
*
|
|
58
|
+
* @remarks
|
|
59
|
+
* The generator is where a scene is written: `yield*` an
|
|
60
|
+
* {@link instantiate} to create something, `yield*` an animator to move it.
|
|
61
|
+
* Yielding is what makes time pass — statements between yields all happen on
|
|
62
|
+
* the same frame.
|
|
63
|
+
*
|
|
64
|
+
* The body does NOT run here. `make` captures it, so a scene can be
|
|
65
|
+
* declared once at module scope and run many times; each run re-executes
|
|
66
|
+
* the body from scratch with fresh entities.
|
|
67
|
+
*
|
|
68
|
+
* `meta` sets what the composition IS — its pixel dimensions and background.
|
|
69
|
+
* That is distinct from playback settings like frame rate and seed, which
|
|
70
|
+
* are chosen later at {@link run} / {@link stream}, because the same scene
|
|
71
|
+
* may legitimately be played back at different rates.
|
|
72
|
+
*
|
|
73
|
+
* An optional leading `name` is a display label for pickers and tooling
|
|
74
|
+
* only; it is never read during playback.
|
|
75
|
+
*
|
|
76
|
+
* @param f - The scene body.
|
|
77
|
+
* @param meta - Composition config: `width`, `height`, `backgroundColor`.
|
|
78
|
+
* @defaultValue `meta` — 1920×1080, transparent background
|
|
79
|
+
* @returns An inert {@link Scene}, ready to run, stream, or play.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* A named 500×300 scene on a dark background.
|
|
83
|
+
* ```typescript
|
|
84
|
+
* const scene = Scene.make(
|
|
85
|
+
* "intro",
|
|
86
|
+
* function* () {
|
|
87
|
+
* const dot = yield* Scene.instantiate("Circle", { radius: 20 });
|
|
88
|
+
* yield* dot.pipe(Motion.moveTo({ x: 400 }, "1 second"));
|
|
89
|
+
* },
|
|
90
|
+
* { width: 500, height: 300, backgroundColor: Color.hex("#16161d") },
|
|
91
|
+
* );
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export const make = (first, second, third) => {
|
|
95
|
+
const [name, f, meta] = typeof first === "string"
|
|
96
|
+
? [first, second, third ?? {}]
|
|
97
|
+
: [undefined, first, second ?? {}];
|
|
98
|
+
return makeScene(Effect.scoped(Effect.gen(f)), meta, name);
|
|
15
99
|
};
|
|
16
|
-
|
|
17
|
-
const makeScene = (runnerEffect, annotations, meta) => ({
|
|
100
|
+
const makeScene = (runnerEffect, meta, name) => ({
|
|
18
101
|
[TypeId]: TypeId,
|
|
102
|
+
// THE erasure seam (design D3): loader requirements in R are phantom —
|
|
103
|
+
// authored yields never dereference their tags — so the body is safe to
|
|
104
|
+
// run with only ExcludeLoaders<R>. Guarded by the loader-free-run test.
|
|
19
105
|
runner: runnerEffect,
|
|
20
|
-
|
|
106
|
+
"~resources": undefined,
|
|
107
|
+
...(name !== undefined ? { name } : {}),
|
|
21
108
|
width: meta.width ?? Runner.defaultComp.width,
|
|
22
109
|
height: meta.height ?? Runner.defaultComp.height,
|
|
23
110
|
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),
|
|
26
111
|
});
|
|
27
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Create an entity and put it in the scene.
|
|
114
|
+
*
|
|
115
|
+
* @remarks
|
|
116
|
+
* `kind` selects the entity and, with it, the exact props allowed — asking
|
|
117
|
+
* for a `"Circle"` gets you `radius`, a `"Text"` gets `text` and
|
|
118
|
+
* `fontSize`. Everything is optional and defaulted, so `instantiate("Circle",
|
|
119
|
+
* {})` is a valid white circle at the origin.
|
|
120
|
+
*
|
|
121
|
+
* The entity appears immediately and stays for the rest of the scene. It is
|
|
122
|
+
* mounted under the ambient parent — the root, or the enclosing Group when
|
|
123
|
+
* created inside one.
|
|
124
|
+
*
|
|
125
|
+
* What you get back is a lightweight HANDLE, not the entity's data. It is
|
|
126
|
+
* what animators take, and it stays valid as the data changes underneath;
|
|
127
|
+
* to read the current state use {@link data}. Because the handle is itself
|
|
128
|
+
* pipeable, you can animate straight off the call without binding it first.
|
|
129
|
+
*
|
|
130
|
+
* Containers (`Group`, `Hud`) accept a `children` array that is deliberately
|
|
131
|
+
* permissive: a bare string becomes a Text, an existing handle is adopted,
|
|
132
|
+
* and an un-yielded `instantiate` is resolved for you.
|
|
133
|
+
*
|
|
134
|
+
* @param kind - Which entity: `"Circle"`, `"Rect"`, `"Text"`, `"Line"`,
|
|
135
|
+
* `"Path"`, `"Ellipse"`, `"Group"`, `"Hud"`, `"Image"`, or `"Camera"`.
|
|
136
|
+
* @param props - Initial field values; all optional.
|
|
137
|
+
* @returns A handle to the live entity.
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* A shape, and a Group adopting mixed children.
|
|
141
|
+
* ```typescript
|
|
142
|
+
* const dot = yield* Scene.instantiate("Circle", {
|
|
143
|
+
* position: Entity.vec3({ x: 100, y: 50 }),
|
|
144
|
+
* radius: 20,
|
|
145
|
+
* fillColor: Color.hex("#7f5af0"),
|
|
146
|
+
* });
|
|
147
|
+
*
|
|
148
|
+
* const panel = yield* Scene.instantiate("Group", {
|
|
149
|
+
* children: [
|
|
150
|
+
* "a bare string becomes a Text",
|
|
151
|
+
* Scene.instantiate("Rect", { width: 200, height: 40 }),
|
|
152
|
+
* dot,
|
|
153
|
+
* ],
|
|
154
|
+
* });
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export const instantiate = Effect.fnUntraced(function* (kind, props) {
|
|
28
158
|
const runner = yield* Runner.Runner;
|
|
29
|
-
return yield* runner.instantiate(
|
|
159
|
+
return yield* runner.instantiate(kind, props);
|
|
30
160
|
});
|
|
31
161
|
export const tick = Effect.gen(function* () {
|
|
32
162
|
const runner = yield* Runner.Runner;
|
|
33
163
|
return yield* runner.phaser.arriveAndAwaitAdvance;
|
|
34
164
|
});
|
|
35
165
|
/**
|
|
36
|
-
* Hold the scene for `duration
|
|
37
|
-
*
|
|
38
|
-
*
|
|
166
|
+
* Hold the scene still for `duration`.
|
|
167
|
+
*
|
|
168
|
+
* @remarks
|
|
169
|
+
* `Effect.sleep`'s sibling, but counted in FRAMES at the runner's frame
|
|
170
|
+
* rate rather than read from a clock. That distinction is load-bearing:
|
|
171
|
+
* a wall-clock sleep would produce a different number of frames on a slow
|
|
172
|
+
* machine, and scenes must be reproducible.
|
|
173
|
+
*
|
|
174
|
+
* A zero-length duration is a no-op — unlike an animator, which always
|
|
175
|
+
* consumes at least one frame.
|
|
176
|
+
*
|
|
177
|
+
* Use `Motion.wait` instead when the hold belongs inside an animator chain.
|
|
178
|
+
*
|
|
179
|
+
* @param duration - How long to hold, in scene time.
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* ```typescript
|
|
183
|
+
* yield* Scene.sleep("500 millis");
|
|
184
|
+
* ```
|
|
39
185
|
*/
|
|
40
186
|
export const sleep = (duration) => Effect.gen(function* () {
|
|
41
187
|
const runner = yield* Runner.Runner;
|
|
@@ -44,7 +190,25 @@ export const sleep = (duration) => Effect.gen(function* () {
|
|
|
44
190
|
yield* tick;
|
|
45
191
|
}
|
|
46
192
|
});
|
|
47
|
-
|
|
193
|
+
/**
|
|
194
|
+
* Advance a running scene by exactly one frame.
|
|
195
|
+
*
|
|
196
|
+
* @remarks
|
|
197
|
+
* Returns the frame that was produced, or `null` once the scene is over —
|
|
198
|
+
* which is the signal to stop pulling. Every concurrent branch advances
|
|
199
|
+
* together on each call, which is what keeps concurrency from affecting the
|
|
200
|
+
* frames a scene produces.
|
|
201
|
+
*
|
|
202
|
+
* A scene that runs past its `maxFrames` cap dies here with a message
|
|
203
|
+
* naming the limit, rather than looping forever — the guard against an
|
|
204
|
+
* accidental `Schedule.forever` with nothing to stop it.
|
|
205
|
+
*
|
|
206
|
+
* @param runningScene - The handle from {@link run}.
|
|
207
|
+
* @returns The next frame, or `null` when the scene has ended.
|
|
208
|
+
*/
|
|
209
|
+
// R is never: everything step touches is bound to the runningScene value
|
|
210
|
+
// (runner instance, phaser, fiber) — no ambient service is read
|
|
211
|
+
export const step = Effect.fnUntraced(function* (runningScene) {
|
|
48
212
|
// done: the scene fiber ended. awaitedCount === 0: the body and every
|
|
49
213
|
// fork completed — the scene is over even if its fiber is still
|
|
50
214
|
// winding down through finalizers. Deciding here, from synchronous
|
|
@@ -75,10 +239,50 @@ export const step = (runningScene) => Effect.gen(function* () {
|
|
|
75
239
|
if (runningScene.framesDelivered >= maxFrames) {
|
|
76
240
|
return yield* Effect.die(new Error(`Scene exceeded maxFrames (${maxFrames}). Raise the maxFrames setting, or set maxFrames: Infinity for an intentionally infinite scene.`));
|
|
77
241
|
}
|
|
78
|
-
|
|
242
|
+
// The end-check above reads bookkeeping that the scene fiber writes, and
|
|
243
|
+
// on the very first step that fiber has not been scheduled yet: the body
|
|
244
|
+
// may still spawn branches and finish before producing anything. A body
|
|
245
|
+
// whose only statement is `Scene.background` does exactly that — it
|
|
246
|
+
// registers a phaser party (synchronously, in Phaser.run) and returns, so
|
|
247
|
+
// the advance we are about to await can never complete: the root has
|
|
248
|
+
// deregistered and the background will never arrive. Racing the scene
|
|
249
|
+
// fiber closes that window without teaching the phaser about branch kinds.
|
|
250
|
+
// After the first advance the fiber is running and the race is already
|
|
251
|
+
// settled, so this costs nothing on the hot path.
|
|
252
|
+
const advanced = yield* Effect.raceFirst(runningScene.runner.phaser.awaitAdvance.pipe(Effect.as(true)), Fiber.await(runningScene.fiber).pipe(Effect.as(false)));
|
|
253
|
+
if (!advanced) {
|
|
254
|
+
// the scene ended before this frame could advance — re-run the end
|
|
255
|
+
// path, which owns failure propagation and background teardown
|
|
256
|
+
return yield* step(runningScene);
|
|
257
|
+
}
|
|
79
258
|
runningScene.framesDelivered++;
|
|
80
|
-
return
|
|
259
|
+
return yield* runningScene.runner.state;
|
|
81
260
|
});
|
|
261
|
+
/**
|
|
262
|
+
* Start a scene and hand back a handle for advancing it manually.
|
|
263
|
+
*
|
|
264
|
+
* @remarks
|
|
265
|
+
* The low-level entry point, for drivers that need to own the frame loop —
|
|
266
|
+
* an exporter writing files, or a player synchronizing to its own clock.
|
|
267
|
+
* Starting a scene does not produce any frames; pair this with {@link step}
|
|
268
|
+
* to pull them one at a time.
|
|
269
|
+
*
|
|
270
|
+
* Most code wants {@link stream} instead, which wraps exactly this pairing
|
|
271
|
+
* in a stream.
|
|
272
|
+
*
|
|
273
|
+
* @param scene - The scene to start.
|
|
274
|
+
* @param settings - Playback settings.
|
|
275
|
+
* @returns A running-scene handle to pass to {@link step}.
|
|
276
|
+
*
|
|
277
|
+
* @example
|
|
278
|
+
* ```typescript
|
|
279
|
+
* const running = yield* Scene.run(scene, { frameRate: 30 });
|
|
280
|
+
* let frame = yield* Scene.step(running);
|
|
281
|
+
* while (frame !== null) {
|
|
282
|
+
* frame = yield* Scene.step(running);
|
|
283
|
+
* }
|
|
284
|
+
* ```
|
|
285
|
+
*/
|
|
82
286
|
export const run = (scene, settings = {}) => Effect.gen(function* () {
|
|
83
287
|
// the runner inherits the ROOT scene's composition config
|
|
84
288
|
const runner = yield* Runner.Runner.make(settings, {
|
|
@@ -154,11 +358,67 @@ export const run = (scene, settings = {}) => Effect.gen(function* () {
|
|
|
154
358
|
};
|
|
155
359
|
return runningScene;
|
|
156
360
|
});
|
|
361
|
+
/**
|
|
362
|
+
* Play a scene and get its frames as a lazy stream — the usual way to
|
|
363
|
+
* consume one.
|
|
364
|
+
*
|
|
365
|
+
* @remarks
|
|
366
|
+
* Frames are produced on demand, so a player can pull at its own pace and a
|
|
367
|
+
* long scene never has to be materialized all at once. The stream ends when
|
|
368
|
+
* the scene does.
|
|
369
|
+
*
|
|
370
|
+
* `settings` is where playback choices live — `frameRate`, `seed`,
|
|
371
|
+
* `maxFrames` — as opposed to what the composition IS (its size and
|
|
372
|
+
* background), which was fixed at {@link make}. The same scene can therefore
|
|
373
|
+
* be streamed at 30fps for a preview and 60fps for a final render without
|
|
374
|
+
* being rewritten.
|
|
375
|
+
*
|
|
376
|
+
* Note the frame count depends on the frame rate: a one-second animation is
|
|
377
|
+
* 30 frames at 30fps and 60 at 60fps, plus a final resting frame.
|
|
378
|
+
*
|
|
379
|
+
* @param scene - The scene to play.
|
|
380
|
+
* @param settings - Playback settings.
|
|
381
|
+
* @defaultValue `frameRate` 60, `seed` `"effect-motion"`, `maxFrames` 36_000
|
|
382
|
+
* @returns A stream of frames.
|
|
383
|
+
*
|
|
384
|
+
* @example
|
|
385
|
+
* Collect every frame of a scene at 30fps.
|
|
386
|
+
* ```typescript
|
|
387
|
+
* const frames = yield* Scene.stream(scene, { frameRate: 30 }).pipe(
|
|
388
|
+
* Stream.runCollect,
|
|
389
|
+
* );
|
|
390
|
+
* ```
|
|
391
|
+
*/
|
|
157
392
|
export const stream = (scene, settings = {}) => run(scene, settings).pipe(Effect.map((runningScene) => Stream.fromEffectRepeat(step(runningScene)).pipe(
|
|
158
393
|
// refinement: the stream ends at the first null, so the
|
|
159
394
|
// element type is Frame<Entities>, not Frame | null
|
|
160
395
|
Stream.takeWhile((state) => state !== null))), Stream.unwrap);
|
|
161
396
|
const isUpdaterFn = (props) => typeof props === "function";
|
|
397
|
+
/**
|
|
398
|
+
* Read an entity's current data.
|
|
399
|
+
*
|
|
400
|
+
* @remarks
|
|
401
|
+
* An {@link Instance} is only a handle, so this is how you get at the live
|
|
402
|
+
* values behind it — to branch on where something is, or to compute a target
|
|
403
|
+
* relative to its current state.
|
|
404
|
+
*
|
|
405
|
+
* The result is a snapshot for THIS frame, not a live view; read again on a
|
|
406
|
+
* later frame to see later values. The returned type is narrowed by the
|
|
407
|
+
* handle's kind, so a Circle's `radius` is available without casting.
|
|
408
|
+
*
|
|
409
|
+
* Reading a destroyed entity is a loud defect rather than a silent
|
|
410
|
+
* `undefined`.
|
|
411
|
+
*
|
|
412
|
+
* @param instance - Handle to read.
|
|
413
|
+
* @returns The entity's data as of this frame.
|
|
414
|
+
* @see {@link update} to write it.
|
|
415
|
+
*
|
|
416
|
+
* @example
|
|
417
|
+
* ```typescript
|
|
418
|
+
* const { position, radius } = yield* Scene.data(dot);
|
|
419
|
+
* yield* dot.pipe(Motion.moveTo({ x: position.x + radius * 4 }, "1 second"));
|
|
420
|
+
* ```
|
|
421
|
+
*/
|
|
162
422
|
export const data = (instance) => Effect.gen(function* () {
|
|
163
423
|
const runner = yield* Runner.Runner;
|
|
164
424
|
const current = runner.getDataUnsafe(instance);
|
|
@@ -167,6 +427,34 @@ export const data = (instance) => Effect.gen(function* () {
|
|
|
167
427
|
}
|
|
168
428
|
return current;
|
|
169
429
|
});
|
|
430
|
+
/**
|
|
431
|
+
* Set an entity's data immediately, with no animation.
|
|
432
|
+
*
|
|
433
|
+
* @remarks
|
|
434
|
+
* A hard cut on the current frame — the counterpart to the animators, which
|
|
435
|
+
* interpolate. Use it to set something up before animating (jolt the camera,
|
|
436
|
+
* then spring it back), or to change a field no animator covers, like
|
|
437
|
+
* `text`, `visible`, or a Path's `commands`.
|
|
438
|
+
*
|
|
439
|
+
* Pass an object to replace the data, or a function to derive it from the
|
|
440
|
+
* current values — the function form is preferred, since it reads and writes
|
|
441
|
+
* atomically.
|
|
442
|
+
*
|
|
443
|
+
* Updating a destroyed entity is a no-op returning `false`, not an error.
|
|
444
|
+
*
|
|
445
|
+
* @param instance - Handle to update.
|
|
446
|
+
* @param props - New data, or `(current) => next`.
|
|
447
|
+
*
|
|
448
|
+
* @example
|
|
449
|
+
* Retitle a label and jolt the camera, both on this frame.
|
|
450
|
+
* ```typescript
|
|
451
|
+
* yield* Scene.update(label, (d) => ({ ...d, text: "done" }));
|
|
452
|
+
* yield* Scene.update(camera, (d) => ({
|
|
453
|
+
* ...d,
|
|
454
|
+
* position: Entity.vec3({ ...d.position, x: 22 }),
|
|
455
|
+
* }));
|
|
456
|
+
* ```
|
|
457
|
+
*/
|
|
170
458
|
export const update = (instance, props) => Effect.gen(function* () {
|
|
171
459
|
const runner = yield* Runner.Runner;
|
|
172
460
|
if (isUpdaterFn(props)) {
|
|
@@ -204,15 +492,30 @@ export const comp = Effect.fnUntraced(function* () {
|
|
|
204
492
|
return runner.comp;
|
|
205
493
|
});
|
|
206
494
|
/**
|
|
207
|
-
* The active camera
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
* =
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
495
|
+
* The active camera, as an ordinary animatable instance.
|
|
496
|
+
*
|
|
497
|
+
* @remarks
|
|
498
|
+
* There is always a camera — a resting one is present from the first frame,
|
|
499
|
+
* placed so that content at `z = 0` renders exactly as flat 2D. A scene that
|
|
500
|
+
* never touches the camera looks like a plain 2D scene, and reaching for
|
|
501
|
+
* `Scene.camera` is how you opt into depth.
|
|
502
|
+
*
|
|
503
|
+
* It is a normal instance, so every animator drives it with no special
|
|
504
|
+
* vocabulary: `moveTo` flies it (including along `z` to push in or pull
|
|
505
|
+
* back), `tweenTo` on `focalLength` changes the lens, springs and forks work
|
|
506
|
+
* as they do anywhere. `Camera` helpers add aiming on top.
|
|
507
|
+
*
|
|
508
|
+
* The camera is view state and is never itself drawn.
|
|
509
|
+
*
|
|
510
|
+
* @returns A handle to the active camera.
|
|
511
|
+
* @see {@link setCamera} to swap in a different one.
|
|
512
|
+
*
|
|
513
|
+
* @example
|
|
514
|
+
* Push the camera in, revealing depth in the scene.
|
|
515
|
+
* ```typescript
|
|
516
|
+
* const camera = yield* Scene.camera;
|
|
517
|
+
* yield* camera.pipe(Motion.moveTo({ z: -300 }, "1200 millis", "easeInOutCubic"));
|
|
518
|
+
* ```
|
|
216
519
|
*/
|
|
217
520
|
export const camera = Effect.gen(function* () {
|
|
218
521
|
const runner = yield* Runner.Runner;
|
|
@@ -257,13 +560,36 @@ const makeBranch = (runner, kind) => {
|
|
|
257
560
|
return { entry, finishUnsafe, isFinished: () => finished };
|
|
258
561
|
};
|
|
259
562
|
/**
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
563
|
+
* Declare the current branch semantically over, while its code keeps
|
|
564
|
+
* running.
|
|
565
|
+
*
|
|
566
|
+
* @remarks
|
|
567
|
+
* Separates "this is done as far as everyone else is concerned" from "this
|
|
568
|
+
* fiber has stopped". Anyone awaiting the branch's `finished` proceeds
|
|
569
|
+
* immediately, and the branch stops holding the scene open — but code after
|
|
570
|
+
* `finish` keeps running as a TAIL, bounded by the parent exactly like a
|
|
571
|
+
* {@link background}.
|
|
572
|
+
*
|
|
573
|
+
* The use is a beat that should hand off early: an entrance whose successor
|
|
574
|
+
* starts as soon as the element has landed, while a slow ring-out continues
|
|
575
|
+
* underneath. Without `finish`, the successor would wait for the tail.
|
|
576
|
+
*
|
|
577
|
+
* Idempotent, and completion implies finish. Note that a failure in the tail
|
|
578
|
+
* is NOT reported — by then nothing is listening.
|
|
579
|
+
*
|
|
580
|
+
* Calling it outside a running scene is a loud defect.
|
|
581
|
+
*
|
|
582
|
+
* @example
|
|
583
|
+
* Hand off after the landing; the wobble plays on borrowed time.
|
|
584
|
+
* ```typescript
|
|
585
|
+
* yield* Scene.fork(
|
|
586
|
+
* Effect.gen(function* () {
|
|
587
|
+
* yield* badge.pipe(Motion.moveTo({ y: 100 }, "400 millis"));
|
|
588
|
+
* yield* Scene.finish;
|
|
589
|
+
* yield* badge.pipe(Physics.springTo({ y: 96 }, "bounce"));
|
|
590
|
+
* }),
|
|
591
|
+
* );
|
|
592
|
+
* ```
|
|
267
593
|
*/
|
|
268
594
|
export const finish = Effect.gen(function* () {
|
|
269
595
|
const branch = yield* currentBranch();
|
|
@@ -315,13 +641,35 @@ const forkBranch = (runner, effect, kind) => Effect.gen(function* () {
|
|
|
315
641
|
// the phaser's phase counter IS the current frame index
|
|
316
642
|
const frameOf = (runner) => runner.phaser.snapshotUnsafe().phase;
|
|
317
643
|
/**
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
* immediately
|
|
644
|
+
* Play an animation again and again, on a schedule.
|
|
645
|
+
*
|
|
646
|
+
* @remarks
|
|
647
|
+
* `Effect.repeat`'s sibling, paced by FRAMES rather than the wall clock —
|
|
648
|
+
* which is what keeps a looping scene deterministic.
|
|
649
|
+
*
|
|
650
|
+
* The first run happens immediately, and the schedule paces the gaps AFTER
|
|
651
|
+
* each run. So `Schedule.spaced("400 millis")` means "run, rest 400ms, run
|
|
652
|
+
* again", and the loop count comes from the schedule: `Schedule.forever`
|
|
653
|
+
* for ambient motion (usually inside {@link background}), or
|
|
654
|
+
* `Schedule.upTo({ times: 2 })` for a bounded three-run sequence.
|
|
655
|
+
*
|
|
656
|
+
* A failing run fails immediately, without consulting the schedule again.
|
|
657
|
+
*
|
|
658
|
+
* @param effect - The animation to repeat.
|
|
659
|
+
* @param schedule - How often, and how many times.
|
|
660
|
+
* @returns The schedule's final output.
|
|
661
|
+
*
|
|
662
|
+
* @example
|
|
663
|
+
* Three round-trips, resting 400ms between them.
|
|
664
|
+
* ```typescript
|
|
665
|
+
* yield* Scene.repeat(
|
|
666
|
+
* ball.pipe(
|
|
667
|
+
* Motion.moveTo({ x: 430 }, "600 millis", "easeInOutCubic"),
|
|
668
|
+
* Motion.moveTo({ x: 70 }, "600 millis", "easeInOutCubic"),
|
|
669
|
+
* ),
|
|
670
|
+
* Schedule.spaced("400 millis").pipe(Schedule.upTo({ times: 2 })),
|
|
671
|
+
* );
|
|
672
|
+
* ```
|
|
325
673
|
*/
|
|
326
674
|
export const repeat = (effect, schedule) => Effect.gen(function* () {
|
|
327
675
|
const runner = yield* Runner.Runner;
|
|
@@ -339,76 +687,152 @@ export const repeat = (effect, schedule) => Effect.gen(function* () {
|
|
|
339
687
|
}
|
|
340
688
|
});
|
|
341
689
|
/**
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
690
|
+
* Start an animation alongside the rest of the scene and continue
|
|
691
|
+
* immediately, without waiting for it.
|
|
692
|
+
*
|
|
693
|
+
* @remarks
|
|
694
|
+
* Where {@link all} blocks until its branches finish, `fork` returns at
|
|
695
|
+
* once — so the scene body carries on while the forked animation plays.
|
|
696
|
+
* That is what lets independent timelines overlap, and what makes spawning
|
|
697
|
+
* work in a loop possible.
|
|
698
|
+
*
|
|
699
|
+
* Note this INVERTS Effect's own `fork`: the scene's end waits for forked
|
|
700
|
+
* work. A body that returns while forks are still animating keeps producing
|
|
701
|
+
* frames until the last one finishes, so a scene consisting only of a fork
|
|
702
|
+
* still plays in full. For work that should instead be cut off when the
|
|
703
|
+
* scene ends, use {@link background}.
|
|
704
|
+
*
|
|
705
|
+
* The returned handle carries `finished` — yield it to wait for this branch
|
|
706
|
+
* specifically — and `fiber`, to interrupt it early.
|
|
707
|
+
*
|
|
708
|
+
* @param effect - The animation to run alongside.
|
|
709
|
+
* @returns A handle with `finished` and `fiber`.
|
|
710
|
+
* @see {@link background} for work bounded by the scene's end.
|
|
711
|
+
*
|
|
712
|
+
* @example
|
|
713
|
+
* Spawn overlapping dots; the scene lives until the last one has faded.
|
|
714
|
+
* ```typescript
|
|
715
|
+
* yield* Scene.repeat(
|
|
716
|
+
* Scene.fork(
|
|
717
|
+
* Effect.gen(function* () {
|
|
718
|
+
* const dot = yield* Scene.instantiate("Circle", { radius: 8 });
|
|
719
|
+
* yield* dot.pipe(
|
|
720
|
+
* Motion.moveTo({ x: 440 }, "1200 millis"),
|
|
721
|
+
* Motion.fadeTo(0, "300 millis"),
|
|
722
|
+
* );
|
|
723
|
+
* }),
|
|
724
|
+
* ),
|
|
725
|
+
* Schedule.fixed("200 millis").pipe(Schedule.upTo({ times: 5 })),
|
|
726
|
+
* );
|
|
727
|
+
* ```
|
|
352
728
|
*/
|
|
353
729
|
export const fork = (effect) => Effect.gen(function* () {
|
|
354
730
|
const runner = yield* Runner.Runner;
|
|
355
731
|
return yield* forkBranch(runner, effect, "fork");
|
|
356
732
|
});
|
|
357
733
|
/**
|
|
358
|
-
* Like {@link fork}, but the
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
734
|
+
* Like {@link fork}, but the animation is CUT OFF at scene end rather than
|
|
735
|
+
* awaited.
|
|
736
|
+
*
|
|
737
|
+
* @remarks
|
|
738
|
+
* For ambient motion that should play for as long as the scene lasts
|
|
739
|
+
* without deciding how long that is — a pulsing indicator, a drifting
|
|
740
|
+
* backdrop, anything paired with `Schedule.forever`. A background never
|
|
741
|
+
* holds the scene open, so the scene's real content governs its length and
|
|
742
|
+
* the ambient loop simply stops when everything else is done.
|
|
743
|
+
*
|
|
744
|
+
* "Scene end" includes the fork drain: backgrounds keep animating while
|
|
745
|
+
* awaited forks finish, and are stopped only after the last one.
|
|
746
|
+
*
|
|
747
|
+
* Because backgrounds do not keep a scene alive, a body that spawns only
|
|
748
|
+
* backgrounds ends immediately and produces NO frames — the background is
|
|
749
|
+
* not content, so there is nothing to give the scene a length. Pair one
|
|
750
|
+
* with something that does define the length, whether a real animation or
|
|
751
|
+
* an explicit {@link sleep}, or the ambient motion never gets a frame to
|
|
752
|
+
* play on.
|
|
753
|
+
*
|
|
754
|
+
* @param effect - The ambient animation.
|
|
755
|
+
* @returns A handle with `finished` and `fiber`.
|
|
756
|
+
*
|
|
757
|
+
* @example
|
|
758
|
+
* A pulse that runs the whole scene, with the scene's length set by the
|
|
759
|
+
* animation after it.
|
|
760
|
+
* ```typescript
|
|
761
|
+
* yield* Scene.background(
|
|
762
|
+
* Scene.repeat(
|
|
763
|
+
* pulse.pipe(
|
|
764
|
+
* Motion.tweenTo({ radius: 24 }, "400 millis"),
|
|
765
|
+
* Motion.tweenTo({ radius: 10 }, "400 millis"),
|
|
766
|
+
* ),
|
|
767
|
+
* Schedule.forever,
|
|
768
|
+
* ),
|
|
769
|
+
* );
|
|
770
|
+
* yield* title.pipe(Motion.moveTo({ y: 100 }, "2 seconds"));
|
|
771
|
+
* ```
|
|
363
772
|
*/
|
|
364
773
|
export const background = (effect) => Effect.gen(function* () {
|
|
365
774
|
const runner = yield* Runner.Runner;
|
|
366
775
|
return yield* forkBranch(runner, effect, "background");
|
|
367
776
|
});
|
|
368
777
|
/**
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
*
|
|
381
|
-
*
|
|
778
|
+
* Nest a whole scene inside the current one — the precomp.
|
|
779
|
+
*
|
|
780
|
+
* @remarks
|
|
781
|
+
* The door to composing scenes rather than writing one flat timeline. A
|
|
782
|
+
* played scene is authored and tested independently, then dropped into a
|
|
783
|
+
* larger one as a unit: an intro built alone becomes the first beat of a
|
|
784
|
+
* longer piece without edits.
|
|
785
|
+
*
|
|
786
|
+
* The child mounts under an implicit group carrying its OWN bounds. Content
|
|
787
|
+
* clips to them, a non-transparent background paints within them, and the
|
|
788
|
+
* group is placed so those bounds sit centered in the enclosing composition
|
|
789
|
+
* — so a child smaller or larger than its parent still lands sensibly. The
|
|
790
|
+
* handle's `group` is that mount point: move or fade it to transform the
|
|
791
|
+
* entire nested scene as one object.
|
|
792
|
+
*
|
|
793
|
+
* The child shares the movie's frame clock but gets a FRESH seeded random
|
|
794
|
+
* stream, so a nested scene animates exactly as it did standalone under the
|
|
795
|
+
* same seed — nesting never perturbs a child's randomness.
|
|
796
|
+
*
|
|
797
|
+
* Awaited like a {@link fork}: yield `handle.finished` to play children in
|
|
798
|
+
* sequence, or skip the await to run them concurrently.
|
|
799
|
+
*
|
|
800
|
+
* @param scene - The scene to nest.
|
|
801
|
+
* @param options - `parent` to mount elsewhere, `seed` to vary this
|
|
802
|
+
* evaluation.
|
|
803
|
+
* @returns A handle with `finished`, `fiber`, and the mount `group`.
|
|
804
|
+
*
|
|
805
|
+
* @example
|
|
806
|
+
* Play one scene, then another, and fade the second out as a whole.
|
|
807
|
+
* ```typescript
|
|
808
|
+
* const intro = yield* Scene.play(introScene);
|
|
809
|
+
* yield* intro.finished;
|
|
810
|
+
*
|
|
811
|
+
* const outro = yield* Scene.play(outroScene);
|
|
812
|
+
* yield* outro.group.pipe(Motion.fadeTo(0, "500 millis"));
|
|
813
|
+
* ```
|
|
382
814
|
*/
|
|
383
815
|
export const play = (scene, options) => Effect.gen(function* () {
|
|
384
816
|
const runner = yield* Runner.Runner;
|
|
385
|
-
// default placement:
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
// group has no bounds to center in; the child mounts at its origin.
|
|
817
|
+
// default placement: comps are center-anchored in the center-origin
|
|
818
|
+
// frame, so "centered in the enclosing comp" is position (0, 0) by
|
|
819
|
+
// construction — no bounds arithmetic needed.
|
|
389
820
|
const ambient = options?.parent ?? (yield* Runner.CurrentParent);
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
|
821
|
+
// A mounted scene is a render-to-texture boundary: the renderer clips
|
|
822
|
+
// its subtree to the child's bounds and paints the child's background
|
|
823
|
+
// within them. Those bounds are the SCENE's, so they are registered
|
|
824
|
+
// against the mount group's id rather than copied onto it as fields —
|
|
825
|
+
// a Group that happens to carry a size is not what makes a comp.
|
|
403
826
|
const group = yield* runner
|
|
404
|
-
.instantiate(Group, {
|
|
405
|
-
|
|
406
|
-
|
|
827
|
+
.instantiate("Group", {
|
|
828
|
+
position: Entity.vec3({}),
|
|
829
|
+
})
|
|
830
|
+
.pipe(Effect.provideService(Runner.CurrentParent, ambient));
|
|
831
|
+
runner.registerComp(group.id, {
|
|
407
832
|
width: scene.width,
|
|
408
833
|
height: scene.height,
|
|
409
834
|
backgroundColor: scene.backgroundColor,
|
|
410
|
-
})
|
|
411
|
-
.pipe(Effect.provideService(Runner.CurrentParent, ambient));
|
|
835
|
+
});
|
|
412
836
|
const body = scene.runner.pipe(Effect.scoped,
|
|
413
837
|
// the child's instances mount under its bounds group
|
|
414
838
|
Effect.provideService(Runner.CurrentParent, group),
|
|
@@ -420,10 +844,30 @@ export const play = (scene, options) => Effect.gen(function* () {
|
|
|
420
844
|
return { ...handle, group };
|
|
421
845
|
});
|
|
422
846
|
/**
|
|
423
|
-
* Run
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
847
|
+
* Run animations simultaneously, and resolve when the last one finishes.
|
|
848
|
+
*
|
|
849
|
+
* @remarks
|
|
850
|
+
* The everyday way to make things move at once. Every branch advances
|
|
851
|
+
* exactly one frame per tick in lockstep, so two one-second animations run
|
|
852
|
+
* as one second of frames — not two.
|
|
853
|
+
*
|
|
854
|
+
* Branches need not be the same length; `all` waits for the slowest. This
|
|
855
|
+
* is also the idiom for synchronizing springs, whose durations are emergent
|
|
856
|
+
* and unknown up front.
|
|
857
|
+
*
|
|
858
|
+
* There is deliberately no schedule parameter: pacing a list one-at-a-time
|
|
859
|
+
* is {@link chain}, and overlapping starts is {@link stagger}.
|
|
860
|
+
*
|
|
861
|
+
* @param effects - The animations to run together.
|
|
862
|
+
*
|
|
863
|
+
* @example
|
|
864
|
+
* A dot slides while the camera pushes in — one second of frames total.
|
|
865
|
+
* ```typescript
|
|
866
|
+
* yield* Scene.all([
|
|
867
|
+
* dot.pipe(Motion.moveTo({ x: 400 }, "1 second")),
|
|
868
|
+
* camera.pipe(Motion.moveTo({ z: -300 }, "1 second")),
|
|
869
|
+
* ]);
|
|
870
|
+
* ```
|
|
427
871
|
*/
|
|
428
872
|
export const all = Effect.fnUntraced(function* (effects) {
|
|
429
873
|
const runner = yield* Runner.Runner;
|
|
@@ -431,16 +875,34 @@ export const all = Effect.fnUntraced(function* (effects) {
|
|
|
431
875
|
return yield* Phaser.all(effects).pipe(Effect.provideService(Phaser.Phaser, runner.phaser));
|
|
432
876
|
});
|
|
433
877
|
/**
|
|
434
|
-
* Run
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
878
|
+
* Run animations one at a time, in order, optionally resting between them.
|
|
879
|
+
*
|
|
880
|
+
* @remarks
|
|
881
|
+
* Items NEVER overlap — each begins only after the previous one has fully
|
|
882
|
+
* finished. That guarantee is the difference between this and
|
|
883
|
+
* {@link stagger}, and it holds no matter what schedule you pass.
|
|
884
|
+
*
|
|
885
|
+
* Without a schedule this is plain sequencing, equivalent to yielding each
|
|
886
|
+
* item in turn but composable as a list. With one, the schedule paces the
|
|
887
|
+
* GAPS after each item: `Schedule.spaced("400 millis")` rests 400ms between
|
|
888
|
+
* items, while `Schedule.fixed` targets a steady start-to-start cadence.
|
|
889
|
+
*
|
|
890
|
+
* The schedule also decides how many items run: when it ends, the remaining
|
|
891
|
+
* items are skipped. `Schedule.recurs(2)` therefore plays three items — the
|
|
892
|
+
* first, plus two more the schedule released.
|
|
893
|
+
*
|
|
894
|
+
* @param effects - The animations, in order.
|
|
895
|
+
* @param schedule - Optional pacing for the gaps between them.
|
|
896
|
+
* @returns `{ completed }` — how many items actually ran.
|
|
897
|
+
*
|
|
898
|
+
* @example
|
|
899
|
+
* Three shapes flashing in turn, resting 400ms between each.
|
|
900
|
+
* ```typescript
|
|
901
|
+
* const { completed } = yield* Scene.chain(
|
|
902
|
+
* [a, b, c].map((shape) => shape.pipe(Motion.fadeTo(1, "300 millis"))),
|
|
903
|
+
* Schedule.spaced("400 millis"),
|
|
904
|
+
* );
|
|
905
|
+
* ```
|
|
444
906
|
*/
|
|
445
907
|
export const chain = (effects, schedule) => Effect.gen(function* () {
|
|
446
908
|
const list = Array.from(effects);
|
|
@@ -468,14 +930,36 @@ export const chain = (effects, schedule) => Effect.gen(function* () {
|
|
|
468
930
|
return { completed };
|
|
469
931
|
});
|
|
470
932
|
/**
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
*
|
|
933
|
+
* Start animations one after another WITHOUT waiting for each to finish —
|
|
934
|
+
* the cascade.
|
|
935
|
+
*
|
|
936
|
+
* @remarks
|
|
937
|
+
* The first starts immediately and each next one on the schedule's next
|
|
938
|
+
* emission, so earlier items are still running when later ones begin. That
|
|
939
|
+
* overlap is the entire point, and the difference from {@link chain}: use
|
|
940
|
+
* `stagger` for a ripple across many elements, `chain` when items must not
|
|
941
|
+
* coincide.
|
|
942
|
+
*
|
|
943
|
+
* The schedule paces the STARTS here, not the gaps. Resolution waits for
|
|
944
|
+
* every released animation to finish, not merely for the last one to be
|
|
945
|
+
* released — so the whole cascade is complete when this returns.
|
|
946
|
+
*
|
|
947
|
+
* When the schedule ends before the list does, the remaining effects are
|
|
948
|
+
* skipped.
|
|
949
|
+
*
|
|
950
|
+
* @param effects - The animations to release in order.
|
|
951
|
+
* @param schedule - When to release each subsequent one.
|
|
952
|
+
* @returns `{ released }` — how many actually started.
|
|
953
|
+
*
|
|
954
|
+
* @example
|
|
955
|
+
* A row of bars rising in a ripple, each starting 80ms after the last while
|
|
956
|
+
* the earlier ones keep going.
|
|
957
|
+
* ```typescript
|
|
958
|
+
* yield* Scene.stagger(
|
|
959
|
+
* bars.map((bar) => bar.pipe(Motion.moveTo({ y: 40 }, "600 millis"))),
|
|
960
|
+
* Schedule.spaced("80 millis"),
|
|
961
|
+
* );
|
|
962
|
+
* ```
|
|
479
963
|
*/
|
|
480
964
|
export const stagger = (effects, schedule) => Effect.gen(function* () {
|
|
481
965
|
const list = Array.from(effects);
|