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.
- package/README.md +2 -2
- package/dist/Camera.d.ts +186 -49
- package/dist/Camera.js +343 -76
- package/dist/Color.d.ts +101 -1
- package/dist/Color.js +101 -1
- package/dist/EffectMotionError.d.ts +17 -0
- package/dist/EffectMotionError.js +17 -0
- package/dist/Entity.d.ts +682 -38
- package/dist/Entity.js +271 -27
- package/dist/Font.d.ts +108 -0
- package/dist/Font.js +95 -0
- package/dist/Image.d.ts +71 -0
- package/dist/Image.js +50 -0
- package/dist/Instance.d.ts +73 -11
- package/dist/Instance.js +44 -11
- package/dist/Motion.d.ts +328 -53
- package/dist/Motion.js +278 -46
- package/dist/Physics.d.ts +154 -20
- package/dist/Physics.js +92 -11
- package/dist/Projection.d.ts +37 -132
- package/dist/Projection.js +33 -292
- package/dist/Resource.d.ts +26 -0
- package/dist/Resource.js +41 -0
- package/dist/Runner.d.ts +639 -350
- package/dist/Runner.js +197 -150
- package/dist/Scene.d.ts +603 -175
- package/dist/Scene.js +596 -112
- package/dist/Timing.d.ts +170 -13
- package/dist/Timing.js +124 -6
- package/dist/Tree.d.ts +39 -0
- package/dist/Tree.js +127 -0
- package/dist/index.d.ts +54 -5
- package/dist/index.js +56 -5
- package/dist/particles/Particle.d.ts +2 -2
- package/dist/particles/ParticleField.d.ts +8 -6
- package/dist/particles/ParticleField.js +8 -9
- package/dist/particles/constructors.d.ts +5 -6
- package/dist/particles/constructors.js +8 -3
- package/dist/particles/legacy.d.ts +58 -0
- package/dist/particles/legacy.js +46 -0
- package/dist/particles/simulate.js +11 -3
- package/dist/particles/step.js +14 -10
- package/dist/types.d.ts +5 -0
- package/dist/types.js +1 -0
- package/package.json +2 -4
- package/dist/CameraHelpers.d.ts +0 -70
- package/dist/CameraHelpers.js +0 -239
- package/dist/CanvasExporter.d.ts +0 -12
- package/dist/CanvasExporter.js +0 -40
- package/dist/Fonts.d.ts +0 -41
- package/dist/Fonts.js +0 -27
- package/dist/Images.d.ts +0 -33
- package/dist/Images.js +0 -24
- package/dist/PngExporter.d.ts +0 -6
- package/dist/PngExporter.js +0 -85
- package/dist/Renderer.d.ts +0 -118
- package/dist/Renderer.js +0 -508
- package/dist/Shapes.d.ts +0 -11
- package/dist/Shapes.js +0 -11
- package/dist/demo.d.ts +0 -3
- package/dist/demo.js +0 -71
- package/dist/render/dof.d.ts +0 -27
- package/dist/render/dof.js +0 -37
- package/dist/render/paint.d.ts +0 -30
- package/dist/render/paint.js +0 -36
- package/dist/render/shapes.d.ts +0 -42
- package/dist/render/shapes.js +0 -310
- package/dist/shapes/Circle.d.ts +0 -32
- package/dist/shapes/Circle.js +0 -9
- package/dist/shapes/Ellipse.d.ts +0 -35
- package/dist/shapes/Ellipse.js +0 -10
- package/dist/shapes/Group.d.ts +0 -126
- package/dist/shapes/Group.js +0 -89
- package/dist/shapes/Hud.d.ts +0 -38
- package/dist/shapes/Hud.js +0 -35
- package/dist/shapes/Image.d.ts +0 -45
- package/dist/shapes/Image.js +0 -28
- package/dist/shapes/Line.d.ts +0 -47
- package/dist/shapes/Line.js +0 -35
- package/dist/shapes/Path.d.ts +0 -107
- package/dist/shapes/Path.js +0 -32
- package/dist/shapes/Rect.d.ts +0 -51
- package/dist/shapes/Rect.js +0 -22
- package/dist/shapes/Shape2D.d.ts +0 -49
- package/dist/shapes/Shape2D.js +0 -52
- package/dist/shapes/Shapes.d.ts +0 -11
- package/dist/shapes/Shapes.js +0 -11
- package/dist/shapes/Square.d.ts +0 -32
- package/dist/shapes/Square.js +0 -11
- package/dist/shapes/Text.d.ts +0 -51
- 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
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)));
|
package/dist/render/dof.d.ts
DELETED
|
@@ -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;
|
package/dist/render/dof.js
DELETED
|
@@ -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);
|