effect-motion 0.3.2 → 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 +653 -330
- package/dist/Runner.js +208 -158
- package/dist/Scene.d.ts +621 -171
- package/dist/Scene.js +620 -89
- 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 +13 -5
- 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 -381
- 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 -65
- 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 -116
- package/dist/shapes/Group.js +0 -82
- 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/index.js
CHANGED
|
@@ -1,14 +1,65 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* effect-motion — deterministic, frame-exact motion graphics in code.
|
|
3
|
+
*
|
|
4
|
+
* @remarks
|
|
5
|
+
* Scenes are pure descriptions of animation: the same scene produces the
|
|
6
|
+
* same frames on every run and every machine, because time is counted in
|
|
7
|
+
* frames rather than read from a clock and randomness comes from a seed.
|
|
8
|
+
*
|
|
9
|
+
* Where to start:
|
|
10
|
+
*
|
|
11
|
+
* - `Scene` — declare a scene, create entities, compose animations, run it.
|
|
12
|
+
* - `Motion` — animate over a duration, with easing.
|
|
13
|
+
* - `Physics` — animate with springs, whose length emerges from the sim.
|
|
14
|
+
* - `Entity` — the shapes, and `vec3` for positions.
|
|
15
|
+
* - `Color`, `Timing` — the palette and the easing curves.
|
|
16
|
+
* - `Camera` — aiming, orbiting, and dollying the viewpoint.
|
|
17
|
+
*
|
|
18
|
+
* Prefer deep per-actor imports over this barrel:
|
|
19
|
+
*
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import * as Motion from "effect-motion/Motion";
|
|
22
|
+
* import * as Scene from "effect-motion/Scene";
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* A complete scene.
|
|
27
|
+
* ```typescript
|
|
28
|
+
* import * as Color from "effect-motion/Color";
|
|
29
|
+
* import * as Motion from "effect-motion/Motion";
|
|
30
|
+
* import * as Scene from "effect-motion/Scene";
|
|
31
|
+
*
|
|
32
|
+
* export const scene = Scene.make(
|
|
33
|
+
* function* () {
|
|
34
|
+
* const dot = yield* Scene.instantiate("Circle", {
|
|
35
|
+
* radius: 24,
|
|
36
|
+
* fillColor: Color.hex("#7f5af0"),
|
|
37
|
+
* });
|
|
38
|
+
* yield* dot.pipe(
|
|
39
|
+
* Motion.moveTo({ x: 430 }, "1 second", "easeInOutCubic"),
|
|
40
|
+
* Motion.fadeTo(0, "400 millis"),
|
|
41
|
+
* );
|
|
42
|
+
* },
|
|
43
|
+
* { width: 500, height: 300, backgroundColor: Color.rgba(22, 22, 29) },
|
|
44
|
+
* );
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @packageDocumentation
|
|
48
|
+
*/
|
|
49
|
+
export * as Camera from "./Camera.js";
|
|
2
50
|
export * as Color from "./Color.js";
|
|
51
|
+
export { EffectMotionError } from "./EffectMotionError.js";
|
|
52
|
+
// the closed entity world: the union and its tags
|
|
3
53
|
export * as Entity from "./Entity.js";
|
|
4
|
-
export * as
|
|
5
|
-
export * as
|
|
54
|
+
export * as Font from "./Font.js";
|
|
55
|
+
export * as Image from "./Image.js";
|
|
56
|
+
// a reference to a live entity in the runner tree
|
|
6
57
|
export * as Instance from "./Instance.js";
|
|
7
58
|
export * as Motion from "./Motion.js";
|
|
8
59
|
export * as Phaser from "./Phaser.js";
|
|
9
60
|
export * as Physics from "./Physics.js";
|
|
10
61
|
export * as Particles from "./particles/index.js";
|
|
11
|
-
export * as
|
|
62
|
+
export * as Resource from "./Resource.js";
|
|
63
|
+
export * as Runner from "./Runner.js";
|
|
12
64
|
export * as Scene from "./Scene.js";
|
|
13
|
-
export * as Shapes from "./Shapes.js";
|
|
14
65
|
export * as Timing from "./Timing.js";
|
|
@@ -57,7 +57,7 @@ export interface EmitterConfig {
|
|
|
57
57
|
readonly y: number;
|
|
58
58
|
/** launch speed range (px/sec) */
|
|
59
59
|
readonly speed: Range;
|
|
60
|
-
/** launch angle range (degrees; 0 = up,
|
|
60
|
+
/** launch angle range (degrees; 0 = up, counterclockwise-positive) */
|
|
61
61
|
readonly angle: Range;
|
|
62
62
|
/** lifetime range (seconds) */
|
|
63
63
|
readonly life: Range;
|
|
@@ -66,7 +66,7 @@ export interface EmitterConfig {
|
|
|
66
66
|
/** birth opacity range (0..1), drawn per particle; the over-life opacity
|
|
67
67
|
* curve (if any) multiplies this baseline. Omit → every particle 1. */
|
|
68
68
|
readonly opacity?: Range;
|
|
69
|
-
/** shared downward acceleration (px/sec²); not ranged */
|
|
69
|
+
/** shared screen-downward (-y) acceleration (px/sec²); not ranged */
|
|
70
70
|
readonly gravity: number;
|
|
71
71
|
/** colors drawn from uniformly at birth */
|
|
72
72
|
readonly palette: ReadonlyArray<Color.Color>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as Schema from "effect/Schema";
|
|
2
2
|
import * as Color from "../Color.js";
|
|
3
|
-
import * as
|
|
4
|
-
export declare const ParticleField:
|
|
3
|
+
import * as Legacy from "./legacy.js";
|
|
4
|
+
export declare const ParticleField: Legacy.Entity<"particles/ParticleField", {
|
|
5
5
|
x: Schema.withConstructorDefault<Schema.Number>;
|
|
6
6
|
y: Schema.withConstructorDefault<Schema.Number>;
|
|
7
7
|
z: Schema.withConstructorDefault<Schema.Number>;
|
|
@@ -43,8 +43,9 @@ export declare const ParticleField: Entity.Entity<"particles/ParticleField", Sch
|
|
|
43
43
|
readonly alive: Schema.Boolean;
|
|
44
44
|
readonly wrap: Schema.Boolean;
|
|
45
45
|
}>>>;
|
|
46
|
-
}
|
|
47
|
-
readonly "~position":
|
|
46
|
+
}, {
|
|
47
|
+
readonly "~position": Legacy.TraitLens<{
|
|
48
|
+
readonly "~visible": boolean;
|
|
48
49
|
readonly x: number;
|
|
49
50
|
readonly y: number;
|
|
50
51
|
readonly z: number;
|
|
@@ -86,8 +87,9 @@ export declare const ParticleField: Entity.Entity<"particles/ParticleField", Sch
|
|
|
86
87
|
readonly alive: Schema.Boolean;
|
|
87
88
|
readonly wrap: Schema.Boolean;
|
|
88
89
|
}, "Type">[];
|
|
89
|
-
},
|
|
90
|
-
readonly "~opacity":
|
|
90
|
+
}, Legacy.Position>;
|
|
91
|
+
readonly "~opacity": Legacy.TraitLens<{
|
|
92
|
+
readonly "~visible": boolean;
|
|
91
93
|
readonly x: number;
|
|
92
94
|
readonly y: number;
|
|
93
95
|
readonly z: number;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import * as Effect from "effect/Effect";
|
|
2
2
|
import * as Schema from "effect/Schema";
|
|
3
3
|
import * as Color from "../Color.js";
|
|
4
|
-
import * as
|
|
5
|
-
import * as Shape2D from "../shapes/Shape2D.js";
|
|
4
|
+
import * as Legacy from "./legacy.js";
|
|
6
5
|
/**
|
|
7
6
|
* A ParticleField is ONE entity/instance backing many particles. Its data
|
|
8
7
|
* holds the emitter config plus a fixed-capacity buffer of live particle
|
|
@@ -37,11 +36,11 @@ const ParticleSchema = Schema.Struct({
|
|
|
37
36
|
alive: Schema.Boolean,
|
|
38
37
|
wrap: Schema.Boolean,
|
|
39
38
|
});
|
|
40
|
-
export const ParticleField =
|
|
39
|
+
export const ParticleField = Legacy.make("particles/ParticleField", {
|
|
41
40
|
// emitter origin doubles as the field's position (x/y), so the field
|
|
42
41
|
// participates in the position trait like any shape
|
|
43
|
-
...
|
|
44
|
-
...
|
|
42
|
+
...Legacy.position,
|
|
43
|
+
...Legacy.opacity,
|
|
45
44
|
// emitter config. speed/angle/life are emitter-mode props; the typed
|
|
46
45
|
// `emitter()`/`field()` constructors gate which an author may set, but
|
|
47
46
|
// the underlying schema defaults them so ONE struct serves both modes.
|
|
@@ -53,7 +52,7 @@ export const ParticleField = Entity.make("particles/ParticleField", {
|
|
|
53
52
|
// opacity curve multiplies it. Named distinctly from the field's own
|
|
54
53
|
// `opacity` (the ~opacity trait above, which fades the whole field).
|
|
55
54
|
opacityRange: defaultedRange([1, 1]),
|
|
56
|
-
gravity:
|
|
55
|
+
gravity: Legacy.defaultedNumber(0),
|
|
57
56
|
palette: Schema.Array(Color.Color).pipe(Schema.withConstructorDefault(Effect.sync(() => [Color.white]))),
|
|
58
57
|
// fill mode: the region particles spread across and wrap within
|
|
59
58
|
// (defaults to the frame in `simulate`) and their drift speed range
|
|
@@ -62,9 +61,9 @@ export const ParticleField = Entity.make("particles/ParticleField", {
|
|
|
62
61
|
sizeOverLife: Schema.optionalKey(OverLifeSchema),
|
|
63
62
|
opacityOverLife: Schema.optionalKey(OverLifeSchema),
|
|
64
63
|
// lifecycle state
|
|
65
|
-
capacity:
|
|
64
|
+
capacity: Legacy.defaultedNumber(1024),
|
|
66
65
|
buffer: Schema.Array(ParticleSchema).pipe(Schema.withConstructorDefault(Effect.sync(() => []))),
|
|
67
66
|
}, {
|
|
68
|
-
"~position":
|
|
69
|
-
"~opacity":
|
|
67
|
+
"~position": Legacy.positionLens(),
|
|
68
|
+
"~opacity": Legacy.opacityLens(),
|
|
70
69
|
});
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
2
|
import type * as Color from "../Color.js";
|
|
3
3
|
import type * as Instance from "../Instance.js";
|
|
4
|
-
import
|
|
4
|
+
import * as Runner from "../Runner.js";
|
|
5
5
|
import type { Range } from "./Particle.js";
|
|
6
|
-
import { ParticleField } from "./ParticleField.js";
|
|
7
6
|
/**
|
|
8
7
|
* Two typed front doors onto the SAME `ParticleField` entity. The field's
|
|
9
8
|
* emission model is chosen at `simulate`, but the properties that make sense
|
|
@@ -15,7 +14,7 @@ import { ParticleField } from "./ParticleField.js";
|
|
|
15
14
|
*/
|
|
16
15
|
declare const EmitterBrand: unique symbol;
|
|
17
16
|
declare const FloatBrand: unique symbol;
|
|
18
|
-
type BaseField = Instance.
|
|
17
|
+
type BaseField = Instance.Instance;
|
|
19
18
|
/** an emitter (source) field — simulate with `{ burst }` or `{ rate }` */
|
|
20
19
|
export type EmitterField = BaseField & {
|
|
21
20
|
readonly [EmitterBrand]: true;
|
|
@@ -40,7 +39,7 @@ interface CommonInput {
|
|
|
40
39
|
readonly opacityRange?: Range;
|
|
41
40
|
/** colors drawn from uniformly at birth */
|
|
42
41
|
readonly palette?: ReadonlyArray<Color.Color>;
|
|
43
|
-
/** shared downward acceleration (px/sec²) */
|
|
42
|
+
/** shared screen-downward (-y) acceleration (px/sec²) */
|
|
44
43
|
readonly gravity?: number;
|
|
45
44
|
readonly sizeOverLife?: OverLifeInput;
|
|
46
45
|
readonly opacityOverLife?: OverLifeInput;
|
|
@@ -51,7 +50,7 @@ interface CommonInput {
|
|
|
51
50
|
export interface EmitterInput extends CommonInput {
|
|
52
51
|
/** launch speed range (px/sec) */
|
|
53
52
|
readonly speed: Range;
|
|
54
|
-
/** launch angle range (degrees; 0 = up,
|
|
53
|
+
/** launch angle range (degrees; 0 = up, counterclockwise-positive) */
|
|
55
54
|
readonly angle: Range;
|
|
56
55
|
/** lifetime range (seconds) */
|
|
57
56
|
readonly life: Range;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Runner from "../Runner.js";
|
|
2
3
|
import { ParticleField } from "./ParticleField.js";
|
|
3
4
|
/**
|
|
4
5
|
* Create a source emitter field. Simulate it with `{ burst: n }` (all at
|
|
@@ -7,9 +8,13 @@ import { ParticleField } from "./ParticleField.js";
|
|
|
7
8
|
export const emitter = (props) =>
|
|
8
9
|
// the entity schema is one struct with defaults; the branded return type
|
|
9
10
|
// is a compile-time cast, runtime data is a plain ParticleField
|
|
10
|
-
|
|
11
|
+
// ponytail: particles are outside the entity union (design D10) — this
|
|
12
|
+
// goes through the escape hatch until the particle system is rewritten
|
|
13
|
+
Effect.flatMap(Runner.Runner, (runner) => Runner.particlesEscapeInstantiate(runner, ParticleField.data.make(props)));
|
|
11
14
|
/**
|
|
12
15
|
* Create a floating field: particles spread evenly across the region,
|
|
13
16
|
* drifting and wrapping at the edges. Simulate it with `{ fill: n }`.
|
|
14
17
|
*/
|
|
15
|
-
export const field = (props) =>
|
|
18
|
+
export const field = (props) =>
|
|
19
|
+
// ponytail: see `emitter` above — the same D10 escape hatch
|
|
20
|
+
Effect.flatMap(Runner.Runner, (runner) => Runner.particlesEscapeInstantiate(runner, ParticleField.data.make(props)));
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
/**
|
|
3
|
+
* ponytail: the pre-union entity machinery, kept alive for the particle
|
|
4
|
+
* system ALONE (design D10).
|
|
5
|
+
*
|
|
6
|
+
* ParticleField is not a member of the closed entity union. It is slated for
|
|
7
|
+
* a full rewrite, so porting it to a model it will not keep would be
|
|
8
|
+
* throwaway work — but six example scenes depend on it, so it cannot be
|
|
9
|
+
* deleted either. This file is the entire remnant of `Entity.ts` and
|
|
10
|
+
* `shapes/Legacy.ts`, scoped to `particles/` and reachable from nowhere
|
|
11
|
+
* else.
|
|
12
|
+
*
|
|
13
|
+
* Upgrade path: fold ParticleField into the union (or justify its
|
|
14
|
+
* exclusion) as part of the particles rewrite, then delete this file.
|
|
15
|
+
*/
|
|
16
|
+
export declare const TypeId: "~motion/Entity";
|
|
17
|
+
export interface TraitLens<Data, Value> {
|
|
18
|
+
readonly get: (data: Data) => Value;
|
|
19
|
+
readonly set: (data: Data, value: Value) => Data;
|
|
20
|
+
}
|
|
21
|
+
export type Position = {
|
|
22
|
+
readonly x: number;
|
|
23
|
+
readonly y: number;
|
|
24
|
+
readonly z: number;
|
|
25
|
+
};
|
|
26
|
+
export type EntityTraits<Data> = {
|
|
27
|
+
readonly "~position": TraitLens<Data, Position>;
|
|
28
|
+
readonly "~opacity": TraitLens<Data, number>;
|
|
29
|
+
};
|
|
30
|
+
export type EntityData<Data extends Schema.Struct.Fields> = Schema.Struct<Data & {
|
|
31
|
+
readonly "~visible": Schema.withConstructorDefault<Schema.Boolean>;
|
|
32
|
+
}>;
|
|
33
|
+
export type PartialTraits<Data extends Schema.Struct.Fields> = Partial<EntityTraits<EntityData<Data>["Type"]>>;
|
|
34
|
+
export interface Entity<Name extends string = string, Data extends Schema.Struct.Fields = {}, Traits extends PartialTraits<Data> = {}> {
|
|
35
|
+
readonly [TypeId]: typeof TypeId;
|
|
36
|
+
readonly name: Name;
|
|
37
|
+
readonly data: EntityData<Data>;
|
|
38
|
+
readonly traits: Traits;
|
|
39
|
+
}
|
|
40
|
+
export declare const make: <Name extends string, Data extends Schema.Struct.Fields = {}, const Traits extends PartialTraits<Data> = {}>(name: Name, data: Data, traits?: Traits) => Entity<Name, Data, Traits>;
|
|
41
|
+
export declare const is: (u: unknown) => u is Entity<string, {}, {}>;
|
|
42
|
+
export declare const defaultedNumber: (value: number) => Schema.withConstructorDefault<Schema.Number>;
|
|
43
|
+
export declare const position: {
|
|
44
|
+
x: Schema.withConstructorDefault<Schema.Number>;
|
|
45
|
+
y: Schema.withConstructorDefault<Schema.Number>;
|
|
46
|
+
z: Schema.withConstructorDefault<Schema.Number>;
|
|
47
|
+
};
|
|
48
|
+
export declare const opacity: {
|
|
49
|
+
opacity: Schema.withConstructorDefault<Schema.Number>;
|
|
50
|
+
};
|
|
51
|
+
export declare const positionLens: <Data extends {
|
|
52
|
+
x: number;
|
|
53
|
+
y: number;
|
|
54
|
+
z: number;
|
|
55
|
+
}>() => TraitLens<Data, Position>;
|
|
56
|
+
export declare const opacityLens: <Data extends {
|
|
57
|
+
opacity: number;
|
|
58
|
+
}>() => TraitLens<Data, number>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Predicate } from "effect";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Schema from "effect/Schema";
|
|
4
|
+
/**
|
|
5
|
+
* ponytail: the pre-union entity machinery, kept alive for the particle
|
|
6
|
+
* system ALONE (design D10).
|
|
7
|
+
*
|
|
8
|
+
* ParticleField is not a member of the closed entity union. It is slated for
|
|
9
|
+
* a full rewrite, so porting it to a model it will not keep would be
|
|
10
|
+
* throwaway work — but six example scenes depend on it, so it cannot be
|
|
11
|
+
* deleted either. This file is the entire remnant of `Entity.ts` and
|
|
12
|
+
* `shapes/Legacy.ts`, scoped to `particles/` and reachable from nowhere
|
|
13
|
+
* else.
|
|
14
|
+
*
|
|
15
|
+
* Upgrade path: fold ParticleField into the union (or justify its
|
|
16
|
+
* exclusion) as part of the particles rewrite, then delete this file.
|
|
17
|
+
*/
|
|
18
|
+
export const TypeId = "~motion/Entity";
|
|
19
|
+
export const make = (name, data, traits) => ({
|
|
20
|
+
[TypeId]: TypeId,
|
|
21
|
+
name,
|
|
22
|
+
data: Schema.Struct({
|
|
23
|
+
...data,
|
|
24
|
+
"~visible": Schema.Boolean.pipe(Schema.withConstructorDefault(Effect.succeed(true))),
|
|
25
|
+
}),
|
|
26
|
+
traits: traits ?? {},
|
|
27
|
+
});
|
|
28
|
+
export const is = (u) => Predicate.hasProperty(u, TypeId);
|
|
29
|
+
// ── the Shape2D field helpers particles use ──────────────────────────────
|
|
30
|
+
export const defaultedNumber = (value) => Schema.Number.pipe(Schema.withConstructorDefault(Effect.succeed(value)));
|
|
31
|
+
export const position = {
|
|
32
|
+
x: defaultedNumber(0),
|
|
33
|
+
y: defaultedNumber(0),
|
|
34
|
+
z: defaultedNumber(0),
|
|
35
|
+
};
|
|
36
|
+
export const opacity = {
|
|
37
|
+
opacity: defaultedNumber(1),
|
|
38
|
+
};
|
|
39
|
+
export const positionLens = () => ({
|
|
40
|
+
get: (data) => ({ x: data.x, y: data.y, z: data.z }),
|
|
41
|
+
set: (data, value) => ({ ...data, x: value.x, y: value.y, z: value.z }),
|
|
42
|
+
});
|
|
43
|
+
export const opacityLens = () => ({
|
|
44
|
+
get: (data) => data.opacity,
|
|
45
|
+
set: (data, opacity) => ({ ...data, opacity }),
|
|
46
|
+
});
|
|
@@ -23,6 +23,9 @@ const birthsForFrame = (emission, i, fps) => {
|
|
|
23
23
|
const now = Math.floor(perFrame * i);
|
|
24
24
|
return now - before;
|
|
25
25
|
};
|
|
26
|
+
const unreachableField = (id) => {
|
|
27
|
+
throw new Error(`Particles: field "${id}" was destroyed mid-simulation`);
|
|
28
|
+
};
|
|
26
29
|
// read the field's current config out of its data. `region` is the fill
|
|
27
30
|
// area (defaulted to the frame by the caller); particles are positioned in
|
|
28
31
|
// the field's LOCAL space (origin 0,0) — the field's x/y transform offsets
|
|
@@ -49,10 +52,13 @@ const run = Effect.fnUntraced(function* (instance, duration, emission) {
|
|
|
49
52
|
const dt = 1 / fps;
|
|
50
53
|
const mode = "fill" in emission ? "fill" : "emit";
|
|
51
54
|
// fill spreads across a region: the field's own if set, else the frame
|
|
52
|
-
|
|
55
|
+
// ponytail: particles are outside the entity union (design D10) — read
|
|
56
|
+
// and write go through the escape hatch until the system is rewritten
|
|
57
|
+
const current = Runner.particlesEscapeRead(runner, instance) ??
|
|
58
|
+
unreachableField(instance.id);
|
|
53
59
|
const region = {
|
|
54
|
-
w: current.region?.w ?? runner.
|
|
55
|
-
h: current.region?.h ?? runner.
|
|
60
|
+
w: current.region?.w ?? runner.comp.width,
|
|
61
|
+
h: current.region?.h ?? runner.comp.height,
|
|
56
62
|
};
|
|
57
63
|
for (let i = 1; i <= frames; i++) {
|
|
58
64
|
const n = birthsForFrame(emission, i, fps);
|
|
@@ -62,14 +68,16 @@ const run = Effect.fnUntraced(function* (instance, duration, emission) {
|
|
|
62
68
|
for (let s = 0; s < n; s++) {
|
|
63
69
|
seeds.push(Prng.seedFrom(yield* Random.next));
|
|
64
70
|
}
|
|
65
|
-
yield*
|
|
71
|
+
yield* Effect.sync(() => {
|
|
72
|
+
const data = Runner.particlesEscapeRead(runner, instance) ??
|
|
73
|
+
unreachableField(instance.id);
|
|
66
74
|
// buffer is the field's private mutable representation; clone the
|
|
67
75
|
// array reference so setDataUnsafe stores a fresh value
|
|
68
76
|
const buffer = [
|
|
69
77
|
...data.buffer,
|
|
70
78
|
];
|
|
71
79
|
step(buffer, data.capacity, dt, seeds, configOf(data, region), mode);
|
|
72
|
-
|
|
80
|
+
Runner.particlesEscapeWrite(runner, instance, { ...data, buffer });
|
|
73
81
|
});
|
|
74
82
|
yield* Scene.tick;
|
|
75
83
|
}
|
package/dist/particles/step.js
CHANGED
|
@@ -41,14 +41,15 @@ export const birth = (seed, config) => {
|
|
|
41
41
|
[rng, colorPick] = Prng.nextBetween(rng, 0, config.palette.length);
|
|
42
42
|
let opacity;
|
|
43
43
|
[rng, opacity] = drawOpacity(rng, config);
|
|
44
|
-
// angle 0 = straight up;
|
|
44
|
+
// angle 0 = straight up (+y); counterclockwise-positive (the scene
|
|
45
|
+
// rotation convention)
|
|
45
46
|
const rad = angle * DEG;
|
|
46
47
|
const color = config.palette[Math.min(config.palette.length - 1, Math.floor(colorPick))] ?? Color.white;
|
|
47
48
|
return {
|
|
48
49
|
x: config.x,
|
|
49
50
|
y: config.y,
|
|
50
|
-
vx: Math.sin(rad) * speed,
|
|
51
|
-
vy:
|
|
51
|
+
vx: -Math.sin(rad) * speed,
|
|
52
|
+
vy: Math.cos(rad) * speed,
|
|
52
53
|
age: 0,
|
|
53
54
|
life,
|
|
54
55
|
size,
|
|
@@ -89,9 +90,11 @@ export const birthFill = (seed, config) => {
|
|
|
89
90
|
const rad = dir * DEG;
|
|
90
91
|
const color = config.palette[Math.min(config.palette.length - 1, Math.floor(colorPick))] ?? Color.white;
|
|
91
92
|
return {
|
|
92
|
-
// scattered across the region,
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
// scattered across the region, CENTERED on the field's own origin
|
|
94
|
+
// (same [0, w) draw as before — only the interpretation shifts, so
|
|
95
|
+
// seeded streams are unchanged)
|
|
96
|
+
x: config.x + px - w / 2,
|
|
97
|
+
y: config.y + py - h / 2,
|
|
95
98
|
vx: Math.cos(rad) * dspeed,
|
|
96
99
|
vy: Math.sin(rad) * dspeed,
|
|
97
100
|
age: 0,
|
|
@@ -123,16 +126,17 @@ export const step = (buffer, capacity, dt, seeds, config, mode = "emit") => {
|
|
|
123
126
|
if (!p.alive) {
|
|
124
127
|
continue;
|
|
125
128
|
}
|
|
126
|
-
|
|
129
|
+
// positive gravity pulls toward the bottom of the screen (-y)
|
|
130
|
+
p.vy -= config.gravity * dt;
|
|
127
131
|
p.x += p.vx * dt;
|
|
128
132
|
p.y += p.vy * dt;
|
|
129
133
|
if (p.wrap) {
|
|
130
|
-
// wrap within the region,
|
|
134
|
+
// wrap within the region, CENTERED on the field origin (config.x/y)
|
|
131
135
|
if (rw > 0) {
|
|
132
|
-
p.x = config.x + mod(p.x - config.x, rw);
|
|
136
|
+
p.x = config.x + mod(p.x - config.x + rw / 2, rw) - rw / 2;
|
|
133
137
|
}
|
|
134
138
|
if (rh > 0) {
|
|
135
|
-
p.y = config.y + mod(p.y - config.y, rh);
|
|
139
|
+
p.y = config.y + mod(p.y - config.y + rh / 2, rh) - rh / 2;
|
|
136
140
|
}
|
|
137
141
|
continue;
|
|
138
142
|
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type IsLiteral<T> = string extends T ? false : true;
|
|
2
|
+
export interface UnexpectedType<Message extends string> {
|
|
3
|
+
readonly message: Message;
|
|
4
|
+
}
|
|
5
|
+
export type EnsureLiteral<T, Message extends string> = IsLiteral<T> extends true ? T : UnexpectedType<Message> & string;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effect-motion",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Deterministic, frame-exact motion graphics in code, composed with Effect",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,9 +48,7 @@
|
|
|
48
48
|
"vitest": "^4.1.10"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"
|
|
52
|
-
"chroma-js": "^3.2.0",
|
|
53
|
-
"@effect-motion/thorvg": "^0.1.0"
|
|
51
|
+
"chroma-js": "^3.2.0"
|
|
54
52
|
},
|
|
55
53
|
"scripts": {
|
|
56
54
|
"build": "tsc -p tsconfig.build.json",
|
package/dist/CameraHelpers.d.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import * as Duration from "effect/Duration";
|
|
2
|
-
import * as Effect from "effect/Effect";
|
|
3
|
-
import type { Camera } from "./Camera.js";
|
|
4
|
-
import * as Entity from "./Entity.js";
|
|
5
|
-
import * as Instance from "./Instance.js";
|
|
6
|
-
import * as Runner from "./Runner.js";
|
|
7
|
-
import * as Timing from "./Timing.js";
|
|
8
|
-
/**
|
|
9
|
-
* The public Camera surface: the entity/identity from Camera.js plus the
|
|
10
|
-
* directing helpers. This module (not Camera.ts) is what index exports as
|
|
11
|
-
* the `Camera` namespace — Runner/Renderer import the schema file
|
|
12
|
-
* directly, so helpers can depend on Motion/Scene without an import
|
|
13
|
-
* cycle.
|
|
14
|
-
*
|
|
15
|
-
* Naming rule (recorded in the camera-poi-helpers change): verbs that
|
|
16
|
-
* name their target (`lookAt`, `follow`) have no base/To pair — an
|
|
17
|
-
* optional duration selects instant vs eased. Value-animating helpers
|
|
18
|
-
* (`orbit`/`orbitTo`, `dolly`/`dollyTo`) keep the pair, exactly like
|
|
19
|
-
* `move`/`moveTo`.
|
|
20
|
-
*/
|
|
21
|
-
export { Camera, identity } from "./Camera.js";
|
|
22
|
-
export type { CameraState } from "./Camera.js";
|
|
23
|
-
type AnyInstance = Instance.Instance<any, any, any>;
|
|
24
|
-
/**
|
|
25
|
-
* A helper target: an Instance (position read live each frame), an Effect
|
|
26
|
-
* resolving to one (resolved once at helper start, then read live), or a
|
|
27
|
-
* plain position (inherently fixed — the no-entity escape hatch).
|
|
28
|
-
*/
|
|
29
|
-
export type CameraTarget = AnyInstance | Effect.Effect<AnyInstance, never, Runner.Runner> | Partial<Entity.Position>;
|
|
30
|
-
type CamData = (typeof Camera)["data"];
|
|
31
|
-
type CamTraits = (typeof Camera)["traits"];
|
|
32
|
-
type CamOrEffect<E = never, R = Runner.Runner> = Instance.InstanceOrEffect<"Camera", CamData, CamTraits, E, R>;
|
|
33
|
-
type CamInstance = Instance.Instance<"Camera", CamData, CamTraits>;
|
|
34
|
-
type CamEffect = Effect.Effect<CamInstance, never, Runner.Runner>;
|
|
35
|
-
/**
|
|
36
|
-
* Aim the camera at a target. No duration: set the point of interest this
|
|
37
|
-
* frame. With a duration: eased re-aim as a retargeted tween (lands
|
|
38
|
-
* exactly on a moving target, no terminal snap). `offset` shifts the aim
|
|
39
|
-
* relative to the target ("slightly above their head"). Dual:
|
|
40
|
-
* `lookAt(cam, target, ...)` or `cam.pipe(lookAt(target, ...))`.
|
|
41
|
-
*/
|
|
42
|
-
export declare const lookAt: ((target: CameraTarget, duration?: Duration.Input, timing?: Timing.TimingInput, offset?: Partial<Entity.Position>) => (cam: CamOrEffect) => CamEffect) & ((cam: CamOrEffect, target: CameraTarget, duration?: Duration.Input, timing?: Timing.TimingInput, offset?: Partial<Entity.Position>) => CamEffect);
|
|
43
|
-
/**
|
|
44
|
-
* Track a target for the duration: the point of interest is a hard
|
|
45
|
-
* per-frame copy of the target position (+offset). A plain animator — it
|
|
46
|
-
* pipes, `Scene.all`s, staggers, repeats. No timing input (lag is
|
|
47
|
-
* expressed by springing the POI instead). Ordering practice: within a
|
|
48
|
-
* tick, branches run in fork order — a follow forked before its target's
|
|
49
|
-
* animator reads the previous frame's position, a deterministic one-frame
|
|
50
|
-
* trail. Dual like `lookAt`.
|
|
51
|
-
*/
|
|
52
|
-
export declare const follow: ((target: CameraTarget, duration: Duration.Input, offset?: Partial<Entity.Position>) => (cam: CamOrEffect) => CamEffect) & ((cam: CamOrEffect, target: CameraTarget, duration: Duration.Input, offset?: Partial<Entity.Position>) => CamEffect);
|
|
53
|
-
/**
|
|
54
|
-
* Turntable the camera to an absolute azimuth around its point of
|
|
55
|
-
* interest (angle about the world-Y axis through the POI; 0 = directly +z
|
|
56
|
-
* of it). Position travels the arc — orientation comes entirely from the
|
|
57
|
-
* POI, so there is no orientation math to get wrong. Radius and height
|
|
58
|
-
* are preserved. Dies loudly without a POI. Dual like `moveTo`.
|
|
59
|
-
*/
|
|
60
|
-
export declare const orbitTo: ((azimuth: number, duration: Duration.Input, timing?: Timing.TimingInput) => (cam: CamOrEffect) => CamEffect) & ((cam: CamOrEffect, azimuth: number, duration: Duration.Input, timing?: Timing.TimingInput) => CamEffect);
|
|
61
|
-
/** Like `orbitTo`, but from an explicit start azimuth. */
|
|
62
|
-
export declare const orbit: ((from: number, to: number, duration: Duration.Input, timing?: Timing.TimingInput) => (cam: CamOrEffect) => CamEffect) & ((cam: CamOrEffect, from: number, to: number, duration: Duration.Input, timing?: Timing.TimingInput) => CamEffect);
|
|
63
|
-
/**
|
|
64
|
-
* Move the camera along its view axis to an absolute distance from the
|
|
65
|
-
* point of interest (in: toward it, out: away), aim unchanged. Dies
|
|
66
|
-
* loudly without a POI. Dual like `moveTo`.
|
|
67
|
-
*/
|
|
68
|
-
export declare const dollyTo: ((distance: number, duration: Duration.Input, timing?: Timing.TimingInput) => (cam: CamOrEffect) => CamEffect) & ((cam: CamOrEffect, distance: number, duration: Duration.Input, timing?: Timing.TimingInput) => CamEffect);
|
|
69
|
-
/** Like `dollyTo`, but from an explicit start distance. */
|
|
70
|
-
export declare const dolly: ((from: number, to: number, duration: Duration.Input, timing?: Timing.TimingInput) => (cam: CamOrEffect) => CamEffect) & ((cam: CamOrEffect, from: number, to: number, duration: Duration.Input, timing?: Timing.TimingInput) => CamEffect);
|