effect-motion 0.3.2 → 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 +653 -330
  25. package/dist/Runner.js +208 -158
  26. package/dist/Scene.d.ts +621 -171
  27. package/dist/Scene.js +620 -89
  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 +13 -5
  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 -381
  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 -65
  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 -116
  73. package/dist/shapes/Group.js +0 -82
  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/Camera.js CHANGED
@@ -1,85 +1,352 @@
1
1
  import { Effect } from "effect";
2
- import * as Schema from "effect/Schema";
2
+ import * as Duration from "effect/Duration";
3
+ import * as Function from "effect/Function";
3
4
  import * as Entity from "./Entity.js";
4
- import * as Projection from "./Projection.js";
5
- import * as Shape2D from "./shapes/Shape2D.js";
5
+ import * as Instance from "./Instance.js";
6
+ import * as Motion from "./Motion.js";
7
+ import * as Runner from "./Runner.js";
8
+ import * as Scene from "./Scene.js";
9
+ import * as Time from "./Time.js";
10
+ import * as Timing from "./Timing.js";
11
+ // a target argument (vs a duration/timing in the same slot): instances,
12
+ // effects, or a position-like object — Durations are objects too, so
13
+ // exclude them explicitly
14
+ const isTargetArg = (v) => Instance.isInstance(v) ||
15
+ Effect.isEffect(v) ||
16
+ (typeof v === "object" &&
17
+ v !== null &&
18
+ !Duration.isDuration(v) &&
19
+ ("x" in v || "y" in v || "z" in v));
20
+ // data-first iff the first arg is an instance AND the second is a target —
21
+ // plain firstArgIsInstance would misread `cam.pipe`-less pipeable calls
22
+ // whose TARGET is an instance (`lookAt(hero, "1 second")`)
23
+ const dataFirst = (args) => Instance.isInstance(args[0]) && isTargetArg(args[1]);
6
24
  /**
7
- * The camera is view state, not a shape — it is never registered with a sink
8
- * and never drawn. It exists as an ordinary Instance so the existing
9
- * animators drive it for free: `camera.pipe(moveTo({ z: -800 }))`,
10
- * `tween("rotY", ...)`, `spring`, `Scene.fork`, etc.
11
- *
12
- * `~position` (now x/y/z) is the camera's world position; `rotX/rotY/rotZ`
13
- * are its Euler orientation; `focalLength` sets the field of view. At rest
14
- * the camera sits a focal-length back on +z looking down world -z, so a
15
- * world point at z=0 projects to plain-2D screen coordinates — see
16
- * `Projection.ts`. The sink reads these off `FrameMeta.camera` and projects
17
- * every instance through them; instance data stays in world coordinates, so
18
- * determinism and `moveTo` semantics are untouched by the camera.
19
- *
20
- * `z` and `focalLength` have no static schema default: the right resting
21
- * values are width-relative (After Effects' 50mm-equivalent — see
22
- * `Projection.defaultFocalLength`), and only the Runner knows the scene
23
- * width. The Runner fills both at instantiate time for every Camera
24
- * instance, so by the time animators or the renderer read the data they are
25
- * always concrete.
25
+ * Resolve a target once (Effects yield their Instance), returning a
26
+ * per-frame position reader with the offset folded in: live for
27
+ * instances, fixed for plain positions.
26
28
  */
