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.
Files changed (91) hide show
  1. package/README.md +2 -2
  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 +682 -38
  9. package/dist/Entity.js +271 -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 +37 -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 +653 -330
  25. package/dist/Runner.js +208 -158
  26. package/dist/Scene.d.ts +621 -171
  27. package/dist/Scene.js +620 -89
  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 +8 -6
  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 +13 -5
  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 +2 -4
  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 -381
  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 -65
  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 -116
  73. package/dist/shapes/Group.js +0 -82
  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;
@@ -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
- }, Entity.Position>;
90
- readonly "~opacity": Entity.TraitLens<{
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 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,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
- 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
- w: current.region?.w ?? runner.settings.width,
55
- h: current.region?.h ?? runner.settings.height,
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* 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 {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-motion",
3
- "version": "0.3.2",
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
- "@effect/platform-node": "4.0.0-beta.98",
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",
@@ -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);