@classytic/stage 0.1.0 → 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 (86) hide show
  1. package/README.md +2 -2
  2. package/dist/assets/kit/glyphs.d.mts +5 -5
  3. package/dist/assets/kit/glyphs.mjs +4 -4
  4. package/dist/builder/SceneBuilder.mjs +4 -4
  5. package/dist/builder/editor.d.mts +2 -1
  6. package/dist/builder/editor.mjs +14 -4
  7. package/dist/builder/tools.d.mts +1 -1
  8. package/dist/builder/tools.mjs +1 -1
  9. package/dist/chem/index.d.mts +5 -5
  10. package/dist/chem/index.mjs +4 -4
  11. package/dist/circuit/index.d.mts +92 -0
  12. package/dist/circuit/index.mjs +333 -0
  13. package/dist/core/clock.d.mts +1 -1
  14. package/dist/core/clock.mjs +3 -3
  15. package/dist/core/control.d.mts +1 -1
  16. package/dist/core/control.mjs +2 -2
  17. package/dist/core/coords.d.mts +4 -4
  18. package/dist/core/coords.mjs +1 -1
  19. package/dist/core/learner.d.mts +1 -1
  20. package/dist/core/learner.mjs +1 -1
  21. package/dist/core/motion.d.mts +6 -6
  22. package/dist/core/motion.mjs +6 -6
  23. package/dist/core/richText.d.mts +2 -2
  24. package/dist/core/vec.d.mts +1 -1
  25. package/dist/field/index.d.mts +11 -3
  26. package/dist/field/index.mjs +20 -3
  27. package/dist/index.d.mts +3 -1
  28. package/dist/index.mjs +3 -1
  29. package/dist/interaction/MovableDot.d.mts +17 -1
  30. package/dist/interaction/MovableDot.mjs +141 -43
  31. package/dist/interaction/index.d.mts +2 -1
  32. package/dist/interaction/index.mjs +2 -1
  33. package/dist/interaction/useDraggable.d.mts +1 -1
  34. package/dist/interaction/useDraggable.mjs +31 -1
  35. package/dist/interaction/usePressSpring.d.mts +24 -0
  36. package/dist/interaction/usePressSpring.mjs +67 -0
  37. package/dist/logic/ast.d.mts +1 -1
  38. package/dist/logic/index.mjs +8 -3
  39. package/dist/logic/minimize.d.mts +2 -2
  40. package/dist/logic/minimize.mjs +56 -18
  41. package/dist/logic/table.mjs +1 -1
  42. package/dist/math/ast.d.mts +1 -1
  43. package/dist/math/ast.mjs +1 -1
  44. package/dist/math/calculus.mjs +68 -9
  45. package/dist/math/compile.d.mts +8 -0
  46. package/dist/math/compile.mjs +40 -0
  47. package/dist/math/defs.mjs +1 -1
  48. package/dist/math/index.d.mts +4 -3
  49. package/dist/math/index.mjs +4 -3
  50. package/dist/math/parse.mjs +2 -2
  51. package/dist/math/tokenize.mjs +1 -1
  52. package/dist/primitives/CanvasLayer.mjs +3 -3
  53. package/dist/primitives/Grid.d.mts +12 -1
  54. package/dist/primitives/Grid.mjs +15 -9
  55. package/dist/primitives/Label.d.mts +1 -1
  56. package/dist/primitives/Label.mjs +1 -1
  57. package/dist/primitives/Tex.mjs +2 -2
  58. package/dist/scene/Scene.mjs +1 -1
  59. package/dist/scene/assets.d.mts +2 -2
  60. package/dist/scene/assets.mjs +2 -2
  61. package/dist/scene/commands.d.mts +10 -1
  62. package/dist/scene/commands.mjs +42 -2
  63. package/dist/scene/evaluators.mjs +2 -2
  64. package/dist/scene/index.d.mts +2 -2
  65. package/dist/scene/index.mjs +2 -2
  66. package/dist/scene/migrate.mjs +1 -1
  67. package/dist/scene/sims.mjs +1 -1
  68. package/dist/scene/types.d.mts +4 -4
  69. package/dist/sim/particles.d.mts +2 -2
  70. package/dist/sim/rate.d.mts +4 -4
  71. package/dist/sim/rate.mjs +1 -1
  72. package/dist/sim/registry.d.mts +1 -1
  73. package/dist/sim/sampler.d.mts +5 -5
  74. package/dist/sim/thermal.d.mts +3 -3
  75. package/dist/sim/thermal.mjs +2 -2
  76. package/dist/sim/types.d.mts +2 -2
  77. package/dist/sim/wave.d.mts +2 -2
  78. package/dist/sim/wave.mjs +2 -2
  79. package/dist/steps/index.d.mts +1 -1
  80. package/dist/steps/index.mjs +3 -3
  81. package/dist/thermo/index.d.mts +1 -1
  82. package/dist/thermo/index.mjs +1 -1
  83. package/dist/view/Stage.mjs +2 -2
  84. package/dist/view/useInView.mjs +1 -1
  85. package/package.json +8 -4
  86. package/styles.css +23 -0
