effect-motion 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +3 -3
  2. package/dist/Camera.d.ts +186 -49
  3. package/dist/Camera.js +343 -76
  4. package/dist/Color.d.ts +101 -1
  5. package/dist/Color.js +101 -1
  6. package/dist/EffectMotionError.d.ts +17 -0
  7. package/dist/EffectMotionError.js +17 -0
  8. package/dist/Entity.d.ts +684 -30
  9. package/dist/Entity.js +281 -27
  10. package/dist/Font.d.ts +108 -0
  11. package/dist/Font.js +95 -0
  12. package/dist/Image.d.ts +71 -0
  13. package/dist/Image.js +50 -0
  14. package/dist/Instance.d.ts +73 -11
  15. package/dist/Instance.js +44 -11
  16. package/dist/Motion.d.ts +328 -53
  17. package/dist/Motion.js +278 -46
  18. package/dist/Physics.d.ts +154 -20
  19. package/dist/Physics.js +92 -11
  20. package/dist/Projection.d.ts +36 -132
  21. package/dist/Projection.js +33 -292
  22. package/dist/Resource.d.ts +26 -0
  23. package/dist/Resource.js +41 -0
  24. package/dist/Runner.d.ts +714 -349
  25. package/dist/Runner.js +197 -150
  26. package/dist/Scene.d.ts +603 -175
  27. package/dist/Scene.js +596 -112
  28. package/dist/Timing.d.ts +170 -13
  29. package/dist/Timing.js +124 -6
  30. package/dist/Tree.d.ts +39 -0
  31. package/dist/Tree.js +127 -0
  32. package/dist/index.d.ts +54 -5
  33. package/dist/index.js +56 -5
  34. package/dist/particles/Particle.d.ts +2 -2
  35. package/dist/particles/ParticleField.d.ts +44 -42
  36. package/dist/particles/ParticleField.js +8 -9
  37. package/dist/particles/constructors.d.ts +5 -6
  38. package/dist/particles/constructors.js +8 -3
  39. package/dist/particles/legacy.d.ts +58 -0
  40. package/dist/particles/legacy.js +46 -0
  41. package/dist/particles/simulate.js +11 -3
  42. package/dist/particles/step.js +14 -10
  43. package/dist/types.d.ts +5 -0
  44. package/dist/types.js +1 -0
  45. package/package.json +58 -60
  46. package/dist/CameraHelpers.d.ts +0 -70
  47. package/dist/CameraHelpers.js +0 -239
  48. package/dist/CanvasExporter.d.ts +0 -12
  49. package/dist/CanvasExporter.js +0 -40
  50. package/dist/Fonts.d.ts +0 -41
  51. package/dist/Fonts.js +0 -27
  52. package/dist/Images.d.ts +0 -33
  53. package/dist/Images.js +0 -24
  54. package/dist/PngExporter.d.ts +0 -6
  55. package/dist/PngExporter.js +0 -85
  56. package/dist/Renderer.d.ts +0 -118
  57. package/dist/Renderer.js +0 -508
  58. package/dist/Shapes.d.ts +0 -11
  59. package/dist/Shapes.js +0 -11
  60. package/dist/demo.d.ts +0 -3
  61. package/dist/demo.js +0 -71
  62. package/dist/render/dof.d.ts +0 -27
  63. package/dist/render/dof.js +0 -37
  64. package/dist/render/paint.d.ts +0 -30
  65. package/dist/render/paint.js +0 -36
  66. package/dist/render/shapes.d.ts +0 -42
  67. package/dist/render/shapes.js +0 -310
  68. package/dist/shapes/Circle.d.ts +0 -32
  69. package/dist/shapes/Circle.js +0 -9
  70. package/dist/shapes/Ellipse.d.ts +0 -35
  71. package/dist/shapes/Ellipse.js +0 -10
  72. package/dist/shapes/Group.d.ts +0 -126
  73. package/dist/shapes/Group.js +0 -89
  74. package/dist/shapes/Hud.d.ts +0 -38
  75. package/dist/shapes/Hud.js +0 -35
  76. package/dist/shapes/Image.d.ts +0 -45
  77. package/dist/shapes/Image.js +0 -28
  78. package/dist/shapes/Line.d.ts +0 -47
  79. package/dist/shapes/Line.js +0 -35
  80. package/dist/shapes/Path.d.ts +0 -107
  81. package/dist/shapes/Path.js +0 -32
  82. package/dist/shapes/Rect.d.ts +0 -51
  83. package/dist/shapes/Rect.js +0 -22
  84. package/dist/shapes/Shape2D.d.ts +0 -49
  85. package/dist/shapes/Shape2D.js +0 -52
  86. package/dist/shapes/Shapes.d.ts +0 -11
  87. package/dist/shapes/Shapes.js +0 -11
  88. package/dist/shapes/Square.d.ts +0 -32
  89. package/dist/shapes/Square.js +0 -11
  90. package/dist/shapes/Text.d.ts +0 -51
  91. package/dist/shapes/Text.js +0 -24
