effect-motion 0.2.0 → 0.3.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/dist/Camera.d.ts +50 -28
- package/dist/Camera.js +71 -19
- package/dist/CameraHelpers.d.ts +70 -0
- package/dist/CameraHelpers.js +239 -0
- package/dist/CanvasExporter.d.ts +12 -0
- package/dist/CanvasExporter.js +40 -0
- package/dist/Color.d.ts +428 -0
- package/dist/Color.js +535 -0
- package/dist/EffectMotionError.d.ts +10 -0
- package/dist/EffectMotionError.js +6 -0
- package/dist/Entity.d.ts +9 -8
- package/dist/Entity.js +1 -4
- package/dist/Fonts.d.ts +10 -0
- package/dist/Fonts.js +16 -0
- package/dist/Images.d.ts +33 -0
- package/dist/Images.js +24 -0
- package/dist/Instance.d.ts +4 -4
- package/dist/Motion.d.ts +24 -7
- package/dist/Motion.js +54 -9
- package/dist/Phaser.d.ts +1 -1
- package/dist/Phaser.js +13 -9
- package/dist/Physics.d.ts +3 -3
- package/dist/Physics.js +4 -4
- package/dist/PngExporter.d.ts +6 -0
- package/dist/PngExporter.js +85 -0
- package/dist/Projection.d.ts +225 -0
- package/dist/Projection.js +473 -0
- package/dist/Renderer.d.ts +102 -53
- package/dist/Renderer.js +369 -78
- package/dist/Runner.d.ts +126 -42
- package/dist/Runner.js +42 -17
- package/dist/Scene.d.ts +88 -59
- package/dist/Scene.js +24 -15
- package/dist/Shapes.d.ts +11 -0
- package/dist/Shapes.js +11 -0
- package/dist/demo.d.ts +3 -186
- package/dist/demo.js +24 -35
- package/dist/index.d.ts +14 -13
- package/dist/index.js +14 -13
- package/dist/particles/Particle.d.ts +4 -3
- package/dist/particles/ParticleField.d.ts +11 -48
- package/dist/particles/ParticleField.js +5 -4
- package/dist/particles/constructors.d.ts +8 -7
- package/dist/particles/constructors.js +2 -2
- package/dist/particles/index.d.ts +5 -6
- package/dist/particles/index.js +5 -6
- package/dist/particles/overLife.d.ts +1 -1
- package/dist/particles/overLife.js +1 -1
- package/dist/particles/simulate.d.ts +2 -2
- package/dist/particles/simulate.js +6 -6
- package/dist/particles/step.d.ts +2 -2
- package/dist/particles/step.js +5 -3
- package/dist/render/dof.d.ts +27 -0
- package/dist/render/dof.js +37 -0
- package/dist/render/paint.d.ts +30 -0
- package/dist/render/paint.js +36 -0
- package/dist/render/shapes.d.ts +42 -0
- package/dist/render/shapes.js +310 -0
- package/dist/shapes/Circle.d.ts +10 -15
- package/dist/shapes/Circle.js +2 -2
- package/dist/shapes/Ellipse.d.ts +10 -16
- package/dist/shapes/Ellipse.js +2 -2
- package/dist/shapes/Group.d.ts +5 -39
- package/dist/shapes/Group.js +4 -4
- package/dist/shapes/Hud.d.ts +38 -0
- package/dist/shapes/Hud.js +35 -0
- package/dist/shapes/Image.d.ts +45 -0
- package/dist/shapes/Image.js +28 -0
- package/dist/shapes/Line.d.ts +12 -12
- package/dist/shapes/Line.js +11 -6
- package/dist/shapes/Path.d.ts +87 -18
- package/dist/shapes/Path.js +25 -6
- package/dist/shapes/Rect.d.ts +33 -23
- package/dist/shapes/Rect.js +14 -2
- package/dist/shapes/Shape2D.d.ts +13 -4
- package/dist/shapes/Shape2D.js +16 -5
- package/dist/shapes/Shapes.d.ts +11 -0
- package/dist/shapes/Shapes.js +11 -0
- package/dist/shapes/Square.d.ts +10 -15
- package/dist/shapes/Square.js +2 -2
- package/dist/shapes/Text.d.ts +10 -19
- package/dist/shapes/Text.js +2 -2
- package/package.json +14 -4
- package/dist/particles/render.d.ts +0 -13
- package/dist/particles/render.js +0 -33
- package/dist/shapes/Layer.d.ts +0 -9
- package/dist/shapes/Layer.js +0 -23
- package/dist/shapes/index.d.ts +0 -10
- package/dist/shapes/index.js +0 -10
- package/dist/svg/SvgDomRenderer.d.ts +0 -28
- package/dist/svg/SvgDomRenderer.js +0 -50
- package/dist/svg/SvgNode.d.ts +0 -13
- package/dist/svg/SvgNode.js +0 -18
- package/dist/svg/SvgRenderer.d.ts +0 -22
- package/dist/svg/SvgRenderer.js +0 -20
- package/dist/svg/camera.d.ts +0 -21
- package/dist/svg/camera.js +0 -43
- package/dist/svg/index.d.ts +0 -6
- package/dist/svg/index.js +0 -6
- package/dist/svg/layers.d.ts +0 -18
- package/dist/svg/layers.js +0 -13
- package/dist/svg/shapes.d.ts +0 -1089
- package/dist/svg/shapes.js +0 -119
package/dist/Images.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
/**
|
|
3
|
+
* Scene annotation key declaring the images a scene's Image entities depend
|
|
4
|
+
* on: `scene.annotate(Images.Images, [...])`. The runtime never reads it —
|
|
5
|
+
* decoded pixels cannot affect frame data. Consumers (the player, export
|
|
6
|
+
* tools) read it to load assets into their render session before playback.
|
|
7
|
+
*/
|
|
8
|
+
export const Images = Context.Reference("motion/Images", { defaultValue: () => [] });
|
|
9
|
+
/** A scene's declared images — empty for scenes never annotated. */
|
|
10
|
+
export const get = (scene) => Context.get(scene.annotations, Images);
|
|
11
|
+
/**
|
|
12
|
+
* A scene's declared images as a `name -> url` map for the render session's
|
|
13
|
+
* `images` option. Only entries with a `src.url` are included — loading is
|
|
14
|
+
* fetch-by-URL (no filesystem), so `path`-only entries are skipped.
|
|
15
|
+
*/
|
|
16
|
+
export const urlMap = (scene) => {
|
|
17
|
+
const out = {};
|
|
18
|
+
for (const image of get(scene)) {
|
|
19
|
+
if (image.src.url !== undefined) {
|
|
20
|
+
out[image.name] = image.src.url;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
};
|
package/dist/Instance.d.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
2
|
import * as Pipeable from "effect/Pipeable";
|
|
3
3
|
import type * as Schema from "effect/Schema";
|
|
4
|
-
import type * as Entity from "./Entity";
|
|
4
|
+
import type * as Entity from "./Entity.js";
|
|
5
5
|
export declare const TypeId: "~motion/Instance";
|
|
6
6
|
export interface Instance<Name extends string = string, Data extends Schema.Top = Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>> = {}> extends Pipeable.Pipeable {
|
|
7
7
|
readonly [TypeId]: typeof TypeId;
|
|
8
8
|
readonly id: string;
|
|
9
|
-
readonly entity: Entity.Entity<Name, Data, Traits
|
|
9
|
+
readonly entity: Entity.Entity<Name, Data, Traits>;
|
|
10
10
|
}
|
|
11
11
|
/** the Instance type of a given entity, traits included */
|
|
12
|
-
export type Of<E extends Entity.AnyEntity> = E extends Entity.Entity<infer Name, infer Data, infer Traits
|
|
12
|
+
export type Of<E extends Entity.AnyEntity> = E extends Entity.Entity<infer Name, infer Data, infer Traits> ? Instance<Name, Data, Traits> : never;
|
|
13
13
|
export declare const isInstance: (u: unknown) => u is Instance;
|
|
14
|
-
export declare const make: <Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]
|
|
14
|
+
export declare const make: <Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>>>(entity: Entity.Entity<Name, Data, Traits>, id: string) => Instance<Name, Data, Traits>;
|
|
15
15
|
export type InstanceOrEffect<Name extends string = string, Data extends Schema.Top = Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>> = {}, E = never, R = never> = Instance<Name, Data, Traits> | Effect.Effect<Instance<Name, Data, Traits>, E, R>;
|
|
16
16
|
export declare const flatten: <Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>>, E = never, R = never>(instance: InstanceOrEffect<Name, Data, Traits, E, R>) => Effect.Effect<Instance<Name, Data, Traits>, E, R>;
|
package/dist/Motion.d.ts
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import type * as Duration from "effect/Duration";
|
|
2
2
|
import * as Effect from "effect/Effect";
|
|
3
3
|
import type * as Schema from "effect/Schema";
|
|
4
|
-
import * as
|
|
5
|
-
import * as
|
|
6
|
-
import * as
|
|
7
|
-
import * as
|
|
8
|
-
|
|
4
|
+
import * as Color from "./Color.js";
|
|
5
|
+
import * as Entity from "./Entity.js";
|
|
6
|
+
import * as Instance from "./Instance.js";
|
|
7
|
+
import * as Runner from "./Runner.js";
|
|
8
|
+
import * as Timing from "./Timing.js";
|
|
9
|
+
export declare const color: (to: Color.Color, mode: Color.InterpolationMode) => (from: Color.Color, t: number) => Color.Color;
|
|
10
|
+
type ColorInterpolator = (from: Color.Color, t: number) => Color.Color;
|
|
11
|
+
export type InterpolableValue = number | Color.Color;
|
|
9
12
|
type InterpolableKeys<T> = {
|
|
10
|
-
[K in keyof T]
|
|
13
|
+
[K in keyof T]-?: NonNullable<T[K]> extends InterpolableValue ? K : never;
|
|
11
14
|
}[keyof T];
|
|
12
15
|
export type InterpolableOnly<T> = Pick<T, InterpolableKeys<T>>;
|
|
16
|
+
type InterpolableOrInterpolator<T> = {
|
|
17
|
+
[K in keyof T]-?: NonNullable<T[K]> extends number ? number : NonNullable<T[K]> extends Color.Color ? Color.Color | ColorInterpolator : never;
|
|
18
|
+
};
|
|
13
19
|
/** target props, or an updater computing them from the current data */
|
|
14
|
-
export type Target<Data extends Schema.Top> = Partial<
|
|
20
|
+
export type Target<Data extends Schema.Top> = Partial<InterpolableOrInterpolator<Data["Type"]>> | ((data: Data["Type"]) => Partial<InterpolableOrInterpolator<Data["Type"]>>);
|
|
15
21
|
export declare const resolveTarget: <Data extends Schema.Top>(target: Target<Data>, current: Data["Type"]) => Record<string, number>;
|
|
16
22
|
export declare const startValues: (current: unknown, target: Record<string, number>, explicitFrom: Record<string, number>) => Record<string, number>;
|
|
17
23
|
/**
|
|
@@ -23,6 +29,17 @@ export declare const startValues: (current: unknown, target: Record<string, numb
|
|
|
23
29
|
* instance, so animations chain.
|
|
24
30
|
*/
|
|
25
31
|
export declare const tweenTo: (<Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>>>(to: Target<Data>, duration: Duration.Input, timing?: Timing.TimingInput) => <E = never, R = never>(instance: Instance.InstanceOrEffect<Name, Data, Traits, E, R>) => Effect.Effect<Instance.Instance<Name, Data, Traits>, E, R | Runner.Runner>) & (<Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>>, E = never, R = never>(instance: Instance.InstanceOrEffect<Name, Data, Traits, E, R>, to: Target<Data>, duration: Duration.Input, timing?: Timing.TimingInput) => Effect.Effect<Instance.Instance<Name, Data, Traits>, E, R | Runner.Runner>);
|
|
32
|
+
/**
|
|
33
|
+
* Parametric animator: each frame applies `fn` with the eased parameter
|
|
34
|
+
* and the current data, then ticks. The final frame receives exactly
|
|
35
|
+
* `t = 1` for any timing with f(1) = 1; a zero-length duration still
|
|
36
|
+
* takes one frame. This is the primitive under coordinated multi-field
|
|
37
|
+
* motion — arcs, orbits, counters — where independent per-field tweens
|
|
38
|
+
* cannot express the coupling. Determinism: `fn` sees only `(t, data)`.
|
|
39
|
+
* Dual: `drive(instance, duration, timing, fn)` or
|
|
40
|
+
* `instance.pipe(drive(duration, timing, fn))`.
|
|
41
|
+
*/
|
|
42
|
+
export declare const drive: (<Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>>>(duration: Duration.Input, timing: Timing.TimingInput, fn: (t: number, data: Data["Type"]) => Data["Type"]) => <E = never, R = never>(instance: Instance.InstanceOrEffect<Name, Data, Traits, E, R>) => Effect.Effect<Instance.Instance<Name, Data, Traits>, E, R | Runner.Runner>) & (<Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>>, E = never, R = never>(instance: Instance.InstanceOrEffect<Name, Data, Traits, E, R>, duration: Duration.Input, timing: Timing.TimingInput, fn: (t: number, data: Data["Type"]) => Data["Type"]) => Effect.Effect<Instance.Instance<Name, Data, Traits>, E, R | Runner.Runner>);
|
|
26
43
|
/**
|
|
27
44
|
* Like `tweenTo`, but with an explicit start: interpolates the keys of
|
|
28
45
|
* `to` from `from` (keys missing in `from` start at the current data).
|
package/dist/Motion.js
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
2
|
import * as Effectable from "effect/Effectable";
|
|
3
3
|
import { dual } from "effect/Function";
|
|
4
|
-
import * as
|
|
5
|
-
import * as
|
|
6
|
-
import * as
|
|
7
|
-
import * as
|
|
8
|
-
import * as
|
|
9
|
-
import * as
|
|
4
|
+
import * as Color from "./Color.js";
|
|
5
|
+
import * as Entity from "./Entity.js";
|
|
6
|
+
import * as Instance from "./Instance.js";
|
|
7
|
+
import * as Runner from "./Runner.js";
|
|
8
|
+
import * as Scene from "./Scene.js";
|
|
9
|
+
import * as Time from "./Time.js";
|
|
10
|
+
import * as Timing from "./Timing.js";
|
|
11
|
+
export const color = (to, mode) => {
|
|
12
|
+
return (from, t) => Color.mix(from, to, t, mode);
|
|
13
|
+
};
|
|
10
14
|
// extrapolating on purpose: eased t goes outside [0, 1] for back/elastic
|
|
11
15
|
const lerpNumber = (from, to, t) => from + (to - from) * t;
|
|
16
|
+
const lerpColor = (from, to, t, mode) => {
|
|
17
|
+
return Color.mix(from, to, t, mode);
|
|
18
|
+
};
|
|
12
19
|
/**
|
|
13
20
|
* The interpolation engine: from `from` to `to` over `duration`, calling
|
|
14
21
|
* `fn` with the eased value once per frame (each step ends in a
|
|
@@ -25,7 +32,18 @@ const interpolate = Effect.fnUntraced(function* (from, to, duration, fn, timing
|
|
|
25
32
|
const t = timingFn(i / frames);
|
|
26
33
|
const value = {};
|
|
27
34
|
for (const key of keys) {
|
|
28
|
-
|
|
35
|
+
const fromValue = from[key];
|
|
36
|
+
const toValue = to[key];
|
|
37
|
+
if (typeof toValue === "number") {
|
|
38
|
+
value[key] = lerpNumber(from[key], to[key], t);
|
|
39
|
+
}
|
|
40
|
+
else if (typeof toValue === "function") {
|
|
41
|
+
const interpolator = toValue;
|
|
42
|
+
value[key] = interpolator(fromValue, t);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
value[key] = lerpColor(fromValue, toValue, t, "lab");
|
|
46
|
+
}
|
|
29
47
|
}
|
|
30
48
|
yield* fn(value);
|
|
31
49
|
yield* Scene.tick;
|
|
@@ -42,8 +60,13 @@ export const resolveTarget = (target, current) => (typeof target === "function"
|
|
|
42
60
|
export const startValues = (current, target, explicitFrom) => {
|
|
43
61
|
const start = {};
|
|
44
62
|
for (const key of Object.keys(target)) {
|
|
45
|
-
|
|
46
|
-
|
|
63
|
+
const from = explicitFrom[key] ?? current[key];
|
|
64
|
+
// an optional field that was never set has no start value — lerping
|
|
65
|
+
// from undefined would silently produce NaN frames; die loudly instead
|
|
66
|
+
if (from === undefined) {
|
|
67
|
+
throw new Error(`tween: "${key}" has no current value to start from — pass an explicit from`);
|
|
68
|
+
}
|
|
69
|
+
start[key] = from;
|
|
47
70
|
}
|
|
48
71
|
return start;
|
|
49
72
|
};
|
|
@@ -69,6 +92,28 @@ const firstArgIsInstance = (args) => Instance.isInstance(args[0]);
|
|
|
69
92
|
* instance, so animations chain.
|
|
70
93
|
*/
|
|
71
94
|
export const tweenTo = dual(firstArgIsInstance, (instance, to, duration, timing) => animate(instance, undefined, to, duration, timing));
|
|
95
|
+
/**
|
|
96
|
+
* Parametric animator: each frame applies `fn` with the eased parameter
|
|
97
|
+
* and the current data, then ticks. The final frame receives exactly
|
|
98
|
+
* `t = 1` for any timing with f(1) = 1; a zero-length duration still
|
|
99
|
+
* takes one frame. This is the primitive under coordinated multi-field
|
|
100
|
+
* motion — arcs, orbits, counters — where independent per-field tweens
|
|
101
|
+
* cannot express the coupling. Determinism: `fn` sees only `(t, data)`.
|
|
102
|
+
* Dual: `drive(instance, duration, timing, fn)` or
|
|
103
|
+
* `instance.pipe(drive(duration, timing, fn))`.
|
|
104
|
+
*/
|
|
105
|
+
export const drive = dual(firstArgIsInstance, Effect.fnUntraced(function* (instanceOrEffect, duration, timing, fn) {
|
|
106
|
+
const instance = yield* Instance.flatten(instanceOrEffect);
|
|
107
|
+
const runner = yield* Runner.Runner;
|
|
108
|
+
const timingFn = Timing.resolve(timing);
|
|
109
|
+
const frames = Math.max(1, Time.toFrames(duration, runner.settings.frameRate));
|
|
110
|
+
for (let i = 1; i <= frames; i++) {
|
|
111
|
+
const t = timingFn(i / frames);
|
|
112
|
+
yield* Scene.update(instance, (data) => fn(t, data));
|
|
113
|
+
yield* Scene.tick;
|
|
114
|
+
}
|
|
115
|
+
return instance;
|
|
116
|
+
}));
|
|
72
117
|
/**
|
|
73
118
|
* Like `tweenTo`, but with an explicit start: interpolates the keys of
|
|
74
119
|
* `to` from `from` (keys missing in `from` start at the current data).
|
package/dist/Phaser.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export declare class Phaser extends Phaser_base {
|
|
|
44
44
|
* during a sequential handoff or before the scene starts. The slot is
|
|
45
45
|
* released by a finalizer on success, failure, and interrupt alike.
|
|
46
46
|
*/
|
|
47
|
-
export declare const run: <A, E, R>(phaser: Phaser["Service"], scene: Effect.Effect<A, E, R>) => Effect.Effect<import("effect/Fiber").Fiber<A, E>, never, Exclude<R, Phaser>>;
|
|
47
|
+
export declare const run: <A, E = never, R = never>(phaser: Phaser["Service"], scene: Effect.Effect<A, E, R>) => Effect.Effect<import("effect/Fiber").Fiber<A, E>, never, Exclude<R, Phaser>>;
|
|
48
48
|
/**
|
|
49
49
|
* Run `effect`, then arrive at the phase boundary.
|
|
50
50
|
*
|
package/dist/Phaser.js
CHANGED
|
@@ -14,7 +14,7 @@ export class Phaser extends Context.Service()("motion/Phaser", {
|
|
|
14
14
|
let arrived = 0;
|
|
15
15
|
let phaseLatch = Latch.makeUnsafe();
|
|
16
16
|
let state = "idle";
|
|
17
|
-
|
|
17
|
+
const waiters = new Set();
|
|
18
18
|
// The single invariant. Every event that touches `arrived` or
|
|
19
19
|
// `parties` re-runs this.
|
|
20
20
|
const checkQuiescence = () => {
|
|
@@ -40,10 +40,12 @@ export class Phaser extends Context.Service()("motion/Phaser", {
|
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
42
|
case "running": {
|
|
43
|
-
const resume = waiter;
|
|
44
|
-
waiter = null;
|
|
45
43
|
state = "idle";
|
|
46
|
-
|
|
44
|
+
const copy = new Set(waiters);
|
|
45
|
+
waiters.clear();
|
|
46
|
+
for (const waiter of copy) {
|
|
47
|
+
waiter();
|
|
48
|
+
}
|
|
47
49
|
return;
|
|
48
50
|
}
|
|
49
51
|
}
|
|
@@ -69,11 +71,13 @@ export class Phaser extends Context.Service()("motion/Phaser", {
|
|
|
69
71
|
})));
|
|
70
72
|
}));
|
|
71
73
|
const awaitAdvance = Effect.callback((resume) => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
const waiter = () => resume(Effect.succeed(phase));
|
|
75
|
+
waiters.add(waiter);
|
|
76
|
+
if (waiters.size > 1) {
|
|
77
|
+
return Effect.sync(() => {
|
|
78
|
+
waiters.delete(waiter);
|
|
79
|
+
});
|
|
75
80
|
}
|
|
76
|
-
waiter = () => resume(Effect.succeed(phase));
|
|
77
81
|
if (arrived === parties) {
|
|
78
82
|
// quiescent: arm an advance so parties run the next phase
|
|
79
83
|
state = "pending";
|
|
@@ -87,7 +91,7 @@ export class Phaser extends Context.Service()("motion/Phaser", {
|
|
|
87
91
|
}
|
|
88
92
|
return Effect.sync(() => {
|
|
89
93
|
// interrupted while suspended
|
|
90
|
-
waiter
|
|
94
|
+
waiters.delete(waiter);
|
|
91
95
|
state = "idle";
|
|
92
96
|
});
|
|
93
97
|
});
|
package/dist/Physics.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
2
|
import type * as Schema from "effect/Schema";
|
|
3
|
-
import * as Entity from "./Entity";
|
|
4
|
-
import * as Instance from "./Instance";
|
|
5
|
-
import * as Runner from "./Runner";
|
|
3
|
+
import * as Entity from "./Entity.js";
|
|
4
|
+
import * as Instance from "./Instance.js";
|
|
5
|
+
import * as Runner from "./Runner.js";
|
|
6
6
|
/**
|
|
7
7
|
* Physics-based motion: durationless animations that carry momentum and
|
|
8
8
|
* end on their own. A spring is a damped harmonic oscillator simulated
|
package/dist/Physics.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
2
|
import { dual } from "effect/Function";
|
|
3
|
-
import * as Entity from "./Entity";
|
|
4
|
-
import * as Instance from "./Instance";
|
|
5
|
-
import * as Runner from "./Runner";
|
|
6
|
-
import * as Scene from "./Scene";
|
|
3
|
+
import * as Entity from "./Entity.js";
|
|
4
|
+
import * as Instance from "./Instance.js";
|
|
5
|
+
import * as Runner from "./Runner.js";
|
|
6
|
+
import * as Scene from "./Scene.js";
|
|
7
7
|
/** fast, gently damped general-purpose spring */
|
|
8
8
|
export const defaultSpring = {
|
|
9
9
|
mass: 0.05,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import { FileSystem } from "effect/FileSystem";
|
|
3
|
+
import { EffectMotionError } from "./EffectMotionError.js";
|
|
4
|
+
import type * as Renderer from "./Renderer.js";
|
|
5
|
+
export declare const toBuffer: (fb: Renderer.Framebuffer) => Effect.Effect<Uint8Array<ArrayBuffer>, EffectMotionError, never>;
|
|
6
|
+
export declare const toFile: (fb: Renderer.Framebuffer, outPath: string) => Effect.Effect<void, EffectMotionError | import("effect/PlatformError").PlatformError, FileSystem>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { deflateSync } from "node:zlib";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import { FileSystem } from "effect/FileSystem";
|
|
4
|
+
import { EffectMotionError } from "./EffectMotionError.js";
|
|
5
|
+
const SIGNATURE = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
6
|
+
const crcTable = (() => {
|
|
7
|
+
const t = new Uint32Array(256);
|
|
8
|
+
for (let n = 0; n < 256; n++) {
|
|
9
|
+
let c = n;
|
|
10
|
+
for (let k = 0; k < 8; k++) {
|
|
11
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
12
|
+
}
|
|
13
|
+
t[n] = c >>> 0;
|
|
14
|
+
}
|
|
15
|
+
return t;
|
|
16
|
+
})();
|
|
17
|
+
const crc32 = (bytes) => {
|
|
18
|
+
let c = 0xffffffff;
|
|
19
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
20
|
+
c = crcTable[(c ^ bytes[i]) & 0xff] ^ (c >>> 8);
|
|
21
|
+
}
|
|
22
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
23
|
+
};
|
|
24
|
+
const chunk = (type, data) => {
|
|
25
|
+
const typeBytes = new Uint8Array([
|
|
26
|
+
type.charCodeAt(0),
|
|
27
|
+
type.charCodeAt(1),
|
|
28
|
+
type.charCodeAt(2),
|
|
29
|
+
type.charCodeAt(3),
|
|
30
|
+
]);
|
|
31
|
+
const body = new Uint8Array(typeBytes.length + data.length);
|
|
32
|
+
body.set(typeBytes, 0);
|
|
33
|
+
body.set(data, typeBytes.length);
|
|
34
|
+
const out = new Uint8Array(4 + body.length + 4);
|
|
35
|
+
const view = new DataView(out.buffer);
|
|
36
|
+
view.setUint32(0, data.length, false);
|
|
37
|
+
out.set(body, 4);
|
|
38
|
+
view.setUint32(4 + body.length, crc32(body), false);
|
|
39
|
+
return out;
|
|
40
|
+
};
|
|
41
|
+
/** Encode a raw RGBA8888 buffer (`width * height * 4` bytes) as a PNG. */
|
|
42
|
+
const encodePng = Effect.fnUntraced(function* (rgba, width, height) {
|
|
43
|
+
if (rgba.length !== width * height * 4) {
|
|
44
|
+
return yield* Effect.fail(EffectMotionError.of(`Could not encode PNG: buffer is ${rgba.length} bytes, expected ${width * height * 4} (${width}x${height} RGBA)`));
|
|
45
|
+
}
|
|
46
|
+
// prepend a filter byte (0 = none) to each scanline
|
|
47
|
+
const stride = width * 4;
|
|
48
|
+
const raw = new Uint8Array((stride + 1) * height);
|
|
49
|
+
for (let y = 0; y < height; y++) {
|
|
50
|
+
raw[y * (stride + 1)] = 0;
|
|
51
|
+
raw.set(rgba.subarray(y * stride, y * stride + stride), y * (stride + 1) + 1);
|
|
52
|
+
}
|
|
53
|
+
const ihdr = new Uint8Array(13);
|
|
54
|
+
const ihdrView = new DataView(ihdr.buffer);
|
|
55
|
+
ihdrView.setUint32(0, width, false);
|
|
56
|
+
ihdrView.setUint32(4, height, false);
|
|
57
|
+
ihdr[8] = 8; // bit depth
|
|
58
|
+
ihdr[9] = 6; // color type: RGBA
|
|
59
|
+
// [10] compression, [11] filter, [12] interlace all 0
|
|
60
|
+
const idat = new Uint8Array(deflateSync(raw));
|
|
61
|
+
const chunks = [
|
|
62
|
+
SIGNATURE,
|
|
63
|
+
chunk("IHDR", ihdr),
|
|
64
|
+
chunk("IDAT", idat),
|
|
65
|
+
chunk("IEND", new Uint8Array(0)),
|
|
66
|
+
];
|
|
67
|
+
const total = chunks.reduce((n, c) => n + c.length, 0);
|
|
68
|
+
const png = new Uint8Array(total);
|
|
69
|
+
let offset = 0;
|
|
70
|
+
for (const c of chunks) {
|
|
71
|
+
png.set(c, offset);
|
|
72
|
+
offset += c.length;
|
|
73
|
+
}
|
|
74
|
+
return png;
|
|
75
|
+
});
|
|
76
|
+
export const toBuffer = (fb) => {
|
|
77
|
+
return encodePng(fb.rgba, fb.width, fb.height);
|
|
78
|
+
};
|
|
79
|
+
export const toFile = (fb, outPath) => {
|
|
80
|
+
return Effect.gen(function* () {
|
|
81
|
+
const fs = yield* FileSystem;
|
|
82
|
+
const png = yield* toBuffer(fb);
|
|
83
|
+
yield* fs.writeFile(outPath, png);
|
|
84
|
+
});
|
|
85
|
+
};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Perspective projection — pure math shared by every sink, usable without
|
|
3
|
+
* the Effect runtime. No wall-clock, no RNG: projecting the same camera and
|
|
4
|
+
* point twice is bit-for-bit identical, which is what keeps 2.5D scenes
|
|
5
|
+
* deterministic.
|
|
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.
|
|
12
|
+
*
|
|
13
|
+
* Identity invariant: the default camera (see `defaultFocalLength` and
|
|
14
|
+
* `defaultCameraZ`) projects a world point at `z = 0` to screen `(x, y)` at
|
|
15
|
+
* scale 1 — so a scene that never touches depth renders exactly as the old
|
|
16
|
+
* plain-2D pipeline did. That falls out of placing the resting camera a
|
|
17
|
+
* focal-length back on +z and dividing by focal length at the z=0 plane —
|
|
18
|
+
* and holds for ANY focal length, which is why the default can be
|
|
19
|
+
* width-relative without breaking plain-2D scenes.
|
|
20
|
+
*/
|
|
21
|
+
export interface Vec3 {
|
|
22
|
+
readonly x: number;
|
|
23
|
+
readonly y: number;
|
|
24
|
+
readonly z: number;
|
|
25
|
+
}
|
|
26
|
+
export interface Vec2 {
|
|
27
|
+
readonly x: number;
|
|
28
|
+
readonly y: number;
|
|
29
|
+
}
|
|
30
|
+
/** The camera view, as it arrives on the frame from the runner. */
|
|
31
|
+
export interface CameraView {
|
|
32
|
+
readonly x: number;
|
|
33
|
+
readonly y: number;
|
|
34
|
+
readonly z: number;
|
|
35
|
+
readonly rotX: number;
|
|
36
|
+
readonly rotY: number;
|
|
37
|
+
readonly rotZ: number;
|
|
38
|
+
readonly focalLength: number;
|
|
39
|
+
/**
|
|
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.
|
|
42
|
+
*/
|
|
43
|
+
readonly focusDistance: number;
|
|
44
|
+
/** depth-of-field blur strength; 0 (the default) = pinhole, DoF off */
|
|
45
|
+
readonly aperture: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A projected point: screen position, the view-space depth used as the
|
|
49
|
+
* painter's-sort key (larger = farther from the camera), and the uniform
|
|
50
|
+
* `scale` a billboard at this point receives (focalLength / depth).
|
|
51
|
+
*/
|
|
52
|
+
export interface Projected {
|
|
53
|
+
readonly x: number;
|
|
54
|
+
readonly y: number;
|
|
55
|
+
readonly depth: number;
|
|
56
|
+
readonly scale: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Default focal length (px) for a comp of the given width — After Effects'
|
|
60
|
+
* default lens: 50mm on 36mm-wide film, so `zoom = width × 50/36`. Width-
|
|
61
|
+
* relative so perspective strength (how much a given z moves/scales a shape)
|
|
62
|
+
* reads the same at every output resolution.
|
|
63
|
+
*/
|
|
64
|
+
export declare const defaultFocalLength: (width: number) => number;
|
|
65
|
+
/**
|
|
66
|
+
* The resting camera sits this far back on +z, looking toward -z, so that
|
|
67
|
+
* the `z = 0` plane is exactly `focalLength` in front of it — the identity
|
|
68
|
+
* invariant. A camera authored without a `z` uses this.
|
|
69
|
+
*/
|
|
70
|
+
export declare const defaultCameraZ: (focalLength: number) => number;
|
|
71
|
+
/** Optional point of interest carried beside a camera view (world coords). */
|
|
72
|
+
export interface PointOfInterest {
|
|
73
|
+
readonly poiX?: number;
|
|
74
|
+
readonly poiY?: number;
|
|
75
|
+
readonly poiZ?: number;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 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
|
|
81
|
+
* (in-front is +z), which inverts rotation handedness vs. world space;
|
|
82
|
+
* that subtlety is handled here, exactly once, pinned by tests that
|
|
83
|
+
* project the POI and assert it lands on the viewport center.
|
|
84
|
+
*/
|
|
85
|
+
export declare const lookAtOrientation: (position: Vec3, poi: Vec3) => {
|
|
86
|
+
rotX: number;
|
|
87
|
+
rotY: number;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* The effective camera view: when a point of interest is set, auto-orient
|
|
91
|
+
* toward it with the explicit Euler composing AFTER the aim — the After
|
|
92
|
+
* Effects two-node rule. "After" means the user's rotation applies in the
|
|
93
|
+
* camera's own frame (so a lone `rotZ` rolls about the view axis and the
|
|
94
|
+
* POI stays centered); the exact composed rotation is extracted back to
|
|
95
|
+
* 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.
|
|
109
|
+
*/
|
|
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];
|
|
120
|
+
/**
|
|
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.
|
|
126
|
+
*/
|
|
127
|
+
export declare const toView: (camera: CameraView, p: Vec3, origin: Vec2) => Vec3;
|
|
128
|
+
/**
|
|
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
|
|
132
|
+
* placement. A point at or behind the camera (view-z <= 0) has no valid
|
|
133
|
+
* projection; `depth` is still returned for sorting, `scale` clamps to 0.
|
|
134
|
+
*/
|
|
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;
|
|
219
|
+
/**
|
|
220
|
+
* The view-space depth of a world point — the painter's-sort key alone.
|
|
221
|
+
* 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.
|
|
224
|
+
*/
|
|
225
|
+
export declare const depthOf: (camera: CameraView, p: Vec3) => number;
|