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/Physics.js CHANGED
@@ -4,12 +4,36 @@ import * as Entity from "./Entity.js";
4
4
  import * as Instance from "./Instance.js";
5
5
  import * as Runner from "./Runner.js";
6
6
  import * as Scene from "./Scene.js";
7
- /** fast, gently damped general-purpose spring */
7
+ /**
8
+ * The spring used when a call names none: quick and only lightly damped, so
9
+ * it overshoots a little before settling.
10
+ */
8
11
  export const defaultSpring = {
9
12
  mass: 0.05,
10
13
  stiffness: 10,
11
14
  damping: 0.5,
12
15
  };
16
+ /**
17
+ * The named spring presets, each a calibrated point in `{ mass, stiffness,
18
+ * damping }` space. Pass the NAME to any spring animator — it autocompletes.
19
+ *
20
+ * @remarks
21
+ * Durations below are measured for a 100px move at 60fps and scale with
22
+ * distance; a spring's length is emergent, so treat them as character, not
23
+ * contract:
24
+ *
25
+ * - `strike` — hardest and fastest, launched with initial velocity (~0.7s).
26
+ * - `jump` — a quick launched hop (~1.4s).
27
+ * - `smooth` — mild overshoot, unobtrusive (~2s).
28
+ * - `beat` — a small, tight pulse; the least overshoot of the set (~2.6s).
29
+ * - `swing` — a relaxed, pendulum-like arc (~3.5s).
30
+ * - `plop` — lands heavy and wobbles noticeably (~7s).
31
+ * - `bounce` — barely damped, so it rings for a LONG time: about 37 seconds
32
+ * and 90 direction changes for a 100px move. Striking as a one-off accent,
33
+ * but check the frame count before putting it in a timed sequence.
34
+ *
35
+ * @see {@link Spring} to define your own.
36
+ */
13
37
  export const springs = {
14
38
  beat: { mass: 0.13, stiffness: 5.7, damping: 1.2, initialVelocity: 10 },
15
39
  plop: { mass: 0.2, stiffness: 20, damping: 0.68 },
@@ -95,23 +119,80 @@ const simulate = Effect.fnUntraced(function* (from, to, springInput, fn, settleT
95
119
  yield* Scene.tick;
96
120
  });
97
121
  const springPosition = Effect.fnUntraced(function* (instanceOrEffect, from, to, springInput, settleTolerance) {
98
- const instance = yield* Instance.flatten(instanceOrEffect);
99
- const lens = Entity.traitOrDie(instance.entity, "~position");
100
- const current = lens.get(yield* Scene.data(instance));
122
+ const instance = yield* Instance.flattenInstance(instanceOrEffect);
123
+ // flatten the Vec3 for the simulator: it works on flat numeric records
124
+ // (design D2), so the tagged struct is unwrapped here and rebuilt below
125
+ const position = (yield* Scene.data(instance)).position;
126
+ const current = { x: position.x, y: position.y, z: position.z };
101
127
  // partial targets/origins hold the missing axis at its current value
102
128
  const target = { ...current, ...to };
103
129
  const start = { ...current, ...(from ?? {}) };
104
- yield* simulate(start, target, springInput ?? defaultSpring, (value) => Scene.update(instance, (data) => lens.set(data, value)), settleTolerance);
130
+ yield* simulate(start, target, springInput ?? defaultSpring, (value) => Scene.update(instance, (data) => ({
131
+ ...data,
132
+ position: Entity.vec3(value),
133
+ })), settleTolerance);
105
134
  return instance;
106
135
  });
107
136
  const firstArgIsInstance = (args) => Instance.isInstance(args[0]);
108
137
  /**
109
- * Spring an instance to a position via its `~position` trait — the
110
- * durationless counterpart of `Motion.moveTo` (settles exactly, length
111
- * emerges from the physics). Dual:
112
- * `springTo(instance, to, springInput?, settleTolerance?)` or
113
- * `instance.pipe(springTo(to, springInput?, settleTolerance?))`.
138
+ * Spring an instance to a position — `Motion.moveTo` with momentum instead
139
+ * of a duration.
140
+ *
141
+ * @remarks
142
+ * The animation runs until the simulation settles, then lands exactly on
143
+ * `to`, so chaining springs is safe: each starts precisely where the last
144
+ * finished, with no accumulated drift. How long that takes depends on the
145
+ * spring AND the distance — a longer move rings longer.
146
+ *
147
+ * Because the length is emergent, a spring is awkward to synchronize
148
+ * against a fixed beat. When several things must finish together, either
149
+ * spring them inside a `Scene.all` and let the slowest govern, or use a
150
+ * duration-based tween with an elastic easing.
151
+ *
152
+ * Targets are partial, like `moveTo` — naming only `x` holds the other axes.
153
+ * Every axis is simulated by the same spring, so a diagonal move settles as
154
+ * one motion.
155
+ *
156
+ * @param to - Target position; omitted axes hold their current value.
157
+ * @param springInput - A preset name from {@link springs}, or a custom
158
+ * {@link Spring}.
159
+ * @param settleTolerance - How close to the target (in both displacement
160
+ * and velocity) counts as settled. Larger ends sooner and cuts the tail.
161
+ * @defaultValue `springInput` — {@link defaultSpring}; `settleTolerance` — `0.001`
162
+ * @returns The instance, so animators chain.
163
+ * @see {@link spring} to start from an explicit position.
164
+ *
165
+ * @example
166
+ * A struck-then-settling entrance, using two different presets.
167
+ * ```typescript
168
+ * yield* badge.pipe(
169
+ * Physics.springTo({ y: 120 }, "strike"),
170
+ * Physics.springTo({ x: 300 }, "smooth"),
171
+ * );
172
+ * ```
114
173
  */
115
174
  export const springTo = dual(firstArgIsInstance, (instance, to, springInput, settleTolerance) => springPosition(instance, undefined, to, springInput, settleTolerance));
116
- /** Like `springTo`, but from an explicit position (partials filled from current). */
175
+ /**
176
+ * Like {@link springTo}, but starting from an explicit position.
177
+ *
178
+ * @remarks
179
+ * The springy entrance: an instance declared at its resting place can still
180
+ * fly in from off-screen and settle, because the origin is stated rather
181
+ * than read from the entity. Both `from` and `to` are partial and fill
182
+ * missing axes from the current position.
183
+ *
184
+ * @param from - Starting position; omitted axes start at the current value.
185
+ * @param to - Target position; omitted axes hold their current value.
186
+ * @param springInput - A preset name from {@link springs}, or a custom
187
+ * {@link Spring}.
188
+ * @param settleTolerance - How close counts as settled.
189
+ * @defaultValue `springInput` — {@link defaultSpring}; `settleTolerance` — `0.001`
190
+ * @returns The instance, so animators chain.
191
+ *
192
+ * @example
193
+ * Drop a card in from above and let it settle where it was declared.
194
+ * ```typescript
195
+ * yield* card.pipe(Physics.spring({ y: -200 }, { y: 150 }, "plop"));
196
+ * ```
197
+ */
117
198
  export const spring = dual(firstArgIsInstance, (instance, from, to, springInput, settleTolerance) => springPosition(instance, from, to, springInput, settleTolerance));
@@ -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 is the After Effects one: a camera with a world position, Euler
8
- * orientation, and a focal length, looking down world -z at rest. World
9
- * points are transformed into the camera's frame (the view transform, which
10
- * is the inverse of the camera's own world transform), then divided by their
11
- * depth in front of the camera to land on screen.
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
@@ -23,10 +25,6 @@ export interface Vec3 {
23
25
  readonly y: number;
24
26
  readonly z: number;
25
27
  }
26
- export interface Vec2 {
27
- readonly x: number;
28
- readonly y: number;
29
- }
30
28
  /** The camera view, as it arrives on the frame from the runner. */
31
29
  export interface CameraView {
32
30
  readonly x: number;
@@ -37,11 +35,19 @@ export interface CameraView {
37
35
  readonly rotZ: number;
38
36
  readonly focalLength: number;
39
37
  /**
40
- * view-space distance to the sharp plane (depth of field). Runner-filled
41
- * to the resting camera distance, so the z=0 plane is in focus untouched.
38
+ * View-space distance to the sharp plane, world units. Runner-filled to
39
+ * the resting camera distance.
42
40
  */
43
41
  readonly focusDistance: number;
44
- /** depth-of-field blur strength; 0 (the default) = pinhole, DoF off */
42
+ /**
43
+ * Lens radius in world units; 0 is a pinhole and bypasses depth of field.
44
+ *
45
+ * @remarks
46
+ * Thin-lens circle of confusion: a point at view distance d blurs to
47
+ * radius `aperture · |d − focusDistance| / d` world units on the focus
48
+ * plane. Opaque shapes only (semi-transparent ones blur at their own
49
+ * depth); perspective only; roughly doubles the scene's draw cost.
50
+ */
45
51
  readonly aperture: number;
46
52
  }
47
53
  /**
@@ -76,8 +82,7 @@ export interface PointOfInterest {
76
82
  }
77
83
  /**
78
84
  * The auto-orient Euler angles (yaw + pitch, no roll) aiming a camera at
79
- * `poi` from its WORLD position (viewport-center pan already composed in —
80
- * see `resolveCamera`). The view transform flips z before rotating
85
+ * `poi` from its WORLD position. The view transform flips z before rotating
81
86
  * (in-front is +z), which inverts rotation handedness vs. world space;
82
87
  * that subtlety is handled here, exactly once, pinned by tests that
83
88
  * project the POI and assert it lands on the viewport center.
@@ -93,133 +98,32 @@ export declare const lookAtOrientation: (position: Vec3, poi: Vec3) => {
93
98
  * camera's own frame (so a lone `rotZ` rolls about the view axis and the
94
99
  * POI stays centered); the exact composed rotation is extracted back to
95
100
  * the fixed Rz·Ry·Rx Euler convention the view transform consumes.
96
- * Camera `x`/`y` are pan-from-viewport-center, so the world position
97
- * composes `origin` in before aiming. Absent POI is a pass-through
98
- * (one-node camera, unchanged); a partial POI is a loud defect. The
99
- * user's rotation fields are never written back — derivation happens
100
- * here, at view-assembly time.
101
- */
102
- export declare const resolveCamera: (camera: CameraView & PointOfInterest, origin: Vec2) => CameraView;
103
- /**
104
- * The four world-space corners of a flat rectangular plane. The rect spans
105
- * local `[x, x+width] × [y, y+height]` on the z=0 plane; each corner is
106
- * rotated about the rect's local origin `(x, y)` by the Euler orientation,
107
- * then translated to `world` (the plane's composed world anchor minus its own
108
- * local x/y, so rotation pivots on the anchor). Winding: TL, TR, BR, BL.
101
+ * Camera `x`/`y` are world coordinates (the center-origin frame makes a
102
+ * resting camera sit on the optical axis through world (0, 0)). Absent POI
103
+ * is a pass-through (one-node camera, unchanged); a partial POI is a loud
104
+ * defect. The user's rotation fields are never written back — derivation
105
+ * happens here, at view-assembly time.
109
106
  */
110
- export declare const planeCorners: (rect: {
111
- x: number;
112
- y: number;
113
- width: number;
114
- height: number;
115
- }, orientation: {
116
- rotX: number;
117
- rotY: number;
118
- rotZ: number;
119
- }, world: Vec3) => [Vec3, Vec3, Vec3, Vec3];
107
+ export declare const resolveCamera: (camera: CameraView & PointOfInterest) => CameraView;
120
108
  /**
121
- * A world point in the camera's frame, measured relative to `origin` (the
122
- * viewport center). The camera's `x`/`y` are a pan *from* the origin, so a
123
- * resting camera (pan 0) keeps world-x/y = screen-x/y at unit scale. `+z` in
124
- * the result is in front of the camera: the camera looks down world -z, so a
125
- * resting camera at z=focalLength sees the z=0 plane at view-z = focalLength.
109
+ * A world point in the camera's frame. The camera's `x`/`y` are its world
110
+ * position (origin at the viewport center), so a resting camera keeps
111
+ * world-x/y = screen-x/y at unit scale. `+z` in the result is in front of
112
+ * the camera: the camera looks down world -z, so a resting camera at
113
+ * z=focalLength sees the z=0 plane at view-z = focalLength.
126
114
  */
127
- export declare const toView: (camera: CameraView, p: Vec3, origin: Vec2) => Vec3;
115
+ export declare const toView: (camera: CameraView, p: Vec3) => Vec3;
128
116
  /**
129
- * Project a world point to screen. `origin` is the screen point the camera's
130
- * optical axis passes through — the viewport center — so pan/zoom read as
131
- * "into the middle of the shot", while a resting camera reproduces plain-2D
117
+ * Project a world point to screen. The camera's optical axis passes through
118
+ * screen (0, 0) — the viewport center — so pan/zoom read as "into the
119
+ * middle of the shot", while a resting camera reproduces plain-2D
132
120
  * placement. A point at or behind the camera (view-z <= 0) has no valid
133
121
  * projection; `depth` is still returned for sorting, `scale` clamps to 0.
134
122
  */
135
- export declare const project: (camera: CameraView, p: Vec3, origin: Vec2) => Projected;
136
- /**
137
- * The affine placement for a camera-facing billboard whose local origin is
138
- * `anchor`: translate the anchor's projected screen point and uniformly
139
- * scale by its perspective scale. Returned as SVG matrix components
140
- * `(a b c d e f)` — a and d carry the scale, e/f the translation — so a shape
141
- * authored in local space lands correctly without knowing about the camera.
142
- */
143
- export interface Affine {
144
- readonly a: number;
145
- readonly b: number;
146
- readonly c: number;
147
- readonly d: number;
148
- readonly e: number;
149
- readonly f: number;
150
- }
151
- export declare const billboardAffine: (proj: Projected, anchor: Vec2) => Affine;
152
- /**
153
- * Project the world-space corners of a tilted plane to a screen polygon.
154
- * The polygon is clipped against the near plane in view space
155
- * (Sutherland–Hodgman, winding preserved) before the per-vertex perspective
156
- * divide, so a plane crossing the camera renders its visible part instead
157
- * of folding — a quad straddling the plane yields up to 5 vertices; a plane
158
- * fully behind yields none (cull). Fully in front, this is plain per-corner
159
- * projection: a receding plane is a true perspective trapezoid.
160
- */
161
- export declare const projectPlane: (camera: CameraView, corners: ReadonlyArray<Vec3>, origin: Vec2) => Array<Vec2>;
162
- /** A projected segment: exact screen endpoints plus its single sort key. */
163
- export interface ProjectedSegment {
164
- readonly a: Vec2;
165
- readonly b: Vec2;
166
- /** midpoint view-space depth of the visible (clipped) segment */
167
- readonly depth: number;
168
- /** perspective scale at that midpoint (focalLength / depth) */
169
- readonly scale: number;
170
- }
171
- /**
172
- * Project a world-space segment (a skeletal shape's two endpoints) to
173
- * screen. Both endpoints go to view space, are clipped against the near
174
- * plane (lerp to z = NEAR — the 1D case of projectPlane's polygon clip),
175
- * and are projected individually, so a line spanning depth foreshortens
176
- * per endpoint. Returns `undefined` when the segment lies entirely behind
177
- * the near plane (cull). `depth`/`scale` come from the visible midpoint.
178
- */
179
- export declare const projectSegment: (camera: CameraView, a: Vec3, b: Vec3, origin: Vec2) => ProjectedSegment | undefined;
180
- /** A subpath of a skeletal path: world-space points, closed or open. */
181
- export interface Subpath3 {
182
- readonly points: ReadonlyArray<Vec3>;
183
- readonly closed: boolean;
184
- }
185
- /** A projected path: screen subpaths plus the path's single sort key. */
186
- export interface ProjectedPath {
187
- readonly subpaths: ReadonlyArray<{
188
- readonly points: ReadonlyArray<Vec2>;
189
- readonly closed: boolean;
190
- }>;
191
- /** mean view-space depth of the near-visible points */
192
- readonly depth: number;
193
- /** perspective scale at that depth (focalLength / depth) */
194
- readonly scale: number;
195
- }
196
- /**
197
- * Project a skeletal path's world-space subpaths to screen — the N-point
198
- * generalization of `projectSegment`. Every point goes to view space and is
199
- * projected individually, so a path spanning depth foreshortens per point.
200
- * Near-plane clipping is per subpath: a closed subpath clips as a polygon
201
- * (Sutherland–Hodgman, winding preserved — the tilted-plane treatment); an
202
- * open subpath clips per span (lerp to z = NEAR) and SPLITS into separate
203
- * visible pieces when interior points fall behind the plane. Returns
204
- * `undefined` when nothing survives (cull). `depth`/`scale` come from the
205
- * mean view depth of all emitted points — one key per path, the same
206
- * ceiling as a segment's midpoint.
207
- */
208
- export declare const projectPath: (camera: CameraView, subpaths: ReadonlyArray<Subpath3>, origin: Vec2) => ProjectedPath | undefined;
209
- /**
210
- * Clip a screen-space segment to a rectangle (Liang–Barsky). Returns the
211
- * clipped pair, or `undefined` when the segment lies entirely outside.
212
- * ThorVG's software rasterizer pays stroke cost proportional to a path's
213
- * full extent — offscreen included — so segments are clipped to the
214
- * viewport (plus a stroke margin) before painting; a near-camera line can
215
- * project tens of thousands of px wide otherwise (measured ~7× the cost
216
- * of its visible part).
217
- */
218
- export declare const clipSegmentToRect: (a: Vec2, b: Vec2, min: Vec2, max: Vec2) => readonly [Vec2, Vec2] | undefined;
123
+ export declare const project: (camera: CameraView, p: Vec3) => Projected;
219
124
  /**
220
125
  * The view-space depth of a world point — the painter's-sort key alone.
221
126
  * With no camera rotation this is `camera.z - p.z`; rotation tilts the
222
- * depth axis, so the full view transform is used. Origin only shifts x/y,
223
- * never depth, so a zero origin suffices.
127
+ * depth axis, so the full view transform is used.
224
128
  */
225
129
  export declare const depthOf: (camera: CameraView, p: Vec3) => number;