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.
Files changed (225) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/dist/animation/anim.d.ts +57 -0
  4. package/dist/animation/anim.js +318 -0
  5. package/dist/animation/combinators.d.ts +39 -0
  6. package/dist/animation/combinators.js +113 -0
  7. package/dist/animation/easings.d.ts +5 -0
  8. package/dist/animation/easings.js +5 -0
  9. package/dist/animation/index.d.ts +3 -0
  10. package/dist/animation/index.js +3 -0
  11. package/dist/assert/algebra.d.ts +20 -0
  12. package/dist/assert/algebra.js +79 -0
  13. package/dist/assert/claim.d.ts +40 -0
  14. package/dist/assert/claim.js +129 -0
  15. package/dist/assert/index.d.ts +7 -0
  16. package/dist/assert/index.js +19 -0
  17. package/dist/assert/predicates.d.ts +18 -0
  18. package/dist/assert/predicates.js +43 -0
  19. package/dist/assert/record.d.ts +20 -0
  20. package/dist/assert/record.js +78 -0
  21. package/dist/assert/scope.d.ts +42 -0
  22. package/dist/assert/scope.js +233 -0
  23. package/dist/assert/span.d.ts +37 -0
  24. package/dist/assert/span.js +68 -0
  25. package/dist/assert/tree.d.ts +22 -0
  26. package/dist/assert/tree.js +65 -0
  27. package/dist/code/code.d.ts +70 -0
  28. package/dist/code/code.js +361 -0
  29. package/dist/code/index.d.ts +2 -0
  30. package/dist/code/index.js +9 -0
  31. package/dist/code/morph.d.ts +5 -0
  32. package/dist/code/morph.js +194 -0
  33. package/dist/code/tokenize.d.ts +8 -0
  34. package/dist/code/tokenize.js +51 -0
  35. package/dist/constraints/cluster.d.ts +83 -0
  36. package/dist/constraints/cluster.js +213 -0
  37. package/dist/constraints/drivers.d.ts +15 -0
  38. package/dist/constraints/drivers.js +40 -0
  39. package/dist/constraints/factories.d.ts +73 -0
  40. package/dist/constraints/factories.js +248 -0
  41. package/dist/constraints/index.d.ts +11 -0
  42. package/dist/constraints/index.js +39 -0
  43. package/dist/constraints/interaction.d.ts +21 -0
  44. package/dist/constraints/interaction.js +148 -0
  45. package/dist/constraints/linalg.d.ts +18 -0
  46. package/dist/constraints/linalg.js +141 -0
  47. package/dist/constraints/phases.d.ts +21 -0
  48. package/dist/constraints/phases.js +60 -0
  49. package/dist/constraints/physics.d.ts +34 -0
  50. package/dist/constraints/physics.js +128 -0
  51. package/dist/constraints/rigid.d.ts +210 -0
  52. package/dist/constraints/rigid.js +835 -0
  53. package/dist/constraints/solver.d.ts +107 -0
  54. package/dist/constraints/solver.js +510 -0
  55. package/dist/constraints/term.d.ts +50 -0
  56. package/dist/constraints/term.js +80 -0
  57. package/dist/constraints/terms.d.ts +80 -0
  58. package/dist/constraints/terms.js +302 -0
  59. package/dist/constraints/world.d.ts +31 -0
  60. package/dist/constraints/world.js +245 -0
  61. package/dist/core/aggregates.d.ts +64 -0
  62. package/dist/core/aggregates.js +198 -0
  63. package/dist/core/anim.d.ts +84 -0
  64. package/dist/core/anim.js +301 -0
  65. package/dist/core/index.d.ts +38 -0
  66. package/dist/core/index.js +38 -0
  67. package/dist/core/introspect.d.ts +5 -0
  68. package/dist/core/introspect.js +31 -0
  69. package/dist/core/lenses/closed-form-policies.d.ts +64 -0
  70. package/dist/core/lenses/closed-form-policies.js +452 -0
  71. package/dist/core/lenses/domain-aggregates.d.ts +54 -0
  72. package/dist/core/lenses/domain-aggregates.js +259 -0
  73. package/dist/core/lenses/factor-lens.d.ts +42 -0
  74. package/dist/core/lenses/factor-lens.js +419 -0
  75. package/dist/core/lenses/index.d.ts +5 -0
  76. package/dist/core/lenses/index.js +16 -0
  77. package/dist/core/lenses/memory.d.ts +47 -0
  78. package/dist/core/lenses/memory.js +102 -0
  79. package/dist/core/lenses/typed-factor.d.ts +45 -0
  80. package/dist/core/lenses/typed-factor.js +376 -0
  81. package/dist/core/network-utils.d.ts +14 -0
  82. package/dist/core/network-utils.js +62 -0
  83. package/dist/core/new-primitives.d.ts +33 -0
  84. package/dist/core/new-primitives.js +113 -0
  85. package/dist/core/signal.d.ts +254 -0
  86. package/dist/core/signal.js +1349 -0
  87. package/dist/core/traits.d.ts +61 -0
  88. package/dist/core/traits.js +56 -0
  89. package/dist/core/tree.d.ts +23 -0
  90. package/dist/core/tree.js +62 -0
  91. package/dist/core/values/anchor.d.ts +23 -0
  92. package/dist/core/values/anchor.js +23 -0
  93. package/dist/core/values/audio.d.ts +33 -0
  94. package/dist/core/values/audio.js +107 -0
  95. package/dist/core/values/bool.d.ts +37 -0
  96. package/dist/core/values/bool.js +75 -0
  97. package/dist/core/values/box.d.ts +77 -0
  98. package/dist/core/values/box.js +211 -0
  99. package/dist/core/values/canvas.d.ts +71 -0
  100. package/dist/core/values/canvas.js +495 -0
  101. package/dist/core/values/color.d.ts +49 -0
  102. package/dist/core/values/color.js +106 -0
  103. package/dist/core/values/flags.d.ts +18 -0
  104. package/dist/core/values/flags.js +50 -0
  105. package/dist/core/values/gpu.d.ts +74 -0
  106. package/dist/core/values/gpu.js +426 -0
  107. package/dist/core/values/matrix.d.ts +53 -0
  108. package/dist/core/values/matrix.js +140 -0
  109. package/dist/core/values/num.d.ts +62 -0
  110. package/dist/core/values/num.js +166 -0
  111. package/dist/core/values/pose.d.ts +31 -0
  112. package/dist/core/values/pose.js +83 -0
  113. package/dist/core/values/range.d.ts +83 -0
  114. package/dist/core/values/range.js +167 -0
  115. package/dist/core/values/str.d.ts +76 -0
  116. package/dist/core/values/str.js +346 -0
  117. package/dist/core/values/template.d.ts +49 -0
  118. package/dist/core/values/template.js +148 -0
  119. package/dist/core/values/transform.d.ts +49 -0
  120. package/dist/core/values/transform.js +115 -0
  121. package/dist/core/values/tri.d.ts +31 -0
  122. package/dist/core/values/tri.js +95 -0
  123. package/dist/core/values/vec.d.ts +72 -0
  124. package/dist/core/values/vec.js +219 -0
  125. package/dist/core/writable.d.ts +15 -0
  126. package/dist/core/writable.js +29 -0
  127. package/dist/ext/events.d.ts +10 -0
  128. package/dist/ext/events.js +31 -0
  129. package/dist/ext/index.d.ts +4 -0
  130. package/dist/ext/index.js +4 -0
  131. package/dist/ext/snapshot.d.ts +8 -0
  132. package/dist/ext/snapshot.js +29 -0
  133. package/dist/ext/timeline.d.ts +56 -0
  134. package/dist/ext/timeline.js +94 -0
  135. package/dist/ext/waapi.d.ts +25 -0
  136. package/dist/ext/waapi.js +198 -0
  137. package/dist/index.d.ts +8 -0
  138. package/dist/index.js +10 -0
  139. package/dist/propagators/index.d.ts +6 -0
  140. package/dist/propagators/index.js +6 -0
  141. package/dist/propagators/layout.d.ts +68 -0
  142. package/dist/propagators/layout.js +336 -0
  143. package/dist/propagators/network.d.ts +52 -0
  144. package/dist/propagators/network.js +185 -0
  145. package/dist/propagators/propagator.d.ts +12 -0
  146. package/dist/propagators/propagator.js +16 -0
  147. package/dist/propagators/range.d.ts +45 -0
  148. package/dist/propagators/range.js +147 -0
  149. package/dist/propagators/relations.d.ts +60 -0
  150. package/dist/propagators/relations.js +343 -0
  151. package/dist/shapes/annular-sector.d.ts +15 -0
  152. package/dist/shapes/annular-sector.js +64 -0
  153. package/dist/shapes/button.d.ts +14 -0
  154. package/dist/shapes/button.js +31 -0
  155. package/dist/shapes/choreographers.d.ts +22 -0
  156. package/dist/shapes/choreographers.js +69 -0
  157. package/dist/shapes/circle.d.ts +17 -0
  158. package/dist/shapes/circle.js +57 -0
  159. package/dist/shapes/clip.d.ts +5 -0
  160. package/dist/shapes/clip.js +31 -0
  161. package/dist/shapes/connect.d.ts +16 -0
  162. package/dist/shapes/connect.js +70 -0
  163. package/dist/shapes/curve.d.ts +60 -0
  164. package/dist/shapes/curve.js +285 -0
  165. package/dist/shapes/dashed.d.ts +16 -0
  166. package/dist/shapes/dashed.js +142 -0
  167. package/dist/shapes/debug.d.ts +43 -0
  168. package/dist/shapes/debug.js +97 -0
  169. package/dist/shapes/group.d.ts +5 -0
  170. package/dist/shapes/group.js +10 -0
  171. package/dist/shapes/handle.d.ts +32 -0
  172. package/dist/shapes/handle.js +88 -0
  173. package/dist/shapes/index.d.ts +23 -0
  174. package/dist/shapes/index.js +23 -0
  175. package/dist/shapes/interaction.d.ts +32 -0
  176. package/dist/shapes/interaction.js +187 -0
  177. package/dist/shapes/label.d.ts +20 -0
  178. package/dist/shapes/label.js +42 -0
  179. package/dist/shapes/layout.d.ts +29 -0
  180. package/dist/shapes/layout.js +74 -0
  181. package/dist/shapes/line.d.ts +21 -0
  182. package/dist/shapes/line.js +79 -0
  183. package/dist/shapes/list.d.ts +18 -0
  184. package/dist/shapes/list.js +51 -0
  185. package/dist/shapes/mount.d.ts +7 -0
  186. package/dist/shapes/mount.js +10 -0
  187. package/dist/shapes/path.d.ts +77 -0
  188. package/dist/shapes/path.js +227 -0
  189. package/dist/shapes/rect.d.ts +30 -0
  190. package/dist/shapes/rect.js +131 -0
  191. package/dist/shapes/shape.d.ts +132 -0
  192. package/dist/shapes/shape.js +306 -0
  193. package/dist/shapes/text.d.ts +24 -0
  194. package/dist/shapes/text.js +53 -0
  195. package/dist/shapes/tokens.d.ts +28 -0
  196. package/dist/shapes/tokens.js +27 -0
  197. package/dist/shapes/transitions.d.ts +23 -0
  198. package/dist/shapes/transitions.js +62 -0
  199. package/dist/tex/decorations.d.ts +26 -0
  200. package/dist/tex/decorations.js +116 -0
  201. package/dist/tex/index.d.ts +5 -0
  202. package/dist/tex/index.js +5 -0
  203. package/dist/tex/marker.d.ts +17 -0
  204. package/dist/tex/marker.js +63 -0
  205. package/dist/tex/motion.d.ts +43 -0
  206. package/dist/tex/motion.js +290 -0
  207. package/dist/tex/parts.d.ts +65 -0
  208. package/dist/tex/parts.js +149 -0
  209. package/dist/tex/tex.d.ts +45 -0
  210. package/dist/tex/tex.js +244 -0
  211. package/dist/web/attr.d.ts +16 -0
  212. package/dist/web/attr.js +98 -0
  213. package/dist/web/diagram.d.ts +49 -0
  214. package/dist/web/diagram.js +260 -0
  215. package/dist/web/index.d.ts +6 -0
  216. package/dist/web/index.js +6 -0
  217. package/dist/web/md-marker.d.ts +6 -0
  218. package/dist/web/md-marker.js +39 -0
  219. package/dist/web/md-tex.d.ts +6 -0
  220. package/dist/web/md-tex.js +61 -0
  221. package/dist/web/raf.d.ts +6 -0
  222. package/dist/web/raf.js +24 -0
  223. package/dist/web/viewport.d.ts +7 -0
  224. package/dist/web/viewport.js +13 -0
  225. package/package.json +87 -0
