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,290 @@
1
+ // Motion combinators over tex shapes (`pluck`, `morph`, …).
2
+ import { easeInOut, easeOut } from "../animation/index.js";
3
+ import { effect, num } from "../core/index.js";
4
+ import { Shape } from "../shapes/index.js";
5
+ /** Pulse a part's `highlighted` signal for `dt` seconds. */
6
+ export function* highlight(part, dt = 0.6) {
7
+ part.highlighted.value = true;
8
+ try {
9
+ yield dt;
10
+ }
11
+ finally {
12
+ part.highlighted.value = false;
13
+ }
14
+ }
15
+ /** Reveal an eq left-to-right via a clip-path sweep on the inner HTML
16
+ * wrapper. (Animated clip-path on the outer `<g>` tears foreignObject
17
+ * content in Chromium.) */
18
+ export function* write(eq, dt = 0.6, ease = easeOut) {
19
+ const target = clipTarget(eq);
20
+ const progress = num(0);
21
+ const stop = effect(() => {
22
+ target.style.clipPath = `inset(0 ${(1 - progress.value) * 100}% 0 0)`;
23
+ });
24
+ try {
25
+ yield* progress.to(1, dt, ease);
26
+ }
27
+ finally {
28
+ target.style.clipPath = "";
29
+ stop();
30
+ }
31
+ }
32
+ /** Reverse of `write`; ends hidden (`opacity: 0`, clip-path cleared). */
33
+ export function* writeOut(eq, dt = 0.4, ease = easeOut) {
34
+ const target = clipTarget(eq);
35
+ const progress = num(1);
36
+ const stop = effect(() => {
37
+ target.style.clipPath = `inset(0 ${(1 - progress.value) * 100}% 0 0)`;
38
+ });
39
+ try {
40
+ yield* progress.to(0, dt, ease);
41
+ }
42
+ finally {
43
+ stop();
44
+ eq.opacity.value = 0;
45
+ target.style.clipPath = "";
46
+ }
47
+ }
48
+ /** Inner HTML wrapper inside the foreignObject; falls back to `<g>`. */
49
+ const clipTarget = (eq) => {
50
+ const fo = eq.intrinsic;
51
+ const wrapper = fo?.firstElementChild;
52
+ return wrapper instanceof HTMLElement ? wrapper : eq.el;
53
+ };
54
+ const findMathWrapper = (matchedEl) => {
55
+ let cur = matchedEl.parentElement;
56
+ while (cur && cur.tagName.toLowerCase() !== "math") {
57
+ cur = cur.parentElement;
58
+ }
59
+ if (!cur)
60
+ return null;
61
+ const wrapper = cur.parentElement;
62
+ return wrapper instanceof HTMLElement ? wrapper : null;
63
+ };
64
+ /** Part's matched-mrow position in parent-frame coords. */
65
+ const partPose = (part) => {
66
+ const tr = part.host.translate.value;
67
+ const a = part.box.value;
68
+ return { x: tr.x + a.x, y: tr.y + a.y };
69
+ };
70
+ /** A Part lifted out of its TexShape into a free Shape. `translate` is
71
+ * the matched mrow's TL in parent coords; `scale` pivots around it.
72
+ * Zeroes the source's opacity until `dispose()`. */
73
+ export class Plucked extends Shape {
74
+ source;
75
+ #sourcePrevOpacity;
76
+ constructor(source) {
77
+ const a = source.box.peek();
78
+ const local = { x: 0, y: 0, w: a.w, h: a.h };
79
+ super("foreignObject", () => local);
80
+ this.source = source;
81
+ this.#sourcePrevOpacity = source.opacity.peek();
82
+ source.opacity.value = 0;
83
+ }
84
+ dispose() {
85
+ this.source.opacity.value = this.#sourcePrevOpacity;
86
+ super.dispose();
87
+ }
88
+ }
89
+ /** Lift `part` out of its TexShape, mount under the same parent,
90
+ * return the Plucked. Restore via `plucked.dispose()` or `unpluck`. */
91
+ export function pluck(part) {
92
+ const liveEl = part.el;
93
+ const host = part.host;
94
+ if (!liveEl || !host.parent) {
95
+ throw new Error("pluck: TexShape isn't mounted yet — `s(eq)` it before plucking");
96
+ }
97
+ const wrapper = findMathWrapper(liveEl);
98
+ if (!wrapper)
99
+ throw new Error("pluck: cannot find <math> wrapper");
100
+ const boxLocal = part.box.value;
101
+ const pose = partPose(part);
102
+ // Deep-clone, then hide all but the matched mrow via `visibility`
103
+ // (not `display: none`, which would drop the preserved layout).
104
+ const clonedWrapper = wrapper.cloneNode(true);
105
+ const matchedClone = clonedWrapper.querySelector(`.bireactive-part-${part.name}`);
106
+ const mathClone = clonedWrapper.querySelector("math");
107
+ if (!matchedClone || !mathClone) {
108
+ throw new Error("pluck: cloned wrapper lost its matched mrow");
109
+ }
110
+ mathClone.style.visibility = "hidden";
111
+ matchedClone.style.visibility = "visible";
112
+ // Shift the mrow to local (0, 0); with `Plucked.box = (0,0,w,h)` this
113
+ // makes `plucked.translate` mean "matched mrow TL in parent coords".
114
+ clonedWrapper.style.transform = `translate(${-boxLocal.x}px, ${-boxLocal.y}px)`;
115
+ clonedWrapper.style.transformOrigin = "0 0";
116
+ const plucked = new Plucked(part);
117
+ const fo = plucked.intrinsic;
118
+ fo.setAttribute("x", "0");
119
+ fo.setAttribute("y", "0");
120
+ fo.setAttribute("width", String(Math.max(host.width.value + 32, 1)));
121
+ fo.setAttribute("height", String(Math.max(host.height.value + 16, 1)));
122
+ fo.style.overflow = "visible";
123
+ fo.style.pointerEvents = "none";
124
+ fo.appendChild(clonedWrapper);
125
+ plucked.translate.value = pose;
126
+ host.parent.add(plucked);
127
+ return plucked;
128
+ }
129
+ /** Animate `plucked` into `target`'s pose (or back to its source), then
130
+ * dispose. Translates only; animate `scale` yourself to fit a target. */
131
+ export function* unpluck(plucked, target, dt = 0.5, ease = easeInOut) {
132
+ const dest = target ?? plucked.source;
133
+ try {
134
+ yield* plucked.translate.to(partPose(dest), dt, ease);
135
+ }
136
+ finally {
137
+ plucked.dispose();
138
+ }
139
+ }
140
+ /** Animate `from` → `to`, matching Parts by marker identity (same
141
+ * marker reference, or markers sharing a `group` root). Branches
142
+ * by cardinality of each identity:
143
+ *
144
+ * 1↔1 same content → single rider, scaled to dest
145
+ * 1↔1 different → dual rider, source fades out, dest fades in
146
+ * 1↔N → N riders emerge from source, fan to dests
147
+ * N↔1 → N riders converge to dest, fade out
148
+ * N↔M → pair by index; extras parent-crossfade
149
+ *
150
+ * Unmatched parts cross-fade with the parent. Assumes both shapes
151
+ * share a parent and have translate-only transforms. */
152
+ export function* morph(from, to, dt = 0.6, ease = easeInOut) {
153
+ const parent = from.parent;
154
+ if (!parent || from.parent !== to.parent) {
155
+ if (to.opacity.peek() < 1)
156
+ to.opacity.value = 0;
157
+ yield [from.opacity.to(0, dt, ease), to.opacity.to(1, dt, ease)];
158
+ return;
159
+ }
160
+ // Hide `to` so riders supply visible content during the flight.
161
+ if (to.opacity.peek() !== 0)
162
+ to.opacity.value = 0;
163
+ const animators = [from.opacity.to(0, dt, ease), to.opacity.to(1, dt, ease)];
164
+ const cleanups = [];
165
+ const fromByRoot = groupByRoot(from.parts);
166
+ const toByRoot = groupByRoot(to.parts);
167
+ for (const root of new Set([...fromByRoot.keys(), ...toByRoot.keys()])) {
168
+ const fps = fromByRoot.get(root) ?? [];
169
+ const tps = toByRoot.get(root) ?? [];
170
+ if (fps.length === 0 || tps.length === 0)
171
+ continue;
172
+ if (fps.length === 1 && tps.length === 1) {
173
+ ride(fps[0], tps[0], dt, ease, animators, cleanups);
174
+ }
175
+ else if (fps.length === 1 && tps.length > 1) {
176
+ fanOut(fps[0], tps, dt, ease, animators, cleanups);
177
+ }
178
+ else if (fps.length > 1 && tps.length === 1) {
179
+ fanIn(fps, tps[0], dt, ease, animators, cleanups);
180
+ }
181
+ else {
182
+ // N↔M: pair by index; extras parent-crossfade.
183
+ const n = Math.min(fps.length, tps.length);
184
+ for (let i = 0; i < n; i++)
185
+ ride(fps[i], tps[i], dt, ease, animators, cleanups);
186
+ }
187
+ }
188
+ try {
189
+ yield animators;
190
+ }
191
+ finally {
192
+ for (const c of cleanups)
193
+ c();
194
+ from.opacity.value = 0;
195
+ to.opacity.value = 1;
196
+ }
197
+ }
198
+ /** Topmost marker in a `marker.group` chain — two markers share
199
+ * identity for morph iff they share a root. */
200
+ const groupRoot = (m) => {
201
+ let r = m;
202
+ while (r.group)
203
+ r = r.group;
204
+ return r;
205
+ };
206
+ const groupByRoot = (parts) => {
207
+ const out = new Map();
208
+ for (const p of parts) {
209
+ if (!p.el)
210
+ continue;
211
+ const a = p.box.value;
212
+ if (a.w === 0 || a.h === 0)
213
+ continue;
214
+ const r = groupRoot(p.marker);
215
+ const list = out.get(r);
216
+ if (list)
217
+ list.push(p);
218
+ else
219
+ out.set(r, [p]);
220
+ }
221
+ return out;
222
+ };
223
+ /** 1↔1 ride. Same content: single source clone, scaled to dest.
224
+ * Different content: dual clone on the same trajectory, crossfade. */
225
+ const ride = (p, q, dt, ease, animators, cleanups) => {
226
+ const pa = p.box.value;
227
+ const qa = q.box.value;
228
+ const destPose = partPose(q);
229
+ const sameContent = p.content.peek() === q.content.peek();
230
+ const src = pluck(p);
231
+ animators.push(src.translate.to(destPose, dt, ease), src.scale.to({ x: qa.w / pa.w, y: qa.h / pa.h }, dt, ease));
232
+ cleanups.push(() => src.dispose());
233
+ if (sameContent) {
234
+ // Source rider IS the right content; just hide q during flight.
235
+ const prevQ = q.opacity.peek();
236
+ q.opacity.value = 0;
237
+ cleanups.push(() => {
238
+ q.opacity.value = prevQ;
239
+ });
240
+ }
241
+ else {
242
+ // Dest rider starts at p's pose, scaled to p's footprint, faded
243
+ // out — then rides to q's pose at full size, fading in.
244
+ const dst = pluck(q);
245
+ dst.translate.value = partPose(p);
246
+ dst.scale.value = { x: pa.w / qa.w, y: pa.h / qa.h };
247
+ dst.opacity.value = 0;
248
+ animators.push(src.opacity.to(0, dt, ease), dst.translate.to(destPose, dt, ease), dst.scale.to({ x: 1, y: 1 }, dt, ease), dst.opacity.to(1, dt, ease));
249
+ cleanups.push(() => dst.dispose());
250
+ }
251
+ };
252
+ /** 1→N. Source rider fades out in place; N dest riders emerge from
253
+ * source's pose and fan out to their respective slots. */
254
+ const fanOut = (p, qs, dt, ease, animators, cleanups) => {
255
+ const pa = p.box.value;
256
+ const pPose = partPose(p);
257
+ const src = pluck(p);
258
+ animators.push(src.opacity.to(0, dt, ease));
259
+ cleanups.push(() => src.dispose());
260
+ for (const q of qs) {
261
+ const qa = q.box.value;
262
+ const dst = pluck(q);
263
+ dst.translate.value = pPose;
264
+ dst.scale.value = { x: pa.w / qa.w, y: pa.h / qa.h };
265
+ dst.opacity.value = 0;
266
+ animators.push(dst.translate.to(partPose(q), dt, ease), dst.scale.to({ x: 1, y: 1 }, dt, ease), dst.opacity.to(1, dt, ease));
267
+ cleanups.push(() => dst.dispose());
268
+ }
269
+ };
270
+ /** N→1. N source riders converge to dest's slot, fading out;
271
+ * dest fades in there. */
272
+ const fanIn = (ps, q, dt, ease, animators, cleanups) => {
273
+ const qa = q.box.value;
274
+ const qPose = partPose(q);
275
+ for (const p of ps) {
276
+ const pa = p.box.value;
277
+ const src = pluck(p);
278
+ animators.push(src.translate.to(qPose, dt, ease), src.scale.to({ x: qa.w / pa.w, y: qa.h / pa.h }, dt, ease), src.opacity.to(0, dt, ease));
279
+ cleanups.push(() => src.dispose());
280
+ }
281
+ // Dest rider fades in at q's pos so q is visible during the
282
+ // parent's partial crossfade.
283
+ const dst = pluck(q);
284
+ dst.opacity.value = 0;
285
+ animators.push(dst.opacity.to(1, dt, ease));
286
+ cleanups.push(() => dst.dispose());
287
+ };
288
+ // No part-level `swap`: to swap two parts visually, morph between two
289
+ // equations holding them in opposite slots — `morph(tex`${a}${b}`,
290
+ // tex`${b}${a}`)`. See md-tex-demo.
@@ -0,0 +1,65 @@
1
+ import { type Box, Cell, type Val, type Writable } from "../core/index.js";
2
+ import { type Marker } from "./marker.js";
3
+ import type { TexShape } from "./tex.js";
4
+ export type { Marker };
5
+ export type PartContent = Val<string>;
6
+ /** Named, addressable region of a TexShape (read-only, template-bound).
7
+ * Reach into `part.box` for axes/cardinals. */
8
+ export declare class Part<N extends string = string> {
9
+ #private;
10
+ readonly name: N;
11
+ readonly content: Cell<string>;
12
+ readonly marker: PartMarker;
13
+ readonly host: TexShape;
14
+ /** Background-tint highlight; written by `highlight()` and `bindParts()`. */
15
+ readonly highlighted: Writable<Cell<boolean>>;
16
+ readonly opacity: Writable<import("../core/index.js").Num>;
17
+ readonly box: Box;
18
+ el: HTMLElement | null;
19
+ constructor(name: N, content: Cell<string>, box: Box, marker: PartMarker, host: TexShape);
20
+ /** @internal Wire reactive state to `el`'s inline styles. */
21
+ bind(el: HTMLElement | null, highlightColor: string): void;
22
+ /** @internal */
23
+ dispose(): void;
24
+ }
25
+ /** Marker emitted by `part()` / `parts()`; valid inside `tex\`…\`` holes.
26
+ * Group members share one inner `Marker` so they share identity. */
27
+ export declare class PartMarker<N extends string = string> {
28
+ #private;
29
+ readonly name: N;
30
+ readonly group: PartMarker | null;
31
+ /** Per-instance color; `null` walks up the group chain. */
32
+ readonly color: Writable<Cell<string | null>>;
33
+ readonly content: Cell<string>;
34
+ constructor(name: N, source: PartContent, group?: PartMarker | null);
35
+ /** True when any rendering of this identity (prose/shape/anim) is active. */
36
+ get active(): Cell<boolean>;
37
+ /** Bind a local boolean signal to this marker's identity. */
38
+ bind(local: Writable<Cell<boolean>>): () => void;
39
+ /** Register in the global lookup under `id`. */
40
+ register(id: string): this;
41
+ /** One-off content override, same identity. */
42
+ with(content: PartContent): PartMarker<N>;
43
+ /** Expand into named child markers sharing this identity (1↔N morph). */
44
+ expand<T extends Record<string, PartContent>>(spec: T): {
45
+ readonly [K in keyof T & string]: PartMarker<K>;
46
+ };
47
+ }
48
+ export declare function part<N extends string>(name: N, content?: PartContent): PartMarker<N>;
49
+ export declare function parts<T extends readonly (string | Record<string, PartContent>)[]>(...specs: T): MarkersFromSpecs<T>;
50
+ type MarkersFromSpecs<T extends readonly (string | Record<string, PartContent>)[]> = {
51
+ readonly [K in NameOf<T[number]>]: PartMarker<K>;
52
+ };
53
+ type NameOf<S> = S extends string ? S : S extends Record<infer K, PartContent> ? K & string : never;
54
+ /** Set the same color on N markers at once. */
55
+ export declare function tint(color: string | null, ...markers: readonly {
56
+ color: Writable<Cell<string | null>>;
57
+ }[]): void;
58
+ /** Wire hover on each `Part.el` to its `Marker`; drive `highlighted`
59
+ * from `active`. Unmatched names are silently skipped. */
60
+ export declare function bindParts(eq: {
61
+ parts: Iterable<Part<string>>;
62
+ }, markers: Partial<Record<string, Marker>>): () => void;
63
+ export type PartList<Names extends string = string> = readonly Part[] & {
64
+ readonly [K in Names]: Part<K>;
65
+ };
@@ -0,0 +1,149 @@
1
+ // Observable handles into a tex-rendered formula. Identity for morph
2
+ // is by marker reference; `with`/`expand` share the root's identity.
3
+ // Color cascades up the `group` chain via `effectiveColor`.
4
+ import { Cell, cell, derive, effect, num, } from "../core/index.js";
5
+ import { highlightTint, hover, marker, registerMarker } from "./marker.js";
6
+ /** Walk the `marker.group` chain to first non-null color. */
7
+ const effectiveColor = (m) => {
8
+ for (let cur = m; cur; cur = cur.group) {
9
+ const c = cur.color.value;
10
+ if (c !== null)
11
+ return c;
12
+ }
13
+ return null;
14
+ };
15
+ /** Named, addressable region of a TexShape (read-only, template-bound).
16
+ * Reach into `part.box` for axes/cardinals. */
17
+ export class Part {
18
+ name;
19
+ content;
20
+ marker;
21
+ host;
22
+ /** Background-tint highlight; written by `highlight()` and `bindParts()`. */
23
+ highlighted = cell(false);
24
+ opacity = num(1);
25
+ box;
26
+ el = null;
27
+ #disposers = [];
28
+ constructor(name, content, box, marker, host) {
29
+ this.name = name;
30
+ this.content = content;
31
+ this.marker = marker;
32
+ this.host = host;
33
+ this.box = box;
34
+ }
35
+ /** @internal Wire reactive state to `el`'s inline styles. */
36
+ bind(el, highlightColor) {
37
+ for (const d of this.#disposers)
38
+ d();
39
+ this.#disposers.length = 0;
40
+ this.el = el;
41
+ if (!el)
42
+ return;
43
+ this.#disposers.push(effect(() => {
44
+ if (this.highlighted.value) {
45
+ const color = effectiveColor(this.marker);
46
+ el.style.backgroundColor = color ? highlightTint(color) : highlightColor;
47
+ }
48
+ else {
49
+ el.style.backgroundColor = "transparent";
50
+ }
51
+ }), effect(() => {
52
+ el.style.opacity = String(this.opacity.value);
53
+ }), effect(() => {
54
+ el.style.color = effectiveColor(this.marker) ?? "";
55
+ }));
56
+ }
57
+ /** @internal */
58
+ dispose() {
59
+ for (const d of this.#disposers)
60
+ d();
61
+ this.#disposers.length = 0;
62
+ this.el = null;
63
+ }
64
+ }
65
+ /** Marker emitted by `part()` / `parts()`; valid inside `tex\`…\`` holes.
66
+ * Group members share one inner `Marker` so they share identity. */
67
+ export class PartMarker {
68
+ name;
69
+ group;
70
+ /** Per-instance color; `null` walks up the group chain. */
71
+ color = cell(null);
72
+ content;
73
+ /** Shared inner Marker; all group members alias the root's instance. */
74
+ #m;
75
+ constructor(name, source, group = null) {
76
+ this.name = name;
77
+ this.group = group;
78
+ this.content =
79
+ source instanceof Cell
80
+ ? source
81
+ : typeof source === "function"
82
+ ? derive(source)
83
+ : cell(source);
84
+ this.#m = group ? group.#m : marker();
85
+ }
86
+ /** True when any rendering of this identity (prose/shape/anim) is active. */
87
+ get active() {
88
+ return this.#m.active;
89
+ }
90
+ /** Bind a local boolean signal to this marker's identity. */
91
+ bind(local) {
92
+ return this.#m.bind(local);
93
+ }
94
+ /** Register in the global lookup under `id`. */
95
+ register(id) {
96
+ registerMarker(id, this);
97
+ return this;
98
+ }
99
+ /** One-off content override, same identity. */
100
+ with(content) {
101
+ return new PartMarker(this.name, content, this);
102
+ }
103
+ /** Expand into named child markers sharing this identity (1↔N morph). */
104
+ expand(spec) {
105
+ const out = {};
106
+ for (const k in spec)
107
+ out[k] = new PartMarker(k, spec[k], this);
108
+ return out;
109
+ }
110
+ }
111
+ export function part(name, content = name) {
112
+ return new PartMarker(name, content);
113
+ }
114
+ export function parts(...specs) {
115
+ const out = {};
116
+ for (const spec of specs) {
117
+ if (typeof spec === "string") {
118
+ out[spec] = new PartMarker(spec, spec);
119
+ }
120
+ else {
121
+ for (const k in spec)
122
+ out[k] = new PartMarker(k, spec[k]);
123
+ }
124
+ }
125
+ return out;
126
+ }
127
+ /** Set the same color on N markers at once. */
128
+ export function tint(color, ...markers) {
129
+ for (const m of markers)
130
+ m.color.value = color;
131
+ }
132
+ /** Wire hover on each `Part.el` to its `Marker`; drive `highlighted`
133
+ * from `active`. Unmatched names are silently skipped. */
134
+ export function bindParts(eq, markers) {
135
+ const ds = [];
136
+ for (const p of eq.parts) {
137
+ const m = markers[p.name];
138
+ if (!m || !p.el)
139
+ continue;
140
+ ds.push(hover(p.el, m));
141
+ ds.push(effect(() => {
142
+ p.highlighted.value = m.active.value;
143
+ }));
144
+ }
145
+ return () => {
146
+ for (const d of ds)
147
+ d();
148
+ };
149
+ }
@@ -0,0 +1,45 @@
1
+ import { type Cell } from "../core/index.js";
2
+ import { Shape, type ShapeOpts } from "../shapes/index.js";
3
+ import { type PartList, PartMarker } from "./parts.js";
4
+ /** A `tex\`…\`` interpolation slot. Strings splice verbatim; PartMarkers
5
+ * wrap content in `\class{bireactive-part-N}{…}` to be re-found post-render. */
6
+ export type TexInterp = string | PartMarker;
7
+ export interface TexOpts extends ShapeOpts {
8
+ /** Font size in user units. Defaults to `tokens.fontSize`. */
9
+ size?: number;
10
+ /** Font family. Defaults to `tokens.mathFont`. */
11
+ font?: string;
12
+ /** Background tint applied while a part's `highlighted` signal is
13
+ * true. Default: `tokens.tex.highlightColor`. */
14
+ highlightColor?: string;
15
+ /** "inline" (default) or "block" display style. Maps to Temml's
16
+ * `displayMode` — bigger fractions, limits above/below, `\begin{…}`. */
17
+ display?: "inline" | "block";
18
+ }
19
+ /** Union of `PartMarker` names in an interpolation tuple (strings drop out). */
20
+ export type NamesOf<V extends readonly TexInterp[]> = V extends readonly (infer U)[] ? U extends PartMarker<infer N> ? N : never : never;
21
+ /** Render LaTeX → MathML via Temml (trust on, lenient errors). */
22
+ export declare const renderToMathML: (source: string, opts?: {
23
+ displayMode?: boolean;
24
+ }) => string;
25
+ /** A LaTeX-rendered shape with addressable Parts. See `tex` (factory)
26
+ * and `parts.ts` (Part / PartMarker). */
27
+ export declare class TexShape<Names extends string = string> extends Shape {
28
+ readonly parts: PartList<Names>;
29
+ /** Width in local-frame user units (the rendered MathML bounding rect). */
30
+ readonly width: Cell<number>;
31
+ /** Height in local-frame user units. */
32
+ readonly height: Cell<number>;
33
+ constructor(strings: TemplateStringsArray | readonly string[], values: readonly TexInterp[], opts?: TexOpts);
34
+ /** Sugar: `eq.highlight("a")` → `eq.parts.a.highlighted.value = true`. */
35
+ highlight(name: Names, on?: boolean): void;
36
+ }
37
+ /** Render a LaTeX formula via Temml. Three forms:
38
+ *
39
+ * tex`E = mc^2` // direct, default size
40
+ * tex(28)`E = mc^2` // size-only shorthand
41
+ * tex({ size: 28, display: "block" })`...` // full options
42
+ *
43
+ * Single-backslash LaTeX works directly (reads `strings.raw`). */
44
+ export declare function tex<V extends readonly TexInterp[]>(strings: TemplateStringsArray, ...values: V): TexShape<NamesOf<V>>;
45
+ export declare function tex(opts: TexOpts | number): <V extends readonly TexInterp[]>(strings: TemplateStringsArray, ...values: V) => TexShape<NamesOf<V>>;