effect-motion 0.3.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +2 -2
  2. package/dist/Camera.d.ts +186 -49
  3. package/dist/Camera.js +343 -76
  4. package/dist/Color.d.ts +101 -1
  5. package/dist/Color.js +101 -1
  6. package/dist/EffectMotionError.d.ts +17 -0
  7. package/dist/EffectMotionError.js +17 -0
  8. package/dist/Entity.d.ts +682 -38
  9. package/dist/Entity.js +271 -27
  10. package/dist/Font.d.ts +108 -0
  11. package/dist/Font.js +95 -0
  12. package/dist/Image.d.ts +71 -0
  13. package/dist/Image.js +50 -0
  14. package/dist/Instance.d.ts +73 -11
  15. package/dist/Instance.js +44 -11
  16. package/dist/Motion.d.ts +328 -53
  17. package/dist/Motion.js +278 -46
  18. package/dist/Physics.d.ts +154 -20
  19. package/dist/Physics.js +92 -11
  20. package/dist/Projection.d.ts +37 -132
  21. package/dist/Projection.js +33 -292
  22. package/dist/Resource.d.ts +26 -0
  23. package/dist/Resource.js +41 -0
  24. package/dist/Runner.d.ts +653 -330
  25. package/dist/Runner.js +208 -158
  26. package/dist/Scene.d.ts +621 -171
  27. package/dist/Scene.js +620 -89
  28. package/dist/Timing.d.ts +170 -13
  29. package/dist/Timing.js +124 -6
  30. package/dist/Tree.d.ts +39 -0
  31. package/dist/Tree.js +127 -0
  32. package/dist/index.d.ts +54 -5
  33. package/dist/index.js +56 -5
  34. package/dist/particles/Particle.d.ts +2 -2
  35. package/dist/particles/ParticleField.d.ts +8 -6
  36. package/dist/particles/ParticleField.js +8 -9
  37. package/dist/particles/constructors.d.ts +5 -6
  38. package/dist/particles/constructors.js +8 -3
  39. package/dist/particles/legacy.d.ts +58 -0
  40. package/dist/particles/legacy.js +46 -0
  41. package/dist/particles/simulate.js +13 -5
  42. package/dist/particles/step.js +14 -10
  43. package/dist/types.d.ts +5 -0
  44. package/dist/types.js +1 -0
  45. package/package.json +2 -4
  46. package/dist/CameraHelpers.d.ts +0 -70
  47. package/dist/CameraHelpers.js +0 -239
  48. package/dist/CanvasExporter.d.ts +0 -12
  49. package/dist/CanvasExporter.js +0 -40
  50. package/dist/Fonts.d.ts +0 -41
  51. package/dist/Fonts.js +0 -27
  52. package/dist/Images.d.ts +0 -33
  53. package/dist/Images.js +0 -24
  54. package/dist/PngExporter.d.ts +0 -6
  55. package/dist/PngExporter.js +0 -85
  56. package/dist/Renderer.d.ts +0 -118
  57. package/dist/Renderer.js +0 -381
  58. package/dist/Shapes.d.ts +0 -11
  59. package/dist/Shapes.js +0 -11
  60. package/dist/demo.d.ts +0 -3
  61. package/dist/demo.js +0 -65
  62. package/dist/render/dof.d.ts +0 -27
  63. package/dist/render/dof.js +0 -37
  64. package/dist/render/paint.d.ts +0 -30
  65. package/dist/render/paint.js +0 -36
  66. package/dist/render/shapes.d.ts +0 -42
  67. package/dist/render/shapes.js +0 -310
  68. package/dist/shapes/Circle.d.ts +0 -32
  69. package/dist/shapes/Circle.js +0 -9
  70. package/dist/shapes/Ellipse.d.ts +0 -35
  71. package/dist/shapes/Ellipse.js +0 -10
  72. package/dist/shapes/Group.d.ts +0 -116
  73. package/dist/shapes/Group.js +0 -82
  74. package/dist/shapes/Hud.d.ts +0 -38
  75. package/dist/shapes/Hud.js +0 -35
  76. package/dist/shapes/Image.d.ts +0 -45
  77. package/dist/shapes/Image.js +0 -28
  78. package/dist/shapes/Line.d.ts +0 -47
  79. package/dist/shapes/Line.js +0 -35
  80. package/dist/shapes/Path.d.ts +0 -107
  81. package/dist/shapes/Path.js +0 -32
  82. package/dist/shapes/Rect.d.ts +0 -51
  83. package/dist/shapes/Rect.js +0 -22
  84. package/dist/shapes/Shape2D.d.ts +0 -49
  85. package/dist/shapes/Shape2D.js +0 -52
  86. package/dist/shapes/Shapes.d.ts +0 -11
  87. package/dist/shapes/Shapes.js +0 -11
  88. package/dist/shapes/Square.d.ts +0 -32
  89. package/dist/shapes/Square.js +0 -11
  90. package/dist/shapes/Text.d.ts +0 -51
  91. package/dist/shapes/Text.js +0 -24