27
- const fields = {
28
- x: Shape2D.defaultedNumber(0),
29
- y: Shape2D.defaultedNumber(0),
30
- z: Schema.Number.pipe(Schema.withConstructorDefault(Effect.succeed(0))),
31
- rotX: Shape2D.defaultedNumber(0),
32
- rotY: Shape2D.defaultedNumber(0),
33
- rotZ: Shape2D.defaultedNumber(0),
34
- focalLength: Schema.Number.pipe(Schema.withConstructorDefault(Effect.succeed(0))),
35
- // depth of field: view-space distance to the sharp plane, and blur
36
- // strength. Like z/focalLength, focusDistance's right default is
37
- // width-relative (the resting distance, so z=0 is in focus) — the Runner
38
- // fills it. aperture 0 = pinhole = DoF off (explicit opt-in).
39
- focusDistance: Schema.Number.pipe(Schema.withConstructorDefault(Effect.succeed(0))),
40
- aperture: Shape2D.defaultedNumber(0),
41
- // Optional point of interest (world coordinates): when present the camera
42
- // auto-orients toward it and explicit Euler composes AFTER the aim (the
43
- // AE two-node model — see Projection.resolveCamera). Flat numeric fields
44
- // so tween/spring drive the aim like any other field. NOT Runner-filled:
45
- // absent = today's one-node camera, byte-identical (explicit opt-in).
46
- poiX: Schema.optionalKey(Schema.Number),
47
- poiY: Schema.optionalKey(Schema.Number),
48
- poiZ: Schema.optionalKey(Schema.Number),
29
+ const targetReader = Effect.fnUntraced(function* (target, offset) {
30
+ const ox = offset?.x ?? 0;
31
+ const oy = offset?.y ?? 0;
32
+ const oz = offset?.z ?? 0;
33
+ if (Instance.isInstance(target) || Effect.isEffect(target)) {
34
+ const instance = yield* Instance.flattenInstance(target);
35
+ return Scene.data(instance).pipe(Effect.map((data) => {
36
+ const p = data.position;
37
+ return { x: p.x + ox, y: p.y + oy, z: p.z + oz };
38
+ }));
39
+ }
40
+ // AnyInstance's `any` params defeat narrowing — the guards above
41
+ // returned for instances/effects, so this is a plain position
42
+ const point = target;
43
+ const fixed = {
44
+ x: (point.x ?? 0) + ox,
45
+ y: (point.y ?? 0) + oy,
46
+ z: (point.z ?? 0) + oz,
47
+ };
48
+ return Effect.succeed(fixed);
49
+ });
50
+ const setPoi = (data, p) => ({
51
+ ...data,
52
+ poi: Entity.vec3(p),
53
+ });
54
+ // the camera's WORLD position (center-origin frame: position IS world)
55
+ const worldPosition = Effect.fnUntraced(function* (cam) {
56
+ const data = yield* Scene.data(cam);
57
+ return {
58
+ x: data.position.x,
59
+ y: data.position.y,
60
+ z: data.position.z,
61
+ };
62
+ });
63
+ const lookAtImpl = Effect.fnUntraced(function* (camOrEffect, target, duration, timing, offset) {
64
+ const cam = yield* Instance.flattenInstance(camOrEffect);
65
+ const read = yield* targetReader(target, offset);
66
+ if (duration === undefined) {
67
+ const p = yield* read;
68
+ yield* Scene.update(cam, (d) => setPoi(d, p));
69
+ return cam;
70
+ }
71
+ // eased re-aim: a RETARGETED tween — each frame interpolates from the
72
+ // start POI toward the target's CURRENT position, converging exactly
73
+ // onto a moving target at t = 1 (a plain-Position target degenerates to
74
+ // a fixed tween). Effectful per-frame read, so this runs its own loop
75
+ // rather than Motion.drive (whose callback is pure).
76
+ const runner = yield* Runner.Runner;
77
+ const timingFn = Timing.resolve(timing ?? "linear");
78
+ const data = yield* Scene.data(cam);
79
+ let start;
80
+ if (data.poi !== null) {
81
+ start = data.poi;
82
+ }
83
+ else {
84
+ // no POI yet: seed on the camera's UNAIMED axis (straight down world
85
+ // -z) at the target's distance. resolveCamera derives zero aim for
86
+ // that point, so the explicit Euler alone carries the view — engaging
87
+ // POI mode is snap-free for ANY current orientation, and the tween
88
+ // takes over from there.
89
+ const world = yield* worldPosition(cam);
90
+ const first = yield* read;
91
+ const distance = Math.hypot(first.x - world.x, first.y - world.y, first.z - world.z);
92
+ start = { x: world.x, y: world.y, z: world.z - distance };
93
+ }
94
+ const frames = Math.max(1, Time.toFrames(duration, runner.settings.frameRate));
95
+ for (let i = 1; i <= frames; i++) {
96
+ const t = timingFn(i / frames);
97
+ const p = yield* read;
98
+ yield* Scene.update(cam, (d) => setPoi(d, {
99
+ x: start.x + (p.x - start.x) * t,
100
+ y: start.y + (p.y - start.y) * t,
101
+ z: start.z + (p.z - start.z) * t,
102
+ }));
103
+ yield* Scene.tick;
104
+ }
105
+ return cam;
106
+ });
107
+ /**
108
+ * Point the camera at something.
109
+ *
110
+ * @remarks
111
+ * Aiming is expressed as a point of interest rather than as rotation
112
+ * angles, which is what makes it composable: the target can be a live
113
+ * instance, and the camera keeps facing it as it moves. There is no
114
+ * `lookAtTo` — the verb already names its target — so an optional
115
+ * `duration` selects between the two behaviors:
116
+ *
117
+ * - **Omitted** — aim snaps this frame. Use it to establish the shot before
118
+ * anything moves.
119
+ * - **Given** — the aim eases over that time as a RETARGETED tween: each
120
+ * frame interpolates toward the target's current position, so it converges
121
+ * exactly onto a moving target with no snap at the end.
122
+ *
123
+ * Setting a point of interest is also the prerequisite for
124
+ * {@link orbitTo} and {@link dollyTo}, which are both defined relative to it.
125
+ *
126
+ * @param target - An instance to track, or a fixed world position.
127
+ * @param duration - Omit to snap; give a time to ease the re-aim.
128
+ * @param timing - An easing name or function.
129
+ * @param offset - Shifts the aim relative to the target, e.g. slightly above.
130
+ * @defaultValue `timing` — `"linear"`
131
+ * @returns The camera, so animators chain.
132
+ *
133
+ * @example
134
+ * Snap to establish, then ease across to a second subject.
135
+ * ```typescript
136
+ * const camera = yield* Scene.camera;
137
+ * yield* camera.pipe(Camera.lookAt(hero));
138
+ * yield* camera.pipe(Camera.lookAt(villain, "1 second", "easeInOutCubic"));
139
+ * ```
140
+ */
141
+ export const lookAt = Function.dual(dataFirst, lookAtImpl);
142
+ /**
143
+ * Keep the camera locked onto a moving target for `duration`.
144
+ *
145
+ * @remarks
146
+ * Where {@link lookAt} with a duration EASES toward a target and stops,
147
+ * `follow` holds the aim on it: every frame copies the target's position, so
148
+ * the subject stays pinned while it moves. Run it concurrently with the
149
+ * subject's own animation — typically inside `Scene.all` or a `Scene.fork`.
150
+ *
151
+ * There is no timing parameter, because tracking is a hard per-frame copy
152
+ * rather than an interpolation. For a lagging, weighted camera, animate the
153
+ * point of interest with a spring instead.
154
+ *
155
+ * Ordering note: within a frame, branches run in the order they were forked.
156
+ * A follow forked BEFORE its target's animator reads the previous frame's
157
+ * position — a deterministic one-frame trail, not a bug.
158
+ *
159
+ * @param target - An instance to track, or a fixed world position.
160
+ * @param duration - How long to keep tracking.
161
+ * @param offset - Shifts the aim relative to the target.
162
+ * @returns The camera, so animators chain.
163
+ *
164
+ * @example
165
+ * Track the runner for the two seconds it is crossing frame.
166
+ * ```typescript
167
+ * yield* Scene.all([
168
+ * runner.pipe(Motion.moveTo({ x: 900 }, "2 seconds")),
169
+ * camera.pipe(Camera.follow(runner, "2 seconds")),
170
+ * ]);
171
+ * ```
172
+ */
173
+ export const follow = Function.dual(dataFirst, Effect.fnUntraced(function* (camOrEffect, target, duration, offset) {
174
+ const cam = yield* Instance.flattenInstance(camOrEffect);
175
+ const read = yield* targetReader(target, offset);
176
+ const runner = yield* Runner.Runner;
177
+ const frames = Math.max(1, Time.toFrames(duration, runner.settings.frameRate));
178
+ for (let i = 1; i <= frames; i++) {
179
+ const p = yield* read;
180
+ yield* Scene.update(cam, (d) => setPoi(d, p));
181
+ yield* Scene.tick;
182
+ }
183
+ return cam;
184
+ }));
185
+ // orbit/dolly are defined relative to the POI — die loudly without one
186
+ const poiOrDie = (data) => {
187
+ if (data.poi === null) {
188
+ throw new Error("Camera: orbit/dolly need a point of interest — set one first (Camera.lookAt(target))");
189
+ }
190
+ return data.poi;
49
191
  };
50
- export const Camera = Entity.make("Camera", fields, {
51
- // x/y/z is the camera position; rotation + focalLength are raw numeric
52
- // fields animated via tween. Inlined (not positionLens()) so the data
53
- // type flows into the lens.
54
- "~position": {
55
- // z is filled by the Runner at instantiate; the ?? 0 is unreachable,
56
- // it only satisfies the optional schema type
57
- get: (data) => ({ x: data.x, y: data.y, z: data.z ?? 0 }),
58
- set: (data, value) => ({
59
- ...data,
60
- x: value.x,
61
- y: value.y,
62
- z: value.z,
63
- }),
64
- },
192
+ const orbitImpl = Effect.fnUntraced(function* (camOrEffect, from, to, duration, timing) {
193
+ const cam = yield* Instance.flattenInstance(camOrEffect);
194
+ const startData = yield* Scene.data(cam);
195
+ const poi = poiOrDie(startData);
196
+ const world = {
197
+ x: startData.position.x,
198
+ z: startData.position.z,
199
+ };
200
+ // azimuth 0 = directly +z of the POI (the resting side); positive sweeps
201
+ // by the right-hand rule about world +y (+z toward +x). radius = the
202
+ // current horizontal distance, preserved through the arc; height too
203
+ const radius = Math.hypot(world.x - poi.x, world.z - poi.z);
204
+ const startAzimuth = from ?? Math.atan2(world.x - poi.x, world.z - poi.z);
205
+ return yield* Motion.drive(cam, duration, timing ?? "linear", (t, d) => {
206
+ const data = d;
207
+ // POI read from live data: orbiting a moving POI stays centered on it
208
+ const p = poiOrDie(data);
209
+ const angle = startAzimuth + (to - startAzimuth) * t;
210
+ return {
211
+ ...d,
212
+ position: Entity.vec3({
213
+ x: p.x + radius * Math.sin(angle),
214
+ y: d.position.y,
215
+ z: p.z + radius * Math.cos(angle),
216
+ }),
217
+ };
218
+ });
65
219
  });
66
220
  /**
67
- * The identity view for a comp of the given width: resting camera, no
68
- * rotation, the width-relative default focal length (AE's 50mm equivalent).
69
- * Projects z=0 content to its plain-2D screen position at scale 1.
221
+ * Swing the camera around its point of interest, like a turntable.
222
+ *
223
+ * @remarks
224
+ * The camera travels an arc at a fixed radius and height while continuing to
225
+ * face the subject — the standard "orbit the product" move. Only the
226
+ * position is animated; the aim follows from the point of interest, so there
227
+ * is no orientation math to get wrong.
228
+ *
229
+ * `azimuth` is an ABSOLUTE angle in radians about the world-Y axis through
230
+ * the point of interest, where 0 is directly in front (+z of it). A full
231
+ * turn is `Math.PI * 2`; the sign chooses direction.
232
+ *
233
+ * Requires a point of interest — call {@link lookAt} first, or this fails
234
+ * loudly telling you so.
235
+ *
236
+ * @param azimuth - Target angle in radians; 0 is directly in front.
237
+ * @param duration - How long, in scene time.
238
+ * @param timing - An easing name or function.
239
+ * @defaultValue `timing` — `"linear"`
240
+ * @returns The camera, so animators chain.
241
+ * @see {@link orbit} to start from an explicit angle.
242
+ *
243
+ * @example
244
+ * A quarter turn around a subject.
245
+ * ```typescript
246
+ * yield* camera.pipe(
247
+ * Camera.lookAt(subject),
248
+ * Camera.orbitTo(Math.PI / 2, "2 seconds", "easeInOutCubic"),
249
+ * );
250
+ * ```
251
+ */
252
+ export const orbitTo = Function.dual((args) => Instance.isInstance(args[0]), ((cam, azimuth, duration, timing) => orbitImpl(cam, undefined, azimuth, duration, timing)));
253
+ /**
254
+ * Like {@link orbitTo}, but starting from an explicit azimuth.
255
+ *
256
+ * @remarks
257
+ * Stating both ends is what makes a full revolution expressible: `orbit(0,
258
+ * Math.PI * 2, …)` sweeps all the way around, whereas `orbitTo(Math.PI * 2)`
259
+ * from a resting camera would already be at its target and not move.
260
+ *
261
+ * @param from - Starting angle in radians.
262
+ * @param to - Target angle in radians.
263
+ * @param duration - How long, in scene time.
264
+ * @param timing - An easing name or function.
265
+ * @returns The camera, so animators chain.
266
+ *
267
+ * @example
268
+ * One full revolution.
269
+ * ```typescript
270
+ * yield* camera.pipe(Camera.orbit(0, Math.PI * 2, "4 seconds"));
271
+ * ```
70
272
  */
71
- export const identity = (width) => {
72
- const focalLength = Projection.defaultFocalLength(width);
73
- return {
74
- x: 0,
75
- y: 0,
76
- z: Projection.defaultCameraZ(focalLength),
77
- rotX: 0,
78
- rotY: 0,
79
- rotZ: 0,
80
- focalLength,
81
- // the z=0 plane in focus, no depth of field
82
- focusDistance: Projection.defaultCameraZ(focalLength),
83
- aperture: 0,
273
+ export const orbit = Function.dual((args) => Instance.isInstance(args[0]), orbitImpl);
274
+ const dollyImpl = Effect.fnUntraced(function* (camOrEffect, from, to, duration, timing) {
275
+ const cam = yield* Instance.flattenInstance(camOrEffect);
276
+ const startData = yield* Scene.data(cam);
277
+ const poi = poiOrDie(startData);
278
+ const world = {
279
+ x: startData.position.x,
280
+ y: startData.position.y,
281
+ z: startData.position.z,
84
282
  };
85
- };
283
+ const startDistance = Math.hypot(world.x - poi.x, world.y - poi.y, world.z - poi.z);
284
+ // the fixed unit direction POI → camera; a zero distance has no
285
+ // direction to dolly along
286
+ if (startDistance === 0) {
287
+ throw new Error("Camera: dolly from distance 0 — the camera sits ON its point of interest, so there is no view axis to move along");
288
+ }
289
+ const u = {
290
+ x: (world.x - poi.x) / startDistance,
291
+ y: (world.y - poi.y) / startDistance,
292
+ z: (world.z - poi.z) / startDistance,
293
+ };
294
+ const d0 = from ?? startDistance;
295
+ return yield* Motion.drive(cam, duration, timing ?? "linear", (t, d) => {
296
+ const data = d;
297
+ const p = poiOrDie(data);
298
+ const dist = d0 + (to - d0) * t;
299
+ return {
300
+ ...d,
301
+ position: Entity.vec3({
302
+ x: p.x + u.x * dist,
303
+ y: p.y + u.y * dist,
304
+ z: p.z + u.z * dist,
305
+ }),
306
+ };
307
+ });
308
+ });
309
+ /**
310
+ * Move the camera toward or away from its point of interest along the view
311
+ * axis.
312
+ *
313
+ * @remarks
314
+ * A push-in or pull-back that keeps the subject centered and the aim
315
+ * unchanged — only the distance changes. Distinct from tweening the camera's
316
+ * `z`, which moves along the WORLD axis and would slide the subject off
317
+ * center once the camera is aimed obliquely.
318
+ *
319
+ * Requires a point of interest — call {@link lookAt} first.
320
+ *
321
+ * @param distance - Target distance from the point of interest; smaller is
322
+ * closer.
323
+ * @param duration - How long, in scene time.
324
+ * @param timing - An easing name or function.
325
+ * @defaultValue `timing` — `"linear"`
326
+ * @returns The camera, so animators chain.
327
+ * @see {@link dolly} to start from an explicit distance.
328
+ *
329
+ * @example
330
+ * Push in on a subject.
331
+ * ```typescript
332
+ * yield* camera.pipe(
333
+ * Camera.lookAt(subject),
334
+ * Camera.dollyTo(300, "1500 millis", "easeInOutCubic"),
335
+ * );
336
+ * ```
337
+ */
338
+ export const dollyTo = Function.dual((args) => Instance.isInstance(args[0]), ((cam, distance, duration, timing) => dollyImpl(cam, undefined, distance, duration, timing)));
339
+ /**
340
+ * Like {@link dollyTo}, but starting from an explicit distance.
341
+ *
342
+ * @remarks
343
+ * For a push-in that begins further out than the camera currently sits —
344
+ * the move starts at `from` regardless of where the camera was left.
345
+ *
346
+ * @param from - Starting distance from the point of interest.
347
+ * @param to - Target distance.
348
+ * @param duration - How long, in scene time.
349
+ * @param timing - An easing name or function.
350
+ * @returns The camera, so animators chain.
351
+ */
352
+ export const dolly = Function.dual((args) => Instance.isInstance(args[0]), dollyImpl);
package/dist/Color.d.ts CHANGED
@@ -1,3 +1,24 @@
1
+ /**
2
+ * Colors: construction, adjustment, and conversion.
3
+ *
4
+ * @remarks
5
+ * Reach for {@link hex} for CSS strings, {@link rgba} for byte components,
6
+ * or {@link tw} for the built-in Tailwind palette. Perceptual spaces
7
+ * ({@link oklch}, {@link lab}) are available when you want even lightness
8
+ * across hues.
9
+ *
10
+ * Values are immutable and every adjustment is a dual, so operations
11
+ * compose in either call style. Colors interpolate, so any fill or stroke
12
+ * animates with the ordinary tween animators.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * import * as Color from "effect-motion/Color";
17
+ *
18
+ * const brand = Color.hex("#7f5af0");
19
+ * const dim = brand.pipe(Color.darken(0.6), Color.alpha(0.75));
20
+ * ```
21
+ */
1
22
  import chroma from "chroma-js";
2
23
  import { Schema } from "effect";
3
24
  import * as Pipeable from "effect/Pipeable";
@@ -8,6 +29,21 @@ declare const Color_base: Schema.Class<Color, Schema.TaggedStruct<"Color", {
8
29
  readonly "~b": Schema.Number;
9
30
  readonly "~a": Schema.Number;
10
31
  }>, {}>;
32
+ /**
33
+ * A color with an alpha channel.
34
+ *
35
+ * @remarks
36
+ * Build one with a constructor below — {@link hex} for CSS strings,
37
+ * {@link rgba} for byte components, {@link tw} for the Tailwind palette —
38
+ * rather than the class directly.
39
+ *
40
+ * RGB channels are 0–255 while alpha is 0–1, matching CSS. Colors are
41
+ * immutable and pipeable, so adjustments compose:
42
+ * `Color.hex("#7f5af0").pipe(Color.darken(0.5), Color.alpha(0.8))`.
43
+ *
44
+ * Colors are interpolable, so any fill or stroke can be animated with the
45
+ * ordinary tween animators.
46
+ */
11
47
  export declare class Color extends Color_base implements Pipeable.Pipeable {
12
48
  static of(r: number, g: number, b: number, a?: number): Color;
13
49
  get r(): number;
@@ -16,9 +52,33 @@ export declare class Color extends Color_base implements Pipeable.Pipeable {
16
52
  get a(): number;
17
53
  pipe: Pipeable.Pipeable["pipe"];
18
54
  }
55
+ /** Whether `value` is a {@link Color}. */
19
56
  export declare const is: (value: unknown) => value is Color;
57
+ /**
58
+ * A color from red, green, blue (each 0–255) and optional alpha (0–1).
59
+ *
60
+ * @defaultValue `alpha` — `1`
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * Color.rgba(22, 22, 29)
65
+ * ```
66
+ */
20
67
  export declare const rgba: (r: number, g: number, b: number, alpha?: number) => Color;
21
- /** Parse a CSS color string (`#rgb`, `#rrggbb`, `#rrggbbaa`, named, `rgb()`, …). */
68
+ /**
69
+ * Parse a CSS color string — the most convenient constructor.
70
+ *
71
+ * @remarks
72
+ * Accepts everything CSS does: `#rgb`, `#rrggbb`, `#rrggbbaa`, named colors
73
+ * like `"rebeccapurple"`, and functional forms like `rgb()` / `hsl()`.
74
+ *
75
+ * @param value - A CSS color string.
76
+ *
77
+ * @example
78
+ * ```typescript
79
+ * Color.hex("#7f5af0")
80
+ * ```
81
+ */
22
82
  export declare const hex: (value: string) => Color;
23
83
  export declare const hsl: (h: number, s: number, l: number, alpha?: number) => Color;
24
84
  export declare const hsv: (h: number, s: number, v: number, alpha?: number) => Color;
@@ -29,10 +89,15 @@ export declare const oklab: (l: number, a: number, b: number, alpha?: number) =>
29
89
  export declare const oklch: (l: number, c: number, h: number, alpha?: number) => Color;
30
90
  export declare const cmyk: (c: number, m: number, y: number, k: number) => Color;
31
91
  export declare const gl: (g: number, l: number, r: number, alpha?: number) => Color;
92
+ /** Set a color's opacity (0–1), leaving its channels untouched. */
32
93
  export declare const alpha: ((amount: number) => (self: Color) => Color) & ((self: Color, amount: number) => Color);
94
+ /** Darken by `amount` steps in Lab space. @defaultValue `amount` — `1` */
33
95
  export declare const darken: ((amount?: number) => (self: Color) => Color) & ((self: Color, amount?: number) => Color);
96
+ /** Brighten by `amount` steps in Lab space. @defaultValue `amount` — `1` */
34
97
  export declare const brighten: ((amount?: number) => (self: Color) => Color) & ((self: Color, amount?: number) => Color);
98
+ /** Increase saturation by `amount` steps. @defaultValue `amount` — `1` */
35
99
  export declare const saturate: ((amount?: number) => (self: Color) => Color) & ((self: Color, amount?: number) => Color);
100
+ /** Reduce saturation by `amount` steps. @defaultValue `amount` — `1` */
36
101
  export declare const desaturate: ((amount?: number) => (self: Color) => Color) & ((self: Color, amount?: number) => Color);
37
102
  export type ChannelMap = {
38
103
  rgb: "r" | "g" | "b";
@@ -48,6 +113,18 @@ export type ChannelMap = {
48
113
  oklab: "l" | "a" | "b";
49
114
  oklch: "l" | "c" | "h";
50
115
  };
116
+ /**
117
+ * Blend two colors, `amount` of the way from `self` to `color`.
118
+ *
119
+ * @remarks
120
+ * `mode` picks the color space the blend travels through, which changes the
121
+ * intermediate hues considerably: `"lab"` (the animators' default) keeps
122
+ * perceptual evenness, while `"rgb"` can pass through muddy midpoints.
123
+ *
124
+ * @param color - The color to blend toward.
125
+ * @param amount - 0 keeps `self`, 1 gives `color`.
126
+ * @param mode - Interpolation space.
127
+ */
51
128
  export declare const mix: ((color: Color, amount: number, mode?: chroma.InterpolationMode) => (self: Color) => Color) & ((self: Color, color: Color, amount: number, mode?: chroma.InterpolationMode) => Color);
52
129
  export declare const shade: ((amount?: number, mode?: chroma.InterpolationMode) => (self: Color) => Color) & ((self: Color, amount?: number, mode?: chroma.InterpolationMode) => Color);
53
130
  export declare const tint: ((amount?: number, mode?: chroma.InterpolationMode) => (self: Color) => Color) & ((self: Color, amount?: number, mode?: chroma.InterpolationMode) => Color);
@@ -80,8 +157,14 @@ export declare const bytes: (self: Color) => {
80
157
  b: number;
81
158
  a: number;
82
159
  };
160
+ /** Opaque black. */
83
161
  export declare const black: Color;
162
+ /** Opaque white. */
84
163
  export declare const white: Color;
164
+ /**
165
+ * Fully transparent — the default scene background, which leaves whatever
166
+ * is behind the composition showing through.
167
+ */
85
168
  export declare const transparent: Color;
86
169
  export declare const twMap: {
87
170
  readonly slate: {
@@ -424,5 +507,22 @@ export declare const twMap: {
424
507
  };
425
508
  };
426
509
  type TwColor = keyof typeof twMap;
510
+ /**
511
+ * A color from the Tailwind palette.
512
+ *
513
+ * @remarks
514
+ * A ready-made, perceptually even set of hues — handy for getting a scene
515
+ * looking coherent without hand-picking values.
516
+ *
517
+ * @param color - Palette name, e.g. `"violet"`.
518
+ * @param shade - Lightness step, `"50"` (lightest) to `"950"` (darkest).
519
+ * @param alpha - Opacity, 0–1.
520
+ * @defaultValue `shade` — `"400"`; `alpha` — `1`
521
+ *
522
+ * @example
523
+ * ```typescript
524
+ * Color.tw("violet", "500")
525
+ * ```
526
+ */
427
527
  export declare const tw: (color: TwColor, shade?: "50" | "100" | "200" | "300" | "400" | "500" | "600" | "700" | "800" | "900" | "950", alpha?: number) => Color;
428
528
  export {};