@@ -0,0 +1,131 @@
1
+ import { Box, derive, Num, reader, Vec } from "../core/index.js";
2
+ import { Shape } from "./shape.js";
3
+ import { tokens } from "./tokens.js";
4
+ const HALF_PI = Math.PI / 2;
5
+ export class Rect extends Shape {
6
+ x;
7
+ y;
8
+ w;
9
+ h;
10
+ corner;
11
+ constructor(x, y, w, h, opts = {}) {
12
+ const xs = Num.from(x);
13
+ const ys = Num.from(y);
14
+ const ws = Num.from(w);
15
+ const hs = Num.from(h);
16
+ super(opts.dashed ? "path" : "rect", () => ({ x: xs.value, y: ys.value, w: ws.value, h: hs.value }), opts, {
17
+ origin: derive(() => ({
18
+ x: xs.value + ws.value / 2,
19
+ y: ys.value + hs.value / 2,
20
+ })),
21
+ });
22
+ this.x = xs;
23
+ this.y = ys;
24
+ this.w = ws;
25
+ this.h = hs;
26
+ this.corner = Num.from(opts.corner ?? tokens.corner);
27
+ this.stroke(opts, true, {
28
+ x: xs,
29
+ y: ys,
30
+ width: ws,
31
+ height: hs,
32
+ rx: this.corner,
33
+ ry: this.corner,
34
+ });
35
+ }
36
+ boundary(toward) {
37
+ return Vec.derive(() => {
38
+ const c = this.center.value;
39
+ const b = this.box.value;
40
+ const sc = this.scale.value;
41
+ const t = toward.value;
42
+ const halfW = (b.w / 2) * sc.x;
43
+ const halfH = (b.h / 2) * sc.y;
44
+ const dx = t.x - c.x;
45
+ const dy = t.y - c.y;
46
+ if (dx === 0 && dy === 0)
47
+ return c;
48
+ const k = Math.min(dx === 0 ? Number.POSITIVE_INFINITY : halfW / Math.abs(dx), dy === 0 ? Number.POSITIVE_INFINITY : halfH / Math.abs(dy));
49
+ return { x: c.x + dx * k, y: c.y + dy * k };
50
+ });
51
+ }
52
+ /** Concentric outline — a new unmounted Rect inflated by `by` per
53
+ * side; corner radius bumps to keep curves parallel. */
54
+ outline(by, opts) {
55
+ const b = reader(by);
56
+ return new Rect(derive(() => this.x.value - b()), derive(() => this.y.value - b()), derive(() => this.w.value + 2 * b()), derive(() => this.h.value + 2 * b()), { corner: derive(() => this.corner.value + b()), ...opts });
57
+ }
58
+ /** 4 sides + 4 corner quarter-arcs (or just sides when `corner === 0`). */
59
+ segments() {
60
+ const b = this.box.value;
61
+ const r = Math.min(this.corner.value, b.w / 2, b.h / 2);
62
+ const x = b.x;
63
+ const y = b.y;
64
+ const w = b.w;
65
+ const h = b.h;
66
+ const p = (px, py) => ({ x: px, y: py });
67
+ if (r <= 0) {
68
+ return [
69
+ { type: "line", from: p(x, y), to: p(x + w, y) },
70
+ { type: "line", from: p(x + w, y), to: p(x + w, y + h) },
71
+ { type: "line", from: p(x + w, y + h), to: p(x, y + h) },
72
+ { type: "line", from: p(x, y + h), to: p(x, y) },
73
+ ];
74
+ }
75
+ return [
76
+ { type: "line", from: p(x + r, y), to: p(x + w - r, y) },
77
+ {
78
+ type: "arc",
79
+ cx: () => x + w - r,
80
+ cy: () => y + r,
81
+ r: () => r,
82
+ a0: () => -HALF_PI,
83
+ a1: () => 0,
84
+ },
85
+ { type: "line", from: p(x + w, y + r), to: p(x + w, y + h - r) },
86
+ {
87
+ type: "arc",
88
+ cx: () => x + w - r,
89
+ cy: () => y + h - r,
90
+ r: () => r,
91
+ a0: () => 0,
92
+ a1: () => HALF_PI,
93
+ },
94
+ { type: "line", from: p(x + w - r, y + h), to: p(x + r, y + h) },
95
+ {
96
+ type: "arc",
97
+ cx: () => x + r,
98
+ cy: () => y + h - r,
99
+ r: () => r,
100
+ a0: () => HALF_PI,
101
+ a1: () => Math.PI,
102
+ },
103
+ { type: "line", from: p(x, y + h - r), to: p(x, y + r) },
104
+ {
105
+ type: "arc",
106
+ cx: () => x + r,
107
+ cy: () => y + r,
108
+ r: () => r,
109
+ a0: () => Math.PI,
110
+ a1: () => 3 * HALF_PI,
111
+ },
112
+ ];
113
+ }
114
+ }
115
+ export function rect(a, b, c, d, e) {
116
+ if (a instanceof Box) {
117
+ return new Rect(derive(() => a.value.x), derive(() => a.value.y), derive(() => a.value.w), derive(() => a.value.h), b);
118
+ }
119
+ if (a instanceof Vec && b instanceof Vec) {
120
+ // Bounding rect of two points (any orientation).
121
+ return new Rect(derive(() => Math.min(a.x.value, b.x.value)), derive(() => Math.min(a.y.value, b.y.value)), derive(() => Math.abs(b.x.value - a.x.value)), derive(() => Math.abs(b.y.value - a.y.value)), c);
122
+ }
123
+ if (a instanceof Vec) {
124
+ const w = b;
125
+ const h = c;
126
+ const ws = Num.from(w);
127
+ const hs = Num.from(h);
128
+ return new Rect(derive(() => a.x.value - ws.value / 2), derive(() => a.y.value - hs.value / 2), ws, hs, d);
129
+ }
130
+ return new Rect(a, b, c, d, e);
131
+ }
@@ -0,0 +1,132 @@
1
+ import { type Animator } from "../animation/index.js";
2
+ import { Box, Cell, type Inner, type Matrix, Num, type Val, Vec, type Writable } from "../core/index.js";
3
+ type VecValue = Inner<Vec>;
4
+ export declare const SVG_NS = "http://www.w3.org/2000/svg";
5
+ /** Stroke segment for the dashed renderer; override `segments()`. */
6
+ export type Segment = {
7
+ type: "line";
8
+ from: VecValue;
9
+ to: VecValue;
10
+ } | {
11
+ type: "arc";
12
+ cx: () => number;
13
+ cy: () => number;
14
+ r: () => number;
15
+ a0: () => number;
16
+ a1: () => number;
17
+ };
18
+ /** Shared Shape opts; each prop accepts `Val<T>`. `aside` excludes
19
+ * from parent bounds. */
20
+ export interface ShapeOpts {
21
+ translate?: Val<VecValue>;
22
+ rotate?: Val<number>;
23
+ scale?: Val<VecValue>;
24
+ origin?: Val<VecValue>;
25
+ opacity?: Val<number>;
26
+ aside?: boolean;
27
+ }
28
+ /** Stroked-shape opts. `fill: true` → stroke color; string → that
29
+ * color; omitted → no fill. */
30
+ export interface CommonOpts extends ShapeOpts {
31
+ stroke?: Val<string>;
32
+ strokeWidth?: Val<number>;
33
+ thin?: boolean;
34
+ dashed?: boolean;
35
+ cap?: "butt" | "round" | "square";
36
+ join?: "miter" | "round" | "bevel";
37
+ fill?: Val<string> | true;
38
+ }
39
+ /** Wide-form escape hatch for heterogeneous shape collections. */
40
+ export type AnyShape = Shape<any>;
41
+ export type AnimatableKey = "translate" | "rotate" | "scale" | "origin" | "opacity";
42
+ type AnimatableField<K extends AnimatableKey> = K extends "translate" | "scale" | "origin" ? Writable<Vec> : Writable<Num>;
43
+ /** Anything carrying the listed animatable axes. Combine via union. */
44
+ export type Has<K extends AnimatableKey> = {
45
+ readonly [P in K]: AnimatableField<P>;
46
+ };
47
+ /** Scene-graph node wrapping an SVG `<g>`. `translate`, `rotate`,
48
+ * `scale`, `origin`, `opacity` are independent writable cells; the
49
+ * composed `localFrame` matrix is a derived view. `center`/`top`/…
50
+ * /`at(u,v)` return parent-frame points (writes adjust `translate`);
51
+ * `shape.box.center` is local-frame. */
52
+ export declare class Shape<O extends ShapeOpts = ShapeOpts> {
53
+ #private;
54
+ readonly el: SVGGElement;
55
+ readonly intrinsic?: SVGElement;
56
+ readonly translate: Writable<Vec>;
57
+ readonly rotate: Writable<Num>;
58
+ readonly scale: Writable<Vec>;
59
+ readonly origin: Writable<Vec>;
60
+ readonly opacity: Writable<Num>;
61
+ /** Composed local-frame matrix: `T(t) T(p) R(r) S(s) T(-p)`. */
62
+ readonly localFrame: Cell<Inner<Matrix>>;
63
+ /** Local-frame box; reach into `.x`, `.center`, `.at(u,v)`, etc. */
64
+ readonly box: Box;
65
+ /** Lens-backed parent-frame anchors; writes shift `translate`. */
66
+ get center(): Writable<Vec>;
67
+ get top(): Writable<Vec>;
68
+ get bottom(): Writable<Vec>;
69
+ get left(): Writable<Vec>;
70
+ get right(): Writable<Vec>;
71
+ at(u: number, v: number): Writable<Vec>;
72
+ readonly aside: boolean;
73
+ protected disposers: (() => void)[];
74
+ private readonly _children;
75
+ /** Back-link set by `add()`; cleared by `dispose()`. Non-reactive. */
76
+ parent: AnyShape | null;
77
+ constructor(intrinsicType?: keyof SVGElementTagNameMap, boxFn?: () => Inner<Box>, opts?: O,
78
+ /** Subclass per-prop defaults (kept off `O`). */
79
+ defaults?: ShapeOpts);
80
+ /** Parent-frame perimeter point toward `target`; tighter shapes override. */
81
+ boundary(toward: Vec): Vec;
82
+ /** Stroke segments for the dashed renderer; default = bounding rect. */
83
+ segments(): Segment[];
84
+ /** Bind one SVG attribute; static sets once, reactive runs as effect. */
85
+ attr(name: string, val: Val<string | number>, target?: "intrinsic" | "wrapper"): void;
86
+ /** Bind several attributes at once — `this.attrs({ cx, cy, r })`. */
87
+ attrs(map: Record<string, Val<string | number>>, target?: "intrinsic" | "wrapper"): void;
88
+ /** Wire stroke / fill / dashed for a stroked shape. `nativeAttrs`
89
+ * binds the shape's native geometry (e.g. `{cx, cy, r}` for circle);
90
+ * it's skipped when `opts.dashed` since the intrinsic is then a
91
+ * `<path>` whose `d` is driven by `segments()`. */
92
+ stroke(opts: CommonOpts, closed: boolean, nativeAttrs?: Record<string, Val<string | number>>): void;
93
+ /** Register a disposer to run on `dispose()`. */
94
+ track(dispose: () => void): void;
95
+ /** Reactive effect torn down with the shape. */
96
+ effect(fn: () => void): void;
97
+ on(name: string, handler: (e: Event) => void, opts?: AddEventListenerOptions): () => void;
98
+ /** Wake on the next `name` event; resume with the event. */
99
+ until(name: string): Animator<Event>;
100
+ /** Map client coords into this shape's local frame. */
101
+ toLocal(evt: {
102
+ clientX: number;
103
+ clientY: number;
104
+ }): VecValue;
105
+ /** Map client coords into the SVG root's frame; stable under rotation
106
+ * (unlike `toLocal`). Returns `(0, 0)` when detached. */
107
+ toWorld(evt: {
108
+ clientX: number;
109
+ clientY: number;
110
+ }): VecValue;
111
+ /** Nearest enclosing `<svg>` root, or `null` if this shape isn't mounted
112
+ * under one. Used by drag helpers that need world-space cursor coords. */
113
+ get svgRoot(): SVGSVGElement | null;
114
+ add<T extends AnyShape>(child: T): T;
115
+ add<T extends AnyShape[]>(...children: T): T;
116
+ remove(...toRemove: AnyShape[]): void;
117
+ clear(): void;
118
+ dispose(): void;
119
+ }
120
+ /** Writable centroid of shapes' translates. */
121
+ export declare function centroid(...shapes: {
122
+ translate: Writable<Vec>;
123
+ }[]): Writable<Vec>;
124
+ /** Writable mean rotation. */
125
+ export declare function meanRotation(...shapes: {
126
+ rotate: Writable<Num>;
127
+ }[]): Writable<Num>;
128
+ /** Writable mean scale. */
129
+ export declare function meanScale(...shapes: {
130
+ scale: Writable<Vec>;
131
+ }[]): Writable<Vec>;
132
+ export {};
@@ -0,0 +1,306 @@
1
+ import { suspend } from "../animation/index.js";
2
+ import { Box, BoxMath, Cell, cell, centroidLens, compose, derive, effect, lazy, meanLens, Num, readNow, toMatrixString, transformBox, transformPoint, Vec, } from "../core/index.js";
3
+ import { dashedPath } from "./dashed.js";
4
+ import { tokens } from "./tokens.js";
5
+ export const SVG_NS = "http://www.w3.org/2000/svg";
6
+ /** Scene-graph node wrapping an SVG `<g>`. `translate`, `rotate`,
7
+ * `scale`, `origin`, `opacity` are independent writable cells; the
8
+ * composed `localFrame` matrix is a derived view. `center`/`top`/…
9
+ * /`at(u,v)` return parent-frame points (writes adjust `translate`);
10
+ * `shape.box.center` is local-frame. */
11
+ export class Shape {
12
+ el;
13
+ intrinsic;
14
+ translate;
15
+ rotate;
16
+ scale;
17
+ origin;
18
+ opacity;
19
+ /** Composed local-frame matrix: `T(t) T(p) R(r) S(s) T(-p)`. */
20
+ localFrame;
21
+ /** Local-frame box; reach into `.x`, `.center`, `.at(u,v)`, etc. */
22
+ box;
23
+ /** Lens-backed parent-frame anchors; writes shift `translate`. */
24
+ get center() {
25
+ return lazy(this, "center", () => this.#makeAnchor(0.5, 0.5));
26
+ }
27
+ get top() {
28
+ return lazy(this, "top", () => this.#makeAnchor(0.5, 0));
29
+ }
30
+ get bottom() {
31
+ return lazy(this, "bottom", () => this.#makeAnchor(0.5, 1));
32
+ }
33
+ get left() {
34
+ return lazy(this, "left", () => this.#makeAnchor(0, 0.5));
35
+ }
36
+ get right() {
37
+ return lazy(this, "right", () => this.#makeAnchor(1, 0.5));
38
+ }
39
+ at(u, v) {
40
+ return this.#makeAnchor(u, v);
41
+ }
42
+ aside;
43
+ disposers = [];
44
+ // Cell (not array) so the default group `boxFn` re-unions on add/remove.
45
+ _children = cell([]);
46
+ /** Back-link set by `add()`; cleared by `dispose()`. Non-reactive. */
47
+ parent = null;
48
+ constructor(intrinsicType, boxFn, opts = {},
49
+ /** Subclass per-prop defaults (kept off `O`). */
50
+ defaults = {}) {
51
+ this.el = document.createElementNS(SVG_NS, "g");
52
+ // CSS `transform` (vs SVG `transform`) hits the GPU composite path.
53
+ // Pin origin to userspace 0,0 so composed pivot math is correct.
54
+ this.el.style.transformOrigin = "0 0";
55
+ if (intrinsicType) {
56
+ this.intrinsic = document.createElementNS(SVG_NS, intrinsicType);
57
+ this.el.appendChild(this.intrinsic);
58
+ }
59
+ // Each animatable axis held directly via liftAnimatable; localFrame
60
+ // is the derived matrix below.
61
+ this.translate = liftAnimatable(opts.translate ?? defaults.translate ?? { x: 0, y: 0 }, Vec, this.disposers);
62
+ this.rotate = liftAnimatable(opts.rotate ?? defaults.rotate ?? 0, Num, this.disposers);
63
+ this.scale = liftAnimatable(opts.scale ?? defaults.scale ?? { x: 1, y: 1 }, Vec, this.disposers);
64
+ this.origin = liftAnimatable(opts.origin ?? defaults.origin ?? { x: 0, y: 0 }, Vec, this.disposers);
65
+ this.opacity = liftAnimatable(opts.opacity ?? defaults.opacity ?? 1, Num, this.disposers);
66
+ this.aside = opts.aside ?? defaults.aside ?? false;
67
+ // Group default: union of non-aside children's boxes through localFrame.
68
+ const boxSig = Box.derive(boxFn ??
69
+ (() => {
70
+ const cs = this._children.value
71
+ .filter(c => !c.aside)
72
+ .map(c => transformBox(c.localFrame.value, c.box.value));
73
+ return cs.length ? BoxMath.union(...cs) : { x: 0, y: 0, w: 0, h: 0 };
74
+ }));
75
+ this.box = boxSig;
76
+ // Identity short-circuit avoids reading `origin` on no-transform groups.
77
+ this.localFrame = derive(() => {
78
+ const t = this.translate.value;
79
+ const r = this.rotate.value;
80
+ const sc = this.scale.value;
81
+ if (t.x === 0 && t.y === 0 && r === 0 && sc.x === 1 && sc.y === 1) {
82
+ return compose(t, r, sc, { x: 0, y: 0 });
83
+ }
84
+ return compose(t, r, sc, this.origin.value);
85
+ });
86
+ this.disposers.push(effect(() => {
87
+ this.el.style.transform = toMatrixString(this.localFrame.value);
88
+ this.el.style.opacity = String(this.opacity.value);
89
+ }));
90
+ }
91
+ /** Parent-frame perimeter point toward `target`; tighter shapes override. */
92
+ boundary(toward) {
93
+ return Vec.derive(() => BoxMath.edgeFrom(transformBox(this.localFrame.value, this.box.value), toward.value));
94
+ }
95
+ #makeAnchor(u, v) {
96
+ // Reads box/localFrame/translate; writes only translate, shifted by the
97
+ // world-space delta so the anchor lands at target (anchor-drag = translate).
98
+ return Vec.lens([this.box, this.localFrame, this.translate], vals => {
99
+ const [b, m] = vals;
100
+ return transformPoint(m, { x: b.x + u * b.w, y: b.y + v * b.h });
101
+ }, (target, vals) => {
102
+ const [b, m, tNow] = vals;
103
+ const local = { x: b.x + u * b.w, y: b.y + v * b.h };
104
+ const currentWorld = transformPoint(m, local);
105
+ return [
106
+ undefined,
107
+ undefined,
108
+ {
109
+ x: tNow.x + (target.x - currentWorld.x),
110
+ y: tNow.y + (target.y - currentWorld.y),
111
+ },
112
+ ];
113
+ });
114
+ }
115
+ /** Stroke segments for the dashed renderer; default = bounding rect. */
116
+ segments() {
117
+ const b = this.box.value;
118
+ return [
119
+ { type: "line", from: { x: b.x, y: b.y }, to: { x: b.x + b.w, y: b.y } },
120
+ { type: "line", from: { x: b.x + b.w, y: b.y }, to: { x: b.x + b.w, y: b.y + b.h } },
121
+ { type: "line", from: { x: b.x + b.w, y: b.y + b.h }, to: { x: b.x, y: b.y + b.h } },
122
+ { type: "line", from: { x: b.x, y: b.y + b.h }, to: { x: b.x, y: b.y } },
123
+ ];
124
+ }
125
+ /** Bind one SVG attribute; static sets once, reactive runs as effect. */
126
+ attr(name, val, target = "intrinsic") {
127
+ const el = target === "intrinsic" && this.intrinsic ? this.intrinsic : this.el;
128
+ if (val instanceof Cell || typeof val === "function") {
129
+ this.disposers.push(effect(() => el.setAttribute(name, String(readNow(val)))));
130
+ }
131
+ else {
132
+ el.setAttribute(name, String(val));
133
+ }
134
+ }
135
+ /** Bind several attributes at once — `this.attrs({ cx, cy, r })`. */
136
+ attrs(map, target = "intrinsic") {
137
+ for (const k in map)
138
+ this.attr(k, map[k], target);
139
+ }
140
+ /** Wire stroke / fill / dashed for a stroked shape. `nativeAttrs`
141
+ * binds the shape's native geometry (e.g. `{cx, cy, r}` for circle);
142
+ * it's skipped when `opts.dashed` since the intrinsic is then a
143
+ * `<path>` whose `d` is driven by `segments()`. */
144
+ stroke(opts, closed, nativeAttrs) {
145
+ if (opts.dashed) {
146
+ const cap = opts.cap ?? "round";
147
+ this.attr("stroke-linecap", cap);
148
+ // Resolve strokeWidth at construction; dash geometry assumes
149
+ // a static weight (capExtension is baked into the path string).
150
+ const w = opts.strokeWidth === undefined
151
+ ? opts.thin
152
+ ? tokens.thinWeight
153
+ : tokens.weight
154
+ : readNow(opts.strokeWidth);
155
+ const capExt = cap === "round" ? w : 0;
156
+ this.attr("d", derive(() => dashedPath(this.segments(), { closed, capExtension: capExt })));
157
+ }
158
+ else if (nativeAttrs) {
159
+ this.attrs(nativeAttrs);
160
+ }
161
+ this.attr("stroke", opts.stroke ?? tokens.stroke);
162
+ this.attr("stroke-width", opts.strokeWidth ?? (opts.thin ? tokens.thinWeight : tokens.weight));
163
+ this.attr("vector-effect", "non-scaling-stroke");
164
+ if (opts.cap)
165
+ this.attr("stroke-linecap", opts.cap);
166
+ if (opts.join)
167
+ this.attr("stroke-linejoin", opts.join);
168
+ if (opts.fill === undefined)
169
+ this.attr("fill", "none");
170
+ else if (opts.fill === true)
171
+ this.attr("fill", tokens.stroke);
172
+ else
173
+ this.attr("fill", opts.fill);
174
+ }
175
+ /** Register a disposer to run on `dispose()`. */
176
+ track(dispose) {
177
+ this.disposers.push(dispose);
178
+ }
179
+ /** Reactive effect torn down with the shape. */
180
+ effect(fn) {
181
+ this.disposers.push(effect(fn));
182
+ }
183
+ on(name, handler, opts) {
184
+ const el = this.el;
185
+ el.addEventListener(name, handler, opts);
186
+ const dispose = () => el.removeEventListener(name, handler, opts);
187
+ this.disposers.push(dispose);
188
+ return dispose;
189
+ }
190
+ /** Wake on the next `name` event; resume with the event. */
191
+ until(name) {
192
+ return suspend(wake => {
193
+ const handler = (e) => wake(e);
194
+ return this.on(name, handler, { once: true });
195
+ });
196
+ }
197
+ /** Map client coords into this shape's local frame. */
198
+ toLocal(evt) {
199
+ const target = (this.intrinsic ?? this.el);
200
+ const ctm = target.getScreenCTM();
201
+ if (!ctm)
202
+ return { x: 0, y: 0 };
203
+ const inv = ctm.inverse();
204
+ return {
205
+ x: evt.clientX * inv.a + evt.clientY * inv.c + inv.e,
206
+ y: evt.clientX * inv.b + evt.clientY * inv.d + inv.f,
207
+ };
208
+ }
209
+ /** Map client coords into the SVG root's frame; stable under rotation
210
+ * (unlike `toLocal`). Returns `(0, 0)` when detached. */
211
+ toWorld(evt) {
212
+ const root = this.svgRoot;
213
+ const ctm = root?.getScreenCTM();
214
+ if (!ctm)
215
+ return { x: 0, y: 0 };
216
+ const inv = ctm.inverse();
217
+ return {
218
+ x: evt.clientX * inv.a + evt.clientY * inv.c + inv.e,
219
+ y: evt.clientX * inv.b + evt.clientY * inv.d + inv.f,
220
+ };
221
+ }
222
+ /** Nearest enclosing `<svg>` root, or `null` if this shape isn't mounted
223
+ * under one. Used by drag helpers that need world-space cursor coords. */
224
+ get svgRoot() {
225
+ let walker = this.el;
226
+ while (walker) {
227
+ if (walker.namespaceURI === SVG_NS && walker.tagName === "svg") {
228
+ return walker;
229
+ }
230
+ walker = walker.parentElement;
231
+ }
232
+ return null;
233
+ }
234
+ add(...children) {
235
+ for (const child of children) {
236
+ this.el.appendChild(child.el);
237
+ child.parent = this;
238
+ }
239
+ if (children.length > 0) {
240
+ this._children.value = [...this._children.peek(), ...children];
241
+ }
242
+ return children.length === 1 ? children[0] : children;
243
+ }
244
+ remove(...toRemove) {
245
+ if (toRemove.length === 0)
246
+ return;
247
+ const removeSet = new Set(toRemove);
248
+ const next = [];
249
+ for (const c of this._children.peek()) {
250
+ if (removeSet.has(c))
251
+ c.dispose();
252
+ else
253
+ next.push(c);
254
+ }
255
+ if (next.length !== this._children.peek().length) {
256
+ this._children.value = next;
257
+ }
258
+ }
259
+ clear() {
260
+ const cs = this._children.peek();
261
+ if (cs.length === 0)
262
+ return;
263
+ cs.forEach(c => c.dispose());
264
+ this._children.value = [];
265
+ }
266
+ dispose() {
267
+ this._children.peek().forEach(c => c.dispose());
268
+ this._children.value = [];
269
+ this.disposers.forEach(d => d());
270
+ this.disposers = [];
271
+ this.parent = null;
272
+ this.el.remove();
273
+ }
274
+ }
275
+ // Sugar over the N-input aggregate lenses: read the mean, write the delta
276
+ // evenly to all members.
277
+ /** Writable centroid of shapes' translates. */
278
+ export function centroid(...shapes) {
279
+ return centroidLens(shapes.map(s => s.translate));
280
+ }
281
+ /** Writable mean rotation. */
282
+ export function meanRotation(...shapes) {
283
+ return meanLens(Num, shapes.map(s => s.rotate));
284
+ }
285
+ /** Writable mean scale. */
286
+ export function meanScale(...shapes) {
287
+ return meanLens(Vec, shapes.map(s => s.scale));
288
+ }
289
+ /** Lift a `Val<T>` to a `Writable<Cls<T>>` for Shape's animatable surface.
290
+ * Writable passes through; literal seeds a cell; signal/thunk drives it via
291
+ * a disposer-tracked effect. The library's only effect-driven RO mirror,
292
+ * tolerated because the surface must stay writable for tween/drag/write. */
293
+ function liftAnimatable(src, Cls, disposers) {
294
+ if (src instanceof Cls)
295
+ return src;
296
+ const target = new Cls();
297
+ if (src instanceof Cell || typeof src === "function") {
298
+ disposers.push(effect(() => {
299
+ target.value = readNow(src);
300
+ }));
301
+ }
302
+ else {
303
+ target.value = src;
304
+ }
305
+ return target;
306
+ }
@@ -0,0 +1,24 @@
1
+ interface TextStyle {
2
+ bold?: boolean;
3
+ italic?: boolean;
4
+ muted?: boolean;
5
+ sub?: boolean;
6
+ sup?: boolean;
7
+ }
8
+ export type TextPart = string | Text;
9
+ export declare class Text {
10
+ parts: TextPart[];
11
+ style: TextStyle;
12
+ constructor(parts: TextPart[], style?: TextStyle);
13
+ bold(): Text;
14
+ italic(): Text;
15
+ muted(): Text;
16
+ sub(...parts: TextPart[]): Text;
17
+ sup(...parts: TextPart[]): Text;
18
+ }
19
+ export type Content = string | Text;
20
+ export declare const t: (...parts: TextPart[]) => Text;
21
+ export declare const renderContent: (c: Content) => string;
22
+ /** Plain-text flatten — used to approximate label widths. */
23
+ export declare function flattenText(c: Content): string;
24
+ export {};
@@ -0,0 +1,53 @@
1
+ // Chainable rich text — nested styled spans, pure values (no DOM).
2
+ // Rendered to `<tspan>` markup by `label()`.
3
+ import { tokens } from "./tokens.js";
4
+ export class Text {
5
+ parts;
6
+ style;
7
+ constructor(parts, style = {}) {
8
+ this.parts = parts;
9
+ this.style = style;
10
+ }
11
+ bold() {
12
+ return new Text(this.parts, { ...this.style, bold: true });
13
+ }
14
+ italic() {
15
+ return new Text(this.parts, { ...this.style, italic: true });
16
+ }
17
+ muted() {
18
+ return new Text(this.parts, { ...this.style, muted: true });
19
+ }
20
+ sub(...parts) {
21
+ return new Text([this, new Text(parts, { sub: true })]);
22
+ }
23
+ sup(...parts) {
24
+ return new Text([this, new Text(parts, { sup: true })]);
25
+ }
26
+ }
27
+ export const t = (...parts) => new Text(parts);
28
+ const escapeXml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
29
+ function renderTextNode(node) {
30
+ if (typeof node === "string")
31
+ return escapeXml(node);
32
+ const inner = node.parts.map(renderTextNode).join("");
33
+ const a = [];
34
+ if (node.style.bold)
35
+ a.push('font-weight="700"');
36
+ if (node.style.italic)
37
+ a.push('font-style="italic"');
38
+ if (node.style.muted)
39
+ a.push(`opacity="${tokens.mutedOpacity}"`);
40
+ if (node.style.sub)
41
+ a.push(`baseline-shift="sub" font-size="${tokens.subFontSize}"`);
42
+ if (node.style.sup)
43
+ a.push(`baseline-shift="super" font-size="${tokens.subFontSize}"`);
44
+ return a.length ? `<tspan ${a.join(" ")}>${inner}</tspan>` : inner;
45
+ }
46
+ export const renderContent = (c) => typeof c === "string" ? escapeXml(c) : renderTextNode(c);
47
+ /** Plain-text flatten — used to approximate label widths. */
48
+ export function flattenText(c) {
49
+ if (typeof c === "string")
50
+ return c;
51
+ const walk = (n) => (typeof n === "string" ? n : n.parts.map(walk).join(""));
52
+ return walk(c);
53
+ }
@@ -0,0 +1,28 @@
1
+ export declare const tokens: {
2
+ /** CSS var so dark mode flips automatically. */
3
+ readonly stroke: "var(--text-color)";
4
+ readonly weight: 2;
5
+ readonly thinWeight: 1.5;
6
+ readonly corner: 2;
7
+ readonly font: "'New CM', monospace";
8
+ /** Stack of fonts with OpenType MATH tables. */
9
+ readonly mathFont: "'New CM Math', 'Cambria Math', 'STIXTwoMath-Regular', 'NotoSansMath-Regular', 'New CM', math, serif";
10
+ readonly fontSize: 14;
11
+ /** Approximate glyph aspect (SVG can't measure). */
12
+ readonly charWidth: 0.6;
13
+ readonly subFontSize: "0.75em";
14
+ readonly mutedOpacity: 0.5;
15
+ readonly tex: {
16
+ readonly size: 26;
17
+ readonly highlightColor: "rgba(255, 220, 80, 0.45)";
18
+ readonly highlightDurationMs: 120;
19
+ readonly highlightCorner: 2;
20
+ };
21
+ readonly decoration: {
22
+ readonly gap: 2;
23
+ readonly braceHeight: 5;
24
+ readonly braceGap: 3;
25
+ readonly crossGap: 1;
26
+ };
27
+ };
28
+ export type Tokens = typeof tokens;