@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.
- package/LICENSE +21 -0
- package/README.md +65 -0
- package/dist/assets/index.d.mts +3 -0
- package/dist/assets/index.mjs +5 -0
- package/dist/assets/kit/glyphs.d.mts +61 -0
- package/dist/assets/kit/glyphs.mjs +256 -0
- package/dist/assets/kit/index.d.mts +2 -0
- package/dist/assets/kit/index.mjs +4 -0
- package/dist/assets/kit/svg-defs.d.mts +6 -0
- package/dist/assets/kit/svg-defs.mjs +137 -0
- package/dist/builder/Palette.d.mts +30 -0
- package/dist/builder/Palette.mjs +100 -0
- package/dist/builder/SceneBuilder.d.mts +26 -0
- package/dist/builder/SceneBuilder.mjs +340 -0
- package/dist/builder/controls.d.mts +18 -0
- package/dist/builder/controls.mjs +29 -0
- package/dist/builder/editor.d.mts +46 -0
- package/dist/builder/editor.mjs +130 -0
- package/dist/builder/icons.d.mts +14 -0
- package/dist/builder/icons.mjs +143 -0
- package/dist/builder/index.d.mts +6 -0
- package/dist/builder/index.mjs +7 -0
- package/dist/builder/tools.d.mts +47 -0
- package/dist/builder/tools.mjs +303 -0
- package/dist/chem/index.d.mts +118 -0
- package/dist/chem/index.mjs +131 -0
- package/dist/core/clock.d.mts +41 -0
- package/dist/core/clock.mjs +62 -0
- package/dist/core/context.d.mts +13 -0
- package/dist/core/context.mjs +29 -0
- package/dist/core/control.d.mts +58 -0
- package/dist/core/control.mjs +113 -0
- package/dist/core/coords.d.mts +51 -0
- package/dist/core/coords.mjs +44 -0
- package/dist/core/index.d.mts +8 -0
- package/dist/core/index.mjs +9 -0
- package/dist/core/learner.d.mts +44 -0
- package/dist/core/learner.mjs +23 -0
- package/dist/core/motion.d.mts +73 -0
- package/dist/core/motion.mjs +103 -0
- package/dist/core/richText.d.mts +28 -0
- package/dist/core/richText.mjs +52 -0
- package/dist/core/vec.d.mts +47 -0
- package/dist/core/vec.mjs +144 -0
- package/dist/field/index.d.mts +58 -0
- package/dist/field/index.mjs +156 -0
- package/dist/index.d.mts +54 -0
- package/dist/index.mjs +55 -0
- package/dist/interaction/MovableDot.d.mts +33 -0
- package/dist/interaction/MovableDot.mjs +66 -0
- package/dist/interaction/hitTest.d.mts +16 -0
- package/dist/interaction/hitTest.mjs +39 -0
- package/dist/interaction/index.d.mts +5 -0
- package/dist/interaction/index.mjs +6 -0
- package/dist/interaction/useDraggable.d.mts +51 -0
- package/dist/interaction/useDraggable.mjs +116 -0
- package/dist/interaction/useScreenToMath.d.mts +12 -0
- package/dist/interaction/useScreenToMath.mjs +25 -0
- package/dist/logic/ast.d.mts +29 -0
- package/dist/logic/ast.mjs +37 -0
- package/dist/logic/index.d.mts +25 -0
- package/dist/logic/index.mjs +36 -0
- package/dist/logic/latex.d.mts +6 -0
- package/dist/logic/latex.mjs +33 -0
- package/dist/logic/minimize.d.mts +37 -0
- package/dist/logic/minimize.mjs +175 -0
- package/dist/logic/parse.d.mts +6 -0
- package/dist/logic/parse.mjs +68 -0
- package/dist/logic/table.d.mts +24 -0
- package/dist/logic/table.mjs +65 -0
- package/dist/logic/tokenize.mjs +258 -0
- package/dist/math/ast.d.mts +35 -0
- package/dist/math/ast.mjs +94 -0
- package/dist/math/calculus.d.mts +9 -0
- package/dist/math/calculus.mjs +188 -0
- package/dist/math/defs.mjs +51 -0
- package/dist/math/index.d.mts +26 -0
- package/dist/math/index.mjs +42 -0
- package/dist/math/latex.d.mts +7 -0
- package/dist/math/latex.mjs +115 -0
- package/dist/math/parse.d.mts +6 -0
- package/dist/math/parse.mjs +91 -0
- package/dist/math/tokenize.mjs +112 -0
- package/dist/primitives/CanvasLayer.d.mts +26 -0
- package/dist/primitives/CanvasLayer.mjs +87 -0
- package/dist/primitives/Dot.d.mts +27 -0
- package/dist/primitives/Dot.mjs +40 -0
- package/dist/primitives/Grid.d.mts +30 -0
- package/dist/primitives/Grid.mjs +122 -0
- package/dist/primitives/Label.d.mts +33 -0
- package/dist/primitives/Label.mjs +45 -0
- package/dist/primitives/Lines.d.mts +50 -0
- package/dist/primitives/Lines.mjs +82 -0
- package/dist/primitives/Plot.d.mts +50 -0
- package/dist/primitives/Plot.mjs +87 -0
- package/dist/primitives/Shapes.d.mts +76 -0
- package/dist/primitives/Shapes.mjs +79 -0
- package/dist/primitives/Tex.d.mts +27 -0
- package/dist/primitives/Tex.mjs +54 -0
- package/dist/primitives/index.d.mts +10 -0
- package/dist/primitives/index.mjs +10 -0
- package/dist/primitives/props.d.mts +15 -0
- package/dist/scene/Scene.d.mts +31 -0
- package/dist/scene/Scene.mjs +168 -0
- package/dist/scene/assets.d.mts +37 -0
- package/dist/scene/assets.mjs +31 -0
- package/dist/scene/commands.d.mts +47 -0
- package/dist/scene/commands.mjs +162 -0
- package/dist/scene/evaluators.d.mts +17 -0
- package/dist/scene/evaluators.mjs +210 -0
- package/dist/scene/index.d.mts +13 -0
- package/dist/scene/index.mjs +13 -0
- package/dist/scene/migrate.d.mts +11 -0
- package/dist/scene/migrate.mjs +35 -0
- package/dist/scene/render.d.mts +14 -0
- package/dist/scene/render.mjs +126 -0
- package/dist/scene/resolve.d.mts +12 -0
- package/dist/scene/resolve.mjs +95 -0
- package/dist/scene/schema.d.mts +9 -0
- package/dist/scene/schema.mjs +51 -0
- package/dist/scene/sims.d.mts +18 -0
- package/dist/scene/sims.mjs +54 -0
- package/dist/scene/store.d.mts +22 -0
- package/dist/scene/store.mjs +37 -0
- package/dist/scene/types.d.mts +228 -0
- package/dist/scene/types.mjs +27 -0
- package/dist/sim/equilibrium.d.mts +36 -0
- package/dist/sim/equilibrium.mjs +54 -0
- package/dist/sim/index.d.mts +9 -0
- package/dist/sim/index.mjs +9 -0
- package/dist/sim/particles.d.mts +43 -0
- package/dist/sim/particles.mjs +136 -0
- package/dist/sim/rate.d.mts +36 -0
- package/dist/sim/rate.mjs +39 -0
- package/dist/sim/registry.d.mts +10 -0
- package/dist/sim/registry.mjs +27 -0
- package/dist/sim/sampler.d.mts +47 -0
- package/dist/sim/sampler.mjs +79 -0
- package/dist/sim/thermal.d.mts +78 -0
- package/dist/sim/thermal.mjs +172 -0
- package/dist/sim/types.d.mts +24 -0
- package/dist/sim/wave.d.mts +34 -0
- package/dist/sim/wave.mjs +61 -0
- package/dist/steps/index.d.mts +62 -0
- package/dist/steps/index.mjs +133 -0
- package/dist/thermo/index.d.mts +82 -0
- package/dist/thermo/index.mjs +133 -0
- package/dist/view/Stage.d.mts +39 -0
- package/dist/view/Stage.mjs +91 -0
- package/dist/view/index.d.mts +4 -0
- package/dist/view/index.mjs +5 -0
- package/dist/view/useElementSize.d.mts +13 -0
- package/dist/view/useElementSize.mjs +40 -0
- package/dist/view/useInView.d.mts +9 -0
- package/dist/view/useInView.mjs +33 -0
- package/package.json +141 -0
- 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 };
|