effect-motion 0.2.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 +40 -0
- package/dist/Camera.d.ts +49 -0
- package/dist/Camera.js +33 -0
- package/dist/Entity.d.ts +40 -0
- package/dist/Entity.js +32 -0
- package/dist/Fonts.d.ts +31 -0
- package/dist/Fonts.js +11 -0
- package/dist/Instance.d.ts +16 -0
- package/dist/Instance.js +18 -0
- package/dist/Motion.d.ts +74 -0
- package/dist/Motion.js +125 -0
- package/dist/Phaser.d.ts +65 -0
- package/dist/Phaser.js +170 -0
- package/dist/Physics.d.ts +78 -0
- package/dist/Physics.js +117 -0
- package/dist/Renderer.d.ts +69 -0
- package/dist/Renderer.js +90 -0
- package/dist/Runner.d.ts +360 -0
- package/dist/Runner.js +257 -0
- package/dist/Scene.d.ts +241 -0
- package/dist/Scene.js +454 -0
- package/dist/Time.d.ts +38 -0
- package/dist/Time.js +43 -0
- package/dist/Timing.d.ts +96 -0
- package/dist/Timing.js +151 -0
- package/dist/demo.d.ts +186 -0
- package/dist/demo.js +76 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +13 -0
- package/dist/particles/Particle.d.ts +91 -0
- package/dist/particles/Particle.js +1 -0
- package/dist/particles/ParticleField.d.ts +170 -0
- package/dist/particles/ParticleField.js +69 -0
- package/dist/particles/Prng.d.ts +28 -0
- package/dist/particles/Prng.js +43 -0
- package/dist/particles/constructors.d.ts +78 -0
- package/dist/particles/constructors.js +15 -0
- package/dist/particles/index.d.ts +6 -0
- package/dist/particles/index.js +6 -0
- package/dist/particles/overLife.d.ts +16 -0
- package/dist/particles/overLife.js +21 -0
- package/dist/particles/render.d.ts +13 -0
- package/dist/particles/render.js +33 -0
- package/dist/particles/simulate.d.ts +39 -0
- package/dist/particles/simulate.js +81 -0
- package/dist/particles/step.d.ts +24 -0
- package/dist/particles/step.js +167 -0
- package/dist/shapes/Circle.d.ts +37 -0
- package/dist/shapes/Circle.js +9 -0
- package/dist/shapes/Ellipse.d.ts +41 -0
- package/dist/shapes/Ellipse.js +10 -0
- package/dist/shapes/Group.d.ts +150 -0
- package/dist/shapes/Group.js +82 -0
- package/dist/shapes/Layer.d.ts +9 -0
- package/dist/shapes/Layer.js +23 -0
- package/dist/shapes/Line.d.ts +47 -0
- package/dist/shapes/Line.js +30 -0
- package/dist/shapes/Path.d.ts +38 -0
- package/dist/shapes/Path.js +13 -0
- package/dist/shapes/Rect.d.ts +41 -0
- package/dist/shapes/Rect.js +10 -0
- package/dist/shapes/Shape2D.d.ts +40 -0
- package/dist/shapes/Shape2D.js +41 -0
- package/dist/shapes/Square.d.ts +37 -0
- package/dist/shapes/Square.js +11 -0
- package/dist/shapes/Text.d.ts +60 -0
- package/dist/shapes/Text.js +24 -0
- package/dist/shapes/index.d.ts +10 -0
- package/dist/shapes/index.js +10 -0
- package/dist/svg/SvgDomRenderer.d.ts +28 -0
- package/dist/svg/SvgDomRenderer.js +50 -0
- package/dist/svg/SvgNode.d.ts +13 -0
- package/dist/svg/SvgNode.js +18 -0
- package/dist/svg/SvgRenderer.d.ts +22 -0
- package/dist/svg/SvgRenderer.js +20 -0
- package/dist/svg/camera.d.ts +21 -0
- package/dist/svg/camera.js +43 -0
- package/dist/svg/index.d.ts +6 -0
- package/dist/svg/index.js +6 -0
- package/dist/svg/layers.d.ts +18 -0
- package/dist/svg/layers.js +13 -0
- package/dist/svg/shapes.d.ts +1089 -0
- package/dist/svg/shapes.js +119 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# effect-motion
|
|
2
|
+
|
|
3
|
+
Deterministic, frame-exact motion graphics in code, composed with [Effect](https://effect.website).
|
|
4
|
+
|
|
5
|
+
A scene is an Effect generator program: instantiate entities, then tween or spring their properties, composing motions sequentially or in parallel. Scenes are **deterministic** — seeded randomness and a frame-locked clock make every run byte-identical — and **finite**, which is what lets a scene be scrubbed and replayed like a video.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
`effect` is a peer dependency — install it alongside:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add effect-motion effect
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Write a scene
|
|
16
|
+
|
|
17
|
+
Every `yield*` composes another effect into the scene.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { Motion, Scene, Shapes } from "effect-motion";
|
|
21
|
+
|
|
22
|
+
export const scene = Scene.make(function* () {
|
|
23
|
+
const circle = yield* Scene.instantiate(Shapes.Circle, {
|
|
24
|
+
x: 60,
|
|
25
|
+
y: 150,
|
|
26
|
+
radius: 16,
|
|
27
|
+
fill: "#7f5af0",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
yield* circle.pipe(
|
|
31
|
+
Motion.tweenTo({ x: 440 }, "1 second", "easeInOutCubic"),
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Render it to SVG, play it in React with [`@effect-motion/react`](https://www.npmjs.com/package/@effect-motion/react), or export it to a video with [`@effect-motion/export`](https://www.npmjs.com/package/@effect-motion/export).
|
|
37
|
+
|
|
38
|
+
## Documentation
|
|
39
|
+
|
|
40
|
+
Full docs, concepts, and live examples: **https://github.com/julia-script/effect-motion**
|
package/dist/Camera.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type * as Schema from "effect/Schema";
|
|
2
|
+
import * as Entity from "./Entity";
|
|
3
|
+
/**
|
|
4
|
+
* The camera is view state, not a shape — it is never registered with a
|
|
5
|
+
* sink and never drawn. It exists as an ordinary Instance so the existing
|
|
6
|
+
* animators drive it for free: `camera.pipe(moveTo({ x: 400 }))`,
|
|
7
|
+
* `tween("zoom", ...)`, `spring`, `Scene.fork`, etc.
|
|
8
|
+
*
|
|
9
|
+
* `~position` (x/y) is the pan in world units; `zoom` is a uniform scale
|
|
10
|
+
* (1 = identity). The sink reads these off `FrameMeta.camera` and applies
|
|
11
|
+
* them per top-level layer, scaled by each layer's `depth` — instance data
|
|
12
|
+
* stays in world coordinates, so determinism and `moveTo` semantics are
|
|
13
|
+
* untouched by the camera.
|
|
14
|
+
*/
|
|
15
|
+
declare const fields: {
|
|
16
|
+
x: Schema.withConstructorDefault<Schema.Number>;
|
|
17
|
+
y: Schema.withConstructorDefault<Schema.Number>;
|
|
18
|
+
zoom: Schema.withConstructorDefault<Schema.Number>;
|
|
19
|
+
};
|
|
20
|
+
type CameraData = Schema.Struct<typeof fields>["Type"];
|
|
21
|
+
export declare const Camera: Entity.Entity<"Camera", Schema.Struct<{
|
|
22
|
+
x: Schema.withConstructorDefault<Schema.Number>;
|
|
23
|
+
y: Schema.withConstructorDefault<Schema.Number>;
|
|
24
|
+
zoom: Schema.withConstructorDefault<Schema.Number>;
|
|
25
|
+
}>, {
|
|
26
|
+
readonly "~position": {
|
|
27
|
+
get: (data: CameraData) => {
|
|
28
|
+
x: number;
|
|
29
|
+
y: number;
|
|
30
|
+
};
|
|
31
|
+
set: (data: CameraData, value: Entity.Position) => CameraData;
|
|
32
|
+
};
|
|
33
|
+
}, {
|
|
34
|
+
readonly x?: number;
|
|
35
|
+
readonly y?: number;
|
|
36
|
+
readonly zoom?: number;
|
|
37
|
+
}>;
|
|
38
|
+
/** The identity view: no pan, no zoom. */
|
|
39
|
+
export declare const IDENTITY: {
|
|
40
|
+
readonly x: 0;
|
|
41
|
+
readonly y: 0;
|
|
42
|
+
readonly zoom: 1;
|
|
43
|
+
};
|
|
44
|
+
export interface CameraState {
|
|
45
|
+
readonly x: number;
|
|
46
|
+
readonly y: number;
|
|
47
|
+
readonly zoom: number;
|
|
48
|
+
}
|
|
49
|
+
export {};
|
package/dist/Camera.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as Entity from "./Entity";
|
|
2
|
+
import * as Shape2D from "./shapes/Shape2D";
|
|
3
|
+
/**
|
|
4
|
+
* The camera is view state, not a shape — it is never registered with a
|
|
5
|
+
* sink and never drawn. It exists as an ordinary Instance so the existing
|
|
6
|
+
* animators drive it for free: `camera.pipe(moveTo({ x: 400 }))`,
|
|
7
|
+
* `tween("zoom", ...)`, `spring`, `Scene.fork`, etc.
|
|
8
|
+
*
|
|
9
|
+
* `~position` (x/y) is the pan in world units; `zoom` is a uniform scale
|
|
10
|
+
* (1 = identity). The sink reads these off `FrameMeta.camera` and applies
|
|
11
|
+
* them per top-level layer, scaled by each layer's `depth` — instance data
|
|
12
|
+
* stays in world coordinates, so determinism and `moveTo` semantics are
|
|
13
|
+
* untouched by the camera.
|
|
14
|
+
*/
|
|
15
|
+
const fields = {
|
|
16
|
+
...Shape2D.position,
|
|
17
|
+
zoom: Shape2D.defaultedNumber(1),
|
|
18
|
+
};
|
|
19
|
+
export const Camera = Entity.make("Camera", fields, {
|
|
20
|
+
// only ~position: zoom is a raw numeric field, animated via tween.
|
|
21
|
+
// Inlined (not positionLens()) so the data type flows into the lens —
|
|
22
|
+
// the generic helper needs a second trait present to infer it.
|
|
23
|
+
"~position": {
|
|
24
|
+
get: (data) => ({ x: data.x, y: data.y }),
|
|
25
|
+
set: (data, value) => ({
|
|
26
|
+
...data,
|
|
27
|
+
x: value.x,
|
|
28
|
+
y: value.y,
|
|
29
|
+
}),
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
/** The identity view: no pan, no zoom. */
|
|
33
|
+
export const IDENTITY = { x: 0, y: 0, zoom: 1 };
|
package/dist/Entity.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import type { AnyStructSchema } from "effect/unstable/workflow/Workflow";
|
|
3
|
+
export declare const TypeId: "~motion/Entity";
|
|
4
|
+
/**
|
|
5
|
+
* A trait is a complete get/set lens over the entity's data — all or
|
|
6
|
+
* nothing: a lone getter or setter is unrepresentable. `set` receives
|
|
7
|
+
* the whole data and returns a new immutable whole with the change
|
|
8
|
+
* applied, so each entity owns its semantics (e.g. Line's position
|
|
9
|
+
* translates both endpoints).
|
|
10
|
+
*/
|
|
11
|
+
export interface TraitLens<Data, Value> {
|
|
12
|
+
readonly get: (data: Data) => Value;
|
|
13
|
+
readonly set: (data: Data, value: Value) => Data;
|
|
14
|
+
}
|
|
15
|
+
/** a type alias (not interface) so it satisfies Record constraints */
|
|
16
|
+
export type Position = {
|
|
17
|
+
readonly x: number;
|
|
18
|
+
readonly y: number;
|
|
19
|
+
};
|
|
20
|
+
export type EntityTraits<Data> = {
|
|
21
|
+
readonly "~position": TraitLens<Data, Position>;
|
|
22
|
+
readonly "~opacity": TraitLens<Data, number>;
|
|
23
|
+
};
|
|
24
|
+
export type TraitKey = keyof EntityTraits<unknown>;
|
|
25
|
+
export interface Entity<Name extends string = string, Data extends Schema.Top = Schema.Top, Traits extends Partial<EntityTraits<Data["Type"]>> = {}, MakeInput = Data["~type.make.in"]> {
|
|
26
|
+
readonly [TypeId]: typeof TypeId;
|
|
27
|
+
readonly name: Name;
|
|
28
|
+
readonly data: Data;
|
|
29
|
+
readonly traits: Traits;
|
|
30
|
+
make(input: MakeInput | Data["~type.make.in"]): Data["Type"];
|
|
31
|
+
readonly _MakeInput?: MakeInput;
|
|
32
|
+
}
|
|
33
|
+
export type AnyEntity = Entity<any, any, any, any>;
|
|
34
|
+
/** the entity's trait lens, or a defect naming entity and trait */
|
|
35
|
+
export declare const traitOrDie: <Data, Value>(entity: AnyEntity, key: TraitKey) => TraitLens<Data, Value>;
|
|
36
|
+
type NormalizeStructLike<T extends Schema.Struct.Fields | AnyStructSchema> = T extends AnyStructSchema ? T : T extends Schema.Struct.Fields ? Schema.Struct<T> : never;
|
|
37
|
+
export declare const make: <Name extends string, Data extends Schema.Struct.Fields | AnyStructSchema, const Traits extends Partial<EntityTraits<NormalizeStructLike<Data>["Type"]>> = {}, MakeInput = NormalizeStructLike<Data>["~type.make.in"]>(name: Name, data: Data, traits?: Traits, options?: {
|
|
38
|
+
readonly normalize: (input: MakeInput | NormalizeStructLike<Data>["~type.make.in"]) => NormalizeStructLike<Data>["~type.make.in"];
|
|
39
|
+
}) => Entity<Name, NormalizeStructLike<Data>, Traits, MakeInput>;
|
|
40
|
+
export {};
|
package/dist/Entity.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
export const TypeId = "~motion/Entity";
|
|
3
|
+
/** the entity's trait lens, or a defect naming entity and trait */
|
|
4
|
+
export const traitOrDie = (entity, key) => {
|
|
5
|
+
const lens = entity.traits[key];
|
|
6
|
+
if (lens === undefined) {
|
|
7
|
+
throw new Error(`Entity "${entity.name}" does not implement the "${key}" trait`);
|
|
8
|
+
}
|
|
9
|
+
return lens;
|
|
10
|
+
};
|
|
11
|
+
const normalizeStructLike = (data) => {
|
|
12
|
+
return (Schema.isSchema(data) ? data : Schema.Struct(data));
|
|
13
|
+
};
|
|
14
|
+
export const make = (name, data, traits, options) => {
|
|
15
|
+
const normalized = normalizeStructLike(data);
|
|
16
|
+
// `$` is reserved for builtin, engine-owned instance properties (e.g.
|
|
17
|
+
// `$visible`), which live beside the data — never as entity-data fields.
|
|
18
|
+
for (const field of Object.keys(normalized.fields)) {
|
|
19
|
+
if (field.startsWith("$")) {
|
|
20
|
+
throw new Error(`Entity "${name}": field "${field}" uses the reserved "$" prefix (reserved for builtin instance properties like $visible)`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
[TypeId]: TypeId,
|
|
25
|
+
name,
|
|
26
|
+
data: normalized,
|
|
27
|
+
traits: traits ?? {},
|
|
28
|
+
make: (input) => normalized.make(options === undefined
|
|
29
|
+
? input
|
|
30
|
+
: options.normalize(input)),
|
|
31
|
+
};
|
|
32
|
+
};
|
package/dist/Fonts.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
/**
|
|
3
|
+
* One loadable font face. `family` is the name text entities reference in
|
|
4
|
+
* `fontFamily`; `src` carries per-environment sources — browsers load
|
|
5
|
+
* `url`, offline rasterizers read `path`. Consumers skip sources they
|
|
6
|
+
* can't use. `weight`/`style` are browser variant-matching descriptors;
|
|
7
|
+
* file-based rasterizers read them from the font file itself. Declare one
|
|
8
|
+
* entry per face (e.g. Inter 400 and Inter 700 are two entries).
|
|
9
|
+
*/
|
|
10
|
+
export interface FontResource {
|
|
11
|
+
readonly family: string;
|
|
12
|
+
readonly src: {
|
|
13
|
+
readonly url?: string;
|
|
14
|
+
readonly path?: string;
|
|
15
|
+
};
|
|
16
|
+
/** CSS font-weight (e.g. 400, 700) */
|
|
17
|
+
readonly weight?: number;
|
|
18
|
+
readonly style?: "normal" | "italic";
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Scene annotation key declaring the fonts a scene's text depends on:
|
|
22
|
+
* `scene.annotate(Fonts.Fonts, [...])`. The runtime never reads it — the
|
|
23
|
+
* engine cannot measure text, so fonts cannot affect frame data. Consumers
|
|
24
|
+
* (the player, export tools) read it to prepare their environment before
|
|
25
|
+
* rendering.
|
|
26
|
+
*/
|
|
27
|
+
export declare const Fonts: Context.Reference<readonly FontResource[]>;
|
|
28
|
+
/** A scene's declared fonts — empty for scenes never annotated. */
|
|
29
|
+
export declare const get: (scene: {
|
|
30
|
+
readonly annotations: Context.Context<never>;
|
|
31
|
+
}) => ReadonlyArray<FontResource>;
|
package/dist/Fonts.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
/**
|
|
3
|
+
* Scene annotation key declaring the fonts a scene's text depends on:
|
|
4
|
+
* `scene.annotate(Fonts.Fonts, [...])`. The runtime never reads it — the
|
|
5
|
+
* engine cannot measure text, so fonts cannot affect frame data. Consumers
|
|
6
|
+
* (the player, export tools) read it to prepare their environment before
|
|
7
|
+
* rendering.
|
|
8
|
+
*/
|
|
9
|
+
export const Fonts = Context.Reference("motion/Fonts", { defaultValue: () => [] });
|
|
10
|
+
/** A scene's declared fonts — empty for scenes never annotated. */
|
|
11
|
+
export const get = (scene) => Context.get(scene.annotations, Fonts);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Pipeable from "effect/Pipeable";
|
|
3
|
+
import type * as Schema from "effect/Schema";
|
|
4
|
+
import type * as Entity from "./Entity";
|
|
5
|
+
export declare const TypeId: "~motion/Instance";
|
|
6
|
+
export interface Instance<Name extends string = string, Data extends Schema.Top = Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>> = {}> extends Pipeable.Pipeable {
|
|
7
|
+
readonly [TypeId]: typeof TypeId;
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly entity: Entity.Entity<Name, Data, Traits, any>;
|
|
10
|
+
}
|
|
11
|
+
/** the Instance type of a given entity, traits included */
|
|
12
|
+
export type Of<E extends Entity.AnyEntity> = E extends Entity.Entity<infer Name, infer Data, infer Traits, any> ? Instance<Name, Data, Traits> : never;
|
|
13
|
+
export declare const isInstance: (u: unknown) => u is Instance;
|
|
14
|
+
export declare const make: <Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>>, MakeInput>(entity: Entity.Entity<Name, Data, Traits, MakeInput>, id: string) => Instance<Name, Data, Traits>;
|
|
15
|
+
export type InstanceOrEffect<Name extends string = string, Data extends Schema.Top = Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>> = {}, E = never, R = never> = Instance<Name, Data, Traits> | Effect.Effect<Instance<Name, Data, Traits>, E, R>;
|
|
16
|
+
export declare const flatten: <Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>>, E = never, R = never>(instance: InstanceOrEffect<Name, Data, Traits, E, R>) => Effect.Effect<Instance<Name, Data, Traits>, E, R>;
|
package/dist/Instance.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Pipeable from "effect/Pipeable";
|
|
3
|
+
export const TypeId = "~motion/Instance";
|
|
4
|
+
export const isInstance = (u) => typeof u === "object" && u !== null && TypeId in u;
|
|
5
|
+
const Proto = {
|
|
6
|
+
[TypeId]: TypeId,
|
|
7
|
+
pipe() {
|
|
8
|
+
// biome-ignore lint: lint/style/noArguments: Pipeable's variadic protocol
|
|
9
|
+
return Pipeable.pipeArguments(this, arguments);
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
export const make = (entity, id) => Object.assign(Object.create(Proto), { id, entity });
|
|
13
|
+
export const flatten = (instance) => {
|
|
14
|
+
if (isInstance(instance)) {
|
|
15
|
+
return Effect.succeed(instance);
|
|
16
|
+
}
|
|
17
|
+
return instance;
|
|
18
|
+
};
|
package/dist/Motion.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type * as Duration from "effect/Duration";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import type * as Schema from "effect/Schema";
|
|
4
|
+
import * as Entity from "./Entity";
|
|
5
|
+
import * as Instance from "./Instance";
|
|
6
|
+
import * as Runner from "./Runner";
|
|
7
|
+
import * as Timing from "./Timing";
|
|
8
|
+
export type InterpolableValue = number;
|
|
9
|
+
type InterpolableKeys<T> = {
|
|
10
|
+
[K in keyof T]: T[K] extends InterpolableValue ? K : never;
|
|
11
|
+
}[keyof T];
|
|
12
|
+
export type InterpolableOnly<T> = Pick<T, InterpolableKeys<T>>;
|
|
13
|
+
/** target props, or an updater computing them from the current data */
|
|
14
|
+
export type Target<Data extends Schema.Top> = Partial<InterpolableOnly<Data["Type"]>> | ((data: Data["Type"]) => Partial<InterpolableOnly<Data["Type"]>>);
|
|
15
|
+
export declare const resolveTarget: <Data extends Schema.Top>(target: Target<Data>, current: Data["Type"]) => Record<string, number>;
|
|
16
|
+
export declare const startValues: (current: unknown, target: Record<string, number>, explicitFrom: Record<string, number>) => Record<string, number>;
|
|
17
|
+
/**
|
|
18
|
+
* Animate interpolable (numeric) props of an instance toward `to` over
|
|
19
|
+
* `duration` by raw field name, starting from the instance's current
|
|
20
|
+
* data, optionally paced by a timing function (name or function, default
|
|
21
|
+
* linear). Dual: `tweenTo(instance, to, duration, timing?)` or
|
|
22
|
+
* `instance.pipe(tweenTo(to, duration, timing?))`. Resolves with the
|
|
23
|
+
* instance, so animations chain.
|
|
24
|
+
*/
|
|
25
|
+
export declare const tweenTo: (<Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>>>(to: Target<Data>, duration: Duration.Input, timing?: Timing.TimingInput) => <E = never, R = never>(instance: Instance.InstanceOrEffect<Name, Data, Traits, E, R>) => Effect.Effect<Instance.Instance<Name, Data, Traits>, E, R | Runner.Runner>) & (<Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>>, E = never, R = never>(instance: Instance.InstanceOrEffect<Name, Data, Traits, E, R>, to: Target<Data>, duration: Duration.Input, timing?: Timing.TimingInput) => Effect.Effect<Instance.Instance<Name, Data, Traits>, E, R | Runner.Runner>);
|
|
26
|
+
/**
|
|
27
|
+
* Like `tweenTo`, but with an explicit start: interpolates the keys of
|
|
28
|
+
* `to` from `from` (keys missing in `from` start at the current data).
|
|
29
|
+
* Dual: `tween(instance, from, to, duration, timing?)` or
|
|
30
|
+
* `instance.pipe(tween(from, to, duration, timing?))`.
|
|
31
|
+
*/
|
|
32
|
+
export declare const tween: (<Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>>>(from: Target<Data>, to: Target<Data>, duration: Duration.Input, timing?: Timing.TimingInput) => <E = never, R = never>(instance: Instance.InstanceOrEffect<Name, Data, Traits, E, R>) => Effect.Effect<Instance.Instance<Name, Data, Traits>, E, R | Runner.Runner>) & (<Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>>, E = never, R = never>(instance: Instance.InstanceOrEffect<Name, Data, Traits, E, R>, from: Target<Data>, to: Target<Data>, duration: Duration.Input, timing?: Timing.TimingInput) => Effect.Effect<Instance.Instance<Name, Data, Traits>, E, R | Runner.Runner>);
|
|
33
|
+
type HasPosition<Data extends Schema.Top> = {
|
|
34
|
+
readonly "~position": Entity.TraitLens<Data["Type"], Entity.Position>;
|
|
35
|
+
};
|
|
36
|
+
type HasOpacity<Data extends Schema.Top> = {
|
|
37
|
+
readonly "~opacity": Entity.TraitLens<Data["Type"], number>;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Move an instance to a position via its `~position` trait — per-entity
|
|
41
|
+
* semantics (a Line translates whole, a Group carries its subtree).
|
|
42
|
+
* Partial targets hold the missing axis. Dual:
|
|
43
|
+
* `moveTo(instance, to, duration, timing?)` or
|
|
44
|
+
* `instance.pipe(moveTo(to, duration, timing?))`.
|
|
45
|
+
*/
|
|
46
|
+
export declare const moveTo: (<Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>> & HasPosition<Data>>(to: Partial<Entity.Position>, duration: Duration.Input, timing?: Timing.TimingInput) => <E = never, R = never>(instance: Instance.InstanceOrEffect<Name, Data, Traits, E, R>) => Effect.Effect<Instance.Instance<Name, Data, Traits>, E, R | Runner.Runner>) & (<Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>> & HasPosition<Data>, E = never, R = never>(instance: Instance.InstanceOrEffect<Name, Data, Traits, E, R>, to: Partial<Entity.Position>, duration: Duration.Input, timing?: Timing.TimingInput) => Effect.Effect<Instance.Instance<Name, Data, Traits>, E, R | Runner.Runner>);
|
|
47
|
+
/** Like `moveTo`, but from an explicit position (partials filled from current). */
|
|
48
|
+
export declare const move: (<Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>> & HasPosition<Data>>(from: Partial<Entity.Position>, to: Partial<Entity.Position>, duration: Duration.Input, timing?: Timing.TimingInput) => <E = never, R = never>(instance: Instance.InstanceOrEffect<Name, Data, Traits, E, R>) => Effect.Effect<Instance.Instance<Name, Data, Traits>, E, R | Runner.Runner>) & (<Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>> & HasPosition<Data>, E = never, R = never>(instance: Instance.InstanceOrEffect<Name, Data, Traits, E, R>, from: Partial<Entity.Position>, to: Partial<Entity.Position>, duration: Duration.Input, timing?: Timing.TimingInput) => Effect.Effect<Instance.Instance<Name, Data, Traits>, E, R | Runner.Runner>);
|
|
49
|
+
/**
|
|
50
|
+
* Fade an instance's opacity via its `~opacity` trait. Dual:
|
|
51
|
+
* `fadeTo(instance, opacity, duration, timing?)` or
|
|
52
|
+
* `instance.pipe(fadeTo(opacity, duration, timing?))`.
|
|
53
|
+
*/
|
|
54
|
+
export declare const fadeTo: (<Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>> & HasOpacity<Data>>(to: number, duration: Duration.Input, timing?: Timing.TimingInput) => <E = never, R = never>(instance: Instance.InstanceOrEffect<Name, Data, Traits, E, R>) => Effect.Effect<Instance.Instance<Name, Data, Traits>, E, R | Runner.Runner>) & (<Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>> & HasOpacity<Data>, E = never, R = never>(instance: Instance.InstanceOrEffect<Name, Data, Traits, E, R>, to: number, duration: Duration.Input, timing?: Timing.TimingInput) => Effect.Effect<Instance.Instance<Name, Data, Traits>, E, R | Runner.Runner>);
|
|
55
|
+
/** Like `fadeTo`, but from an explicit opacity. */
|
|
56
|
+
export declare const fade: (<Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>> & HasOpacity<Data>>(from: number, to: number, duration: Duration.Input, timing?: Timing.TimingInput) => <E = never, R = never>(instance: Instance.InstanceOrEffect<Name, Data, Traits, E, R>) => Effect.Effect<Instance.Instance<Name, Data, Traits>, E, R | Runner.Runner>) & (<Name extends string, Data extends Schema.Top, Traits extends Partial<Entity.EntityTraits<Data["Type"]>> & HasOpacity<Data>, E = never, R = never>(instance: Instance.InstanceOrEffect<Name, Data, Traits, E, R>, from: number, to: number, duration: Duration.Input, timing?: Timing.TimingInput) => Effect.Effect<Instance.Instance<Name, Data, Traits>, E, R | Runner.Runner>);
|
|
57
|
+
/**
|
|
58
|
+
* `Motion.wait(duration)` is both an Effect and a pipe step: yield it
|
|
59
|
+
* directly, or place it between chained animations, where it holds the
|
|
60
|
+
* scene AFTER the previous step and passes that step's result through.
|
|
61
|
+
*/
|
|
62
|
+
export interface Wait extends Effect.Effect<void, never, Runner.Runner> {
|
|
63
|
+
<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R | Runner.Runner>;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Hold the scene for `duration` of scene time (frames at the runner's
|
|
67
|
+
* frame rate) — `Scene.sleep`'s chainable sibling.
|
|
68
|
+
*
|
|
69
|
+
* - `yield* Motion.wait("1 second")` — plain frame-based sleep
|
|
70
|
+
* - `instance.pipe(moveTo(...), Motion.wait("1 second"), fadeTo(...))`
|
|
71
|
+
* — the hold runs between the two animations and the instance flows on
|
|
72
|
+
*/
|
|
73
|
+
export declare const wait: (duration: Duration.Input) => Wait;
|
|
74
|
+
export {};
|
package/dist/Motion.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Effectable from "effect/Effectable";
|
|
3
|
+
import { dual } from "effect/Function";
|
|
4
|
+
import * as Entity from "./Entity";
|
|
5
|
+
import * as Instance from "./Instance";
|
|
6
|
+
import * as Runner from "./Runner";
|
|
7
|
+
import * as Scene from "./Scene";
|
|
8
|
+
import * as Time from "./Time";
|
|
9
|
+
import * as Timing from "./Timing";
|
|
10
|
+
// extrapolating on purpose: eased t goes outside [0, 1] for back/elastic
|
|
11
|
+
const lerpNumber = (from, to, t) => from + (to - from) * t;
|
|
12
|
+
/**
|
|
13
|
+
* The interpolation engine: from `from` to `to` over `duration`, calling
|
|
14
|
+
* `fn` with the eased value once per frame (each step ends in a
|
|
15
|
+
* Scene.tick). The last call receives exactly `to` for any timing with
|
|
16
|
+
* f(1) = 1; a zero-length duration still takes one frame. Internal —
|
|
17
|
+
* public animators apply to instances.
|
|
18
|
+
*/
|
|
19
|
+
const interpolate = Effect.fnUntraced(function* (from, to, duration, fn, timing = "linear") {
|
|
20
|
+
const runner = yield* Runner.Runner;
|
|
21
|
+
const timingFn = Timing.resolve(timing);
|
|
22
|
+
const keys = Object.keys(from);
|
|
23
|
+
const frames = Math.max(1, Time.toFrames(duration, runner.settings.frameRate));
|
|
24
|
+
for (let i = 1; i <= frames; i++) {
|
|
25
|
+
const t = timingFn(i / frames);
|
|
26
|
+
const value = {};
|
|
27
|
+
for (const key of keys) {
|
|
28
|
+
value[key] = lerpNumber(from[key], to[key], t);
|
|
29
|
+
}
|
|
30
|
+
yield* fn(value);
|
|
31
|
+
yield* Scene.tick;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
// InterpolableOnly of an opaque Data["Type"] can't be proven
|
|
35
|
+
// index-compatible with Record<string, number>; the runtime shape is
|
|
36
|
+
// guaranteed by the Target type, so cast once here.
|
|
37
|
+
export const resolveTarget = (target, current) => (typeof target === "function"
|
|
38
|
+
? target(current)
|
|
39
|
+
: target);
|
|
40
|
+
// start values for the keys of `target`: from `explicitFrom` where given,
|
|
41
|
+
// otherwise from the instance's current data
|
|
42
|
+
export const startValues = (current, target, explicitFrom) => {
|
|
43
|
+
const start = {};
|
|
44
|
+
for (const key of Object.keys(target)) {
|
|
45
|
+
start[key] =
|
|
46
|
+
explicitFrom[key] ?? current[key];
|
|
47
|
+
}
|
|
48
|
+
return start;
|
|
49
|
+
};
|
|
50
|
+
const animate = Effect.fnUntraced(function* (instanceOrEffect, from, to, duration, timing) {
|
|
51
|
+
const instance = yield* Instance.flatten(instanceOrEffect);
|
|
52
|
+
const current = yield* Scene.data(instance);
|
|
53
|
+
const target = resolveTarget(to, current);
|
|
54
|
+
const start = startValues(current, target, from === undefined ? {} : resolveTarget(from, current));
|
|
55
|
+
yield* interpolate(start, target, duration, (value) =>
|
|
56
|
+
// Data["Type"] is opaque to TS, so spread is disallowed — assign + cast
|
|
57
|
+
Scene.update(instance, (data) => Object.assign({}, data, value)), timing);
|
|
58
|
+
return instance;
|
|
59
|
+
});
|
|
60
|
+
// dispatch on the first argument, not arity: the optional trailing
|
|
61
|
+
// `timing` makes call arity ambiguous between the two forms
|
|
62
|
+
const firstArgIsInstance = (args) => Instance.isInstance(args[0]);
|
|
63
|
+
/**
|
|
64
|
+
* Animate interpolable (numeric) props of an instance toward `to` over
|
|
65
|
+
* `duration` by raw field name, starting from the instance's current
|
|
66
|
+
* data, optionally paced by a timing function (name or function, default
|
|
67
|
+
* linear). Dual: `tweenTo(instance, to, duration, timing?)` or
|
|
68
|
+
* `instance.pipe(tweenTo(to, duration, timing?))`. Resolves with the
|
|
69
|
+
* instance, so animations chain.
|
|
70
|
+
*/
|
|
71
|
+
export const tweenTo = dual(firstArgIsInstance, (instance, to, duration, timing) => animate(instance, undefined, to, duration, timing));
|
|
72
|
+
/**
|
|
73
|
+
* Like `tweenTo`, but with an explicit start: interpolates the keys of
|
|
74
|
+
* `to` from `from` (keys missing in `from` start at the current data).
|
|
75
|
+
* Dual: `tween(instance, from, to, duration, timing?)` or
|
|
76
|
+
* `instance.pipe(tween(from, to, duration, timing?))`.
|
|
77
|
+
*/
|
|
78
|
+
export const tween = dual(firstArgIsInstance, (instance, from, to, duration, timing) => animate(instance, from, to, duration, timing));
|
|
79
|
+
const animatePosition = Effect.fnUntraced(function* (instanceOrEffect, from, to, duration, timing) {
|
|
80
|
+
const instance = yield* Instance.flatten(instanceOrEffect);
|
|
81
|
+
const lens = Entity.traitOrDie(instance.entity, "~position");
|
|
82
|
+
const current = lens.get(yield* Scene.data(instance));
|
|
83
|
+
// partial targets/origins hold the missing axis at its current value
|
|
84
|
+
const target = { ...current, ...to };
|
|
85
|
+
const start = { ...current, ...(from ?? {}) };
|
|
86
|
+
yield* interpolate(start, target, duration, (value) => Scene.update(instance, (data) => lens.set(data, value)), timing);
|
|
87
|
+
return instance;
|
|
88
|
+
});
|
|
89
|
+
const animateOpacity = Effect.fnUntraced(function* (instanceOrEffect, from, to, duration, timing) {
|
|
90
|
+
const instance = yield* Instance.flatten(instanceOrEffect);
|
|
91
|
+
const lens = Entity.traitOrDie(instance.entity, "~opacity");
|
|
92
|
+
const current = lens.get(yield* Scene.data(instance));
|
|
93
|
+
yield* interpolate({ opacity: from ?? current }, { opacity: to }, duration, (value) => Scene.update(instance, (data) => lens.set(data, value.opacity)), timing);
|
|
94
|
+
return instance;
|
|
95
|
+
});
|
|
96
|
+
/**
|
|
97
|
+
* Move an instance to a position via its `~position` trait — per-entity
|
|
98
|
+
* semantics (a Line translates whole, a Group carries its subtree).
|
|
99
|
+
* Partial targets hold the missing axis. Dual:
|
|
100
|
+
* `moveTo(instance, to, duration, timing?)` or
|
|
101
|
+
* `instance.pipe(moveTo(to, duration, timing?))`.
|
|
102
|
+
*/
|
|
103
|
+
export const moveTo = dual(firstArgIsInstance, (instance, to, duration, timing) => animatePosition(instance, undefined, to, duration, timing));
|
|
104
|
+
/** Like `moveTo`, but from an explicit position (partials filled from current). */
|
|
105
|
+
export const move = dual(firstArgIsInstance, (instance, from, to, duration, timing) => animatePosition(instance, from, to, duration, timing));
|
|
106
|
+
/**
|
|
107
|
+
* Fade an instance's opacity via its `~opacity` trait. Dual:
|
|
108
|
+
* `fadeTo(instance, opacity, duration, timing?)` or
|
|
109
|
+
* `instance.pipe(fadeTo(opacity, duration, timing?))`.
|
|
110
|
+
*/
|
|
111
|
+
export const fadeTo = dual(firstArgIsInstance, (instance, to, duration, timing) => animateOpacity(instance, undefined, to, duration, timing));
|
|
112
|
+
/** Like `fadeTo`, but from an explicit opacity. */
|
|
113
|
+
export const fade = dual(firstArgIsInstance, (instance, from, to, duration, timing) => animateOpacity(instance, from, to, duration, timing));
|
|
114
|
+
/**
|
|
115
|
+
* Hold the scene for `duration` of scene time (frames at the runner's
|
|
116
|
+
* frame rate) — `Scene.sleep`'s chainable sibling.
|
|
117
|
+
*
|
|
118
|
+
* - `yield* Motion.wait("1 second")` — plain frame-based sleep
|
|
119
|
+
* - `instance.pipe(moveTo(...), Motion.wait("1 second"), fadeTo(...))`
|
|
120
|
+
* — the hold runs between the two animations and the instance flows on
|
|
121
|
+
*/
|
|
122
|
+
export const wait = (duration) => Object.assign((effect) => Effect.tap(effect, () => Scene.sleep(duration)), Effectable.Prototype({
|
|
123
|
+
label: "Motion.wait",
|
|
124
|
+
evaluate: () => Scene.sleep(duration),
|
|
125
|
+
}));
|
package/dist/Phaser.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Context, Effect } from "effect";
|
|
2
|
+
declare const Phaser_base: Context.ServiceClass<Phaser, "motion/Phaser", {
|
|
3
|
+
register: (n: number) => void;
|
|
4
|
+
deregister: (n: number) => void;
|
|
5
|
+
arriveAndAwaitAdvance: Effect.Effect<void, never, never>;
|
|
6
|
+
awaitAdvance: Effect.Effect<number, never, never>;
|
|
7
|
+
/** debug/test view of the internal counters */
|
|
8
|
+
snapshotUnsafe: () => {
|
|
9
|
+
phase: number;
|
|
10
|
+
parties: number;
|
|
11
|
+
arrived: number;
|
|
12
|
+
state: "idle" | "pending" | "running";
|
|
13
|
+
};
|
|
14
|
+
}> & {
|
|
15
|
+
readonly make: Effect.Effect<{
|
|
16
|
+
register: (n: number) => void;
|
|
17
|
+
deregister: (n: number) => void;
|
|
18
|
+
arriveAndAwaitAdvance: Effect.Effect<void, never, never>;
|
|
19
|
+
awaitAdvance: Effect.Effect<number, never, never>;
|
|
20
|
+
/** debug/test view of the internal counters */
|
|
21
|
+
snapshotUnsafe: () => {
|
|
22
|
+
phase: number;
|
|
23
|
+
parties: number;
|
|
24
|
+
arrived: number;
|
|
25
|
+
state: "idle" | "pending" | "running";
|
|
26
|
+
};
|
|
27
|
+
}, never, never>;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* An externally paced phaser (cf. java.util.concurrent.Phaser).
|
|
31
|
+
*
|
|
32
|
+
* Unlike java's Phaser, which advances the moment the last party arrives,
|
|
33
|
+
* this phaser HOLDS at quiescence until the controller calls `awaitAdvance`,
|
|
34
|
+
* which arms the advance and resolves once every registered party has
|
|
35
|
+
* arrived again. One call = one phase = one animation frame.
|
|
36
|
+
*/
|
|
37
|
+
export declare class Phaser extends Phaser_base {
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Fork `scene` as the root party of `phaser`.
|
|
41
|
+
*
|
|
42
|
+
* Registration happens synchronously before the fork, so between combinators
|
|
43
|
+
* the scene is registered-but-running and `awaitAdvance` can never resolve
|
|
44
|
+
* during a sequential handoff or before the scene starts. The slot is
|
|
45
|
+
* released by a finalizer on success, failure, and interrupt alike.
|
|
46
|
+
*/
|
|
47
|
+
export declare const run: <A, E, R>(phaser: Phaser["Service"], scene: Effect.Effect<A, E, R>) => Effect.Effect<import("effect/Fiber").Fiber<A, E>, never, Exclude<R, Phaser>>;
|
|
48
|
+
/**
|
|
49
|
+
* Run `effect`, then arrive at the phase boundary.
|
|
50
|
+
*
|
|
51
|
+
* Borrows the caller's party slot (no register/deregister) — that is what
|
|
52
|
+
* makes consecutive `one` calls handoff-gap-free.
|
|
53
|
+
*/
|
|
54
|
+
export declare const one: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R | Phaser>;
|
|
55
|
+
/**
|
|
56
|
+
* Run branches in parallel, sharing phases.
|
|
57
|
+
*
|
|
58
|
+
* N branches share N slots: the caller's one (the caller is blocked, not
|
|
59
|
+
* arrived, so it must not hold a countable slot) plus N-1 minted here. A
|
|
60
|
+
* finished branch releases a slot immediately so it cannot hold the phase
|
|
61
|
+
* open — except the last branch, whose slot returns to the resuming caller
|
|
62
|
+
* with no deregister/register gap.
|
|
63
|
+
*/
|
|
64
|
+
export declare const all: <Eff extends Effect.Effect<any, any, any>>(effects: Iterable<Eff>) => Effect.Effect<void, Eff extends Effect.Effect<any, infer E, any> ? E : never, (Eff extends Effect.Effect<any, any, infer R> ? R : never) | Phaser>;
|
|
65
|
+
export {};
|