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/dist/Renderer.js DELETED
@@ -1,508 +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 * as Group from "./shapes/Group.js";
9
- import { Hud } from "./shapes/Hud.js";
10
- // in frame data, a `children: string[]` field means child instance ids
11
- const childIdsOf = (data) => {
12
- const children = data?.children;
13
- return Array.isArray(children) ? children : [];
14
- };
15
- /**
16
- * Split a Path's command list into world-space subpaths: `M` starts one,
17
- * `L` extends it, `Z` closes it. Command points are LOCAL to the path's
18
- * anchor; `z` absent means 0. An `L` directly after a `Z` starts a new open
19
- * subpath from the closed subpath's start point (SVG semantics).
20
- */
21
- const pathSubpaths = (commands, anchor) => {
22
- const subpaths = [];
23
- let current = [];
24
- let lastMove = anchor;
25
- const world = (p) => ({
26
- x: anchor.x + p.x,
27
- y: anchor.y + p.y,
28
- z: anchor.z + (p.z ?? 0),
29
- });
30
- const flush = (closed) => {
31
- if (current.length >= 2) {
32
- subpaths.push({ points: current, closed });
33
- }
34
- current = [];
35
- };
36
- for (const command of commands) {
37
- switch (command._tag) {
38
- case "M": {
39
- flush(false);
40
- lastMove = world(command);
41
- current = [lastMove];
42
- break;
43
- }
44
- case "L": {
45
- if (current.length === 0) {
46
- current.push(lastMove);
47
- }
48
- current.push(world(command));
49
- break;
50
- }
51
- case "Z": {
52
- flush(true);
53
- break;
54
- }
55
- }
56
- }
57
- flush(false);
58
- return subpaths;
59
- };
60
- // a hidden instance ($visible false) and its subtree are skipped entirely
61
- const isVisible = (frame, id) => frame.instances[id]?.$visible !== false;
62
- /**
63
- * Fold a frame onto the shared ThorVG canvas + scene: flatten the instance
64
- * tree to a depth-sorted draw list, project each paintable through the
65
- * frame's camera, and paint far→near. The pipeline (flatten, world-offset
66
- * composition, projection, quad, stable-id depth sort, visibility skip,
67
- * cycle/duplicate defects) is target-agnostic; only the per-entity paint is
68
- * ThorVG-specific.
69
- */
70
- const renderToCanvas = (frame, canvas, scene,
71
- // blur sigmas are canvas-pixel amounts; the scene is authored in logical
72
- // units, so depth-of-field scales them by the device-pixel ratio
73
- dpr = 1) => Effect.gen(function* () {
74
- const origin = {
75
- x: frame.width / 2,
76
- y: frame.height / 2,
77
- };
78
- // the effective view: a camera with a point of interest auto-orients
79
- // toward it here (explicit Euler composes after — see resolveCamera);
80
- // the frame's camera data is never mutated
81
- const camera = Projection.resolveCamera(frame.camera, origin);
82
- // HUD subtrees project through the identity camera: camera-independent
83
- // placement, and structurally exempt from depth of field (aperture 0)
84
- const identityCamera = CameraMod.identity(frame.width);
85
- const visited = new Set();
86
- const paintables = [];
87
- // Flatten the tree to a draw list. A container (a node with a
88
- // `children` field) is NOT a paint-order boundary: its position
89
- // composes into its children's world coordinates, and each child is
90
- // emitted into the same flat list. `offset` is the accumulated world
91
- // translation from ancestor containers. `hud` marks an ancestor Hud:
92
- // the subtree projects through the identity camera and paints in the
93
- // top tier. `inWorldContainer` marks any ordinary container above —
94
- // a Hud there would compose world offsets into screen coordinates,
95
- // which is incoherent and dies loudly.
96
- const flatten = (id, offset, hud, inWorldContainer,
97
- // where this node's paintables land: the top-level list, or an
98
- // enclosing sub-composition's own sub-list
99
- sink) => Effect.gen(function* () {
100
- if (visited.has(id)) {
101
- return yield* Effect.die(new Error(`Renderer: instance "${id}" is referenced more than once (duplicate parent or cycle)`));
102
- }
103
- visited.add(id);
104
- const entry = frame.instances[id];
105
- if (entry === undefined) {
106
- return yield* Effect.die(new Error(`Renderer: unknown instance id "${id}"`));
107
- }
108
- const isHud = entry.entity.name === Hud.name;
109
- if (isHud && inWorldContainer) {
110
- 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)`));
111
- }
112
- const subtreeHud = hud || isHud;
113
- const effectiveCamera = subtreeHud ? identityCamera : camera;
114
- const data = entry.data;
115
- // world anchor = ancestor offset + this node's own position
116
- const world = {
117
- x: offset.x + (data.x ?? 0),
118
- y: offset.y + (data.y ?? 0),
119
- z: offset.z + (data.z ?? 0),
120
- };
121
- const childIds = childIdsOf(entry.data).filter((childId) => isVisible(frame, childId));
122
- // a SIZED container is a sub-composition (Scene.play's mount
123
- // group, or any Group given bounds): its subtree collects into
124
- // its own unit at the group's depth — clipped, backed, and
125
- // transformed as one layer, AE-precomp-style
126
- const isComp = Array.isArray(entry.data.children) &&
127
- typeof data.width === "number" &&
128
- typeof data.height === "number";
129
- if (isComp) {
130
- const proj = Projection.project(effectiveCamera, world, origin);
131
- const subs = [];
132
- yield* Effect.all(childIds.map((childId) => flatten(childId, world, subtreeHud, inWorldContainer || !subtreeHud, subs)));
133
- const groupData = entry.data;
134
- sink.push({
135
- id,
136
- entry,
137
- projection: {
138
- screen: Projection.billboardAffine(proj, {
139
- x: data.x ?? 0,
140
- y: data.y ?? 0,
141
- }),
142
- depth: proj.depth,
143
- scale: proj.scale,
144
- },
145
- hud: subtreeHud,
146
- comp: {
147
- width: data.width,
148
- height: data.height,
149
- ...(groupData.backgroundColor !== undefined
150
- ? { backgroundColor: groupData.backgroundColor }
151
- : {}),
152
- opacity: groupData.opacity ?? 1,
153
- transform: groupData.transform ?? Group.identityTransform,
154
- anchor: { x: proj.x, y: proj.y },
155
- subs,
156
- },
157
- });
158
- return;
159
- }
160
- if (childIds.length > 0) {
161
- // a pure container: contribute position, recurse, paint
162
- // nothing itself (the root, Groups, Huds). ponytail: only
163
- // translation composes down — a Group's 2D affine transform
164
- // is not yet threaded into child world coords.
165
- yield* Effect.all(childIds.map((childId) => flatten(childId, world, subtreeHud, inWorldContainer || !subtreeHud, sink)));
166
- return;
167
- }
168
- // a skeletal path leaf (Path): every command point is an
169
- // independent world point (anchor + local), projected per point
170
- // with per-subpath near-plane clipping — the paint fn emits the
171
- // exact screen subpaths. ponytail: no viewport clip yet — ThorVG
172
- // stroke cost scales with the path's full extent, offscreen
173
- // included; upgrade is per-span clipSegmentToRect with the same
174
- // splitting the near clip uses.
175
- if (Array.isArray(data.commands)) {
176
- const projected = Projection.projectPath(effectiveCamera, pathSubpaths(data.commands, world), origin);
177
- if (projected === undefined) {
178
- // entirely behind the near plane — cull
179
- return;
180
- }
181
- // anchor the billboard affine on the first visible point, as
182
- // the segment branch does — path paints ignore it (their
183
- // geometry is already screen-space) but the field stays coherent
184
- const first = projected.subpaths[0]?.points[0] ?? origin;
185
- sink.push({
186
- id,
187
- entry,
188
- projection: {
189
- screen: Projection.billboardAffine({
190
- x: first.x,
191
- y: first.y,
192
- depth: projected.depth,
193
- scale: projected.scale,
194
- }, { x: data.x ?? 0, y: data.y ?? 0 }),
195
- depth: projected.depth,
196
- scale: projected.scale,
197
- subpaths: projected.subpaths,
198
- },
199
- hud: subtreeHud,
200
- });
201
- return;
202
- }
203
- // a skeletal leaf (Line): both endpoints are independent world
204
- // points — project the pair per point (near-plane-clipped) and
205
- // hand the exact screen segment to the paint fn. Unconditional
206
- // for any x2/y2 leaf: the identity invariant makes the flat
207
- // case pixel-identical to the old billboard path.
208
- if (typeof data.x2 === "number" && typeof data.y2 === "number") {
209
- const seg = Projection.projectSegment(effectiveCamera, world, {
210
- x: offset.x + data.x2,
211
- y: offset.y + data.y2,
212
- z: offset.z + (data.z2 ?? 0),
213
- }, origin);
214
- if (seg === undefined) {
215
- // entirely behind the near plane — cull
216
- return;
217
- }
218
- // clip to the viewport before painting: ThorVG stroke cost
219
- // scales with the path's full extent, offscreen included.
220
- // The margin covers the scaled stroke (caps/joins) so the
221
- // visible pixels are untouched by the clip.
222
- const strokeMargin = (entry.data.strokeWidth ?? 1) *
223
- seg.scale +
224
- 1;
225
- const clipped = Projection.clipSegmentToRect(seg.a, seg.b, { x: -strokeMargin, y: -strokeMargin }, {
226
- x: frame.width + strokeMargin,
227
- y: frame.height + strokeMargin,
228
- });
229
- if (clipped === undefined) {
230
- // entirely offscreen — cull
231
- return;
232
- }
233
- // ponytail: one depth/scale for the whole segment (its
234
- // visible midpoint) — a depth-spanning line blurs and sorts
235
- // as one unit, the same ceiling as a tilted plane's quad.
236
- // Upgrade: split the segment where its (linear) view depth
237
- // crosses DoF bucket boundaries → gradient blur along the
238
- // line plus per-piece sort keys.
239
- sink.push({
240
- id,
241
- entry,
242
- projection: {
243
- 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 }),
244
- depth: seg.depth,
245
- scale: seg.scale,
246
- segment: clipped,
247
- },
248
- hud: subtreeHud,
249
- });
250
- return;
251
- }
252
- // a leaf paintable: project its world anchor for placement +
253
- // depth, but anchor the billboard affine on the shape's own
254
- // LOCAL coordinates — the shape paints at local (data.x/y), so
255
- // the transform maps local → screen. The composed ancestor
256
- // offset lives in the transform's translation.
257
- const proj = Projection.project(effectiveCamera, world, origin);
258
- // A rectangular plane with any nonzero rotation tilts: project
259
- // its four corners so the paint fn can emit an exact path. The
260
- // pivot is the plane's world anchor, so rotation spins it in
261
- // place. Billboards (no rotation, or non-rect shapes) skip this.
262
- const rotX = data.rotX ?? 0;
263
- const rotY = data.rotY ?? 0;
264
- const rotZ = data.rotZ ?? 0;
265
- const tilted = (rotX !== 0 || rotY !== 0 || rotZ !== 0) &&
266
- data.width !== undefined &&
267
- data.height !== undefined;
268
- const quad = tilted
269
- ? Projection.projectPlane(effectiveCamera, Projection.planeCorners({
270
- x: data.x ?? 0,
271
- y: data.y ?? 0,
272
- width: data.width,
273
- height: data.height,
274
- }, { rotX, rotY, rotZ }, world), origin)
275
- : undefined;
276
- if (quad !== undefined && quad.length < 3) {
277
- // the tilted plane is entirely behind the near plane — cull
278
- return;
279
- }
280
- sink.push({
281
- id,
282
- entry,
283
- projection: {
284
- screen: Projection.billboardAffine(proj, {
285
- x: data.x ?? 0,
286
- y: data.y ?? 0,
287
- }),
288
- depth: proj.depth,
289
- scale: proj.scale,
290
- ...(quad !== undefined ? { quad } : {}),
291
- },
292
- hud: subtreeHud,
293
- });
294
- });
295
- const rootEntry = frame.instances[frame.root];
296
- if (rootEntry === undefined) {
297
- return yield* Effect.die(new Error(`Renderer: missing root instance "${frame.root}"`));
298
- }
299
- visited.add(frame.root);
300
- yield* Effect.all(childIdsOf(rootEntry.data)
301
- .filter((childId) => isVisible(frame, childId))
302
- .map((childId) => flatten(childId, { x: 0, y: 0, z: 0 }, false, false, paintables)));
303
- // painter's order: two tiers — world content by depth (farthest
304
- // first), then HUD content by depth among itself, each with a stable
305
- // id tie-break so equal-depth paintables paint deterministically.
306
- // ponytail: naive O(n log n) per frame — swap for a spatial structure
307
- // only if a scene with thousands of objects proves it.
308
- const byPaintOrder = (a, b) => (a.hud ? 1 : 0) - (b.hud ? 1 : 0) ||
309
- b.projection.depth - a.projection.depth ||
310
- (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
311
- paintables.sort(byPaintOrder);
312
- const meta = {
313
- frameRate: frame.frameRate,
314
- width: frame.width,
315
- height: frame.height,
316
- backgroundColor: frame.backgroundColor,
317
- camera: frame.camera,
318
- };
319
- // Depth of field (camera-depth-of-field D3): with aperture > 0,
320
- // contiguous depth-sorted runs sharing a quantized blur sigma paint
321
- // into nested scenes that get a gaussian-blur effect; sharp runs paint
322
- // into the root as always, so aperture 0 (the default) takes the
323
- // unchanged single-scene path. Buckets are added to the root at run
324
- // boundaries, preserving painter's order exactly. Sigma is scaled by
325
- // dpr because the blur operates in canvas pixels while the scene is
326
- // authored in logical units.
327
- const aperture = frame.camera.aperture;
328
- let bucketScene = scene;
329
- let bucketSigma = 0;
330
- const closeBucket = Effect.gen(function* () {
331
- if (bucketSigma > 0) {
332
- // direction 0 = both axes, border 0 = duplicate, quality 75
333
- // (upstream default) — verified in the scene-blur spike
334
- yield* Tvg.Scene.addGaussianBlur(bucketScene, bucketSigma * dpr, 0, 0, 75);
335
- yield* Tvg.Scene.add(scene, bucketScene);
336
- }
337
- });
338
- // paint one paintable into `target`: a leaf via its entity's paint fn,
339
- // a sub-composition as one clipped nested scene (below)
340
- const paintOne = (p, target) => Effect.gen(function* () {
341
- if (p.comp !== undefined) {
342
- return yield* paintComp(p, p.comp, target);
343
- }
344
- // the concrete paint fn for this entity name; the map is exhaustive
345
- // over Entities by construction (PaintFunctions<Entities>). The
346
- // specific member depends on the instance's entity, known only at
347
- // runtime — hence the cast to the erased paint-fn type.
348
- const paint = builtinPaints[p.entry.entity.name];
349
- if (paint === undefined) {
350
- return yield* Effect.die(new Error(`Renderer: no paint function for entity "${p.entry.entity.name}"`));
351
- }
352
- yield* paint({
353
- entity: p.entry.entity,
354
- id: p.id,
355
- data: p.entry.data,
356
- projection: p.projection,
357
- canvas,
358
- scene: target,
359
- meta,
360
- });
361
- });
362
- // A sub-composition paints as ONE layer: inner scene clipped to the
363
- // bounds (background rect first, then the subtree in painter's order),
364
- // outer scene carrying the group's transform and opacity — the clip
365
- // lives inside, so the bounds move/scale WITH the unit, as an AE
366
- // precomp layer's frame does.
367
- const paintComp = (p, comp, target) => Effect.gen(function* () {
368
- const scale = p.projection.scale;
369
- // bounds start at the group's anchor: a comp's local space is
370
- // top-left-anchored like every shape (world = screen at rest)
371
- const w = comp.width * scale;
372
- const h = comp.height * scale;
373
- const x = comp.anchor.x;
374
- const y = comp.anchor.y;
375
- const outer = yield* Tvg.Scene.make();
376
- const clipShape = yield* Tvg.Shape.make();
377
- yield* Tvg.Shape.appendRect(clipShape, x, y, w, h);
378
- yield* Tvg.Paint.clip(outer, clipShape);
379
- if (comp.backgroundColor !== undefined) {
380
- const { r, g, b, a } = Color.bytes(comp.backgroundColor);
381
- if (a > 0) {
382
- const bg = yield* Tvg.Shape.make();
383
- yield* Tvg.Shape.appendRect(bg, x, y, w, h);
384
- yield* Tvg.Shape.setFillColor(bg, r, g, b, a);
385
- yield* Tvg.Scene.add(outer, bg);
386
- }
387
- }
388
- // ponytail: the unit's content paints sharp — depth of field
389
- // treats the comp as one layer at its anchor depth (like a
390
- // tilted plane's quad); bucket inside the unit if a deep comp
391
- // ever needs per-child blur.
392
- for (const sub of [...comp.subs].sort(byPaintOrder)) {
393
- if (sub.projection.scale <= 0 && sub.projection.quad === undefined) {
394
- continue;
395
- }
396
- yield* paintOne(sub, outer);
397
- }
398
- if (!Group.isIdentityTransform(comp.transform)) {
399
- // the group's local matrix conjugated into screen space about
400
- // the BOUNDS CENTER (AE scales a precomp layer about its
401
- // anchor, default the layer center):
402
- // screen = T(c)·S(scale)·M·S(1/scale)·T(-c), c = bounds center
403
- const cx = x + w / 2;
404
- const cy = y + h / 2;
405
- const t = (e, f) => ({
406
- a: 1,
407
- b: 0,
408
- c: 0,
409
- d: 1,
410
- e,
411
- f,
412
- });
413
- const s = (k) => ({
414
- a: k,
415
- b: 0,
416
- c: 0,
417
- d: k,
418
- e: 0,
419
- f: 0,
420
- });
421
- const m = [
422
- t(cx, cy),
423
- s(scale),
424
- comp.transform,
425
- s(1 / scale),
426
- t(-cx, -cy),
427
- ].reduce(Group.multiplyTransforms);
428
- yield* Tvg.Paint.setTransform(outer, m);
429
- }
430
- if (comp.opacity < 1) {
431
- yield* Tvg.Paint.setOpacity(outer, Math.round(Math.max(0, Math.min(1, comp.opacity)) * 255));
432
- }
433
- yield* Tvg.Scene.add(target, outer);
434
- });
435
- // paint far→near. A paintable whose anchor is behind the camera
436
- // (scale <= 0) is culled here so paint functions never see an invalid
437
- // placement — EXCEPT a tilted plane carrying a quad: its polygon is
438
- // already near-plane-clipped, and it can be visible (near part in
439
- // front) while its anchor corner is behind.
440
- for (const p of paintables) {
441
- const { projection, hud } = p;
442
- if (projection.scale <= 0 && projection.quad === undefined) {
443
- continue;
444
- }
445
- // HUD content is structurally sharp: its effective camera is the
446
- // identity camera (aperture 0), so it never enters a blur bucket
447
- const sigma = aperture > 0 && !hud
448
- ? quantizeSigma(circleOfConfusion(projection.depth, frame.camera))
449
- : 0;
450
- if (sigma !== bucketSigma) {
451
- yield* closeBucket;
452
- bucketScene = sigma === 0 ? scene : yield* Tvg.Scene.make();
453
- bucketSigma = sigma;
454
- }
455
- yield* paintOne(p, bucketScene);
456
- }
457
- yield* closeBucket;
458
- });
459
- /**
460
- * Render one frame to an RGBA framebuffer, shared by both output adapters.
461
- *
462
- * Uses the RenderSession's canvas (resized in place to the frame's physical
463
- * size, cleared of the previous frame), adds a root scene, folds the frame
464
- * onto it via `Renderer.render`, then update/draw/sync and reads the SW
465
- * framebuffer. The scene and every painted shape are scoped per frame; the
466
- * canvas belongs to the session (a player mount, an export run) and is
467
- * deleted when the session closes.
468
- *
469
- * The background is painted as a filled rect (not a canvas clear color) so it
470
- * survives into the buffer the same way the SVG sink emitted a background
471
- * rect.
472
- */
473
- export const render = (frame, options) => Effect.gen(function* () {
474
- const logicalWidth = frame.width;
475
- const logicalHeight = frame.height;
476
- const dpr = options?.dpr ?? 1;
477
- const width = Math.round(logicalWidth * dpr);
478
- const height = Math.round(logicalHeight * dpr);
479
- const canvas = yield* Tvg.Session.canvasSized(width, height);
480
- const scene = yield* Tvg.Scene.make();
481
- // background as a filled rect covering the viewport (mirrors the SVG
482
- // sink's background rect; survives into the raster buffer)
483
- const bg = yield* Tvg.Shape.make();
484
- yield* Tvg.Shape.appendRect(bg, 0, 0, logicalWidth, logicalHeight);
485
- const { r, g, b, a } = Color.bytes(frame.backgroundColor);
486
- yield* Tvg.Shape.setFillColor(bg, r, g, b, a);
487
- yield* Tvg.Scene.add(scene, bg);
488
- yield* renderToCanvas(frame, canvas, scene, dpr);
489
- // scale the whole subtree to physical pixels; children keep their own
490
- // logical affines (parent transforms compose in the scene graph)
491
- if (dpr !== 1) {
492
- yield* Tvg.Paint.scale(scene, dpr);
493
- }
494
- yield* Tvg.Canvas.add(canvas, scene);
495
- yield* Tvg.Canvas.update(canvas);
496
- // clear the target buffer: with a transparent background nothing else
497
- // overwrites the previous frame's (or uninitialized) pixels
498
- yield* Tvg.Canvas.draw(canvas, true);
499
- yield* Tvg.Canvas.sync(canvas);
500
- const buffer = yield* Tvg.Canvas.render(canvas);
501
- return {
502
- rgba: new Uint8Array(buffer),
503
- width,
504
- height,
505
- logicalWidth,
506
- logicalHeight,
507
- };
508
- });
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,71 +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
- // the demo's comp config — the old runner defaults, now explicit on the scene
13
- const demoComp = {
14
- width: 500,
15
- height: 300,
16
- backgroundColor: Color.rgba(22, 22, 29),
17
- };
18
- // children live in the group's local coordinates: one motion moves them all
19
- export const scene = Scene.make(function* () {
20
- const duo = yield* Scene.instantiate(Shapes.Group, {
21
- x: 70,
22
- y: 200,
23
- children: [
24
- Scene.instantiate(Shapes.Circle, {
25
- x: 0,
26
- y: 0,
27
- radius: 14,
28
- fill: Color.hex("#e53170"),
29
- }),
30
- Scene.instantiate(Shapes.Square, {
31
- x: 20,
32
- y: -16,
33
- size: 28,
34
- fill: Color.hex("#a786df"),
35
- }),
36
- ],
37
- });
38
- yield* Motion.wait("1.5 seconds");
39
- 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"));
40
- }, demoComp);
41
- // schedule-driven composition: a background pulse loops for the scene's
42
- // duration while three staggered dots define its actual length
43
- export const staggered = Scene.make(function* () {
44
- const stage = yield* Scene.instantiate(Shapes.Group, { x: 70, y: 150 });
45
- yield* Scene.background(Scene.repeat(stage.pipe(Motion.moveTo({ y: 130 }, "500 millis", "easeInOutCubic"), Motion.moveTo({ y: 150 }, "500 millis", "easeInOutCubic")), Schedule.forever));
46
- const dot = (x) => Effect.gen(function* () {
47
- const circle = yield* Scene.instantiate(Shapes.Circle, {
48
- x,
49
- y: 0,
50
- radius: 10,
51
- fill: Color.hex("#e53170"),
52
- });
53
- // reparent the freshly-born dot from root into the stage group
54
- yield* Scene.appendChild(stage, circle);
55
- yield* Motion.tweenTo(circle, { x: x + 300 }, "1 second", "easeInOutCubic");
56
- });
57
- yield* Scene.stagger([dot(0), dot(25), dot(50)], Schedule.spaced("250 millis"));
58
- }, demoComp);
59
- // render the middle frame of the duo scene to a PNG through the single ThorVG
60
- // renderer (Node adapter) — the end-to-end path: Scene.stream → renderToPng.
61
- const movie = Effect.gen(function* () {
62
- const frames = yield* Scene.stream(scene).pipe(Stream.runCollect);
63
- const list = [...frames];
64
- const mid = list[Math.floor(list.length / 2)];
65
- const framebuffer = yield* Renderer.render(mid);
66
- const _png = yield* PngExporter.toBuffer(framebuffer);
67
- yield* PngExporter.toFile(framebuffer, "output.png");
68
- });
69
- Effect.runPromise(movie.pipe(Effect.scoped, Effect.provide(Layer.provideMerge(
70
- // a demo scene with declared assets would add fonts/images maps here
71
- 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);