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.
- package/README.md +3 -3
- 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 +684 -30
- package/dist/Entity.js +281 -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 +36 -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 +714 -349
- package/dist/Runner.js +197 -150
- package/dist/Scene.d.ts +603 -175
- package/dist/Scene.js +596 -112
- package/dist/Timing.d.ts +170 -13
- package/dist/Timing.js +124 -6
- package/dist/Tree.d.ts +39 -0
- package/dist/Tree.js +127 -0
- package/dist/index.d.ts +54 -5
- package/dist/index.js +56 -5
- package/dist/particles/Particle.d.ts +2 -2
- package/dist/particles/ParticleField.d.ts +44 -42
- package/dist/particles/ParticleField.js +8 -9
- package/dist/particles/constructors.d.ts +5 -6
- package/dist/particles/constructors.js +8 -3
- package/dist/particles/legacy.d.ts +58 -0
- package/dist/particles/legacy.js +46 -0
- package/dist/particles/simulate.js +11 -3
- package/dist/particles/step.js +14 -10
- package/dist/types.d.ts +5 -0
- package/dist/types.js +1 -0
- package/package.json +58 -60
- package/dist/CameraHelpers.d.ts +0 -70
- package/dist/CameraHelpers.js +0 -239
- package/dist/CanvasExporter.d.ts +0 -12
- package/dist/CanvasExporter.js +0 -40
- package/dist/Fonts.d.ts +0 -41
- package/dist/Fonts.js +0 -27
- package/dist/Images.d.ts +0 -33
- package/dist/Images.js +0 -24
- package/dist/PngExporter.d.ts +0 -6
- package/dist/PngExporter.js +0 -85
- package/dist/Renderer.d.ts +0 -118
- package/dist/Renderer.js +0 -508
- package/dist/Shapes.d.ts +0 -11
- package/dist/Shapes.js +0 -11
- package/dist/demo.d.ts +0 -3
- package/dist/demo.js +0 -71
- package/dist/render/dof.d.ts +0 -27
- package/dist/render/dof.js +0 -37
- package/dist/render/paint.d.ts +0 -30
- package/dist/render/paint.js +0 -36
- package/dist/render/shapes.d.ts +0 -42
- package/dist/render/shapes.js +0 -310
- package/dist/shapes/Circle.d.ts +0 -32
- package/dist/shapes/Circle.js +0 -9
- package/dist/shapes/Ellipse.d.ts +0 -35
- package/dist/shapes/Ellipse.js +0 -10
- package/dist/shapes/Group.d.ts +0 -126
- package/dist/shapes/Group.js +0 -89
- package/dist/shapes/Hud.d.ts +0 -38
- package/dist/shapes/Hud.js +0 -35
- package/dist/shapes/Image.d.ts +0 -45
- package/dist/shapes/Image.js +0 -28
- package/dist/shapes/Line.d.ts +0 -47
- package/dist/shapes/Line.js +0 -35
- package/dist/shapes/Path.d.ts +0 -107
- package/dist/shapes/Path.js +0 -32
- package/dist/shapes/Rect.d.ts +0 -51
- package/dist/shapes/Rect.js +0 -22
- package/dist/shapes/Shape2D.d.ts +0 -49
- package/dist/shapes/Shape2D.js +0 -52
- package/dist/shapes/Shapes.d.ts +0 -11
- package/dist/shapes/Shapes.js +0 -11
- package/dist/shapes/Square.d.ts +0 -32
- package/dist/shapes/Square.js +0 -11
- package/dist/shapes/Text.d.ts +0 -51
- package/dist/shapes/Text.js +0 -24
package/dist/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;
|
|
@@ -56,10 +57,10 @@ export declare const ParticleField: Entity.Entity<"particles/ParticleField", Sch
|
|
|
56
57
|
readonly opacityRange: readonly [number, number];
|
|
57
58
|
readonly gravity: number;
|
|
58
59
|
readonly palette: readonly Color.Color[];
|
|
59
|
-
readonly region?:
|
|
60
|
-
readonly w:
|
|
61
|
-
readonly h:
|
|
62
|
-
}
|
|
60
|
+
readonly region?: {
|
|
61
|
+
readonly w: number;
|
|
62
|
+
readonly h: number;
|
|
63
|
+
};
|
|
63
64
|
readonly drift?: readonly [number, number];
|
|
64
65
|
readonly sizeOverLife?: {
|
|
65
66
|
readonly from: number;
|
|
@@ -72,22 +73,23 @@ export declare const ParticleField: Entity.Entity<"particles/ParticleField", Sch
|
|
|
72
73
|
readonly ease?: string;
|
|
73
74
|
};
|
|
74
75
|
readonly capacity: number;
|
|
75
|
-
readonly buffer: readonly
|
|
76
|
-
readonly x:
|
|
77
|
-
readonly y:
|
|
78
|
-
readonly vx:
|
|
79
|
-
readonly vy:
|
|
80
|
-
readonly age:
|
|
81
|
-
readonly life:
|
|
82
|
-
readonly size:
|
|
83
|
-
readonly opacity:
|
|
84
|
-
readonly color:
|
|
85
|
-
readonly rng:
|
|
86
|
-
readonly alive:
|
|
87
|
-
readonly wrap:
|
|
88
|
-
}
|
|
89
|
-
},
|
|
90
|
-
readonly "~opacity":
|
|
76
|
+
readonly buffer: readonly {
|
|
77
|
+
readonly x: number;
|
|
78
|
+
readonly y: number;
|
|
79
|
+
readonly vx: number;
|
|
80
|
+
readonly vy: number;
|
|
81
|
+
readonly age: number;
|
|
82
|
+
readonly life: number;
|
|
83
|
+
readonly size: number;
|
|
84
|
+
readonly opacity: number;
|
|
85
|
+
readonly color: Color.Color;
|
|
86
|
+
readonly rng: number;
|
|
87
|
+
readonly alive: boolean;
|
|
88
|
+
readonly wrap: boolean;
|
|
89
|
+
}[];
|
|
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;
|
|
@@ -99,10 +101,10 @@ export declare const ParticleField: Entity.Entity<"particles/ParticleField", Sch
|
|
|
99
101
|
readonly opacityRange: readonly [number, number];
|
|
100
102
|
readonly gravity: number;
|
|
101
103
|
readonly palette: readonly Color.Color[];
|
|
102
|
-
readonly region?:
|
|
103
|
-
readonly w:
|
|
104
|
-
readonly h:
|
|
105
|
-
}
|
|
104
|
+
readonly region?: {
|
|
105
|
+
readonly w: number;
|
|
106
|
+
readonly h: number;
|
|
107
|
+
};
|
|
106
108
|
readonly drift?: readonly [number, number];
|
|
107
109
|
readonly sizeOverLife?: {
|
|
108
110
|
readonly from: number;
|
|
@@ -115,19 +117,19 @@ export declare const ParticleField: Entity.Entity<"particles/ParticleField", Sch
|
|
|
115
117
|
readonly ease?: string;
|
|
116
118
|
};
|
|
117
119
|
readonly capacity: number;
|
|
118
|
-
readonly buffer: readonly
|
|
119
|
-
readonly x:
|
|
120
|
-
readonly y:
|
|
121
|
-
readonly vx:
|
|
122
|
-
readonly vy:
|
|
123
|
-
readonly age:
|
|
124
|
-
readonly life:
|
|
125
|
-
readonly size:
|
|
126
|
-
readonly opacity:
|
|
127
|
-
readonly color:
|
|
128
|
-
readonly rng:
|
|
129
|
-
readonly alive:
|
|
130
|
-
readonly wrap:
|
|
131
|
-
}
|
|
120
|
+
readonly buffer: readonly {
|
|
121
|
+
readonly x: number;
|
|
122
|
+
readonly y: number;
|
|
123
|
+
readonly vx: number;
|
|
124
|
+
readonly vy: number;
|
|
125
|
+
readonly age: number;
|
|
126
|
+
readonly life: number;
|
|
127
|
+
readonly size: number;
|
|
128
|
+
readonly opacity: number;
|
|
129
|
+
readonly color: Color.Color;
|
|
130
|
+
readonly rng: number;
|
|
131
|
+
readonly alive: boolean;
|
|
132
|
+
readonly wrap: boolean;
|
|
133
|
+
}[];
|
|
132
134
|
}, number>;
|
|
133
135
|
}>;
|
|
@@ -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,7 +52,10 @@ 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
60
|
w: current.region?.w ?? runner.comp.width,
|
|
55
61
|
h: current.region?.h ?? runner.comp.height,
|
|
@@ -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 {};
|