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/Motion.js
CHANGED
|
@@ -49,7 +49,7 @@ const interpolate = Effect.fnUntraced(function* (from, to, duration, fn, timing
|
|
|
49
49
|
yield* Scene.tick;
|
|
50
50
|
}
|
|
51
51
|
});
|
|
52
|
-
// InterpolableOnly of an opaque
|
|
52
|
+
// InterpolableOnly of an opaque Entity.EntityByTag<Tag> can't be proven
|
|
53
53
|
// index-compatible with Record<string, number>; the runtime shape is
|
|
54
54
|
// guaranteed by the Target type, so cast once here.
|
|
55
55
|
export const resolveTarget = (target, current) => (typeof target === "function"
|
|
@@ -71,12 +71,12 @@ export const startValues = (current, target, explicitFrom) => {
|
|
|
71
71
|
return start;
|
|
72
72
|
};
|
|
73
73
|
const animate = Effect.fnUntraced(function* (instanceOrEffect, from, to, duration, timing) {
|
|
74
|
-
const instance = yield* Instance.
|
|
74
|
+
const instance = yield* Instance.flattenInstance(instanceOrEffect);
|
|
75
75
|
const current = yield* Scene.data(instance);
|
|
76
76
|
const target = resolveTarget(to, current);
|
|
77
77
|
const start = startValues(current, target, from === undefined ? {} : resolveTarget(from, current));
|
|
78
78
|
yield* interpolate(start, target, duration, (value) =>
|
|
79
|
-
//
|
|
79
|
+
// Entity.EntityByTag<Tag> is opaque to TS, so spread is disallowed — assign + cast
|
|
80
80
|
Scene.update(instance, (data) => Object.assign({}, data, value)), timing);
|
|
81
81
|
return instance;
|
|
82
82
|
});
|
|
@@ -84,26 +84,87 @@ const animate = Effect.fnUntraced(function* (instanceOrEffect, from, to, duratio
|
|
|
84
84
|
// `timing` makes call arity ambiguous between the two forms
|
|
85
85
|
const firstArgIsInstance = (args) => Instance.isInstance(args[0]);
|
|
86
86
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
87
|
+
* Interpolate numeric fields of an instance to `to` over `duration`,
|
|
88
|
+
* starting from whatever they currently are.
|
|
89
|
+
*
|
|
90
|
+
* @remarks
|
|
91
|
+
* This is the RAW layer: fields are addressed by their schema name, so
|
|
92
|
+
* `tweenTo` reaches everything the semantic animators don't cover —
|
|
93
|
+
* `radius`, `width`, `height`, `fontSize`, `strokeWidth`, a Camera's
|
|
94
|
+
* `focalLength`. Prefer {@link moveTo} / {@link fadeTo} where they apply;
|
|
95
|
+
* they move an entity as a unit, which a per-field tween cannot.
|
|
96
|
+
*
|
|
97
|
+
* Only the fields named in `to` are touched; the rest are left alone.
|
|
98
|
+
* `to` may also be a function of the current data, for a target computed
|
|
99
|
+
* at animation start. Colors are interpolable too, mixed through Lab by
|
|
100
|
+
* default.
|
|
101
|
+
*
|
|
102
|
+
* The final frame receives exactly `to` (for any easing with f(1) = 1), so
|
|
103
|
+
* a chain of tweens never accumulates drift. A field that has no current
|
|
104
|
+
* value — an optional one never set — is a loud defect rather than a
|
|
105
|
+
* silent NaN; pass an explicit origin with {@link tween} instead.
|
|
106
|
+
*
|
|
107
|
+
* @param to - The fields to animate and their target values.
|
|
108
|
+
* @param duration - How long, in scene time; rounded to whole frames at
|
|
109
|
+
* the runner's frame rate. A zero duration still takes one frame.
|
|
110
|
+
* @param timing - An easing name from `Timing.timingFunctions`, or your own
|
|
111
|
+
* `(t: number) => number`.
|
|
112
|
+
* @defaultValue `timing` — `"linear"`
|
|
113
|
+
* @returns The instance, so animators chain.
|
|
114
|
+
* @see {@link tween} to specify the starting value explicitly.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* Grow a circle's radius, then shrink it back with a different curve.
|
|
118
|
+
* ```typescript
|
|
119
|
+
* yield* dot.pipe(
|
|
120
|
+
* Motion.tweenTo({ radius: 40 }, "600 millis", "easeOutBack"),
|
|
121
|
+
* Motion.tweenTo({ radius: 4 }, "400 millis", "easeInQuad"),
|
|
122
|
+
* );
|
|
123
|
+
* ```
|
|
93
124
|
*/
|
|
94
125
|
export const tweenTo = dual(firstArgIsInstance, (instance, to, duration, timing) => animate(instance, undefined, to, duration, timing));
|
|
95
126
|
/**
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* cannot express
|
|
102
|
-
*
|
|
103
|
-
*
|
|
127
|
+
* The escape hatch: run `fn` once per frame with the eased progress and the
|
|
128
|
+
* entity's current data, and use whatever it returns as the new data.
|
|
129
|
+
*
|
|
130
|
+
* @remarks
|
|
131
|
+
* Reach for `drive` when fields must move TOGETHER in a way independent
|
|
132
|
+
* tweens cannot express. Tweening `x` and `y` separately gives you a
|
|
133
|
+
* straight line; a circular orbit needs both derived from one angle, and
|
|
134
|
+
* that coupling is what this provides. It is the primitive `Camera.orbit`
|
|
135
|
+
* and `Camera.dolly` are themselves built on.
|
|
136
|
+
*
|
|
137
|
+
* `fn` must be pure — it receives only `(t, data)` and returns new data, so
|
|
138
|
+
* a scene stays reproducible frame-for-frame. Reading a clock or a random
|
|
139
|
+
* number here is what breaks determinism. `t` is the EASED parameter, so a
|
|
140
|
+
* non-linear `timing` reshapes the pacing without `fn` knowing.
|
|
141
|
+
*
|
|
142
|
+
* The final frame receives exactly `t = 1` for any easing with f(1) = 1, so
|
|
143
|
+
* the motion lands precisely on whatever `fn(1, …)` computes.
|
|
144
|
+
*
|
|
145
|
+
* @param duration - How long, in scene time.
|
|
146
|
+
* @param timing - An easing name or function; applied to `t` before `fn`
|
|
147
|
+
* sees it.
|
|
148
|
+
* @param fn - Pure `(t, data) => data`, called once per frame.
|
|
149
|
+
* @returns The instance, so animators chain.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* Sweep a dot around a circle — one angle driving both axes, which two
|
|
153
|
+
* independent tweens could not do.
|
|
154
|
+
* ```typescript
|
|
155
|
+
* yield* dot.pipe(
|
|
156
|
+
* Motion.drive("2 seconds", "linear", (t, data) => ({
|
|
157
|
+
* ...data,
|
|
158
|
+
* position: Entity.vec3({
|
|
159
|
+
* x: 250 + Math.cos(t * 2 * Math.PI) * 120,
|
|
160
|
+
* y: 150 + Math.sin(t * 2 * Math.PI) * 120,
|
|
161
|
+
* }),
|
|
162
|
+
* })),
|
|
163
|
+
* );
|
|
164
|
+
* ```
|
|
104
165
|
*/
|
|
105
166
|
export const drive = dual(firstArgIsInstance, Effect.fnUntraced(function* (instanceOrEffect, duration, timing, fn) {
|
|
106
|
-
const instance = yield* Instance.
|
|
167
|
+
const instance = yield* Instance.flattenInstance(instanceOrEffect);
|
|
107
168
|
const runner = yield* Runner.Runner;
|
|
108
169
|
const timingFn = Timing.resolve(timing);
|
|
109
170
|
const frames = Math.max(1, Time.toFrames(duration, runner.settings.frameRate));
|
|
@@ -115,56 +176,227 @@ export const drive = dual(firstArgIsInstance, Effect.fnUntraced(function* (insta
|
|
|
115
176
|
return instance;
|
|
116
177
|
}));
|
|
117
178
|
/**
|
|
118
|
-
* Like
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
179
|
+
* Like {@link tweenTo}, but with an explicit starting value.
|
|
180
|
+
*
|
|
181
|
+
* @remarks
|
|
182
|
+
* Reach for this when the animation should not begin where the entity
|
|
183
|
+
* currently sits — a "flash in from nothing" that must start at 0 no matter
|
|
184
|
+
* what the last animation left behind, or a field with no current value at
|
|
185
|
+
* all (an optional one never set), which {@link tweenTo} rejects as a
|
|
186
|
+
* defect.
|
|
187
|
+
*
|
|
188
|
+
* Keys present in `to` but missing from `from` still start at the current
|
|
189
|
+
* data, so a partial origin is a targeted override rather than an
|
|
190
|
+
* all-or-nothing switch.
|
|
191
|
+
*
|
|
192
|
+
* @param from - Starting values; keys omitted here start at the current data.
|
|
193
|
+
* @param to - Target values — this key set defines what gets animated.
|
|
194
|
+
* @param duration - How long, in scene time.
|
|
195
|
+
* @param timing - An easing name or function.
|
|
196
|
+
* @defaultValue `timing` — `"linear"`
|
|
197
|
+
* @returns The instance, so animators chain.
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* Pop a label in from zero size, regardless of its current `fontSize`.
|
|
201
|
+
* ```typescript
|
|
202
|
+
* yield* label.pipe(
|
|
203
|
+
* Motion.tween({ fontSize: 0 }, { fontSize: 32 }, "400 millis", "easeOutBack"),
|
|
204
|
+
* );
|
|
205
|
+
* ```
|
|
122
206
|
*/
|
|
123
207
|
export const tween = dual(firstArgIsInstance, (instance, from, to, duration, timing) => animate(instance, from, to, duration, timing));
|
|
208
|
+
const readPosition = (data) => data.position;
|
|
209
|
+
/**
|
|
210
|
+
* Structural parameter, not `EntityByTag<Tag>`: `Tag extends Fadeable`
|
|
211
|
+
* guarantees the field, but TypeScript will not distribute that constraint
|
|
212
|
+
* over the union, so asking for the shape directly is both true and simpler
|
|
213
|
+
* than an intersection type that never resolves.
|
|
214
|
+
*/
|
|
215
|
+
const readOpacity = (data) => data.opacity;
|
|
216
|
+
/**
|
|
217
|
+
* Write a position, holding unnamed channels at their current value — the
|
|
218
|
+
* channel-level sparseness rule (design D8) that has to survive `position`
|
|
219
|
+
* becoming a nested Vec3. A partial Vec3 must never reach `interpolate`:
|
|
220
|
+
* lerping an absent channel yields NaN frames.
|
|
221
|
+
*/
|
|
222
|
+
const writePosition = (data, value) => ({
|
|
223
|
+
...data,
|
|
224
|
+
position: Entity.vec3({
|
|
225
|
+
x: value.x ?? data.position.x,
|
|
226
|
+
y: value.y ?? data.position.y,
|
|
227
|
+
z: value.z ?? data.position.z,
|
|
228
|
+
}),
|
|
229
|
+
});
|
|
124
230
|
const animatePosition = Effect.fnUntraced(function* (instanceOrEffect, from, to, duration, timing) {
|
|
125
|
-
const instance = yield* Instance.
|
|
126
|
-
const
|
|
127
|
-
const current = lens.get(yield* Scene.data(instance));
|
|
231
|
+
const instance = yield* Instance.flattenInstance(instanceOrEffect);
|
|
232
|
+
const current = readPosition(yield* Scene.data(instance));
|
|
128
233
|
// partial targets/origins hold the missing axis at its current value
|
|
129
234
|
const target = { ...current, ...to };
|
|
130
235
|
const start = { ...current, ...(from ?? {}) };
|
|
131
|
-
yield* interpolate(start, target, duration, (value) => Scene.update(instance, (data) =>
|
|
236
|
+
yield* interpolate(start, target, duration, (value) => Scene.update(instance, (data) => writePosition(data, value)), timing);
|
|
132
237
|
return instance;
|
|
133
238
|
});
|
|
134
239
|
const animateOpacity = Effect.fnUntraced(function* (instanceOrEffect, from, to, duration, timing) {
|
|
135
|
-
const instance = yield* Instance.
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
yield* interpolate({ opacity: from ?? current }, { opacity: to }, duration, (value) => Scene.update(instance, (data) => lens.set(data, value.opacity)), timing);
|
|
240
|
+
const instance = yield* Instance.flattenInstance(instanceOrEffect);
|
|
241
|
+
const current = readOpacity(yield* Scene.data(instance));
|
|
242
|
+
yield* interpolate({ opacity: from ?? current }, { opacity: to }, duration, (value) => Scene.update(instance, (data) => ({ ...data, opacity: value.opacity })), timing);
|
|
139
243
|
return instance;
|
|
140
244
|
});
|
|
141
245
|
/**
|
|
142
|
-
* Move an instance to a position
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
246
|
+
* Move an instance to a position over `duration`.
|
|
247
|
+
*
|
|
248
|
+
* @remarks
|
|
249
|
+
* The semantic counterpart to tweening x/y/z by hand, and what you should
|
|
250
|
+
* reach for whenever you want something to travel. Because every entity's
|
|
251
|
+
* geometry is expressed RELATIVE to its `position`, one `moveTo` translates
|
|
252
|
+
* the whole shape rigidly: a Line carries both endpoints, a Path its whole
|
|
253
|
+
* command list, a Group its entire subtree. There is no per-entity special
|
|
254
|
+
* casing to remember.
|
|
255
|
+
*
|
|
256
|
+
* Targets are partial — naming only `x` holds `y` and `z` at their current
|
|
257
|
+
* values, which is what makes single-axis motion read cleanly. `z` is depth:
|
|
258
|
+
* moving along it changes an entity's size and parallax under a perspective
|
|
259
|
+
* camera rather than just its sort order.
|
|
260
|
+
*
|
|
261
|
+
* Accepted for any entity that carries a position, which is all of them —
|
|
262
|
+
* the Camera included, so this is also how you fly the camera.
|
|
263
|
+
*
|
|
264
|
+
* @param to - Target position; omitted axes hold their current value.
|
|
265
|
+
* @param duration - How long, in scene time.
|
|
266
|
+
* @param timing - An easing name or function.
|
|
267
|
+
* @defaultValue `timing` — `"linear"`
|
|
268
|
+
* @returns The instance, so animators chain.
|
|
269
|
+
* @see {@link move} to start from an explicit position, and
|
|
270
|
+
* `Physics.springTo` for durationless motion with momentum.
|
|
271
|
+
*
|
|
272
|
+
* @example
|
|
273
|
+
* Slide right, then drop — the second leg holds the x reached by the first.
|
|
274
|
+
* ```typescript
|
|
275
|
+
* yield* box.pipe(
|
|
276
|
+
* Motion.moveTo({ x: 400 }, "1 second", "easeInOutCubic"),
|
|
277
|
+
* Motion.moveTo({ y: 250 }, "600 millis", "easeInQuad"),
|
|
278
|
+
* );
|
|
279
|
+
* ```
|
|
147
280
|
*/
|
|
148
281
|
export const moveTo = dual(firstArgIsInstance, (instance, to, duration, timing) => animatePosition(instance, undefined, to, duration, timing));
|
|
149
|
-
/**
|
|
282
|
+
/**
|
|
283
|
+
* Like {@link moveTo}, but starting from an explicit position.
|
|
284
|
+
*
|
|
285
|
+
* @remarks
|
|
286
|
+
* The entrance animator: an instance can sit at its final position in the
|
|
287
|
+
* scene tree and still fly in from off-screen, because the origin is stated
|
|
288
|
+
* rather than read. Both `from` and `to` are partial, and each fills its
|
|
289
|
+
* missing axes from the current position.
|
|
290
|
+
*
|
|
291
|
+
* @param from - Starting position; omitted axes start at the current value.
|
|
292
|
+
* @param to - Target position; omitted axes hold their current value.
|
|
293
|
+
* @param duration - How long, in scene time.
|
|
294
|
+
* @param timing - An easing name or function.
|
|
295
|
+
* @defaultValue `timing` — `"linear"`
|
|
296
|
+
* @returns The instance, so animators chain.
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* Slide a title in from off the left edge to where it was declared.
|
|
300
|
+
* ```typescript
|
|
301
|
+
* yield* title.pipe(
|
|
302
|
+
* Motion.move({ x: -200 }, { x: 60 }, "700 millis", "easeOutCubic"),
|
|
303
|
+
* );
|
|
304
|
+
* ```
|
|
305
|
+
*/
|
|
150
306
|
export const move = dual(firstArgIsInstance, (instance, from, to, duration, timing) => animatePosition(instance, from, to, duration, timing));
|
|
151
307
|
/**
|
|
152
|
-
* Fade an instance's opacity
|
|
153
|
-
*
|
|
154
|
-
*
|
|
308
|
+
* Fade an instance's `opacity` to `to` over `duration`.
|
|
309
|
+
*
|
|
310
|
+
* @remarks
|
|
311
|
+
* Opacity runs 0 (invisible) to 1 (fully opaque), and applies to the whole
|
|
312
|
+
* subtree: fading a Group fades everything inside it, which is the usual way
|
|
313
|
+
* to dissolve a composed element as one piece.
|
|
314
|
+
*
|
|
315
|
+
* Only entities that actually paint carry `opacity`, so this is statically
|
|
316
|
+
* gated — fading a Camera is a compile error naming the missing field, not a
|
|
317
|
+
* runtime surprise.
|
|
318
|
+
*
|
|
319
|
+
* @param to - Target opacity, 0 to 1.
|
|
320
|
+
* @param duration - How long, in scene time.
|
|
321
|
+
* @param timing - An easing name or function.
|
|
322
|
+
* @defaultValue `timing` — `"linear"`
|
|
323
|
+
* @returns The instance, so animators chain.
|
|
324
|
+
* @see {@link fade} to start from an explicit opacity.
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* Land, hold, then dissolve out.
|
|
328
|
+
* ```typescript
|
|
329
|
+
* yield* card.pipe(
|
|
330
|
+
* Motion.moveTo({ y: 100 }, "500 millis"),
|
|
331
|
+
* Motion.wait("1 second"),
|
|
332
|
+
* Motion.fadeTo(0, "400 millis"),
|
|
333
|
+
* );
|
|
334
|
+
* ```
|
|
155
335
|
*/
|
|
156
336
|
export const fadeTo = dual(firstArgIsInstance, (instance, to, duration, timing) => animateOpacity(instance, undefined, to, duration, timing));
|
|
157
|
-
/**
|
|
337
|
+
/**
|
|
338
|
+
* Like {@link fadeTo}, but starting from an explicit opacity.
|
|
339
|
+
*
|
|
340
|
+
* @remarks
|
|
341
|
+
* The fade-in idiom: entities are born fully opaque, so `fadeTo(1)` alone
|
|
342
|
+
* would be a no-op. Stating the origin is what makes an entrance possible
|
|
343
|
+
* without first writing `opacity: 0` at instantiate time.
|
|
344
|
+
*
|
|
345
|
+
* @param from - Starting opacity, 0 to 1.
|
|
346
|
+
* @param to - Target opacity, 0 to 1.
|
|
347
|
+
* @param duration - How long, in scene time.
|
|
348
|
+
* @param timing - An easing name or function.
|
|
349
|
+
* @defaultValue `timing` — `"linear"`
|
|
350
|
+
* @returns The instance, so animators chain.
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* Fade a caption in from nothing.
|
|
354
|
+
* ```typescript
|
|
355
|
+
* yield* caption.pipe(Motion.fade(0, 1, "500 millis"));
|
|
356
|
+
* ```
|
|
357
|
+
*/
|
|
158
358
|
export const fade = dual(firstArgIsInstance, (instance, from, to, duration, timing) => animateOpacity(instance, from, to, duration, timing));
|
|
159
359
|
/**
|
|
160
|
-
* Hold
|
|
161
|
-
*
|
|
360
|
+
* Hold for `duration` of scene time — the chainable sibling of
|
|
361
|
+
* `Scene.sleep`.
|
|
362
|
+
*
|
|
363
|
+
* @remarks
|
|
364
|
+
* This is frame time, not wall time: `Effect.sleep` would block on a real
|
|
365
|
+
* clock and make the scene non-deterministic, so `wait` advances whole
|
|
366
|
+
* frames at the runner's frame rate instead. The same hold produces the
|
|
367
|
+
* same frame count on every run and on every machine.
|
|
368
|
+
*
|
|
369
|
+
* Its value over `Scene.sleep` is placement — because it doubles as a pipe
|
|
370
|
+
* step, a beat can sit inside an animator chain without breaking it apart
|
|
371
|
+
* into separate statements.
|
|
372
|
+
*
|
|
373
|
+
* As a pipe step it wraps the PRECEDING step and passes its value through,
|
|
374
|
+
* so it sits anywhere in a chain — including first, where the step it wraps
|
|
375
|
+
* is the bare instance handle.
|
|
376
|
+
*
|
|
377
|
+
* @param duration - How long to hold, in scene time.
|
|
378
|
+
*
|
|
379
|
+
* @example
|
|
380
|
+
* A hold between two animations, mid-chain — `card` flows through to the
|
|
381
|
+
* fade.
|
|
382
|
+
* ```typescript
|
|
383
|
+
* yield* card.pipe(
|
|
384
|
+
* Motion.moveTo({ y: 100 }, "500 millis"),
|
|
385
|
+
* Motion.wait("1 second"),
|
|
386
|
+
* Motion.fadeTo(0, "300 millis"),
|
|
387
|
+
* );
|
|
388
|
+
* ```
|
|
162
389
|
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
390
|
+
* @example
|
|
391
|
+
* Or yielded on its own, as a plain pause in the scene body.
|
|
392
|
+
* ```typescript
|
|
393
|
+
* yield* Motion.wait("2 seconds");
|
|
394
|
+
* ```
|
|
166
395
|
*/
|
|
167
|
-
export const wait = (duration) => Object.assign((effect) => Effect.tap(
|
|
396
|
+
export const wait = (duration) => Object.assign((effect) => Effect.tap(
|
|
397
|
+
// an Instance is Pipeable but NOT an Effect, so a bare handle as
|
|
398
|
+
// the opening pipe step must be lifted before tapping
|
|
399
|
+
Instance.flattenInstance(effect), () => Scene.sleep(duration)), Effectable.Prototype({
|
|
168
400
|
label: "Motion.wait",
|
|
169
401
|
evaluate: () => Scene.sleep(duration),
|
|
170
402
|
}));
|
package/dist/Physics.d.ts
CHANGED
|
@@ -1,23 +1,105 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
|
-
import type * as Schema from "effect/Schema";
|
|
3
2
|
import * as Entity from "./Entity.js";
|
|
4
3
|
import * as Instance from "./Instance.js";
|
|
4
|
+
import type * as Motion from "./Motion.js";
|
|
5
5
|
import * as Runner from "./Runner.js";
|
|
6
6
|
/**
|
|
7
|
-
* Physics-based motion:
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* Physics-based motion: animations with no duration, whose length emerges
|
|
8
|
+
* from a simulation rather than being declared.
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* A spring is a damped harmonic oscillator, integrated until both
|
|
12
|
+
* displacement and velocity fall inside a tolerance — then snapped exactly
|
|
13
|
+
* onto the target. You describe the FEEL (mass, stiffness, damping) and the
|
|
14
|
+
* physics decides how long it takes. That is the whole reason springs live
|
|
15
|
+
* here as their own animators rather than as another easing name: an easing
|
|
16
|
+
* curve is a function of a duration you supply, and a spring has no duration
|
|
17
|
+
* to supply.
|
|
18
|
+
*
|
|
19
|
+
* Reach for a spring when motion should feel physical — something landing,
|
|
20
|
+
* settling, or reacting — and for `Motion`'s tweens when you need a beat to
|
|
21
|
+
* take an exact, known amount of time. A springy easing (`easeOutElastic`,
|
|
22
|
+
* `easeOutBounce`) is the middle ground: spring-like shape, fixed duration.
|
|
23
|
+
*
|
|
24
|
+
* Named presets live in {@link springs}; the same call takes a custom
|
|
25
|
+
* `{ mass, stiffness, damping }` when none of them fit.
|
|
26
|
+
*
|
|
27
|
+
* Determinism holds despite the simulation: integration runs at a fixed
|
|
28
|
+
* internal timestep, so a spring produces identical frames at any frame rate
|
|
29
|
+
* and on any machine.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* Three springs chained — each starts where the last settled, and the whole
|
|
33
|
+
* sequence takes however long the physics takes.
|
|
34
|
+
* ```typescript
|
|
35
|
+
* import * as Physics from "effect-motion/Physics";
|
|
36
|
+
* import * as Scene from "effect-motion/Scene";
|
|
37
|
+
*
|
|
38
|
+
* const scene = Scene.make(function* () {
|
|
39
|
+
* const ball = yield* Scene.instantiate("Circle", { radius: 24 });
|
|
40
|
+
* yield* ball.pipe(
|
|
41
|
+
* Physics.springTo({ x: 430 }, "swing"),
|
|
42
|
+
* Physics.springTo({ x: 70 }, "bounce"),
|
|
43
|
+
* Physics.springTo({ x: 250, y: 70 }, "jump"),
|
|
44
|
+
* );
|
|
45
|
+
* });
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
/**
|
|
49
|
+
* A spring's physical parameters — the knobs that decide how motion feels
|
|
50
|
+
* and, indirectly, how long it lasts.
|
|
51
|
+
*
|
|
52
|
+
* @remarks
|
|
53
|
+
* The three interact, so tune by feel rather than in isolation: heavier
|
|
54
|
+
* `mass` makes motion sluggish and prolongs it; higher `stiffness` pulls
|
|
55
|
+
* harder toward the target and speeds it up; higher `damping` bleeds off
|
|
56
|
+
* energy, trading overshoot and wobble for a quicker settle. Low damping
|
|
57
|
+
* with low stiffness is what produces a long ringing tail.
|
|
58
|
+
*
|
|
59
|
+
* Start from a preset in {@link springs} and adjust from there — the
|
|
60
|
+
* presets are the calibrated points in this space.
|
|
12
61
|
*/
|
|
13
62
|
export interface Spring {
|
|
63
|
+
/** Inertia. Higher is heavier and slower to start and stop. */
|
|
14
64
|
readonly mass: number;
|
|
65
|
+
/** Pull toward the target. Higher is snappier. */
|
|
15
66
|
readonly stiffness: number;
|
|
67
|
+
/** Energy loss. Higher settles sooner with less overshoot; near 0 rings. */
|
|
16
68
|
readonly damping: number;
|
|
69
|
+
/**
|
|
70
|
+
* Speed the motion already carries at frame one, for launching something
|
|
71
|
+
* that should feel struck or thrown rather than pulled.
|
|
72
|
+
*
|
|
73
|
+
* @defaultValue `0`
|
|
74
|
+
*/
|
|
17
75
|
readonly initialVelocity?: number;
|
|
18
76
|
}
|
|
19
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* The spring used when a call names none: quick and only lightly damped, so
|
|
79
|
+
* it overshoots a little before settling.
|
|
80
|
+
*/
|
|
20
81
|
export declare const defaultSpring: Spring;
|
|
82
|
+
/**
|
|
83
|
+
* The named spring presets, each a calibrated point in `{ mass, stiffness,
|
|
84
|
+
* damping }` space. Pass the NAME to any spring animator — it autocompletes.
|
|
85
|
+
*
|
|
86
|
+
* @remarks
|
|
87
|
+
* Durations below are measured for a 100px move at 60fps and scale with
|
|
88
|
+
* distance; a spring's length is emergent, so treat them as character, not
|
|
89
|
+
* contract:
|
|
90
|
+
*
|
|
91
|
+
* - `strike` — hardest and fastest, launched with initial velocity (~0.7s).
|
|
92
|
+
* - `jump` — a quick launched hop (~1.4s).
|
|
93
|
+
* - `smooth` — mild overshoot, unobtrusive (~2s).
|
|
94
|
+
* - `beat` — a small, tight pulse; the least overshoot of the set (~2.6s).
|
|
95
|
+
* - `swing` — a relaxed, pendulum-like arc (~3.5s).
|
|
96
|
+
* - `plop` — lands heavy and wobbles noticeably (~7s).
|
|
97
|
+
* - `bounce` — barely damped, so it rings for a LONG time: about 37 seconds
|
|
98
|
+
* and 90 direction changes for a 100px move. Striking as a one-off accent,
|
|
99
|
+
* but check the frame count before putting it in a timed sequence.
|
|
100
|
+
*
|
|
101
|
+
* @see {@link Spring} to define your own.
|
|
102
|
+
*/
|
|
21
103
|
export declare const springs: {
|
|
22
104
|
readonly beat: {
|
|
23
105
|
readonly mass: 0.13;
|
|
@@ -58,21 +140,73 @@ export declare const springs: {
|
|
|
58
140
|
readonly damping: 1.88;
|
|
59
141
|
};
|
|
60
142
|
};
|
|
143
|
+
/** The name of a built-in preset in {@link springs}. */
|
|
61
144
|
export type SpringName = keyof typeof springs;
|
|
62
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* What the spring animators accept: a preset name (autocompleted at the call
|
|
147
|
+
* site) or a full {@link Spring} of your own.
|
|
148
|
+
*/
|
|
63
149
|
export type SpringInput = SpringName | Spring;
|
|
64
150
|
export declare const resolve: (input: SpringInput) => Effect.Effect<Spring>;
|
|
65
|
-
type HasPosition<Data extends Schema.Top> = {
|
|
66
|
-
readonly "~position": Entity.TraitLens<Data["Type"], Entity.Position>;
|
|
67
|
-
};
|
|
68
151
|
/**
|
|
69
|
-
* Spring an instance to a position
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
152
|
+
* Spring an instance to a position — `Motion.moveTo` with momentum instead
|
|
153
|
+
* of a duration.
|
|
154
|
+
*
|
|
155
|
+
* @remarks
|
|
156
|
+
* The animation runs until the simulation settles, then lands exactly on
|
|
157
|
+
* `to`, so chaining springs is safe: each starts precisely where the last
|
|
158
|
+
* finished, with no accumulated drift. How long that takes depends on the
|
|
159
|
+
* spring AND the distance — a longer move rings longer.
|
|
160
|
+
*
|
|
161
|
+
* Because the length is emergent, a spring is awkward to synchronize
|
|
162
|
+
* against a fixed beat. When several things must finish together, either
|
|
163
|
+
* spring them inside a `Scene.all` and let the slowest govern, or use a
|
|
164
|
+
* duration-based tween with an elastic easing.
|
|
165
|
+
*
|
|
166
|
+
* Targets are partial, like `moveTo` — naming only `x` holds the other axes.
|
|
167
|
+
* Every axis is simulated by the same spring, so a diagonal move settles as
|
|
168
|
+
* one motion.
|
|
169
|
+
*
|
|
170
|
+
* @param to - Target position; omitted axes hold their current value.
|
|
171
|
+
* @param springInput - A preset name from {@link springs}, or a custom
|
|
172
|
+
* {@link Spring}.
|
|
173
|
+
* @param settleTolerance - How close to the target (in both displacement
|
|
174
|
+
* and velocity) counts as settled. Larger ends sooner and cuts the tail.
|
|
175
|
+
* @defaultValue `springInput` — {@link defaultSpring}; `settleTolerance` — `0.001`
|
|
176
|
+
* @returns The instance, so animators chain.
|
|
177
|
+
* @see {@link spring} to start from an explicit position.
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* A struck-then-settling entrance, using two different presets.
|
|
181
|
+
* ```typescript
|
|
182
|
+
* yield* badge.pipe(
|
|
183
|
+
* Physics.springTo({ y: 120 }, "strike"),
|
|
184
|
+
* Physics.springTo({ x: 300 }, "smooth"),
|
|
185
|
+
* );
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
export declare const springTo: (<Tag extends Entity.TagsWith<"position">>(to: Partial<Motion.Position>, springInput?: SpringInput, settleTolerance?: number) => <E = never, R = never>(instance: Instance.InstanceOrEffect<Tag, E, R>) => Effect.Effect<Instance.Instance<Tag>, E, R | Runner.Runner>) & (<Tag extends Entity.TagsWith<"position">, E = never, R = never>(instance: Instance.InstanceOrEffect<Tag, E, R>, to: Partial<Motion.Position>, springInput?: SpringInput, settleTolerance?: number) => Effect.Effect<Instance.Instance<Tag>, E, R | Runner.Runner>);
|
|
189
|
+
/**
|
|
190
|
+
* Like {@link springTo}, but starting from an explicit position.
|
|
191
|
+
*
|
|
192
|
+
* @remarks
|
|
193
|
+
* The springy entrance: an instance declared at its resting place can still
|
|
194
|
+
* fly in from off-screen and settle, because the origin is stated rather
|
|
195
|
+
* than read from the entity. Both `from` and `to` are partial and fill
|
|
196
|
+
* missing axes from the current position.
|
|
197
|
+
*
|
|
198
|
+
* @param from - Starting position; omitted axes start at the current value.
|
|
199
|
+
* @param to - Target position; omitted axes hold their current value.
|
|
200
|
+
* @param springInput - A preset name from {@link springs}, or a custom
|
|
201
|
+
* {@link Spring}.
|
|
202
|
+
* @param settleTolerance - How close counts as settled.
|
|
203
|
+
* @defaultValue `springInput` — {@link defaultSpring}; `settleTolerance` — `0.001`
|
|
204
|
+
* @returns The instance, so animators chain.
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* Drop a card in from above and let it settle where it was declared.
|
|
208
|
+
* ```typescript
|
|
209
|
+
* yield* card.pipe(Physics.spring({ y: -200 }, { y: 150 }, "plop"));
|
|
210
|
+
* ```
|
|
74
211
|
*/
|
|
75
|
-
export declare const
|
|
76
|
-
/** Like `springTo`, but from an explicit position (partials filled from current). */
|
|
77
|
-
export declare const spring: (<Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>> & HasPosition<Data>>(from: Partial<Entity.Position>, to: Partial<Entity.Position>, springInput?: SpringInput, settleTolerance?: number) => <E = never, R = never>(instance: Instance.InstanceOrEffect<Name, Data, Traits, E, R>) => Effect.Effect<Instance.Instance<Name, Data, Traits>, E, R | Runner.Runner>) & (<Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>> & HasPosition<Data>, E = never, R = never>(instance: Instance.InstanceOrEffect<Name, Data, Traits, E, R>, from: Partial<Entity.Position>, to: Partial<Entity.Position>, springInput?: SpringInput, settleTolerance?: number) => Effect.Effect<Instance.Instance<Name, Data, Traits>, E, R | Runner.Runner>);
|
|
78
|
-
export {};
|
|
212
|
+
export declare const spring: (<Tag extends Entity.TagsWith<"position">>(from: Partial<Motion.Position>, to: Partial<Motion.Position>, springInput?: SpringInput, settleTolerance?: number) => <E = never, R = never>(instance: Instance.InstanceOrEffect<Tag, E, R>) => Effect.Effect<Instance.Instance<Tag>, E, R | Runner.Runner>) & (<Tag extends Entity.TagsWith<"position">, E = never, R = never>(instance: Instance.InstanceOrEffect<Tag, E, R>, from: Partial<Motion.Position>, to: Partial<Motion.Position>, springInput?: SpringInput, settleTolerance?: number) => Effect.Effect<Instance.Instance<Tag>, E, R | Runner.Runner>);
|