effect-motion 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +2 -2
  2. package/dist/Camera.d.ts +186 -49
  3. package/dist/Camera.js +343 -76
  4. package/dist/Color.d.ts +101 -1
  5. package/dist/Color.js +101 -1
  6. package/dist/EffectMotionError.d.ts +17 -0
  7. package/dist/EffectMotionError.js +17 -0
  8. package/dist/Entity.d.ts +682 -38
  9. package/dist/Entity.js +271 -27
  10. package/dist/Font.d.ts +108 -0
  11. package/dist/Font.js +95 -0
  12. package/dist/Image.d.ts +71 -0
  13. package/dist/Image.js +50 -0
  14. package/dist/Instance.d.ts +73 -11
  15. package/dist/Instance.js +44 -11
  16. package/dist/Motion.d.ts +328 -53
  17. package/dist/Motion.js +278 -46
  18. package/dist/Physics.d.ts +154 -20
  19. package/dist/Physics.js +92 -11
  20. package/dist/Projection.d.ts +37 -132
  21. package/dist/Projection.js +33 -292
  22. package/dist/Resource.d.ts +26 -0
  23. package/dist/Resource.js +41 -0
  24. package/dist/Runner.d.ts +639 -350
  25. package/dist/Runner.js +197 -150
  26. package/dist/Scene.d.ts +603 -175
  27. package/dist/Scene.js +596 -112
  28. package/dist/Timing.d.ts +170 -13
  29. package/dist/Timing.js +124 -6
  30. package/dist/Tree.d.ts +39 -0
  31. package/dist/Tree.js +127 -0
  32. package/dist/index.d.ts +54 -5
  33. package/dist/index.js +56 -5
  34. package/dist/particles/Particle.d.ts +2 -2
  35. package/dist/particles/ParticleField.d.ts +8 -6
  36. package/dist/particles/ParticleField.js +8 -9
  37. package/dist/particles/constructors.d.ts +5 -6
  38. package/dist/particles/constructors.js +8 -3
  39. package/dist/particles/legacy.d.ts +58 -0
  40. package/dist/particles/legacy.js +46 -0
  41. package/dist/particles/simulate.js +11 -3
  42. package/dist/particles/step.js +14 -10
  43. package/dist/types.d.ts +5 -0
  44. package/dist/types.js +1 -0
  45. package/package.json +2 -4
  46. package/dist/CameraHelpers.d.ts +0 -70
  47. package/dist/CameraHelpers.js +0 -239
  48. package/dist/CanvasExporter.d.ts +0 -12
  49. package/dist/CanvasExporter.js +0 -40
  50. package/dist/Fonts.d.ts +0 -41
  51. package/dist/Fonts.js +0 -27
  52. package/dist/Images.d.ts +0 -33
  53. package/dist/Images.js +0 -24
  54. package/dist/PngExporter.d.ts +0 -6
  55. package/dist/PngExporter.js +0 -85
  56. package/dist/Renderer.d.ts +0 -118
  57. package/dist/Renderer.js +0 -508
  58. package/dist/Shapes.d.ts +0 -11
  59. package/dist/Shapes.js +0 -11
  60. package/dist/demo.d.ts +0 -3
  61. package/dist/demo.js +0 -71
  62. package/dist/render/dof.d.ts +0 -27
  63. package/dist/render/dof.js +0 -37
  64. package/dist/render/paint.d.ts +0 -30
  65. package/dist/render/paint.js +0 -36
  66. package/dist/render/shapes.d.ts +0 -42
  67. package/dist/render/shapes.js +0 -310
  68. package/dist/shapes/Circle.d.ts +0 -32
  69. package/dist/shapes/Circle.js +0 -9
  70. package/dist/shapes/Ellipse.d.ts +0 -35
  71. package/dist/shapes/Ellipse.js +0 -10
  72. package/dist/shapes/Group.d.ts +0 -126
  73. package/dist/shapes/Group.js +0 -89
  74. package/dist/shapes/Hud.d.ts +0 -38
  75. package/dist/shapes/Hud.js +0 -35
  76. package/dist/shapes/Image.d.ts +0 -45
  77. package/dist/shapes/Image.js +0 -28
  78. package/dist/shapes/Line.d.ts +0 -47
  79. package/dist/shapes/Line.js +0 -35
  80. package/dist/shapes/Path.d.ts +0 -107
  81. package/dist/shapes/Path.js +0 -32
  82. package/dist/shapes/Rect.d.ts +0 -51
  83. package/dist/shapes/Rect.js +0 -22
  84. package/dist/shapes/Shape2D.d.ts +0 -49
  85. package/dist/shapes/Shape2D.js +0 -52
  86. package/dist/shapes/Shapes.d.ts +0 -11
  87. package/dist/shapes/Shapes.js +0 -11
  88. package/dist/shapes/Square.d.ts +0 -32
  89. package/dist/shapes/Square.js +0 -11
  90. package/dist/shapes/Text.d.ts +0 -51
  91. package/dist/shapes/Text.js +0 -24
