effect-motion 0.2.0 → 0.3.1
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/dist/Camera.d.ts +50 -28
- package/dist/Camera.js +71 -19
- package/dist/CameraHelpers.d.ts +70 -0
- package/dist/CameraHelpers.js +239 -0
- package/dist/CanvasExporter.d.ts +12 -0
- package/dist/CanvasExporter.js +40 -0
- package/dist/Color.d.ts +428 -0
- package/dist/Color.js +535 -0
- package/dist/EffectMotionError.d.ts +10 -0
- package/dist/EffectMotionError.js +6 -0
- package/dist/Entity.d.ts +9 -8
- package/dist/Entity.js +1 -4
- package/dist/Fonts.d.ts +10 -0
- package/dist/Fonts.js +16 -0
- package/dist/Images.d.ts +33 -0
- package/dist/Images.js +24 -0
- package/dist/Instance.d.ts +4 -4
- package/dist/Motion.d.ts +24 -7
- package/dist/Motion.js +54 -9
- package/dist/Phaser.d.ts +1 -1
- package/dist/Phaser.js +13 -9
- package/dist/Physics.d.ts +3 -3
- package/dist/Physics.js +4 -4
- package/dist/PngExporter.d.ts +6 -0
- package/dist/PngExporter.js +85 -0
- package/dist/Projection.d.ts +225 -0
- package/dist/Projection.js +473 -0
- package/dist/Renderer.d.ts +102 -53
- package/dist/Renderer.js +369 -78
- package/dist/Runner.d.ts +126 -42
- package/dist/Runner.js +42 -17
- package/dist/Scene.d.ts +88 -59
- package/dist/Scene.js +24 -15
- package/dist/Shapes.d.ts +11 -0
- package/dist/Shapes.js +11 -0
- package/dist/demo.d.ts +3 -186
- package/dist/demo.js +24 -35
- package/dist/index.d.ts +14 -13
- package/dist/index.js +14 -13
- package/dist/particles/Particle.d.ts +4 -3
- package/dist/particles/ParticleField.d.ts +11 -48
- package/dist/particles/ParticleField.js +5 -4
- package/dist/particles/constructors.d.ts +8 -7
- package/dist/particles/constructors.js +2 -2
- package/dist/particles/index.d.ts +5 -6
- package/dist/particles/index.js +5 -6
- package/dist/particles/overLife.d.ts +1 -1
- package/dist/particles/overLife.js +1 -1
- package/dist/particles/simulate.d.ts +2 -2
- package/dist/particles/simulate.js +6 -6
- package/dist/particles/step.d.ts +2 -2
- package/dist/particles/step.js +5 -3
- package/dist/render/dof.d.ts +27 -0
- package/dist/render/dof.js +37 -0
- package/dist/render/paint.d.ts +30 -0
- package/dist/render/paint.js +36 -0
- package/dist/render/shapes.d.ts +42 -0
- package/dist/render/shapes.js +310 -0
- package/dist/shapes/Circle.d.ts +10 -15
- package/dist/shapes/Circle.js +2 -2
- package/dist/shapes/Ellipse.d.ts +10 -16
- package/dist/shapes/Ellipse.js +2 -2
- package/dist/shapes/Group.d.ts +5 -39
- package/dist/shapes/Group.js +4 -4
- package/dist/shapes/Hud.d.ts +38 -0
- package/dist/shapes/Hud.js +35 -0
- package/dist/shapes/Image.d.ts +45 -0
- package/dist/shapes/Image.js +28 -0
- package/dist/shapes/Line.d.ts +12 -12
- package/dist/shapes/Line.js +11 -6
- package/dist/shapes/Path.d.ts +87 -18
- package/dist/shapes/Path.js +25 -6
- package/dist/shapes/Rect.d.ts +33 -23
- package/dist/shapes/Rect.js +14 -2
- package/dist/shapes/Shape2D.d.ts +13 -4
- package/dist/shapes/Shape2D.js +16 -5
- package/dist/shapes/Shapes.d.ts +11 -0
- package/dist/shapes/Shapes.js +11 -0
- package/dist/shapes/Square.d.ts +10 -15
- package/dist/shapes/Square.js +2 -2
- package/dist/shapes/Text.d.ts +10 -19
- package/dist/shapes/Text.js +2 -2
- package/package.json +13 -3
- package/dist/particles/render.d.ts +0 -13
- package/dist/particles/render.js +0 -33
- package/dist/shapes/Layer.d.ts +0 -9
- package/dist/shapes/Layer.js +0 -23
- package/dist/shapes/index.d.ts +0 -10
- package/dist/shapes/index.js +0 -10
- package/dist/svg/SvgDomRenderer.d.ts +0 -28
- package/dist/svg/SvgDomRenderer.js +0 -50
- package/dist/svg/SvgNode.d.ts +0 -13
- package/dist/svg/SvgNode.js +0 -18
- package/dist/svg/SvgRenderer.d.ts +0 -22
- package/dist/svg/SvgRenderer.js +0 -20
- package/dist/svg/camera.d.ts +0 -21
- package/dist/svg/camera.js +0 -43
- package/dist/svg/index.d.ts +0 -6
- package/dist/svg/index.js +0 -6
- package/dist/svg/layers.d.ts +0 -18
- package/dist/svg/layers.js +0 -13
- package/dist/svg/shapes.d.ts +0 -1089
- package/dist/svg/shapes.js +0 -119
package/dist/Camera.d.ts
CHANGED
|
@@ -1,49 +1,71 @@
|
|
|
1
|
-
import
|
|
2
|
-
import * as Entity from "./Entity";
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import * as Entity from "./Entity.js";
|
|
3
|
+
import * as Projection from "./Projection.js";
|
|
3
4
|
/**
|
|
4
|
-
* The camera is view state, not a shape — it is never registered with a
|
|
5
|
-
*
|
|
6
|
-
* animators drive it for free: `camera.pipe(moveTo({
|
|
7
|
-
* `tween("
|
|
5
|
+
* The camera is view state, not a shape — it is never registered with a sink
|
|
6
|
+
* and never drawn. It exists as an ordinary Instance so the existing
|
|
7
|
+
* animators drive it for free: `camera.pipe(moveTo({ z: -800 }))`,
|
|
8
|
+
* `tween("rotY", ...)`, `spring`, `Scene.fork`, etc.
|
|
8
9
|
*
|
|
9
|
-
* `~position` (x/y) is the
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* `~position` (now x/y/z) is the camera's world position; `rotX/rotY/rotZ`
|
|
11
|
+
* are its Euler orientation; `focalLength` sets the field of view. At rest
|
|
12
|
+
* the camera sits a focal-length back on +z looking down world -z, so a
|
|
13
|
+
* world point at z=0 projects to plain-2D screen coordinates — see
|
|
14
|
+
* `Projection.ts`. The sink reads these off `FrameMeta.camera` and projects
|
|
15
|
+
* every instance through them; instance data stays in world coordinates, so
|
|
16
|
+
* determinism and `moveTo` semantics are untouched by the camera.
|
|
17
|
+
*
|
|
18
|
+
* `z` and `focalLength` have no static schema default: the right resting
|
|
19
|
+
* values are width-relative (After Effects' 50mm-equivalent — see
|
|
20
|
+
* `Projection.defaultFocalLength`), and only the Runner knows the scene
|
|
21
|
+
* width. The Runner fills both at instantiate time for every Camera
|
|
22
|
+
* instance, so by the time animators or the renderer read the data they are
|
|
23
|
+
* always concrete.
|
|
14
24
|
*/
|
|
15
25
|
declare const fields: {
|
|
16
26
|
x: Schema.withConstructorDefault<Schema.Number>;
|
|
17
27
|
y: Schema.withConstructorDefault<Schema.Number>;
|
|
18
|
-
|
|
28
|
+
z: Schema.withConstructorDefault<Schema.Number>;
|
|
29
|
+
rotX: Schema.withConstructorDefault<Schema.Number>;
|
|
30
|
+
rotY: Schema.withConstructorDefault<Schema.Number>;
|
|
31
|
+
rotZ: Schema.withConstructorDefault<Schema.Number>;
|
|
32
|
+
focalLength: Schema.withConstructorDefault<Schema.Number>;
|
|
33
|
+
focusDistance: Schema.withConstructorDefault<Schema.Number>;
|
|
34
|
+
aperture: Schema.withConstructorDefault<Schema.Number>;
|
|
35
|
+
poiX: Schema.optionalKey<Schema.Number>;
|
|
36
|
+
poiY: Schema.optionalKey<Schema.Number>;
|
|
37
|
+
poiZ: Schema.optionalKey<Schema.Number>;
|
|
19
38
|
};
|
|
20
39
|
type CameraData = Schema.Struct<typeof fields>["Type"];
|
|
21
40
|
export declare const Camera: Entity.Entity<"Camera", Schema.Struct<{
|
|
22
41
|
x: Schema.withConstructorDefault<Schema.Number>;
|
|
23
42
|
y: Schema.withConstructorDefault<Schema.Number>;
|
|
24
|
-
|
|
43
|
+
z: Schema.withConstructorDefault<Schema.Number>;
|
|
44
|
+
rotX: Schema.withConstructorDefault<Schema.Number>;
|
|
45
|
+
rotY: Schema.withConstructorDefault<Schema.Number>;
|
|
46
|
+
rotZ: Schema.withConstructorDefault<Schema.Number>;
|
|
47
|
+
focalLength: Schema.withConstructorDefault<Schema.Number>;
|
|
48
|
+
focusDistance: Schema.withConstructorDefault<Schema.Number>;
|
|
49
|
+
aperture: Schema.withConstructorDefault<Schema.Number>;
|
|
50
|
+
poiX: Schema.optionalKey<Schema.Number>;
|
|
51
|
+
poiY: Schema.optionalKey<Schema.Number>;
|
|
52
|
+
poiZ: Schema.optionalKey<Schema.Number>;
|
|
25
53
|
}>, {
|
|
26
54
|
readonly "~position": {
|
|
27
55
|
get: (data: CameraData) => {
|
|
28
56
|
x: number;
|
|
29
57
|
y: number;
|
|
58
|
+
z: number;
|
|
30
59
|
};
|
|
31
60
|
set: (data: CameraData, value: Entity.Position) => CameraData;
|
|
32
61
|
};
|
|
33
|
-
}, {
|
|
34
|
-
readonly x?: number;
|
|
35
|
-
readonly y?: number;
|
|
36
|
-
readonly zoom?: number;
|
|
37
62
|
}>;
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
readonly y: number;
|
|
47
|
-
readonly zoom: number;
|
|
48
|
-
}
|
|
63
|
+
/**
|
|
64
|
+
* The identity view for a comp of the given width: resting camera, no
|
|
65
|
+
* rotation, the width-relative default focal length (AE's 50mm equivalent).
|
|
66
|
+
* Projects z=0 content to its plain-2D screen position at scale 1.
|
|
67
|
+
*/
|
|
68
|
+
export declare const identity: (width: number) => CameraState;
|
|
69
|
+
/** The camera view carried on each frame (POI rides along when set). */
|
|
70
|
+
export type CameraState = Projection.CameraView & Projection.PointOfInterest;
|
|
49
71
|
export {};
|
package/dist/Camera.js
CHANGED
|
@@ -1,33 +1,85 @@
|
|
|
1
|
-
import
|
|
2
|
-
import * as
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import * as Entity from "./Entity.js";
|
|
4
|
+
import * as Projection from "./Projection.js";
|
|
5
|
+
import * as Shape2D from "./shapes/Shape2D.js";
|
|
3
6
|
/**
|
|
4
|
-
* The camera is view state, not a shape — it is never registered with a
|
|
5
|
-
*
|
|
6
|
-
* animators drive it for free: `camera.pipe(moveTo({
|
|
7
|
-
* `tween("
|
|
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.
|
|
8
11
|
*
|
|
9
|
-
* `~position` (x/y) is the
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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.
|
|
14
26
|
*/
|
|
15
27
|
const fields = {
|
|
16
|
-
|
|
17
|
-
|
|
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),
|
|
18
49
|
};
|
|
19
50
|
export const Camera = Entity.make("Camera", fields, {
|
|
20
|
-
//
|
|
21
|
-
// Inlined (not positionLens()) so the data
|
|
22
|
-
//
|
|
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.
|
|
23
54
|
"~position": {
|
|
24
|
-
|
|
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 }),
|
|
25
58
|
set: (data, value) => ({
|
|
26
59
|
...data,
|
|
27
60
|
x: value.x,
|
|
28
61
|
y: value.y,
|
|
62
|
+
z: value.z,
|
|
29
63
|
}),
|
|
30
64
|
},
|
|
31
65
|
});
|
|
32
|
-
/**
|
|
33
|
-
|
|
66
|
+
/**
|
|
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.
|
|
70
|
+
*/
|
|
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,
|
|
84
|
+
};
|
|
85
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as Duration from "effect/Duration";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import type { Camera } from "./Camera.js";
|
|
4
|
+
import * as Entity from "./Entity.js";
|
|
5
|
+
import * as Instance from "./Instance.js";
|
|
6
|
+
import * as Runner from "./Runner.js";
|
|
7
|
+
import * as Timing from "./Timing.js";
|
|
8
|
+
/**
|
|
9
|
+
* The public Camera surface: the entity/identity from Camera.js plus the
|
|
10
|
+
* directing helpers. This module (not Camera.ts) is what index exports as
|
|
11
|
+
* the `Camera` namespace — Runner/Renderer import the schema file
|
|
12
|
+
* directly, so helpers can depend on Motion/Scene without an import
|
|
13
|
+
* cycle.
|
|
14
|
+
*
|
|
15
|
+
* Naming rule (recorded in the camera-poi-helpers change): verbs that
|
|
16
|
+
* name their target (`lookAt`, `follow`) have no base/To pair — an
|
|
17
|
+
* optional duration selects instant vs eased. Value-animating helpers
|
|
18
|
+
* (`orbit`/`orbitTo`, `dolly`/`dollyTo`) keep the pair, exactly like
|
|
19
|
+
* `move`/`moveTo`.
|
|
20
|
+
*/
|
|
21
|
+
export { Camera, identity } from "./Camera.js";
|
|
22
|
+
export type { CameraState } from "./Camera.js";
|
|
23
|
+
type AnyInstance = Instance.Instance<any, any, any>;
|
|
24
|
+
/**
|
|
25
|
+
* A helper target: an Instance (position read live each frame), an Effect
|
|
26
|
+
* resolving to one (resolved once at helper start, then read live), or a
|
|
27
|
+
* plain position (inherently fixed — the no-entity escape hatch).
|
|
28
|
+
*/
|
|
29
|
+
export type CameraTarget = AnyInstance | Effect.Effect<AnyInstance, never, Runner.Runner> | Partial<Entity.Position>;
|
|
30
|
+
type CamData = (typeof Camera)["data"];
|
|
31
|
+
type CamTraits = (typeof Camera)["traits"];
|
|
32
|
+
type CamOrEffect<E = never, R = Runner.Runner> = Instance.InstanceOrEffect<"Camera", CamData, CamTraits, E, R>;
|
|
33
|
+
type CamInstance = Instance.Instance<"Camera", CamData, CamTraits>;
|
|
34
|
+
type CamEffect = Effect.Effect<CamInstance, never, Runner.Runner>;
|
|
35
|
+
/**
|
|
36
|
+
* Aim the camera at a target. No duration: set the point of interest this
|
|
37
|
+
* frame. With a duration: eased re-aim as a retargeted tween (lands
|
|
38
|
+
* exactly on a moving target, no terminal snap). `offset` shifts the aim
|
|
39
|
+
* relative to the target ("slightly above their head"). Dual:
|
|
40
|
+
* `lookAt(cam, target, ...)` or `cam.pipe(lookAt(target, ...))`.
|
|
41
|
+
*/
|
|
42
|
+
export declare const lookAt: ((target: CameraTarget, duration?: Duration.Input, timing?: Timing.TimingInput, offset?: Partial<Entity.Position>) => (cam: CamOrEffect) => CamEffect) & ((cam: CamOrEffect, target: CameraTarget, duration?: Duration.Input, timing?: Timing.TimingInput, offset?: Partial<Entity.Position>) => CamEffect);
|
|
43
|
+
/**
|
|
44
|
+
* Track a target for the duration: the point of interest is a hard
|
|
45
|
+
* per-frame copy of the target position (+offset). A plain animator — it
|
|
46
|
+
* pipes, `Scene.all`s, staggers, repeats. No timing input (lag is
|
|
47
|
+
* expressed by springing the POI instead). Ordering practice: within a
|
|
48
|
+
* tick, branches run in fork order — a follow forked before its target's
|
|
49
|
+
* animator reads the previous frame's position, a deterministic one-frame
|
|
50
|
+
* trail. Dual like `lookAt`.
|
|
51
|
+
*/
|
|
52
|
+
export declare const follow: ((target: CameraTarget, duration: Duration.Input, offset?: Partial<Entity.Position>) => (cam: CamOrEffect) => CamEffect) & ((cam: CamOrEffect, target: CameraTarget, duration: Duration.Input, offset?: Partial<Entity.Position>) => CamEffect);
|
|
53
|
+
/**
|
|
54
|
+
* Turntable the camera to an absolute azimuth around its point of
|
|
55
|
+
* interest (angle about the world-Y axis through the POI; 0 = directly +z
|
|
56
|
+
* of it). Position travels the arc — orientation comes entirely from the
|
|
57
|
+
* POI, so there is no orientation math to get wrong. Radius and height
|
|
58
|
+
* are preserved. Dies loudly without a POI. Dual like `moveTo`.
|
|
59
|
+
*/
|
|
60
|
+
export declare const orbitTo: ((azimuth: number, duration: Duration.Input, timing?: Timing.TimingInput) => (cam: CamOrEffect) => CamEffect) & ((cam: CamOrEffect, azimuth: number, duration: Duration.Input, timing?: Timing.TimingInput) => CamEffect);
|
|
61
|
+
/** Like `orbitTo`, but from an explicit start azimuth. */
|
|
62
|
+
export declare const orbit: ((from: number, to: number, duration: Duration.Input, timing?: Timing.TimingInput) => (cam: CamOrEffect) => CamEffect) & ((cam: CamOrEffect, from: number, to: number, duration: Duration.Input, timing?: Timing.TimingInput) => CamEffect);
|
|
63
|
+
/**
|
|
64
|
+
* Move the camera along its view axis to an absolute distance from the
|
|
65
|
+
* point of interest (in: toward it, out: away), aim unchanged. Dies
|
|
66
|
+
* loudly without a POI. Dual like `moveTo`.
|
|
67
|
+
*/
|
|
68
|
+
export declare const dollyTo: ((distance: number, duration: Duration.Input, timing?: Timing.TimingInput) => (cam: CamOrEffect) => CamEffect) & ((cam: CamOrEffect, distance: number, duration: Duration.Input, timing?: Timing.TimingInput) => CamEffect);
|
|
69
|
+
/** Like `dollyTo`, but from an explicit start distance. */
|
|
70
|
+
export declare const dolly: ((from: number, to: number, duration: Duration.Input, timing?: Timing.TimingInput) => (cam: CamOrEffect) => CamEffect) & ((cam: CamOrEffect, from: number, to: number, duration: Duration.Input, timing?: Timing.TimingInput) => CamEffect);
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import * as Duration from "effect/Duration";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import { dual } from "effect/Function";
|
|
4
|
+
import * as Entity from "./Entity.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
|
+
/**
|
|
12
|
+
* The public Camera surface: the entity/identity from Camera.js plus the
|
|
13
|
+
* directing helpers. This module (not Camera.ts) is what index exports as
|
|
14
|
+
* the `Camera` namespace — Runner/Renderer import the schema file
|
|
15
|
+
* directly, so helpers can depend on Motion/Scene without an import
|
|
16
|
+
* cycle.
|
|
17
|
+
*
|
|
18
|
+
* Naming rule (recorded in the camera-poi-helpers change): verbs that
|
|
19
|
+
* name their target (`lookAt`, `follow`) have no base/To pair — an
|
|
20
|
+
* optional duration selects instant vs eased. Value-animating helpers
|
|
21
|
+
* (`orbit`/`orbitTo`, `dolly`/`dollyTo`) keep the pair, exactly like
|
|
22
|
+
* `move`/`moveTo`.
|
|
23
|
+
*/
|
|
24
|
+
// explicit named re-exports (not `export *`) so the public Camera surface
|
|
25
|
+
// is enumerable at a glance: the entity + identity from the schema module,
|
|
26
|
+
// plus the helpers below
|
|
27
|
+
export { Camera, identity } from "./Camera.js";
|
|
28
|
+
// a target argument (vs a duration/timing in the same slot): instances,
|
|
29
|
+
// effects, or a position-like object — Durations are objects too, so
|
|
30
|
+
// exclude them explicitly
|
|
31
|
+
const isTargetArg = (v) => Instance.isInstance(v) ||
|
|
32
|
+
Effect.isEffect(v) ||
|
|
33
|
+
(typeof v === "object" &&
|
|
34
|
+
v !== null &&
|
|
35
|
+
!Duration.isDuration(v) &&
|
|
36
|
+
("x" in v || "y" in v || "z" in v));
|
|
37
|
+
// data-first iff the first arg is an instance AND the second is a target —
|
|
38
|
+
// plain firstArgIsInstance would misread `cam.pipe`-less pipeable calls
|
|
39
|
+
// whose TARGET is an instance (`lookAt(hero, "1 second")`)
|
|
40
|
+
const dataFirst = (args) => Instance.isInstance(args[0]) && isTargetArg(args[1]);
|
|
41
|
+
/**
|
|
42
|
+
* Resolve a target once (Effects yield their Instance), returning a
|
|
43
|
+
* per-frame position reader with the offset folded in: live for
|
|
44
|
+
* instances, fixed for plain positions.
|
|
45
|
+
*/
|
|
46
|
+
const targetReader = Effect.fnUntraced(function* (target, offset) {
|
|
47
|
+
const ox = offset?.x ?? 0;
|
|
48
|
+
const oy = offset?.y ?? 0;
|
|
49
|
+
const oz = offset?.z ?? 0;
|
|
50
|
+
if (Instance.isInstance(target) || Effect.isEffect(target)) {
|
|
51
|
+
const instance = yield* Instance.flatten(target);
|
|
52
|
+
const lens = Entity.traitOrDie(instance.entity, "~position");
|
|
53
|
+
return Scene.data(instance).pipe(Effect.map((data) => {
|
|
54
|
+
const p = lens.get(data);
|
|
55
|
+
return { x: p.x + ox, y: p.y + oy, z: p.z + oz };
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
// AnyInstance's `any` params defeat narrowing — the guards above
|
|
59
|
+
// returned for instances/effects, so this is a plain position
|
|
60
|
+
const point = target;
|
|
61
|
+
const fixed = {
|
|
62
|
+
x: (point.x ?? 0) + ox,
|
|
63
|
+
y: (point.y ?? 0) + oy,
|
|
64
|
+
z: (point.z ?? 0) + oz,
|
|
65
|
+
};
|
|
66
|
+
return Effect.succeed(fixed);
|
|
67
|
+
});
|
|
68
|
+
const setPoi = (data, p) => Object.assign({}, data, { poiX: p.x, poiY: p.y, poiZ: p.z });
|
|
69
|
+
// the camera's WORLD position: x/y are pan-from-viewport-center
|
|
70
|
+
const worldPosition = Effect.fnUntraced(function* (cam) {
|
|
71
|
+
const { settings } = yield* Runner.Runner;
|
|
72
|
+
const data = (yield* Scene.data(cam));
|
|
73
|
+
return {
|
|
74
|
+
x: settings.width / 2 + data.x,
|
|
75
|
+
y: settings.height / 2 + data.y,
|
|
76
|
+
z: data.z ?? 0,
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
const lookAtImpl = Effect.fnUntraced(function* (camOrEffect, target, duration, timing, offset) {
|
|
80
|
+
const cam = yield* Instance.flatten(camOrEffect);
|
|
81
|
+
const read = yield* targetReader(target, offset);
|
|
82
|
+
if (duration === undefined) {
|
|
83
|
+
const p = yield* read;
|
|
84
|
+
yield* Scene.update(cam, (d) => setPoi(d, p));
|
|
85
|
+
return cam;
|
|
86
|
+
}
|
|
87
|
+
// eased re-aim: a RETARGETED tween — each frame interpolates from the
|
|
88
|
+
// start POI toward the target's CURRENT position, converging exactly
|
|
89
|
+
// onto a moving target at t = 1 (a plain-Position target degenerates to
|
|
90
|
+
// a fixed tween). Effectful per-frame read, so this runs its own loop
|
|
91
|
+
// rather than Motion.drive (whose callback is pure).
|
|
92
|
+
const runner = yield* Runner.Runner;
|
|
93
|
+
const timingFn = Timing.resolve(timing ?? "linear");
|
|
94
|
+
const data = (yield* Scene.data(cam));
|
|
95
|
+
let start;
|
|
96
|
+
if (data.poiX !== undefined &&
|
|
97
|
+
data.poiY !== undefined &&
|
|
98
|
+
data.poiZ !== undefined) {
|
|
99
|
+
start = { x: data.poiX, y: data.poiY, z: data.poiZ };
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
// no POI yet: seed on the camera's UNAIMED axis (straight down world
|
|
103
|
+
// -z) at the target's distance. resolveCamera derives zero aim for
|
|
104
|
+
// that point, so the explicit Euler alone carries the view — engaging
|
|
105
|
+
// POI mode is snap-free for ANY current orientation, and the tween
|
|
106
|
+
// takes over from there.
|
|
107
|
+
const world = yield* worldPosition(cam);
|
|
108
|
+
const first = yield* read;
|
|
109
|
+
const distance = Math.hypot(first.x - world.x, first.y - world.y, first.z - world.z);
|
|
110
|
+
start = { x: world.x, y: world.y, z: world.z - distance };
|
|
111
|
+
}
|
|
112
|
+
const frames = Math.max(1, Time.toFrames(duration, runner.settings.frameRate));
|
|
113
|
+
for (let i = 1; i <= frames; i++) {
|
|
114
|
+
const t = timingFn(i / frames);
|
|
115
|
+
const p = yield* read;
|
|
116
|
+
yield* Scene.update(cam, (d) => setPoi(d, {
|
|
117
|
+
x: start.x + (p.x - start.x) * t,
|
|
118
|
+
y: start.y + (p.y - start.y) * t,
|
|
119
|
+
z: start.z + (p.z - start.z) * t,
|
|
120
|
+
}));
|
|
121
|
+
yield* Scene.tick;
|
|
122
|
+
}
|
|
123
|
+
return cam;
|
|
124
|
+
});
|
|
125
|
+
/**
|
|
126
|
+
* Aim the camera at a target. No duration: set the point of interest this
|
|
127
|
+
* frame. With a duration: eased re-aim as a retargeted tween (lands
|
|
128
|
+
* exactly on a moving target, no terminal snap). `offset` shifts the aim
|
|
129
|
+
* relative to the target ("slightly above their head"). Dual:
|
|
130
|
+
* `lookAt(cam, target, ...)` or `cam.pipe(lookAt(target, ...))`.
|
|
131
|
+
*/
|
|
132
|
+
export const lookAt = dual(dataFirst, lookAtImpl);
|
|
133
|
+
/**
|
|
134
|
+
* Track a target for the duration: the point of interest is a hard
|
|
135
|
+
* per-frame copy of the target position (+offset). A plain animator — it
|
|
136
|
+
* pipes, `Scene.all`s, staggers, repeats. No timing input (lag is
|
|
137
|
+
* expressed by springing the POI instead). Ordering practice: within a
|
|
138
|
+
* tick, branches run in fork order — a follow forked before its target's
|
|
139
|
+
* animator reads the previous frame's position, a deterministic one-frame
|
|
140
|
+
* trail. Dual like `lookAt`.
|
|
141
|
+
*/
|
|
142
|
+
export const follow = dual(dataFirst, Effect.fnUntraced(function* (camOrEffect, target, duration, offset) {
|
|
143
|
+
const cam = yield* Instance.flatten(camOrEffect);
|
|
144
|
+
const read = yield* targetReader(target, offset);
|
|
145
|
+
const runner = yield* Runner.Runner;
|
|
146
|
+
const frames = Math.max(1, Time.toFrames(duration, runner.settings.frameRate));
|
|
147
|
+
for (let i = 1; i <= frames; i++) {
|
|
148
|
+
const p = yield* read;
|
|
149
|
+
yield* Scene.update(cam, (d) => setPoi(d, p));
|
|
150
|
+
yield* Scene.tick;
|
|
151
|
+
}
|
|
152
|
+
return cam;
|
|
153
|
+
}));
|
|
154
|
+
// orbit/dolly are defined relative to the POI — die loudly without one
|
|
155
|
+
const poiOrDie = (data) => {
|
|
156
|
+
if (data.poiX === undefined ||
|
|
157
|
+
data.poiY === undefined ||
|
|
158
|
+
data.poiZ === undefined) {
|
|
159
|
+
throw new Error("Camera: orbit/dolly need a point of interest — set one first (Camera.lookAt(target))");
|
|
160
|
+
}
|
|
161
|
+
return { x: data.poiX, y: data.poiY, z: data.poiZ };
|
|
162
|
+
};
|
|
163
|
+
const orbitImpl = Effect.fnUntraced(function* (camOrEffect, from, to, duration, timing) {
|
|
164
|
+
const cam = yield* Instance.flatten(camOrEffect);
|
|
165
|
+
const { settings } = yield* Runner.Runner;
|
|
166
|
+
const origin = { x: settings.width / 2, y: settings.height / 2 };
|
|
167
|
+
const startData = (yield* Scene.data(cam));
|
|
168
|
+
const poi = poiOrDie(startData);
|
|
169
|
+
const world = {
|
|
170
|
+
x: origin.x + startData.x,
|
|
171
|
+
z: startData.z ?? 0,
|
|
172
|
+
};
|
|
173
|
+
// azimuth 0 = directly +z of the POI (the resting side); radius = the
|
|
174
|
+
// current horizontal distance, preserved through the arc; height too
|
|
175
|
+
const radius = Math.hypot(world.x - poi.x, world.z - poi.z);
|
|
176
|
+
const startAzimuth = from ?? Math.atan2(world.x - poi.x, world.z - poi.z);
|
|
177
|
+
return yield* Motion.drive(cam, duration, timing ?? "linear", (t, d) => {
|
|
178
|
+
const data = d;
|
|
179
|
+
// POI read from live data: orbiting a moving POI stays centered on it
|
|
180
|
+
const p = poiOrDie(data);
|
|
181
|
+
const angle = startAzimuth + (to - startAzimuth) * t;
|
|
182
|
+
return Object.assign({}, d, {
|
|
183
|
+
x: p.x + radius * Math.sin(angle) - origin.x,
|
|
184
|
+
z: p.z + radius * Math.cos(angle),
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
/**
|
|
189
|
+
* Turntable the camera to an absolute azimuth around its point of
|
|
190
|
+
* interest (angle about the world-Y axis through the POI; 0 = directly +z
|
|
191
|
+
* of it). Position travels the arc — orientation comes entirely from the
|
|
192
|
+
* POI, so there is no orientation math to get wrong. Radius and height
|
|
193
|
+
* are preserved. Dies loudly without a POI. Dual like `moveTo`.
|
|
194
|
+
*/
|
|
195
|
+
export const orbitTo = dual((args) => Instance.isInstance(args[0]), ((cam, azimuth, duration, timing) => orbitImpl(cam, undefined, azimuth, duration, timing)));
|
|
196
|
+
/** Like `orbitTo`, but from an explicit start azimuth. */
|
|
197
|
+
export const orbit = dual((args) => Instance.isInstance(args[0]), orbitImpl);
|
|
198
|
+
const dollyImpl = Effect.fnUntraced(function* (camOrEffect, from, to, duration, timing) {
|
|
199
|
+
const cam = yield* Instance.flatten(camOrEffect);
|
|
200
|
+
const { settings } = yield* Runner.Runner;
|
|
201
|
+
const origin = { x: settings.width / 2, y: settings.height / 2 };
|
|
202
|
+
const startData = (yield* Scene.data(cam));
|
|
203
|
+
const poi = poiOrDie(startData);
|
|
204
|
+
const world = {
|
|
205
|
+
x: origin.x + startData.x,
|
|
206
|
+
y: origin.y + startData.y,
|
|
207
|
+
z: startData.z ?? 0,
|
|
208
|
+
};
|
|
209
|
+
const startDistance = Math.hypot(world.x - poi.x, world.y - poi.y, world.z - poi.z);
|
|
210
|
+
// the fixed unit direction POI → camera; a zero distance has no
|
|
211
|
+
// direction to dolly along
|
|
212
|
+
if (startDistance === 0) {
|
|
213
|
+
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");
|
|
214
|
+
}
|
|
215
|
+
const u = {
|
|
216
|
+
x: (world.x - poi.x) / startDistance,
|
|
217
|
+
y: (world.y - poi.y) / startDistance,
|
|
218
|
+
z: (world.z - poi.z) / startDistance,
|
|
219
|
+
};
|
|
220
|
+
const d0 = from ?? startDistance;
|
|
221
|
+
return yield* Motion.drive(cam, duration, timing ?? "linear", (t, d) => {
|
|
222
|
+
const data = d;
|
|
223
|
+
const p = poiOrDie(data);
|
|
224
|
+
const dist = d0 + (to - d0) * t;
|
|
225
|
+
return Object.assign({}, d, {
|
|
226
|
+
x: p.x + u.x * dist - origin.x,
|
|
227
|
+
y: p.y + u.y * dist - origin.y,
|
|
228
|
+
z: p.z + u.z * dist,
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
/**
|
|
233
|
+
* Move the camera along its view axis to an absolute distance from the
|
|
234
|
+
* point of interest (in: toward it, out: away), aim unchanged. Dies
|
|
235
|
+
* loudly without a POI. Dual like `moveTo`.
|
|
236
|
+
*/
|
|
237
|
+
export const dollyTo = dual((args) => Instance.isInstance(args[0]), ((cam, distance, duration, timing) => dollyImpl(cam, undefined, distance, duration, timing)));
|
|
238
|
+
/** Like `dollyTo`, but from an explicit start distance. */
|
|
239
|
+
export const dolly = dual((args) => Instance.isInstance(args[0]), dollyImpl);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { EffectMotionError } from "./EffectMotionError.js";
|
|
3
|
+
import type * as Renderer from "./Renderer.js";
|
|
4
|
+
/**
|
|
5
|
+
* Blit a framebuffer onto a DOM canvas. The canvas buffer takes the
|
|
6
|
+
* framebuffer's PHYSICAL size (logical × dpr); CSS display size is left to
|
|
7
|
+
* the caller — the intrinsic aspect ratio is preserved by the attributes, so
|
|
8
|
+
* responsive styling (`width: 100%`) or a fixed `fb.logicalWidth`px both
|
|
9
|
+
* work. Writing CSS here would clobber caller styling (e.g. React inline
|
|
10
|
+
* styles are not re-applied on rerender).
|
|
11
|
+
*/
|
|
12
|
+
export declare const toCanvas: (fb: Renderer.Framebuffer, target: "string" | HTMLCanvasElement) => Effect.Effect<undefined, EffectMotionError, never>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { EffectMotionError } from "./EffectMotionError.js";
|
|
3
|
+
const resolveCanvas = Effect.fnUntraced(function* (target) {
|
|
4
|
+
if (typeof target === "string") {
|
|
5
|
+
if (typeof document === "undefined") {
|
|
6
|
+
return yield* Effect.fail(EffectMotionError.of("'document' is not available in the current environment"));
|
|
7
|
+
}
|
|
8
|
+
const maybeCanvas = document.querySelector(target);
|
|
9
|
+
if (maybeCanvas === null) {
|
|
10
|
+
return yield* Effect.fail(EffectMotionError.of(`Could not find canvas with selector '${target}'`));
|
|
11
|
+
}
|
|
12
|
+
if (!(maybeCanvas instanceof HTMLCanvasElement)) {
|
|
13
|
+
return yield* Effect.fail(EffectMotionError.of(`Could not find canvas with selector '${target}'`));
|
|
14
|
+
}
|
|
15
|
+
return maybeCanvas;
|
|
16
|
+
}
|
|
17
|
+
if (!(target instanceof HTMLCanvasElement)) {
|
|
18
|
+
return yield* Effect.fail(EffectMotionError.of(`'target' is not a HTMLCanvasElement`));
|
|
19
|
+
}
|
|
20
|
+
return target;
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* Blit a framebuffer onto a DOM canvas. The canvas buffer takes the
|
|
24
|
+
* framebuffer's PHYSICAL size (logical × dpr); CSS display size is left to
|
|
25
|
+
* the caller — the intrinsic aspect ratio is preserved by the attributes, so
|
|
26
|
+
* responsive styling (`width: 100%`) or a fixed `fb.logicalWidth`px both
|
|
27
|
+
* work. Writing CSS here would clobber caller styling (e.g. React inline
|
|
28
|
+
* styles are not re-applied on rerender).
|
|
29
|
+
*/
|
|
30
|
+
export const toCanvas = Effect.fnUntraced(function* (fb, target) {
|
|
31
|
+
const canvas = yield* resolveCanvas(target);
|
|
32
|
+
canvas.width = fb.width;
|
|
33
|
+
canvas.height = fb.height;
|
|
34
|
+
const ctx = canvas.getContext("2d");
|
|
35
|
+
if (ctx === null) {
|
|
36
|
+
return yield* Effect.fail(EffectMotionError.of("Could not get a 2D context from the canvas"));
|
|
37
|
+
}
|
|
38
|
+
const clamped = Uint8ClampedArray.from(fb.rgba);
|
|
39
|
+
ctx.putImageData(new ImageData(clamped, fb.width, fb.height), 0, 0);
|
|
40
|
+
});
|