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,343 @@
1
+ // relations.ts — propagator combinators (the solver-role layer).
2
+ //
3
+ // Where lenses define values, these propagators impose relations
4
+ // between cells that already exist for other reasons (handles,
5
+ // animation targets, external signals). Each combinator returns
6
+ // `Propagator`s for `p.add(...)`.
7
+ //
8
+ // Arithmetic combinators dispatch on the value class's `linear`
9
+ // trait, so `add(a, b, c)` works for `Num`, `Vec`, `Box`, `Pose`,
10
+ // anything `Linear<T>`.
11
+ import { isCell, reader, requireLinear } from "../core/index.js";
12
+ import { propagator } from "./propagator.js";
13
+ /** `a + b = c`. Three propagators (any two derive the third). */
14
+ export function add(a, b, c) {
15
+ const L = requireLinear(a);
16
+ return [
17
+ propagator([a, b], [c], () => {
18
+ c.value = L.add(a.value, b.value);
19
+ }),
20
+ propagator([a, c], [b], () => {
21
+ b.value = L.sub(c.value, a.value);
22
+ }),
23
+ propagator([b, c], [a], () => {
24
+ a.value = L.sub(c.value, b.value);
25
+ }),
26
+ ];
27
+ }
28
+ /** `a - b = c`. b-deriving propagator listed before a-deriving so
29
+ * drag-on-c updates b first (matches "c changed because b changed"). */
30
+ export function sub(a, b, c) {
31
+ const L = requireLinear(a);
32
+ return [
33
+ propagator([a, b], [c], () => {
34
+ c.value = L.sub(a.value, b.value);
35
+ }),
36
+ propagator([a, c], [b], () => {
37
+ b.value = L.sub(a.value, c.value);
38
+ }),
39
+ propagator([b, c], [a], () => {
40
+ a.value = L.add(b.value, c.value);
41
+ }),
42
+ ];
43
+ }
44
+ /** `(a + b) / 2 = m` (midpoint). Drag m → both a and b translate
45
+ * by the delta; drag a or b → m re-derives. */
46
+ export function mid(a, b, m) {
47
+ const L = requireLinear(a);
48
+ return [
49
+ propagator([a, b], [m], () => {
50
+ m.value = L.scale(L.add(a.value, b.value), 0.5);
51
+ }),
52
+ propagator([m], [a, b], () => {
53
+ const cur = L.scale(L.add(a.value, b.value), 0.5);
54
+ const delta = L.sub(m.value, cur);
55
+ a.value = L.add(a.value, delta);
56
+ b.value = L.add(b.value, delta);
57
+ }),
58
+ ];
59
+ }
60
+ /** Centroid of N values: `c = mean(...vs)`. Drag any vertex →
61
+ * centroid follows; drag centroid → all vertices translate by the
62
+ * delta (rigid translation of the cluster). */
63
+ export function centroid(c, ...vs) {
64
+ if (vs.length === 0)
65
+ return [];
66
+ const L = requireLinear(c);
67
+ const inv = 1 / vs.length;
68
+ const computeMean = () => {
69
+ let acc = vs[0].value;
70
+ for (let i = 1; i < vs.length; i++)
71
+ acc = L.add(acc, vs[i].value);
72
+ return L.scale(acc, inv);
73
+ };
74
+ return [
75
+ propagator(vs, [c], () => {
76
+ c.value = computeMean();
77
+ }),
78
+ propagator([c], vs, () => {
79
+ const cur = computeMean();
80
+ const delta = L.sub(c.value, cur);
81
+ for (const v of vs)
82
+ v.value = L.add(v.value, delta);
83
+ }),
84
+ ];
85
+ }
86
+ /** `a * b = c` (scalar). Three propagators; division-by-zero skips
87
+ * the inverse direction. */
88
+ export function mul(a, b, c) {
89
+ return [
90
+ propagator([a, b], [c], () => {
91
+ c.value = a.value * b.value;
92
+ }),
93
+ propagator([a, c], [b], () => {
94
+ const av = a.value;
95
+ if (av !== 0)
96
+ b.value = c.value / av;
97
+ }),
98
+ propagator([b, c], [a], () => {
99
+ const bv = b.value;
100
+ if (bv !== 0)
101
+ a.value = c.value / bv;
102
+ }),
103
+ ];
104
+ }
105
+ /** `a / b = k` constant aspect ratio (scalar). */
106
+ export function aspectRatio(a, b, k) {
107
+ return [
108
+ propagator([a], [b], () => {
109
+ b.value = a.value / k;
110
+ }),
111
+ propagator([b], [a], () => {
112
+ a.value = b.value * k;
113
+ }),
114
+ ];
115
+ }
116
+ /** `a₁ + a₂ + … + aₙ = total` (scalar). N+1 propagators: parts → total,
117
+ * and total + (n-1 parts) → missing part for each i. */
118
+ export function sum(parts, total) {
119
+ if (parts.length === 0)
120
+ return [];
121
+ if (parts.length === 1)
122
+ return eq(parts[0], total);
123
+ const props = [];
124
+ props.push(propagator(parts, [total], () => {
125
+ let s = 0;
126
+ for (const p of parts)
127
+ s += p.value;
128
+ total.value = s;
129
+ }));
130
+ for (let i = 0; i < parts.length; i++) {
131
+ const missing = parts[i];
132
+ const others = parts.filter((_, j) => j !== i);
133
+ props.push(propagator([total, ...others], [missing], () => {
134
+ let s = 0;
135
+ for (const o of others)
136
+ s += o.value;
137
+ missing.value = total.value - s;
138
+ }));
139
+ }
140
+ return props;
141
+ }
142
+ /** `a = b`. Bidirectional. Works for any value type. */
143
+ export function eq(a, b) {
144
+ return [
145
+ propagator([a], [b], () => {
146
+ b.value = a.value;
147
+ }),
148
+ propagator([b], [a], () => {
149
+ a.value = b.value;
150
+ }),
151
+ ];
152
+ }
153
+ /** Pin a signal to a fixed value. Subscribes to its own target so
154
+ * external writes that diverge from the constant get restored. */
155
+ export function constant(s, v) {
156
+ return propagator([s], [s], () => {
157
+ s.value = v;
158
+ });
159
+ }
160
+ /** Variadic mutual equality: all cells share the same value. Drag
161
+ * any one → others follow. N(N-1) propagators. */
162
+ export function align(...cells) {
163
+ const props = [];
164
+ for (let i = 0; i < cells.length; i++) {
165
+ for (let j = 0; j < cells.length; j++) {
166
+ if (i === j)
167
+ continue;
168
+ const src = cells[i];
169
+ const dst = cells[j];
170
+ props.push(propagator([src], [dst], () => {
171
+ dst.value = src.value;
172
+ }));
173
+ }
174
+ }
175
+ return props;
176
+ }
177
+ /** Point at parameter `t ∈ [0,1]` along segment a → b.
178
+ *
179
+ * Drag p (default): project p onto the segment, update t (clamped).
180
+ * Drag p with `freeze: 'a'`: a stays, b moves to fit.
181
+ * Drag p with `freeze: 'b'`: b stays, a moves to fit. */
182
+ export function between(a, b, t, p, freeze) {
183
+ const props = [];
184
+ // Forward: p = a + t * (b - a).
185
+ props.push(propagator([a.x, a.y, b.x, b.y, t], [p.x, p.y], () => {
186
+ const tv = t.value;
187
+ p.x.value = a.x.value + tv * (b.x.value - a.x.value);
188
+ p.y.value = a.y.value + tv * (b.y.value - a.y.value);
189
+ }));
190
+ if (freeze === "b") {
191
+ props.push(propagator([p.x, p.y, t, b.x, b.y], [a.x, a.y], () => {
192
+ const tv = t.value;
193
+ if (tv === 1)
194
+ return;
195
+ a.x.value = (p.x.value - tv * b.x.value) / (1 - tv);
196
+ a.y.value = (p.y.value - tv * b.y.value) / (1 - tv);
197
+ }));
198
+ }
199
+ else if (freeze === "a") {
200
+ props.push(propagator([p.x, p.y, t, a.x, a.y], [b.x, b.y], () => {
201
+ const tv = t.value;
202
+ if (tv === 0)
203
+ return;
204
+ b.x.value = (p.x.value - (1 - tv) * a.x.value) / tv;
205
+ b.y.value = (p.y.value - (1 - tv) * a.y.value) / tv;
206
+ }));
207
+ }
208
+ else {
209
+ props.push(propagator([p.x, p.y, a.x, a.y, b.x, b.y], [t], () => {
210
+ const dx = b.x.value - a.x.value;
211
+ const dy = b.y.value - a.y.value;
212
+ const len2 = dx * dx + dy * dy;
213
+ if (len2 < 1e-12)
214
+ return;
215
+ const px = p.x.value - a.x.value;
216
+ const py = p.y.value - a.y.value;
217
+ t.value = Math.max(0, Math.min(1, (px * dx + py * dy) / len2));
218
+ }));
219
+ }
220
+ return props;
221
+ }
222
+ /** Keep `|a − b| = d`. Drag a → b moves along (b−a) to maintain
223
+ * distance; drag b → symmetric. `d` may be a number, a Num signal,
224
+ * a computed (e.g. `|p − q|` driven by other points), or a closure. */
225
+ export function keepDistance(a, b, d) {
226
+ const dRead = reader(d);
227
+ const dDeps = isCell(d) ? [d] : [];
228
+ return [
229
+ propagator([a.x, a.y, ...dDeps], [b.x, b.y], () => {
230
+ const dx = b.x.value - a.x.value;
231
+ const dy = b.y.value - a.y.value;
232
+ const cur = Math.hypot(dx, dy);
233
+ if (cur < 1e-12)
234
+ return;
235
+ const k = dRead() / cur;
236
+ b.x.value = a.x.value + dx * k;
237
+ b.y.value = a.y.value + dy * k;
238
+ }),
239
+ propagator([b.x, b.y, ...dDeps], [a.x, a.y], () => {
240
+ const dx = a.x.value - b.x.value;
241
+ const dy = a.y.value - b.y.value;
242
+ const cur = Math.hypot(dx, dy);
243
+ if (cur < 1e-12)
244
+ return;
245
+ const k = dRead() / cur;
246
+ a.x.value = b.x.value + dx * k;
247
+ a.y.value = b.y.value + dy * k;
248
+ }),
249
+ ];
250
+ }
251
+ /** Project `p` onto the line through `a → b`. p sticks to the line. */
252
+ export function onLine(p, a, b) {
253
+ return propagator([p.x, p.y, a.x, a.y, b.x, b.y], [p.x, p.y], () => {
254
+ const dx = b.x.value - a.x.value;
255
+ const dy = b.y.value - a.y.value;
256
+ const len2 = dx * dx + dy * dy;
257
+ if (len2 < 1e-12)
258
+ return;
259
+ const px = p.x.value - a.x.value;
260
+ const py = p.y.value - a.y.value;
261
+ const t = (px * dx + py * dy) / len2;
262
+ const newX = a.x.value + t * dx;
263
+ const newY = a.y.value + t * dy;
264
+ if (Math.abs(newX - p.x.value) > 1e-9 || Math.abs(newY - p.y.value) > 1e-9) {
265
+ p.x.value = newX;
266
+ p.y.value = newY;
267
+ }
268
+ });
269
+ }
270
+ /** Keep `p` on a circle of radius `r` around `c`. `r` may be a
271
+ * number, a Num signal, a computed, or a closure. */
272
+ export function onCircle(p, c, r) {
273
+ const rRead = reader(r);
274
+ const rDeps = isCell(r) ? [r] : [];
275
+ return propagator([p.x, p.y, c.x, c.y, ...rDeps], [p.x, p.y], () => {
276
+ const dx = p.x.value - c.x.value;
277
+ const dy = p.y.value - c.y.value;
278
+ const cur = Math.hypot(dx, dy);
279
+ if (cur < 1e-12)
280
+ return;
281
+ const k = rRead() / cur;
282
+ const newX = c.x.value + dx * k;
283
+ const newY = c.y.value + dy * k;
284
+ if (Math.abs(newX - p.x.value) > 1e-9 || Math.abs(newY - p.y.value) > 1e-9) {
285
+ p.x.value = newX;
286
+ p.y.value = newY;
287
+ }
288
+ });
289
+ }
290
+ /** Reflect `src` across the line through `a → b` to get `dst`.
291
+ * Bidirectional. */
292
+ export function reflect(src, a, b, dst) {
293
+ const reflectPt = (px, py, ax, ay, bx, by) => {
294
+ const dx = bx - ax;
295
+ const dy = by - ay;
296
+ const len2 = dx * dx + dy * dy;
297
+ if (len2 < 1e-12)
298
+ return [px, py];
299
+ const t = ((px - ax) * dx + (py - ay) * dy) / len2;
300
+ const cx = ax + t * dx;
301
+ const cy = ay + t * dy;
302
+ return [2 * cx - px, 2 * cy - py];
303
+ };
304
+ return [
305
+ propagator([src.x, src.y, a.x, a.y, b.x, b.y], [dst.x, dst.y], () => {
306
+ const [x, y] = reflectPt(src.x.value, src.y.value, a.x.value, a.y.value, b.x.value, b.y.value);
307
+ dst.x.value = x;
308
+ dst.y.value = y;
309
+ }),
310
+ propagator([dst.x, dst.y, a.x, a.y, b.x, b.y], [src.x, src.y], () => {
311
+ const [x, y] = reflectPt(dst.x.value, dst.y.value, a.x.value, a.y.value, b.x.value, b.y.value);
312
+ src.x.value = x;
313
+ src.y.value = y;
314
+ }),
315
+ ];
316
+ }
317
+ /** "These cells must contain DIFFERENT values." If any cell is a
318
+ * singleton {v}, eliminate v from the others. */
319
+ export function allDifferent(...cells) {
320
+ const props = [];
321
+ for (let i = 0; i < cells.length; i++) {
322
+ for (let j = 0; j < cells.length; j++) {
323
+ if (i === j)
324
+ continue;
325
+ const src = cells[i];
326
+ const dst = cells[j];
327
+ props.push(propagator([src], [dst], () => {
328
+ const sv = src.value;
329
+ if (sv.size !== 1)
330
+ return;
331
+ const [only] = sv;
332
+ const dv = dst.value;
333
+ if (!dv.has(only))
334
+ return;
335
+ const next = new Set(dv);
336
+ next.delete(only);
337
+ if (next.size !== dv.size)
338
+ dst.value = next;
339
+ }));
340
+ }
341
+ }
342
+ return props;
343
+ }
@@ -0,0 +1,15 @@
1
+ import { type Cell, type Val, type Vec } from "../core/index.js";
2
+ import { type CommonOpts, type Segment, Shape } from "./shape.js";
3
+ export interface AnnularSectorOpts extends CommonOpts {
4
+ }
5
+ /** Pie wedge with a hole — between two radii swept across two angles. */
6
+ export declare class AnnularSector<O extends AnnularSectorOpts = AnnularSectorOpts> extends Shape<O> {
7
+ readonly rOuter: Cell<number>;
8
+ readonly rInner: Cell<number>;
9
+ readonly a0: Cell<number>;
10
+ readonly a1: Cell<number>;
11
+ constructor(center: Vec, rOuter: Val<number>, rInner: Val<number>, a0: Val<number>, a1: Val<number>, opts?: O);
12
+ /** Local-frame segments, derived from the Box (center = the given center). */
13
+ segments(): Segment[];
14
+ }
15
+ export declare const annularSector: <const O extends AnnularSectorOpts>(center: Vec, rOuter: Val<number>, rInner: Val<number>, a0: Val<number>, a1: Val<number>, opts?: O) => AnnularSector<O>;
@@ -0,0 +1,64 @@
1
+ import { derive, Num } from "../core/index.js";
2
+ import { Shape } from "./shape.js";
3
+ /** Pie wedge with a hole — between two radii swept across two angles. */
4
+ export class AnnularSector extends Shape {
5
+ rOuter;
6
+ rInner;
7
+ a0;
8
+ a1;
9
+ constructor(center, rOuter, rInner, a0, a1, opts = {}) {
10
+ const ro = Num.from(rOuter);
11
+ const ri = Num.from(rInner);
12
+ const a0s = Num.from(a0);
13
+ const a1s = Num.from(a1);
14
+ super("path", () => ({
15
+ x: center.x.value - ro.value,
16
+ y: center.y.value - ro.value,
17
+ w: 2 * ro.value,
18
+ h: 2 * ro.value,
19
+ }), opts, { origin: center });
20
+ this.rOuter = ro;
21
+ this.rInner = ri;
22
+ this.a0 = a0s;
23
+ this.a1 = a1s;
24
+ this.stroke(opts, true, {
25
+ d: derive(() => {
26
+ const cx = center.x.value;
27
+ const cy = center.y.value;
28
+ const _ro = ro.value;
29
+ const _ri = ri.value;
30
+ const _a0 = a0s.value;
31
+ const _a1 = a1s.value;
32
+ const span = Math.abs(_a1 - _a0);
33
+ const largeArc = span > Math.PI ? 1 : 0;
34
+ const sweep = _a1 > _a0 ? 1 : 0;
35
+ const back = sweep ? 0 : 1;
36
+ const o0x = cx + _ro * Math.cos(_a0), o0y = cy + _ro * Math.sin(_a0);
37
+ const o1x = cx + _ro * Math.cos(_a1), o1y = cy + _ro * Math.sin(_a1);
38
+ const i1x = cx + _ri * Math.cos(_a1), i1y = cy + _ri * Math.sin(_a1);
39
+ const i0x = cx + _ri * Math.cos(_a0), i0y = cy + _ri * Math.sin(_a0);
40
+ return `M ${o0x},${o0y} A ${_ro},${_ro} 0 ${largeArc} ${sweep} ${o1x},${o1y} L ${i1x},${i1y} A ${_ri},${_ri} 0 ${largeArc} ${back} ${i0x},${i0y} Z`;
41
+ }),
42
+ });
43
+ }
44
+ /** Local-frame segments, derived from the Box (center = the given center). */
45
+ segments() {
46
+ const cx = () => this.box.value.x + this.box.value.w / 2;
47
+ const cy = () => this.box.value.y + this.box.value.h / 2;
48
+ const ro = () => this.rOuter.value;
49
+ const ri = () => this.rInner.value;
50
+ const a0 = () => this.a0.value;
51
+ const a1 = () => this.a1.value;
52
+ const polar = (rfn, afn) => ({
53
+ x: cx() + rfn() * Math.cos(afn()),
54
+ y: cy() + rfn() * Math.sin(afn()),
55
+ });
56
+ return [
57
+ { type: "arc", cx, cy, r: ro, a0, a1 },
58
+ { type: "line", from: polar(ro, a1), to: polar(ri, a1) },
59
+ { type: "arc", cx, cy, r: ri, a0: a1, a1: a0 },
60
+ { type: "line", from: polar(ri, a0), to: polar(ro, a0) },
61
+ ];
62
+ }
63
+ }
64
+ export const annularSector = (center, rOuter, rInner, a0, a1, opts) => new AnnularSector(center, rOuter, rInner, a0, a1, opts);
@@ -0,0 +1,14 @@
1
+ import { type Cell, type Val, type Vec, type Writable } from "../core/index.js";
2
+ import type { AnyShape } from "./shape.js";
3
+ import type { Content } from "./text.js";
4
+ export interface ButtonOpts {
5
+ width?: number;
6
+ height?: number;
7
+ size?: Val<number>;
8
+ /** Externally-controlled hover signal — share across shapes if needed. */
9
+ hovered?: Writable<Cell<boolean>>;
10
+ }
11
+ /** A clickable, labelled region positioned at `pos` (top-left). The
12
+ * `hovered` signal (auto-created) tracks pointer state — computed from
13
+ * it to drive ancillary visuals. */
14
+ export declare function button(pos: Vec, content: Val<Content>, onClick: () => void, opts?: ButtonOpts): AnyShape;
@@ -0,0 +1,31 @@
1
+ // Labelled, clickable region — group + tinted-rect + label, with
2
+ // hover/click handlers wired.
3
+ import { Anchor, cell, derive, Num, vec, } from "../core/index.js";
4
+ import { group } from "./group.js";
5
+ import { label } from "./label.js";
6
+ import { rect } from "./rect.js";
7
+ import { tokens } from "./tokens.js";
8
+ /** A clickable, labelled region positioned at `pos` (top-left). The
9
+ * `hovered` signal (auto-created) tracks pointer state — computed from
10
+ * it to drive ancillary visuals. */
11
+ export function button(pos, content, onClick, opts = {}) {
12
+ const w = opts.width ?? 80;
13
+ const h = opts.height ?? 26;
14
+ const size = Num.from(opts.size ?? 11);
15
+ const hovered = opts.hovered ?? cell(false);
16
+ // Hover tint behind the border so outline weight stays constant.
17
+ const g = group({ translate: pos }, rect(0, 0, w, h, {
18
+ fill: tokens.stroke,
19
+ opacity: derive(() => (hovered.value ? 0.08 : 0)),
20
+ stroke: "none",
21
+ }), rect(0, 0, w, h, { thin: true }), label(vec(w / 2, h / 2), content, { size, align: Anchor.Center }));
22
+ g.on("pointerover", () => {
23
+ hovered.value = true;
24
+ });
25
+ g.on("pointerout", () => {
26
+ hovered.value = false;
27
+ });
28
+ g.on("click", onClick);
29
+ g.el.style.cursor = "pointer";
30
+ return g;
31
+ }
@@ -0,0 +1,22 @@
1
+ import { type Animator, type Easing, type Yieldable } from "../animation/index.js";
2
+ import { type Inner, type Val, Vec } from "../core/index.js";
3
+ import type { Has } from "./shape.js";
4
+ /** Swap two shapes' positions over `sec`. */
5
+ export declare function swap(a: Has<"translate">, b: Has<"translate">, sec?: number, ease?: Easing): Animator;
6
+ /** Run `fn(item)` for each item, lagged by `stride` seconds. All in
7
+ * parallel; completes when the longest child finishes:
8
+ * `yield* stagger(0.05, shapes, s => fadeIn(s, 0.3))`. */
9
+ export declare function stagger<S>(stride: number, items: readonly S[], fn: (item: S, i: number) => Yieldable): Animator;
10
+ /** Distribute shapes radially around `center` at `radius`, evenly
11
+ * spaced. Each shape tweens to its slot in parallel. */
12
+ export declare function splay(center: Vec, radius: number, shapes: readonly Has<"translate">[], sec?: number, ease?: Easing): Animator;
13
+ /** Tween each shape to its paired target (matched by index). */
14
+ export declare function assemble(shapes: readonly Has<"translate">[], targets: readonly (Vec | Inner<Vec>)[], sec?: number, ease?: Easing): Animator;
15
+ /** Continuous orbit around `center`, one revolution per `period` seconds.
16
+ * Picks up each shape's current radius/angle (no jump). Never returns.
17
+ * `rate` is a reactive multiplier — tween for ease-in/out; negative
18
+ * reverses; 0 pauses. */
19
+ export declare function orbit(center: Vec, shapes: readonly Has<"translate">[], opts?: {
20
+ period?: number;
21
+ rate?: Val<number>;
22
+ }): Animator;
@@ -0,0 +1,69 @@
1
+ // Multi-shape recipes. `swap`/`splay`/`assemble`/`stagger` are bounded
2
+ // (yield-array compositions over `.to(...)`); `orbit` is continuous (a
3
+ // `drive` step over N translates). For rigid group translate, reach for
4
+ // `centroid(...shapes).to(...)` instead.
5
+ import { drive } from "../animation/index.js";
6
+ import { reader, Vec } from "../core/index.js";
7
+ /** Swap two shapes' positions over `sec`. */
8
+ export function* swap(a, b, sec = 0.5, ease) {
9
+ const av = a.translate.peek();
10
+ const bv = b.translate.peek();
11
+ yield [a.translate.to(bv, sec, ease), b.translate.to(av, sec, ease)];
12
+ }
13
+ /** Run `fn(item)` for each item, lagged by `stride` seconds. All in
14
+ * parallel; completes when the longest child finishes:
15
+ * `yield* stagger(0.05, shapes, s => fadeIn(s, 0.3))`. */
16
+ export function* stagger(stride, items, fn) {
17
+ yield items.map((item, i) => (function* () {
18
+ yield i * stride;
19
+ yield fn(item, i);
20
+ })());
21
+ }
22
+ /** Distribute shapes radially around `center` at `radius`, evenly
23
+ * spaced. Each shape tweens to its slot in parallel. */
24
+ export function* splay(center, radius, shapes, sec = 0.5, ease) {
25
+ const c = center.value;
26
+ const N = shapes.length;
27
+ yield shapes.map((s, i) => {
28
+ // Start at top (-π/2) so first shape sits straight up; clockwise
29
+ // — natural reading order for left-to-right layouts above.
30
+ const angle = (i / N) * Math.PI * 2 - Math.PI / 2;
31
+ return s.translate.to({ x: c.x + radius * Math.cos(angle), y: c.y + radius * Math.sin(angle) }, sec, ease);
32
+ });
33
+ }
34
+ /** Tween each shape to its paired target (matched by index). */
35
+ export function* assemble(shapes, targets, sec = 0.5, ease) {
36
+ yield shapes.map((s, i) => {
37
+ const t = targets[i];
38
+ return s.translate.to(t instanceof Vec ? t.value : t, sec, ease);
39
+ });
40
+ }
41
+ /** Continuous orbit around `center`, one revolution per `period` seconds.
42
+ * Picks up each shape's current radius/angle (no jump). Never returns.
43
+ * `rate` is a reactive multiplier — tween for ease-in/out; negative
44
+ * reverses; 0 pauses. */
45
+ export function orbit(center, shapes, opts = {}) {
46
+ const period = opts.period ?? 4;
47
+ const rateFn = reader(opts.rate ?? 1);
48
+ const omega = (2 * Math.PI) / period;
49
+ const N = shapes.length;
50
+ const c0 = center.value;
51
+ const init = shapes.map(sh => {
52
+ const v = sh.translate.peek();
53
+ const dx = v.x - c0.x;
54
+ const dy = v.y - c0.y;
55
+ return { angle: Math.atan2(dy, dx), radius: Math.hypot(dx, dy) };
56
+ });
57
+ let t = 0;
58
+ return drive(tick => {
59
+ t += tick.dt * rateFn();
60
+ const c = center.value;
61
+ for (let i = 0; i < N; i++) {
62
+ const angle = init[i].angle + omega * t;
63
+ shapes[i].translate.value = {
64
+ x: c.x + init[i].radius * Math.cos(angle),
65
+ y: c.y + init[i].radius * Math.sin(angle),
66
+ };
67
+ }
68
+ });
69
+ }
@@ -0,0 +1,17 @@
1
+ import { Num, type Val, Vec } from "../core/index.js";
2
+ import { type CommonOpts, type Segment, Shape } from "./shape.js";
3
+ export interface CircleOpts extends CommonOpts {
4
+ }
5
+ export declare class Circle<O extends CircleOpts = CircleOpts> extends Shape<O> {
6
+ readonly radius: Num;
7
+ constructor(center: Vec, radius: Val<number>, opts?: O);
8
+ /** Point on perimeter at angle θ (radians, y-down). */
9
+ atAngle(angle: Val<number>): Vec;
10
+ /** Unit tangent at angle θ. */
11
+ tangentAt(angle: Val<number>): Vec;
12
+ boundary(toward: Vec): Vec;
13
+ /** Two half-arcs (each span ≤ π, so `largeArc` stays unambiguous), in
14
+ * local frame — derived from the Box, not parent-frame `this.center`. */
15
+ segments(): Segment[];
16
+ }
17
+ export declare const circle: <const O extends CircleOpts>(at: Vec, r: Val<number>, opts?: O) => Circle<O>;
@@ -0,0 +1,57 @@
1
+ import { Num, reader, Vec } from "../core/index.js";
2
+ import { TWO_PI } from "./dashed.js";
3
+ import { Shape } from "./shape.js";
4
+ export class Circle extends Shape {
5
+ radius;
6
+ constructor(center, radius, opts = {}) {
7
+ const r = Num.from(radius);
8
+ super(opts.dashed ? "path" : "circle", () => ({
9
+ x: center.x.value - r.value,
10
+ y: center.y.value - r.value,
11
+ w: 2 * r.value,
12
+ h: 2 * r.value,
13
+ }), opts, { origin: center });
14
+ this.radius = r;
15
+ this.stroke(opts, true, { cx: center.x, cy: center.y, r });
16
+ }
17
+ /** Point on perimeter at angle θ (radians, y-down). */
18
+ atAngle(angle) {
19
+ const a = reader(angle);
20
+ return Vec.derive(() => ({
21
+ x: this.center.x.value + this.radius.value * Math.cos(a()),
22
+ y: this.center.y.value + this.radius.value * Math.sin(a()),
23
+ }));
24
+ }
25
+ /** Unit tangent at angle θ. */
26
+ tangentAt(angle) {
27
+ const a = reader(angle);
28
+ return Vec.derive(() => ({ x: -Math.sin(a()), y: Math.cos(a()) }));
29
+ }
30
+ boundary(toward) {
31
+ return Vec.derive(() => {
32
+ const t = toward.value;
33
+ const c = this.center.value;
34
+ const sc = this.scale.value;
35
+ // Boundary tracks the visual radius so pulses scale it; for
36
+ // non-uniform scale, approximate as a circle of the larger axis.
37
+ const r = this.radius.value * Math.max(sc.x, sc.y);
38
+ const len = Math.hypot(t.x - c.x, t.y - c.y) || 1;
39
+ return {
40
+ x: c.x + ((t.x - c.x) / len) * r,
41
+ y: c.y + ((t.y - c.y) / len) * r,
42
+ };
43
+ });
44
+ }
45
+ /** Two half-arcs (each span ≤ π, so `largeArc` stays unambiguous), in
46
+ * local frame — derived from the Box, not parent-frame `this.center`. */
47
+ segments() {
48
+ const cx = () => this.box.value.x + this.box.value.w / 2;
49
+ const cy = () => this.box.value.y + this.box.value.h / 2;
50
+ const r = () => this.radius.value;
51
+ return [
52
+ { type: "arc", cx, cy, r, a0: () => 0, a1: () => Math.PI },
53
+ { type: "arc", cx, cy, r, a0: () => Math.PI, a1: () => TWO_PI },
54
+ ];
55
+ }
56
+ }
57
+ export const circle = (at, r, opts) => new Circle(at, r, opts);
@@ -0,0 +1,5 @@
1
+ import { type Shape } from "./shape.js";
2
+ /** Install a `<clipPath>` mirroring `shape` and return a `url(#id)`
3
+ * for use with the CSS `clip-path` property. `shape` must already be
4
+ * mounted in an SVG (the parent SVG is found via `ownerSVGElement`). */
5
+ export declare function clipPath(shape: Shape): string;
@@ -0,0 +1,31 @@
1
+ // `<clipPath>` mirroring a shape's geometry via `<use href>`, so the
2
+ // clip follows the source reactively without copying attributes.
3
+ import { SVG_NS } from "./shape.js";
4
+ let nextId = 0;
5
+ function ensureDefs(svg) {
6
+ let defs = svg.querySelector(":scope > defs");
7
+ if (!defs) {
8
+ defs = document.createElementNS(SVG_NS, "defs");
9
+ svg.insertBefore(defs, svg.firstChild);
10
+ }
11
+ return defs;
12
+ }
13
+ /** Install a `<clipPath>` mirroring `shape` and return a `url(#id)`
14
+ * for use with the CSS `clip-path` property. `shape` must already be
15
+ * mounted in an SVG (the parent SVG is found via `ownerSVGElement`). */
16
+ export function clipPath(shape) {
17
+ const target = shape.intrinsic ?? shape.el;
18
+ const svg = target.ownerSVGElement;
19
+ if (!svg)
20
+ throw new Error("clipPath: shape not yet mounted in an SVG");
21
+ if (!target.id)
22
+ target.id = `clip-target-${nextId++}`;
23
+ const id = `clip-${nextId++}`;
24
+ const cp = document.createElementNS(SVG_NS, "clipPath");
25
+ cp.id = id;
26
+ const use = document.createElementNS(SVG_NS, "use");
27
+ use.setAttribute("href", `#${target.id}`);
28
+ cp.appendChild(use);
29
+ ensureDefs(svg).appendChild(cp);
30
+ return `url(#${id})`;
31
+ }