@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,73 @@
1
+ //#region src/core/motion.d.ts
2
+ /**
3
+ * Motion core — the "physics of feel" layer. Pure, dependency-free functions that
4
+ * turn time into eased / springy / oscillating values. They consume the `dtMs`
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
7
+ * value) and work identically under a Remotion fixed-frame driver.
8
+ *
9
+ * Discipline (mirrors clock.ts): for video-determinism prefer the time-driven
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
12
+ * not reproduce frame-for-frame across drivers, so don't use it for rendered video.
13
+ */
14
+ /** Snap-to-end gate: honor the user's reduced-motion preference (SSR-safe). */
15
+ declare function prefersReducedMotion(): boolean;
16
+ declare const ease: {
17
+ readonly linear: (t: number) => number;
18
+ readonly quadInOut: (t: number) => number;
19
+ readonly cubicIn: (t: number) => number;
20
+ readonly cubicOut: (t: number) => number;
21
+ readonly cubicInOut: (t: number) => number;
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. */
25
+ readonly elasticOut: (t: number) => number;
26
+ };
27
+ type EaseName = keyof typeof ease;
28
+ type EaseFn = (t: number) => number;
29
+ /** Linear interpolate; the building block under `timeline`. */
30
+ declare function lerp(a: number, b: number, t: number): number;
31
+ interface SpringOpts {
32
+ /** Pull toward target (higher = snappier). Default 170. */
33
+ stiffness?: number;
34
+ /** Velocity drag (higher = less overshoot). Default 26 ≈ critically damped at 170. */
35
+ damping?: number;
36
+ }
37
+ /**
38
+ * Advance one spring step toward `target`, returning the next position and
39
+ * MUTATING `vel.current` (keep one `useRef({current:0})` per animated value).
40
+ * Semi-implicit Euler with a clamped dt so a long frame gap can't explode it.
41
+ * Reduced-motion callers should snap to `target` instead of stepping.
42
+ */
43
+ declare function spring(current: number, target: number, vel: {
44
+ current: number;
45
+ }, dtSec: number, opts?: SpringOpts): number;
46
+ type WaveShape = 'sine' | 'square' | 'triangle' | 'saw';
47
+ interface OscillateOpts {
48
+ /** Peak magnitude. Default 1. */
49
+ amp?: number;
50
+ /** Cycles per second. Default 1. */
51
+ freq?: number;
52
+ /** Phase offset in cycles (0..1). Default 0. */
53
+ phase?: number;
54
+ shape?: WaveShape;
55
+ }
56
+ /** A pure periodic signal of `timeSec` — the engine under AC sources, pendulums, waves. */
57
+ declare function oscillate(timeSec: number, opts?: OscillateOpts): number;
58
+ interface Keyframe {
59
+ /** Absolute time on the timeline, seconds. */
60
+ t: number;
61
+ /** Value at this key. */
62
+ v: number;
63
+ /** Easing into this key from the previous one. Default linear. */
64
+ ease?: EaseFn;
65
+ }
66
+ /**
67
+ * Value of a keyframed track at `timeSec`. Before the first key → its value;
68
+ * after the last → its value; between → eased interpolation. Keys must be sorted
69
+ * by `t`. Use for reveal-on-action choreography (sweep a tangent, fade step 2 in).
70
+ */
71
+ declare function timeline(timeSec: number, keys: Keyframe[]): number;
72
+ //#endregion
73
+ export { EaseFn, EaseName, Keyframe, OscillateOpts, SpringOpts, WaveShape, ease, lerp, oscillate, prefersReducedMotion, spring, timeline };
@@ -0,0 +1,103 @@
1
+ //#region src/core/motion.ts
2
+ /**
3
+ * Motion core — the "physics of feel" layer. Pure, dependency-free functions that
4
+ * turn time into eased / springy / oscillating values. They consume the `dtMs`
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
7
+ * value) and work identically under a Remotion fixed-frame driver.
8
+ *
9
+ * Discipline (mirrors clock.ts): for video-determinism prefer the time-driven
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
12
+ * not reproduce frame-for-frame across drivers, so don't use it for rendered video.
13
+ */
14
+ /** Snap-to-end gate: honor the user's reduced-motion preference (SSR-safe). */
15
+ function prefersReducedMotion() {
16
+ return typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
17
+ }
18
+ const clamp01 = (t) => t < 0 ? 0 : t > 1 ? 1 : t;
19
+ const ease = {
20
+ linear: (t) => clamp01(t),
21
+ quadInOut: (t) => {
22
+ t = clamp01(t);
23
+ return t < .5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
24
+ },
25
+ cubicIn: (t) => clamp01(t) ** 3,
26
+ cubicOut: (t) => 1 - (1 - clamp01(t)) ** 3,
27
+ cubicInOut: (t) => {
28
+ t = clamp01(t);
29
+ return t < .5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2;
30
+ },
31
+ quintOut: (t) => 1 - (1 - clamp01(t)) ** 5,
32
+ sineInOut: (t) => -(Math.cos(Math.PI * clamp01(t)) - 1) / 2,
33
+ /** Overshoots then settles — good for "pop" on appear. */
34
+ backOut: (t) => {
35
+ const c1 = 1.70158;
36
+ const c3 = 2.70158;
37
+ t = clamp01(t);
38
+ return 1 + c3 * (t - 1) ** 3 + c1 * (t - 1) ** 2;
39
+ },
40
+ /** Springy overshoot with decaying wobble — for playful reveals. */
41
+ elasticOut: (t) => {
42
+ t = clamp01(t);
43
+ if (t === 0 || t === 1) return t;
44
+ const p = 2 * Math.PI / 3;
45
+ return 2 ** (-10 * t) * Math.sin((t * 10 - .75) * p) + 1;
46
+ }
47
+ };
48
+ /** Linear interpolate; the building block under `timeline`. */
49
+ function lerp(a, b, t) {
50
+ return a + (b - a) * t;
51
+ }
52
+ /**
53
+ * Advance one spring step toward `target`, returning the next position and
54
+ * MUTATING `vel.current` (keep one `useRef({current:0})` per animated value).
55
+ * Semi-implicit Euler with a clamped dt so a long frame gap can't explode it.
56
+ * Reduced-motion callers should snap to `target` instead of stepping.
57
+ */
58
+ function spring(current, target, vel, dtSec, opts = {}) {
59
+ const stiffness = opts.stiffness ?? 170;
60
+ const damping = opts.damping ?? 26;
61
+ const dt = Math.min(.064, Math.max(0, dtSec));
62
+ const accel = -stiffness * (current - target) - damping * vel.current;
63
+ vel.current += accel * dt;
64
+ return current + vel.current * dt;
65
+ }
66
+ /** A pure periodic signal of `timeSec` — the engine under AC sources, pendulums, waves. */
67
+ function oscillate(timeSec, opts = {}) {
68
+ const { amp = 1, freq = 1, phase = 0, shape = "sine" } = opts;
69
+ const turns = freq * timeSec + phase;
70
+ const frac = turns - Math.floor(turns);
71
+ switch (shape) {
72
+ case "square": return amp * (frac < .5 ? 1 : -1);
73
+ case "triangle": return amp * (4 * Math.abs(frac - .5) - 1) * -1;
74
+ case "saw": return amp * (2 * frac - 1);
75
+ default: return amp * Math.sin(2 * Math.PI * turns);
76
+ }
77
+ }
78
+ /**
79
+ * Value of a keyframed track at `timeSec`. Before the first key → its value;
80
+ * after the last → its value; between → eased interpolation. Keys must be sorted
81
+ * by `t`. Use for reveal-on-action choreography (sweep a tangent, fade step 2 in).
82
+ */
83
+ function timeline(timeSec, keys) {
84
+ const n = keys.length;
85
+ if (n === 0) return 0;
86
+ const first = keys[0];
87
+ if (timeSec <= first.t) return first.v;
88
+ const last = keys[n - 1];
89
+ if (timeSec >= last.t) return last.v;
90
+ for (let i = 1; i < n; i++) {
91
+ const k = keys[i];
92
+ const prev = keys[i - 1];
93
+ if (timeSec < k.t) {
94
+ const span = k.t - prev.t;
95
+ const t = span > 0 ? (timeSec - prev.t) / span : 1;
96
+ return lerp(prev.v, k.v, (k.ease ?? ease.linear)(t));
97
+ }
98
+ }
99
+ return last.v;
100
+ }
101
+
102
+ //#endregion
103
+ export { ease, lerp, oscillate, prefersReducedMotion, spring, timeline };
@@ -0,0 +1,28 @@
1
+ //#region src/core/richText.d.ts
2
+ /**
3
+ * richText — the ONE grammar for inline subscript/superscript markup, shared by
4
+ * every renderer so the parsing lives in a single place and can't drift:
5
+ * • stage's SVG `<Label>` turns spans into <tspan> (baseline-shifted), and
6
+ * • labs' HTML `<RichText>` turns them into <sub>/<sup>.
7
+ *
8
+ * Grammar (LaTeX-ish, the universal science convention):
9
+ * `_x` / `^x` → a run of the SAME character class as the char right
10
+ * after the marker (all-letters OR all-digits). So
11
+ * `V_RC` → subscript "RC", `H_2O` → subscript "2" then
12
+ * base "O", `v_max` → subscript "max" — no braces needed.
13
+ * `_{abc}` / `^{ab}` → an explicit group, taken verbatim (use for mixed runs
14
+ * or symbols, e.g. `10^{-3}`, `x^{n+1}`)
15
+ * a lone `_` or `^` → stays literal
16
+ * Examples: `V_C`, `V_{RC}`, `v_b`, `H_2O`, `x^2`, `V_A (rain)`, `10^{-3}`.
17
+ *
18
+ * Pure + dependency-free; no markers → a single base span (the fast path), so
19
+ * plain labels are untouched.
20
+ */
21
+ type Script = 'base' | 'sub' | 'sup';
22
+ interface RichSpan {
23
+ text: string;
24
+ script: Script;
25
+ }
26
+ declare function parseRichText(text: string): RichSpan[];
27
+ //#endregion
28
+ export { RichSpan, Script, parseRichText };
@@ -0,0 +1,52 @@
1
+ //#region src/core/richText.ts
2
+ const DIGIT = /[0-9]/;
3
+ const LETTER = /[A-Za-z]/;
4
+ function parseRichText(text) {
5
+ if (text.indexOf("_") < 0 && text.indexOf("^") < 0) return [{
6
+ text,
7
+ script: "base"
8
+ }];
9
+ const out = [];
10
+ let buf = "";
11
+ const flush = () => {
12
+ if (buf) {
13
+ out.push({
14
+ text: buf,
15
+ script: "base"
16
+ });
17
+ buf = "";
18
+ }
19
+ };
20
+ for (let i = 0; i < text.length;) {
21
+ const ch = text[i];
22
+ if (ch === "_" || ch === "^") {
23
+ const script = ch === "_" ? "sub" : "sup";
24
+ i++;
25
+ let grp = "";
26
+ if (text[i] === "{") {
27
+ i++;
28
+ while (i < text.length && text[i] !== "}") grp += text[i++];
29
+ if (text[i] === "}") i++;
30
+ } else {
31
+ const first = text[i];
32
+ const cls = first !== void 0 && DIGIT.test(first) ? DIGIT : first !== void 0 && LETTER.test(first) ? LETTER : null;
33
+ if (cls) while (i < text.length && cls.test(text[i])) grp += text[i++];
34
+ }
35
+ if (grp) {
36
+ flush();
37
+ out.push({
38
+ text: grp,
39
+ script
40
+ });
41
+ } else buf += ch;
42
+ } else {
43
+ buf += ch;
44
+ i++;
45
+ }
46
+ }
47
+ flush();
48
+ return out;
49
+ }
50
+
51
+ //#endregion
52
+ export { parseRichText };
@@ -0,0 +1,47 @@
1
+ //#region src/core/vec.d.ts
2
+ /**
3
+ * 2D vector + affine-matrix algebra. Points are {x,y} objects (serialize cleanly
4
+ * in the scene JSON). Matrices are SVG-order tuples [a,b,c,d,e,f] mapping
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
7
+ * is not reliably composed into getScreenCTM()).
8
+ */
9
+ interface Vec2 {
10
+ x: number;
11
+ y: number;
12
+ }
13
+ /** SVG-order affine matrix: [a, b, c, d, e, f]. */
14
+ type Matrix = readonly [number, number, number, number, number, number];
15
+ declare const vec: {
16
+ add: (a: Vec2, b: Vec2) => Vec2;
17
+ sub: (a: Vec2, b: Vec2) => Vec2;
18
+ scale: (a: Vec2, k: number) => Vec2;
19
+ mag: (a: Vec2) => number;
20
+ normalize: (a: Vec2) => Vec2;
21
+ dot: (a: Vec2, b: Vec2) => number;
22
+ dist: (a: Vec2, b: Vec2) => number;
23
+ lerp: (a: Vec2, b: Vec2, t: number) => Vec2;
24
+ midpoint: (a: Vec2, b: Vec2) => Vec2;
25
+ angle: (a: Vec2) => number;
26
+ perp: (a: Vec2) => Vec2;
27
+ rotate: (a: Vec2, rad: number) => Vec2;
28
+ rotateAbout: (a: Vec2, center: Vec2, rad: number) => Vec2;
29
+ };
30
+ declare class MatrixBuilder {
31
+ private m;
32
+ constructor(m?: Matrix);
33
+ translate(tx: number, ty: number): MatrixBuilder;
34
+ scale(s: number, sy?: number): MatrixBuilder;
35
+ rotate(rad: number): MatrixBuilder;
36
+ get(): Matrix;
37
+ }
38
+ declare const mat: {
39
+ identity: Matrix;
40
+ mul: (m: Matrix, n: Matrix) => Matrix;
41
+ invert: (m: Matrix) => Matrix;
42
+ apply: (v: Vec2, m: Matrix) => Vec2;
43
+ toSvg: (m: Matrix) => string;
44
+ builder: (m?: Matrix) => MatrixBuilder;
45
+ };
46
+ //#endregion
47
+ export { Matrix, Vec2, mat, vec };
@@ -0,0 +1,144 @@
1
+ //#region src/core/vec.ts
2
+ const add = (a, b) => ({
3
+ x: a.x + b.x,
4
+ y: a.y + b.y
5
+ });
6
+ const sub = (a, b) => ({
7
+ x: a.x - b.x,
8
+ y: a.y - b.y
9
+ });
10
+ const scale = (a, k) => ({
11
+ x: a.x * k,
12
+ y: a.y * k
13
+ });
14
+ const mag = (a) => Math.hypot(a.x, a.y);
15
+ const normalize = (a) => {
16
+ const m = mag(a) || 1;
17
+ return {
18
+ x: a.x / m,
19
+ y: a.y / m
20
+ };
21
+ };
22
+ const dot = (a, b) => a.x * b.x + a.y * b.y;
23
+ const dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
24
+ const lerp = (a, b, t) => ({
25
+ x: a.x + (b.x - a.x) * t,
26
+ y: a.y + (b.y - a.y) * t
27
+ });
28
+ const midpoint = (a, b) => ({
29
+ x: (a.x + b.x) / 2,
30
+ y: (a.y + b.y) / 2
31
+ });
32
+ const angle = (a) => Math.atan2(a.y, a.x);
33
+ const perp = (a) => ({
34
+ x: -a.y,
35
+ y: a.x
36
+ });
37
+ const rotate = (a, rad) => {
38
+ const c = Math.cos(rad);
39
+ const s = Math.sin(rad);
40
+ return {
41
+ x: a.x * c - a.y * s,
42
+ y: a.x * s + a.y * c
43
+ };
44
+ };
45
+ const rotateAbout = (a, center, rad) => add(center, rotate(sub(a, center), rad));
46
+ const vec = {
47
+ add,
48
+ sub,
49
+ scale,
50
+ mag,
51
+ normalize,
52
+ dot,
53
+ dist,
54
+ lerp,
55
+ midpoint,
56
+ angle,
57
+ perp,
58
+ rotate,
59
+ rotateAbout
60
+ };
61
+ const IDENTITY = [
62
+ 1,
63
+ 0,
64
+ 0,
65
+ 1,
66
+ 0,
67
+ 0
68
+ ];
69
+ const matMul = (m, n) => [
70
+ m[0] * n[0] + m[2] * n[1],
71
+ m[1] * n[0] + m[3] * n[1],
72
+ m[0] * n[2] + m[2] * n[3],
73
+ m[1] * n[2] + m[3] * n[3],
74
+ m[0] * n[4] + m[2] * n[5] + m[4],
75
+ m[1] * n[4] + m[3] * n[5] + m[5]
76
+ ];
77
+ const matInvert = (m) => {
78
+ const id = 1 / (m[0] * m[3] - m[1] * m[2] || 1);
79
+ return [
80
+ m[3] * id,
81
+ -m[1] * id,
82
+ -m[2] * id,
83
+ m[0] * id,
84
+ (m[2] * m[5] - m[3] * m[4]) * id,
85
+ (m[1] * m[4] - m[0] * m[5]) * id
86
+ ];
87
+ };
88
+ const matApply = (v, m) => ({
89
+ x: m[0] * v.x + m[2] * v.y + m[4],
90
+ y: m[1] * v.x + m[3] * v.y + m[5]
91
+ });
92
+ const matToSvg = (m) => `matrix(${m[0]} ${m[1]} ${m[2]} ${m[3]} ${m[4]} ${m[5]})`;
93
+ var MatrixBuilder = class MatrixBuilder {
94
+ m;
95
+ constructor(m = IDENTITY) {
96
+ this.m = m;
97
+ }
98
+ translate(tx, ty) {
99
+ return new MatrixBuilder(matMul(this.m, [
100
+ 1,
101
+ 0,
102
+ 0,
103
+ 1,
104
+ tx,
105
+ ty
106
+ ]));
107
+ }
108
+ scale(s, sy) {
109
+ return new MatrixBuilder(matMul(this.m, [
110
+ s,
111
+ 0,
112
+ 0,
113
+ sy ?? s,
114
+ 0,
115
+ 0
116
+ ]));
117
+ }
118
+ rotate(rad) {
119
+ const c = Math.cos(rad);
120
+ const s = Math.sin(rad);
121
+ return new MatrixBuilder(matMul(this.m, [
122
+ c,
123
+ s,
124
+ -s,
125
+ c,
126
+ 0,
127
+ 0
128
+ ]));
129
+ }
130
+ get() {
131
+ return this.m;
132
+ }
133
+ };
134
+ const mat = {
135
+ identity: IDENTITY,
136
+ mul: matMul,
137
+ invert: matInvert,
138
+ apply: matApply,
139
+ toSvg: matToSvg,
140
+ builder: (m) => new MatrixBuilder(m)
141
+ };
142
+
143
+ //#endregion
144
+ export { mat, vec };
@@ -0,0 +1,58 @@
1
+ import { Vec2 } from "../core/vec.mjs";
2
+
3
+ //#region src/field/index.d.ts
4
+ interface PointCharge {
5
+ kind: 'point';
6
+ at: Vec2;
7
+ /** Signed strength. >0 source / N-pole, <0 sink / S-pole. */
8
+ q: number;
9
+ }
10
+ interface LineCurrent {
11
+ kind: 'wire';
12
+ at: Vec2;
13
+ /** Signed current ⊥ to the plane; sets the circulation handedness. */
14
+ i: number;
15
+ }
16
+ type FieldSource = PointCharge | LineCurrent;
17
+ interface Bounds {
18
+ xMin: number;
19
+ xMax: number;
20
+ yMin: number;
21
+ yMax: number;
22
+ }
23
+ /** Net field vector at point `p` from all sources (superposition). */
24
+ declare function fieldAt(sources: readonly FieldSource[], p: Vec2): Vec2;
25
+ declare function fieldMag(sources: readonly FieldSource[], p: Vec2): number;
26
+ interface TraceOpts {
27
+ /** Follow the field (+1) or run against it (−1). Default +1. */
28
+ dir?: number;
29
+ /** Arc-length step in world units. Default 0.06. */
30
+ step?: number;
31
+ maxSteps?: number;
32
+ bounds?: Bounds;
33
+ /** Stop when this close to any source (a field line terminates at a source). */
34
+ stopRadius?: number;
35
+ }
36
+ /** Trace one field line from `start`, integrating along the unit field with RK4. */
37
+ declare function traceLine(sources: readonly FieldSource[], start: Vec2, opts?: TraceOpts): Vec2[];
38
+ interface FieldLinesOpts extends TraceOpts {
39
+ /** Lines seeded around each source. Default 12. */
40
+ perSource?: number;
41
+ /** Seed radius around a source. Default 0.2. */
42
+ seed?: number;
43
+ }
44
+ /** Convenience: a full set of field lines — radial from each charge/pole, circular
45
+ * loops around each wire. Each line carries the sign of its originating source. */
46
+ declare function fieldLines(sources: readonly FieldSource[], opts?: FieldLinesOpts): {
47
+ points: Vec2[];
48
+ sign: number;
49
+ }[];
50
+ /** Sample the field on a regular grid — for a quiver / arrow plot. */
51
+ declare function fieldGrid(sources: readonly FieldSource[], bounds: Bounds, nx: number, ny: number): {
52
+ at: Vec2;
53
+ v: Vec2;
54
+ }[];
55
+ /** Build a bar magnet as an N(+)/S(−) pole pair, centred at `center` along `dir`. */
56
+ declare function barMagnet(center: Vec2, dir: Vec2, strength?: number, length?: number): [PointCharge, PointCharge];
57
+ //#endregion
58
+ export { Bounds, FieldLinesOpts, FieldSource, LineCurrent, PointCharge, TraceOpts, barMagnet, fieldAt, fieldGrid, fieldLines, fieldMag, traceLine };
@@ -0,0 +1,156 @@
1
+ //#region src/field/index.ts
2
+ const SOFT = .001;
3
+ /** Net field vector at point `p` from all sources (superposition). */
4
+ function fieldAt(sources, p) {
5
+ let fx = 0, fy = 0;
6
+ for (const s of sources) {
7
+ const dx = p.x - s.at.x, dy = p.y - s.at.y;
8
+ const r2 = dx * dx + dy * dy + SOFT;
9
+ const r = Math.sqrt(r2);
10
+ if (s.kind === "point") {
11
+ const c = s.q / r2;
12
+ fx += c * dx / r;
13
+ fy += c * dy / r;
14
+ } else {
15
+ const c = s.i / r2;
16
+ fx += c * -dy;
17
+ fy += c * dx;
18
+ }
19
+ }
20
+ return {
21
+ x: fx,
22
+ y: fy
23
+ };
24
+ }
25
+ function fieldMag(sources, p) {
26
+ const v = fieldAt(sources, p);
27
+ return Math.hypot(v.x, v.y);
28
+ }
29
+ function unit(v, dir) {
30
+ const m = Math.hypot(v.x, v.y);
31
+ if (m < 1e-9) return null;
32
+ return {
33
+ x: dir * v.x / m,
34
+ y: dir * v.y / m
35
+ };
36
+ }
37
+ /** Trace one field line from `start`, integrating along the unit field with RK4. */
38
+ function traceLine(sources, start, opts = {}) {
39
+ const { dir = 1, step = .06, maxSteps = 900, bounds, stopRadius = .16 } = opts;
40
+ const pts = [start];
41
+ let p = start;
42
+ for (let n = 0; n < maxSteps; n++) {
43
+ const k1 = unit(fieldAt(sources, p), dir);
44
+ if (!k1) break;
45
+ const k2 = unit(fieldAt(sources, {
46
+ x: p.x + k1.x * step / 2,
47
+ y: p.y + k1.y * step / 2
48
+ }), dir) ?? k1;
49
+ const k3 = unit(fieldAt(sources, {
50
+ x: p.x + k2.x * step / 2,
51
+ y: p.y + k2.y * step / 2
52
+ }), dir) ?? k2;
53
+ const k4 = unit(fieldAt(sources, {
54
+ x: p.x + k3.x * step,
55
+ y: p.y + k3.y * step
56
+ }), dir) ?? k3;
57
+ p = {
58
+ x: p.x + (k1.x + 2 * k2.x + 2 * k3.x + k4.x) / 6 * step,
59
+ y: p.y + (k1.y + 2 * k2.y + 2 * k3.y + k4.y) / 6 * step
60
+ };
61
+ pts.push(p);
62
+ if (bounds && (p.x < bounds.xMin || p.x > bounds.xMax || p.y < bounds.yMin || p.y > bounds.yMax)) break;
63
+ for (const s of sources) if (Math.hypot(p.x - s.at.x, p.y - s.at.y) < stopRadius) return pts;
64
+ }
65
+ return pts;
66
+ }
67
+ /** Convenience: a full set of field lines — radial from each charge/pole, circular
68
+ * loops around each wire. Each line carries the sign of its originating source. */
69
+ function fieldLines(sources, opts = {}) {
70
+ const { perSource = 12, seed = .2, ...trace } = opts;
71
+ const out = [];
72
+ const hasPositive = sources.some((s) => s.kind === "point" && s.q > 0);
73
+ for (const s of sources) if (s.kind === "point") {
74
+ if (s.q === 0) continue;
75
+ const sign = Math.sign(s.q);
76
+ if (sign < 0 && hasPositive) continue;
77
+ for (let k = 0; k < perSource; k++) {
78
+ const a = k / perSource * Math.PI * 2;
79
+ const startPt = {
80
+ x: s.at.x + Math.cos(a) * seed,
81
+ y: s.at.y + Math.sin(a) * seed
82
+ };
83
+ out.push({
84
+ points: traceLine(sources, startPt, {
85
+ ...trace,
86
+ dir: sign,
87
+ stopRadius: trace.stopRadius ?? seed * .85
88
+ }),
89
+ sign
90
+ });
91
+ }
92
+ } else {
93
+ const sign = Math.sign(s.i) || 1;
94
+ for (const rr of [
95
+ .4,
96
+ .8,
97
+ 1.3,
98
+ 1.9,
99
+ 2.6
100
+ ]) {
101
+ const startPt = {
102
+ x: s.at.x + rr,
103
+ y: s.at.y
104
+ };
105
+ out.push({
106
+ points: traceLine(sources, startPt, {
107
+ ...trace,
108
+ dir: sign,
109
+ stopRadius: 0
110
+ }),
111
+ sign
112
+ });
113
+ }
114
+ }
115
+ return out;
116
+ }
117
+ /** Sample the field on a regular grid — for a quiver / arrow plot. */
118
+ function fieldGrid(sources, bounds, nx, ny) {
119
+ const out = [];
120
+ const dx = (bounds.xMax - bounds.xMin) / (nx + 1);
121
+ const dy = (bounds.yMax - bounds.yMin) / (ny + 1);
122
+ for (let i = 1; i <= nx; i++) for (let j = 1; j <= ny; j++) {
123
+ const at = {
124
+ x: bounds.xMin + i * dx,
125
+ y: bounds.yMin + j * dy
126
+ };
127
+ out.push({
128
+ at,
129
+ v: fieldAt(sources, at)
130
+ });
131
+ }
132
+ return out;
133
+ }
134
+ /** Build a bar magnet as an N(+)/S(−) pole pair, centred at `center` along `dir`. */
135
+ function barMagnet(center, dir, strength = 1, length = 1.6) {
136
+ const m = Math.hypot(dir.x, dir.y) || 1;
137
+ const ux = dir.x / m * (length / 2), uy = dir.y / m * (length / 2);
138
+ return [{
139
+ kind: "point",
140
+ at: {
141
+ x: center.x + ux,
142
+ y: center.y + uy
143
+ },
144
+ q: strength
145
+ }, {
146
+ kind: "point",
147
+ at: {
148
+ x: center.x - ux,
149
+ y: center.y - uy
150
+ },
151
+ q: -strength
152
+ }];
153
+ }
154
+
155
+ //#endregion
156
+ export { barMagnet, fieldAt, fieldGrid, fieldLines, fieldMag, traceLine };