bireactive 0.2.0 → 0.2.1
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 +4 -4
- package/dist/{core/anim.d.ts → animation/animators.d.ts} +5 -3
- package/dist/{core/anim.js → animation/animators.js} +8 -5
- package/dist/animation/index.d.ts +1 -0
- package/dist/animation/index.js +1 -0
- package/dist/constraints/cluster.js +1 -1
- package/dist/constraints/expose.d.ts +17 -0
- package/dist/constraints/expose.js +61 -0
- package/dist/constraints/index.d.ts +1 -0
- package/dist/constraints/index.js +1 -0
- package/dist/constraints/linalg.d.ts +1 -8
- package/dist/constraints/linalg.js +5 -108
- package/dist/core/{signal.d.ts → cell.d.ts} +29 -6
- package/dist/core/{signal.js → cell.js} +134 -53
- package/dist/core/derived-geometry.d.ts +11 -0
- package/dist/core/derived-geometry.js +32 -0
- package/dist/core/index.d.ts +5 -10
- package/dist/core/index.js +5 -10
- package/dist/core/{aggregates.d.ts → lenses/aggregates.d.ts} +3 -14
- package/dist/core/{aggregates.js → lenses/aggregates.js} +5 -78
- package/dist/core/lenses/closed-form-policies.d.ts +6 -13
- package/dist/core/lenses/closed-form-policies.js +14 -24
- package/dist/core/lenses/decompositions.d.ts +14 -0
- package/dist/core/lenses/decompositions.js +224 -0
- package/dist/core/lenses/domain-aggregates.d.ts +10 -22
- package/dist/core/lenses/domain-aggregates.js +25 -39
- package/dist/core/{new-primitives.d.ts → lenses/geometry.d.ts} +11 -14
- package/dist/core/{new-primitives.js → lenses/geometry.js} +23 -37
- package/dist/core/lenses/index.d.ts +6 -4
- package/dist/core/lenses/index.js +6 -15
- package/dist/core/lenses/typed-factor.d.ts +1 -6
- package/dist/core/lenses/typed-factor.js +12 -114
- package/dist/core/{network-utils.d.ts → lifecycle.d.ts} +1 -1
- package/dist/core/{network-utils.js → lifecycle.js} +2 -2
- package/dist/core/linalg.d.ts +10 -0
- package/dist/core/linalg.js +109 -0
- package/dist/core/values/anchor.d.ts +1 -1
- package/dist/core/values/audio.d.ts +1 -1
- package/dist/core/values/audio.js +1 -1
- package/dist/core/values/bool.d.ts +1 -1
- package/dist/core/values/bool.js +1 -1
- package/dist/core/values/box.d.ts +2 -3
- package/dist/core/values/box.js +5 -6
- package/dist/core/values/canvas.d.ts +1 -1
- package/dist/core/values/canvas.js +2 -3
- package/dist/core/values/color.d.ts +2 -3
- package/dist/core/values/color.js +3 -4
- package/dist/core/values/flags.d.ts +1 -1
- package/dist/core/values/flags.js +1 -1
- package/dist/core/values/matrix.d.ts +1 -1
- package/dist/core/values/matrix.js +2 -3
- package/dist/core/values/num.d.ts +2 -3
- package/dist/core/values/num.js +2 -2
- package/dist/core/values/pose.d.ts +5 -1
- package/dist/core/values/pose.js +11 -1
- package/dist/core/values/range.d.ts +6 -4
- package/dist/core/values/range.js +16 -11
- package/dist/core/values/str.d.ts +1 -1
- package/dist/core/values/str.js +1 -1
- package/dist/core/values/template.d.ts +1 -1
- package/dist/core/values/transform.d.ts +2 -3
- package/dist/core/values/transform.js +2 -3
- package/dist/core/values/tri.d.ts +1 -1
- package/dist/core/values/tri.js +1 -1
- package/dist/core/values/vec.d.ts +2 -3
- package/dist/core/values/vec.js +4 -5
- package/dist/ext/timeline.js +3 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/propagators/layout.js +1 -1
- package/dist/shapes/handle.js +4 -4
- package/dist/shapes/shape.js +7 -7
- package/dist/shapes/transitions.js +2 -2
- package/package.json +7 -2
- package/dist/core/introspect.d.ts +0 -5
- package/dist/core/introspect.js +0 -31
- package/dist/core/lenses/factor-lens.d.ts +0 -42
- package/dist/core/lenses/factor-lens.js +0 -419
- package/dist/core/writable.d.ts +0 -15
- package/dist/core/writable.js +0 -29
- /package/dist/{core/tree.d.ts → tree.d.ts} +0 -0
- /package/dist/{core/tree.js → tree.js} +0 -0
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
// line/circle, and PCA decompose into combinations of them.
|
|
7
7
|
//
|
|
8
8
|
// Layout: building-block actions (rigidTranslate, rotateAbout,
|
|
9
|
-
// scaleAbout, scaleAboutXY),
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
import {
|
|
9
|
+
// scaleAbout, scaleAboutXY), then closed-form decompositions
|
|
10
|
+
// (bestFitLine, bestFitCircle, pca, total). All exact, idempotent,
|
|
11
|
+
// cross-channel invariant by construction, on the same `Cls.lens`
|
|
12
|
+
// machinery — no engine changes.
|
|
13
|
+
import { mean, Num, Vec, } from "../index.js";
|
|
14
14
|
import { continuous, remember } from "./memory.js";
|
|
15
15
|
// Pivotal trait lookup via the value class's `static traits.pivotal` slot.
|
|
16
16
|
// biome-ignore lint/suspicious/noExplicitAny: dynamic trait lookup
|
|
@@ -25,9 +25,9 @@ function pivotalOf(input) {
|
|
|
25
25
|
return p;
|
|
26
26
|
}
|
|
27
27
|
/** Writable centroid; on write, translates every point by the delta.
|
|
28
|
-
*
|
|
28
|
+
* The Vec-specific group-action reading of `mean`. */
|
|
29
29
|
export function rigidTranslate(points) {
|
|
30
|
-
return
|
|
30
|
+
return mean(points);
|
|
31
31
|
}
|
|
32
32
|
/** Writable angle from `pivot` to `points[0]`; write rotates every input
|
|
33
33
|
* about `pivot` by (target − current) via its `Pivotal` trait.
|
|
@@ -35,7 +35,7 @@ export function rigidTranslate(points) {
|
|
|
35
35
|
* Trait-generic: Vec rotates position; Pose rotates position AND
|
|
36
36
|
* orientation. Rotation-about-pivot fixes the pivot and preserves radial
|
|
37
37
|
* distances, so scale-about-pivot reads unchanged. `pivot` is reactive
|
|
38
|
-
* (re-read per write); pass `
|
|
38
|
+
* (re-read per write); pass `rigidTranslate(points)` for rotation about
|
|
39
39
|
* the cluster's own centroid. */
|
|
40
40
|
export function rotateAbout(points, pivot) {
|
|
41
41
|
const K = points.length;
|
|
@@ -153,16 +153,6 @@ export function scaleAboutXY(points, pivot) {
|
|
|
153
153
|
},
|
|
154
154
|
});
|
|
155
155
|
}
|
|
156
|
-
/** Same semantics as `factor-lens.ts`'s `procrustesLens`, decomposed
|
|
157
|
-
* into three building-block lenses sharing a centroid. */
|
|
158
|
-
export function procrustesViaBuildingBlocks(points) {
|
|
159
|
-
if (points.length < 2)
|
|
160
|
-
throw new Error("procrustes: need ≥ 2 points");
|
|
161
|
-
const centroid = rigidTranslate(points);
|
|
162
|
-
const rotation = rotateAbout(points, centroid);
|
|
163
|
-
const scale = scaleAbout(points, centroid);
|
|
164
|
-
return { centroid, rotation, scale };
|
|
165
|
-
}
|
|
166
156
|
// Best-fit line.
|
|
167
157
|
//
|
|
168
158
|
// K points → {point: centroid, direction: principal-axis angle}.
|
|
@@ -188,7 +178,7 @@ function covariance(points, cx, cy) {
|
|
|
188
178
|
}
|
|
189
179
|
return { cxx: cxx / K, cxy: cxy / K, cyy: cyy / K };
|
|
190
180
|
}
|
|
191
|
-
export function
|
|
181
|
+
export function bestFitLine(points) {
|
|
192
182
|
const K = points.length;
|
|
193
183
|
if (K < 2)
|
|
194
184
|
throw new Error("bestFitLine: need ≥ 2 points");
|
|
@@ -242,7 +232,7 @@ export function bestFitLineLens(points) {
|
|
|
242
232
|
// write radius → scale all about center by target/current
|
|
243
233
|
// Simplest closed-form fit (mean center). Invariance: translation
|
|
244
234
|
// preserves radii; uniform scale-about-center preserves the center.
|
|
245
|
-
export function
|
|
235
|
+
export function bestFitCircle(points) {
|
|
246
236
|
const K = points.length;
|
|
247
237
|
if (K < 1)
|
|
248
238
|
throw new Error("bestFitCircle: need ≥ 1 point");
|
|
@@ -280,10 +270,10 @@ export function bestFitCircleLens(points) {
|
|
|
280
270
|
// write major/minor → scale along that axis by target/current
|
|
281
271
|
// Each write is a single group action; cross-channel invariance holds
|
|
282
272
|
// for all pairs.
|
|
283
|
-
export function
|
|
273
|
+
export function pca(points) {
|
|
284
274
|
const K = points.length;
|
|
285
275
|
if (K < 2)
|
|
286
|
-
throw new Error("
|
|
276
|
+
throw new Error("pca: need ≥ 2 points");
|
|
287
277
|
const mean = rigidTranslate(points);
|
|
288
278
|
// 2×2 symmetric eigendecomp → {θ, λ_major, λ_minor}; null when fully
|
|
289
279
|
// collapsed (λ_major ≈ 0).
|
|
@@ -430,10 +420,10 @@ export function pcaLens(points) {
|
|
|
430
420
|
* preserving their ratios. A `remember` anchored at zero with a signed
|
|
431
421
|
* sum feature: a collapse to zero reinflates the stored ratios, seeded
|
|
432
422
|
* uniform so an all-zero start splits evenly. */
|
|
433
|
-
export function
|
|
423
|
+
export function total(parts) {
|
|
434
424
|
const K = parts.length;
|
|
435
425
|
if (K < 1)
|
|
436
|
-
throw new Error("
|
|
426
|
+
throw new Error("total: need ≥ 1 part");
|
|
437
427
|
return remember(parts, {
|
|
438
428
|
anchor: () => 0,
|
|
439
429
|
feature: (vals) => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Num, Vec, type Writable } from "../index.js";
|
|
2
|
+
export declare function meanDiff(a: Num, b: Num): {
|
|
3
|
+
mean: Writable<Num>;
|
|
4
|
+
diff: Writable<Num>;
|
|
5
|
+
};
|
|
6
|
+
export declare function procrustes(points: readonly Writable<Vec>[]): {
|
|
7
|
+
centroid: Writable<Vec>;
|
|
8
|
+
rotation: Writable<Num>;
|
|
9
|
+
scale: Writable<Num>;
|
|
10
|
+
};
|
|
11
|
+
export declare function bbox(points: readonly Writable<Vec>[]): {
|
|
12
|
+
center: Writable<Vec>;
|
|
13
|
+
size: Writable<Vec>;
|
|
14
|
+
};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// decompositions.ts — closed-form N→M lens decompositions (Vec/Num).
|
|
2
|
+
//
|
|
3
|
+
// N inputs → M coupled writable outputs, where writing one output
|
|
4
|
+
// preserves the readings of the other M−1 (cross-channel invariance).
|
|
5
|
+
// Each bwd is a hand-rolled group action (translate / rotate / scale
|
|
6
|
+
// about the centroid), so cross-channel invariance is EXACT and one
|
|
7
|
+
// write lands in O(K). For the generic numerical N→M escape hatch (when
|
|
8
|
+
// no closed form fits) see `factor` in `typed-factor.ts`.
|
|
9
|
+
import { Num, Vec } from "../index.js";
|
|
10
|
+
// meanDiff — M=2 isomorphism baseline.
|
|
11
|
+
//
|
|
12
|
+
// (a, b) → ((a+b)/2, a−b). Square full-rank linear lens; bwd is the
|
|
13
|
+
// inverse change of basis — exact, cross-channel invariant.
|
|
14
|
+
export function meanDiff(a, b) {
|
|
15
|
+
const mean = Num.lens([a, b], vals => (vals[0] + vals[1]) / 2, (target, vals) => {
|
|
16
|
+
const d = vals[0] - vals[1];
|
|
17
|
+
return [target + d / 2, target - d / 2];
|
|
18
|
+
});
|
|
19
|
+
const diff = Num.lens([a, b], vals => vals[0] - vals[1], (target, vals) => {
|
|
20
|
+
const m = (vals[0] + vals[1]) / 2;
|
|
21
|
+
return [m + target / 2, m - target / 2];
|
|
22
|
+
});
|
|
23
|
+
return { mean, diff };
|
|
24
|
+
}
|
|
25
|
+
// procrustes — closed-form similarity (the showcase).
|
|
26
|
+
//
|
|
27
|
+
// K writable Vecs → {centroid, rotation (angle of point[0] about
|
|
28
|
+
// centroid), scale (its distance from centroid)}. Each bwd is a
|
|
29
|
+
// closed-form transform about the centroid:
|
|
30
|
+
// write centroid → translate every point by (c − old c)
|
|
31
|
+
// write rotation → rotate every point about centroid by (θ − old θ)
|
|
32
|
+
// write scale → scale every point about centroid by (s / old s)
|
|
33
|
+
// These commute on the cluster's similarity orbit, so the three outputs
|
|
34
|
+
// have EXACT cross-channel invariance. Degenerate: K < 2 leaves
|
|
35
|
+
// rotation/scale undefined; a collapsed cluster (scale → 0) makes
|
|
36
|
+
// rotation singular and scale writes no-ops; target scale = 0 collapses
|
|
37
|
+
// to the centroid.
|
|
38
|
+
export function procrustes(points) {
|
|
39
|
+
const K = points.length;
|
|
40
|
+
if (K < 2)
|
|
41
|
+
throw new Error("procrustes: need ≥ 2 points");
|
|
42
|
+
const centroid = Vec.lens(points, (vals) => {
|
|
43
|
+
let sx = 0;
|
|
44
|
+
let sy = 0;
|
|
45
|
+
for (let i = 0; i < K; i++) {
|
|
46
|
+
sx += vals[i].x;
|
|
47
|
+
sy += vals[i].y;
|
|
48
|
+
}
|
|
49
|
+
return { x: sx / K, y: sy / K };
|
|
50
|
+
}, (target, vals) => {
|
|
51
|
+
let sx = 0;
|
|
52
|
+
let sy = 0;
|
|
53
|
+
for (let i = 0; i < K; i++) {
|
|
54
|
+
sx += vals[i].x;
|
|
55
|
+
sy += vals[i].y;
|
|
56
|
+
}
|
|
57
|
+
const dx = target.x - sx / K;
|
|
58
|
+
const dy = target.y - sy / K;
|
|
59
|
+
const out = new Array(K);
|
|
60
|
+
for (let i = 0; i < K; i++)
|
|
61
|
+
out[i] = { x: vals[i].x + dx, y: vals[i].y + dy };
|
|
62
|
+
return out;
|
|
63
|
+
});
|
|
64
|
+
const rotation = Num.lens(points, (vals) => {
|
|
65
|
+
let sx = 0;
|
|
66
|
+
let sy = 0;
|
|
67
|
+
for (let i = 0; i < K; i++) {
|
|
68
|
+
sx += vals[i].x;
|
|
69
|
+
sy += vals[i].y;
|
|
70
|
+
}
|
|
71
|
+
const cx = sx / K;
|
|
72
|
+
const cy = sy / K;
|
|
73
|
+
return Math.atan2(vals[0].y - cy, vals[0].x - cx);
|
|
74
|
+
}, (target, vals) => {
|
|
75
|
+
let sx = 0;
|
|
76
|
+
let sy = 0;
|
|
77
|
+
for (let i = 0; i < K; i++) {
|
|
78
|
+
sx += vals[i].x;
|
|
79
|
+
sy += vals[i].y;
|
|
80
|
+
}
|
|
81
|
+
const cx = sx / K;
|
|
82
|
+
const cy = sy / K;
|
|
83
|
+
const rx0 = vals[0].x - cx;
|
|
84
|
+
const ry0 = vals[0].y - cy;
|
|
85
|
+
if (rx0 * rx0 + ry0 * ry0 < 1e-24) {
|
|
86
|
+
// Collapsed cluster; no angle to rotate from.
|
|
87
|
+
return vals.map(() => undefined);
|
|
88
|
+
}
|
|
89
|
+
const oldθ = Math.atan2(ry0, rx0);
|
|
90
|
+
const dθ = target - oldθ;
|
|
91
|
+
const cos = Math.cos(dθ);
|
|
92
|
+
const sin = Math.sin(dθ);
|
|
93
|
+
const out = new Array(K);
|
|
94
|
+
for (let i = 0; i < K; i++) {
|
|
95
|
+
const rx = vals[i].x - cx;
|
|
96
|
+
const ry = vals[i].y - cy;
|
|
97
|
+
out[i] = { x: cx + cos * rx - sin * ry, y: cy + sin * rx + cos * ry };
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
});
|
|
101
|
+
const centroidOf = (vals) => {
|
|
102
|
+
let sx = 0;
|
|
103
|
+
let sy = 0;
|
|
104
|
+
for (let i = 0; i < K; i++) {
|
|
105
|
+
sx += vals[i].x;
|
|
106
|
+
sy += vals[i].y;
|
|
107
|
+
}
|
|
108
|
+
return { x: sx / K, y: sy / K };
|
|
109
|
+
};
|
|
110
|
+
const refreshDevs = (devs, vals) => {
|
|
111
|
+
const c = centroidOf(vals);
|
|
112
|
+
return devs.map((d, i) => {
|
|
113
|
+
const dx = vals[i].x - c.x;
|
|
114
|
+
const dy = vals[i].y - c.y;
|
|
115
|
+
return dx * dx + dy * dy > 1e-18 ? { x: dx, y: dy } : d;
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
const scale = Num.lens(points, {
|
|
119
|
+
init: (vals) => {
|
|
120
|
+
const c = centroidOf(vals);
|
|
121
|
+
return { devs: vals.map(v => ({ x: v.x - c.x, y: v.y - c.y })) };
|
|
122
|
+
},
|
|
123
|
+
step: (vals, c) => ({ devs: refreshDevs(c.devs, vals) }),
|
|
124
|
+
fwd: (vals) => {
|
|
125
|
+
const c = centroidOf(vals);
|
|
126
|
+
return Math.hypot(vals[0].x - c.x, vals[0].y - c.y);
|
|
127
|
+
},
|
|
128
|
+
bwd: (target, vals, c) => {
|
|
129
|
+
const cen = centroidOf(vals);
|
|
130
|
+
const d0 = c.devs[0];
|
|
131
|
+
const r0 = Math.hypot(d0.x, d0.y);
|
|
132
|
+
if (r0 < 1e-12)
|
|
133
|
+
return { updates: vals.map(() => undefined), complement: c };
|
|
134
|
+
const k = target / r0;
|
|
135
|
+
const out = c.devs.map(d => ({ x: cen.x + k * d.x, y: cen.y + k * d.y }));
|
|
136
|
+
return { updates: out, complement: c };
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
return { centroid, rotation, scale };
|
|
140
|
+
}
|
|
141
|
+
// bbox — closed-form axis-aligned bounding box.
|
|
142
|
+
//
|
|
143
|
+
// K Vecs → {center, size}. Forward is min/max (piecewise-constant
|
|
144
|
+
// Jacobian — fatal for FD), but the closed-form bwd is exact:
|
|
145
|
+
// write center → translate all points by (c − old c)
|
|
146
|
+
// write size → scale all about center by component-wise ratio
|
|
147
|
+
// Center↔size invariance is exact. Degenerate axes (size = 0) write
|
|
148
|
+
// as no-ops; negative size reflects (kept permissive).
|
|
149
|
+
export function bbox(points) {
|
|
150
|
+
const K = points.length;
|
|
151
|
+
if (K < 1)
|
|
152
|
+
throw new Error("bbox: need ≥ 1 point");
|
|
153
|
+
const computeBox = (vals) => {
|
|
154
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
155
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
156
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
157
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
158
|
+
for (let i = 0; i < K; i++) {
|
|
159
|
+
const x = vals[i].x;
|
|
160
|
+
const y = vals[i].y;
|
|
161
|
+
if (x < minX)
|
|
162
|
+
minX = x;
|
|
163
|
+
if (x > maxX)
|
|
164
|
+
maxX = x;
|
|
165
|
+
if (y < minY)
|
|
166
|
+
minY = y;
|
|
167
|
+
if (y > maxY)
|
|
168
|
+
maxY = y;
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
cx: (minX + maxX) / 2,
|
|
172
|
+
cy: (minY + maxY) / 2,
|
|
173
|
+
sx: maxX - minX,
|
|
174
|
+
sy: maxY - minY,
|
|
175
|
+
};
|
|
176
|
+
};
|
|
177
|
+
const center = Vec.lens(points, (vals) => {
|
|
178
|
+
const b = computeBox(vals);
|
|
179
|
+
return { x: b.cx, y: b.cy };
|
|
180
|
+
}, (target, vals) => {
|
|
181
|
+
const b = computeBox(vals);
|
|
182
|
+
const dx = target.x - b.cx;
|
|
183
|
+
const dy = target.y - b.cy;
|
|
184
|
+
const out = new Array(K);
|
|
185
|
+
for (let i = 0; i < K; i++)
|
|
186
|
+
out[i] = { x: vals[i].x + dx, y: vals[i].y + dy };
|
|
187
|
+
return out;
|
|
188
|
+
});
|
|
189
|
+
const refreshFracs = (fracs, vals) => {
|
|
190
|
+
const b = computeBox(vals);
|
|
191
|
+
const hx = b.sx > 1e-12 ? b.sx / 2 : 0;
|
|
192
|
+
const hy = b.sy > 1e-12 ? b.sy / 2 : 0;
|
|
193
|
+
return fracs.map((f, i) => ({
|
|
194
|
+
x: hx > 0 ? (vals[i].x - b.cx) / hx : f.x,
|
|
195
|
+
y: hy > 0 ? (vals[i].y - b.cy) / hy : f.y,
|
|
196
|
+
}));
|
|
197
|
+
};
|
|
198
|
+
const size = Vec.lens(points, {
|
|
199
|
+
init: (vals) => {
|
|
200
|
+
const b = computeBox(vals);
|
|
201
|
+
const halfX0 = b.sx > 1e-12 ? b.sx / 2 : 1;
|
|
202
|
+
const halfY0 = b.sy > 1e-12 ? b.sy / 2 : 1;
|
|
203
|
+
return {
|
|
204
|
+
fracs: vals.map(v => ({
|
|
205
|
+
x: b.sx > 1e-12 ? (v.x - b.cx) / halfX0 : 0,
|
|
206
|
+
y: b.sy > 1e-12 ? (v.y - b.cy) / halfY0 : 0,
|
|
207
|
+
})),
|
|
208
|
+
};
|
|
209
|
+
},
|
|
210
|
+
step: (vals, c) => ({ fracs: refreshFracs(c.fracs, vals) }),
|
|
211
|
+
fwd: (vals) => {
|
|
212
|
+
const b = computeBox(vals);
|
|
213
|
+
return { x: b.sx, y: b.sy };
|
|
214
|
+
},
|
|
215
|
+
bwd: (target, vals, c) => {
|
|
216
|
+
const b = computeBox(vals);
|
|
217
|
+
const halfTx = target.x / 2;
|
|
218
|
+
const halfTy = target.y / 2;
|
|
219
|
+
const out = c.fracs.map(f => ({ x: b.cx + f.x * halfTx, y: b.cy + f.y * halfTy }));
|
|
220
|
+
return { updates: out, complement: c };
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
return { center, size };
|
|
224
|
+
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { type Cell, Num, type Read, type Traits, type Val, Vec, type Writable } from "../index.js";
|
|
2
|
-
/**
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* named for the geometric intent. */
|
|
6
|
-
export declare function rigidTranslateOf<S extends Traits<any, "linear">>(inputs: readonly Writable<S>[]): Writable<S>;
|
|
2
|
+
/** Equal-weight mean (writable of `inputs[0]`'s class); writes distribute
|
|
3
|
+
* the delta evenly. Class inferred from the first input; needs `linear`. */
|
|
4
|
+
export declare function mean<S extends Traits<any, "linear">>(inputs: readonly Writable<S>[]): Writable<S>;
|
|
7
5
|
/** Weighted blend of K branches over any `Linear` type. See module note. */
|
|
8
6
|
export declare function mix<S extends Traits<any, "linear">>(weights: readonly Val<number>[], branches: readonly Writable<S>[]): Writable<S>;
|
|
9
7
|
/** Two-branch router (mix simplex *vertex*): reads the live branch, writes
|
|
@@ -13,15 +11,6 @@ export declare function select<S extends Traits<any, "linear">>(cond: Read<boole
|
|
|
13
11
|
/** Two-branch crossfade (mix simplex *edge*): `lerp(a, b, t)`. Writing
|
|
14
12
|
* keeps `t` fixed and splits the delta by influence. */
|
|
15
13
|
export declare function crossfade<S extends Traits<any, "linear">>(t: Read<number>, a: Writable<S>, b: Writable<S>): Writable<S>;
|
|
16
|
-
type ColorV = {
|
|
17
|
-
r: number;
|
|
18
|
-
g: number;
|
|
19
|
-
b: number;
|
|
20
|
-
a: number;
|
|
21
|
-
};
|
|
22
|
-
/** Mean color of a palette; write shifts every color by the delta
|
|
23
|
-
* (rigid translate in RGBA). Via `meanOf`. */
|
|
24
|
-
export declare function meanColor(colors: readonly Writable<Traits<ColorV, "linear">>[]): Writable<Traits<ColorV, "linear">>;
|
|
25
14
|
/** Mean radial distance from the centroid; write scales the cluster's
|
|
26
15
|
* deviations so the new mean matches the target. Trait-driven via
|
|
27
16
|
* `Linear` + `Metric`, so it works for any class declaring both (Vec,
|
|
@@ -32,23 +21,22 @@ export declare function meanColor(colors: readonly Writable<Traits<ColorV, "line
|
|
|
32
21
|
* and a collapse (spread → 0) reinflates the original SHAPE. Centroid is
|
|
33
22
|
* recomputed every read/write, so an intervening mean translate is not
|
|
34
23
|
* stale. */
|
|
35
|
-
export declare function
|
|
36
|
-
/**
|
|
37
|
-
* uniform scale about it. `
|
|
38
|
-
* Linear + Metric class. */
|
|
39
|
-
export declare function
|
|
24
|
+
export declare function spread<T extends NonNullable<unknown>, S extends Cell<T> & Traits<T, "linear" | "metric">>(inputs: readonly Writable<S>[]): Writable<Num>;
|
|
25
|
+
/** Mean/spread decomposition: K values → {mean, spread}, i.e. centroid +
|
|
26
|
+
* uniform scale about it. `mean` ∘ `spread`; works for any
|
|
27
|
+
* Linear + Metric class (palettes, point clouds, poses, …). */
|
|
28
|
+
export declare function meanSpread<T extends NonNullable<unknown>, S extends Cell<T> & Traits<T, "linear" | "metric">>(colors: readonly Writable<S>[]): {
|
|
40
29
|
mean: Writable<S>;
|
|
41
30
|
spread: Writable<Num>;
|
|
42
31
|
};
|
|
43
|
-
export declare function
|
|
32
|
+
export declare function bezierGestalt(p0: Writable<Vec>, p1: Writable<Vec>, p2: Writable<Vec>, p3: Writable<Vec>): {
|
|
44
33
|
start: Writable<Vec>;
|
|
45
34
|
end: Writable<Vec>;
|
|
46
35
|
startTangent: Writable<Vec>;
|
|
47
36
|
endTangent: Writable<Vec>;
|
|
48
37
|
};
|
|
49
38
|
/** Time-series scalar aggregate over Num values as (i, value_i) samples. */
|
|
50
|
-
export declare function
|
|
39
|
+
export declare function timeSeries(values: readonly Writable<Num>[]): {
|
|
51
40
|
mean: Writable<Num>;
|
|
52
41
|
slope: Writable<Num>;
|
|
53
42
|
};
|
|
54
|
-
export {};
|
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
// domain-aggregates.ts — closed-form lenses beyond point clouds.
|
|
2
2
|
//
|
|
3
3
|
// The group-action patterns from `closed-form-policies.ts`, applied to:
|
|
4
|
-
// (1) Generic Linear/Metric-trait aggregates — `
|
|
5
|
-
// `
|
|
6
|
-
// (2)
|
|
7
|
-
// (3)
|
|
8
|
-
// (4) Time-series ({mean, slope}) over (i, value) samples.
|
|
4
|
+
// (1) Generic Linear/Metric-trait aggregates — `mean`, `spread`,
|
|
5
|
+
// `palette` work for colors, poses, ranges, boxes for free.
|
|
6
|
+
// (2) Bezier gestalt handles ({start, end, startTangent, endTangent}).
|
|
7
|
+
// (3) Time-series ({mean, slope}) over (i, value) samples.
|
|
9
8
|
// All exact, idempotent, cross-channel invariant by construction.
|
|
10
9
|
import { Num, reader, Vec, } from "../index.js";
|
|
11
10
|
import { remember } from "./memory.js";
|
|
12
11
|
// Generic Linear-trait aggregates.
|
|
13
12
|
//
|
|
14
|
-
// Ergonomic entry points
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
/**
|
|
13
|
+
// Ergonomic entry points that infer the value class from the first input
|
|
14
|
+
// (`mean(colors)` works for any `linear` class). Same engine, no new
|
|
15
|
+
// infrastructure.
|
|
16
|
+
/** Equal-weight mean (writable of `inputs[0]`'s class); writes distribute
|
|
17
|
+
* the delta evenly. Class inferred from the first input; needs `linear`. */
|
|
18
18
|
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
19
|
-
export function
|
|
19
|
+
export function mean(inputs) {
|
|
20
20
|
if (inputs.length === 0)
|
|
21
|
-
throw new Error("
|
|
21
|
+
throw new Error("mean: need ≥ 1 input");
|
|
22
22
|
// biome-ignore lint/suspicious/noExplicitAny: dynamic class lookup
|
|
23
23
|
const Cls = inputs[0].constructor;
|
|
24
24
|
// biome-ignore lint/suspicious/noExplicitAny: dynamic trait lookup
|
|
25
25
|
const lin = Cls.traits?.linear;
|
|
26
26
|
if (!lin)
|
|
27
|
-
throw new Error(`
|
|
27
|
+
throw new Error(`mean: ${Cls.name ?? "?"} has no traits.linear`);
|
|
28
28
|
const n = inputs.length;
|
|
29
29
|
const inv = 1 / n;
|
|
30
30
|
// biome-ignore lint/suspicious/noExplicitAny: variance escape on Cls.lens
|
|
@@ -49,15 +49,9 @@ export function meanOf(inputs) {
|
|
|
49
49
|
return out;
|
|
50
50
|
});
|
|
51
51
|
}
|
|
52
|
-
/** Rigid-translate aggregate over any Linear type. Alias of `meanOf`,
|
|
53
|
-
* named for the geometric intent. */
|
|
54
|
-
// biome-ignore lint/suspicious/noExplicitAny: variance escape
|
|
55
|
-
export function rigidTranslateOf(inputs) {
|
|
56
|
-
return meanOf(inputs);
|
|
57
|
-
}
|
|
58
52
|
// Weighted blend (the mix simplex).
|
|
59
53
|
//
|
|
60
|
-
// `mix` is `
|
|
54
|
+
// `mix` is `mean` with the uniform-weight assumption lifted: the read
|
|
61
55
|
// is the normalized weighted sum `Σ wᵢ·aᵢ`, the write is the minimum-norm
|
|
62
56
|
// delta `daᵢ = wᵢ·δ / Σwⱼ²` (the pseudoinverse of `wᵀ·da = δ`), so a
|
|
63
57
|
// zero-weight branch is left untouched. Weights are read-only controls —
|
|
@@ -66,7 +60,7 @@ export function rigidTranslateOf(inputs) {
|
|
|
66
60
|
//
|
|
67
61
|
// The control lives on the K-simplex: a one-hot vertex is `select`
|
|
68
62
|
// (the live branch absorbs everything), a `(1−t, t)` edge is `crossfade`,
|
|
69
|
-
// uniform weights recover `
|
|
63
|
+
// uniform weights recover `mean`. Reactive weights are dynamically
|
|
70
64
|
// tracked (read via `.value` inside fwd), so flipping a Bool or sliding a
|
|
71
65
|
// Num re-reads with no extra wiring.
|
|
72
66
|
/** Weighted blend of K branches over any `Linear` type. See module note. */
|
|
@@ -131,11 +125,6 @@ export function select(cond, whenFalse, whenTrue) {
|
|
|
131
125
|
export function crossfade(t, a, b) {
|
|
132
126
|
return mix([Num.derive(() => 1 - t.value), Num.derive(() => t.value)], [a, b]);
|
|
133
127
|
}
|
|
134
|
-
/** Mean color of a palette; write shifts every color by the delta
|
|
135
|
-
* (rigid translate in RGBA). Via `meanOf`. */
|
|
136
|
-
export function meanColor(colors) {
|
|
137
|
-
return meanOf(colors);
|
|
138
|
-
}
|
|
139
128
|
/** Mean radial distance from the centroid; write scales the cluster's
|
|
140
129
|
* deviations so the new mean matches the target. Trait-driven via
|
|
141
130
|
* `Linear` + `Metric`, so it works for any class declaring both (Vec,
|
|
@@ -146,16 +135,16 @@ export function meanColor(colors) {
|
|
|
146
135
|
* and a collapse (spread → 0) reinflates the original SHAPE. Centroid is
|
|
147
136
|
* recomputed every read/write, so an intervening mean translate is not
|
|
148
137
|
* stale. */
|
|
149
|
-
export function
|
|
138
|
+
export function spread(inputs) {
|
|
150
139
|
const K = inputs.length;
|
|
151
140
|
if (K < 1)
|
|
152
|
-
throw new Error("
|
|
141
|
+
throw new Error("spread: need ≥ 1 input");
|
|
153
142
|
// biome-ignore lint/suspicious/noExplicitAny: dynamic class lookup
|
|
154
143
|
const Cls = inputs[0].constructor;
|
|
155
144
|
const lin = Cls.traits?.linear;
|
|
156
145
|
const met = Cls.traits?.metric;
|
|
157
146
|
if (!lin || !met) {
|
|
158
|
-
throw new Error(`
|
|
147
|
+
throw new Error(`spread: ${Cls.name ?? "?"} needs Linear + Metric`);
|
|
159
148
|
}
|
|
160
149
|
const inv = 1 / K;
|
|
161
150
|
const centroid = (vals) => {
|
|
@@ -178,16 +167,16 @@ export function spreadOf(inputs) {
|
|
|
178
167
|
feature: (vals, ctr) => meanSpread(vals, ctr),
|
|
179
168
|
});
|
|
180
169
|
}
|
|
181
|
-
/**
|
|
182
|
-
* uniform scale about it. `
|
|
183
|
-
* Linear + Metric class. */
|
|
184
|
-
export function
|
|
170
|
+
/** Mean/spread decomposition: K values → {mean, spread}, i.e. centroid +
|
|
171
|
+
* uniform scale about it. `mean` ∘ `spread`; works for any
|
|
172
|
+
* Linear + Metric class (palettes, point clouds, poses, …). */
|
|
173
|
+
export function meanSpread(colors) {
|
|
185
174
|
return {
|
|
186
|
-
mean:
|
|
187
|
-
spread:
|
|
175
|
+
mean: mean(colors),
|
|
176
|
+
spread: spread(colors),
|
|
188
177
|
};
|
|
189
178
|
}
|
|
190
|
-
export function
|
|
179
|
+
export function bezierGestalt(p0, p1, p2, p3) {
|
|
191
180
|
const start = Vec.lens([p0, p1], (vals) => vals[0], (target, vals) => {
|
|
192
181
|
const dx = target.x - vals[0].x;
|
|
193
182
|
const dy = target.y - vals[0].y;
|
|
@@ -210,7 +199,7 @@ export function bezierGestaltLens(p0, p1, p2, p3) {
|
|
|
210
199
|
// mean and slope are invariant under each other (a y-shift preserves
|
|
211
200
|
// slope; tilting about the mean preserves the mean).
|
|
212
201
|
/** Time-series scalar aggregate over Num values as (i, value_i) samples. */
|
|
213
|
-
export function
|
|
202
|
+
export function timeSeries(values) {
|
|
214
203
|
const N = values.length;
|
|
215
204
|
if (N < 2)
|
|
216
205
|
throw new Error("timeSeries: need ≥ 2 values");
|
|
@@ -254,6 +243,3 @@ export function timeSeriesLens(values) {
|
|
|
254
243
|
});
|
|
255
244
|
return { mean, slope };
|
|
256
245
|
}
|
|
257
|
-
// `meanOf` / `rigidTranslateOf` / `spreadOf` are fully trait-driven
|
|
258
|
-
// (Linear, Metric); `bezierGestalt` and `timeSeries` stay value-specific
|
|
259
|
-
// (Vec / Num) since their operations don't benefit from the trait layer.
|
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import type { Cell, Writable } from "
|
|
2
|
-
import { Num } from "
|
|
3
|
-
import { Vec } from "
|
|
1
|
+
import type { Cell, Read, Writable } from "../cell.js";
|
|
2
|
+
import { Num } from "../values/num.js";
|
|
3
|
+
import { Vec } from "../values/vec.js";
|
|
4
4
|
type V = {
|
|
5
5
|
x: number;
|
|
6
6
|
y: number;
|
|
7
7
|
};
|
|
8
|
-
/** Distance between two Vecs
|
|
9
|
-
*
|
|
10
|
-
export declare function
|
|
11
|
-
/** Angle from `a` to `b`, in radians
|
|
12
|
-
|
|
8
|
+
/** Distance between two Vecs; writing scales them symmetrically about
|
|
9
|
+
* their midpoint (collapse to 0 reinflates the last direction). */
|
|
10
|
+
export declare function distance(a: Writable<Vec>, b: Writable<Vec>): Writable<Num>;
|
|
11
|
+
/** Angle from `a` to `b`, in radians; writing rotates `b` about `a`
|
|
12
|
+
* (a fixed, separation preserved). */
|
|
13
|
+
export declare function angle(a: Read<V>, b: Writable<Vec>): Writable<Num>;
|
|
13
14
|
/** Reflect `point` across the line through `axisStart`/`axisEnd`. Writes
|
|
14
15
|
* the reflected position back to `point` (axis unchanged); reflection is
|
|
15
16
|
* involutive, so the same formula reads and writes. */
|
|
16
|
-
export declare function
|
|
17
|
+
export declare function reflection(point: Cell<V>, axisStart: Cell<V>, axisEnd: Cell<V>): Writable<Vec>;
|
|
17
18
|
/** Lerp between two Vecs at parameter `t`. Writing the interpolated point
|
|
18
19
|
* shifts both endpoints rigidly (preserving t). */
|
|
19
20
|
export declare function vecLerp(a: Cell<V>, b: Cell<V>, t: Cell<number>): Writable<Vec>;
|
|
@@ -22,12 +23,8 @@ export declare function vecLerp(a: Cell<V>, b: Cell<V>, t: Cell<number>): Writab
|
|
|
22
23
|
export declare function pulleySum(a: Num, b: Num): Writable<Num>;
|
|
23
24
|
/** Difference of two nums: `a - b`. Writing the difference shifts
|
|
24
25
|
* both inputs symmetrically by ±half-delta. */
|
|
25
|
-
export declare function
|
|
26
|
+
export declare function diff(a: Num, b: Num): Writable<Num>;
|
|
26
27
|
/** Mean of N nums, clamped to `[lo, hi]` on read and write (writes are
|
|
27
28
|
* clamped before the delta is distributed). */
|
|
28
29
|
export declare function clampedMean(parents: readonly Num[], lo: number, hi: number): Writable<Num>;
|
|
29
|
-
/** Quadratic Bézier point at parameter `t`. RO. */
|
|
30
|
-
export declare function bezier2(p0: Cell<V>, p1: Cell<V>, p2: Cell<V>, t: Cell<number>): Vec;
|
|
31
|
-
/** Cubic Bézier point at parameter `t`. RO. */
|
|
32
|
-
export declare function bezier3(p0: Cell<V>, p1: Cell<V>, p2: Cell<V>, p3: Cell<V>, t: Cell<number>): Vec;
|
|
33
30
|
export {};
|
|
@@ -1,20 +1,30 @@
|
|
|
1
|
-
//
|
|
2
|
-
// `Cls.derive` forms. All are a few lines on top of the engine.
|
|
3
|
-
import { Num } from "
|
|
4
|
-
import { Vec } from "
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
// geometry.ts — geometric lens building blocks over the N-input
|
|
2
|
+
// `Cls.lens` / `Cls.derive` forms. All are a few lines on top of the engine.
|
|
3
|
+
import { Num } from "../values/num.js";
|
|
4
|
+
import { Vec } from "../values/vec.js";
|
|
5
|
+
import { rotateAbout } from "./closed-form-policies.js";
|
|
6
|
+
import { remember } from "./memory.js";
|
|
7
|
+
/** Distance between two Vecs; writing scales them symmetrically about
|
|
8
|
+
* their midpoint (collapse to 0 reinflates the last direction). */
|
|
9
|
+
export function distance(a, b) {
|
|
10
|
+
const points = [a, b];
|
|
11
|
+
return remember(points, {
|
|
12
|
+
anchor: (vals) => ({
|
|
13
|
+
x: (vals[0].x + vals[1].x) / 2,
|
|
14
|
+
y: (vals[0].y + vals[1].y) / 2,
|
|
15
|
+
}),
|
|
16
|
+
feature: (vals) => Math.hypot(vals[0].x - vals[1].x, vals[0].y - vals[1].y),
|
|
17
|
+
});
|
|
9
18
|
}
|
|
10
|
-
/** Angle from `a` to `b`, in radians
|
|
11
|
-
|
|
12
|
-
|
|
19
|
+
/** Angle from `a` to `b`, in radians; writing rotates `b` about `a`
|
|
20
|
+
* (a fixed, separation preserved). */
|
|
21
|
+
export function angle(a, b) {
|
|
22
|
+
return rotateAbout([b], a);
|
|
13
23
|
}
|
|
14
24
|
/** Reflect `point` across the line through `axisStart`/`axisEnd`. Writes
|
|
15
25
|
* the reflected position back to `point` (axis unchanged); reflection is
|
|
16
26
|
* involutive, so the same formula reads and writes. */
|
|
17
|
-
export function
|
|
27
|
+
export function reflection(point, axisStart, axisEnd) {
|
|
18
28
|
const reflect = (p, a, b) => {
|
|
19
29
|
const dx = b.x - a.x;
|
|
20
30
|
const dy = b.y - a.y;
|
|
@@ -53,7 +63,7 @@ export function pulleySum(a, b) {
|
|
|
53
63
|
}
|
|
54
64
|
/** Difference of two nums: `a - b`. Writing the difference shifts
|
|
55
65
|
* both inputs symmetrically by ±half-delta. */
|
|
56
|
-
export function
|
|
66
|
+
export function diff(a, b) {
|
|
57
67
|
return Num.lens([a, b], vals => vals[0] - vals[1], (target, vals) => {
|
|
58
68
|
const [av, bv] = vals;
|
|
59
69
|
const cur = av - bv;
|
|
@@ -87,27 +97,3 @@ export function clampedMean(parents, lo, hi) {
|
|
|
87
97
|
return out;
|
|
88
98
|
});
|
|
89
99
|
}
|
|
90
|
-
/** Quadratic Bézier point at parameter `t`. RO. */
|
|
91
|
-
export function bezier2(p0, p1, p2, t) {
|
|
92
|
-
return Vec.derive([p0, p1, p2, t], vals => {
|
|
93
|
-
const [a, b, c, tv] = vals;
|
|
94
|
-
const u = 1 - tv;
|
|
95
|
-
return {
|
|
96
|
-
x: u * u * a.x + 2 * u * tv * b.x + tv * tv * c.x,
|
|
97
|
-
y: u * u * a.y + 2 * u * tv * b.y + tv * tv * c.y,
|
|
98
|
-
};
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
/** Cubic Bézier point at parameter `t`. RO. */
|
|
102
|
-
export function bezier3(p0, p1, p2, p3, t) {
|
|
103
|
-
return Vec.derive([p0, p1, p2, p3, t], vals => {
|
|
104
|
-
const [a, b, c, d, tv] = vals;
|
|
105
|
-
const u = 1 - tv;
|
|
106
|
-
const u2 = u * u;
|
|
107
|
-
const t2 = tv * tv;
|
|
108
|
-
return {
|
|
109
|
-
x: u2 * u * a.x + 3 * u2 * tv * b.x + 3 * u * t2 * c.x + t2 * tv * d.x,
|
|
110
|
-
y: u2 * u * a.y + 3 * u2 * tv * b.y + 3 * u * t2 * c.y + t2 * tv * d.y,
|
|
111
|
-
};
|
|
112
|
-
});
|
|
113
|
-
}
|