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
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as Entity from "../Entity";
|
|
2
|
+
export declare const Rect: Entity.Entity<"shapes/Rect", import("effect/Schema").Struct<{
|
|
3
|
+
x: import("effect/Schema").withConstructorDefault<import("effect/Schema").Number>;
|
|
4
|
+
y: import("effect/Schema").withConstructorDefault<import("effect/Schema").Number>;
|
|
5
|
+
opacity: import("effect/Schema").withConstructorDefault<import("effect/Schema").Number>;
|
|
6
|
+
fill: import("effect/Schema").withConstructorDefault<import("effect/Schema").String>;
|
|
7
|
+
stroke: import("effect/Schema").optionalKey<import("effect/Schema").String>;
|
|
8
|
+
strokeWidth: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
|
|
9
|
+
width: import("effect/Schema").withConstructorDefault<import("effect/Schema").Number>;
|
|
10
|
+
height: import("effect/Schema").withConstructorDefault<import("effect/Schema").Number>;
|
|
11
|
+
}>, {
|
|
12
|
+
readonly "~position": Entity.TraitLens<{
|
|
13
|
+
readonly x: number;
|
|
14
|
+
readonly y: number;
|
|
15
|
+
readonly opacity: number;
|
|
16
|
+
readonly fill: string;
|
|
17
|
+
readonly stroke?: string;
|
|
18
|
+
readonly strokeWidth?: number;
|
|
19
|
+
readonly width: number;
|
|
20
|
+
readonly height: number;
|
|
21
|
+
}, Entity.Position>;
|
|
22
|
+
readonly "~opacity": Entity.TraitLens<{
|
|
23
|
+
readonly x: number;
|
|
24
|
+
readonly y: number;
|
|
25
|
+
readonly opacity: number;
|
|
26
|
+
readonly fill: string;
|
|
27
|
+
readonly stroke?: string;
|
|
28
|
+
readonly strokeWidth?: number;
|
|
29
|
+
readonly width: number;
|
|
30
|
+
readonly height: number;
|
|
31
|
+
}, number>;
|
|
32
|
+
}, {
|
|
33
|
+
readonly x?: number;
|
|
34
|
+
readonly y?: number;
|
|
35
|
+
readonly opacity?: number;
|
|
36
|
+
readonly fill?: string;
|
|
37
|
+
readonly stroke?: string;
|
|
38
|
+
readonly strokeWidth?: number;
|
|
39
|
+
readonly width?: number;
|
|
40
|
+
readonly height?: number;
|
|
41
|
+
}>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as Entity from "../Entity";
|
|
2
|
+
import * as Shape2D from "./Shape2D";
|
|
3
|
+
export const Rect = Entity.make("shapes/Rect", {
|
|
4
|
+
...Shape2D.filled,
|
|
5
|
+
width: Shape2D.defaultedNumber(100),
|
|
6
|
+
height: Shape2D.defaultedNumber(100),
|
|
7
|
+
}, {
|
|
8
|
+
"~position": Shape2D.positionLens(),
|
|
9
|
+
"~opacity": Shape2D.opacityLens(),
|
|
10
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import type * as Entity from "../Entity";
|
|
3
|
+
/**
|
|
4
|
+
* The portable styling prop set shared by built-in shapes — deliberately
|
|
5
|
+
* limited to what any plausible render target (SVG, canvas, Lottie) can
|
|
6
|
+
* express. Camera pan/zoom is a sink-level view transform (see Camera.ts);
|
|
7
|
+
* per-shape transforms (rotation) remain future territory.
|
|
8
|
+
*
|
|
9
|
+
* Visible defaults: a default-constructed shape must be visible on the
|
|
10
|
+
* default (dark) background. Filled shapes default `fill` to white with
|
|
11
|
+
* stroke absent; Line overrides this (stroke white, width 1 — see
|
|
12
|
+
* Line.ts). Absent optional props are omitted by render targets, never
|
|
13
|
+
* emitted with placeholder values.
|
|
14
|
+
*/
|
|
15
|
+
export declare const defaultedNumber: (value: number) => Schema.withConstructorDefault<Schema.Number>;
|
|
16
|
+
export declare const position: {
|
|
17
|
+
x: Schema.withConstructorDefault<Schema.Number>;
|
|
18
|
+
y: Schema.withConstructorDefault<Schema.Number>;
|
|
19
|
+
};
|
|
20
|
+
export declare const opacity: {
|
|
21
|
+
opacity: Schema.withConstructorDefault<Schema.Number>;
|
|
22
|
+
};
|
|
23
|
+
/** Common fields for fillable shapes. */
|
|
24
|
+
export declare const filled: {
|
|
25
|
+
x: Schema.withConstructorDefault<Schema.Number>;
|
|
26
|
+
y: Schema.withConstructorDefault<Schema.Number>;
|
|
27
|
+
opacity: Schema.withConstructorDefault<Schema.Number>;
|
|
28
|
+
fill: Schema.withConstructorDefault<Schema.String>;
|
|
29
|
+
stroke: Schema.optionalKey<Schema.String>;
|
|
30
|
+
strokeWidth: Schema.optionalKey<Schema.Number>;
|
|
31
|
+
};
|
|
32
|
+
/** standard x/y position lens for shapes whose position IS x/y */
|
|
33
|
+
export declare const positionLens: <Data extends {
|
|
34
|
+
x: number;
|
|
35
|
+
y: number;
|
|
36
|
+
}>() => Entity.TraitLens<Data, Entity.Position>;
|
|
37
|
+
/** standard opacity lens */
|
|
38
|
+
export declare const opacityLens: <Data extends {
|
|
39
|
+
opacity: number;
|
|
40
|
+
}>() => Entity.TraitLens<Data, number>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
/**
|
|
4
|
+
* The portable styling prop set shared by built-in shapes — deliberately
|
|
5
|
+
* limited to what any plausible render target (SVG, canvas, Lottie) can
|
|
6
|
+
* express. Camera pan/zoom is a sink-level view transform (see Camera.ts);
|
|
7
|
+
* per-shape transforms (rotation) remain future territory.
|
|
8
|
+
*
|
|
9
|
+
* Visible defaults: a default-constructed shape must be visible on the
|
|
10
|
+
* default (dark) background. Filled shapes default `fill` to white with
|
|
11
|
+
* stroke absent; Line overrides this (stroke white, width 1 — see
|
|
12
|
+
* Line.ts). Absent optional props are omitted by render targets, never
|
|
13
|
+
* emitted with placeholder values.
|
|
14
|
+
*/
|
|
15
|
+
export const defaultedNumber = (value) => Schema.Number.pipe(Schema.withConstructorDefault(Effect.succeed(value)));
|
|
16
|
+
export const position = {
|
|
17
|
+
x: defaultedNumber(0),
|
|
18
|
+
y: defaultedNumber(0),
|
|
19
|
+
};
|
|
20
|
+
export const opacity = {
|
|
21
|
+
opacity: defaultedNumber(1),
|
|
22
|
+
};
|
|
23
|
+
/** Common fields for fillable shapes. */
|
|
24
|
+
export const filled = {
|
|
25
|
+
...position,
|
|
26
|
+
fill: Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed("white"))),
|
|
27
|
+
stroke: Schema.optionalKey(Schema.String),
|
|
28
|
+
strokeWidth: Schema.optionalKey(Schema.Number),
|
|
29
|
+
...opacity,
|
|
30
|
+
};
|
|
31
|
+
/** standard x/y position lens for shapes whose position IS x/y */
|
|
32
|
+
export const positionLens = () => ({
|
|
33
|
+
get: (data) => ({ x: data.x, y: data.y }),
|
|
34
|
+
// spread of a generic yields Data & {...}; assignable back to Data
|
|
35
|
+
set: (data, value) => ({ ...data, x: value.x, y: value.y }),
|
|
36
|
+
});
|
|
37
|
+
/** standard opacity lens */
|
|
38
|
+
export const opacityLens = () => ({
|
|
39
|
+
get: (data) => data.opacity,
|
|
40
|
+
set: (data, opacity) => ({ ...data, opacity }),
|
|
41
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as Entity from "../Entity";
|
|
2
|
+
export declare const Square: Entity.Entity<"shapes/Square", import("effect/Schema").Struct<{
|
|
3
|
+
x: import("effect/Schema").withConstructorDefault<import("effect/Schema").Number>;
|
|
4
|
+
y: import("effect/Schema").withConstructorDefault<import("effect/Schema").Number>;
|
|
5
|
+
opacity: import("effect/Schema").withConstructorDefault<import("effect/Schema").Number>;
|
|
6
|
+
fill: import("effect/Schema").withConstructorDefault<import("effect/Schema").String>;
|
|
7
|
+
stroke: import("effect/Schema").optionalKey<import("effect/Schema").String>;
|
|
8
|
+
strokeWidth: import("effect/Schema").optionalKey<import("effect/Schema").Number>;
|
|
9
|
+
size: import("effect/Schema").withConstructorDefault<import("effect/Schema").Number>;
|
|
10
|
+
}>, {
|
|
11
|
+
readonly "~position": Entity.TraitLens<{
|
|
12
|
+
readonly x: number;
|
|
13
|
+
readonly y: number;
|
|
14
|
+
readonly opacity: number;
|
|
15
|
+
readonly fill: string;
|
|
16
|
+
readonly stroke?: string;
|
|
17
|
+
readonly strokeWidth?: number;
|
|
18
|
+
readonly size: number;
|
|
19
|
+
}, Entity.Position>;
|
|
20
|
+
readonly "~opacity": Entity.TraitLens<{
|
|
21
|
+
readonly x: number;
|
|
22
|
+
readonly y: number;
|
|
23
|
+
readonly opacity: number;
|
|
24
|
+
readonly fill: string;
|
|
25
|
+
readonly stroke?: string;
|
|
26
|
+
readonly strokeWidth?: number;
|
|
27
|
+
readonly size: number;
|
|
28
|
+
}, number>;
|
|
29
|
+
}, {
|
|
30
|
+
readonly x?: number;
|
|
31
|
+
readonly y?: number;
|
|
32
|
+
readonly opacity?: number;
|
|
33
|
+
readonly fill?: string;
|
|
34
|
+
readonly stroke?: string;
|
|
35
|
+
readonly strokeWidth?: number;
|
|
36
|
+
readonly size?: number;
|
|
37
|
+
}>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as Entity from "../Entity";
|
|
2
|
+
import * as Shape2D from "./Shape2D";
|
|
3
|
+
// its own entity, not Rect sugar: the schema IS the width === height
|
|
4
|
+
// constraint, which a Rect can't enforce through updates
|
|
5
|
+
export const Square = Entity.make("shapes/Square", {
|
|
6
|
+
...Shape2D.filled,
|
|
7
|
+
size: Shape2D.defaultedNumber(100),
|
|
8
|
+
}, {
|
|
9
|
+
"~position": Shape2D.positionLens(),
|
|
10
|
+
"~opacity": Shape2D.opacityLens(),
|
|
11
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import * as Entity from "../Entity";
|
|
3
|
+
/**
|
|
4
|
+
* A plain-string text leaf. `text` is required. The engine cannot measure
|
|
5
|
+
* text, so alignment is delegated to SVG via `textAnchor` / `baseline`.
|
|
6
|
+
* Inline formatting and multi-run styling are expressed by composing
|
|
7
|
+
* multiple `Text` instances (via `children`), not by an in-engine tree.
|
|
8
|
+
*/
|
|
9
|
+
export declare const Text: Entity.Entity<"shapes/Text", Schema.Struct<{
|
|
10
|
+
x: Schema.withConstructorDefault<Schema.Number>;
|
|
11
|
+
y: Schema.withConstructorDefault<Schema.Number>;
|
|
12
|
+
opacity: Schema.withConstructorDefault<Schema.Number>;
|
|
13
|
+
fill: Schema.withConstructorDefault<Schema.String>;
|
|
14
|
+
stroke: Schema.optionalKey<Schema.String>;
|
|
15
|
+
strokeWidth: Schema.optionalKey<Schema.Number>;
|
|
16
|
+
text: Schema.String;
|
|
17
|
+
fontSize: Schema.withConstructorDefault<Schema.Number>;
|
|
18
|
+
fontFamily: Schema.withConstructorDefault<Schema.String>;
|
|
19
|
+
textAnchor: Schema.optionalKey<Schema.Literals<readonly ["start", "middle", "end"]>>;
|
|
20
|
+
baseline: Schema.optionalKey<Schema.Literals<readonly ["auto", "middle", "hanging"]>>;
|
|
21
|
+
}>, {
|
|
22
|
+
readonly "~position": Entity.TraitLens<{
|
|
23
|
+
readonly x: number;
|
|
24
|
+
readonly y: number;
|
|
25
|
+
readonly opacity: number;
|
|
26
|
+
readonly fill: string;
|
|
27
|
+
readonly stroke?: string;
|
|
28
|
+
readonly strokeWidth?: number;
|
|
29
|
+
readonly text: string;
|
|
30
|
+
readonly fontSize: number;
|
|
31
|
+
readonly fontFamily: string;
|
|
32
|
+
readonly textAnchor?: "end" | "middle" | "start";
|
|
33
|
+
readonly baseline?: "auto" | "hanging" | "middle";
|
|
34
|
+
}, Entity.Position>;
|
|
35
|
+
readonly "~opacity": Entity.TraitLens<{
|
|
36
|
+
readonly x: number;
|
|
37
|
+
readonly y: number;
|
|
38
|
+
readonly opacity: number;
|
|
39
|
+
readonly fill: string;
|
|
40
|
+
readonly stroke?: string;
|
|
41
|
+
readonly strokeWidth?: number;
|
|
42
|
+
readonly text: string;
|
|
43
|
+
readonly fontSize: number;
|
|
44
|
+
readonly fontFamily: string;
|
|
45
|
+
readonly textAnchor?: "end" | "middle" | "start";
|
|
46
|
+
readonly baseline?: "auto" | "hanging" | "middle";
|
|
47
|
+
}, number>;
|
|
48
|
+
}, {
|
|
49
|
+
readonly x?: number;
|
|
50
|
+
readonly y?: number;
|
|
51
|
+
readonly opacity?: number;
|
|
52
|
+
readonly fill?: string;
|
|
53
|
+
readonly stroke?: string;
|
|
54
|
+
readonly strokeWidth?: number;
|
|
55
|
+
readonly text: string;
|
|
56
|
+
readonly fontSize?: number;
|
|
57
|
+
readonly fontFamily?: string;
|
|
58
|
+
readonly textAnchor?: "end" | "middle" | "start";
|
|
59
|
+
readonly baseline?: "auto" | "hanging" | "middle";
|
|
60
|
+
}>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import * as Entity from "../Entity";
|
|
4
|
+
import * as Shape2D from "./Shape2D";
|
|
5
|
+
/**
|
|
6
|
+
* A plain-string text leaf. `text` is required. The engine cannot measure
|
|
7
|
+
* text, so alignment is delegated to SVG via `textAnchor` / `baseline`.
|
|
8
|
+
* Inline formatting and multi-run styling are expressed by composing
|
|
9
|
+
* multiple `Text` instances (via `children`), not by an in-engine tree.
|
|
10
|
+
*/
|
|
11
|
+
export const Text = Entity.make("shapes/Text", {
|
|
12
|
+
...Shape2D.filled,
|
|
13
|
+
text: Schema.String,
|
|
14
|
+
// numeric, therefore tweenable: tweenTo({ fontSize }) just works
|
|
15
|
+
fontSize: Shape2D.defaultedNumber(16),
|
|
16
|
+
// the generic family resolves to a sane system sans on every
|
|
17
|
+
// platform — no bet on a named font existing
|
|
18
|
+
fontFamily: Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed("sans-serif"))),
|
|
19
|
+
textAnchor: Schema.optionalKey(Schema.Literals(["start", "middle", "end"])),
|
|
20
|
+
baseline: Schema.optionalKey(Schema.Literals(["auto", "middle", "hanging"])),
|
|
21
|
+
}, {
|
|
22
|
+
"~position": Shape2D.positionLens(),
|
|
23
|
+
"~opacity": Shape2D.opacityLens(),
|
|
24
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { Circle } from "./Circle";
|
|
2
|
+
export { Ellipse } from "./Ellipse";
|
|
3
|
+
export * from "./Group";
|
|
4
|
+
export { Layer } from "./Layer";
|
|
5
|
+
export { Line } from "./Line";
|
|
6
|
+
export { Path } from "./Path";
|
|
7
|
+
export { Rect } from "./Rect";
|
|
8
|
+
export * as Shape2D from "./Shape2D";
|
|
9
|
+
export { Square } from "./Square";
|
|
10
|
+
export { Text } from "./Text";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { Circle } from "./Circle";
|
|
2
|
+
export { Ellipse } from "./Ellipse";
|
|
3
|
+
export * from "./Group";
|
|
4
|
+
export { Layer } from "./Layer";
|
|
5
|
+
export { Line } from "./Line";
|
|
6
|
+
export { Path } from "./Path";
|
|
7
|
+
export { Rect } from "./Rect";
|
|
8
|
+
export * as Shape2D from "./Shape2D";
|
|
9
|
+
export { Square } from "./Square";
|
|
10
|
+
export { Text } from "./Text";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Renderer from "../Renderer";
|
|
3
|
+
import { type SvgNode } from "./SvgNode";
|
|
4
|
+
export interface SvgDomConfig {
|
|
5
|
+
readonly target: HTMLElement;
|
|
6
|
+
/** viewport size — defaults to the frame's own width/height metadata */
|
|
7
|
+
readonly width?: number;
|
|
8
|
+
readonly height?: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Materializes a frame into `target` as live DOM.
|
|
12
|
+
*
|
|
13
|
+
* ponytail: clear-and-rebuild per frame; upgrade to keyed reconciliation
|
|
14
|
+
* (instance ids are stable keys) inside this sink if it ever gets slow.
|
|
15
|
+
*/
|
|
16
|
+
export declare const SvgDomRenderer: {
|
|
17
|
+
Context: import("effect/Context").Service<{
|
|
18
|
+
render: <const Entities extends import("../Entity").AnyEntity>(frame: import("../Scene").Frame<Entities>, config: SvgDomConfig) => Effect.Effect<void, never, Entities extends import("../Entity").AnyEntity ? { [K in Entities as K["name"]]: Renderer.EntityRenderer<`SvgDomRenderer/${K["name"]}`, SvgNode, K, never, never>; }[Entities["name"]] : never>;
|
|
19
|
+
}, {
|
|
20
|
+
render: <const Entities extends import("../Entity").AnyEntity>(frame: import("../Scene").Frame<Entities>, config: SvgDomConfig) => Effect.Effect<void, never, Entities extends import("../Entity").AnyEntity ? { [K in Entities as K["name"]]: Renderer.EntityRenderer<`SvgDomRenderer/${K["name"]}`, SvgNode, K, never, never>; }[Entities["name"]] : never>;
|
|
21
|
+
}>;
|
|
22
|
+
layer: import("effect/Layer").Layer<{
|
|
23
|
+
render: <const Entities extends import("../Entity").AnyEntity>(frame: import("../Scene").Frame<Entities>, config: SvgDomConfig) => Effect.Effect<void, never, Entities extends import("../Entity").AnyEntity ? { [K in Entities as K["name"]]: Renderer.EntityRenderer<`SvgDomRenderer/${K["name"]}`, SvgNode, K, never, never>; }[Entities["name"]] : never>;
|
|
24
|
+
}, never, never>;
|
|
25
|
+
makeEntityRendererContext: <const Ent extends import("../Entity").AnyEntity, E = never, R = never>(entity: Ent) => import("effect/Context").Service<Renderer.EntityRenderer<`SvgDomRenderer/${Ent["name"]}`, SvgNode, Ent, E, R>, Renderer.EntityRenderer<`SvgDomRenderer/${Ent["name"]}`, SvgNode, Ent, E, R>>;
|
|
26
|
+
makeEntityRendererService: <const Ent extends import("../Entity").AnyEntity, E = never, R = never>(entity: Ent, render: Renderer.RenderFunction<SvgNode, Ent, E, R>) => Renderer.EntityRenderer<`SvgDomRenderer/${Ent["name"]}`, SvgNode, Ent, E, R>;
|
|
27
|
+
makeEntityRendererLayer: <const Ent extends import("../Entity").AnyEntity, E = never, R = never>(entity: Ent, render: Renderer.RenderFunction<SvgNode, Ent, E, R>) => import("effect/Layer").Layer<Renderer.EntityRenderer<`SvgDomRenderer/${Ent["name"]}`, SvgNode, Ent, E, R>, never, never>;
|
|
28
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Renderer from "../Renderer";
|
|
3
|
+
import { depthOf, layerTransform, wrapLayer } from "./camera";
|
|
4
|
+
import { SVG_NS } from "./SvgNode";
|
|
5
|
+
const createSvgElement = (doc, node) => {
|
|
6
|
+
// namespace is sink-owned: entity renderers never see it
|
|
7
|
+
const el = doc.createElementNS(SVG_NS, node.tag);
|
|
8
|
+
for (const [key, value] of Object.entries(node.props)) {
|
|
9
|
+
el.setAttribute(key, String(value));
|
|
10
|
+
}
|
|
11
|
+
if (typeof node.children === "string") {
|
|
12
|
+
el.textContent = node.children;
|
|
13
|
+
}
|
|
14
|
+
else if (node.children !== undefined) {
|
|
15
|
+
for (const child of node.children) {
|
|
16
|
+
el.append(createSvgElement(doc, child));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return el;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Materializes a frame into `target` as live DOM.
|
|
23
|
+
*
|
|
24
|
+
* ponytail: clear-and-rebuild per frame; upgrade to keyed reconciliation
|
|
25
|
+
* (instance ids are stable keys) inside this sink if it ever gets slow.
|
|
26
|
+
*/
|
|
27
|
+
export const SvgDomRenderer = Renderer.make()("SvgDomRenderer", {
|
|
28
|
+
render: (entities, config, meta) => Effect.gen(function* () {
|
|
29
|
+
const doc = config.target.ownerDocument;
|
|
30
|
+
const width = config.width ?? meta.width;
|
|
31
|
+
const height = config.height ?? meta.height;
|
|
32
|
+
const root = doc.createElementNS(SVG_NS, "svg");
|
|
33
|
+
root.setAttribute("width", String(width));
|
|
34
|
+
root.setAttribute("height", String(height));
|
|
35
|
+
root.append(createSvgElement(doc, {
|
|
36
|
+
tag: "rect",
|
|
37
|
+
props: {
|
|
38
|
+
width: "100%",
|
|
39
|
+
height: "100%",
|
|
40
|
+
fill: meta.backgroundColor,
|
|
41
|
+
},
|
|
42
|
+
}));
|
|
43
|
+
// each top-level layer gets the camera scaled by its own depth
|
|
44
|
+
for (const { render, entry } of entities) {
|
|
45
|
+
const transform = layerTransform(meta.camera, depthOf(entry.data), width, height);
|
|
46
|
+
root.append(createSvgElement(doc, wrapLayer(yield* render, transform)));
|
|
47
|
+
}
|
|
48
|
+
config.target.replaceChildren(root);
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialized SVG node — the data contract between entity renderers and
|
|
3
|
+
* sinks. Entity renderers return descriptions; sinks own materialization
|
|
4
|
+
* (string folding, DOM creation, namespaces).
|
|
5
|
+
*/
|
|
6
|
+
export interface SvgNode {
|
|
7
|
+
readonly tag: string;
|
|
8
|
+
readonly props: Record<string, string | number>;
|
|
9
|
+
/** nested nodes, or a string for text content */
|
|
10
|
+
readonly children?: ReadonlyArray<SvgNode> | string;
|
|
11
|
+
}
|
|
12
|
+
export declare const SVG_NS = "http://www.w3.org/2000/svg";
|
|
13
|
+
export declare const vnodeToString: (node: SvgNode) => string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const SVG_NS = "http://www.w3.org/2000/svg";
|
|
2
|
+
const escapeAttr = (value) => String(value)
|
|
3
|
+
.replaceAll("&", "&")
|
|
4
|
+
.replaceAll('"', """)
|
|
5
|
+
.replaceAll("<", "<");
|
|
6
|
+
const escapeText = (value) => value.replaceAll("&", "&").replaceAll("<", "<");
|
|
7
|
+
export const vnodeToString = (node) => {
|
|
8
|
+
const props = Object.entries(node.props)
|
|
9
|
+
.map(([key, value]) => ` ${key}="${escapeAttr(value)}"`)
|
|
10
|
+
.join("");
|
|
11
|
+
if (node.children === undefined) {
|
|
12
|
+
return `<${node.tag}${props} />`;
|
|
13
|
+
}
|
|
14
|
+
const children = typeof node.children === "string"
|
|
15
|
+
? escapeText(node.children)
|
|
16
|
+
: node.children.map(vnodeToString).join("");
|
|
17
|
+
return `<${node.tag}${props}>${children}</${node.tag}>`;
|
|
18
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Renderer from "../Renderer";
|
|
3
|
+
import { type SvgNode } from "./SvgNode";
|
|
4
|
+
export interface SvgConfig {
|
|
5
|
+
/** viewport size — defaults to the frame's own width/height metadata */
|
|
6
|
+
readonly width?: number;
|
|
7
|
+
readonly height?: number;
|
|
8
|
+
}
|
|
9
|
+
/** Folds a frame into a single self-contained SVG document string. */
|
|
10
|
+
export declare const SvgRenderer: {
|
|
11
|
+
Context: import("effect/Context").Service<{
|
|
12
|
+
render: <const Entities extends import("../Entity").AnyEntity>(frame: import("../Scene").Frame<Entities>, config: SvgConfig) => Effect.Effect<string, never, Entities extends import("../Entity").AnyEntity ? { [K in Entities as K["name"]]: Renderer.EntityRenderer<`SvgRenderer/${K["name"]}`, SvgNode, K, never, never>; }[Entities["name"]] : never>;
|
|
13
|
+
}, {
|
|
14
|
+
render: <const Entities extends import("../Entity").AnyEntity>(frame: import("../Scene").Frame<Entities>, config: SvgConfig) => Effect.Effect<string, never, Entities extends import("../Entity").AnyEntity ? { [K in Entities as K["name"]]: Renderer.EntityRenderer<`SvgRenderer/${K["name"]}`, SvgNode, K, never, never>; }[Entities["name"]] : never>;
|
|
15
|
+
}>;
|
|
16
|
+
layer: import("effect/Layer").Layer<{
|
|
17
|
+
render: <const Entities extends import("../Entity").AnyEntity>(frame: import("../Scene").Frame<Entities>, config: SvgConfig) => Effect.Effect<string, never, Entities extends import("../Entity").AnyEntity ? { [K in Entities as K["name"]]: Renderer.EntityRenderer<`SvgRenderer/${K["name"]}`, SvgNode, K, never, never>; }[Entities["name"]] : never>;
|
|
18
|
+
}, never, never>;
|
|
19
|
+
makeEntityRendererContext: <const Ent extends import("../Entity").AnyEntity, E = never, R = never>(entity: Ent) => import("effect/Context").Service<Renderer.EntityRenderer<`SvgRenderer/${Ent["name"]}`, SvgNode, Ent, E, R>, Renderer.EntityRenderer<`SvgRenderer/${Ent["name"]}`, SvgNode, Ent, E, R>>;
|
|
20
|
+
makeEntityRendererService: <const Ent extends import("../Entity").AnyEntity, E = never, R = never>(entity: Ent, render: Renderer.RenderFunction<SvgNode, Ent, E, R>) => Renderer.EntityRenderer<`SvgRenderer/${Ent["name"]}`, SvgNode, Ent, E, R>;
|
|
21
|
+
makeEntityRendererLayer: <const Ent extends import("../Entity").AnyEntity, E = never, R = never>(entity: Ent, render: Renderer.RenderFunction<SvgNode, Ent, E, R>) => import("effect/Layer").Layer<Renderer.EntityRenderer<`SvgRenderer/${Ent["name"]}`, SvgNode, Ent, E, R>, never, never>;
|
|
22
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Renderer from "../Renderer";
|
|
3
|
+
import { depthOf, layerTransform, wrapLayer } from "./camera";
|
|
4
|
+
import { SVG_NS, vnodeToString } from "./SvgNode";
|
|
5
|
+
/** Folds a frame into a single self-contained SVG document string. */
|
|
6
|
+
export const SvgRenderer = Renderer.make()("SvgRenderer", {
|
|
7
|
+
render: (entities, config, meta) => Effect.gen(function* () {
|
|
8
|
+
const width = config.width ?? meta.width;
|
|
9
|
+
const height = config.height ?? meta.height;
|
|
10
|
+
let svg = `<svg xmlns="${SVG_NS}" width="${width}" height="${height}">`;
|
|
11
|
+
// a rect, not a style attr — survives rasterizers that ignore CSS
|
|
12
|
+
svg += `<rect width="100%" height="100%" fill="${meta.backgroundColor}"/>`;
|
|
13
|
+
// each top-level layer gets the camera scaled by its own depth
|
|
14
|
+
for (const { render, entry } of entities) {
|
|
15
|
+
const transform = layerTransform(meta.camera, depthOf(entry.data), width, height);
|
|
16
|
+
svg += vnodeToString(wrapLayer(yield* render, transform));
|
|
17
|
+
}
|
|
18
|
+
return `${svg}</svg>`;
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FrameMeta } from "../Renderer";
|
|
2
|
+
import type { SvgNode } from "./SvgNode";
|
|
3
|
+
/**
|
|
4
|
+
* The camera as an SVG transform for one top-level layer, scaled by that
|
|
5
|
+
* layer's `depth` (pan AND zoom together). `depth` lerps the layer's
|
|
6
|
+
* transform from identity toward the full camera:
|
|
7
|
+
*
|
|
8
|
+
* depth 1 → full camera depth 0 → identity (screen-fixed HUD)
|
|
9
|
+
* depth d → 1 + (zoom-1)*d zoom, d*pan translation
|
|
10
|
+
*
|
|
11
|
+
* Zoom is about the viewport center so it reads as "zoom into the middle of
|
|
12
|
+
* the shot", not the world origin. Returns "" when the layer transform is
|
|
13
|
+
* identity, so identity cameras add no attribute (output byte-identical to
|
|
14
|
+
* before the camera existed).
|
|
15
|
+
*/
|
|
16
|
+
export declare const layerTransform: (camera: FrameMeta["camera"], depth: number, width: number, height: number) => string;
|
|
17
|
+
/** `depth` for a top-level layer's data (a Layer carries it; others = 1). */
|
|
18
|
+
export declare const depthOf: (data: unknown) => number;
|
|
19
|
+
/** Wrap a rendered layer node in a camera `<g>`, or return it unwrapped
|
|
20
|
+
* when the transform is identity (keeps the common case flat). */
|
|
21
|
+
export declare const wrapLayer: (node: SvgNode, transform: string) => SvgNode;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The camera as an SVG transform for one top-level layer, scaled by that
|
|
3
|
+
* layer's `depth` (pan AND zoom together). `depth` lerps the layer's
|
|
4
|
+
* transform from identity toward the full camera:
|
|
5
|
+
*
|
|
6
|
+
* depth 1 → full camera depth 0 → identity (screen-fixed HUD)
|
|
7
|
+
* depth d → 1 + (zoom-1)*d zoom, d*pan translation
|
|
8
|
+
*
|
|
9
|
+
* Zoom is about the viewport center so it reads as "zoom into the middle of
|
|
10
|
+
* the shot", not the world origin. Returns "" when the layer transform is
|
|
11
|
+
* identity, so identity cameras add no attribute (output byte-identical to
|
|
12
|
+
* before the camera existed).
|
|
13
|
+
*/
|
|
14
|
+
export const layerTransform = (camera, depth, width, height) => {
|
|
15
|
+
const layerZoom = 1 + (camera.zoom - 1) * depth;
|
|
16
|
+
const panX = camera.x * depth;
|
|
17
|
+
const panY = camera.y * depth;
|
|
18
|
+
if (layerZoom === 1 && panX === 0 && panY === 0) {
|
|
19
|
+
return "";
|
|
20
|
+
}
|
|
21
|
+
const cx = width / 2;
|
|
22
|
+
const cy = height / 2;
|
|
23
|
+
// pan first, then zoom about the viewport center: reads right-to-left as
|
|
24
|
+
// translate(-pan) → recenter → scale → uncenter applied to the layer
|
|
25
|
+
const parts = [];
|
|
26
|
+
if (layerZoom !== 1) {
|
|
27
|
+
parts.push(`translate(${cx} ${cy})`, `scale(${layerZoom})`, `translate(${-cx} ${-cy})`);
|
|
28
|
+
}
|
|
29
|
+
if (panX !== 0 || panY !== 0) {
|
|
30
|
+
parts.push(`translate(${-panX} ${-panY})`);
|
|
31
|
+
}
|
|
32
|
+
return parts.join(" ");
|
|
33
|
+
};
|
|
34
|
+
/** `depth` for a top-level layer's data (a Layer carries it; others = 1). */
|
|
35
|
+
export const depthOf = (data) => {
|
|
36
|
+
const depth = data?.depth;
|
|
37
|
+
return typeof depth === "number" ? depth : 1;
|
|
38
|
+
};
|
|
39
|
+
/** Wrap a rendered layer node in a camera `<g>`, or return it unwrapped
|
|
40
|
+
* when the transform is identity (keeps the common case flat). */
|
|
41
|
+
export const wrapLayer = (node, transform) => transform === ""
|
|
42
|
+
? node
|
|
43
|
+
: { tag: "g", props: { transform }, children: [node] };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { entityRendererLayer, layer } from "./layers";
|
|
2
|
+
export { type SvgDomConfig, SvgDomRenderer } from "./SvgDomRenderer";
|
|
3
|
+
export { SVG_NS, type SvgNode, vnodeToString } from "./SvgNode";
|
|
4
|
+
export { type SvgConfig, SvgRenderer } from "./SvgRenderer";
|
|
5
|
+
export * as shapes from "./shapes";
|
|
6
|
+
export { shapesLayer } from "./shapes";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { entityRendererLayer, layer } from "./layers";
|
|
2
|
+
export { SvgDomRenderer } from "./SvgDomRenderer";
|
|
3
|
+
export { SVG_NS, vnodeToString } from "./SvgNode";
|
|
4
|
+
export { SvgRenderer } from "./SvgRenderer";
|
|
5
|
+
export * as shapes from "./shapes";
|
|
6
|
+
export { shapesLayer } from "./shapes";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Layer } from "effect";
|
|
2
|
+
import type * as Entity from "../Entity";
|
|
3
|
+
import type * as Renderer from "../Renderer";
|
|
4
|
+
import type { SvgNode } from "./SvgNode";
|
|
5
|
+
/**
|
|
6
|
+
* Register one SvgNode render function for an entity with BOTH sinks.
|
|
7
|
+
*
|
|
8
|
+
* Entity renderer context keys are per renderer family
|
|
9
|
+
* (`SvgRenderer/<name>` and `SvgDomRenderer/<name>`), so sharing a render
|
|
10
|
+
* function across sinks means registering it twice — this does both.
|
|
11
|
+
*/
|
|
12
|
+
export declare const entityRendererLayer: <const Ent extends Entity.AnyEntity>(entity: Ent, render: Renderer.RenderFunction<SvgNode, Ent>) => Layer.Layer<Renderer.EntityRenderer<`SvgDomRenderer/${Ent["name"]}`, SvgNode, Ent, never, never> | Renderer.EntityRenderer<`SvgRenderer/${Ent["name"]}`, SvgNode, Ent, never, never>, never, never>;
|
|
13
|
+
/** Both sinks' frame renderers, ready to provide. */
|
|
14
|
+
export declare const layer: Layer.Layer<{
|
|
15
|
+
render: <const Entities extends Entity.AnyEntity>(frame: import("../Scene").Frame<Entities>, config: import("./SvgRenderer").SvgConfig) => import("effect/Effect").Effect<string, never, Entities extends Entity.AnyEntity ? { [K in Entities as K["name"]]: Renderer.EntityRenderer<`SvgRenderer/${K["name"]}`, SvgNode, K, never, never>; }[Entities["name"]] : never>;
|
|
16
|
+
} | {
|
|
17
|
+
render: <const Entities extends Entity.AnyEntity>(frame: import("../Scene").Frame<Entities>, config: import("./SvgDomRenderer").SvgDomConfig) => import("effect/Effect").Effect<void, never, Entities extends Entity.AnyEntity ? { [K in Entities as K["name"]]: Renderer.EntityRenderer<`SvgDomRenderer/${K["name"]}`, SvgNode, K, never, never>; }[Entities["name"]] : never>;
|
|
18
|
+
}, never, never>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Layer } from "effect";
|
|
2
|
+
import { SvgDomRenderer } from "./SvgDomRenderer";
|
|
3
|
+
import { SvgRenderer } from "./SvgRenderer";
|
|
4
|
+
/**
|
|
5
|
+
* Register one SvgNode render function for an entity with BOTH sinks.
|
|
6
|
+
*
|
|
7
|
+
* Entity renderer context keys are per renderer family
|
|
8
|
+
* (`SvgRenderer/<name>` and `SvgDomRenderer/<name>`), so sharing a render
|
|
9
|
+
* function across sinks means registering it twice — this does both.
|
|
10
|
+
*/
|
|
11
|
+
export const entityRendererLayer = (entity, render) => Layer.mergeAll(SvgRenderer.makeEntityRendererLayer(entity, render), SvgDomRenderer.makeEntityRendererLayer(entity, render));
|
|
12
|
+
/** Both sinks' frame renderers, ready to provide. */
|
|
13
|
+
export const layer = Layer.mergeAll(SvgRenderer.layer, SvgDomRenderer.layer);
|