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.
- package/README.md +2 -2
- 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 +682 -38
- package/dist/Entity.js +271 -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 +37 -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 +653 -330
- package/dist/Runner.js +208 -158
- package/dist/Scene.d.ts +621 -171
- package/dist/Scene.js +620 -89
- 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 +8 -6
- 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 +13 -5
- package/dist/particles/step.js +14 -10
- package/dist/types.d.ts +5 -0
- package/dist/types.js +1 -0
- package/package.json +2 -4
- 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 -381
- 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 -65
- 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 -116
- package/dist/shapes/Group.js +0 -82
- 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/Camera.js
CHANGED
|
@@ -1,85 +1,352 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
|
-
import * as
|
|
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
|
|
5
|
-
import * as
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
28
|
-
x
|
|
29
|
-
y
|
|
30
|
-
z
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
/**
|
|
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 {};
|