@@ -0,0 +1,333 @@
1
+ //#region src/circuit/index.ts
2
+ /** NMOS square-law operating point: drain current + its small-signal conductances. */
3
+ function nmosOp(Vgs, Vds, vth, k) {
4
+ const Vov = Vgs - vth;
5
+ if (Vov <= 0) return {
6
+ Id: 0,
7
+ gm: 0,
8
+ gds: GMIN
9
+ };
10
+ if (Vds < Vov) return {
11
+ Id: k * (Vov * Vds - .5 * Vds * Vds),
12
+ gm: k * Vds,
13
+ gds: k * (Vov - Vds) + GMIN
14
+ };
15
+ return {
16
+ Id: .5 * k * Vov * Vov,
17
+ gm: k * Vov,
18
+ gds: GMIN
19
+ };
20
+ }
21
+ const VT = .025852;
22
+ const GMIN = 1e-12;
23
+ function nodeCount(elems) {
24
+ let max = 0;
25
+ for (const e of elems) max = Math.max(max, e.n1, e.n2, e.n3 ?? 0);
26
+ return max + 1;
27
+ }
28
+ /** Solve A x = b by Gaussian elimination with partial pivoting. Returns null if singular. */
29
+ function solveLinear(A, b) {
30
+ const n = b.length;
31
+ const M = A.map((row, i) => [...row, b[i]]);
32
+ for (let col = 0; col < n; col++) {
33
+ let piv = col;
34
+ for (let r = col + 1; r < n; r++) if (Math.abs(M[r][col]) > Math.abs(M[piv][col])) piv = r;
35
+ if (Math.abs(M[piv][col]) < 1e-18) return null;
36
+ [M[col], M[piv]] = [M[piv], M[col]];
37
+ const pivVal = M[col][col];
38
+ for (let r = 0; r < n; r++) {
39
+ if (r === col) continue;
40
+ const f = M[r][col] / pivVal;
41
+ if (f === 0) continue;
42
+ for (let c = col; c <= n; c++) M[r][c] -= f * M[col][c];
43
+ }
44
+ }
45
+ const x = new Array(n);
46
+ for (let i = 0; i < n; i++) x[i] = M[i][n] / M[i][i];
47
+ return x;
48
+ }
49
+ /** Build the MNA system A x = z for one solve (DC if no dt, else one transient step). */
50
+ function buildMNA(elems, opts) {
51
+ const N = nodeCount(elems);
52
+ const dc = opts.dt === void 0;
53
+ const vbranch = [];
54
+ elems.forEach((e) => {
55
+ if (e.kind === "V" || e.kind === "L" && dc) vbranch.push({
56
+ elem: e,
57
+ row: 0
58
+ });
59
+ });
60
+ const n = N - 1 + vbranch.length;
61
+ vbranch.forEach((vb, j) => {
62
+ vb.row = N - 1 + j;
63
+ });
64
+ const A = Array.from({ length: n }, () => new Array(n).fill(0));
65
+ const z = new Array(n).fill(0);
66
+ const ix = (node) => node - 1;
67
+ const stampG = (a, b, g) => {
68
+ if (a >= 0) A[a][a] += g;
69
+ if (b >= 0) A[b][b] += g;
70
+ if (a >= 0 && b >= 0) {
71
+ A[a][b] -= g;
72
+ A[b][a] -= g;
73
+ }
74
+ };
75
+ const stampI = (a, b, I) => {
76
+ if (a >= 0) z[a] -= I;
77
+ if (b >= 0) z[b] += I;
78
+ };
79
+ for (let k = 1; k < N; k++) A[ix(k)][ix(k)] += GMIN;
80
+ let vi = 0;
81
+ elems.forEach((e, idx) => {
82
+ const a = ix(e.n1), b = ix(e.n2);
83
+ if (e.kind === "R") stampG(a, b, 1 / e.value);
84
+ else if (e.kind === "I") stampI(a, b, e.value);
85
+ else if (e.kind === "V" || e.kind === "L" && dc) {
86
+ const row = vbranch[vi++].row;
87
+ const E = e.kind === "V" ? e.value : 0;
88
+ if (a >= 0) {
89
+ A[a][row] += 1;
90
+ A[row][a] += 1;
91
+ }
92
+ if (b >= 0) {
93
+ A[b][row] -= 1;
94
+ A[row][b] -= 1;
95
+ }
96
+ z[row] += E;
97
+ } else if (e.kind === "C" && !dc) {
98
+ const Geq = e.value / opts.dt;
99
+ const Ieq = -Geq * (opts.prevV?.get(idx) ?? 0);
100
+ stampG(a, b, Geq);
101
+ stampI(a, b, Ieq);
102
+ } else if (e.kind === "L" && !dc) {
103
+ const Geq = opts.dt / e.value;
104
+ const Ieq = opts.prevI?.get(idx) ?? 0;
105
+ stampG(a, b, Geq);
106
+ stampI(a, b, Ieq);
107
+ } else if (e.kind === "D") {
108
+ const Is = e.is ?? 1e-12, nvt = (e.nIdeal ?? 1) * VT;
109
+ const V = opts.diodeV?.get(idx) ?? 0;
110
+ const Id = Is * (Math.exp(Math.min(V / nvt, 80)) - 1);
111
+ const gd = (Id + Is) / nvt + GMIN;
112
+ const Ieq = Id - gd * V;
113
+ stampG(a, b, gd);
114
+ stampI(a, b, Ieq);
115
+ } else if (e.kind === "M") {
116
+ const D = a, S = b, G = ix(e.n3 ?? 0);
117
+ const nv = opts.prevNodeV;
118
+ const Vg = nv?.[e.n3 ?? 0] ?? 0, Vd = nv?.[e.n1] ?? 0, Vs = nv?.[e.n2] ?? 0;
119
+ if (e.pmos) {
120
+ const { Id, gm, gds } = nmosOp(Vs - Vg, Vs - Vd, Math.abs(e.vth ?? 2), e.k ?? .5);
121
+ const Ieq = Id - gm * (Vs - Vg) - gds * (Vs - Vd);
122
+ stampG(S, D, gds);
123
+ if (S >= 0) A[S][S] += gm;
124
+ if (S >= 0 && G >= 0) A[S][G] -= gm;
125
+ if (D >= 0 && S >= 0) A[D][S] -= gm;
126
+ if (D >= 0 && G >= 0) A[D][G] += gm;
127
+ stampI(S, D, Ieq);
128
+ } else {
129
+ const { Id, gm, gds } = nmosOp(Vg - Vs, Vd - Vs, e.vth ?? 2, e.k ?? .5);
130
+ const Ieq = Id - gm * (Vg - Vs) - gds * (Vd - Vs);
131
+ stampG(D, S, gds);
132
+ if (D >= 0 && G >= 0) A[D][G] += gm;
133
+ if (D >= 0 && S >= 0) A[D][S] -= gm;
134
+ if (S >= 0 && G >= 0) A[S][G] -= gm;
135
+ if (S >= 0) A[S][S] += gm;
136
+ stampI(D, S, Ieq);
137
+ }
138
+ }
139
+ });
140
+ return {
141
+ A,
142
+ z,
143
+ N,
144
+ vbranch
145
+ };
146
+ }
147
+ /** DC operating point: capacitors open, inductors short. Routes to the nonlinear solver for diodes / MOSFETs. */
148
+ function solveDC(elems) {
149
+ if (elems.some((e) => e.kind === "D" || e.kind === "M")) return solveDCNonlinear(elems);
150
+ const { A, z, N, vbranch } = buildMNA(elems, {});
151
+ const x = solveLinear(A, z);
152
+ const nodeV = new Array(N).fill(0);
153
+ const current = {};
154
+ if (!x) return {
155
+ nodeV,
156
+ current,
157
+ ok: false
158
+ };
159
+ for (let k = 1; k < N; k++) nodeV[k] = x[k - 1];
160
+ vbranch.forEach((vb) => {
161
+ if (vb.elem.id) current[vb.elem.id] = x[vb.row];
162
+ });
163
+ return {
164
+ nodeV,
165
+ current,
166
+ ok: true
167
+ };
168
+ }
169
+ /**
170
+ * Nonlinear DC via Newton-Raphson: each iteration linearizes every nonlinear device
171
+ * (diode Shockley with pnjlim limiting; NMOS square-law about the previous node
172
+ * voltages) into a companion conductance + current source, solves the linear system,
173
+ * and repeats until the solution settles.
174
+ */
175
+ function solveDCNonlinear(elems, opts = {}) {
176
+ const maxIter = opts.maxIter ?? 100, tol = opts.tol ?? 1e-7;
177
+ const N0 = nodeCount(elems);
178
+ const diodes = elems.map((e, i) => ({
179
+ e,
180
+ i
181
+ })).filter((x) => x.e.kind === "D");
182
+ const mosfets = elems.map((e, i) => ({
183
+ e,
184
+ i
185
+ })).filter((x) => x.e.kind === "M");
186
+ const diodeV = /* @__PURE__ */ new Map();
187
+ diodes.forEach((d) => diodeV.set(d.i, 0));
188
+ let prevNodeV = new Array(N0).fill(0);
189
+ let x = null;
190
+ let N = N0;
191
+ let vbranch = [];
192
+ let converged = false;
193
+ for (let iter = 0; iter < maxIter; iter++) {
194
+ const built = buildMNA(elems, {
195
+ diodeV,
196
+ prevNodeV
197
+ });
198
+ N = built.N;
199
+ vbranch = built.vbranch;
200
+ const sol = solveLinear(built.A, built.z);
201
+ if (!sol) break;
202
+ x = sol;
203
+ const newNodeV = new Array(N).fill(0);
204
+ for (let k = 1; k < N; k++) newNodeV[k] = sol[k - 1];
205
+ if (mosfets.length > 0) {
206
+ const VMAXSTEP = 2;
207
+ for (let k = 1; k < N; k++) {
208
+ const old = prevNodeV[k] ?? 0;
209
+ const dv = (newNodeV[k] ?? 0) - old;
210
+ if (Math.abs(dv) > VMAXSTEP) newNodeV[k] = old + Math.sign(dv) * VMAXSTEP;
211
+ }
212
+ }
213
+ let maxDelta = 0;
214
+ for (const d of diodes) {
215
+ let Vnew = (newNodeV[d.e.n1] ?? 0) - (newNodeV[d.e.n2] ?? 0);
216
+ const nvt = (d.e.nIdeal ?? 1) * VT, Is = d.e.is ?? 1e-12;
217
+ const Vold = diodeV.get(d.i);
218
+ const Vcrit = nvt * Math.log(nvt / (Is * Math.SQRT2));
219
+ if (Vnew > Vcrit && Vnew > Vold) Vnew = Vold + nvt * Math.log(1 + (Vnew - Vold) / nvt);
220
+ maxDelta = Math.max(maxDelta, Math.abs(Vnew - Vold));
221
+ diodeV.set(d.i, Vnew);
222
+ }
223
+ for (let k = 1; k < N; k++) maxDelta = Math.max(maxDelta, Math.abs((newNodeV[k] ?? 0) - (prevNodeV[k] ?? 0)));
224
+ prevNodeV = newNodeV;
225
+ if (maxDelta < tol) {
226
+ converged = true;
227
+ break;
228
+ }
229
+ }
230
+ const nodeV = new Array(N).fill(0);
231
+ const current = {};
232
+ if (!x || !converged) return {
233
+ nodeV,
234
+ current,
235
+ ok: false
236
+ };
237
+ for (let k = 1; k < N; k++) nodeV[k] = x[k - 1];
238
+ vbranch.forEach((vb) => {
239
+ if (vb.elem.id) current[vb.elem.id] = x[vb.row];
240
+ });
241
+ diodes.forEach((d) => {
242
+ if (!d.e.id) return;
243
+ const nvt = (d.e.nIdeal ?? 1) * VT, Is = d.e.is ?? 1e-12;
244
+ current[d.e.id] = Is * (Math.exp(Math.min((diodeV.get(d.i) ?? 0) / nvt, 80)) - 1);
245
+ });
246
+ mosfets.forEach((m) => {
247
+ if (!m.e.id) return;
248
+ const Vg = nodeV[m.e.n3 ?? 0] ?? 0, Vd = nodeV[m.e.n1] ?? 0, Vs = nodeV[m.e.n2] ?? 0;
249
+ current[m.e.id] = m.e.pmos ? nmosOp(Vs - Vg, Vs - Vd, Math.abs(m.e.vth ?? 2), m.e.k ?? .5).Id : nmosOp(Vg - Vs, Vd - Vs, m.e.vth ?? 2, m.e.k ?? .5).Id;
250
+ });
251
+ return {
252
+ nodeV,
253
+ current,
254
+ ok: true
255
+ };
256
+ }
257
+ /** Current through an element given solved node voltages (R/I directly; C/L need state). */
258
+ function elementCurrent(e, nodeV) {
259
+ const v = (nodeV[e.n1] ?? 0) - (nodeV[e.n2] ?? 0);
260
+ if (e.kind === "R") return v / e.value;
261
+ if (e.kind === "I") return e.value;
262
+ return NaN;
263
+ }
264
+ /**
265
+ * Time-stepped transient (Backward Euler). Returns the node-voltage trajectory,
266
+ * including t = 0. Reactive elements use companion models rebuilt every step.
267
+ */
268
+ function solveTransient(elems, opts) {
269
+ const { dt, steps } = opts;
270
+ const N = nodeCount(elems);
271
+ const prevV = /* @__PURE__ */ new Map();
272
+ const prevI = /* @__PURE__ */ new Map();
273
+ elems.forEach((e, i) => {
274
+ if (e.kind === "C" || e.kind === "L") prevV.set(i, opts.initialV?.get(i) ?? 0);
275
+ if (e.kind === "L") prevI.set(i, 0);
276
+ });
277
+ const out = [];
278
+ const record = (t, nodeV) => {
279
+ out.push({
280
+ t,
281
+ nodeV: [...nodeV]
282
+ });
283
+ };
284
+ let nodeV = new Array(N).fill(0);
285
+ if (!elems.some((e) => e.kind === "D" || e.kind === "M")) {
286
+ const { A, z } = buildMNA(elems.map((e, i) => e.kind === "C" ? {
287
+ ...e,
288
+ kind: "V",
289
+ value: opts.initialV?.get(i) ?? 0
290
+ } : e.kind === "L" ? {
291
+ ...e,
292
+ kind: "I",
293
+ value: prevI.get(i) ?? 0
294
+ } : e), {});
295
+ const x = solveLinear(A, z);
296
+ if (x) for (let k = 1; k < N; k++) nodeV[k] = x[k - 1] ?? 0;
297
+ }
298
+ record(0, nodeV);
299
+ for (let s = 1; s <= steps; s++) {
300
+ const { A, z } = buildMNA(elems, {
301
+ dt,
302
+ prevV,
303
+ prevI
304
+ });
305
+ const x = solveLinear(A, z);
306
+ if (!x) break;
307
+ nodeV = new Array(N).fill(0);
308
+ for (let k = 1; k < N; k++) nodeV[k] = x[k - 1];
309
+ elems.forEach((e, i) => {
310
+ if (e.kind === "C" || e.kind === "L") {
311
+ const v = (nodeV[e.n1] ?? 0) - (nodeV[e.n2] ?? 0);
312
+ prevV.set(i, v);
313
+ if (e.kind === "L") {
314
+ const Geq = dt / e.value;
315
+ prevI.set(i, Geq * v + (prevI.get(i) ?? 0));
316
+ }
317
+ }
318
+ });
319
+ record(s * dt, nodeV);
320
+ }
321
+ return out;
322
+ }
323
+ /** Equivalent resistance of resistors in series. */
324
+ function seriesR(...rs) {
325
+ return rs.reduce((a, r) => a + r, 0);
326
+ }
327
+ /** Equivalent resistance of resistors in parallel. */
328
+ function parallelR(...rs) {
329
+ return 1 / rs.reduce((a, r) => a + 1 / r, 0);
330
+ }
331
+
332
+ //#endregion
333
+ export { elementCurrent, parallelR, seriesR, solveDC, solveDCNonlinear, solveLinear, solveTransient };
@@ -30,7 +30,7 @@ declare function ClockProvider({
30
30
  * is false the loop is idle. `cb` is held in a ref so changing it doesn't
31
31
  * restart the loop.
32
32
  *
33
- * Visibility/offscreen pausing is NOT this hook's job gate `running` on
33
+ * Visibility/offscreen pausing is NOT this hook's job, gate `running` on
34
34
  * `useInView()` (the one canonical hook) for that: `useFrameLoop(tick, { running:
35
35
  * playing && inView })`. Keeps the clock a single-purpose primitive.
36
36
  */
@@ -7,12 +7,12 @@ import { createContext, createElement, useContext, useEffect, useRef } from "rea
7
7
  * Injectable clock. Components drive animation through `useFrameLoop(cb)` instead
8
8
  * of calling `requestAnimationFrame` directly. By default that's a RAF loop
9
9
  * (interactive). A host can wrap a subtree in `<ClockProvider driver={…}>` to
10
- * supply frames from another source e.g. a Remotion app pushing
10
+ * supply frames from another source, e.g. a Remotion app pushing
11
11
  * `useCurrentFrame()`-derived frames for deterministic, scrubbable video. The
12
12
  * engine itself stays unaware of Remotion.
13
13
  *
14
14
  * Discipline for video-determinism: compute visuals from the `FrameInfo` you
15
- * receive (timeMs / frame), not a private accumulating counter so the same
15
+ * receive (timeMs / frame), not a private accumulating counter, so the same
16
16
  * component renders identically at any frame.
17
17
  */
18
18
  const DriverContext = createContext(null);
@@ -26,7 +26,7 @@ const now = () => typeof performance !== "undefined" ? performance.now() : Date.
26
26
  * is false the loop is idle. `cb` is held in a ref so changing it doesn't
27
27
  * restart the loop.
28
28
  *
29
- * Visibility/offscreen pausing is NOT this hook's job gate `running` on
29
+ * Visibility/offscreen pausing is NOT this hook's job, gate `running` on
30
30
  * `useInView()` (the one canonical hook) for that: `useFrameLoop(tick, { running:
31
31
  * playing && inView })`. Keeps the clock a single-purpose primitive.
32
32
  */
@@ -50,7 +50,7 @@ interface ControlSurface {
50
50
  declare function useControlSurface(id: string | undefined, controls: ControlMap): void;
51
51
  /** Look up a live control surface by id (returns null if no such widget is mounted). */
52
52
  declare function getControlSurface(id: string): ControlSurface | null;
53
- /** Every mounted control surface for an agent to discover what's on the page. */
53
+ /** Every mounted control surface, for an agent to discover what's on the page. */
54
54
  declare function listControlSurfaces(): ControlSurface[];
55
55
  /** Subscribe to registry changes (surfaces mounting/unmounting or values set). */
56
56
  declare function onControlChange(fn: () => void): () => void;
@@ -4,7 +4,7 @@ import { useEffect, useRef } from "react";
4
4
 
5
5
  //#region src/core/control.ts
6
6
  /**
7
- * control an addressable control surface so an external driver (a voice/AI
7
+ * control, an addressable control surface so an external driver (a voice/AI
8
8
  * agent, an autograder, a Remotion timeline, a "show me" button) can inspect and
9
9
  * operate a live visualization without prop-drilling.
10
10
  *
@@ -99,7 +99,7 @@ function getControlSurface(id) {
99
99
  const getControls = registry.get(id);
100
100
  return getControls ? makeSurface(id, getControls) : null;
101
101
  }
102
- /** Every mounted control surface for an agent to discover what's on the page. */
102
+ /** Every mounted control surface, for an agent to discover what's on the page. */
103
103
  function listControlSurfaces() {
104
104
  return [...registry.entries()].map(([id, getControls]) => makeSurface(id, getControls));
105
105
  }
@@ -1,11 +1,11 @@
1
1
  //#region src/core/coords.d.ts
2
2
  /**
3
- * CoordinateSystem the math↔pixel mapping that decouples mathematical
3
+ * CoordinateSystem, the math↔pixel mapping that decouples mathematical
4
4
  * coordinates from screen pixels. Primitives draw in math units; pan/zoom/resize
5
5
  * are trivial because only the mapping changes.
6
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
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
9
  * is no global CSS-matrix group (it would mirror text, scale strokes, and desync
10
10
  * getScreenCTM-based pointer math under zoom).
11
11
  */
@@ -31,7 +31,7 @@ interface CoordinateSystem {
31
31
  }
32
32
  /**
33
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
34
+ * below sub-pixel). Use this for EVERY pixel value written into SVG attributes ,
35
35
  * never for `toMath`/hit-testing math. It exists for SSR determinism: a
36
36
  * coordinate derived from a transcendental (`Math.sin`, `pow`, the expr engine)
37
37
  * can differ in its last bits between the server's V8 and the browser's V8,
@@ -1,7 +1,7 @@
1
1
  //#region src/core/coords.ts
2
2
  /**
3
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
4
+ * below sub-pixel). Use this for EVERY pixel value written into SVG attributes ,
5
5
  * never for `toMath`/hit-testing math. It exists for SSR determinism: a
6
6
  * coordinate derived from a transcendental (`Math.sin`, `pow`, the expr engine)
7
7
  * can differ in its last bits between the server's V8 and the browser's V8,
@@ -15,7 +15,7 @@ interface LearnerResult {
15
15
  response?: string;
16
16
  /** Mark the activity complete. */
17
17
  completion?: boolean;
18
- /** Pedagogy context (from the lab's LabMeta) e.g. the objective this event
18
+ /** Pedagogy context (from the lab's LabMeta), e.g. the objective this event
19
19
  * evidences. The host maps it into xAPI context.extensions. */
20
20
  objectiveId?: string;
21
21
  /** 1-based attempt number, when the host tracks tries. */
@@ -4,7 +4,7 @@ import { createContext, createElement, useContext } from "react";
4
4
 
5
5
  //#region src/core/learner.ts
6
6
  /**
7
- * Learner / assessment seam a host-agnostic interface so a visualization can
7
+ * Learner / assessment seam, a host-agnostic interface so a visualization can
8
8
  * report "the learner did X / got it right" without knowing how it's stored.
9
9
  * Mentora provides a `Learner` that maps `report()` to xAPI statements; a plain
10
10
  * docs/demo app provides none and the viz degrades to a stateless playground.
@@ -1,14 +1,14 @@
1
1
  //#region src/core/motion.d.ts
2
2
  /**
3
- * Motion core the "physics of feel" layer. Pure, dependency-free functions that
3
+ * Motion core, the "physics of feel" layer. Pure, dependency-free functions that
4
4
  * turn time into eased / springy / oscillating values. They consume the `dtMs`
5
5
  * (or `timeMs`) you already receive from `useFrameLoop`; they do NOT own a loop,
6
- * schedule frames, or touch React so they stay deterministic (same time → same
6
+ * schedule frames, or touch React, so they stay deterministic (same time → same
7
7
  * value) and work identically under a Remotion fixed-frame driver.
8
8
  *
9
9
  * Discipline (mirrors clock.ts): for video-determinism prefer the time-driven
10
10
  * helpers (`oscillate`, `timeline`) that are a pure function of `timeMs`. `spring`
11
- * is stateful (it integrates a velocity) fine for interactive feel, but it will
11
+ * is stateful (it integrates a velocity), fine for interactive feel, but it will
12
12
  * not reproduce frame-for-frame across drivers, so don't use it for rendered video.
13
13
  */
14
14
  /** Snap-to-end gate: honor the user's reduced-motion preference (SSR-safe). */
@@ -20,8 +20,8 @@ declare const ease: {
20
20
  readonly cubicOut: (t: number) => number;
21
21
  readonly cubicInOut: (t: number) => number;
22
22
  readonly quintOut: (t: number) => number;
23
- readonly sineInOut: (t: number) => number; /** Overshoots then settles good for "pop" on appear. */
24
- readonly backOut: (t: number) => number; /** Springy overshoot with decaying wobble for playful reveals. */
23
+ readonly sineInOut: (t: number) => number; /** Overshoots then settles, good for "pop" on appear. */
24
+ readonly backOut: (t: number) => number; /** Springy overshoot with decaying wobble, for playful reveals. */
25
25
  readonly elasticOut: (t: number) => number;
26
26
  };
27
27
  type EaseName = keyof typeof ease;
@@ -53,7 +53,7 @@ interface OscillateOpts {
53
53
  phase?: number;
54
54
  shape?: WaveShape;
55
55
  }
56
- /** A pure periodic signal of `timeSec` the engine under AC sources, pendulums, waves. */
56
+ /** A pure periodic signal of `timeSec`, the engine under AC sources, pendulums, waves. */
57
57
  declare function oscillate(timeSec: number, opts?: OscillateOpts): number;
58
58
  interface Keyframe {
59
59
  /** Absolute time on the timeline, seconds. */
@@ -1,14 +1,14 @@
1
1
  //#region src/core/motion.ts
2
2
  /**
3
- * Motion core the "physics of feel" layer. Pure, dependency-free functions that
3
+ * Motion core, the "physics of feel" layer. Pure, dependency-free functions that
4
4
  * turn time into eased / springy / oscillating values. They consume the `dtMs`
5
5
  * (or `timeMs`) you already receive from `useFrameLoop`; they do NOT own a loop,
6
- * schedule frames, or touch React so they stay deterministic (same time → same
6
+ * schedule frames, or touch React, so they stay deterministic (same time → same
7
7
  * value) and work identically under a Remotion fixed-frame driver.
8
8
  *
9
9
  * Discipline (mirrors clock.ts): for video-determinism prefer the time-driven
10
10
  * helpers (`oscillate`, `timeline`) that are a pure function of `timeMs`. `spring`
11
- * is stateful (it integrates a velocity) fine for interactive feel, but it will
11
+ * is stateful (it integrates a velocity), fine for interactive feel, but it will
12
12
  * not reproduce frame-for-frame across drivers, so don't use it for rendered video.
13
13
  */
14
14
  /** Snap-to-end gate: honor the user's reduced-motion preference (SSR-safe). */
@@ -30,14 +30,14 @@ const ease = {
30
30
  },
31
31
  quintOut: (t) => 1 - (1 - clamp01(t)) ** 5,
32
32
  sineInOut: (t) => -(Math.cos(Math.PI * clamp01(t)) - 1) / 2,
33
- /** Overshoots then settles good for "pop" on appear. */
33
+ /** Overshoots then settles, good for "pop" on appear. */
34
34
  backOut: (t) => {
35
35
  const c1 = 1.70158;
36
36
  const c3 = 2.70158;
37
37
  t = clamp01(t);
38
38
  return 1 + c3 * (t - 1) ** 3 + c1 * (t - 1) ** 2;
39
39
  },
40
- /** Springy overshoot with decaying wobble for playful reveals. */
40
+ /** Springy overshoot with decaying wobble, for playful reveals. */
41
41
  elasticOut: (t) => {
42
42
  t = clamp01(t);
43
43
  if (t === 0 || t === 1) return t;
@@ -63,7 +63,7 @@ function spring(current, target, vel, dtSec, opts = {}) {
63
63
  vel.current += accel * dt;
64
64
  return current + vel.current * dt;
65
65
  }
66
- /** A pure periodic signal of `timeSec` the engine under AC sources, pendulums, waves. */
66
+ /** A pure periodic signal of `timeSec`, the engine under AC sources, pendulums, waves. */
67
67
  function oscillate(timeSec, opts = {}) {
68
68
  const { amp = 1, freq = 1, phase = 0, shape = "sine" } = opts;
69
69
  const turns = freq * timeSec + phase;
@@ -1,6 +1,6 @@
1
1
  //#region src/core/richText.d.ts
2
2
  /**
3
- * richText the ONE grammar for inline subscript/superscript markup, shared by
3
+ * richText, the ONE grammar for inline subscript/superscript markup, shared by
4
4
  * every renderer so the parsing lives in a single place and can't drift:
5
5
  * • stage's SVG `<Label>` turns spans into <tspan> (baseline-shifted), and
6
6
  * • labs' HTML `<RichText>` turns them into <sub>/<sup>.
@@ -9,7 +9,7 @@
9
9
  * `_x` / `^x` → a run of the SAME character class as the char right
10
10
  * after the marker (all-letters OR all-digits). So
11
11
  * `V_RC` → subscript "RC", `H_2O` → subscript "2" then
12
- * base "O", `v_max` → subscript "max" no braces needed.
12
+ * base "O", `v_max` → subscript "max", no braces needed.
13
13
  * `_{abc}` / `^{ab}` → an explicit group, taken verbatim (use for mixed runs
14
14
  * or symbols, e.g. `10^{-3}`, `x^{n+1}`)
15
15
  * a lone `_` or `^` → stays literal
@@ -3,7 +3,7 @@
3
3
  * 2D vector + affine-matrix algebra. Points are {x,y} objects (serialize cleanly
4
4
  * in the scene JSON). Matrices are SVG-order tuples [a,b,c,d,e,f] mapping
5
5
  * (x,y) → (a·x + c·y + e, b·x + d·y + f), so `mat.toSvg` is a direct
6
- * `matrix(a b c d e f)` attribute string never a CSS variable (a CSS transform
6
+ * `matrix(a b c d e f)` attribute string, never a CSS variable (a CSS transform
7
7
  * is not reliably composed into getScreenCTM()).
8
8
  */
9
9
  interface Vec2 {
@@ -23,6 +23,14 @@ interface Bounds {
23
23
  /** Net field vector at point `p` from all sources (superposition). */
24
24
  declare function fieldAt(sources: readonly FieldSource[], p: Vec2): Vec2;
25
25
  declare function fieldMag(sources: readonly FieldSource[], p: Vec2): number;
26
+ /**
27
+ * Scalar electric potential at `p`: V = Σ k·q / r (superposition). This is the
28
+ * companion to `fieldAt` for potential / equipotential / work labs, where the work
29
+ * to move a charge Q from A to B is W = Q·(V_A − V_B), path-independent. `wire`
30
+ * sources have no scalar potential and are skipped. Same softening as `fieldAt`,
31
+ * so V stays finite at a source.
32
+ */
33
+ declare function potentialAt(sources: readonly FieldSource[], p: Vec2, k?: number): number;
26
34
  interface TraceOpts {
27
35
  /** Follow the field (+1) or run against it (−1). Default +1. */
28
36
  dir?: number;
@@ -41,13 +49,13 @@ interface FieldLinesOpts extends TraceOpts {
41
49
  /** Seed radius around a source. Default 0.2. */
42
50
  seed?: number;
43
51
  }
44
- /** Convenience: a full set of field lines radial from each charge/pole, circular
52
+ /** Convenience: a full set of field lines, radial from each charge/pole, circular
45
53
  * loops around each wire. Each line carries the sign of its originating source. */
46
54
  declare function fieldLines(sources: readonly FieldSource[], opts?: FieldLinesOpts): {
47
55
  points: Vec2[];
48
56
  sign: number;
49
57
  }[];
50
- /** Sample the field on a regular grid for a quiver / arrow plot. */
58
+ /** Sample the field on a regular grid, for a quiver / arrow plot. */
51
59
  declare function fieldGrid(sources: readonly FieldSource[], bounds: Bounds, nx: number, ny: number): {
52
60
  at: Vec2;
53
61
  v: Vec2;
@@ -55,4 +63,4 @@ declare function fieldGrid(sources: readonly FieldSource[], bounds: Bounds, nx:
55
63
  /** Build a bar magnet as an N(+)/S(−) pole pair, centred at `center` along `dir`. */
56
64
  declare function barMagnet(center: Vec2, dir: Vec2, strength?: number, length?: number): [PointCharge, PointCharge];
57
65
  //#endregion
58
- export { Bounds, FieldLinesOpts, FieldSource, LineCurrent, PointCharge, TraceOpts, barMagnet, fieldAt, fieldGrid, fieldLines, fieldMag, traceLine };
66
+ export { Bounds, FieldLinesOpts, FieldSource, LineCurrent, PointCharge, TraceOpts, barMagnet, fieldAt, fieldGrid, fieldLines, fieldMag, potentialAt, traceLine };
@@ -26,6 +26,23 @@ function fieldMag(sources, p) {
26
26
  const v = fieldAt(sources, p);
27
27
  return Math.hypot(v.x, v.y);
28
28
  }
29
+ /**
30
+ * Scalar electric potential at `p`: V = Σ k·q / r (superposition). This is the
31
+ * companion to `fieldAt` for potential / equipotential / work labs, where the work
32
+ * to move a charge Q from A to B is W = Q·(V_A − V_B), path-independent. `wire`
33
+ * sources have no scalar potential and are skipped. Same softening as `fieldAt`,
34
+ * so V stays finite at a source.
35
+ */
36
+ function potentialAt(sources, p, k = 1) {
37
+ let v = 0;
38
+ for (const s of sources) {
39
+ if (s.kind !== "point") continue;
40
+ const dx = p.x - s.at.x, dy = p.y - s.at.y;
41
+ const r = Math.sqrt(dx * dx + dy * dy + SOFT);
42
+ v += k * s.q / r;
43
+ }
44
+ return v;
45
+ }
29
46
  function unit(v, dir) {
30
47
  const m = Math.hypot(v.x, v.y);
31
48
  if (m < 1e-9) return null;
@@ -64,7 +81,7 @@ function traceLine(sources, start, opts = {}) {
64
81
  }
65
82
  return pts;
66
83
  }
67
- /** Convenience: a full set of field lines radial from each charge/pole, circular
84
+ /** Convenience: a full set of field lines, radial from each charge/pole, circular
68
85
  * loops around each wire. Each line carries the sign of its originating source. */
69
86
  function fieldLines(sources, opts = {}) {
70
87
  const { perSource = 12, seed = .2, ...trace } = opts;
@@ -114,7 +131,7 @@ function fieldLines(sources, opts = {}) {
114
131
  }
115
132
  return out;
116
133
  }
117
- /** Sample the field on a regular grid for a quiver / arrow plot. */
134
+ /** Sample the field on a regular grid, for a quiver / arrow plot. */
118
135
  function fieldGrid(sources, bounds, nx, ny) {
119
136
  const out = [];
120
137
  const dx = (bounds.xMax - bounds.xMin) / (nx + 1);
@@ -153,4 +170,4 @@ function barMagnet(center, dir, strength = 1, length = 1.6) {
153
170
  }
154
171
 
155
172
  //#endregion
156
- export { barMagnet, fieldAt, fieldGrid, fieldLines, fieldMag, traceLine };
173
+ export { barMagnet, fieldAt, fieldGrid, fieldLines, fieldMag, potentialAt, traceLine };