package/dist/index.js CHANGED
@@ -1,14 +1,65 @@
1
- export * as Camera from "./CameraHelpers.js";
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 Fonts from "./Fonts.js";
5
- export * as Images from "./Images.js";
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 Renderer from "./Renderer.js";
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, clockwise-positive) */
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 Entity from "../Entity.js";
4
- export declare const ParticleField: Entity.Entity<"particles/ParticleField", Schema.Struct<{
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": Entity.TraitLens<{
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?: Schema.Struct.ReadonlySide<{
60
- readonly w: Schema.Number;
61
- readonly h: Schema.Number;
62
- }, "Type">;
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 Schema.Struct.ReadonlySide<{
76
- readonly x: Schema.Number;
77
- readonly y: Schema.Number;
78
- readonly vx: Schema.Number;
79
- readonly vy: Schema.Number;
80
- readonly age: Schema.Number;
81
- readonly life: Schema.Number;
82
- readonly size: Schema.Number;
83
- readonly opacity: Schema.Number;
84
- readonly color: typeof Color.Color;
85
- readonly rng: Schema.Number;
86
- readonly alive: Schema.Boolean;
87
- readonly wrap: Schema.Boolean;
88
- }, "Type">[];
89
- }, Entity.Position>;
90
- readonly "~opacity": Entity.TraitLens<{
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?: Schema.Struct.ReadonlySide<{
103
- readonly w: Schema.Number;
104
- readonly h: Schema.Number;
105
- }, "Type">;
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 Schema.Struct.ReadonlySide<{
119
- readonly x: Schema.Number;
120
- readonly y: Schema.Number;
121
- readonly vx: Schema.Number;
122
- readonly vy: Schema.Number;
123
- readonly age: Schema.Number;
124
- readonly life: Schema.Number;
125
- readonly size: Schema.Number;
126
- readonly opacity: Schema.Number;
127
- readonly color: typeof Color.Color;
128
- readonly rng: Schema.Number;
129
- readonly alive: Schema.Boolean;
130
- readonly wrap: Schema.Boolean;
131
- }, "Type">[];
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 Entity from "../Entity.js";
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 = Entity.make("particles/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
- ...Shape2D.position,
44
- ...Shape2D.opacity,
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: Shape2D.defaultedNumber(0),
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: Shape2D.defaultedNumber(1024),
64
+ capacity: Legacy.defaultedNumber(1024),
66
65
  buffer: Schema.Array(ParticleSchema).pipe(Schema.withConstructorDefault(Effect.sync(() => []))),
67
66
  }, {
68
- "~position": Shape2D.positionLens(),
69
- "~opacity": Shape2D.opacityLens(),
67
+ "~position": Legacy.positionLens(),
68
+ "~opacity": Legacy.opacityLens(),
70
69
  });
@@ -1,9 +1,8 @@
1
- import type * as Effect from "effect/Effect";
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 type * as Runner from "../Runner.js";
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.Of<typeof ParticleField>;
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, clockwise-positive) */
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 Scene from "../Scene.js";
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
- Scene.instantiate(ParticleField, props);
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) => Scene.instantiate(ParticleField, 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
- const current = yield* Scene.data(instance);
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* Scene.update(instance, (data) => {
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
- return { ...data, buffer };
80
+ Runner.particlesEscapeWrite(runner, instance, { ...data, buffer });
73
81
  });
74
82
  yield* Scene.tick;
75
83
  }
@@ -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; clockwise-positive
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: -Math.cos(rad) * speed,
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, offset by the field's own origin
93
- x: config.x + px,
94
- y: config.y + py,
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
- p.vy += config.gravity * dt;
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, which sits at the field origin (config.x/y)
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
  }
@@ -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 {};