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/Projection.js
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
* point twice is bit-for-bit identical, which is what keeps 2.5D scenes
|
|
5
5
|
* deterministic.
|
|
6
6
|
*
|
|
7
|
-
* The model
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* The model: a camera with a world position, Euler orientation, and a focal
|
|
8
|
+
* length, looking down world -z at rest. World points are transformed into
|
|
9
|
+
* the camera's frame (the view transform, which is the inverse of the
|
|
10
|
+
* camera's own world transform), then divided by their depth in front of the
|
|
11
|
+
* camera to land on screen. Scene space is right-handed — x right, y up,
|
|
12
|
+
* origin at the viewport center, +z toward the viewer — so the camera's
|
|
13
|
+
* optical axis passes through world (0, 0) at rest.
|
|
12
14
|
*
|
|
13
15
|
* Identity invariant: the default camera (see `defaultFocalLength` and
|
|
14
16
|
* `defaultCameraZ`) projects a world point at `z = 0` to screen `(x, y)` at
|
|
@@ -102,8 +104,7 @@ const rotate = (v, rx, ry, rz) => {
|
|
|
102
104
|
};
|
|
103
105
|
/**
|
|
104
106
|
* The auto-orient Euler angles (yaw + pitch, no roll) aiming a camera at
|
|
105
|
-
* `poi` from its WORLD position
|
|
106
|
-
* see `resolveCamera`). The view transform flips z before rotating
|
|
107
|
+
* `poi` from its WORLD position. The view transform flips z before rotating
|
|
107
108
|
* (in-front is +z), which inverts rotation handedness vs. world space;
|
|
108
109
|
* that subtlety is handled here, exactly once, pinned by tests that
|
|
109
110
|
* project the POI and assert it lands on the viewport center.
|
|
@@ -125,13 +126,13 @@ export const lookAtOrientation = (position, poi) => {
|
|
|
125
126
|
* camera's own frame (so a lone `rotZ` rolls about the view axis and the
|
|
126
127
|
* POI stays centered); the exact composed rotation is extracted back to
|
|
127
128
|
* the fixed Rz·Ry·Rx Euler convention the view transform consumes.
|
|
128
|
-
* Camera `x`/`y` are
|
|
129
|
-
*
|
|
130
|
-
* (one-node camera, unchanged); a partial POI is a loud
|
|
131
|
-
* user's rotation fields are never written back — derivation
|
|
132
|
-
* here, at view-assembly time.
|
|
129
|
+
* Camera `x`/`y` are world coordinates (the center-origin frame makes a
|
|
130
|
+
* resting camera sit on the optical axis through world (0, 0)). Absent POI
|
|
131
|
+
* is a pass-through (one-node camera, unchanged); a partial POI is a loud
|
|
132
|
+
* defect. The user's rotation fields are never written back — derivation
|
|
133
|
+
* happens here, at view-assembly time.
|
|
133
134
|
*/
|
|
134
|
-
export const resolveCamera = (camera
|
|
135
|
+
export const resolveCamera = (camera) => {
|
|
135
136
|
const { poiX, poiY, poiZ } = camera;
|
|
136
137
|
const present = [poiX, poiY, poiZ].filter((v) => v !== undefined).length;
|
|
137
138
|
if (present === 0) {
|
|
@@ -141,8 +142,8 @@ export const resolveCamera = (camera, origin) => {
|
|
|
141
142
|
throw new Error("Camera: a point of interest requires all of poiX, poiY, poiZ — got a partial POI");
|
|
142
143
|
}
|
|
143
144
|
const world = {
|
|
144
|
-
x:
|
|
145
|
-
y:
|
|
145
|
+
x: camera.x,
|
|
146
|
+
y: camera.y,
|
|
146
147
|
z: camera.z,
|
|
147
148
|
};
|
|
148
149
|
const aim = lookAtOrientation(world, {
|
|
@@ -173,301 +174,41 @@ export const resolveCamera = (camera, origin) => {
|
|
|
173
174
|
return { ...camera, rotX, rotY, rotZ };
|
|
174
175
|
};
|
|
175
176
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
177
|
+
* A world point in the camera's frame. The camera's `x`/`y` are its world
|
|
178
|
+
* position (origin at the viewport center), so a resting camera keeps
|
|
179
|
+
* world-x/y = screen-x/y at unit scale. `+z` in the result is in front of
|
|
180
|
+
* the camera: the camera looks down world -z, so a resting camera at
|
|
181
|
+
* z=focalLength sees the z=0 plane at view-z = focalLength.
|
|
181
182
|
*/
|
|
182
|
-
export const
|
|
183
|
-
const { rotX, rotY, rotZ } = orientation;
|
|
184
|
-
// local corners relative to the rect's own origin (the rotation pivot)
|
|
185
|
-
const local = [
|
|
186
|
-
{ x: 0, y: 0 },
|
|
187
|
-
{ x: rect.width, y: 0 },
|
|
188
|
-
{ x: rect.width, y: rect.height },
|
|
189
|
-
{ x: 0, y: rect.height },
|
|
190
|
-
];
|
|
191
|
-
const place = (c) => {
|
|
192
|
-
const r = rotate({ x: c.x, y: c.y, z: 0 }, rotX, rotY, rotZ);
|
|
193
|
-
return { x: world.x + r.x, y: world.y + r.y, z: world.z + r.z };
|
|
194
|
-
};
|
|
195
|
-
return [place(local[0]), place(local[1]), place(local[2]), place(local[3])];
|
|
196
|
-
};
|
|
197
|
-
/**
|
|
198
|
-
* A world point in the camera's frame, measured relative to `origin` (the
|
|
199
|
-
* viewport center). The camera's `x`/`y` are a pan *from* the origin, so a
|
|
200
|
-
* resting camera (pan 0) keeps world-x/y = screen-x/y at unit scale. `+z` in
|
|
201
|
-
* the result is in front of the camera: the camera looks down world -z, so a
|
|
202
|
-
* resting camera at z=focalLength sees the z=0 plane at view-z = focalLength.
|
|
203
|
-
*/
|
|
204
|
-
export const toView = (camera, p, origin) => {
|
|
183
|
+
export const toView = (camera, p) => {
|
|
205
184
|
const translated = {
|
|
206
|
-
x: p.x -
|
|
207
|
-
y: p.y -
|
|
185
|
+
x: p.x - camera.x,
|
|
186
|
+
y: p.y - camera.y,
|
|
208
187
|
z: camera.z - p.z, // flip so in-front is +z (camera looks toward -world-z)
|
|
209
188
|
};
|
|
210
189
|
return rotateInverse(translated, camera.rotX, camera.rotY, camera.rotZ);
|
|
211
190
|
};
|
|
212
191
|
/**
|
|
213
|
-
* Project a world point to screen.
|
|
214
|
-
*
|
|
215
|
-
*
|
|
192
|
+
* Project a world point to screen. The camera's optical axis passes through
|
|
193
|
+
* screen (0, 0) — the viewport center — so pan/zoom read as "into the
|
|
194
|
+
* middle of the shot", while a resting camera reproduces plain-2D
|
|
216
195
|
* placement. A point at or behind the camera (view-z <= 0) has no valid
|
|
217
196
|
* projection; `depth` is still returned for sorting, `scale` clamps to 0.
|
|
218
197
|
*/
|
|
219
|
-
export const project = (camera, p
|
|
220
|
-
const v = toView(camera, p
|
|
198
|
+
export const project = (camera, p) => {
|
|
199
|
+
const v = toView(camera, p);
|
|
221
200
|
const depth = v.z;
|
|
222
201
|
const scale = depth > 0 ? camera.focalLength / depth : 0;
|
|
223
202
|
return {
|
|
224
|
-
x:
|
|
225
|
-
y:
|
|
203
|
+
x: v.x * scale,
|
|
204
|
+
y: v.y * scale,
|
|
226
205
|
depth,
|
|
227
206
|
scale,
|
|
228
207
|
};
|
|
229
208
|
};
|
|
230
|
-
export const billboardAffine = (proj, anchor) => ({
|
|
231
|
-
a: proj.scale,
|
|
232
|
-
b: 0,
|
|
233
|
-
c: 0,
|
|
234
|
-
d: proj.scale,
|
|
235
|
-
// place the anchor's local coords at the projected screen point
|
|
236
|
-
e: proj.x - anchor.x * proj.scale,
|
|
237
|
-
f: proj.y - anchor.y * proj.scale,
|
|
238
|
-
});
|
|
239
|
-
/**
|
|
240
|
-
* View-space depth (px) of the near clip plane. A tilted plane can be
|
|
241
|
-
* PARTIALLY behind the camera — projecting a corner at depth <= 0 is
|
|
242
|
-
* meaningless (the old code pinned it to the viewport center, folding the
|
|
243
|
-
* polygon), so the polygon is clipped against this plane first. 1px keeps
|
|
244
|
-
* the worst-case projected scale bounded at `focalLength` per unit.
|
|
245
|
-
*/
|
|
246
|
-
const NEAR = 1;
|
|
247
|
-
/**
|
|
248
|
-
* Project the world-space corners of a tilted plane to a screen polygon.
|
|
249
|
-
* The polygon is clipped against the near plane in view space
|
|
250
|
-
* (Sutherland–Hodgman, winding preserved) before the per-vertex perspective
|
|
251
|
-
* divide, so a plane crossing the camera renders its visible part instead
|
|
252
|
-
* of folding — a quad straddling the plane yields up to 5 vertices; a plane
|
|
253
|
-
* fully behind yields none (cull). Fully in front, this is plain per-corner
|
|
254
|
-
* projection: a receding plane is a true perspective trapezoid.
|
|
255
|
-
*/
|
|
256
|
-
export const projectPlane = (camera, corners, origin) => {
|
|
257
|
-
const view = corners.map((c) => toView(camera, c, origin));
|
|
258
|
-
return clipPolygonNear(view).map((v) => {
|
|
259
|
-
const scale = camera.focalLength / v.z;
|
|
260
|
-
return { x: origin.x + v.x * scale, y: origin.y + v.y * scale };
|
|
261
|
-
});
|
|
262
|
-
};
|
|
263
|
-
/**
|
|
264
|
-
* Sutherland–Hodgman clip of a view-space polygon against the near plane,
|
|
265
|
-
* winding preserved. Shared by tilted planes and closed path subpaths.
|
|
266
|
-
*/
|
|
267
|
-
const clipPolygonNear = (view) => {
|
|
268
|
-
const clipped = [];
|
|
269
|
-
for (let i = 0; i < view.length; i++) {
|
|
270
|
-
// biome-ignore lint/style/noNonNullAssertion: i and (i+1)%length are in bounds
|
|
271
|
-
const a = view[i];
|
|
272
|
-
// biome-ignore lint/style/noNonNullAssertion: see above
|
|
273
|
-
const b = view[(i + 1) % view.length];
|
|
274
|
-
const aIn = a.z >= NEAR;
|
|
275
|
-
if (aIn) {
|
|
276
|
-
clipped.push(a);
|
|
277
|
-
}
|
|
278
|
-
if (aIn !== b.z >= NEAR) {
|
|
279
|
-
const t = (NEAR - a.z) / (b.z - a.z);
|
|
280
|
-
clipped.push({
|
|
281
|
-
x: a.x + (b.x - a.x) * t,
|
|
282
|
-
y: a.y + (b.y - a.y) * t,
|
|
283
|
-
z: NEAR,
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
return clipped;
|
|
288
|
-
};
|
|
289
|
-
/**
|
|
290
|
-
* Project a world-space segment (a skeletal shape's two endpoints) to
|
|
291
|
-
* screen. Both endpoints go to view space, are clipped against the near
|
|
292
|
-
* plane (lerp to z = NEAR — the 1D case of projectPlane's polygon clip),
|
|
293
|
-
* and are projected individually, so a line spanning depth foreshortens
|
|
294
|
-
* per endpoint. Returns `undefined` when the segment lies entirely behind
|
|
295
|
-
* the near plane (cull). `depth`/`scale` come from the visible midpoint.
|
|
296
|
-
*/
|
|
297
|
-
export const projectSegment = (camera, a, b, origin) => {
|
|
298
|
-
let va = toView(camera, a, origin);
|
|
299
|
-
let vb = toView(camera, b, origin);
|
|
300
|
-
if (va.z < NEAR && vb.z < NEAR) {
|
|
301
|
-
return undefined;
|
|
302
|
-
}
|
|
303
|
-
const clip = (inside, outside) => {
|
|
304
|
-
const t = (NEAR - inside.z) / (outside.z - inside.z);
|
|
305
|
-
return {
|
|
306
|
-
x: inside.x + (outside.x - inside.x) * t,
|
|
307
|
-
y: inside.y + (outside.y - inside.y) * t,
|
|
308
|
-
z: NEAR,
|
|
309
|
-
};
|
|
310
|
-
};
|
|
311
|
-
if (va.z < NEAR) {
|
|
312
|
-
va = clip(vb, va);
|
|
313
|
-
}
|
|
314
|
-
else if (vb.z < NEAR) {
|
|
315
|
-
vb = clip(va, vb);
|
|
316
|
-
}
|
|
317
|
-
const toScreen = (v) => {
|
|
318
|
-
const s = camera.focalLength / v.z;
|
|
319
|
-
return { x: origin.x + v.x * s, y: origin.y + v.y * s };
|
|
320
|
-
};
|
|
321
|
-
const depth = (va.z + vb.z) / 2;
|
|
322
|
-
return {
|
|
323
|
-
a: toScreen(va),
|
|
324
|
-
b: toScreen(vb),
|
|
325
|
-
depth,
|
|
326
|
-
scale: camera.focalLength / depth,
|
|
327
|
-
};
|
|
328
|
-
};
|
|
329
|
-
/**
|
|
330
|
-
* Project a skeletal path's world-space subpaths to screen — the N-point
|
|
331
|
-
* generalization of `projectSegment`. Every point goes to view space and is
|
|
332
|
-
* projected individually, so a path spanning depth foreshortens per point.
|
|
333
|
-
* Near-plane clipping is per subpath: a closed subpath clips as a polygon
|
|
334
|
-
* (Sutherland–Hodgman, winding preserved — the tilted-plane treatment); an
|
|
335
|
-
* open subpath clips per span (lerp to z = NEAR) and SPLITS into separate
|
|
336
|
-
* visible pieces when interior points fall behind the plane. Returns
|
|
337
|
-
* `undefined` when nothing survives (cull). `depth`/`scale` come from the
|
|
338
|
-
* mean view depth of all emitted points — one key per path, the same
|
|
339
|
-
* ceiling as a segment's midpoint.
|
|
340
|
-
*/
|
|
341
|
-
export const projectPath = (camera, subpaths, origin) => {
|
|
342
|
-
const pieces = [];
|
|
343
|
-
for (const subpath of subpaths) {
|
|
344
|
-
const view = subpath.points.map((p) => toView(camera, p, origin));
|
|
345
|
-
if (subpath.closed) {
|
|
346
|
-
const clipped = clipPolygonNear(view);
|
|
347
|
-
if (clipped.length >= 2) {
|
|
348
|
-
pieces.push({ points: clipped, closed: true });
|
|
349
|
-
}
|
|
350
|
-
continue;
|
|
351
|
-
}
|
|
352
|
-
// open: walk the spans, clipping and splitting at the near plane
|
|
353
|
-
let current = [];
|
|
354
|
-
const clip = (a, b) => {
|
|
355
|
-
const t = (NEAR - a.z) / (b.z - a.z);
|
|
356
|
-
return {
|
|
357
|
-
x: a.x + (b.x - a.x) * t,
|
|
358
|
-
y: a.y + (b.y - a.y) * t,
|
|
359
|
-
z: NEAR,
|
|
360
|
-
};
|
|
361
|
-
};
|
|
362
|
-
const flush = () => {
|
|
363
|
-
if (current.length >= 2) {
|
|
364
|
-
pieces.push({ points: current, closed: false });
|
|
365
|
-
}
|
|
366
|
-
current = [];
|
|
367
|
-
};
|
|
368
|
-
for (let i = 0; i + 1 < view.length; i++) {
|
|
369
|
-
// biome-ignore lint/style/noNonNullAssertion: i and i+1 are in bounds
|
|
370
|
-
const a = view[i];
|
|
371
|
-
// biome-ignore lint/style/noNonNullAssertion: see above
|
|
372
|
-
const b = view[i + 1];
|
|
373
|
-
const aIn = a.z >= NEAR;
|
|
374
|
-
const bIn = b.z >= NEAR;
|
|
375
|
-
if (aIn) {
|
|
376
|
-
if (current.length === 0) {
|
|
377
|
-
current.push(a);
|
|
378
|
-
}
|
|
379
|
-
if (bIn) {
|
|
380
|
-
current.push(b);
|
|
381
|
-
}
|
|
382
|
-
else {
|
|
383
|
-
current.push(clip(a, b));
|
|
384
|
-
flush();
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
else if (bIn) {
|
|
388
|
-
current = [clip(b, a), b];
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
flush();
|
|
392
|
-
}
|
|
393
|
-
if (pieces.length === 0) {
|
|
394
|
-
return undefined;
|
|
395
|
-
}
|
|
396
|
-
let depthSum = 0;
|
|
397
|
-
let depthCount = 0;
|
|
398
|
-
for (const piece of pieces) {
|
|
399
|
-
for (const v of piece.points) {
|
|
400
|
-
depthSum += v.z;
|
|
401
|
-
depthCount++;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
const depth = depthSum / depthCount;
|
|
405
|
-
const toScreen = (v) => {
|
|
406
|
-
const s = camera.focalLength / v.z;
|
|
407
|
-
return { x: origin.x + v.x * s, y: origin.y + v.y * s };
|
|
408
|
-
};
|
|
409
|
-
return {
|
|
410
|
-
subpaths: pieces.map((piece) => ({
|
|
411
|
-
points: piece.points.map(toScreen),
|
|
412
|
-
closed: piece.closed,
|
|
413
|
-
})),
|
|
414
|
-
depth,
|
|
415
|
-
scale: camera.focalLength / depth,
|
|
416
|
-
};
|
|
417
|
-
};
|
|
418
|
-
/**
|
|
419
|
-
* Clip a screen-space segment to a rectangle (Liang–Barsky). Returns the
|
|
420
|
-
* clipped pair, or `undefined` when the segment lies entirely outside.
|
|
421
|
-
* ThorVG's software rasterizer pays stroke cost proportional to a path's
|
|
422
|
-
* full extent — offscreen included — so segments are clipped to the
|
|
423
|
-
* viewport (plus a stroke margin) before painting; a near-camera line can
|
|
424
|
-
* project tens of thousands of px wide otherwise (measured ~7× the cost
|
|
425
|
-
* of its visible part).
|
|
426
|
-
*/
|
|
427
|
-
export const clipSegmentToRect = (a, b, min, max) => {
|
|
428
|
-
const dx = b.x - a.x;
|
|
429
|
-
const dy = b.y - a.y;
|
|
430
|
-
let t0 = 0;
|
|
431
|
-
let t1 = 1;
|
|
432
|
-
const edges = [
|
|
433
|
-
[-dx, a.x - min.x],
|
|
434
|
-
[dx, max.x - a.x],
|
|
435
|
-
[-dy, a.y - min.y],
|
|
436
|
-
[dy, max.y - a.y],
|
|
437
|
-
];
|
|
438
|
-
for (const [p, q] of edges) {
|
|
439
|
-
if (p === 0) {
|
|
440
|
-
// parallel to this edge: outside it means fully outside
|
|
441
|
-
if (q < 0) {
|
|
442
|
-
return undefined;
|
|
443
|
-
}
|
|
444
|
-
continue;
|
|
445
|
-
}
|
|
446
|
-
const r = q / p;
|
|
447
|
-
if (p < 0) {
|
|
448
|
-
if (r > t1) {
|
|
449
|
-
return undefined;
|
|
450
|
-
}
|
|
451
|
-
if (r > t0) {
|
|
452
|
-
t0 = r;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
else {
|
|
456
|
-
if (r < t0) {
|
|
457
|
-
return undefined;
|
|
458
|
-
}
|
|
459
|
-
if (r < t1) {
|
|
460
|
-
t1 = r;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
const at = (t) => ({ x: a.x + dx * t, y: a.y + dy * t });
|
|
465
|
-
return [t0 === 0 ? a : at(t0), t1 === 1 ? b : at(t1)];
|
|
466
|
-
};
|
|
467
209
|
/**
|
|
468
210
|
* The view-space depth of a world point — the painter's-sort key alone.
|
|
469
211
|
* With no camera rotation this is `camera.z - p.z`; rotation tilts the
|
|
470
|
-
* depth axis, so the full view transform is used.
|
|
471
|
-
* never depth, so a zero origin suffices.
|
|
212
|
+
* depth axis, so the full view transform is used.
|
|
472
213
|
*/
|
|
473
|
-
export const depthOf = (camera, p) => toView(camera, p
|
|
214
|
+
export const depthOf = (camera, p) => toView(camera, p).z;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { EffectMotionError } from "./EffectMotionError.js";
|
|
3
|
+
/**
|
|
4
|
+
* The shared loader brand. Every loader service (FontLoader, ImageLoader,
|
|
5
|
+
* future kinds) carries it, and it is the ONLY thing they share: loader
|
|
6
|
+
* shapes stay separate per kind (fonts grow font metadata, images image
|
|
7
|
+
* metadata) so nothing over-abstracts, while the type utilities below are
|
|
8
|
+
* written once against the brand.
|
|
9
|
+
*/
|
|
10
|
+
export declare const LoaderTypeId: "~effect-motion/Resources/Loader";
|
|
11
|
+
export interface LoaderBrand {
|
|
12
|
+
readonly [LoaderTypeId]: typeof LoaderTypeId;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The loader members of a requirements union — what a scene's frames carry
|
|
16
|
+
* as `Frame<Resources>` and what the renderer requires. Distributes:
|
|
17
|
+
* `FontLoader<"a"> | Runner` → `FontLoader<"a">`.
|
|
18
|
+
*/
|
|
19
|
+
export type ExtractLoaders<R> = R extends LoaderBrand ? R : never;
|
|
20
|
+
/**
|
|
21
|
+
* A requirements union with the loader members removed — what running a
|
|
22
|
+
* scene actually needs. Frames are pure of resource bytes (the engine
|
|
23
|
+
* cannot measure text), so `Scene.run`/`stream` require only this.
|
|
24
|
+
*/
|
|
25
|
+
export type ExcludeLoaders<R> = R extends LoaderBrand ? never : R;
|
|
26
|
+
export declare const fetchBytes: (url: string) => Effect.Effect<Uint8Array, EffectMotionError>;
|
package/dist/Resource.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { EffectMotionError } from "./EffectMotionError.js";
|
|
3
|
+
/**
|
|
4
|
+
* The shared loader brand. Every loader service (FontLoader, ImageLoader,
|
|
5
|
+
* future kinds) carries it, and it is the ONLY thing they share: loader
|
|
6
|
+
* shapes stay separate per kind (fonts grow font metadata, images image
|
|
7
|
+
* metadata) so nothing over-abstracts, while the type utilities below are
|
|
8
|
+
* written once against the brand.
|
|
9
|
+
*/
|
|
10
|
+
export const LoaderTypeId = "~effect-motion/Resources/Loader";
|
|
11
|
+
/**
|
|
12
|
+
* Fetch a resource's bytes by URL — the common browser-side load effect for
|
|
13
|
+
* `Font.layer`/`Image.layer`. Fails with a typed error naming the URL, so a
|
|
14
|
+
* bad source surfaces at layer construction (e.g. the player's error
|
|
15
|
+
* state), never at frame time. Compose retries on top as needed.
|
|
16
|
+
*
|
|
17
|
+
* Memoized per URL at module level: asset bytes are immutable, and layers
|
|
18
|
+
* rebuild per Player mount (studio scene switches), so each URL is fetched
|
|
19
|
+
* at most once per process. A FAILED fetch is not cached — the next
|
|
20
|
+
* construction retries. Custom load effects cache themselves (this pattern)
|
|
21
|
+
* when they should.
|
|
22
|
+
*/
|
|
23
|
+
const fetchCache = new Map();
|
|
24
|
+
export const fetchBytes = (url) => Effect.tryPromise({
|
|
25
|
+
try: () => {
|
|
26
|
+
let pending = fetchCache.get(url);
|
|
27
|
+
if (pending === undefined) {
|
|
28
|
+
pending = fetch(url).then(async (response) => {
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw new Error(`HTTP ${response.status}`);
|
|
31
|
+
}
|
|
32
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
33
|
+
});
|
|
34
|
+
// evict on failure so a transient error can retry
|
|
35
|
+
pending.catch(() => fetchCache.delete(url));
|
|
36
|
+
fetchCache.set(url, pending);
|
|
37
|
+
}
|
|
38
|
+
return pending;
|
|
39
|
+
},
|
|
40
|
+
catch: (cause) => EffectMotionError.of(`Resource fetch failed: ${url}`, cause),
|
|
41
|
+
});
|