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,167 @@
|
|
|
1
|
+
import * as Prng from "./Prng";
|
|
2
|
+
/**
|
|
3
|
+
* The per-frame simulation as a PURE fold: `(buffer, dt, births) → buffer`.
|
|
4
|
+
* No external entropy — the only randomness is each particle's own carried
|
|
5
|
+
* PRNG state, seeded at birth. `births` is the list of seeds for particles
|
|
6
|
+
* to birth THIS frame (how many, and their seeds, is decided by the caller
|
|
7
|
+
* — burst vs. stream — and drawn from the scene's seeded Random there, so
|
|
8
|
+
* this function stays pure and testable).
|
|
9
|
+
*
|
|
10
|
+
* Order per frame: integrate + age live particles, kill expired, then emit
|
|
11
|
+
* new ones into free slots (overwriting oldest when full).
|
|
12
|
+
*
|
|
13
|
+
* DETERMINISM: the draw order at birth is FIXED and part of the contract —
|
|
14
|
+
* angle, speed, life, size, color, in that sequence. Changing it shifts
|
|
15
|
+
* seeded output and is a breaking change.
|
|
16
|
+
*/
|
|
17
|
+
const DEG = Math.PI / 180;
|
|
18
|
+
// positive modulo — JS `%` keeps the sign of the dividend, which would let a
|
|
19
|
+
// particle drifting off the left edge land at a negative coordinate
|
|
20
|
+
const mod = (n, m) => ((n % m) + m) % m;
|
|
21
|
+
// sample a uniform range from a particle's own rng, threading state
|
|
22
|
+
const draw = (rng, [min, max]) => Prng.nextBetween(rng, min, max);
|
|
23
|
+
// draw the baseline opacity — LAST in the draw order for both births, so
|
|
24
|
+
// adding it didn't shift earlier draws. Absent `opacity` config → 1 (and no
|
|
25
|
+
// draw is consumed, keeping opacity-less scenes byte-identical).
|
|
26
|
+
const drawOpacity = (rng, config) => config.opacity ? draw(rng, config.opacity) : [rng, 1];
|
|
27
|
+
/** birth one particle from a seed + config (fixed draw order). */
|
|
28
|
+
export const birth = (seed, config) => {
|
|
29
|
+
let rng = seed;
|
|
30
|
+
let angle;
|
|
31
|
+
let speed;
|
|
32
|
+
let life;
|
|
33
|
+
let size;
|
|
34
|
+
let colorPick;
|
|
35
|
+
[rng, angle] = draw(rng, config.angle);
|
|
36
|
+
[rng, speed] = draw(rng, config.speed);
|
|
37
|
+
[rng, life] = draw(rng, config.life);
|
|
38
|
+
[rng, size] = draw(rng, config.size);
|
|
39
|
+
[rng, colorPick] = Prng.nextBetween(rng, 0, config.palette.length);
|
|
40
|
+
let opacity;
|
|
41
|
+
[rng, opacity] = drawOpacity(rng, config);
|
|
42
|
+
// angle 0 = straight up; clockwise-positive
|
|
43
|
+
const rad = angle * DEG;
|
|
44
|
+
const color = config.palette[Math.min(config.palette.length - 1, Math.floor(colorPick))] ?? "white";
|
|
45
|
+
return {
|
|
46
|
+
x: config.x,
|
|
47
|
+
y: config.y,
|
|
48
|
+
vx: Math.sin(rad) * speed,
|
|
49
|
+
vy: -Math.cos(rad) * speed,
|
|
50
|
+
age: 0,
|
|
51
|
+
life,
|
|
52
|
+
size,
|
|
53
|
+
opacity,
|
|
54
|
+
color,
|
|
55
|
+
rng,
|
|
56
|
+
alive: true,
|
|
57
|
+
wrap: false,
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Birth a `fill`-mode particle: scattered at a random point in the region
|
|
62
|
+
* (not at the origin), given a small isotropic drift velocity (not a
|
|
63
|
+
* launch), and marked to wrap forever instead of aging out. This is what
|
|
64
|
+
* makes an evenly-spread floating field rather than a fountain. Same fixed
|
|
65
|
+
* draw order — position x, position y, drift direction, drift speed, size,
|
|
66
|
+
* color — so seeded output is stable.
|
|
67
|
+
*/
|
|
68
|
+
export const birthFill = (seed, config) => {
|
|
69
|
+
const w = config.region?.w ?? 0;
|
|
70
|
+
const h = config.region?.h ?? 0;
|
|
71
|
+
const drift = config.drift ?? [0, 0];
|
|
72
|
+
let rng = seed;
|
|
73
|
+
let px;
|
|
74
|
+
let py;
|
|
75
|
+
let dir;
|
|
76
|
+
let dspeed;
|
|
77
|
+
let size;
|
|
78
|
+
let colorPick;
|
|
79
|
+
[rng, px] = Prng.nextBetween(rng, 0, w);
|
|
80
|
+
[rng, py] = Prng.nextBetween(rng, 0, h);
|
|
81
|
+
[rng, dir] = Prng.nextBetween(rng, 0, 360);
|
|
82
|
+
[rng, dspeed] = draw(rng, drift);
|
|
83
|
+
[rng, size] = draw(rng, config.size);
|
|
84
|
+
[rng, colorPick] = Prng.nextBetween(rng, 0, config.palette.length);
|
|
85
|
+
let opacity;
|
|
86
|
+
[rng, opacity] = drawOpacity(rng, config);
|
|
87
|
+
const rad = dir * DEG;
|
|
88
|
+
const color = config.palette[Math.min(config.palette.length - 1, Math.floor(colorPick))] ?? "white";
|
|
89
|
+
return {
|
|
90
|
+
// scattered across the region, offset by the field's own origin
|
|
91
|
+
x: config.x + px,
|
|
92
|
+
y: config.y + py,
|
|
93
|
+
vx: Math.cos(rad) * dspeed,
|
|
94
|
+
vy: Math.sin(rad) * dspeed,
|
|
95
|
+
age: 0,
|
|
96
|
+
life: Number.POSITIVE_INFINITY,
|
|
97
|
+
size,
|
|
98
|
+
opacity,
|
|
99
|
+
color,
|
|
100
|
+
rng,
|
|
101
|
+
alive: true,
|
|
102
|
+
wrap: true,
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Advance the buffer one frame. `dt` is seconds per frame. `seeds` are the
|
|
107
|
+
* per-particle seeds to birth this frame (already drawn upstream). The
|
|
108
|
+
* buffer is mutated in place and returned — it is the field's private
|
|
109
|
+
* representation, owned by one instance, so in-place is safe and flat.
|
|
110
|
+
* ponytail: array-of-structs, mutated in place. SoA + typed arrays is the
|
|
111
|
+
* upgrade path if a capacity-target benchmark shows this is the wall.
|
|
112
|
+
*/
|
|
113
|
+
export const step = (buffer, capacity, dt, seeds, config, mode = "emit") => {
|
|
114
|
+
const spawn = mode === "fill" ? birthFill : birth;
|
|
115
|
+
// integrate live particles. Emitter particles age and die; fill (wrap)
|
|
116
|
+
// particles never age and instead wrap around the region's edges, so an
|
|
117
|
+
// evenly-spread field stays populated forever.
|
|
118
|
+
const rw = config.region?.w ?? 0;
|
|
119
|
+
const rh = config.region?.h ?? 0;
|
|
120
|
+
for (const p of buffer) {
|
|
121
|
+
if (!p.alive) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
p.vy += config.gravity * dt;
|
|
125
|
+
p.x += p.vx * dt;
|
|
126
|
+
p.y += p.vy * dt;
|
|
127
|
+
if (p.wrap) {
|
|
128
|
+
// wrap within the region, which sits at the field origin (config.x/y)
|
|
129
|
+
if (rw > 0) {
|
|
130
|
+
p.x = config.x + mod(p.x - config.x, rw);
|
|
131
|
+
}
|
|
132
|
+
if (rh > 0) {
|
|
133
|
+
p.y = config.y + mod(p.y - config.y, rh);
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
p.age += dt;
|
|
138
|
+
if (p.age >= p.life) {
|
|
139
|
+
p.alive = false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// emit into free slots; when none, overwrite the oldest live particle
|
|
143
|
+
for (const seed of seeds) {
|
|
144
|
+
const fresh = spawn(seed, config);
|
|
145
|
+
const deadIdx = buffer.findIndex((p) => !p.alive);
|
|
146
|
+
if (deadIdx !== -1) {
|
|
147
|
+
buffer[deadIdx] = fresh;
|
|
148
|
+
}
|
|
149
|
+
else if (buffer.length < capacity) {
|
|
150
|
+
buffer.push(fresh);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
// full and all alive: overwrite the oldest (highest age)
|
|
154
|
+
let oldest = 0;
|
|
155
|
+
for (let i = 1; i < buffer.length; i++) {
|
|
156
|
+
// biome-ignore lint/style/noNonNullAssertion: indices are in range
|
|
157
|
+
if (buffer[i].age > buffer[oldest].age) {
|
|
158
|
+
oldest = i;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
buffer[oldest] = fresh;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return buffer;
|
|
165
|
+
};
|
|
166
|
+
/** count of currently-live particles */
|
|
167
|
+
export const liveCount = (buffer) => buffer.reduce((n, p) => (p.alive ? n + 1 : n), 0);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as Entity from "../Entity";
|
|
2
|
+
export declare const Circle: Entity.Entity<"shapes/Circle", 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
|
+
radius: 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 radius: 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 radius: 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 radius?: number;
|
|
37
|
+
}>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as Entity from "../Entity";
|
|
2
|
+
import * as Shape2D from "./Shape2D";
|
|
3
|
+
export const Circle = Entity.make("shapes/Circle", {
|
|
4
|
+
...Shape2D.filled,
|
|
5
|
+
radius: Shape2D.defaultedNumber(10),
|
|
6
|
+
}, {
|
|
7
|
+
"~position": Shape2D.positionLens(),
|
|
8
|
+
"~opacity": Shape2D.opacityLens(),
|
|
9
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as Entity from "../Entity";
|
|
2
|
+
export declare const Ellipse: Entity.Entity<"shapes/Ellipse", 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
|
+
rx: import("effect/Schema").withConstructorDefault<import("effect/Schema").Number>;
|
|
10
|
+
ry: 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 rx: number;
|
|
20
|
+
readonly ry: 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 rx: number;
|
|
30
|
+
readonly ry: 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 rx?: number;
|
|
40
|
+
readonly ry?: number;
|
|
41
|
+
}>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as Entity from "../Entity";
|
|
2
|
+
import * as Shape2D from "./Shape2D";
|
|
3
|
+
export const Ellipse = Entity.make("shapes/Ellipse", {
|
|
4
|
+
...Shape2D.filled,
|
|
5
|
+
rx: Shape2D.defaultedNumber(20),
|
|
6
|
+
ry: Shape2D.defaultedNumber(10),
|
|
7
|
+
}, {
|
|
8
|
+
"~position": Shape2D.positionLens(),
|
|
9
|
+
"~opacity": Shape2D.opacityLens(),
|
|
10
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import * as Entity from "../Entity";
|
|
3
|
+
export declare const TransformMatrix: Schema.Struct<{
|
|
4
|
+
readonly a: Schema.Number;
|
|
5
|
+
readonly b: Schema.Number;
|
|
6
|
+
readonly c: Schema.Number;
|
|
7
|
+
readonly d: Schema.Number;
|
|
8
|
+
readonly e: Schema.Number;
|
|
9
|
+
readonly f: Schema.Number;
|
|
10
|
+
}>;
|
|
11
|
+
export type TransformMatrix = typeof TransformMatrix.Type;
|
|
12
|
+
export declare const identityTransform: {
|
|
13
|
+
a: number;
|
|
14
|
+
b: number;
|
|
15
|
+
c: number;
|
|
16
|
+
d: number;
|
|
17
|
+
e: number;
|
|
18
|
+
f: number;
|
|
19
|
+
};
|
|
20
|
+
export declare const multiplyTransforms: (left: TransformMatrix, right: TransformMatrix) => TransformMatrix;
|
|
21
|
+
export declare const TransformOperation: Schema.TaggedUnion<{
|
|
22
|
+
readonly "transform/matrix": Schema.TaggedStruct<"transform/matrix", {
|
|
23
|
+
readonly a: Schema.Number;
|
|
24
|
+
readonly b: Schema.Number;
|
|
25
|
+
readonly c: Schema.Number;
|
|
26
|
+
readonly d: Schema.Number;
|
|
27
|
+
readonly e: Schema.Number;
|
|
28
|
+
readonly f: Schema.Number;
|
|
29
|
+
}>;
|
|
30
|
+
readonly "transform/scale": Schema.TaggedStruct<"transform/scale", {
|
|
31
|
+
readonly x: Schema.Number;
|
|
32
|
+
readonly y: Schema.Number;
|
|
33
|
+
}>;
|
|
34
|
+
readonly "transform/translate": Schema.TaggedStruct<"transform/translate", {
|
|
35
|
+
readonly x: Schema.Number;
|
|
36
|
+
readonly y: Schema.Number;
|
|
37
|
+
}>;
|
|
38
|
+
}>;
|
|
39
|
+
declare const TransformOperations: Schema.$Array<Schema.TaggedUnion<{
|
|
40
|
+
readonly "transform/matrix": Schema.TaggedStruct<"transform/matrix", {
|
|
41
|
+
readonly a: Schema.Number;
|
|
42
|
+
readonly b: Schema.Number;
|
|
43
|
+
readonly c: Schema.Number;
|
|
44
|
+
readonly d: Schema.Number;
|
|
45
|
+
readonly e: Schema.Number;
|
|
46
|
+
readonly f: Schema.Number;
|
|
47
|
+
}>;
|
|
48
|
+
readonly "transform/scale": Schema.TaggedStruct<"transform/scale", {
|
|
49
|
+
readonly x: Schema.Number;
|
|
50
|
+
readonly y: Schema.Number;
|
|
51
|
+
}>;
|
|
52
|
+
readonly "transform/translate": Schema.TaggedStruct<"transform/translate", {
|
|
53
|
+
readonly x: Schema.Number;
|
|
54
|
+
readonly y: Schema.Number;
|
|
55
|
+
}>;
|
|
56
|
+
}>>;
|
|
57
|
+
export declare const Transform: Schema.decodeTo<Schema.Struct<{
|
|
58
|
+
readonly a: Schema.Number;
|
|
59
|
+
readonly b: Schema.Number;
|
|
60
|
+
readonly c: Schema.Number;
|
|
61
|
+
readonly d: Schema.Number;
|
|
62
|
+
readonly e: Schema.Number;
|
|
63
|
+
readonly f: Schema.Number;
|
|
64
|
+
}>, Schema.$Array<Schema.TaggedUnion<{
|
|
65
|
+
readonly "transform/matrix": Schema.TaggedStruct<"transform/matrix", {
|
|
66
|
+
readonly a: Schema.Number;
|
|
67
|
+
readonly b: Schema.Number;
|
|
68
|
+
readonly c: Schema.Number;
|
|
69
|
+
readonly d: Schema.Number;
|
|
70
|
+
readonly e: Schema.Number;
|
|
71
|
+
readonly f: Schema.Number;
|
|
72
|
+
}>;
|
|
73
|
+
readonly "transform/scale": Schema.TaggedStruct<"transform/scale", {
|
|
74
|
+
readonly x: Schema.Number;
|
|
75
|
+
readonly y: Schema.Number;
|
|
76
|
+
}>;
|
|
77
|
+
readonly "transform/translate": Schema.TaggedStruct<"transform/translate", {
|
|
78
|
+
readonly x: Schema.Number;
|
|
79
|
+
readonly y: Schema.Number;
|
|
80
|
+
}>;
|
|
81
|
+
}>>, never, never>;
|
|
82
|
+
declare const fields: {
|
|
83
|
+
x: Schema.withConstructorDefault<Schema.Number>;
|
|
84
|
+
y: Schema.withConstructorDefault<Schema.Number>;
|
|
85
|
+
opacity: Schema.withConstructorDefault<Schema.Number>;
|
|
86
|
+
transform: Schema.withConstructorDefault<Schema.Struct<{
|
|
87
|
+
readonly a: Schema.Number;
|
|
88
|
+
readonly b: Schema.Number;
|
|
89
|
+
readonly c: Schema.Number;
|
|
90
|
+
readonly d: Schema.Number;
|
|
91
|
+
readonly e: Schema.Number;
|
|
92
|
+
readonly f: Schema.Number;
|
|
93
|
+
}>>;
|
|
94
|
+
children: Schema.withConstructorDefault<Schema.$Array<Schema.String>>;
|
|
95
|
+
};
|
|
96
|
+
type StoredInput = Schema.Struct<typeof fields>["~type.make.in"];
|
|
97
|
+
type GroupInput = Omit<StoredInput, "transform"> & {
|
|
98
|
+
readonly transform?: typeof TransformOperations.Type;
|
|
99
|
+
};
|
|
100
|
+
export declare const Group: Entity.Entity<"shapes/Group", Schema.Struct<{
|
|
101
|
+
x: Schema.withConstructorDefault<Schema.Number>;
|
|
102
|
+
y: Schema.withConstructorDefault<Schema.Number>;
|
|
103
|
+
opacity: Schema.withConstructorDefault<Schema.Number>;
|
|
104
|
+
transform: Schema.withConstructorDefault<Schema.Struct<{
|
|
105
|
+
readonly a: Schema.Number;
|
|
106
|
+
readonly b: Schema.Number;
|
|
107
|
+
readonly c: Schema.Number;
|
|
108
|
+
readonly d: Schema.Number;
|
|
109
|
+
readonly e: Schema.Number;
|
|
110
|
+
readonly f: Schema.Number;
|
|
111
|
+
}>>;
|
|
112
|
+
children: Schema.withConstructorDefault<Schema.$Array<Schema.String>>;
|
|
113
|
+
}>, {
|
|
114
|
+
"~position": Entity.TraitLens<Schema.Struct.ReadonlySide<{
|
|
115
|
+
x: Schema.withConstructorDefault<Schema.Number>;
|
|
116
|
+
y: Schema.withConstructorDefault<Schema.Number>;
|
|
117
|
+
opacity: Schema.withConstructorDefault<Schema.Number>;
|
|
118
|
+
transform: Schema.withConstructorDefault<Schema.Struct<{
|
|
119
|
+
readonly a: Schema.Number;
|
|
120
|
+
readonly b: Schema.Number;
|
|
121
|
+
readonly c: Schema.Number;
|
|
122
|
+
readonly d: Schema.Number;
|
|
123
|
+
readonly e: Schema.Number;
|
|
124
|
+
readonly f: Schema.Number;
|
|
125
|
+
}>>;
|
|
126
|
+
children: Schema.withConstructorDefault<Schema.$Array<Schema.String>>;
|
|
127
|
+
}, "Type">, Entity.Position>;
|
|
128
|
+
"~opacity": Entity.TraitLens<Schema.Struct.ReadonlySide<{
|
|
129
|
+
x: Schema.withConstructorDefault<Schema.Number>;
|
|
130
|
+
y: Schema.withConstructorDefault<Schema.Number>;
|
|
131
|
+
opacity: Schema.withConstructorDefault<Schema.Number>;
|
|
132
|
+
transform: Schema.withConstructorDefault<Schema.Struct<{
|
|
133
|
+
readonly a: Schema.Number;
|
|
134
|
+
readonly b: Schema.Number;
|
|
135
|
+
readonly c: Schema.Number;
|
|
136
|
+
readonly d: Schema.Number;
|
|
137
|
+
readonly e: Schema.Number;
|
|
138
|
+
readonly f: Schema.Number;
|
|
139
|
+
}>>;
|
|
140
|
+
children: Schema.withConstructorDefault<Schema.$Array<Schema.String>>;
|
|
141
|
+
}, "Type">, number>;
|
|
142
|
+
}, GroupInput>;
|
|
143
|
+
/** The group's x/y position composed outside its normalized local transform. */
|
|
144
|
+
export declare const resolvedTransform: (data: {
|
|
145
|
+
readonly x: number;
|
|
146
|
+
readonly y: number;
|
|
147
|
+
readonly transform: TransformMatrix;
|
|
148
|
+
}) => TransformMatrix;
|
|
149
|
+
export declare const isIdentityTransform: (matrix: TransformMatrix) => boolean;
|
|
150
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import * as SchemaGetter from "effect/SchemaGetter";
|
|
4
|
+
import * as Entity from "../Entity";
|
|
5
|
+
import * as Shape2D from "./Shape2D";
|
|
6
|
+
export const TransformMatrix = Schema.Struct({
|
|
7
|
+
a: Schema.Number,
|
|
8
|
+
b: Schema.Number,
|
|
9
|
+
c: Schema.Number,
|
|
10
|
+
d: Schema.Number,
|
|
11
|
+
e: Schema.Number,
|
|
12
|
+
f: Schema.Number,
|
|
13
|
+
});
|
|
14
|
+
export const identityTransform = {
|
|
15
|
+
a: 1,
|
|
16
|
+
b: 0,
|
|
17
|
+
c: 0,
|
|
18
|
+
d: 1,
|
|
19
|
+
e: 0,
|
|
20
|
+
f: 0,
|
|
21
|
+
};
|
|
22
|
+
export const multiplyTransforms = (left, right) => ({
|
|
23
|
+
a: left.a * right.a + left.c * right.b,
|
|
24
|
+
b: left.b * right.a + left.d * right.b,
|
|
25
|
+
c: left.a * right.c + left.c * right.d,
|
|
26
|
+
d: left.b * right.c + left.d * right.d,
|
|
27
|
+
e: left.a * right.e + left.c * right.f + left.e,
|
|
28
|
+
f: left.b * right.e + left.d * right.f + left.f,
|
|
29
|
+
});
|
|
30
|
+
export const TransformOperation = Schema.TaggedUnion({
|
|
31
|
+
"transform/translate": { x: Schema.Number, y: Schema.Number },
|
|
32
|
+
"transform/scale": { x: Schema.Number, y: Schema.Number },
|
|
33
|
+
"transform/matrix": TransformMatrix.fields,
|
|
34
|
+
});
|
|
35
|
+
const TransformOperations = Schema.Array(TransformOperation);
|
|
36
|
+
const operationMatrix = (operation) => TransformOperation.match(operation, {
|
|
37
|
+
"transform/translate": ({ x, y }) => ({
|
|
38
|
+
...identityTransform,
|
|
39
|
+
e: x,
|
|
40
|
+
f: y,
|
|
41
|
+
}),
|
|
42
|
+
"transform/scale": ({ x, y }) => ({
|
|
43
|
+
...identityTransform,
|
|
44
|
+
a: x,
|
|
45
|
+
d: y,
|
|
46
|
+
}),
|
|
47
|
+
"transform/matrix": ({ a, b, c, d, e, f }) => ({ a, b, c, d, e, f }),
|
|
48
|
+
});
|
|
49
|
+
export const Transform = TransformOperations.pipe(Schema.decodeTo(TransformMatrix, {
|
|
50
|
+
// Post-multiplication preserves the authored transform-list order.
|
|
51
|
+
decode: SchemaGetter.transform((operations) => operations.reduce((matrix, operation) => multiplyTransforms(matrix, operationMatrix(operation)), identityTransform)),
|
|
52
|
+
encode: SchemaGetter.transform((matrix) => [
|
|
53
|
+
{ _tag: "transform/matrix", ...matrix },
|
|
54
|
+
]),
|
|
55
|
+
}));
|
|
56
|
+
const fields = {
|
|
57
|
+
...Shape2D.position,
|
|
58
|
+
...Shape2D.opacity,
|
|
59
|
+
transform: TransformMatrix.pipe(Schema.withConstructorDefault(Effect.succeed(identityTransform))),
|
|
60
|
+
children: Schema.Array(Schema.String).pipe(Schema.withConstructorDefault(Effect.sync(() => []))),
|
|
61
|
+
};
|
|
62
|
+
const decodeTransform = Schema.decodeUnknownSync(Transform);
|
|
63
|
+
const normalizeInput = (input) => Array.isArray(input.transform)
|
|
64
|
+
? { ...input, transform: decodeTransform(input.transform) }
|
|
65
|
+
: input;
|
|
66
|
+
// A container: positions and structures its children, paints nothing.
|
|
67
|
+
// Transform operations are normalized once to a target-independent affine
|
|
68
|
+
// matrix; render targets never interpret the operation list.
|
|
69
|
+
const traits = {
|
|
70
|
+
// moving a group moves the subtree (children keep local coordinates)
|
|
71
|
+
"~position": Shape2D.positionLens(),
|
|
72
|
+
"~opacity": Shape2D.opacityLens(),
|
|
73
|
+
};
|
|
74
|
+
export const Group = Entity.make("shapes/Group", fields, traits, { normalize: normalizeInput });
|
|
75
|
+
/** The group's x/y position composed outside its normalized local transform. */
|
|
76
|
+
export const resolvedTransform = (data) => multiplyTransforms({ ...identityTransform, e: data.x, f: data.y }, data.transform);
|
|
77
|
+
export const isIdentityTransform = (matrix) => matrix.a === 1 &&
|
|
78
|
+
matrix.b === 0 &&
|
|
79
|
+
matrix.c === 0 &&
|
|
80
|
+
matrix.d === 1 &&
|
|
81
|
+
matrix.e === 0 &&
|
|
82
|
+
matrix.f === 0;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import * as Entity from "../Entity";
|
|
3
|
+
export declare const Layer: Entity.Entity<"shapes/Layer", Schema.Struct<{
|
|
4
|
+
depth: Schema.withConstructorDefault<Schema.Number>;
|
|
5
|
+
children: Schema.withConstructorDefault<Schema.$Array<Schema.String>>;
|
|
6
|
+
}>, {}, {
|
|
7
|
+
readonly depth?: number;
|
|
8
|
+
readonly children?: readonly string[];
|
|
9
|
+
}>;
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
// A parallax layer: a container that holds children at a camera `depth`, and
|
|
6
|
+
// nothing else. Unlike Group it carries NO position or opacity of its own —
|
|
7
|
+
// `depth` is the only field, so it can never collide with the transform
|
|
8
|
+
// semantics a Group is expected to grow (position now, rotation/scale later),
|
|
9
|
+
// and a future guard can restrict Layers without touching Group.
|
|
10
|
+
//
|
|
11
|
+
// `depth` is the fraction of the camera this layer feels (pan AND zoom): 1 =
|
|
12
|
+
// full camera (default), 0 = pinned to the screen (a HUD), between = parallax.
|
|
13
|
+
// The sink reads it off each top-level layer (see svg/camera.ts).
|
|
14
|
+
//
|
|
15
|
+
// ponytail: a Layer nested inside another Layer is undefined behavior — the
|
|
16
|
+
// point of a dedicated entity is to leave room for a rule here, but the
|
|
17
|
+
// semantics ("a depth inside a depth"?) aren't decided yet, so no guard ships.
|
|
18
|
+
// Add one in Runner.instantiate (dispatch on entity.name === Layer.name) once
|
|
19
|
+
// nesting semantics are settled.
|
|
20
|
+
export const Layer = Entity.make("shapes/Layer", {
|
|
21
|
+
depth: Shape2D.defaultedNumber(1),
|
|
22
|
+
children: Schema.Array(Schema.String).pipe(Schema.withConstructorDefault(Effect.sync(() => []))),
|
|
23
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import * as Entity from "../Entity";
|
|
3
|
+
declare const fields: {
|
|
4
|
+
x: Schema.withConstructorDefault<Schema.Number>;
|
|
5
|
+
y: Schema.withConstructorDefault<Schema.Number>;
|
|
6
|
+
opacity: Schema.withConstructorDefault<Schema.Number>;
|
|
7
|
+
x2: Schema.withConstructorDefault<Schema.Number>;
|
|
8
|
+
y2: Schema.withConstructorDefault<Schema.Number>;
|
|
9
|
+
stroke: Schema.withConstructorDefault<Schema.String>;
|
|
10
|
+
strokeWidth: Schema.withConstructorDefault<Schema.Number>;
|
|
11
|
+
};
|
|
12
|
+
type LineData = Schema.Struct<typeof fields>["Type"];
|
|
13
|
+
export declare const Line: Entity.Entity<"shapes/Line", Schema.Struct<{
|
|
14
|
+
x: Schema.withConstructorDefault<Schema.Number>;
|
|
15
|
+
y: Schema.withConstructorDefault<Schema.Number>;
|
|
16
|
+
opacity: Schema.withConstructorDefault<Schema.Number>;
|
|
17
|
+
x2: Schema.withConstructorDefault<Schema.Number>;
|
|
18
|
+
y2: Schema.withConstructorDefault<Schema.Number>;
|
|
19
|
+
stroke: Schema.withConstructorDefault<Schema.String>;
|
|
20
|
+
strokeWidth: Schema.withConstructorDefault<Schema.Number>;
|
|
21
|
+
}>, {
|
|
22
|
+
readonly "~position": {
|
|
23
|
+
get: (data: LineData) => {
|
|
24
|
+
x: number;
|
|
25
|
+
y: number;
|
|
26
|
+
};
|
|
27
|
+
set: (data: LineData, value: Entity.Position) => LineData;
|
|
28
|
+
};
|
|
29
|
+
readonly "~opacity": Entity.TraitLens<Schema.Struct.ReadonlySide<{
|
|
30
|
+
x: Schema.withConstructorDefault<Schema.Number>;
|
|
31
|
+
y: Schema.withConstructorDefault<Schema.Number>;
|
|
32
|
+
opacity: Schema.withConstructorDefault<Schema.Number>;
|
|
33
|
+
x2: Schema.withConstructorDefault<Schema.Number>;
|
|
34
|
+
y2: Schema.withConstructorDefault<Schema.Number>;
|
|
35
|
+
stroke: Schema.withConstructorDefault<Schema.String>;
|
|
36
|
+
strokeWidth: Schema.withConstructorDefault<Schema.Number>;
|
|
37
|
+
}, "Type">, number>;
|
|
38
|
+
}, {
|
|
39
|
+
readonly x?: number;
|
|
40
|
+
readonly y?: number;
|
|
41
|
+
readonly opacity?: number;
|
|
42
|
+
readonly x2?: number;
|
|
43
|
+
readonly y2?: number;
|
|
44
|
+
readonly stroke?: string;
|
|
45
|
+
readonly strokeWidth?: number;
|
|
46
|
+
}>;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
// No fill — a line is unfillable, so unlike the filled shapes it defaults
|
|
6
|
+
// stroke white / strokeWidth 1: the visible default (see Shape2D.ts).
|
|
7
|
+
// x/y is the start point, x2/y2 the end point.
|
|
8
|
+
const fields = {
|
|
9
|
+
...Shape2D.position,
|
|
10
|
+
x2: Shape2D.defaultedNumber(0),
|
|
11
|
+
y2: Shape2D.defaultedNumber(0),
|
|
12
|
+
stroke: Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed("white"))),
|
|
13
|
+
strokeWidth: Shape2D.defaultedNumber(1),
|
|
14
|
+
...Shape2D.opacity,
|
|
15
|
+
};
|
|
16
|
+
export const Line = Entity.make("shapes/Line", fields, {
|
|
17
|
+
// position moves the WHOLE line: translating x/y alone would stretch
|
|
18
|
+
// it, so set shifts both endpoints by the delta (get = start point)
|
|
19
|
+
"~position": {
|
|
20
|
+
get: (data) => ({ x: data.x, y: data.y }),
|
|
21
|
+
set: (data, value) => ({
|
|
22
|
+
...data,
|
|
23
|
+
x: value.x,
|
|
24
|
+
y: value.y,
|
|
25
|
+
x2: data.x2 + (value.x - data.x),
|
|
26
|
+
y2: data.y2 + (value.y - data.y),
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
"~opacity": Shape2D.opacityLens(),
|
|
30
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import * as Entity from "../Entity";
|
|
3
|
+
export declare const Path: Entity.Entity<"shapes/Path", Schema.Struct<{
|
|
4
|
+
x: Schema.withConstructorDefault<Schema.Number>;
|
|
5
|
+
y: Schema.withConstructorDefault<Schema.Number>;
|
|
6
|
+
opacity: Schema.withConstructorDefault<Schema.Number>;
|
|
7
|
+
fill: Schema.withConstructorDefault<Schema.String>;
|
|
8
|
+
stroke: Schema.optionalKey<Schema.String>;
|
|
9
|
+
strokeWidth: Schema.optionalKey<Schema.Number>;
|
|
10
|
+
d: Schema.String;
|
|
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 d: string;
|
|
20
|
+
}, Entity.Position>;
|
|
21
|
+
readonly "~opacity": Entity.TraitLens<{
|
|
22
|
+
readonly x: number;
|
|
23
|
+
readonly y: number;
|
|
24
|
+
readonly opacity: number;
|
|
25
|
+
readonly fill: string;
|
|
26
|
+
readonly stroke?: string;
|
|
27
|
+
readonly strokeWidth?: number;
|
|
28
|
+
readonly d: string;
|
|
29
|
+
}, number>;
|
|
30
|
+
}, {
|
|
31
|
+
readonly x?: number;
|
|
32
|
+
readonly y?: number;
|
|
33
|
+
readonly opacity?: number;
|
|
34
|
+
readonly fill?: string;
|
|
35
|
+
readonly stroke?: string;
|
|
36
|
+
readonly strokeWidth?: number;
|
|
37
|
+
readonly d: string;
|
|
38
|
+
}>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import * as Entity from "../Entity";
|
|
3
|
+
import * as Shape2D from "./Shape2D";
|
|
4
|
+
// `d` is required — an empty path can never be visible, so there is no
|
|
5
|
+
// sensible default. x/y offset the whole path (targets translate it),
|
|
6
|
+
// keeping position animatable without rewriting `d`.
|
|
7
|
+
export const Path = Entity.make("shapes/Path", {
|
|
8
|
+
...Shape2D.filled,
|
|
9
|
+
d: Schema.String,
|
|
10
|
+
}, {
|
|
11
|
+
"~position": Shape2D.positionLens(),
|
|
12
|
+
"~opacity": Shape2D.opacityLens(),
|
|
13
|
+
});
|