bireactive 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/LICENSE +21 -0
- package/README.md +81 -0
- package/dist/animation/anim.d.ts +57 -0
- package/dist/animation/anim.js +318 -0
- package/dist/animation/combinators.d.ts +39 -0
- package/dist/animation/combinators.js +113 -0
- package/dist/animation/easings.d.ts +5 -0
- package/dist/animation/easings.js +5 -0
- package/dist/animation/index.d.ts +3 -0
- package/dist/animation/index.js +3 -0
- package/dist/assert/algebra.d.ts +20 -0
- package/dist/assert/algebra.js +79 -0
- package/dist/assert/claim.d.ts +40 -0
- package/dist/assert/claim.js +129 -0
- package/dist/assert/index.d.ts +7 -0
- package/dist/assert/index.js +19 -0
- package/dist/assert/predicates.d.ts +18 -0
- package/dist/assert/predicates.js +43 -0
- package/dist/assert/record.d.ts +20 -0
- package/dist/assert/record.js +78 -0
- package/dist/assert/scope.d.ts +42 -0
- package/dist/assert/scope.js +233 -0
- package/dist/assert/span.d.ts +37 -0
- package/dist/assert/span.js +68 -0
- package/dist/assert/tree.d.ts +22 -0
- package/dist/assert/tree.js +65 -0
- package/dist/code/code.d.ts +70 -0
- package/dist/code/code.js +361 -0
- package/dist/code/index.d.ts +2 -0
- package/dist/code/index.js +9 -0
- package/dist/code/morph.d.ts +5 -0
- package/dist/code/morph.js +194 -0
- package/dist/code/tokenize.d.ts +8 -0
- package/dist/code/tokenize.js +51 -0
- package/dist/constraints/cluster.d.ts +83 -0
- package/dist/constraints/cluster.js +213 -0
- package/dist/constraints/drivers.d.ts +15 -0
- package/dist/constraints/drivers.js +40 -0
- package/dist/constraints/factories.d.ts +73 -0
- package/dist/constraints/factories.js +248 -0
- package/dist/constraints/index.d.ts +11 -0
- package/dist/constraints/index.js +39 -0
- package/dist/constraints/interaction.d.ts +21 -0
- package/dist/constraints/interaction.js +148 -0
- package/dist/constraints/linalg.d.ts +18 -0
- package/dist/constraints/linalg.js +141 -0
- package/dist/constraints/phases.d.ts +21 -0
- package/dist/constraints/phases.js +60 -0
- package/dist/constraints/physics.d.ts +34 -0
- package/dist/constraints/physics.js +128 -0
- package/dist/constraints/rigid.d.ts +210 -0
- package/dist/constraints/rigid.js +835 -0
- package/dist/constraints/solver.d.ts +107 -0
- package/dist/constraints/solver.js +510 -0
- package/dist/constraints/term.d.ts +50 -0
- package/dist/constraints/term.js +80 -0
- package/dist/constraints/terms.d.ts +80 -0
- package/dist/constraints/terms.js +302 -0
- package/dist/constraints/world.d.ts +31 -0
- package/dist/constraints/world.js +245 -0
- package/dist/core/aggregates.d.ts +64 -0
- package/dist/core/aggregates.js +198 -0
- package/dist/core/anim.d.ts +84 -0
- package/dist/core/anim.js +301 -0
- package/dist/core/index.d.ts +38 -0
- package/dist/core/index.js +38 -0
- package/dist/core/introspect.d.ts +5 -0
- package/dist/core/introspect.js +31 -0
- package/dist/core/lenses/closed-form-policies.d.ts +64 -0
- package/dist/core/lenses/closed-form-policies.js +452 -0
- package/dist/core/lenses/domain-aggregates.d.ts +54 -0
- package/dist/core/lenses/domain-aggregates.js +259 -0
- package/dist/core/lenses/factor-lens.d.ts +42 -0
- package/dist/core/lenses/factor-lens.js +419 -0
- package/dist/core/lenses/index.d.ts +5 -0
- package/dist/core/lenses/index.js +16 -0
- package/dist/core/lenses/memory.d.ts +47 -0
- package/dist/core/lenses/memory.js +102 -0
- package/dist/core/lenses/typed-factor.d.ts +45 -0
- package/dist/core/lenses/typed-factor.js +376 -0
- package/dist/core/network-utils.d.ts +14 -0
- package/dist/core/network-utils.js +62 -0
- package/dist/core/new-primitives.d.ts +33 -0
- package/dist/core/new-primitives.js +113 -0
- package/dist/core/signal.d.ts +254 -0
- package/dist/core/signal.js +1349 -0
- package/dist/core/traits.d.ts +61 -0
- package/dist/core/traits.js +56 -0
- package/dist/core/tree.d.ts +23 -0
- package/dist/core/tree.js +62 -0
- package/dist/core/values/anchor.d.ts +23 -0
- package/dist/core/values/anchor.js +23 -0
- package/dist/core/values/audio.d.ts +33 -0
- package/dist/core/values/audio.js +107 -0
- package/dist/core/values/bool.d.ts +37 -0
- package/dist/core/values/bool.js +75 -0
- package/dist/core/values/box.d.ts +77 -0
- package/dist/core/values/box.js +211 -0
- package/dist/core/values/canvas.d.ts +71 -0
- package/dist/core/values/canvas.js +495 -0
- package/dist/core/values/color.d.ts +49 -0
- package/dist/core/values/color.js +106 -0
- package/dist/core/values/flags.d.ts +18 -0
- package/dist/core/values/flags.js +50 -0
- package/dist/core/values/gpu.d.ts +74 -0
- package/dist/core/values/gpu.js +426 -0
- package/dist/core/values/matrix.d.ts +53 -0
- package/dist/core/values/matrix.js +140 -0
- package/dist/core/values/num.d.ts +62 -0
- package/dist/core/values/num.js +166 -0
- package/dist/core/values/pose.d.ts +31 -0
- package/dist/core/values/pose.js +83 -0
- package/dist/core/values/range.d.ts +83 -0
- package/dist/core/values/range.js +167 -0
- package/dist/core/values/str.d.ts +76 -0
- package/dist/core/values/str.js +346 -0
- package/dist/core/values/template.d.ts +49 -0
- package/dist/core/values/template.js +148 -0
- package/dist/core/values/transform.d.ts +49 -0
- package/dist/core/values/transform.js +115 -0
- package/dist/core/values/tri.d.ts +31 -0
- package/dist/core/values/tri.js +95 -0
- package/dist/core/values/vec.d.ts +72 -0
- package/dist/core/values/vec.js +219 -0
- package/dist/core/writable.d.ts +15 -0
- package/dist/core/writable.js +29 -0
- package/dist/ext/events.d.ts +10 -0
- package/dist/ext/events.js +31 -0
- package/dist/ext/index.d.ts +4 -0
- package/dist/ext/index.js +4 -0
- package/dist/ext/snapshot.d.ts +8 -0
- package/dist/ext/snapshot.js +29 -0
- package/dist/ext/timeline.d.ts +56 -0
- package/dist/ext/timeline.js +94 -0
- package/dist/ext/waapi.d.ts +25 -0
- package/dist/ext/waapi.js +198 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +10 -0
- package/dist/propagators/index.d.ts +6 -0
- package/dist/propagators/index.js +6 -0
- package/dist/propagators/layout.d.ts +68 -0
- package/dist/propagators/layout.js +336 -0
- package/dist/propagators/network.d.ts +52 -0
- package/dist/propagators/network.js +185 -0
- package/dist/propagators/propagator.d.ts +12 -0
- package/dist/propagators/propagator.js +16 -0
- package/dist/propagators/range.d.ts +45 -0
- package/dist/propagators/range.js +147 -0
- package/dist/propagators/relations.d.ts +60 -0
- package/dist/propagators/relations.js +343 -0
- package/dist/shapes/annular-sector.d.ts +15 -0
- package/dist/shapes/annular-sector.js +64 -0
- package/dist/shapes/button.d.ts +14 -0
- package/dist/shapes/button.js +31 -0
- package/dist/shapes/choreographers.d.ts +22 -0
- package/dist/shapes/choreographers.js +69 -0
- package/dist/shapes/circle.d.ts +17 -0
- package/dist/shapes/circle.js +57 -0
- package/dist/shapes/clip.d.ts +5 -0
- package/dist/shapes/clip.js +31 -0
- package/dist/shapes/connect.d.ts +16 -0
- package/dist/shapes/connect.js +70 -0
- package/dist/shapes/curve.d.ts +60 -0
- package/dist/shapes/curve.js +285 -0
- package/dist/shapes/dashed.d.ts +16 -0
- package/dist/shapes/dashed.js +142 -0
- package/dist/shapes/debug.d.ts +43 -0
- package/dist/shapes/debug.js +97 -0
- package/dist/shapes/group.d.ts +5 -0
- package/dist/shapes/group.js +10 -0
- package/dist/shapes/handle.d.ts +32 -0
- package/dist/shapes/handle.js +88 -0
- package/dist/shapes/index.d.ts +23 -0
- package/dist/shapes/index.js +23 -0
- package/dist/shapes/interaction.d.ts +32 -0
- package/dist/shapes/interaction.js +187 -0
- package/dist/shapes/label.d.ts +20 -0
- package/dist/shapes/label.js +42 -0
- package/dist/shapes/layout.d.ts +29 -0
- package/dist/shapes/layout.js +74 -0
- package/dist/shapes/line.d.ts +21 -0
- package/dist/shapes/line.js +79 -0
- package/dist/shapes/list.d.ts +18 -0
- package/dist/shapes/list.js +51 -0
- package/dist/shapes/mount.d.ts +7 -0
- package/dist/shapes/mount.js +10 -0
- package/dist/shapes/path.d.ts +77 -0
- package/dist/shapes/path.js +227 -0
- package/dist/shapes/rect.d.ts +30 -0
- package/dist/shapes/rect.js +131 -0
- package/dist/shapes/shape.d.ts +132 -0
- package/dist/shapes/shape.js +306 -0
- package/dist/shapes/text.d.ts +24 -0
- package/dist/shapes/text.js +53 -0
- package/dist/shapes/tokens.d.ts +28 -0
- package/dist/shapes/tokens.js +27 -0
- package/dist/shapes/transitions.d.ts +23 -0
- package/dist/shapes/transitions.js +62 -0
- package/dist/tex/decorations.d.ts +26 -0
- package/dist/tex/decorations.js +116 -0
- package/dist/tex/index.d.ts +5 -0
- package/dist/tex/index.js +5 -0
- package/dist/tex/marker.d.ts +17 -0
- package/dist/tex/marker.js +63 -0
- package/dist/tex/motion.d.ts +43 -0
- package/dist/tex/motion.js +290 -0
- package/dist/tex/parts.d.ts +65 -0
- package/dist/tex/parts.js +149 -0
- package/dist/tex/tex.d.ts +45 -0
- package/dist/tex/tex.js +244 -0
- package/dist/web/attr.d.ts +16 -0
- package/dist/web/attr.js +98 -0
- package/dist/web/diagram.d.ts +49 -0
- package/dist/web/diagram.js +260 -0
- package/dist/web/index.d.ts +6 -0
- package/dist/web/index.js +6 -0
- package/dist/web/md-marker.d.ts +6 -0
- package/dist/web/md-marker.js +39 -0
- package/dist/web/md-tex.d.ts +6 -0
- package/dist/web/md-tex.js +61 -0
- package/dist/web/raf.d.ts +6 -0
- package/dist/web/raf.js +24 -0
- package/dist/web/viewport.d.ts +7 -0
- package/dist/web/viewport.js +13 -0
- package/package.json +87 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Spatial composition primitives.
|
|
2
|
+
import { Box, BoxMath, reader, transformBox } from "../core/index.js";
|
|
3
|
+
/** Lay out `shapes` in a row/column. First stays put; the rest bind
|
|
4
|
+
* their `translate` reactively to sit `gap` past the previous.
|
|
5
|
+
* Reflows on size or anchor change. */
|
|
6
|
+
export function arrange(shapes, axis, opts = {}) {
|
|
7
|
+
const gap = opts.gap ?? 0;
|
|
8
|
+
const cross = opts.align ?? 0;
|
|
9
|
+
if (shapes.length < 2)
|
|
10
|
+
return;
|
|
11
|
+
const anchor = shapes[0];
|
|
12
|
+
for (let i = 1; i < shapes.length; i++) {
|
|
13
|
+
const prev = shapes[i - 1];
|
|
14
|
+
const cur = shapes[i];
|
|
15
|
+
cur.effect(() => {
|
|
16
|
+
// prev/anchor in the parent frame so upstream transforms
|
|
17
|
+
// cascade; cur stays local since we're writing its own translate.
|
|
18
|
+
const pBox = transformBox(prev.localFrame.value, prev.box.value);
|
|
19
|
+
const aBox = transformBox(anchor.localFrame.value, anchor.box.value);
|
|
20
|
+
const cb = cur.box.value;
|
|
21
|
+
if (axis === "row") {
|
|
22
|
+
const targetX = pBox.x + pBox.w + gap;
|
|
23
|
+
const targetY = aBox.y + cross * aBox.h - cross * cb.h;
|
|
24
|
+
cur.translate.value = {
|
|
25
|
+
x: targetX - cb.x,
|
|
26
|
+
y: targetY - cb.y,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const targetY = pBox.y + pBox.h + gap;
|
|
31
|
+
const targetX = aBox.x + cross * aBox.w - cross * cb.w;
|
|
32
|
+
cur.translate.value = {
|
|
33
|
+
x: targetX - cb.x,
|
|
34
|
+
y: targetY - cb.y,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Inflate a Box on each side by `by`. */
|
|
41
|
+
export function expand(b, by) {
|
|
42
|
+
const byFn = reader(by);
|
|
43
|
+
return Box.derive(() => BoxMath.expand(b.value, byFn()));
|
|
44
|
+
}
|
|
45
|
+
/** Split a Box along an axis into N reactive sub-Boxes.
|
|
46
|
+
*
|
|
47
|
+
* split(b, "x", 3) — 3 equal columns
|
|
48
|
+
* split(b, "x", [3, 2, 2]) — weighted 3:2:2
|
|
49
|
+
* split(b, "x", 3, { gap: 4 }) — 4px between
|
|
50
|
+
*/
|
|
51
|
+
export function split(source, axis, parts, opts = {}) {
|
|
52
|
+
const ratios = typeof parts === "number" ? new Array(parts).fill(1) : parts;
|
|
53
|
+
const total = ratios.reduce((a, b) => a + b, 0);
|
|
54
|
+
const cumBefore = ratios.map((_, i) => ratios.slice(0, i).reduce((a, b) => a + b, 0));
|
|
55
|
+
const gapFn = reader(opts.gap ?? 0);
|
|
56
|
+
return ratios.map((r, i) => Box.derive(() => {
|
|
57
|
+
const b = source.value;
|
|
58
|
+
const gap = gapFn();
|
|
59
|
+
const gapTotal = gap * (ratios.length - 1);
|
|
60
|
+
if (axis === "x") {
|
|
61
|
+
const free = b.w - gapTotal;
|
|
62
|
+
const offset = (cumBefore[i] / total) * free + gap * i;
|
|
63
|
+
return { x: b.x + offset, y: b.y, w: (r / total) * free, h: b.h };
|
|
64
|
+
}
|
|
65
|
+
const free = b.h - gapTotal;
|
|
66
|
+
const offset = (cumBefore[i] / total) * free + gap * i;
|
|
67
|
+
return { x: b.x, y: b.y + offset, w: b.w, h: (r / total) * free };
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
/** Two-axis split into a `rows × cols` grid (sugar over `split`).
|
|
71
|
+
* Returns `[row][col]`. */
|
|
72
|
+
export function grid(source, rows, cols, opts = {}) {
|
|
73
|
+
return split(source, "y", rows, opts).map(row => split(row, "x", cols, opts));
|
|
74
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Cell, type Val, Vec } from "../core/index.js";
|
|
2
|
+
import { type CommonOpts, type Segment, Shape } from "./shape.js";
|
|
3
|
+
export interface LineOpts extends CommonOpts {
|
|
4
|
+
}
|
|
5
|
+
export declare class Line<O extends LineOpts = LineOpts> extends Shape<O> {
|
|
6
|
+
#private;
|
|
7
|
+
readonly from: Vec;
|
|
8
|
+
readonly to: Vec;
|
|
9
|
+
constructor(from: Vec, to: Vec, opts?: O);
|
|
10
|
+
/** Position at fraction `t` (0=from, 1=to). Symmetric with
|
|
11
|
+
* `Path.pointAt`. */
|
|
12
|
+
pointAt(t: Val<number>): Vec;
|
|
13
|
+
tangentAt(_t?: Val<number>): Vec;
|
|
14
|
+
normalAt(_t?: Val<number>): Vec;
|
|
15
|
+
angleAt(_t?: Val<number>): Cell<number>;
|
|
16
|
+
length(): Cell<number>;
|
|
17
|
+
/** Closer endpoint to `toward`. */
|
|
18
|
+
boundary(toward: Vec): Vec;
|
|
19
|
+
segments(): Segment[];
|
|
20
|
+
}
|
|
21
|
+
export declare const line: <const O extends LineOpts>(from: Vec, to: Vec, opts?: O) => Line<O>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { derive, Vec } from "../core/index.js";
|
|
2
|
+
import { Shape } from "./shape.js";
|
|
3
|
+
export class Line extends Shape {
|
|
4
|
+
from;
|
|
5
|
+
to;
|
|
6
|
+
constructor(from, to, opts = {}) {
|
|
7
|
+
super(opts.dashed ? "path" : "line", () => {
|
|
8
|
+
const a = from.value;
|
|
9
|
+
const b = to.value;
|
|
10
|
+
return {
|
|
11
|
+
x: Math.min(a.x, b.x),
|
|
12
|
+
y: Math.min(a.y, b.y),
|
|
13
|
+
w: Math.abs(b.x - a.x),
|
|
14
|
+
h: Math.abs(b.y - a.y),
|
|
15
|
+
};
|
|
16
|
+
}, opts, {
|
|
17
|
+
origin: derive(() => {
|
|
18
|
+
const a = from.value;
|
|
19
|
+
const b = to.value;
|
|
20
|
+
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
this.from = from;
|
|
24
|
+
this.to = to;
|
|
25
|
+
this.attr("stroke-linecap", opts.cap ?? "round");
|
|
26
|
+
this.stroke(opts, false, { x1: from.x, y1: from.y, x2: to.x, y2: to.y });
|
|
27
|
+
}
|
|
28
|
+
// Tangent/normal/angle are constant along a Line; `t` is accepted
|
|
29
|
+
// for API symmetry with Path but ignored. Cached lazily.
|
|
30
|
+
#tangent;
|
|
31
|
+
#normal;
|
|
32
|
+
#angle;
|
|
33
|
+
#length;
|
|
34
|
+
/** Position at fraction `t` (0=from, 1=to). Symmetric with
|
|
35
|
+
* `Path.pointAt`. */
|
|
36
|
+
pointAt(t) {
|
|
37
|
+
if (typeof t === "number") {
|
|
38
|
+
if (t === 0)
|
|
39
|
+
return this.from;
|
|
40
|
+
if (t === 1)
|
|
41
|
+
return this.to;
|
|
42
|
+
}
|
|
43
|
+
return this.from.lerp(this.to, t);
|
|
44
|
+
}
|
|
45
|
+
tangentAt(_t = 0) {
|
|
46
|
+
return (this.#tangent ??= this.to.sub(this.from).normalize());
|
|
47
|
+
}
|
|
48
|
+
normalAt(_t = 0) {
|
|
49
|
+
return (this.#normal ??= this.tangentAt().perp());
|
|
50
|
+
}
|
|
51
|
+
angleAt(_t = 0) {
|
|
52
|
+
if (this.#angle)
|
|
53
|
+
return this.#angle;
|
|
54
|
+
const tan = this.tangentAt();
|
|
55
|
+
return (this.#angle = derive(() => Math.atan2(tan.y.value, tan.x.value)));
|
|
56
|
+
}
|
|
57
|
+
length() {
|
|
58
|
+
return (this.#length ??= derive(() => {
|
|
59
|
+
const a = this.from.value;
|
|
60
|
+
const b = this.to.value;
|
|
61
|
+
return Math.hypot(b.x - a.x, b.y - a.y);
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
/** Closer endpoint to `toward`. */
|
|
65
|
+
boundary(toward) {
|
|
66
|
+
return Vec.derive(() => {
|
|
67
|
+
const t = toward.value;
|
|
68
|
+
const a = this.from.value;
|
|
69
|
+
const b = this.to.value;
|
|
70
|
+
const da = (t.x - a.x) ** 2 + (t.y - a.y) ** 2;
|
|
71
|
+
const db = (t.x - b.x) ** 2 + (t.y - b.y) ** 2;
|
|
72
|
+
return da <= db ? a : b;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
segments() {
|
|
76
|
+
return [{ type: "line", from: this.from.value, to: this.to.value }];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export const line = (from, to, opts) => new Line(from, to, opts);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Val } from "../core/index.js";
|
|
2
|
+
import type { AnyShape } from "./shape.js";
|
|
3
|
+
export interface ForEachOptions<T> {
|
|
4
|
+
/** Stable identity per item; defaults to index (fine for fixed
|
|
5
|
+
* positions, makes swaps look like replacement). */
|
|
6
|
+
key?: (item: T, index: number) => unknown;
|
|
7
|
+
}
|
|
8
|
+
/** Reactive list result. `at(i)` returns the primary shape (the first
|
|
9
|
+
* if `render` returned an array). */
|
|
10
|
+
export interface ForEachResult {
|
|
11
|
+
dispose: () => void;
|
|
12
|
+
at: (i: number) => AnyShape | undefined;
|
|
13
|
+
all: (i: number) => readonly AnyShape[] | undefined;
|
|
14
|
+
}
|
|
15
|
+
/** Render a shape (or shapes) per item in `source`, mounting under
|
|
16
|
+
* `parent`. Re-runs only on structural changes; per-item reactivity
|
|
17
|
+
* is the render fn's job. */
|
|
18
|
+
export declare function forEach<T>(parent: AnyShape, source: Val<readonly T[]>, render: (item: T, index: number) => AnyShape | AnyShape[], options?: ForEachOptions<T>): ForEachResult;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Reactive list rendering. Diffs `parent`'s children as `source`
|
|
2
|
+
// changes; stable keys preserve per-shape state across updates.
|
|
3
|
+
import { effect, readNow, untracked } from "../core/index.js";
|
|
4
|
+
/** Render a shape (or shapes) per item in `source`, mounting under
|
|
5
|
+
* `parent`. Re-runs only on structural changes; per-item reactivity
|
|
6
|
+
* is the render fn's job. */
|
|
7
|
+
export function forEach(parent, source, render, options = {}) {
|
|
8
|
+
const { key: keyOf } = options;
|
|
9
|
+
let entries = [];
|
|
10
|
+
const eff = effect(() => {
|
|
11
|
+
const next = readNow(source);
|
|
12
|
+
// Diff in `untracked` so internal reads/writes don't re-trigger.
|
|
13
|
+
untracked(() => {
|
|
14
|
+
const prevByKey = new Map();
|
|
15
|
+
for (const e of entries)
|
|
16
|
+
prevByKey.set(e.key, e);
|
|
17
|
+
const nextEntries = [];
|
|
18
|
+
for (let i = 0; i < next.length; i++) {
|
|
19
|
+
const item = next[i];
|
|
20
|
+
const k = keyOf ? keyOf(item, i) : i;
|
|
21
|
+
const existing = prevByKey.get(k);
|
|
22
|
+
if (existing) {
|
|
23
|
+
nextEntries.push(existing);
|
|
24
|
+
prevByKey.delete(k);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const result = render(item, i);
|
|
28
|
+
const shapes = Array.isArray(result) ? result : [result];
|
|
29
|
+
parent.add(...shapes);
|
|
30
|
+
nextEntries.push({ key: k, shapes });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Anything left in prevByKey is gone from the source.
|
|
34
|
+
for (const removed of prevByKey.values()) {
|
|
35
|
+
parent.remove(...removed.shapes);
|
|
36
|
+
}
|
|
37
|
+
entries = nextEntries;
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
dispose: () => {
|
|
42
|
+
eff();
|
|
43
|
+
const toRemove = entries;
|
|
44
|
+
entries = [];
|
|
45
|
+
for (const e of toRemove)
|
|
46
|
+
parent.remove(...e.shapes);
|
|
47
|
+
},
|
|
48
|
+
at: (i) => entries[i]?.shapes[0],
|
|
49
|
+
all: (i) => entries[i]?.shapes,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Callable mount handle: `s(shape)` adds children under a root Shape and returns them.
|
|
2
|
+
export function mount(root) {
|
|
3
|
+
const fn = ((...shapes) => {
|
|
4
|
+
for (const s of shapes)
|
|
5
|
+
root.add(s);
|
|
6
|
+
return shapes.length === 1 ? shapes[0] : shapes;
|
|
7
|
+
});
|
|
8
|
+
Object.defineProperty(fn, "root", { value: root });
|
|
9
|
+
return fn;
|
|
10
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { type Cell, type Read, type Val, Vec } from "../core/index.js";
|
|
2
|
+
import { type CommonOpts, type Segment, Shape } from "./shape.js";
|
|
3
|
+
export interface PathOpts extends CommonOpts {
|
|
4
|
+
closed?: boolean;
|
|
5
|
+
}
|
|
6
|
+
/** Open or closed polyline through a reactive list of Points.
|
|
7
|
+
*
|
|
8
|
+
* path(start, opts?).to(p2).to(p3) — fluent (preferred)
|
|
9
|
+
* new Path([p1, p2, p3], opts?) — explicit array
|
|
10
|
+
*
|
|
11
|
+
* Extension methods (`to`/`u`/`d`/`l`/`r`/`offset`/`along`) mutate in
|
|
12
|
+
* place and return `this`. The `d` attribute and all sampling methods
|
|
13
|
+
* react to point changes automatically. */
|
|
14
|
+
export declare class Path<O extends PathOpts = PathOpts> extends Shape<O> {
|
|
15
|
+
private readonly _points;
|
|
16
|
+
readonly closed: boolean;
|
|
17
|
+
readonly length: Cell<number>;
|
|
18
|
+
/** Sample at `t ∈ [0, 1]`. Named to avoid shadowing the Box `at(u, v)`
|
|
19
|
+
* anchor — same symmetry as `tangentAt` / `normalAt` / `angleAt`. */
|
|
20
|
+
readonly pointAt: (t: Val<number>) => Vec;
|
|
21
|
+
readonly atDistance: (d: Val<number>) => Vec;
|
|
22
|
+
readonly tangentAt: (t: Val<number>) => Vec;
|
|
23
|
+
readonly normalAt: (t: Val<number>) => Vec;
|
|
24
|
+
readonly angleAt: (t: Val<number>) => Cell<number>;
|
|
25
|
+
constructor(start?: Vec | readonly Vec[], opts?: O);
|
|
26
|
+
/** Untracked snapshot of the points list. */
|
|
27
|
+
get points(): readonly Vec[];
|
|
28
|
+
private get last();
|
|
29
|
+
private extend;
|
|
30
|
+
to(p: Vec): this;
|
|
31
|
+
/** Step `n` up from the last vertex. */
|
|
32
|
+
u(n: Val<number>): this;
|
|
33
|
+
/** Step `n` down from the last vertex. */
|
|
34
|
+
d(n: Val<number>): this;
|
|
35
|
+
/** Step `n` left from the last vertex. */
|
|
36
|
+
l(n: Val<number>): this;
|
|
37
|
+
/** Step `n` right from the last vertex. */
|
|
38
|
+
r(n: Val<number>): this;
|
|
39
|
+
offset(dx: Val<number>, dy: Val<number>): this;
|
|
40
|
+
/** Walk `dist` at `angle` (radians, y-down). */
|
|
41
|
+
along(angle: Val<number>, dist: Val<number>): this;
|
|
42
|
+
segments(): Segment[];
|
|
43
|
+
}
|
|
44
|
+
/** Start a fluent path at `start`. Chain `.to(p)` / `.u(n)` / `.d(n)`
|
|
45
|
+
* / `.l(n)` / `.r(n)` / `.offset(dx, dy)` / `.along(angle, dist)` and
|
|
46
|
+
* pass to `s(...)` to render. */
|
|
47
|
+
export declare const path: <const O extends PathOpts>(start: Vec, opts?: O) => Path<O>;
|
|
48
|
+
export interface PathDOpts {
|
|
49
|
+
/** Stroke color. Default: `tokens.stroke`. */
|
|
50
|
+
stroke?: Val<string>;
|
|
51
|
+
/** Fill color. Default: `"none"`. */
|
|
52
|
+
fill?: Val<string>;
|
|
53
|
+
/** Stroke width override; trumps `thin`. */
|
|
54
|
+
strokeWidth?: Val<number>;
|
|
55
|
+
/** Use the thin stroke weight (`tokens.thinWeight`). Default: false. */
|
|
56
|
+
thin?: boolean;
|
|
57
|
+
/** Stroke line cap. */
|
|
58
|
+
cap?: "butt" | "round" | "square";
|
|
59
|
+
/** Stroke line join. */
|
|
60
|
+
join?: "miter" | "round" | "bevel";
|
|
61
|
+
/** Stroke dash array (e.g. `"3 5"`). */
|
|
62
|
+
dasharray?: Val<string>;
|
|
63
|
+
/** Reactive opacity. */
|
|
64
|
+
opacity?: Val<number>;
|
|
65
|
+
/** Reactive bbox for auto-fit. Default: zero box (caller manages view). */
|
|
66
|
+
box?: () => {
|
|
67
|
+
x: number;
|
|
68
|
+
y: number;
|
|
69
|
+
w: number;
|
|
70
|
+
h: number;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** Low-level `<path>` driven by a reactive `d` string — build the `d`
|
|
74
|
+
* attribute directly (dense plots, custom samplers). One attr-effect drives
|
|
75
|
+
* the whole path, far cheaper than N shapes for dense geometry. Pass `box`
|
|
76
|
+
* for auto-fit; otherwise contributes a zero box (caller owns the viewBox). */
|
|
77
|
+
export declare function pathD(d: Read<string>, opts?: PathDOpts): Shape;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { cell, derive, reader, Vec, } from "../core/index.js";
|
|
2
|
+
import { Shape } from "./shape.js";
|
|
3
|
+
import { tokens } from "./tokens.js";
|
|
4
|
+
const clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v);
|
|
5
|
+
/** Geometry sampler over a reactive list of Points. `pts` is tracked
|
|
6
|
+
* by every computed, so mutating the list re-runs sampling. */
|
|
7
|
+
function sampler(pts) {
|
|
8
|
+
const cumLen = derive(() => {
|
|
9
|
+
const points = pts.value;
|
|
10
|
+
const lens = [0];
|
|
11
|
+
for (let i = 1; i < points.length; i++) {
|
|
12
|
+
const a = points[i - 1].value;
|
|
13
|
+
const b = points[i].value;
|
|
14
|
+
lens.push(lens[i - 1] + Math.hypot(b.x - a.x, b.y - a.y));
|
|
15
|
+
}
|
|
16
|
+
return lens;
|
|
17
|
+
});
|
|
18
|
+
const length = derive(() => {
|
|
19
|
+
const lens = cumLen.value;
|
|
20
|
+
return lens[lens.length - 1] ?? 0;
|
|
21
|
+
});
|
|
22
|
+
/** Locate by arc-length `d` (px), clamped to `[0, total]`. */
|
|
23
|
+
const locateAt = (d, points) => {
|
|
24
|
+
const lens = cumLen.value;
|
|
25
|
+
const total = lens[lens.length - 1] ?? 0;
|
|
26
|
+
if (points.length < 2 || total === 0)
|
|
27
|
+
return { i: 0, segT: 0 };
|
|
28
|
+
const target = d < 0 ? 0 : d > total ? total : d;
|
|
29
|
+
let i = 1;
|
|
30
|
+
while (i < lens.length - 1 && lens[i] < target)
|
|
31
|
+
i++;
|
|
32
|
+
const segLen = lens[i] - lens[i - 1];
|
|
33
|
+
const segT = segLen > 0 ? (target - lens[i - 1]) / segLen : 0;
|
|
34
|
+
return { i: i - 1, segT };
|
|
35
|
+
};
|
|
36
|
+
const sampleAt = (ds) => Vec.derive(() => {
|
|
37
|
+
const points = pts.value;
|
|
38
|
+
if (points.length === 0)
|
|
39
|
+
return { x: 0, y: 0 };
|
|
40
|
+
if (points.length === 1)
|
|
41
|
+
return points[0].value;
|
|
42
|
+
const { i, segT } = locateAt(ds.value, points);
|
|
43
|
+
const a = points[i].value;
|
|
44
|
+
const b = points[i + 1].value;
|
|
45
|
+
return { x: a.x + (b.x - a.x) * segT, y: a.y + (b.y - a.y) * segT };
|
|
46
|
+
});
|
|
47
|
+
const at = (t) => {
|
|
48
|
+
const ts = reader(t);
|
|
49
|
+
return sampleAt(derive(() => clamp01(ts()) * length.value));
|
|
50
|
+
};
|
|
51
|
+
/** Sample at absolute arc-length (px from start). */
|
|
52
|
+
const atDistance = (d) => {
|
|
53
|
+
const ds = reader(d);
|
|
54
|
+
return sampleAt(derive(ds));
|
|
55
|
+
};
|
|
56
|
+
const tangentAt = (t) => {
|
|
57
|
+
const ts = reader(t);
|
|
58
|
+
return Vec.derive(() => {
|
|
59
|
+
const points = pts.value;
|
|
60
|
+
if (points.length < 2)
|
|
61
|
+
return { x: 1, y: 0 };
|
|
62
|
+
const total = length.value;
|
|
63
|
+
const { i } = locateAt(clamp01(ts()) * total, points);
|
|
64
|
+
const a = points[i].value;
|
|
65
|
+
const b = points[i + 1].value;
|
|
66
|
+
const dx = b.x - a.x;
|
|
67
|
+
const dy = b.y - a.y;
|
|
68
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
69
|
+
return { x: dx / len, y: dy / len };
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
const normalAt = (t) => tangentAt(t).perp();
|
|
73
|
+
const angleAt = (t) => {
|
|
74
|
+
const tan = tangentAt(t);
|
|
75
|
+
return derive(() => Math.atan2(tan.y.value, tan.x.value));
|
|
76
|
+
};
|
|
77
|
+
return { length, at, atDistance, tangentAt, normalAt, angleAt };
|
|
78
|
+
}
|
|
79
|
+
/** Open or closed polyline through a reactive list of Points.
|
|
80
|
+
*
|
|
81
|
+
* path(start, opts?).to(p2).to(p3) — fluent (preferred)
|
|
82
|
+
* new Path([p1, p2, p3], opts?) — explicit array
|
|
83
|
+
*
|
|
84
|
+
* Extension methods (`to`/`u`/`d`/`l`/`r`/`offset`/`along`) mutate in
|
|
85
|
+
* place and return `this`. The `d` attribute and all sampling methods
|
|
86
|
+
* react to point changes automatically. */
|
|
87
|
+
export class Path extends Shape {
|
|
88
|
+
_points;
|
|
89
|
+
closed;
|
|
90
|
+
length;
|
|
91
|
+
/** Sample at `t ∈ [0, 1]`. Named to avoid shadowing the Box `at(u, v)`
|
|
92
|
+
* anchor — same symmetry as `tangentAt` / `normalAt` / `angleAt`. */
|
|
93
|
+
pointAt;
|
|
94
|
+
atDistance;
|
|
95
|
+
tangentAt;
|
|
96
|
+
normalAt;
|
|
97
|
+
angleAt;
|
|
98
|
+
constructor(start = [], opts = {}) {
|
|
99
|
+
const init = start instanceof Vec ? [start] : start;
|
|
100
|
+
const points = cell(init);
|
|
101
|
+
const closed = opts.closed ?? false;
|
|
102
|
+
super("path", () => {
|
|
103
|
+
const ps = points.value;
|
|
104
|
+
if (ps.length === 0)
|
|
105
|
+
return { x: 0, y: 0, w: 0, h: 0 };
|
|
106
|
+
let xMin = Number.POSITIVE_INFINITY, yMin = Number.POSITIVE_INFINITY, xMax = Number.NEGATIVE_INFINITY, yMax = Number.NEGATIVE_INFINITY;
|
|
107
|
+
for (const p of ps) {
|
|
108
|
+
const v = p.value;
|
|
109
|
+
if (v.x < xMin)
|
|
110
|
+
xMin = v.x;
|
|
111
|
+
if (v.y < yMin)
|
|
112
|
+
yMin = v.y;
|
|
113
|
+
if (v.x > xMax)
|
|
114
|
+
xMax = v.x;
|
|
115
|
+
if (v.y > yMax)
|
|
116
|
+
yMax = v.y;
|
|
117
|
+
}
|
|
118
|
+
return { x: xMin, y: yMin, w: xMax - xMin, h: yMax - yMin };
|
|
119
|
+
}, opts, {
|
|
120
|
+
// First vertex — matches `path.pointAt(0)`. Override via `origin`
|
|
121
|
+
// for a different pivot.
|
|
122
|
+
origin: derive(() => {
|
|
123
|
+
const ps = points.value;
|
|
124
|
+
return ps.length > 0 ? ps[0].value : { x: 0, y: 0 };
|
|
125
|
+
}),
|
|
126
|
+
});
|
|
127
|
+
this._points = points;
|
|
128
|
+
this.closed = closed;
|
|
129
|
+
const s = sampler(points);
|
|
130
|
+
this.length = s.length;
|
|
131
|
+
this.pointAt = s.at;
|
|
132
|
+
this.atDistance = s.atDistance;
|
|
133
|
+
this.tangentAt = s.tangentAt;
|
|
134
|
+
this.normalAt = s.normalAt;
|
|
135
|
+
this.angleAt = s.angleAt;
|
|
136
|
+
this.stroke(opts, closed, {
|
|
137
|
+
d: derive(() => {
|
|
138
|
+
const ps = points.value;
|
|
139
|
+
if (ps.length === 0)
|
|
140
|
+
return "";
|
|
141
|
+
const parts = [`M ${ps[0].x.value} ${ps[0].y.value}`];
|
|
142
|
+
for (let i = 1; i < ps.length; i++) {
|
|
143
|
+
parts.push(`L ${ps[i].x.value} ${ps[i].y.value}`);
|
|
144
|
+
}
|
|
145
|
+
if (closed)
|
|
146
|
+
parts.push("Z");
|
|
147
|
+
return parts.join(" ");
|
|
148
|
+
}),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/** Untracked snapshot of the points list. */
|
|
152
|
+
get points() {
|
|
153
|
+
return this._points.peek();
|
|
154
|
+
}
|
|
155
|
+
get last() {
|
|
156
|
+
const ps = this._points.peek();
|
|
157
|
+
return ps[ps.length - 1];
|
|
158
|
+
}
|
|
159
|
+
extend(p) {
|
|
160
|
+
this._points.value = [...this._points.peek(), p];
|
|
161
|
+
return this;
|
|
162
|
+
}
|
|
163
|
+
to(p) {
|
|
164
|
+
return this.extend(p);
|
|
165
|
+
}
|
|
166
|
+
/** Step `n` up from the last vertex. */
|
|
167
|
+
u(n) {
|
|
168
|
+
return this.extend(this.last.up(n));
|
|
169
|
+
}
|
|
170
|
+
/** Step `n` down from the last vertex. */
|
|
171
|
+
d(n) {
|
|
172
|
+
return this.extend(this.last.down(n));
|
|
173
|
+
}
|
|
174
|
+
/** Step `n` left from the last vertex. */
|
|
175
|
+
l(n) {
|
|
176
|
+
return this.extend(this.last.left(n));
|
|
177
|
+
}
|
|
178
|
+
/** Step `n` right from the last vertex. */
|
|
179
|
+
r(n) {
|
|
180
|
+
return this.extend(this.last.right(n));
|
|
181
|
+
}
|
|
182
|
+
offset(dx, dy) {
|
|
183
|
+
return this.extend(this.last.offset(dx, dy));
|
|
184
|
+
}
|
|
185
|
+
/** Walk `dist` at `angle` (radians, y-down). */
|
|
186
|
+
along(angle, dist) {
|
|
187
|
+
const af = reader(angle);
|
|
188
|
+
const df = reader(dist);
|
|
189
|
+
return this.extend(this.last.offset(derive(() => Math.cos(af()) * df()), derive(() => Math.sin(af()) * df())));
|
|
190
|
+
}
|
|
191
|
+
segments() {
|
|
192
|
+
const ps = this._points.peek();
|
|
193
|
+
const segs = [];
|
|
194
|
+
for (let i = 0; i < ps.length - 1; i++) {
|
|
195
|
+
segs.push({ type: "line", from: ps[i].value, to: ps[i + 1].value });
|
|
196
|
+
}
|
|
197
|
+
if (this.closed && ps.length > 1) {
|
|
198
|
+
segs.push({ type: "line", from: ps[ps.length - 1].value, to: ps[0].value });
|
|
199
|
+
}
|
|
200
|
+
return segs;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** Start a fluent path at `start`. Chain `.to(p)` / `.u(n)` / `.d(n)`
|
|
204
|
+
* / `.l(n)` / `.r(n)` / `.offset(dx, dy)` / `.along(angle, dist)` and
|
|
205
|
+
* pass to `s(...)` to render. */
|
|
206
|
+
export const path = (start, opts) => new Path(start, opts);
|
|
207
|
+
/** Low-level `<path>` driven by a reactive `d` string — build the `d`
|
|
208
|
+
* attribute directly (dense plots, custom samplers). One attr-effect drives
|
|
209
|
+
* the whole path, far cheaper than N shapes for dense geometry. Pass `box`
|
|
210
|
+
* for auto-fit; otherwise contributes a zero box (caller owns the viewBox). */
|
|
211
|
+
export function pathD(d, opts = {}) {
|
|
212
|
+
const sh = new Shape("path", opts.box ?? (() => ({ x: 0, y: 0, w: 0, h: 0 })), {
|
|
213
|
+
opacity: opts.opacity,
|
|
214
|
+
});
|
|
215
|
+
sh.attr("d", d);
|
|
216
|
+
sh.attr("fill", opts.fill ?? "none");
|
|
217
|
+
sh.attr("stroke", opts.stroke ?? tokens.stroke);
|
|
218
|
+
sh.attr("stroke-width", opts.strokeWidth ?? (opts.thin ? tokens.thinWeight : tokens.weight));
|
|
219
|
+
sh.attr("vector-effect", "non-scaling-stroke");
|
|
220
|
+
if (opts.cap)
|
|
221
|
+
sh.attr("stroke-linecap", opts.cap);
|
|
222
|
+
if (opts.join)
|
|
223
|
+
sh.attr("stroke-linejoin", opts.join);
|
|
224
|
+
if (opts.dasharray !== undefined)
|
|
225
|
+
sh.attr("stroke-dasharray", opts.dasharray);
|
|
226
|
+
return sh;
|
|
227
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Box, Num, type Val, Vec } from "../core/index.js";
|
|
2
|
+
import { type CommonOpts, type Segment, Shape } from "./shape.js";
|
|
3
|
+
export interface RectOpts extends CommonOpts {
|
|
4
|
+
corner?: Val<number>;
|
|
5
|
+
}
|
|
6
|
+
export declare class Rect<O extends RectOpts = RectOpts> extends Shape<O> {
|
|
7
|
+
readonly x: Num;
|
|
8
|
+
readonly y: Num;
|
|
9
|
+
readonly w: Num;
|
|
10
|
+
readonly h: Num;
|
|
11
|
+
readonly corner: Num;
|
|
12
|
+
constructor(x: Val<number>, y: Val<number>, w: Val<number>, h: Val<number>, opts?: O);
|
|
13
|
+
boundary(toward: Vec): Vec;
|
|
14
|
+
/** Concentric outline — a new unmounted Rect inflated by `by` per
|
|
15
|
+
* side; corner radius bumps to keep curves parallel. */
|
|
16
|
+
outline(by: Val<number>, opts?: RectOpts): Rect;
|
|
17
|
+
/** 4 sides + 4 corner quarter-arcs (or just sides when `corner === 0`). */
|
|
18
|
+
segments(): Segment[];
|
|
19
|
+
}
|
|
20
|
+
/** Rect factory:
|
|
21
|
+
*
|
|
22
|
+
* rect(x, y, w, h, opts?) — corner-based (canonical)
|
|
23
|
+
* rect(box, opts?) — fill another Box (use `shape.box`)
|
|
24
|
+
* rect(center: Point, w, h, opts?) — centered on a Point
|
|
25
|
+
* rect(p1: Point, p2: Point, opts?) — between two corner Points
|
|
26
|
+
*/
|
|
27
|
+
export declare function rect<const O extends RectOpts>(b: Box, opts?: O): Rect<O>;
|
|
28
|
+
export declare function rect<const O extends RectOpts>(p1: Vec, p2: Vec, opts?: O): Rect<O>;
|
|
29
|
+
export declare function rect<const O extends RectOpts>(center: Vec, w: Val<number>, h: Val<number>, opts?: O): Rect<O>;
|
|
30
|
+
export declare function rect<const O extends RectOpts>(x: Val<number>, y: Val<number>, w: Val<number>, h: Val<number>, opts?: O): Rect<O>;
|