effect-motion 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +3 -3
  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 +684 -30
  9. package/dist/Entity.js +281 -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 +36 -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 +714 -349
  25. package/dist/Runner.js +197 -150
  26. package/dist/Scene.d.ts +603 -175
  27. package/dist/Scene.js +596 -112
  28. package/dist/Timing.d.ts +170 -13
  29. package/dist/Timing.js +124 -6
  30. package/dist/Tree.d.ts +39 -0
  31. package/dist/Tree.js +127 -0
  32. package/dist/index.d.ts +54 -5
  33. package/dist/index.js +56 -5
  34. package/dist/particles/Particle.d.ts +2 -2
  35. package/dist/particles/ParticleField.d.ts +44 -42
  36. package/dist/particles/ParticleField.js +8 -9
  37. package/dist/particles/constructors.d.ts +5 -6
  38. package/dist/particles/constructors.js +8 -3
  39. package/dist/particles/legacy.d.ts +58 -0
  40. package/dist/particles/legacy.js +46 -0
  41. package/dist/particles/simulate.js +11 -3
  42. package/dist/particles/step.js +14 -10
  43. package/dist/types.d.ts +5 -0
  44. package/dist/types.js +1 -0
  45. package/package.json +58 -60
  46. package/dist/CameraHelpers.d.ts +0 -70
  47. package/dist/CameraHelpers.js +0 -239
  48. package/dist/CanvasExporter.d.ts +0 -12
  49. package/dist/CanvasExporter.js +0 -40
  50. package/dist/Fonts.d.ts +0 -41
  51. package/dist/Fonts.js +0 -27
  52. package/dist/Images.d.ts +0 -33
  53. package/dist/Images.js +0 -24
  54. package/dist/PngExporter.d.ts +0 -6
  55. package/dist/PngExporter.js +0 -85
  56. package/dist/Renderer.d.ts +0 -118
  57. package/dist/Renderer.js +0 -508
  58. package/dist/Shapes.d.ts +0 -11
  59. package/dist/Shapes.js +0 -11
  60. package/dist/demo.d.ts +0 -3
  61. package/dist/demo.js +0 -71
  62. package/dist/render/dof.d.ts +0 -27
  63. package/dist/render/dof.js +0 -37
  64. package/dist/render/paint.d.ts +0 -30
  65. package/dist/render/paint.js +0 -36
  66. package/dist/render/shapes.d.ts +0 -42
  67. package/dist/render/shapes.js +0 -310
  68. package/dist/shapes/Circle.d.ts +0 -32
  69. package/dist/shapes/Circle.js +0 -9
  70. package/dist/shapes/Ellipse.d.ts +0 -35
  71. package/dist/shapes/Ellipse.js +0 -10
  72. package/dist/shapes/Group.d.ts +0 -126
  73. package/dist/shapes/Group.js +0 -89
  74. package/dist/shapes/Hud.d.ts +0 -38
  75. package/dist/shapes/Hud.js +0 -35
  76. package/dist/shapes/Image.d.ts +0 -45
  77. package/dist/shapes/Image.js +0 -28
  78. package/dist/shapes/Line.d.ts +0 -47
  79. package/dist/shapes/Line.js +0 -35
  80. package/dist/shapes/Path.d.ts +0 -107
  81. package/dist/shapes/Path.js +0 -32
  82. package/dist/shapes/Rect.d.ts +0 -51
  83. package/dist/shapes/Rect.js +0 -22
  84. package/dist/shapes/Shape2D.d.ts +0 -49
  85. package/dist/shapes/Shape2D.js +0 -52
  86. package/dist/shapes/Shapes.d.ts +0 -11
  87. package/dist/shapes/Shapes.js +0 -11
  88. package/dist/shapes/Square.d.ts +0 -32
  89. package/dist/shapes/Square.js +0 -11
  90. package/dist/shapes/Text.d.ts +0 -51
  91. package/dist/shapes/Text.js +0 -24