package/dist/Renderer.js DELETED
@@ -1,381 +0,0 @@
1
- import * as Tvg from "@effect-motion/thorvg";
2
- import * as Effect from "effect/Effect";
3
- import * as CameraMod from "./Camera.js";
4
- import * as Color from "./Color.js";
5
- import * as Projection from "./Projection.js";
6
- import { circleOfConfusion, quantizeSigma } from "./render/dof.js";
7
- import { builtinPaints } from "./render/shapes.js";
8
- import { Hud } from "./shapes/Hud.js";
9
- // in frame data, a `children: string[]` field means child instance ids
10
- const childIdsOf = (data) => {
11
- const children = data?.children;
12
- return Array.isArray(children) ? children : [];
13
- };
14
- /**
15
- * Split a Path's command list into world-space subpaths: `M` starts one,
16
- * `L` extends it, `Z` closes it. Command points are LOCAL to the path's
17
- * anchor; `z` absent means 0. An `L` directly after a `Z` starts a new open
18
- * subpath from the closed subpath's start point (SVG semantics).
19
- */
20
- const pathSubpaths = (commands, anchor) => {
21
- const subpaths = [];
22
- let current = [];
23
- let lastMove = anchor;
24
- const world = (p) => ({
25
- x: anchor.x + p.x,
26
- y: anchor.y + p.y,
27
- z: anchor.z + (p.z ?? 0),
28
- });
29
- const flush = (closed) => {
30
- if (current.length >= 2) {
31
- subpaths.push({ points: current, closed });
32
- }
33
- current = [];
34
- };
35
- for (const command of commands) {
36
- switch (command._tag) {
37
- case "M": {
38
- flush(false);
39
- lastMove = world(command);
40
- current = [lastMove];
41
- break;
42
- }
43
- case "L": {
44
- if (current.length === 0) {
45
- current.push(lastMove);
46
- }
47
- current.push(world(command));
48
- break;
49
- }
50
- case "Z": {
51
- flush(true);
52
- break;
53
- }
54
- }
55
- }
56
- flush(false);
57
- return subpaths;
58
- };
59
- // a hidden instance ($visible false) and its subtree are skipped entirely
60
- const isVisible = (frame, id) => frame.instances[id]?.$visible !== false;
61
- /**
62
- * Fold a frame onto the shared ThorVG canvas + scene: flatten the instance
63
- * tree to a depth-sorted draw list, project each paintable through the
64
- * frame's camera, and paint far→near. The pipeline (flatten, world-offset
65
- * composition, projection, quad, stable-id depth sort, visibility skip,
66
- * cycle/duplicate defects) is target-agnostic; only the per-entity paint is
67
- * ThorVG-specific.
68
- */
69
- const renderToCanvas = (frame, canvas, scene,
70
- // blur sigmas are canvas-pixel amounts; the scene is authored in logical
71
- // units, so depth-of-field scales them by the device-pixel ratio
72
- dpr = 1) => Effect.gen(function* () {
73
- const origin = {
74
- x: frame.width / 2,
75
- y: frame.height / 2,
76
- };
77
- // the effective view: a camera with a point of interest auto-orients
78
- // toward it here (explicit Euler composes after — see resolveCamera);
79
- // the frame's camera data is never mutated
80
- const camera = Projection.resolveCamera(frame.camera, origin);
81
- // HUD subtrees project through the identity camera: camera-independent
82
- // placement, and structurally exempt from depth of field (aperture 0)
83
- const identityCamera = CameraMod.identity(frame.width);
84
- const visited = new Set();
85
- const paintables = [];
86
- // Flatten the tree to a draw list. A container (a node with a
87
- // `children` field) is NOT a paint-order boundary: its position
88
- // composes into its children's world coordinates, and each child is
89
- // emitted into the same flat list. `offset` is the accumulated world
90
- // translation from ancestor containers. `hud` marks an ancestor Hud:
91
- // the subtree projects through the identity camera and paints in the
92
- // top tier. `inWorldContainer` marks any ordinary container above —
93
- // a Hud there would compose world offsets into screen coordinates,
94
- // which is incoherent and dies loudly.
95
- const flatten = (id, offset, hud, inWorldContainer) => Effect.gen(function* () {
96
- if (visited.has(id)) {
97
- return yield* Effect.die(new Error(`Renderer: instance "${id}" is referenced more than once (duplicate parent or cycle)`));
98
- }
99
- visited.add(id);
100
- const entry = frame.instances[id];
101
- if (entry === undefined) {
102
- return yield* Effect.die(new Error(`Renderer: unknown instance id "${id}"`));
103
- }
104
- const isHud = entry.entity.name === Hud.name;
105
- if (isHud && inWorldContainer) {
106
- return yield* Effect.die(new Error(`Renderer: Hud "${id}" is nested inside world content — a Hud must be a top-level child of the root (or of another Hud)`));
107
- }
108
- const subtreeHud = hud || isHud;
109
- const effectiveCamera = subtreeHud ? identityCamera : camera;
110
- const data = entry.data;
111
- // world anchor = ancestor offset + this node's own position
112
- const world = {
113
- x: offset.x + (data.x ?? 0),
114
- y: offset.y + (data.y ?? 0),
115
- z: offset.z + (data.z ?? 0),
116
- };
117
- const childIds = childIdsOf(entry.data).filter((childId) => isVisible(frame, childId));
118
- if (childIds.length > 0) {
119
- // a pure container: contribute position, recurse, paint
120
- // nothing itself (the root, Groups, Huds). ponytail: only
121
- // translation composes down — a Group's 2D affine transform
122
- // is not yet threaded into child world coords.
123
- yield* Effect.all(childIds.map((childId) => flatten(childId, world, subtreeHud, inWorldContainer || !subtreeHud)));
124
- return;
125
- }
126
- // a skeletal path leaf (Path): every command point is an
127
- // independent world point (anchor + local), projected per point
128
- // with per-subpath near-plane clipping — the paint fn emits the
129
- // exact screen subpaths. ponytail: no viewport clip yet — ThorVG
130
- // stroke cost scales with the path's full extent, offscreen
131
- // included; upgrade is per-span clipSegmentToRect with the same
132
- // splitting the near clip uses.
133
- if (Array.isArray(data.commands)) {
134
- const projected = Projection.projectPath(effectiveCamera, pathSubpaths(data.commands, world), origin);
135
- if (projected === undefined) {
136
- // entirely behind the near plane — cull
137
- return;
138
- }
139
- // anchor the billboard affine on the first visible point, as
140
- // the segment branch does — path paints ignore it (their
141
- // geometry is already screen-space) but the field stays coherent
142
- const first = projected.subpaths[0]?.points[0] ?? origin;
143
- paintables.push({
144
- id,
145
- entry,
146
- projection: {
147
- screen: Projection.billboardAffine({
148
- x: first.x,
149
- y: first.y,
150
- depth: projected.depth,
151
- scale: projected.scale,
152
- }, { x: data.x ?? 0, y: data.y ?? 0 }),
153
- depth: projected.depth,
154
- scale: projected.scale,
155
- subpaths: projected.subpaths,
156
- },
157
- hud: subtreeHud,
158
- });
159
- return;
160
- }
161
- // a skeletal leaf (Line): both endpoints are independent world
162
- // points — project the pair per point (near-plane-clipped) and
163
- // hand the exact screen segment to the paint fn. Unconditional
164
- // for any x2/y2 leaf: the identity invariant makes the flat
165
- // case pixel-identical to the old billboard path.
166
- if (typeof data.x2 === "number" && typeof data.y2 === "number") {
167
- const seg = Projection.projectSegment(effectiveCamera, world, {
168
- x: offset.x + data.x2,
169
- y: offset.y + data.y2,
170
- z: offset.z + (data.z2 ?? 0),
171
- }, origin);
172
- if (seg === undefined) {
173
- // entirely behind the near plane — cull
174
- return;
175
- }
176
- // clip to the viewport before painting: ThorVG stroke cost
177
- // scales with the path's full extent, offscreen included.
178
- // The margin covers the scaled stroke (caps/joins) so the
179
- // visible pixels are untouched by the clip.
180
- const strokeMargin = (entry.data.strokeWidth ?? 1) *
181
- seg.scale +
182
- 1;
183
- const clipped = Projection.clipSegmentToRect(seg.a, seg.b, { x: -strokeMargin, y: -strokeMargin }, {
184
- x: frame.width + strokeMargin,
185
- y: frame.height + strokeMargin,
186
- });
187
- if (clipped === undefined) {
188
- // entirely offscreen — cull
189
- return;
190
- }
191
- // ponytail: one depth/scale for the whole segment (its
192
- // visible midpoint) — a depth-spanning line blurs and sorts
193
- // as one unit, the same ceiling as a tilted plane's quad.
194
- // Upgrade: split the segment where its (linear) view depth
195
- // crosses DoF bucket boundaries → gradient blur along the
196
- // line plus per-piece sort keys.
197
- paintables.push({
198
- id,
199
- entry,
200
- projection: {
201
- screen: Projection.billboardAffine({ x: seg.a.x, y: seg.a.y, depth: seg.depth, scale: seg.scale }, { x: data.x ?? 0, y: data.y ?? 0 }),
202
- depth: seg.depth,
203
- scale: seg.scale,
204
- segment: clipped,
205
- },
206
- hud: subtreeHud,
207
- });
208
- return;
209
- }
210
- // a leaf paintable: project its world anchor for placement +
211
- // depth, but anchor the billboard affine on the shape's own
212
- // LOCAL coordinates — the shape paints at local (data.x/y), so
213
- // the transform maps local → screen. The composed ancestor
214
- // offset lives in the transform's translation.
215
- const proj = Projection.project(effectiveCamera, world, origin);
216
- // A rectangular plane with any nonzero rotation tilts: project
217
- // its four corners so the paint fn can emit an exact path. The
218
- // pivot is the plane's world anchor, so rotation spins it in
219
- // place. Billboards (no rotation, or non-rect shapes) skip this.
220
- const rotX = data.rotX ?? 0;
221
- const rotY = data.rotY ?? 0;
222
- const rotZ = data.rotZ ?? 0;
223
- const tilted = (rotX !== 0 || rotY !== 0 || rotZ !== 0) &&
224
- data.width !== undefined &&
225
- data.height !== undefined;
226
- const quad = tilted
227
- ? Projection.projectPlane(effectiveCamera, Projection.planeCorners({
228
- x: data.x ?? 0,
229
- y: data.y ?? 0,
230
- width: data.width,
231
- height: data.height,
232
- }, { rotX, rotY, rotZ }, world), origin)
233
- : undefined;
234
- if (quad !== undefined && quad.length < 3) {
235
- // the tilted plane is entirely behind the near plane — cull
236
- return;
237
- }
238
- paintables.push({
239
- id,
240
- entry,
241
- projection: {
242
- screen: Projection.billboardAffine(proj, {
243
- x: data.x ?? 0,
244
- y: data.y ?? 0,
245
- }),
246
- depth: proj.depth,
247
- scale: proj.scale,
248
- ...(quad !== undefined ? { quad } : {}),
249
- },
250
- hud: subtreeHud,
251
- });
252
- });
253
- const rootEntry = frame.instances[frame.root];
254
- if (rootEntry === undefined) {
255
- return yield* Effect.die(new Error(`Renderer: missing root instance "${frame.root}"`));
256
- }
257
- visited.add(frame.root);
258
- yield* Effect.all(childIdsOf(rootEntry.data)
259
- .filter((childId) => isVisible(frame, childId))
260
- .map((childId) => flatten(childId, { x: 0, y: 0, z: 0 }, false, false)));
261
- // painter's order: two tiers — world content by depth (farthest
262
- // first), then HUD content by depth among itself, each with a stable
263
- // id tie-break so equal-depth paintables paint deterministically.
264
- // ponytail: naive O(n log n) per frame — swap for a spatial structure
265
- // only if a scene with thousands of objects proves it.
266
- paintables.sort((a, b) => (a.hud ? 1 : 0) - (b.hud ? 1 : 0) ||
267
- b.projection.depth - a.projection.depth ||
268
- (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
269
- const meta = {
270
- frameRate: frame.frameRate,
271
- width: frame.width,
272
- height: frame.height,
273
- backgroundColor: frame.backgroundColor,
274
- camera: frame.camera,
275
- };
276
- // Depth of field (camera-depth-of-field D3): with aperture > 0,
277
- // contiguous depth-sorted runs sharing a quantized blur sigma paint
278
- // into nested scenes that get a gaussian-blur effect; sharp runs paint
279
- // into the root as always, so aperture 0 (the default) takes the
280
- // unchanged single-scene path. Buckets are added to the root at run
281
- // boundaries, preserving painter's order exactly. Sigma is scaled by
282
- // dpr because the blur operates in canvas pixels while the scene is
283
- // authored in logical units.
284
- const aperture = frame.camera.aperture;
285
- let bucketScene = scene;
286
- let bucketSigma = 0;
287
- const closeBucket = Effect.gen(function* () {
288
- if (bucketSigma > 0) {
289
- // direction 0 = both axes, border 0 = duplicate, quality 75
290
- // (upstream default) — verified in the scene-blur spike
291
- yield* Tvg.Scene.addGaussianBlur(bucketScene, bucketSigma * dpr, 0, 0, 75);
292
- yield* Tvg.Scene.add(scene, bucketScene);
293
- }
294
- });
295
- // paint far→near. A paintable whose anchor is behind the camera
296
- // (scale <= 0) is culled here so paint functions never see an invalid
297
- // placement — EXCEPT a tilted plane carrying a quad: its polygon is
298
- // already near-plane-clipped, and it can be visible (near part in
299
- // front) while its anchor corner is behind.
300
- for (const { id, entry, projection, hud } of paintables) {
301
- if (projection.scale <= 0 && projection.quad === undefined) {
302
- continue;
303
- }
304
- // HUD content is structurally sharp: its effective camera is the
305
- // identity camera (aperture 0), so it never enters a blur bucket
306
- const sigma = aperture > 0 && !hud
307
- ? quantizeSigma(circleOfConfusion(projection.depth, frame.camera))
308
- : 0;
309
- if (sigma !== bucketSigma) {
310
- yield* closeBucket;
311
- bucketScene = sigma === 0 ? scene : yield* Tvg.Scene.make();
312
- bucketSigma = sigma;
313
- }
314
- // the concrete paint fn for this entity name; the map is exhaustive
315
- // over Entities by construction (PaintFunctions<Entities>). The
316
- // specific member depends on the instance's entity, known only at
317
- // runtime — hence the cast to the erased paint-fn type.
318
- const paint = builtinPaints[entry.entity.name];
319
- if (paint === undefined) {
320
- return yield* Effect.die(new Error(`Renderer: no paint function for entity "${entry.entity.name}"`));
321
- }
322
- yield* paint({
323
- entity: entry.entity,
324
- id,
325
- data: entry.data,
326
- projection,
327
- canvas,
328
- scene: bucketScene,
329
- meta,
330
- });
331
- }
332
- yield* closeBucket;
333
- });
334
- /**
335
- * Render one frame to an RGBA framebuffer, shared by both output adapters.
336
- *
337
- * Uses the RenderSession's canvas (resized in place to the frame's physical
338
- * size, cleared of the previous frame), adds a root scene, folds the frame
339
- * onto it via `Renderer.render`, then update/draw/sync and reads the SW
340
- * framebuffer. The scene and every painted shape are scoped per frame; the
341
- * canvas belongs to the session (a player mount, an export run) and is
342
- * deleted when the session closes.
343
- *
344
- * The background is painted as a filled rect (not a canvas clear color) so it
345
- * survives into the buffer the same way the SVG sink emitted a background
346
- * rect.
347
- */
348
- export const render = (frame, options) => Effect.gen(function* () {
349
- const logicalWidth = frame.width;
350
- const logicalHeight = frame.height;
351
- const dpr = options?.dpr ?? 1;
352
- const width = Math.round(logicalWidth * dpr);
353
- const height = Math.round(logicalHeight * dpr);
354
- const canvas = yield* Tvg.Session.canvasSized(width, height);
355
- const scene = yield* Tvg.Scene.make();
356
- // background as a filled rect covering the viewport (mirrors the SVG
357
- // sink's background rect; survives into the raster buffer)
358
- const bg = yield* Tvg.Shape.make();
359
- yield* Tvg.Shape.appendRect(bg, 0, 0, logicalWidth, logicalHeight);
360
- const { r, g, b, a } = Color.bytes(frame.backgroundColor);
361
- yield* Tvg.Shape.setFillColor(bg, r, g, b, a);
362
- yield* Tvg.Scene.add(scene, bg);
363
- yield* renderToCanvas(frame, canvas, scene, dpr);
364
- // scale the whole subtree to physical pixels; children keep their own
365
- // logical affines (parent transforms compose in the scene graph)
366
- if (dpr !== 1) {
367
- yield* Tvg.Paint.scale(scene, dpr);
368
- }
369
- yield* Tvg.Canvas.add(canvas, scene);
370
- yield* Tvg.Canvas.update(canvas);
371
- yield* Tvg.Canvas.draw(canvas);
372
- yield* Tvg.Canvas.sync(canvas);
373
- const buffer = yield* Tvg.Canvas.render(canvas);
374
- return {
375
- rgba: new Uint8Array(buffer),
376
- width,
377
- height,
378
- logicalWidth,
379
- logicalHeight,
380
- };
381
- });
package/dist/Shapes.d.ts DELETED
@@ -1,11 +0,0 @@
1
- export { Circle } from "./shapes/Circle.js";
2
- export { Ellipse } from "./shapes/Ellipse.js";
3
- export * from "./shapes/Group.js";
4
- export { Hud } from "./shapes/Hud.js";
5
- export { Image } from "./shapes/Image.js";
6
- export { Line } from "./shapes/Line.js";
7
- export { Path } from "./shapes/Path.js";
8
- export { Rect } from "./shapes/Rect.js";
9
- export * as Shape2D from "./shapes/Shape2D.js";
10
- export { Square } from "./shapes/Square.js";
11
- export { Text } from "./shapes/Text.js";
package/dist/Shapes.js DELETED
@@ -1,11 +0,0 @@
1
- export { Circle } from "./shapes/Circle.js";
2
- export { Ellipse } from "./shapes/Ellipse.js";
3
- export * from "./shapes/Group.js";
4
- export { Hud } from "./shapes/Hud.js";
5
- export { Image } from "./shapes/Image.js";
6
- export { Line } from "./shapes/Line.js";
7
- export { Path } from "./shapes/Path.js";
8
- export { Rect } from "./shapes/Rect.js";
9
- export * as Shape2D from "./shapes/Shape2D.js";
10
- export { Square } from "./shapes/Square.js";
11
- export { Text } from "./shapes/Text.js";
package/dist/demo.d.ts DELETED
@@ -1,3 +0,0 @@
1
- import * as Scene from "./Scene.js";
2
- export declare const scene: Scene.Scene<never, import("./Runner.js").Runner>;
3
- export declare const staggered: Scene.Scene<never, import("./Runner.js").Runner>;
package/dist/demo.js DELETED
@@ -1,65 +0,0 @@
1
- import { NodeServices } from "@effect/platform-node";
2
- import { Session } from "@effect-motion/thorvg";
3
- import { EngineNode } from "@effect-motion/thorvg/node";
4
- import { Effect, Layer, Schedule, Stream } from "effect";
5
- import * as Color from "./Color.js";
6
- import * as Motion from "./Motion.js";
7
- import * as Physics from "./Physics.js";
8
- import * as PngExporter from "./PngExporter.js";
9
- import * as Renderer from "./Renderer.js";
10
- import * as Scene from "./Scene.js";
11
- import * as Shapes from "./Shapes.js";
12
- // children live in the group's local coordinates: one motion moves them all
13
- export const scene = Scene.make(function* () {
14
- const duo = yield* Scene.instantiate(Shapes.Group, {
15
- x: 70,
16
- y: 200,
17
- children: [
18
- Scene.instantiate(Shapes.Circle, {
19
- x: 0,
20
- y: 0,
21
- radius: 14,
22
- fill: Color.hex("#e53170"),
23
- }),
24
- Scene.instantiate(Shapes.Square, {
25
- x: 20,
26
- y: -16,
27
- size: 28,
28
- fill: Color.hex("#a786df"),
29
- }),
30
- ],
31
- });
32
- yield* Motion.wait("1.5 seconds");
33
- yield* duo.pipe(Motion.moveTo({ x: 380 }, "1.5 seconds", "easeInOutCubic"), Motion.moveTo({ x: 70 }, "1.5 seconds", "easeInOutCubic"), Motion.wait("1.5 seconds"), Physics.springTo({ y: 80 }, "jump"), Motion.fadeTo(0.15, "1 second"));
34
- });
35
- // schedule-driven composition: a background pulse loops for the scene's
36
- // duration while three staggered dots define its actual length
37
- export const staggered = Scene.make(function* () {
38
- const stage = yield* Scene.instantiate(Shapes.Group, { x: 70, y: 150 });
39
- yield* Scene.background(Scene.repeat(stage.pipe(Motion.moveTo({ y: 130 }, "500 millis", "easeInOutCubic"), Motion.moveTo({ y: 150 }, "500 millis", "easeInOutCubic")), Schedule.forever));
40
- const dot = (x) => Effect.gen(function* () {
41
- const circle = yield* Scene.instantiate(Shapes.Circle, {
42
- x,
43
- y: 0,
44
- radius: 10,
45
- fill: Color.hex("#e53170"),
46
- });
47
- // reparent the freshly-born dot from root into the stage group
48
- yield* Scene.appendChild(stage, circle);
49
- yield* Motion.tweenTo(circle, { x: x + 300 }, "1 second", "easeInOutCubic");
50
- });
51
- yield* Scene.stagger([dot(0), dot(25), dot(50)], Schedule.spaced("250 millis"));
52
- });
53
- // render the middle frame of the duo scene to a PNG through the single ThorVG
54
- // renderer (Node adapter) — the end-to-end path: Scene.stream → renderToPng.
55
- const movie = Effect.gen(function* () {
56
- const frames = yield* Scene.stream(scene, { width: 500, height: 300 }).pipe(Stream.runCollect);
57
- const list = [...frames];
58
- const mid = list[Math.floor(list.length / 2)];
59
- const framebuffer = yield* Renderer.render(mid);
60
- const _png = yield* PngExporter.toBuffer(framebuffer);
61
- yield* PngExporter.toFile(framebuffer, "output.png");
62
- });
63
- Effect.runPromise(movie.pipe(Effect.scoped, Effect.provide(Layer.provideMerge(
64
- // a demo scene with declared assets would add fonts/images maps here
65
- Session.layer({ width: 500, height: 300 }), EngineNode.layer("sw"))), Effect.provide(NodeServices.layer)));
@@ -1,27 +0,0 @@
1
- import type * as Projection from "../Projection.js";
2
- /**
3
- * Depth-of-field circle of confusion (design D2 of camera-depth-of-field):
4
- * how much gaussian blur a paintable at `depth` receives under `camera`.
5
- * Pure arithmetic on frame data — deterministic by construction.
6
- */
7
- /** defensive ceiling: no author intends a 100px blur; huge sigmas are bugs */
8
- export declare const MAX_SIGMA = 64;
9
- /** quantization step (px): paintables within a step share one blur pass */
10
- export declare const SIGMA_STEP = 0.5;
11
- /** below this, blur is visually nil — treat as sharp (no blur pass at all) */
12
- export declare const SHARP_THRESHOLD = 0.25;
13
- /**
14
- * Blur sigma for a paintable at view-space `depth`: zero at the focus plane,
15
- * growing with distance from it, scaled by aperture and focal length (longer
16
- * lens = shallower field). AE-flavored, not thin-lens-accurate — the contract
17
- * is the shape of the curve, not the constants.
18
- */
19
- export declare const circleOfConfusion: (depth: number, camera: Projection.CameraView) => number;
20
- /**
21
- * Quantize a sigma so contiguous depth-sorted runs share blur passes: 0 for
22
- * visually-sharp values, otherwise the nearest step (never above the ceiling).
23
- * ponytail: 0.5px steps keep pass counts low and banding invisible; the
24
- * upgrade path is per-paintable exact sigma (one blur pass each) if a smooth
25
- * depth gradient ever shows stepping.
26
- */
27
- export declare const quantizeSigma: (sigma: number) => number;
@@ -1,37 +0,0 @@
1
- /**
2
- * Depth-of-field circle of confusion (design D2 of camera-depth-of-field):
3
- * how much gaussian blur a paintable at `depth` receives under `camera`.
4
- * Pure arithmetic on frame data — deterministic by construction.
5
- */
6
- /** defensive ceiling: no author intends a 100px blur; huge sigmas are bugs */
7
- export const MAX_SIGMA = 64;
8
- /** quantization step (px): paintables within a step share one blur pass */
9
- export const SIGMA_STEP = 0.5;
10
- /** below this, blur is visually nil — treat as sharp (no blur pass at all) */
11
- export const SHARP_THRESHOLD = 0.25;
12
- /**
13
- * Blur sigma for a paintable at view-space `depth`: zero at the focus plane,
14
- * growing with distance from it, scaled by aperture and focal length (longer
15
- * lens = shallower field). AE-flavored, not thin-lens-accurate — the contract
16
- * is the shape of the curve, not the constants.
17
- */
18
- export const circleOfConfusion = (depth, camera) => {
19
- if (camera.aperture <= 0 || depth <= 0 || camera.focusDistance <= 0) {
20
- return 0;
21
- }
22
- const sigma = (camera.aperture *
23
- camera.focalLength *
24
- Math.abs(depth - camera.focusDistance)) /
25
- (depth * camera.focusDistance);
26
- return Math.min(sigma, MAX_SIGMA);
27
- };
28
- /**
29
- * Quantize a sigma so contiguous depth-sorted runs share blur passes: 0 for
30
- * visually-sharp values, otherwise the nearest step (never above the ceiling).
31
- * ponytail: 0.5px steps keep pass counts low and banding invisible; the
32
- * upgrade path is per-paintable exact sigma (one blur pass each) if a smooth
33
- * depth gradient ever shows stepping.
34
- */
35
- export const quantizeSigma = (sigma) => sigma < SHARP_THRESHOLD
36
- ? 0
37
- : Math.min(Math.round(sigma / SIGMA_STEP) * SIGMA_STEP, MAX_SIGMA);
@@ -1,30 +0,0 @@
1
- import type { OwnedPaint, ThorvgException } from "@effect-motion/thorvg";
2
- import * as Tvg from "@effect-motion/thorvg";
3
- import * as Effect from "effect/Effect";
4
- import type * as Scope from "effect/Scope";
5
- import * as Color from "../Color.js";
6
- import type { PaintProjection } from "../Renderer.js";
7
- /**
8
- * Shared paint helpers: style application, projection, and the attach step
9
- * every leaf paint function runs. Keeping the ThorVG call sequence in one
10
- * place means each shape's paint function is just "make geometry, then
11
- * `finishPaint`".
12
- */
13
- /** The style fields a filled/stroked shape carries in its data. */
14
- export interface StyleData {
15
- readonly fill?: Color.Color;
16
- readonly stroke?: Color.Color;
17
- readonly strokeWidth?: number;
18
- readonly opacity: number;
19
- }
20
- /** Apply fill/stroke/opacity from shape data onto a ThorVG shape paint. */
21
- export declare const applyStyle: (shape: OwnedPaint, data: StyleData) => Effect.Effect<void, ThorvgException, Tvg.ThorvgWasm>;
22
- /**
23
- * Apply the billboard projection affine to a shape, then add it to the scene.
24
- * The identity affine (resting camera, z=0) is skipped so plain-2D scenes
25
- * issue the minimal draw calls. Ownership transfers to the scene on add.
26
- *
27
- * Callers must NOT have applied any translate/rotate/scale to the shape —
28
- * `setTransform` is the single, final transform (design D3).
29
- */
30
- export declare const finishPaint: (shape: OwnedPaint, scene: OwnedPaint, projection: PaintProjection) => Effect.Effect<void, ThorvgException, Tvg.ThorvgWasm | Scope.Scope>;
@@ -1,36 +0,0 @@
1
- import * as Tvg from "@effect-motion/thorvg";
2
- import * as Effect from "effect/Effect";
3
- import * as Color from "../Color.js";
4
- /** Apply fill/stroke/opacity from shape data onto a ThorVG shape paint. */
5
- export const applyStyle = (shape, data) => Effect.gen(function* () {
6
- if (data.fill !== undefined) {
7
- const { r, g, b, a } = Color.bytes(data.fill);
8
- yield* Tvg.Shape.setFillColor(shape, r, g, b, a);
9
- }
10
- if (data.stroke !== undefined) {
11
- const { r, g, b, a } = Color.bytes(data.stroke);
12
- yield* Tvg.Shape.setStrokeColor(shape, r, g, b, a);
13
- }
14
- if (data.strokeWidth !== undefined) {
15
- yield* Tvg.Shape.setStrokeWidth(shape, data.strokeWidth);
16
- }
17
- // opacity 1 is the ThorVG default; only set when it changes something
18
- if (data.opacity !== 1) {
19
- yield* Tvg.Paint.setOpacity(shape, Math.round(data.opacity * 255));
20
- }
21
- });
22
- const isIdentity = (m) => m.a === 1 && m.b === 0 && m.c === 0 && m.d === 1 && m.e === 0 && m.f === 0;
23
- /**
24
- * Apply the billboard projection affine to a shape, then add it to the scene.
25
- * The identity affine (resting camera, z=0) is skipped so plain-2D scenes
26
- * issue the minimal draw calls. Ownership transfers to the scene on add.
27
- *
28
- * Callers must NOT have applied any translate/rotate/scale to the shape —
29
- * `setTransform` is the single, final transform (design D3).
30
- */
31
- export const finishPaint = (shape, scene, projection) => Effect.gen(function* () {
32
- if (!isIdentity(projection.screen)) {
33
- yield* Tvg.Paint.setTransform(shape, projection.screen);
34
- }
35
- yield* Tvg.Scene.add(scene, shape);
36
- });
@@ -1,42 +0,0 @@
1
- import { ParticleField } from "../particles/ParticleField.js";
2
- import type { PaintFunction, PaintFunctions } from "../Renderer.js";
3
- import * as Circle from "../shapes/Circle.js";
4
- import * as Ellipse from "../shapes/Ellipse.js";
5
- import * as Group from "../shapes/Group.js";
6
- import * as Hud from "../shapes/Hud.js";
7
- import * as Image from "../shapes/Image.js";
8
- import * as Line from "../shapes/Line.js";
9
- import * as Path from "../shapes/Path.js";
10
- import * as Rect from "../shapes/Rect.js";
11
- import * as Square from "../shapes/Square.js";
12
- import * as Text from "../shapes/Text.js";
13
- /**
14
- * ThorVG paint functions for the built-in shapes — this file is the coverage
15
- * manifest: `paints` is typed `PaintFunctions<...>`, so a built-in missing a
16
- * paint function is a type error, not a runtime surprise.
17
- */
18
- export declare const circle: PaintFunction<typeof Circle.Circle>;
19
- export declare const rect: PaintFunction<typeof Rect.Rect>;
20
- export declare const square: PaintFunction<typeof Square.Square>;
21
- export declare const ellipse: PaintFunction<typeof Ellipse.Ellipse>;
22
- export declare const line: PaintFunction<typeof Line.Line>;
23
- export declare const path: PaintFunction<typeof Path.Path>;
24
- export declare const group: PaintFunction<typeof Group.Group>;
25
- export declare const hud: PaintFunction<typeof Hud.Hud>;
26
- export declare const image: PaintFunction<typeof Image.Image>;
27
- export declare const text: PaintFunction<typeof Text.Text>;
28
- /**
29
- * ParticleField: live particles batched into one shape PER (color, opacity)
30
- * bucket — ThorVG fills a shape uniformly, so particles that share a color and
31
- * (quantized) over-life opacity are appended into the same shape and filled
32
- * once. Sized/faded by the over-life curves; dead slots emit nothing.
33
- * ponytail: one shape per distinct (color, opacity-bucket) — far fewer than one
34
- * per particle, but a fully instanced path is the upgrade if this is a wall.
35
- */
36
- export declare const particleField: PaintFunction<typeof ParticleField>;
37
- /**
38
- * The exhaustive paint-function map for every built-in entity. Typed
39
- * `PaintFunctions<...>` so a missing built-in fails to type-check (the old
40
- * "coverage manifest" guarantee, without a Context registry).
41
- */
42
- export declare const builtinPaints: PaintFunctions<typeof Circle.Circle | typeof Rect.Rect | typeof Square.Square | typeof Ellipse.Ellipse | typeof Line.Line | typeof Path.Path | typeof Group.Group | typeof Hud.Hud | typeof Text.Text | typeof Image.Image | typeof ParticleField>;