@classytic/stage 0.1.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 (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -0
  3. package/dist/assets/index.d.mts +3 -0
  4. package/dist/assets/index.mjs +5 -0
  5. package/dist/assets/kit/glyphs.d.mts +61 -0
  6. package/dist/assets/kit/glyphs.mjs +256 -0
  7. package/dist/assets/kit/index.d.mts +2 -0
  8. package/dist/assets/kit/index.mjs +4 -0
  9. package/dist/assets/kit/svg-defs.d.mts +6 -0
  10. package/dist/assets/kit/svg-defs.mjs +137 -0
  11. package/dist/builder/Palette.d.mts +30 -0
  12. package/dist/builder/Palette.mjs +100 -0
  13. package/dist/builder/SceneBuilder.d.mts +26 -0
  14. package/dist/builder/SceneBuilder.mjs +340 -0
  15. package/dist/builder/controls.d.mts +18 -0
  16. package/dist/builder/controls.mjs +29 -0
  17. package/dist/builder/editor.d.mts +46 -0
  18. package/dist/builder/editor.mjs +130 -0
  19. package/dist/builder/icons.d.mts +14 -0
  20. package/dist/builder/icons.mjs +143 -0
  21. package/dist/builder/index.d.mts +6 -0
  22. package/dist/builder/index.mjs +7 -0
  23. package/dist/builder/tools.d.mts +47 -0
  24. package/dist/builder/tools.mjs +303 -0
  25. package/dist/chem/index.d.mts +118 -0
  26. package/dist/chem/index.mjs +131 -0
  27. package/dist/core/clock.d.mts +41 -0
  28. package/dist/core/clock.mjs +62 -0
  29. package/dist/core/context.d.mts +13 -0
  30. package/dist/core/context.mjs +29 -0
  31. package/dist/core/control.d.mts +58 -0
  32. package/dist/core/control.mjs +113 -0
  33. package/dist/core/coords.d.mts +51 -0
  34. package/dist/core/coords.mjs +44 -0
  35. package/dist/core/index.d.mts +8 -0
  36. package/dist/core/index.mjs +9 -0
  37. package/dist/core/learner.d.mts +44 -0
  38. package/dist/core/learner.mjs +23 -0
  39. package/dist/core/motion.d.mts +73 -0
  40. package/dist/core/motion.mjs +103 -0
  41. package/dist/core/richText.d.mts +28 -0
  42. package/dist/core/richText.mjs +52 -0
  43. package/dist/core/vec.d.mts +47 -0
  44. package/dist/core/vec.mjs +144 -0
  45. package/dist/field/index.d.mts +58 -0
  46. package/dist/field/index.mjs +156 -0
  47. package/dist/index.d.mts +54 -0
  48. package/dist/index.mjs +55 -0
  49. package/dist/interaction/MovableDot.d.mts +33 -0
  50. package/dist/interaction/MovableDot.mjs +66 -0
  51. package/dist/interaction/hitTest.d.mts +16 -0
  52. package/dist/interaction/hitTest.mjs +39 -0
  53. package/dist/interaction/index.d.mts +5 -0
  54. package/dist/interaction/index.mjs +6 -0
  55. package/dist/interaction/useDraggable.d.mts +51 -0
  56. package/dist/interaction/useDraggable.mjs +116 -0
  57. package/dist/interaction/useScreenToMath.d.mts +12 -0
  58. package/dist/interaction/useScreenToMath.mjs +25 -0
  59. package/dist/logic/ast.d.mts +29 -0
  60. package/dist/logic/ast.mjs +37 -0
  61. package/dist/logic/index.d.mts +25 -0
  62. package/dist/logic/index.mjs +36 -0
  63. package/dist/logic/latex.d.mts +6 -0
  64. package/dist/logic/latex.mjs +33 -0
  65. package/dist/logic/minimize.d.mts +37 -0
  66. package/dist/logic/minimize.mjs +175 -0
  67. package/dist/logic/parse.d.mts +6 -0
  68. package/dist/logic/parse.mjs +68 -0
  69. package/dist/logic/table.d.mts +24 -0
  70. package/dist/logic/table.mjs +65 -0
  71. package/dist/logic/tokenize.mjs +258 -0
  72. package/dist/math/ast.d.mts +35 -0
  73. package/dist/math/ast.mjs +94 -0
  74. package/dist/math/calculus.d.mts +9 -0
  75. package/dist/math/calculus.mjs +188 -0
  76. package/dist/math/defs.mjs +51 -0
  77. package/dist/math/index.d.mts +26 -0
  78. package/dist/math/index.mjs +42 -0
  79. package/dist/math/latex.d.mts +7 -0
  80. package/dist/math/latex.mjs +115 -0
  81. package/dist/math/parse.d.mts +6 -0
  82. package/dist/math/parse.mjs +91 -0
  83. package/dist/math/tokenize.mjs +112 -0
  84. package/dist/primitives/CanvasLayer.d.mts +26 -0
  85. package/dist/primitives/CanvasLayer.mjs +87 -0
  86. package/dist/primitives/Dot.d.mts +27 -0
  87. package/dist/primitives/Dot.mjs +40 -0
  88. package/dist/primitives/Grid.d.mts +30 -0
  89. package/dist/primitives/Grid.mjs +122 -0
  90. package/dist/primitives/Label.d.mts +33 -0
  91. package/dist/primitives/Label.mjs +45 -0
  92. package/dist/primitives/Lines.d.mts +50 -0
  93. package/dist/primitives/Lines.mjs +82 -0
  94. package/dist/primitives/Plot.d.mts +50 -0
  95. package/dist/primitives/Plot.mjs +87 -0
  96. package/dist/primitives/Shapes.d.mts +76 -0
  97. package/dist/primitives/Shapes.mjs +79 -0
  98. package/dist/primitives/Tex.d.mts +27 -0
  99. package/dist/primitives/Tex.mjs +54 -0
  100. package/dist/primitives/index.d.mts +10 -0
  101. package/dist/primitives/index.mjs +10 -0
  102. package/dist/primitives/props.d.mts +15 -0
  103. package/dist/scene/Scene.d.mts +31 -0
  104. package/dist/scene/Scene.mjs +168 -0
  105. package/dist/scene/assets.d.mts +37 -0
  106. package/dist/scene/assets.mjs +31 -0
  107. package/dist/scene/commands.d.mts +47 -0
  108. package/dist/scene/commands.mjs +162 -0
  109. package/dist/scene/evaluators.d.mts +17 -0
  110. package/dist/scene/evaluators.mjs +210 -0
  111. package/dist/scene/index.d.mts +13 -0
  112. package/dist/scene/index.mjs +13 -0
  113. package/dist/scene/migrate.d.mts +11 -0
  114. package/dist/scene/migrate.mjs +35 -0
  115. package/dist/scene/render.d.mts +14 -0
  116. package/dist/scene/render.mjs +126 -0
  117. package/dist/scene/resolve.d.mts +12 -0
  118. package/dist/scene/resolve.mjs +95 -0
  119. package/dist/scene/schema.d.mts +9 -0
  120. package/dist/scene/schema.mjs +51 -0
  121. package/dist/scene/sims.d.mts +18 -0
  122. package/dist/scene/sims.mjs +54 -0
  123. package/dist/scene/store.d.mts +22 -0
  124. package/dist/scene/store.mjs +37 -0
  125. package/dist/scene/types.d.mts +228 -0
  126. package/dist/scene/types.mjs +27 -0
  127. package/dist/sim/equilibrium.d.mts +36 -0
  128. package/dist/sim/equilibrium.mjs +54 -0
  129. package/dist/sim/index.d.mts +9 -0
  130. package/dist/sim/index.mjs +9 -0
  131. package/dist/sim/particles.d.mts +43 -0
  132. package/dist/sim/particles.mjs +136 -0
  133. package/dist/sim/rate.d.mts +36 -0
  134. package/dist/sim/rate.mjs +39 -0
  135. package/dist/sim/registry.d.mts +10 -0
  136. package/dist/sim/registry.mjs +27 -0
  137. package/dist/sim/sampler.d.mts +47 -0
  138. package/dist/sim/sampler.mjs +79 -0
  139. package/dist/sim/thermal.d.mts +78 -0
  140. package/dist/sim/thermal.mjs +172 -0
  141. package/dist/sim/types.d.mts +24 -0
  142. package/dist/sim/wave.d.mts +34 -0
  143. package/dist/sim/wave.mjs +61 -0
  144. package/dist/steps/index.d.mts +62 -0
  145. package/dist/steps/index.mjs +133 -0
  146. package/dist/thermo/index.d.mts +82 -0
  147. package/dist/thermo/index.mjs +133 -0
  148. package/dist/view/Stage.d.mts +39 -0
  149. package/dist/view/Stage.mjs +91 -0
  150. package/dist/view/index.d.mts +4 -0
  151. package/dist/view/index.mjs +5 -0
  152. package/dist/view/useElementSize.d.mts +13 -0
  153. package/dist/view/useElementSize.mjs +40 -0
  154. package/dist/view/useInView.d.mts +9 -0
  155. package/dist/view/useInView.mjs +33 -0
  156. package/package.json +141 -0
  157. package/styles.css +70 -0
@@ -0,0 +1,131 @@
1
+ //#region src/chem/index.ts
2
+ /**
3
+ * `chem` — acid–base / pH AND electrochemistry / Nernst, as a PURE kernel (no
4
+ * React, no pixels). The engine behind titration curves, pH problems, and galvanic
5
+ * cells.
6
+ *
7
+ * pH at any point is found by solving the exact charge balance for [H⁺] = h:
8
+ *
9
+ * [titrant cation] + h = [OH⁻] + [A⁻]
10
+ * n_b/V + h = K_w/h + (n_a/V)·K_a/(K_a + h)
11
+ *
12
+ * where n_a is the acid initially present, n_b the strong base added, V the total
13
+ * volume. A strong acid is just the K_a → ∞ limit (fully dissociated). Solving the
14
+ * balance (monotonic in h → bisection) is robust everywhere — initial point, buffer
15
+ * region, equivalence, and past it — with no piecewise approximations to get wrong.
16
+ *
17
+ * Concentrations in mol/L, volumes in litres. Currently models an ACID analyte
18
+ * titrated by a STRONG BASE (covers strong-acid and weak-acid curves).
19
+ */
20
+ const Kw = 1e-14;
21
+ const kaOf = (spec) => spec.analyte === "strong-acid" ? 1e9 : 10 ** -(spec.pKa ?? 4.76);
22
+ /** Solve the charge balance n_b/V + h = K_w/h + (n_a/V)·K_a/(K_a+h) for h>0. */
23
+ function solveH(naOverV, nbOverV, Ka) {
24
+ const f = (h) => nbOverV + h - Kw / h - naOverV * Ka / (Ka + h);
25
+ let lo = 1e-14, hi = 10;
26
+ for (let i = 0; i < 200; i++) {
27
+ const mid = Math.sqrt(lo * hi);
28
+ if (f(mid) > 0) hi = mid;
29
+ else lo = mid;
30
+ }
31
+ return Math.sqrt(lo * hi);
32
+ }
33
+ /** pH after adding `Vb` litres of titrant. */
34
+ function pHAt(spec, Vb) {
35
+ const Ka = kaOf(spec);
36
+ const na = spec.Ca * spec.Va;
37
+ const nb = spec.Cb * Math.max(0, Vb);
38
+ const V = spec.Va + Math.max(0, Vb);
39
+ const h = solveH(na / V, nb / V, Ka);
40
+ return -Math.log10(h);
41
+ }
42
+ /** Sample the full titration curve from 0 to `span`·vEq (default 2×). */
43
+ function titrationCurve(spec, n = 160, span = 2) {
44
+ const vEq = spec.Ca * spec.Va / spec.Cb;
45
+ const vMax = vEq * span;
46
+ const points = [];
47
+ for (let i = 0; i <= n; i++) {
48
+ const v = i / n * vMax;
49
+ points.push({
50
+ v,
51
+ pH: pHAt(spec, v)
52
+ });
53
+ }
54
+ return {
55
+ points,
56
+ vEq,
57
+ pHEq: pHAt(spec, vEq),
58
+ pHHalf: pHAt(spec, vEq / 2),
59
+ pKa: spec.analyte === "weak-acid" ? spec.pKa ?? 4.76 : NaN
60
+ };
61
+ }
62
+ /** Universal gas constant, J/(mol·K). */
63
+ const R = 8.314462618;
64
+ /** Faraday constant, C/mol. */
65
+ const F = 96485.332;
66
+ /** Standard temperature, K (25 °C). */
67
+ const T_STD = 298.15;
68
+ const gcd = (a, b) => b === 0 ? a : gcd(b, a % b);
69
+ const lcm = (a, b) => a * b / gcd(a, b);
70
+ /** Galvanic cell from two half-cells; cathode/anode assigned by standard potential. */
71
+ function galvanicCell(x, y, T = T_STD) {
72
+ const xIsCathode = x.E0 > y.E0 || x.E0 === y.E0 && x.conc >= y.conc;
73
+ const cathode = xIsCathode ? x : y;
74
+ const anode = xIsCathode ? y : x;
75
+ const E0cell = cathode.E0 - anode.E0;
76
+ const n = lcm(anode.z, cathode.z);
77
+ const Q = Math.pow(anode.conc, n / anode.z) / Math.pow(cathode.conc, n / cathode.z);
78
+ const E = E0cell - R * T / (n * F) * Math.log(Q);
79
+ return {
80
+ cathode,
81
+ anode,
82
+ E0cell,
83
+ n,
84
+ Q,
85
+ E,
86
+ spontaneous: E > 0
87
+ };
88
+ }
89
+ /** Arrhenius rate constant k = A·e^(−Ea/RT). */
90
+ function arrhenius(A, Ea, T) {
91
+ return A * Math.exp(-Ea / (R * T));
92
+ }
93
+ /** Ratio k(T)/k(Tref) at fixed Ea — the stable way to scale a rate without a raw A. */
94
+ function arrheniusRatio(Ea, T, Tref = T_STD) {
95
+ return Math.exp(-(Ea / R) * (1 / T - 1 / Tref));
96
+ }
97
+ /** Fraction of molecules with energy ≥ Ea (Boltzmann) ≈ e^(−Ea/RT). */
98
+ function fractionAboveEa(Ea, T) {
99
+ return Math.exp(-Ea / (R * T));
100
+ }
101
+ /** Concentration [A] at time t for a reaction of the given order, rate = k·[A]^order. */
102
+ function concAt(order, A0, k, t) {
103
+ if (order === 0) return Math.max(0, A0 - k * t);
104
+ if (order === 1) return A0 * Math.exp(-k * t);
105
+ return A0 / (1 + A0 * k * t);
106
+ }
107
+ /** Half-life for the given order (constant only for first order). */
108
+ function halfLife(order, A0, k) {
109
+ if (order === 0) return A0 / (2 * k);
110
+ if (order === 1) return Math.LN2 / k;
111
+ return 1 / (k * A0);
112
+ }
113
+ /** Solve a balanced reaction for the limiting reagent, extent, products and leftovers. */
114
+ function solveStoichiometry(reactants, products, amountsMol) {
115
+ const ratios = reactants.map((r, i) => (amountsMol[i] ?? 0) / r.coeff);
116
+ const extent = Math.min(...ratios);
117
+ return {
118
+ extent,
119
+ limiting: reactants.filter((_, i) => Math.abs(ratios[i] - extent) < 1e-9).map((r) => r.name),
120
+ consumed: reactants.map((r) => r.coeff * extent),
121
+ leftover: reactants.map((r, i) => (amountsMol[i] ?? 0) - r.coeff * extent),
122
+ products: products.map((p) => ({
123
+ name: p.name,
124
+ moles: p.coeff * extent,
125
+ grams: p.molarMass != null ? p.coeff * extent * p.molarMass : void 0
126
+ }))
127
+ };
128
+ }
129
+
130
+ //#endregion
131
+ export { F, Kw, R, T_STD, arrhenius, arrheniusRatio, concAt, fractionAboveEa, galvanicCell, halfLife, pHAt, solveStoichiometry, titrationCurve };
@@ -0,0 +1,41 @@
1
+ import { ReactNode } from "react";
2
+
3
+ //#region src/core/clock.d.ts
4
+ interface FrameInfo {
5
+ /** Milliseconds on the active timeline. */
6
+ timeMs: number;
7
+ /** Delta since previous tick, ms. */
8
+ dtMs: number;
9
+ /** Monotonic frame counter. */
10
+ frame: number;
11
+ /** Frames per second (measured for RAF; exact for a fixed driver). */
12
+ fps: number;
13
+ }
14
+ /** A frame source: push `FrameInfo` to subscribers. Implement this to drive
15
+ * the engine from a non-RAF timeline (e.g. Remotion). */
16
+ interface FrameDriver {
17
+ subscribe(cb: (f: FrameInfo) => void): () => void;
18
+ }
19
+ interface ClockProviderProps {
20
+ driver: FrameDriver;
21
+ children: ReactNode;
22
+ }
23
+ /** Override the default RAF loop for everything inside (e.g. Remotion). */
24
+ declare function ClockProvider({
25
+ driver,
26
+ children
27
+ }: ClockProviderProps): ReactNode;
28
+ /**
29
+ * Run `cb` once per frame from the active clock. Default = RAF. When `running`
30
+ * is false the loop is idle. `cb` is held in a ref so changing it doesn't
31
+ * restart the loop.
32
+ *
33
+ * Visibility/offscreen pausing is NOT this hook's job — gate `running` on
34
+ * `useInView()` (the one canonical hook) for that: `useFrameLoop(tick, { running:
35
+ * playing && inView })`. Keeps the clock a single-purpose primitive.
36
+ */
37
+ declare function useFrameLoop(cb: (f: FrameInfo) => void, options?: {
38
+ running?: boolean;
39
+ }): void;
40
+ //#endregion
41
+ export { ClockProvider, ClockProviderProps, FrameDriver, FrameInfo, useFrameLoop };
@@ -0,0 +1,62 @@
1
+ 'use client';
2
+
3
+ import { createContext, createElement, useContext, useEffect, useRef } from "react";
4
+
5
+ //#region src/core/clock.ts
6
+ /**
7
+ * Injectable clock. Components drive animation through `useFrameLoop(cb)` instead
8
+ * of calling `requestAnimationFrame` directly. By default that's a RAF loop
9
+ * (interactive). A host can wrap a subtree in `<ClockProvider driver={…}>` to
10
+ * supply frames from another source — e.g. a Remotion app pushing
11
+ * `useCurrentFrame()`-derived frames for deterministic, scrubbable video. The
12
+ * engine itself stays unaware of Remotion.
13
+ *
14
+ * Discipline for video-determinism: compute visuals from the `FrameInfo` you
15
+ * receive (timeMs / frame), not a private accumulating counter — so the same
16
+ * component renders identically at any frame.
17
+ */
18
+ const DriverContext = createContext(null);
19
+ /** Override the default RAF loop for everything inside (e.g. Remotion). */
20
+ function ClockProvider({ driver, children }) {
21
+ return createElement(DriverContext.Provider, { value: driver }, children);
22
+ }
23
+ const now = () => typeof performance !== "undefined" ? performance.now() : Date.now();
24
+ /**
25
+ * Run `cb` once per frame from the active clock. Default = RAF. When `running`
26
+ * is false the loop is idle. `cb` is held in a ref so changing it doesn't
27
+ * restart the loop.
28
+ *
29
+ * Visibility/offscreen pausing is NOT this hook's job — gate `running` on
30
+ * `useInView()` (the one canonical hook) for that: `useFrameLoop(tick, { running:
31
+ * playing && inView })`. Keeps the clock a single-purpose primitive.
32
+ */
33
+ function useFrameLoop(cb, options = {}) {
34
+ const { running = true } = options;
35
+ const driver = useContext(DriverContext);
36
+ const cbRef = useRef(cb);
37
+ cbRef.current = cb;
38
+ useEffect(() => {
39
+ if (!running) return;
40
+ if (driver) return driver.subscribe((f) => cbRef.current(f));
41
+ let raf = 0;
42
+ let last = now();
43
+ let frame = 0;
44
+ const loop = () => {
45
+ const t = now();
46
+ const dtMs = t - last;
47
+ last = t;
48
+ cbRef.current({
49
+ timeMs: t,
50
+ dtMs,
51
+ frame: frame++,
52
+ fps: dtMs > 0 ? 1e3 / dtMs : 60
53
+ });
54
+ raf = requestAnimationFrame(loop);
55
+ };
56
+ raf = requestAnimationFrame(loop);
57
+ return () => cancelAnimationFrame(raf);
58
+ }, [running, driver]);
59
+ }
60
+
61
+ //#endregion
62
+ export { ClockProvider, useFrameLoop };
@@ -0,0 +1,13 @@
1
+ import { Vec2 } from "./vec.mjs";
2
+ import { CoordinateSystem } from "./coords.mjs";
3
+ import { RefObject } from "react";
4
+
5
+ //#region src/core/context.d.ts
6
+ declare const CoordsContext: import("react").Context<CoordinateSystem | null>;
7
+ declare function useCoords(): CoordinateSystem;
8
+ declare const StageRefContext: import("react").Context<RefObject<SVGSVGElement | null> | null>;
9
+ declare function useStageRef(): RefObject<SVGSVGElement | null> | null;
10
+ declare const DragOverlayContext: import("react").Context<ReadonlyMap<string, Vec2>>;
11
+ declare function useDragOverlay(): ReadonlyMap<string, Vec2>;
12
+ //#endregion
13
+ export { CoordsContext, DragOverlayContext, StageRefContext, useCoords, useDragOverlay, useStageRef };
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext } from "react";
4
+
5
+ //#region src/core/context.ts
6
+ /**
7
+ * Engine React contexts. Kept dependency-light: the scene/editor contexts are
8
+ * added in the scene-graph phase. Primitives read CoordsContext; interaction
9
+ * reads StageRefContext (for getScreenCTM) and DragOverlayContext (ephemeral
10
+ * per-frame drag values that resolve() reads without mutating the doc).
11
+ */
12
+ const CoordsContext = createContext(null);
13
+ function useCoords() {
14
+ const c = useContext(CoordsContext);
15
+ if (!c) throw new Error("Stage primitives must be rendered inside <Stage>.");
16
+ return c;
17
+ }
18
+ const StageRefContext = createContext(null);
19
+ function useStageRef() {
20
+ return useContext(StageRefContext);
21
+ }
22
+ const EMPTY_OVERLAY = /* @__PURE__ */ new Map();
23
+ const DragOverlayContext = createContext(EMPTY_OVERLAY);
24
+ function useDragOverlay() {
25
+ return useContext(DragOverlayContext);
26
+ }
27
+
28
+ //#endregion
29
+ export { CoordsContext, DragOverlayContext, StageRefContext, useCoords, useDragOverlay, useStageRef };
@@ -0,0 +1,58 @@
1
+ //#region src/core/control.d.ts
2
+ type ControlSpec = {
3
+ type: 'number';
4
+ label?: string;
5
+ min?: number;
6
+ max?: number;
7
+ step?: number;
8
+ unit?: string;
9
+ get: () => number;
10
+ set: (v: number) => void;
11
+ } | {
12
+ type: 'boolean';
13
+ label?: string;
14
+ get: () => boolean;
15
+ set: (v: boolean) => void;
16
+ } | {
17
+ type: 'enum';
18
+ label?: string;
19
+ options: readonly string[];
20
+ get: () => string;
21
+ set: (v: string) => void;
22
+ } | {
23
+ type: 'action';
24
+ label?: string;
25
+ invoke: () => void;
26
+ };
27
+ type ControlMap = Record<string, ControlSpec>;
28
+ interface ControlSurface {
29
+ readonly id: string;
30
+ /** Machine-readable description of every control (for agent discovery). */
31
+ describe(): Array<{
32
+ name: string;
33
+ type: ControlSpec['type'];
34
+ label?: string;
35
+ value?: unknown;
36
+ min?: number;
37
+ max?: number;
38
+ options?: readonly string[];
39
+ }>;
40
+ get(name: string): unknown;
41
+ set(name: string, value: unknown): boolean;
42
+ invoke(name: string): boolean;
43
+ }
44
+ /**
45
+ * Register a widget's controls under `id` for its lifetime (no-op if `id` is
46
+ * undefined). Registration happens in an effect (render stays side-effect-free,
47
+ * per React 19 / concurrent rules); a ref holds the latest controls so getters
48
+ * and setters always see current state.
49
+ */
50
+ declare function useControlSurface(id: string | undefined, controls: ControlMap): void;
51
+ /** Look up a live control surface by id (returns null if no such widget is mounted). */
52
+ declare function getControlSurface(id: string): ControlSurface | null;
53
+ /** Every mounted control surface — for an agent to discover what's on the page. */
54
+ declare function listControlSurfaces(): ControlSurface[];
55
+ /** Subscribe to registry changes (surfaces mounting/unmounting or values set). */
56
+ declare function onControlChange(fn: () => void): () => void;
57
+ //#endregion
58
+ export { ControlMap, ControlSpec, ControlSurface, getControlSurface, listControlSurfaces, onControlChange, useControlSurface };
@@ -0,0 +1,113 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef } from "react";
4
+
5
+ //#region src/core/control.ts
6
+ /**
7
+ * control — an addressable control surface so an external driver (a voice/AI
8
+ * agent, an autograder, a Remotion timeline, a "show me" button) can inspect and
9
+ * operate a live visualization without prop-drilling.
10
+ *
11
+ * A widget registers named controls under an `id`; anyone can later look the id
12
+ * up and read/set values or invoke actions:
13
+ *
14
+ * useControlSurface('balance-1', {
15
+ * x: { type: 'number', min: 0, max: 8, get: () => x, set: setX },
16
+ * check:{ type: 'action', invoke: () => grade() },
17
+ * });
18
+ *
19
+ * getControlSurface('balance-1')?.set('x', 3);
20
+ * listControlSurfaces(); // discover what's on the page
21
+ *
22
+ * In scene/builder mode, prefer `controlsFromScene(editor, …)` so set() routes
23
+ * through the editor's command stack (undoable + persisted) instead of raw
24
+ * closures.
25
+ */
26
+ const registry = /* @__PURE__ */ new Map();
27
+ const listeners = /* @__PURE__ */ new Set();
28
+ const notify = () => listeners.forEach((l) => l());
29
+ function makeSurface(id, getControls) {
30
+ return {
31
+ id,
32
+ describe: () => Object.entries(getControls()).map(([name, c]) => ({
33
+ name,
34
+ type: c.type,
35
+ label: c.label,
36
+ value: c.type === "action" ? void 0 : c.get(),
37
+ ...c.type === "number" ? {
38
+ min: c.min,
39
+ max: c.max
40
+ } : {},
41
+ ...c.type === "enum" ? { options: c.options } : {}
42
+ })),
43
+ get: (name) => {
44
+ const c = getControls()[name];
45
+ return c && c.type !== "action" ? c.get() : void 0;
46
+ },
47
+ set: (name, value) => {
48
+ const c = getControls()[name];
49
+ if (!c || c.type === "action") return false;
50
+ if (c.type === "number" && typeof value === "number") {
51
+ c.set(value);
52
+ notify();
53
+ return true;
54
+ }
55
+ if (c.type === "boolean" && typeof value === "boolean") {
56
+ c.set(value);
57
+ notify();
58
+ return true;
59
+ }
60
+ if (c.type === "enum" && typeof value === "string" && c.options.includes(value)) {
61
+ c.set(value);
62
+ notify();
63
+ return true;
64
+ }
65
+ return false;
66
+ },
67
+ invoke: (name) => {
68
+ const c = getControls()[name];
69
+ if (c?.type === "action") {
70
+ c.invoke();
71
+ notify();
72
+ return true;
73
+ }
74
+ return false;
75
+ }
76
+ };
77
+ }
78
+ /**
79
+ * Register a widget's controls under `id` for its lifetime (no-op if `id` is
80
+ * undefined). Registration happens in an effect (render stays side-effect-free,
81
+ * per React 19 / concurrent rules); a ref holds the latest controls so getters
82
+ * and setters always see current state.
83
+ */
84
+ function useControlSurface(id, controls) {
85
+ const ref = useRef(controls);
86
+ ref.current = controls;
87
+ useEffect(() => {
88
+ if (!id) return;
89
+ registry.set(id, () => ref.current);
90
+ notify();
91
+ return () => {
92
+ registry.delete(id);
93
+ notify();
94
+ };
95
+ }, [id]);
96
+ }
97
+ /** Look up a live control surface by id (returns null if no such widget is mounted). */
98
+ function getControlSurface(id) {
99
+ const getControls = registry.get(id);
100
+ return getControls ? makeSurface(id, getControls) : null;
101
+ }
102
+ /** Every mounted control surface — for an agent to discover what's on the page. */
103
+ function listControlSurfaces() {
104
+ return [...registry.entries()].map(([id, getControls]) => makeSurface(id, getControls));
105
+ }
106
+ /** Subscribe to registry changes (surfaces mounting/unmounting or values set). */
107
+ function onControlChange(fn) {
108
+ listeners.add(fn);
109
+ return () => listeners.delete(fn);
110
+ }
111
+
112
+ //#endregion
113
+ export { getControlSurface, listControlSurfaces, onControlChange, useControlSurface };
@@ -0,0 +1,51 @@
1
+ //#region src/core/coords.d.ts
2
+ /**
3
+ * CoordinateSystem — the math↔pixel mapping that decouples mathematical
4
+ * coordinates from screen pixels. Primitives draw in math units; pan/zoom/resize
5
+ * are trivial because only the mapping changes.
6
+ *
7
+ * Convention: math y is UP (standard), screen y is DOWN — the mapping flips it.
8
+ * Every stage primitive emits PIXEL coordinates via toPx() into one space — there
9
+ * is no global CSS-matrix group (it would mirror text, scale strokes, and desync
10
+ * getScreenCTM-based pointer math under zoom).
11
+ */
12
+ interface ViewBox {
13
+ /** Visible math range. */
14
+ xMin: number;
15
+ xMax: number;
16
+ yMin: number;
17
+ yMax: number;
18
+ }
19
+ interface CoordinateSystem {
20
+ readonly view: ViewBox;
21
+ readonly width: number;
22
+ readonly height: number;
23
+ /** math (x,y) → pixel (px,py). */
24
+ toPx(x: number, y: number): [number, number];
25
+ /** pixel (px,py) → math (x,y). */
26
+ toMath(px: number, py: number): [number, number];
27
+ /** Scale a math x-distance to pixels. */
28
+ sx(dx: number): number;
29
+ /** Scale a math y-distance to pixels (magnitude). */
30
+ sy(dy: number): number;
31
+ }
32
+ /**
33
+ * Round an emitted SVG coordinate to a stable precision (3 dp ≈ 1/1000 px, far
34
+ * below sub-pixel). Use this for EVERY pixel value written into SVG attributes —
35
+ * never for `toMath`/hit-testing math. It exists for SSR determinism: a
36
+ * coordinate derived from a transcendental (`Math.sin`, `pow`, the expr engine)
37
+ * can differ in its last bits between the server's V8 and the browser's V8,
38
+ * producing a React hydration mismatch on the raw float. Rounding makes the
39
+ * serialized string identical on both sides.
40
+ */
41
+ declare const fmt: (n: number) => number;
42
+ interface CoordsOptions {
43
+ /** Inner padding in pixels (keeps content off the edges). */
44
+ pad?: number;
45
+ /** Preserve aspect ratio (uses the smaller of x/y scales for both). Default true. */
46
+ preserveAspect?: boolean;
47
+ }
48
+ /** Build a CoordinateSystem for a viewport of `width`×`height` (CSS px) showing `view`. */
49
+ declare function createCoords(width: number, height: number, view: ViewBox, options?: CoordsOptions): CoordinateSystem;
50
+ //#endregion
51
+ export { CoordinateSystem, CoordsOptions, ViewBox, createCoords, fmt };
@@ -0,0 +1,44 @@
1
+ //#region src/core/coords.ts
2
+ /**
3
+ * Round an emitted SVG coordinate to a stable precision (3 dp ≈ 1/1000 px, far
4
+ * below sub-pixel). Use this for EVERY pixel value written into SVG attributes —
5
+ * never for `toMath`/hit-testing math. It exists for SSR determinism: a
6
+ * coordinate derived from a transcendental (`Math.sin`, `pow`, the expr engine)
7
+ * can differ in its last bits between the server's V8 and the browser's V8,
8
+ * producing a React hydration mismatch on the raw float. Rounding makes the
9
+ * serialized string identical on both sides.
10
+ */
11
+ const fmt = (n) => Math.round(n * 1e3) / 1e3;
12
+ /** Build a CoordinateSystem for a viewport of `width`×`height` (CSS px) showing `view`. */
13
+ function createCoords(width, height, view, options = {}) {
14
+ const { pad = 0, preserveAspect = true } = options;
15
+ const w = Math.max(1, width - pad * 2);
16
+ const h = Math.max(1, height - pad * 2);
17
+ const spanX = view.xMax - view.xMin || 1;
18
+ const spanY = view.yMax - view.yMin || 1;
19
+ let scaleX = w / spanX;
20
+ let scaleY = h / spanY;
21
+ if (preserveAspect) {
22
+ const s = Math.min(scaleX, scaleY);
23
+ scaleX = s;
24
+ scaleY = s;
25
+ }
26
+ const drawW = spanX * scaleX;
27
+ const drawH = spanY * scaleY;
28
+ const offX = pad + (w - drawW) / 2;
29
+ const offY = pad + (h - drawH) / 2;
30
+ const toPx = (x, y) => [offX + (x - view.xMin) * scaleX, offY + (view.yMax - y) * scaleY];
31
+ const toMath = (px, py) => [view.xMin + (px - offX) / scaleX, view.yMax - (py - offY) / scaleY];
32
+ return {
33
+ view,
34
+ width,
35
+ height,
36
+ toPx,
37
+ toMath,
38
+ sx: (dx) => dx * scaleX,
39
+ sy: (dy) => dy * scaleY
40
+ };
41
+ }
42
+
43
+ //#endregion
44
+ export { createCoords, fmt };
@@ -0,0 +1,8 @@
1
+ import { Matrix, Vec2, mat, vec } from "./vec.mjs";
2
+ import { CoordinateSystem, CoordsOptions, ViewBox, createCoords, fmt } from "./coords.mjs";
3
+ import { ControlMap, ControlSpec, ControlSurface, getControlSurface, listControlSurfaces, onControlChange, useControlSurface } from "./control.mjs";
4
+ import { CoordsContext, DragOverlayContext, StageRefContext, useCoords, useDragOverlay, useStageRef } from "./context.mjs";
5
+ import { ClockProvider, ClockProviderProps, FrameDriver, FrameInfo, useFrameLoop } from "./clock.mjs";
6
+ import { Learner, LearnerProvider, LearnerResult, PriorAttempts, useLearner } from "./learner.mjs";
7
+ import { EaseFn, EaseName, Keyframe, OscillateOpts, SpringOpts, WaveShape, ease, lerp, oscillate, prefersReducedMotion, spring, timeline } from "./motion.mjs";
8
+ export { ClockProvider, type ClockProviderProps, type ControlMap, type ControlSpec, type ControlSurface, type CoordinateSystem, CoordsContext, type CoordsOptions, DragOverlayContext, type EaseFn, type EaseName, type FrameDriver, type FrameInfo, type Keyframe, type Learner, LearnerProvider, type LearnerResult, type Matrix, type OscillateOpts, type PriorAttempts, type SpringOpts, StageRefContext, type Vec2, type ViewBox, type WaveShape, createCoords, ease, fmt, getControlSurface, lerp, listControlSurfaces, mat, onControlChange, oscillate, prefersReducedMotion, spring, timeline, useControlSurface, useCoords, useDragOverlay, useFrameLoop, useLearner, useStageRef, vec };
@@ -0,0 +1,9 @@
1
+ import { createCoords, fmt } from "./coords.mjs";
2
+ import { mat, vec } from "./vec.mjs";
3
+ import { CoordsContext, DragOverlayContext, StageRefContext, useCoords, useDragOverlay, useStageRef } from "./context.mjs";
4
+ import { ClockProvider, useFrameLoop } from "./clock.mjs";
5
+ import { getControlSurface, listControlSurfaces, onControlChange, useControlSurface } from "./control.mjs";
6
+ import { LearnerProvider, useLearner } from "./learner.mjs";
7
+ import { ease, lerp, oscillate, prefersReducedMotion, spring, timeline } from "./motion.mjs";
8
+
9
+ export { ClockProvider, CoordsContext, DragOverlayContext, LearnerProvider, StageRefContext, createCoords, ease, fmt, getControlSurface, lerp, listControlSurfaces, mat, onControlChange, oscillate, prefersReducedMotion, spring, timeline, useControlSurface, useCoords, useDragOverlay, useFrameLoop, useLearner, useStageRef, vec };
@@ -0,0 +1,44 @@
1
+ import { ReactNode } from "react";
2
+
3
+ //#region src/core/learner.d.ts
4
+ interface LearnerResult {
5
+ /** Stable activity id for this interaction (e.g. a checkpoint key). */
6
+ activity: string;
7
+ /** Did the learner satisfy the goal? */
8
+ correct?: boolean;
9
+ /** Numeric score, when applicable. */
10
+ score?: {
11
+ raw: number;
12
+ max: number;
13
+ };
14
+ /** Free-form response (e.g. the chosen value). */
15
+ response?: string;
16
+ /** Mark the activity complete. */
17
+ completion?: boolean;
18
+ /** Pedagogy context (from the lab's LabMeta) — e.g. the objective this event
19
+ * evidences. The host maps it into xAPI context.extensions. */
20
+ objectiveId?: string;
21
+ /** 1-based attempt number, when the host tracks tries. */
22
+ attempt?: number;
23
+ }
24
+ interface PriorAttempts {
25
+ bestPct: number;
26
+ attempts: number;
27
+ }
28
+ interface Learner {
29
+ /** Fire-and-forget: record an attempt/result. */
30
+ report(result: LearnerResult): void;
31
+ /** Optional: prior best for showing "Best: 3/4" etc. */
32
+ getPrior?(activity: string): PriorAttempts | null;
33
+ }
34
+ declare function LearnerProvider({
35
+ learner,
36
+ children
37
+ }: {
38
+ learner: Learner;
39
+ children: ReactNode;
40
+ }): ReactNode;
41
+ /** `null` when no host is wired (stateless preview). */
42
+ declare function useLearner(): Learner | null;
43
+ //#endregion
44
+ export { Learner, LearnerProvider, LearnerResult, PriorAttempts, useLearner };
@@ -0,0 +1,23 @@
1
+ 'use client';
2
+
3
+ import { createContext, createElement, useContext } from "react";
4
+
5
+ //#region src/core/learner.ts
6
+ /**
7
+ * Learner / assessment seam — a host-agnostic interface so a visualization can
8
+ * report "the learner did X / got it right" without knowing how it's stored.
9
+ * Mentora provides a `Learner` that maps `report()` to xAPI statements; a plain
10
+ * docs/demo app provides none and the viz degrades to a stateless playground.
11
+ * This is what powers Brilliant-style checkpoints + progress data.
12
+ */
13
+ const LearnerContext = createContext(null);
14
+ function LearnerProvider({ learner, children }) {
15
+ return createElement(LearnerContext.Provider, { value: learner }, children);
16
+ }
17
+ /** `null` when no host is wired (stateless preview). */
18
+ function useLearner() {
19
+ return useContext(LearnerContext);
20
+ }
21
+
22
+ //#endregion
23
+ export { LearnerProvider, useLearner };