package/package.json CHANGED
@@ -1,61 +1,59 @@
1
1
  {
2
- "name": "effect-motion",
3
- "version": "0.4.0",
4
- "description": "Deterministic, frame-exact motion graphics in code, composed with Effect",
5
- "type": "module",
6
- "license": "MIT",
7
- "repository": {
8
- "type": "git",
9
- "url": "git+https://github.com/julia-script/effect-motion.git",
10
- "directory": "packages/motion"
11
- },
12
- "homepage": "https://github.com/julia-script/effect-motion#readme",
13
- "bugs": "https://github.com/julia-script/effect-motion/issues",
14
- "keywords": [
15
- "effect",
16
- "motion",
17
- "animation",
18
- "motion-graphics",
19
- "svg",
20
- "deterministic"
21
- ],
22
- "exports": {
23
- ".": {
24
- "types": "./dist/index.d.ts",
25
- "default": "./dist/index.js"
26
- },
27
- "./*": {
28
- "types": "./dist/*.d.ts",
29
- "default": "./dist/*.js"
30
- }
31
- },
32
- "files": [
33
- "dist"
34
- ],
35
- "publishConfig": {
36
- "access": "public"
37
- },
38
- "peerDependencies": {
39
- "effect": ">=4.0.0-beta.98"
40
- },
41
- "devDependencies": {
42
- "@types/chroma-js": "^3.1.2",
43
- "@types/node": "^26.1.1",
44
- "effect": "4.0.0-beta.98",
45
- "happy-dom": "^20.10.6",
46
- "tsx": "^4.23.0",
47
- "typescript": "^7.0.2",
48
- "vitest": "^4.1.10"
49
- },
50
- "dependencies": {
51
- "@effect/platform-node": "4.0.0-beta.98",
52
- "chroma-js": "^3.2.0",
53
- "@effect-motion/thorvg": "^0.2.0"
54
- },
55
- "scripts": {
56
- "build": "tsc -p tsconfig.build.json",
57
- "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
58
- "test": "vitest run",
59
- "check": "tsc --noEmit"
60
- }
61
- }
2
+ "name": "effect-motion",
3
+ "version": "0.6.0",
4
+ "description": "Deterministic, frame-exact motion graphics in code, composed with Effect",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/julia-script/effect-motion.git",
10
+ "directory": "packages/motion"
11
+ },
12
+ "homepage": "https://github.com/julia-script/effect-motion#readme",
13
+ "bugs": "https://github.com/julia-script/effect-motion/issues",
14
+ "keywords": [
15
+ "effect",
16
+ "motion",
17
+ "animation",
18
+ "motion-graphics",
19
+ "svg",
20
+ "deterministic"
21
+ ],
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "default": "./dist/index.js"
26
+ },
27
+ "./*": {
28
+ "types": "./dist/*.d.ts",
29
+ "default": "./dist/*.js"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "scripts": {
36
+ "build": "tsc -p tsconfig.build.json",
37
+ "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
38
+ "test": "vitest run",
39
+ "check": "tsc --noEmit"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "peerDependencies": {
45
+ "effect": ">=4.0.0-rc.115"
46
+ },
47
+ "devDependencies": {
48
+ "@types/chroma-js": "^3.1.2",
49
+ "@types/node": "^26.1.1",
50
+ "effect": "4.0.0-rc.115",
51
+ "happy-dom": "^20.10.6",
52
+ "tsx": "^4.23.0",
53
+ "typescript": "^7.0.2",
54
+ "vitest": "^4.1.10"
55
+ },
56
+ "dependencies": {
57
+ "chroma-js": "^3.2.0"
58
+ }
59
+ }
@@ -1,70 +0,0 @@
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
- export type { CameraState } from "./Camera.js";
9
- /**
10
- * The public Camera surface: the entity/identity from Camera.js plus the
11
- * directing helpers. This module (not Camera.ts) is what index exports as
12
- * the `Camera` namespace — Runner/Renderer import the schema file
13
- * directly, so helpers can depend on Motion/Scene without an import
14
- * cycle.
15
- *
16
- * Naming rule (recorded in the camera-poi-helpers change): verbs that
17
- * name their target (`lookAt`, `follow`) have no base/To pair — an
18
- * optional duration selects instant vs eased. Value-animating helpers
19
- * (`orbit`/`orbitTo`, `dolly`/`dollyTo`) keep the pair, exactly like
20
- * `move`/`moveTo`.
21
- */
22
- export { Camera, identity } 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);
@@ -1,239 +0,0 @@
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 { comp } = yield* Runner.Runner;
72
- const data = (yield* Scene.data(cam));
73
- return {
74
- x: comp.width / 2 + data.x,
75
- y: comp.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 { comp } = yield* Runner.Runner;
166
- const origin = { x: comp.width / 2, y: comp.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 { comp } = yield* Runner.Runner;
201
- const origin = { x: comp.width / 2, y: comp.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);
@@ -1,12 +0,0 @@
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>;
@@ -1,40 +0,0 @@
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
- });
package/dist/Fonts.d.ts DELETED
@@ -1,41 +0,0 @@
1
- import * as Context from "effect/Context";
2
- /**
3
- * One loadable font face. `family` is the name text entities reference in
4
- * `fontFamily`; `src` carries per-environment sources — browsers load
5
- * `url`, offline rasterizers read `path`. Consumers skip sources they
6
- * can't use. `weight`/`style` are browser variant-matching descriptors;
7
- * file-based rasterizers read them from the font file itself. Declare one
8
- * entry per face (e.g. Inter 400 and Inter 700 are two entries).
9
- */
10
- export interface FontResource {
11
- readonly family: string;
12
- readonly src: {
13
- readonly url?: string;
14
- readonly path?: string;
15
- };
16
- /** CSS font-weight (e.g. 400, 700) */
17
- readonly weight?: number;
18
- readonly style?: "normal" | "italic";
19
- }
20
- /**
21
- * Scene annotation key declaring the fonts a scene's text depends on:
22
- * `scene.annotate(Fonts.Fonts, [...])`. The runtime never reads it — the
23
- * engine cannot measure text, so fonts cannot affect frame data. Consumers
24
- * (the player, export tools) read it to prepare their environment before
25
- * rendering.
26
- */
27
- export declare const Fonts: Context.Reference<readonly FontResource[]>;
28
- /** A scene's declared fonts — empty for scenes never annotated. */
29
- export declare const get: (scene: {
30
- readonly annotations: Context.Context<never>;
31
- }) => ReadonlyArray<FontResource>;
32
- /**
33
- * A scene's declared fonts as a `family -> url` map for the ThorVG engine's
34
- * `fonts` option. Only entries with a `src.url` are included — the ThorVG
35
- * renderer fetches fonts by URL (no filesystem), so `path`-only entries are
36
- * skipped. Merge this over the engine default so declared families load and a
37
- * `sans-serif` entry can override the default sans.
38
- */
39
- export declare const urlMap: (scene: {
40
- readonly annotations: Context.Context<never>;
41
- }) => Record<string, string>;
package/dist/Fonts.js DELETED
@@ -1,27 +0,0 @@
1
- import * as Context from "effect/Context";
2
- /**
3
- * Scene annotation key declaring the fonts a scene's text depends on:
4
- * `scene.annotate(Fonts.Fonts, [...])`. The runtime never reads it — the
5
- * engine cannot measure text, so fonts cannot affect frame data. Consumers
6
- * (the player, export tools) read it to prepare their environment before
7
- * rendering.
8
- */
9
- export const Fonts = Context.Reference("motion/Fonts", { defaultValue: () => [] });
10
- /** A scene's declared fonts — empty for scenes never annotated. */
11
- export const get = (scene) => Context.get(scene.annotations, Fonts);
12
- /**
13
- * A scene's declared fonts as a `family -> url` map for the ThorVG engine's
14
- * `fonts` option. Only entries with a `src.url` are included — the ThorVG
15
- * renderer fetches fonts by URL (no filesystem), so `path`-only entries are
16
- * skipped. Merge this over the engine default so declared families load and a
17
- * `sans-serif` entry can override the default sans.
18
- */
19
- export const urlMap = (scene) => {
20
- const out = {};
21
- for (const font of get(scene)) {
22
- if (font.src.url !== undefined) {
23
- out[font.family] = font.src.url;
24
- }
25
- }
26
- return out;
27
- };
package/dist/Images.d.ts DELETED
@@ -1,33 +0,0 @@
1
- import * as Context from "effect/Context";
2
- /**
3
- * One loadable image asset. `name` is what `Shapes.Image` entities reference
4
- * in their `image` field; `src` carries per-environment sources — browsers
5
- * load `url`, file-based tooling may read `path` (reserved; url-only today,
6
- * same contract as fonts). Consumers skip sources they can't use.
7
- */
8
- export interface ImageResource {
9
- readonly name: string;
10
- readonly src: {
11
- readonly url?: string;
12
- readonly path?: string;
13
- };
14
- }
15
- /**
16
- * Scene annotation key declaring the images a scene's Image entities depend
17
- * on: `scene.annotate(Images.Images, [...])`. The runtime never reads it —
18
- * decoded pixels cannot affect frame data. Consumers (the player, export
19
- * tools) read it to load assets into their render session before playback.
20
- */
21
- export declare const Images: Context.Reference<readonly ImageResource[]>;
22
- /** A scene's declared images — empty for scenes never annotated. */
23
- export declare const get: (scene: {
24
- readonly annotations: Context.Context<never>;
25
- }) => ReadonlyArray<ImageResource>;
26
- /**
27
- * A scene's declared images as a `name -> url` map for the render session's
28
- * `images` option. Only entries with a `src.url` are included — loading is
29
- * fetch-by-URL (no filesystem), so `path`-only entries are skipped.
30
- */
31
- export declare const urlMap: (scene: {
32
- readonly annotations: Context.Context<never>;
33
- }) => Record<string, string>;
package/dist/Images.js DELETED
@@ -1,24 +0,0 @@
1
- import * as Context from "effect/Context";
2
- /**
3
- * Scene annotation key declaring the images a scene's Image entities depend
4
- * on: `scene.annotate(Images.Images, [...])`. The runtime never reads it —
5
- * decoded pixels cannot affect frame data. Consumers (the player, export
6
- * tools) read it to load assets into their render session before playback.
7
- */
8
- export const Images = Context.Reference("motion/Images", { defaultValue: () => [] });
9
- /** A scene's declared images — empty for scenes never annotated. */
10
- export const get = (scene) => Context.get(scene.annotations, Images);
11
- /**
12
- * A scene's declared images as a `name -> url` map for the render session's
13
- * `images` option. Only entries with a `src.url` are included — loading is
14
- * fetch-by-URL (no filesystem), so `path`-only entries are skipped.
15
- */
16
- export const urlMap = (scene) => {
17
- const out = {};
18
- for (const image of get(scene)) {
19
- if (image.src.url !== undefined) {
20
- out[image.name] = image.src.url;
21
- }
22
- }
23
- return out;
24
- };
@@ -1,6 +0,0 @@
1
- import * as Effect from "effect/Effect";
2
- import { FileSystem } from "effect/FileSystem";
3
- import { EffectMotionError } from "./EffectMotionError.js";
4
- import type * as Renderer from "./Renderer.js";
5
- export declare const toBuffer: (fb: Renderer.Framebuffer) => Effect.Effect<Uint8Array<ArrayBuffer>, EffectMotionError, never>;
6
- export declare const toFile: (fb: Renderer.Framebuffer, outPath: string) => Effect.Effect<void, EffectMotionError | import("effect/PlatformError").PlatformError, FileSystem>;