@@ -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>;
@@ -1,85 +0,0 @@
1
- import { deflateSync } from "node:zlib";
2
- import * as Effect from "effect/Effect";
3
- import { FileSystem } from "effect/FileSystem";
4
- import { EffectMotionError } from "./EffectMotionError.js";
5
- const SIGNATURE = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
6
- const crcTable = (() => {
7
- const t = new Uint32Array(256);
8
- for (let n = 0; n < 256; n++) {
9
- let c = n;
10
- for (let k = 0; k < 8; k++) {
11
- c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
12
- }
13
- t[n] = c >>> 0;
14
- }
15
- return t;
16
- })();
17
- const crc32 = (bytes) => {
18
- let c = 0xffffffff;
19
- for (let i = 0; i < bytes.length; i++) {
20
- c = crcTable[(c ^ bytes[i]) & 0xff] ^ (c >>> 8);
21
- }
22
- return (c ^ 0xffffffff) >>> 0;
23
- };
24
- const chunk = (type, data) => {
25
- const typeBytes = new Uint8Array([
26
- type.charCodeAt(0),
27
- type.charCodeAt(1),
28
- type.charCodeAt(2),
29
- type.charCodeAt(3),
30
- ]);
31
- const body = new Uint8Array(typeBytes.length + data.length);
32
- body.set(typeBytes, 0);
33
- body.set(data, typeBytes.length);
34
- const out = new Uint8Array(4 + body.length + 4);
35
- const view = new DataView(out.buffer);
36
- view.setUint32(0, data.length, false);
37
- out.set(body, 4);
38
- view.setUint32(4 + body.length, crc32(body), false);
39
- return out;
40
- };
41
- /** Encode a raw RGBA8888 buffer (`width * height * 4` bytes) as a PNG. */
42
- const encodePng = Effect.fnUntraced(function* (rgba, width, height) {
43
- if (rgba.length !== width * height * 4) {
44
- return yield* Effect.fail(EffectMotionError.of(`Could not encode PNG: buffer is ${rgba.length} bytes, expected ${width * height * 4} (${width}x${height} RGBA)`));
45
- }
46
- // prepend a filter byte (0 = none) to each scanline
47
- const stride = width * 4;
48
- const raw = new Uint8Array((stride + 1) * height);
49
- for (let y = 0; y < height; y++) {
50
- raw[y * (stride + 1)] = 0;
51
- raw.set(rgba.subarray(y * stride, y * stride + stride), y * (stride + 1) + 1);
52
- }
53
- const ihdr = new Uint8Array(13);
54
- const ihdrView = new DataView(ihdr.buffer);
55
- ihdrView.setUint32(0, width, false);
56
- ihdrView.setUint32(4, height, false);
57
- ihdr[8] = 8; // bit depth
58
- ihdr[9] = 6; // color type: RGBA
59
- // [10] compression, [11] filter, [12] interlace all 0
60
- const idat = new Uint8Array(deflateSync(raw));
61
- const chunks = [
62
- SIGNATURE,
63
- chunk("IHDR", ihdr),
64
- chunk("IDAT", idat),
65
- chunk("IEND", new Uint8Array(0)),
66
- ];
67
- const total = chunks.reduce((n, c) => n + c.length, 0);
68
- const png = new Uint8Array(total);
69
- let offset = 0;
70
- for (const c of chunks) {
71
- png.set(c, offset);
72
- offset += c.length;
73
- }
74
- return png;
75
- });
76
- export const toBuffer = (fb) => {
77
- return encodePng(fb.rgba, fb.width, fb.height);
78
- };
79
- export const toFile = (fb, outPath) => {
80
- return Effect.gen(function* () {
81
- const fs = yield* FileSystem;
82
- const png = yield* toBuffer(fb);
83
- yield* fs.writeFile(outPath, png);
84
- });
85
- };
@@ -1,118 +0,0 @@
1
- import type { OwnedPaint, RenderSession, ThorvgException, ThorvgWasm } from "@effect-motion/thorvg";
2
- import * as Tvg from "@effect-motion/thorvg";
3
- import type { Canvas } from "@effect-motion/thorvg/Canvas";
4
- import * as Effect from "effect/Effect";
5
- import type * as Scope from "effect/Scope";
6
- import * as Color from "./Color.js";
7
- import type * as Entity from "./Entity.js";
8
- import * as Projection from "./Projection.js";
9
- import type { Frame } from "./Scene.js";
10
- /**
11
- * The projection handed to each paint function — how the camera places a
12
- * paintable this frame. `screen` is the projected billboard placement (an
13
- * affine the paint fn applies via `setTransform`); `depth` is the view-space
14
- * sort key; `scale` is the perspective scale (<= 0 means the anchor is behind
15
- * the camera — cull, unless a `quad` or `segment` is present). `quad`, when
16
- * present, is the projected, near-plane-clipped screen polygon of a tilted
17
- * plane (3–5 vertices — see Projection.projectPlane); a shape that can tilt
18
- * paints an exact path from these instead of applying the billboard affine.
19
- * `segment`, when present, is the exact projected screen endpoints of a
20
- * skeletal shape (Line) — each endpoint carries its own world depth, so the
21
- * pair is projected per point (see Projection.projectSegment) and the paint
22
- * fn draws it directly, skipping the billboard affine. `subpaths`, when
23
- * present, is the projected screen geometry of a skeletal path (Path): every
24
- * command point projected individually and near-plane-clipped per subpath
25
- * (see Projection.projectPath) — the paint fn emits it directly at screen
26
- * coordinates.
27
- */
28
- export interface PaintProjection {
29
- readonly screen: Projection.Affine;
30
- readonly depth: number;
31
- readonly scale: number;
32
- readonly quad?: ReadonlyArray<Projection.Vec2>;
33
- readonly segment?: readonly [Projection.Vec2, Projection.Vec2];
34
- readonly subpaths?: Projection.ProjectedPath["subpaths"];
35
- }
36
- /** The frame's render metadata, handed to paint functions. */
37
- export interface FrameMeta {
38
- readonly frameRate: number;
39
- readonly width: number;
40
- readonly height: number;
41
- readonly backgroundColor: Color.Color;
42
- /**
43
- * the active camera's view — world position `{x, y, z}`, Euler
44
- * orientation `{rotX, rotY, rotZ}`, and `focalLength` (FOV). The renderer
45
- * projects every instance through it. The resting camera (see
46
- * Camera.identity) reproduces plain-2D placement for z=0 content.
47
- */
48
- readonly camera: {
49
- readonly x: number;
50
- readonly y: number;
51
- readonly z: number;
52
- readonly rotX: number;
53
- readonly rotY: number;
54
- readonly rotZ: number;
55
- readonly focalLength: number;
56
- };
57
- }
58
- /**
59
- * Paints one entity instance onto the shared ThorVG scene. It issues ThorVG
60
- * C-API calls (make a shape, append geometry, style it, apply the projection,
61
- * add it to `scene`) — there is no intermediate description value. A container
62
- * (Group / root) paints nothing itself; its position has already composed into
63
- * its children's world coordinates by the time this is called.
64
- */
65
- export type PaintFunction<Ent extends Entity.AnyEntity> = (payload: {
66
- readonly entity: Ent;
67
- readonly id: string;
68
- readonly data: Ent["data"]["Type"];
69
- readonly projection: PaintProjection;
70
- readonly canvas: Canvas;
71
- readonly scene: OwnedPaint;
72
- readonly meta: FrameMeta;
73
- }) => Effect.Effect<void, ThorvgException, ThorvgWasm | RenderSession | Scope.Scope>;
74
- /**
75
- * A registry of paint functions keyed by entity name. `PaintFunctions<E>` is
76
- * the exhaustive map over an entity union — a built-in with no paint function
77
- * is a type error at the render call, not a runtime surprise (the old
78
- * "coverage manifest" guarantee, kept without a Context registry).
79
- */
80
- export type PaintFunctions<Entities extends Entity.AnyEntity> = {
81
- readonly [K in Entities as K["name"]]: PaintFunction<K>;
82
- };
83
- /** RGBA8888 framebuffer plus its dimensions, straight from the SW canvas. */
84
- export interface Framebuffer {
85
- readonly rgba: Uint8Array;
86
- /** physical pixel size of the rgba buffer (logical size × dpr) */
87
- readonly width: number;
88
- readonly height: number;
89
- /**
90
- * logical scene size — the resolution the buffer should be displayed at
91
- * (CSS pixels). Equals width/height when rendered at dpr 1.
92
- */
93
- readonly logicalWidth: number;
94
- readonly logicalHeight: number;
95
- }
96
- /**
97
- * Render one frame to an RGBA framebuffer, shared by both output adapters.
98
- *
99
- * Uses the RenderSession's canvas (resized in place to the frame's physical
100
- * size, cleared of the previous frame), adds a root scene, folds the frame
101
- * onto it via `Renderer.render`, then update/draw/sync and reads the SW
102
- * framebuffer. The scene and every painted shape are scoped per frame; the
103
- * canvas belongs to the session (a player mount, an export run) and is
104
- * deleted when the session closes.
105
- *
106
- * The background is painted as a filled rect (not a canvas clear color) so it
107
- * survives into the buffer the same way the SVG sink emitted a background
108
- * rect.
109
- */
110
- export declare const render: (frame: Frame, options?: {
111
- /**
112
- * device-pixel-ratio multiplier for high-dpi displays. The buffer is
113
- * rasterized at `logical × dpr` while paint functions keep working in
114
- * logical scene coordinates (the root scene is scaled). Callers display
115
- * the buffer at the logical size. Default 1 (node/export paths).
116
- */
117
- readonly dpr?: number;
118
- }) => Effect.Effect<Framebuffer, ThorvgException, Tvg.ThorvgWasm | Tvg.RenderSession | Scope.Scope>;