effect-motion 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +2 -2
  2. package/dist/Camera.d.ts +186 -49
  3. package/dist/Camera.js +343 -76
  4. package/dist/Color.d.ts +101 -1
  5. package/dist/Color.js +101 -1
  6. package/dist/EffectMotionError.d.ts +17 -0
  7. package/dist/EffectMotionError.js +17 -0
  8. package/dist/Entity.d.ts +682 -38
  9. package/dist/Entity.js +271 -27
  10. package/dist/Font.d.ts +108 -0
  11. package/dist/Font.js +95 -0
  12. package/dist/Image.d.ts +71 -0
  13. package/dist/Image.js +50 -0
  14. package/dist/Instance.d.ts +73 -11
  15. package/dist/Instance.js +44 -11
  16. package/dist/Motion.d.ts +328 -53
  17. package/dist/Motion.js +278 -46
  18. package/dist/Physics.d.ts +154 -20
  19. package/dist/Physics.js +92 -11
  20. package/dist/Projection.d.ts +37 -132
  21. package/dist/Projection.js +33 -292
  22. package/dist/Resource.d.ts +26 -0
  23. package/dist/Resource.js +41 -0
  24. package/dist/Runner.d.ts +639 -350
  25. package/dist/Runner.js +197 -150
  26. package/dist/Scene.d.ts +603 -175
  27. package/dist/Scene.js +596 -112
  28. package/dist/Timing.d.ts +170 -13
  29. package/dist/Timing.js +124 -6
  30. package/dist/Tree.d.ts +39 -0
  31. package/dist/Tree.js +127 -0
  32. package/dist/index.d.ts +54 -5
  33. package/dist/index.js +56 -5
  34. package/dist/particles/Particle.d.ts +2 -2
  35. package/dist/particles/ParticleField.d.ts +8 -6
  36. package/dist/particles/ParticleField.js +8 -9
  37. package/dist/particles/constructors.d.ts +5 -6
  38. package/dist/particles/constructors.js +8 -3
  39. package/dist/particles/legacy.d.ts +58 -0
  40. package/dist/particles/legacy.js +46 -0
  41. package/dist/particles/simulate.js +11 -3
  42. package/dist/particles/step.js +14 -10
  43. package/dist/types.d.ts +5 -0
  44. package/dist/types.js +1 -0
  45. package/package.json +2 -4
  46. package/dist/CameraHelpers.d.ts +0 -70
  47. package/dist/CameraHelpers.js +0 -239
  48. package/dist/CanvasExporter.d.ts +0 -12
  49. package/dist/CanvasExporter.js +0 -40
  50. package/dist/Fonts.d.ts +0 -41
  51. package/dist/Fonts.js +0 -27
  52. package/dist/Images.d.ts +0 -33
  53. package/dist/Images.js +0 -24
  54. package/dist/PngExporter.d.ts +0 -6
  55. package/dist/PngExporter.js +0 -85
  56. package/dist/Renderer.d.ts +0 -118
  57. package/dist/Renderer.js +0 -508
  58. package/dist/Shapes.d.ts +0 -11
  59. package/dist/Shapes.js +0 -11
  60. package/dist/demo.d.ts +0 -3
  61. package/dist/demo.js +0 -71
  62. package/dist/render/dof.d.ts +0 -27
  63. package/dist/render/dof.js +0 -37
  64. package/dist/render/paint.d.ts +0 -30
  65. package/dist/render/paint.js +0 -36
  66. package/dist/render/shapes.d.ts +0 -42
  67. package/dist/render/shapes.js +0 -310
  68. package/dist/shapes/Circle.d.ts +0 -32
  69. package/dist/shapes/Circle.js +0 -9
  70. package/dist/shapes/Ellipse.d.ts +0 -35
  71. package/dist/shapes/Ellipse.js +0 -10
  72. package/dist/shapes/Group.d.ts +0 -126
  73. package/dist/shapes/Group.js +0 -89
  74. package/dist/shapes/Hud.d.ts +0 -38
  75. package/dist/shapes/Hud.js +0 -35
  76. package/dist/shapes/Image.d.ts +0 -45
  77. package/dist/shapes/Image.js +0 -28
  78. package/dist/shapes/Line.d.ts +0 -47
  79. package/dist/shapes/Line.js +0 -35
  80. package/dist/shapes/Path.d.ts +0 -107
  81. package/dist/shapes/Path.js +0 -32
  82. package/dist/shapes/Rect.d.ts +0 -51
  83. package/dist/shapes/Rect.js +0 -22
  84. package/dist/shapes/Shape2D.d.ts +0 -49
  85. package/dist/shapes/Shape2D.js +0 -52
  86. package/dist/shapes/Shapes.d.ts +0 -11
  87. package/dist/shapes/Shapes.js +0 -11
  88. package/dist/shapes/Square.d.ts +0 -32
  89. package/dist/shapes/Square.js +0 -11
  90. package/dist/shapes/Text.d.ts +0 -51
  91. 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 Data["Type"] can't be proven
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.flatten(instanceOrEffect);
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
- // Data["Type"] is opaque to TS, so spread is disallowed — assign + cast
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
- * Animate interpolable (numeric) props of an instance toward `to` over
88
- * `duration` by raw field name, starting from the instance's current
89
- * data, optionally paced by a timing function (name or function, default
90
- * linear). Dual: `tweenTo(instance, to, duration, timing?)` or
91
- * `instance.pipe(tweenTo(to, duration, timing?))`. Resolves with the
92
- * instance, so animations chain.
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
- * Parametric animator: each frame applies `fn` with the eased parameter
97
- * and the current data, then ticks. The final frame receives exactly
98
- * `t = 1` for any timing with f(1) = 1; a zero-length duration still
99
- * takes one frame. This is the primitive under coordinated multi-field
100
- * motion — arcs, orbits, counters — where independent per-field tweens
101
- * cannot express the coupling. Determinism: `fn` sees only `(t, data)`.
102
- * Dual: `drive(instance, duration, timing, fn)` or
103
- * `instance.pipe(drive(duration, timing, fn))`.
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.flatten(instanceOrEffect);
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 `tweenTo`, but with an explicit start: interpolates the keys of
119
- * `to` from `from` (keys missing in `from` start at the current data).
120
- * Dual: `tween(instance, from, to, duration, timing?)` or
121
- * `instance.pipe(tween(from, to, duration, timing?))`.
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.flatten(instanceOrEffect);
126
- const lens = Entity.traitOrDie(instance.entity, "~position");
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) => lens.set(data, value)), timing);
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.flatten(instanceOrEffect);
136
- const lens = Entity.traitOrDie(instance.entity, "~opacity");
137
- const current = lens.get(yield* Scene.data(instance));
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 via its `~position` trait — per-entity
143
- * semantics (a Line translates whole, a Group carries its subtree).
144
- * Partial targets hold the missing axis. Dual:
145
- * `moveTo(instance, to, duration, timing?)` or
146
- * `instance.pipe(moveTo(to, duration, timing?))`.
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
- /** Like `moveTo`, but from an explicit position (partials filled from current). */
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 via its `~opacity` trait. Dual:
153
- * `fadeTo(instance, opacity, duration, timing?)` or
154
- * `instance.pipe(fadeTo(opacity, duration, timing?))`.
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
- /** Like `fadeTo`, but from an explicit opacity. */
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 the scene for `duration` of scene time (frames at the runner's
161
- * frame rate) — `Scene.sleep`'s chainable sibling.
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
- * - `yield* Motion.wait("1 second")` — plain frame-based sleep
164
- * - `instance.pipe(moveTo(...), Motion.wait("1 second"), fadeTo(...))`
165
- * — the hold runs between the two animations and the instance flows on
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(effect, () => Scene.sleep(duration)), Effectable.Prototype({
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: durationless animations that carry momentum and
8
- * end on their own. A spring is a damped harmonic oscillator simulated
9
- * until displacement and velocity settle within a tolerance — there is
10
- * no duration to specify, so springs are combinators here rather than
11
- * `TimingInput`s.
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
- /** fast, gently damped general-purpose spring */
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
- /** a preset name (autocompleted) or a custom spring configuration */
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 via its `~position` trait — the
70
- * durationless counterpart of `Motion.moveTo` (settles exactly, length
71
- * emerges from the physics). Dual:
72
- * `springTo(instance, to, springInput?, settleTolerance?)` or
73
- * `instance.pipe(springTo(to, springInput?, settleTolerance?))`.
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 springTo: (<Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>> & HasPosition<Data>>(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>, to: Partial<Entity.Position>, springInput?: SpringInput, settleTolerance?: number) => Effect.Effect<Instance.Instance<Name, Data, Traits>, E, R | Runner.Runner>);
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>);