@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,43 @@
1
+ import { SimCore } from "./types.mjs";
2
+
3
+ //#region src/sim/particles.d.ts
4
+ interface Box {
5
+ w: number;
6
+ h: number;
7
+ }
8
+ interface ParticlesParams {
9
+ /** Particle count. Default 80. */
10
+ n?: number;
11
+ box?: Box;
12
+ /** Initial speed (∝ √temperature). Default 2. */
13
+ speed?: number;
14
+ /** Particle radius (for collisions + drawing). Default 0.14. */
15
+ radius?: number;
16
+ /** Elastic pairwise collisions (thermalises the speed distribution). Default true. */
17
+ collide?: boolean;
18
+ /** Start group 0 in the LEFT half, group 1 in the RIGHT half (diffusion). Default false = mixed. */
19
+ split?: boolean;
20
+ seed?: number;
21
+ }
22
+ interface ParticlesState {
23
+ n: number;
24
+ box: Box;
25
+ radius: number;
26
+ collide: boolean;
27
+ px: number[];
28
+ py: number[];
29
+ vx: number[];
30
+ vy: number[];
31
+ /** 0 or 1 — two populations (for diffusion / colour). */
32
+ group: number[];
33
+ tSec: number;
34
+ /** Mean kinetic energy ∝ temperature. */
35
+ temp: number;
36
+ /** Smoothed pressure (wall-collision momentum per unit time per unit perimeter). */
37
+ pressure: number;
38
+ /** Mixing index 0 (separated) → 1 (uniform) — fraction-based, for diffusion. */
39
+ mixed: number;
40
+ }
41
+ declare const ParticlesCore: SimCore<ParticlesState, ParticlesParams>;
42
+ //#endregion
43
+ export { Box, ParticlesCore, ParticlesParams, ParticlesState };
@@ -0,0 +1,136 @@
1
+ //#region src/sim/particles.ts
2
+ function rng(a) {
3
+ a = a + 1831565813 | 0;
4
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
5
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
6
+ return {
7
+ r: ((t ^ t >>> 14) >>> 0) / 4294967296,
8
+ a
9
+ };
10
+ }
11
+ /** Mixing: group-0's share of the RIGHT half, scaled so 0=separated, 1=uniform. */
12
+ function mixIndex(s) {
13
+ let g0 = 0, g0right = 0;
14
+ for (let i = 0; i < s.n; i++) if (s.group[i] === 0) {
15
+ g0++;
16
+ if (s.px[i] > s.box.w / 2) g0right++;
17
+ }
18
+ if (g0 === 0) return 1;
19
+ return Math.min(1, g0right / g0 * 2);
20
+ }
21
+ const ParticlesCore = {
22
+ reset(params = {}) {
23
+ const n = Math.max(1, Math.min(400, Math.round(params.n ?? 80)));
24
+ const box = params.box ?? {
25
+ w: 10,
26
+ h: 6
27
+ };
28
+ const radius = params.radius ?? .14;
29
+ const collide = params.collide ?? true;
30
+ const speed = params.speed ?? 2;
31
+ const split = params.split ?? false;
32
+ let a = (params.seed ?? 1) | 0;
33
+ const px = [], py = [], vx = [], vy = [], group = [];
34
+ for (let i = 0; i < n; i++) {
35
+ const g = i < n / 2 ? 0 : 1;
36
+ let rx = rng(a);
37
+ a = rx.a;
38
+ let ry = rng(a);
39
+ a = ry.a;
40
+ const ra = rng(a);
41
+ a = ra.a;
42
+ const xLo = split ? g === 0 ? radius : box.w / 2 + radius : radius;
43
+ const xHi = split ? g === 0 ? box.w / 2 - radius : box.w - radius : box.w - radius;
44
+ px.push(xLo + rx.r * (xHi - xLo));
45
+ py.push(radius + ry.r * (box.h - 2 * radius));
46
+ const ang = ra.r * Math.PI * 2;
47
+ vx.push(Math.cos(ang) * speed);
48
+ vy.push(Math.sin(ang) * speed);
49
+ group.push(g);
50
+ }
51
+ const base = {
52
+ n,
53
+ box,
54
+ radius,
55
+ collide,
56
+ px,
57
+ py,
58
+ vx,
59
+ vy,
60
+ group,
61
+ tSec: 0,
62
+ temp: .5 * speed * speed,
63
+ pressure: 0
64
+ };
65
+ return {
66
+ ...base,
67
+ mixed: mixIndex(base)
68
+ };
69
+ },
70
+ step(state, dtSec) {
71
+ const dt = Math.min(.04, Math.max(0, dtSec));
72
+ const { n, box, radius, px, py, vx, vy } = state;
73
+ let impulse = 0;
74
+ for (let i = 0; i < n; i++) {
75
+ px[i] += vx[i] * dt;
76
+ py[i] += vy[i] * dt;
77
+ if (px[i] < radius) {
78
+ px[i] = radius;
79
+ vx[i] = -vx[i];
80
+ impulse += 2 * Math.abs(vx[i]);
81
+ } else if (px[i] > box.w - radius) {
82
+ px[i] = box.w - radius;
83
+ vx[i] = -vx[i];
84
+ impulse += 2 * Math.abs(vx[i]);
85
+ }
86
+ if (py[i] < radius) {
87
+ py[i] = radius;
88
+ vy[i] = -vy[i];
89
+ impulse += 2 * Math.abs(vy[i]);
90
+ } else if (py[i] > box.h - radius) {
91
+ py[i] = box.h - radius;
92
+ vy[i] = -vy[i];
93
+ impulse += 2 * Math.abs(vy[i]);
94
+ }
95
+ }
96
+ if (state.collide) {
97
+ const d2 = 2 * radius * (2 * radius);
98
+ for (let i = 0; i < n; i++) for (let j = i + 1; j < n; j++) {
99
+ const dx = px[j] - px[i], dy = py[j] - py[i];
100
+ const dist2 = dx * dx + dy * dy;
101
+ if (dist2 > 1e-9 && dist2 < d2) {
102
+ const dist = Math.sqrt(dist2);
103
+ const nx = dx / dist, ny = dy / dist;
104
+ const dvn = (vx[j] - vx[i]) * nx + (vy[j] - vy[i]) * ny;
105
+ if (dvn < 0) {
106
+ vx[i] += dvn * nx;
107
+ vy[i] += dvn * ny;
108
+ vx[j] -= dvn * nx;
109
+ vy[j] -= dvn * ny;
110
+ }
111
+ const overlap = (2 * radius - dist) / 2;
112
+ px[i] -= nx * overlap;
113
+ py[i] -= ny * overlap;
114
+ px[j] += nx * overlap;
115
+ py[j] += ny * overlap;
116
+ }
117
+ }
118
+ }
119
+ let ke = 0;
120
+ for (let i = 0; i < n; i++) ke += vx[i] * vx[i] + vy[i] * vy[i];
121
+ const temp = .5 * ke / n;
122
+ const perim = 2 * (box.w + box.h);
123
+ const instP = dt > 0 ? impulse / (dt * perim) : 0;
124
+ const pressure = state.pressure * .9 + instP * .1;
125
+ return {
126
+ ...state,
127
+ tSec: state.tSec + dt,
128
+ temp,
129
+ pressure,
130
+ mixed: mixIndex(state)
131
+ };
132
+ }
133
+ };
134
+
135
+ //#endregion
136
+ export { ParticlesCore };
@@ -0,0 +1,36 @@
1
+ import { SimCore } from "./types.mjs";
2
+
3
+ //#region src/sim/rate.d.ts
4
+ interface RateParams {
5
+ /** Initial value. Named `value0`, NOT `value`: the runtime merges params onto
6
+ * state each frame, so an evolving field sharing a param name would be reset
7
+ * every step (the `value` would never relax). Keep them disjoint. */
8
+ value0?: number;
9
+ /** Asymptote the value relaxes toward. Default 0 (pure decay). */
10
+ target?: number;
11
+ /** Time constant τ (seconds) — the e-folding time. Default 1. */
12
+ tau?: number;
13
+ /** Stop after this many seconds (essentially relaxed) so it doesn't run forever. Default ∞. */
14
+ maxTime?: number;
15
+ /** Trace ring-buffer capacity. Default 240. */
16
+ trace?: number;
17
+ }
18
+ interface RateState {
19
+ value: number;
20
+ target: number;
21
+ tau: number;
22
+ /** Seconds elapsed on this core's timeline. */
23
+ tSec: number;
24
+ /** Rolling history of `value`, oldest-first — for the curve. */
25
+ samples: number[];
26
+ cap: number;
27
+ maxTime: number;
28
+ /** True once tSec ≥ maxTime — the runtime stops the loop. */
29
+ done: boolean;
30
+ }
31
+ declare const RateCore: SimCore<RateState, RateParams>;
32
+ /** τ from a half-life (and back) — t½ = τ·ln2. */
33
+ declare const halfLifeToTau: (halfLife: number) => number;
34
+ declare const tauToHalfLife: (tau: number) => number;
35
+ //#endregion
36
+ export { RateCore, RateParams, RateState, halfLifeToTau, tauToHalfLife };
@@ -0,0 +1,39 @@
1
+ //#region src/sim/rate.ts
2
+ const RateCore = {
3
+ reset(params = {}) {
4
+ const value = params.value0 ?? 1;
5
+ return {
6
+ value,
7
+ target: params.target ?? 0,
8
+ tau: Math.max(.001, params.tau ?? 1),
9
+ tSec: 0,
10
+ samples: [value],
11
+ cap: Math.max(2, Math.round(params.trace ?? 240)),
12
+ maxTime: params.maxTime ?? Infinity,
13
+ done: false
14
+ };
15
+ },
16
+ step(state, dtSec) {
17
+ if (state.tSec >= state.maxTime) return state.done ? state : {
18
+ ...state,
19
+ done: true
20
+ };
21
+ const dt = Math.max(0, dtSec);
22
+ const value = state.target + (state.value - state.target) * Math.exp(-dt / state.tau);
23
+ const tSec = state.tSec + dt;
24
+ const samples = state.samples.length >= state.cap ? [...state.samples.slice(state.samples.length - state.cap + 1), value] : [...state.samples, value];
25
+ return {
26
+ ...state,
27
+ value,
28
+ tSec,
29
+ samples,
30
+ done: tSec >= state.maxTime
31
+ };
32
+ }
33
+ };
34
+ /** τ from a half-life (and back) — t½ = τ·ln2. */
35
+ const halfLifeToTau = (halfLife) => halfLife / Math.LN2;
36
+ const tauToHalfLife = (tau) => tau * Math.LN2;
37
+
38
+ //#endregion
39
+ export { RateCore, halfLifeToTau, tauToHalfLife };
@@ -0,0 +1,10 @@
1
+ import { SimCore } from "./types.mjs";
2
+
3
+ //#region src/sim/registry.d.ts
4
+ /** A core with its specifics erased — the runtime reads state fields by name. */
5
+ type AnySim = SimCore<Record<string, unknown>, Record<string, unknown>>;
6
+ declare function registerSim<S extends Record<string, unknown>, P>(name: string, core: SimCore<S, P>): void;
7
+ declare function getSim(name: string): AnySim | undefined;
8
+ declare function listSims(): string[];
9
+ //#endregion
10
+ export { getSim, listSims, registerSim };
@@ -0,0 +1,27 @@
1
+ import { WaveCore } from "./wave.mjs";
2
+ import { SamplerCore } from "./sampler.mjs";
3
+ import { RateCore } from "./rate.mjs";
4
+ import { ParticlesCore } from "./particles.mjs";
5
+ import { ThermalCore } from "./thermal.mjs";
6
+ import { EquilibriumCore } from "./equilibrium.mjs";
7
+
8
+ //#region src/sim/registry.ts
9
+ const SIMS = /* @__PURE__ */ new Map();
10
+ SIMS.set("wave", WaveCore);
11
+ SIMS.set("sampler", SamplerCore);
12
+ SIMS.set("rate", RateCore);
13
+ SIMS.set("particles", ParticlesCore);
14
+ SIMS.set("thermal", ThermalCore);
15
+ SIMS.set("equilibrium", EquilibriumCore);
16
+ function registerSim(name, core) {
17
+ SIMS.set(name, core);
18
+ }
19
+ function getSim(name) {
20
+ return SIMS.get(name);
21
+ }
22
+ function listSims() {
23
+ return [...SIMS.keys()];
24
+ }
25
+
26
+ //#endregion
27
+ export { getSim, listSims, registerSim };
@@ -0,0 +1,47 @@
1
+ import { SimCore } from "./types.mjs";
2
+
3
+ //#region src/sim/sampler.d.ts
4
+ interface SamplerParams {
5
+ /** Relative weights per outcome. Default [1, 1] (a fair coin). */
6
+ weights?: number[];
7
+ /** Draws per step. Default 1 (raise to converge faster). */
8
+ perStep?: number;
9
+ /** PRNG seed. Default 1. */
10
+ seed?: number;
11
+ /** Stop drawing once `n` reaches this — a converged sim shouldn't run forever. Default ∞. */
12
+ maxDraws?: number;
13
+ /** History ring-buffer capacity for the headline estimate p[0]. Default 240. */
14
+ trace?: number;
15
+ }
16
+ interface SamplerState {
17
+ weights: number[];
18
+ perStep: number;
19
+ /**
20
+ * Current PRNG state (advances every draw). Deliberately NOT named `seed`: the
21
+ * Scene runtime merges the SimDecl `params` (which carries the initial `seed`)
22
+ * onto the state each frame, so an evolving field sharing a param name would be
23
+ * clobbered back to its initial value — freezing the RNG. Keep evolving state
24
+ * fields disjoint from param names.
25
+ */
26
+ rng: number;
27
+ /** Total draws so far. */
28
+ n: number;
29
+ /** Per-outcome counts. */
30
+ counts: number[];
31
+ /** Running frequency estimate per outcome (counts / n). */
32
+ p: number[];
33
+ /** True (normalized) probabilities — for convergence comparison. */
34
+ p0: number[];
35
+ /** Index of the most recent draw (-1 before the first). */
36
+ last: number;
37
+ /** Rolling history of p[0], oldest-first — for an LLN convergence plot. */
38
+ samples: number[];
39
+ cap: number;
40
+ /** Draw cap; step() becomes a no-op once n reaches it. */
41
+ maxDraws: number;
42
+ /** True once n ≥ maxDraws — the runtime stops the loop. */
43
+ done: boolean;
44
+ }
45
+ declare const SamplerCore: SimCore<SamplerState, SamplerParams>;
46
+ //#endregion
47
+ export { SamplerCore, SamplerParams, SamplerState };
@@ -0,0 +1,79 @@
1
+ //#region src/sim/sampler.ts
2
+ /** Pure mulberry32 step: returns a uniform r∈[0,1) and the next PRNG state. */
3
+ function rng(a) {
4
+ a = a + 1831565813 | 0;
5
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
6
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
7
+ return {
8
+ r: ((t ^ t >>> 14) >>> 0) / 4294967296,
9
+ a
10
+ };
11
+ }
12
+ function pick(weights, total, r) {
13
+ let x = r * total;
14
+ for (let i = 0; i < weights.length - 1; i++) {
15
+ const w = weights[i] ?? 0;
16
+ if (x < w) return i;
17
+ x -= w;
18
+ }
19
+ return weights.length - 1;
20
+ }
21
+ function normalize(weights) {
22
+ const total = weights.reduce((a, b) => a + b, 0) || 1;
23
+ return weights.map((w) => w / total);
24
+ }
25
+ const SamplerCore = {
26
+ reset(params = {}) {
27
+ const weights = params.weights && params.weights.length >= 2 ? params.weights.slice() : [1, 1];
28
+ const k = weights.length;
29
+ return {
30
+ weights,
31
+ perStep: Math.max(1, Math.round(params.perStep ?? 1)),
32
+ rng: (params.seed ?? 1) | 0,
33
+ n: 0,
34
+ counts: new Array(k).fill(0),
35
+ p: new Array(k).fill(0),
36
+ p0: normalize(weights),
37
+ last: -1,
38
+ samples: [0],
39
+ cap: Math.max(2, Math.round(params.trace ?? 240)),
40
+ maxDraws: params.maxDraws ?? Infinity,
41
+ done: false
42
+ };
43
+ },
44
+ step(state) {
45
+ if (state.n >= state.maxDraws) return state.done ? state : {
46
+ ...state,
47
+ done: true
48
+ };
49
+ state.weights.length;
50
+ const total = state.weights.reduce((a, b) => a + b, 0) || 1;
51
+ const counts = state.counts.slice();
52
+ let a = state.rng;
53
+ let last = state.last;
54
+ for (let d = 0; d < state.perStep; d++) {
55
+ const next = rng(a);
56
+ a = next.a;
57
+ last = pick(state.weights, total, next.r);
58
+ counts[last] = (counts[last] ?? 0) + 1;
59
+ }
60
+ const n = state.n + state.perStep;
61
+ const p = counts.map((c) => c / n);
62
+ const head = p[0] ?? 0;
63
+ const samples = state.samples.length >= state.cap ? [...state.samples.slice(state.samples.length - state.cap + 1), head] : [...state.samples, head];
64
+ return {
65
+ ...state,
66
+ rng: a,
67
+ n,
68
+ counts,
69
+ p,
70
+ p0: normalize(state.weights),
71
+ last,
72
+ samples,
73
+ done: n >= state.maxDraws
74
+ };
75
+ }
76
+ };
77
+
78
+ //#endregion
79
+ export { SamplerCore };
@@ -0,0 +1,78 @@
1
+ import { SimCore } from "./types.mjs";
2
+
3
+ //#region src/sim/thermal.d.ts
4
+ /** A material: how much heat each gram needs to warm 1 °C in each phase, the
5
+ * latent heats of its two transitions, and where those transitions sit. */
6
+ interface Substance {
7
+ name: string;
8
+ solidName: string;
9
+ liquidName: string;
10
+ gasName: string;
11
+ /** Specific heat capacities, J/(g·°C), one per phase. */
12
+ cSolid: number;
13
+ cLiquid: number;
14
+ cGas: number;
15
+ /** Latent heat of fusion (melting) and vaporisation (boiling), J/g. */
16
+ lFusion: number;
17
+ lVapor: number;
18
+ /** Transition temperatures, °C. */
19
+ tMelt: number;
20
+ tBoil: number;
21
+ /** Liquid colour token (for the skin). */
22
+ color: string;
23
+ }
24
+ /** Real water — the canonical example (note: latent vaporisation ≫ everything,
25
+ * so the boiling plateau dwarfs the rest, which is exactly the lesson). */
26
+ declare const WATER: Substance;
27
+ /** Ethanol — lower boiling point, smaller latent heats; a contrasting curve. */
28
+ declare const ETHANOL: Substance;
29
+ type Phase = 'solid' | 'melting' | 'liquid' | 'boiling' | 'gas';
30
+ /** One run of the energy ladder. `kind:'heat'` is a sloped temperature rise
31
+ * (q = m·c·Δθ); `kind:'change'` is a flat phase transition (q = m·L). */
32
+ interface ThermalSeg {
33
+ phase: Phase;
34
+ kind: 'heat' | 'change';
35
+ which: 'melt' | 'boil' | null;
36
+ /** Heat this segment absorbs, J. */
37
+ q: number;
38
+ /** Cumulative heat at the START of this segment, J (x-position on the curve). */
39
+ qStart: number;
40
+ /** Temperature at the start / end of the segment, °C (equal for a change). */
41
+ t0: number;
42
+ t1: number;
43
+ }
44
+ interface ThermalParams {
45
+ substance?: Substance;
46
+ /** Mass, grams. Default 50. */
47
+ mass?: number;
48
+ /** Heating power, J/s. Negative cools. Default 100. */
49
+ powerW?: number;
50
+ /** Starting temperature, °C. Default −20 (cold ice). */
51
+ tStart?: number;
52
+ /** Top of the curve / highest temperature shown, °C. Default tBoil + 40. */
53
+ tMax?: number;
54
+ }
55
+ interface ThermalState {
56
+ sub: Substance;
57
+ mass: number;
58
+ powerW: number;
59
+ tStart: number;
60
+ tMax: number;
61
+ /** The full energy ladder — the heating curve, as data. */
62
+ segs: ThermalSeg[];
63
+ /** Total heat to walk the whole ladder, J. */
64
+ totalJ: number;
65
+ energyJ: number;
66
+ tempC: number;
67
+ phase: Phase;
68
+ /** Completion of the melting / boiling transitions, 0..1. */
69
+ fracMelt: number;
70
+ fracBoil: number;
71
+ tSec: number;
72
+ done: boolean;
73
+ }
74
+ declare const ThermalCore: SimCore<ThermalState, ThermalParams>;
75
+ /** Human label for a segment ("warming ice", "melting", "boiling"…). */
76
+ declare function segLabel(s: ThermalSeg, sub: Substance): string;
77
+ //#endregion
78
+ export { ETHANOL, Phase, Substance, ThermalCore, ThermalParams, ThermalSeg, ThermalState, WATER, segLabel };
@@ -0,0 +1,172 @@
1
+ //#region src/sim/thermal.ts
2
+ const clamp01 = (v) => Math.max(0, Math.min(1, v));
3
+ /** Real water — the canonical example (note: latent vaporisation ≫ everything,
4
+ * so the boiling plateau dwarfs the rest, which is exactly the lesson). */
5
+ const WATER = {
6
+ name: "Water",
7
+ solidName: "ice",
8
+ liquidName: "water",
9
+ gasName: "steam",
10
+ cSolid: 2.09,
11
+ cLiquid: 4.18,
12
+ cGas: 2.01,
13
+ lFusion: 334,
14
+ lVapor: 2260,
15
+ tMelt: 0,
16
+ tBoil: 100,
17
+ color: "var(--stage-accent, #3b82f6)"
18
+ };
19
+ /** Ethanol — lower boiling point, smaller latent heats; a contrasting curve. */
20
+ const ETHANOL = {
21
+ name: "Ethanol",
22
+ solidName: "solid",
23
+ liquidName: "ethanol",
24
+ gasName: "vapour",
25
+ cSolid: 1.2,
26
+ cLiquid: 2.44,
27
+ cGas: 1.42,
28
+ lFusion: 108,
29
+ lVapor: 855,
30
+ tMelt: -114,
31
+ tBoil: 78,
32
+ color: "var(--stage-accent-2, #8b5cf6)"
33
+ };
34
+ /** Build the energy ladder from `tStart` up to `tMax`, including only the phases
35
+ * reachable heating upward (a liquid start skips the solid+melting runs). */
36
+ function buildSegs(sub, mass, tStart, tMax) {
37
+ const segs = [];
38
+ let cum = 0;
39
+ const push = (s) => {
40
+ segs.push({
41
+ ...s,
42
+ qStart: cum
43
+ });
44
+ cum += Math.max(0, s.q);
45
+ };
46
+ let t = tStart;
47
+ if (t < sub.tMelt) {
48
+ push({
49
+ phase: "solid",
50
+ kind: "heat",
51
+ which: null,
52
+ q: mass * sub.cSolid * (sub.tMelt - t),
53
+ t0: t,
54
+ t1: sub.tMelt
55
+ });
56
+ push({
57
+ phase: "melting",
58
+ kind: "change",
59
+ which: "melt",
60
+ q: mass * sub.lFusion,
61
+ t0: sub.tMelt,
62
+ t1: sub.tMelt
63
+ });
64
+ t = sub.tMelt;
65
+ }
66
+ if (t < sub.tBoil) {
67
+ push({
68
+ phase: "liquid",
69
+ kind: "heat",
70
+ which: null,
71
+ q: mass * sub.cLiquid * (sub.tBoil - t),
72
+ t0: t,
73
+ t1: sub.tBoil
74
+ });
75
+ push({
76
+ phase: "boiling",
77
+ kind: "change",
78
+ which: "boil",
79
+ q: mass * sub.lVapor,
80
+ t0: sub.tBoil,
81
+ t1: sub.tBoil
82
+ });
83
+ t = sub.tBoil;
84
+ }
85
+ push({
86
+ phase: "gas",
87
+ kind: "heat",
88
+ which: null,
89
+ q: Math.max(0, mass * sub.cGas * (tMax - t)),
90
+ t0: t,
91
+ t1: tMax
92
+ });
93
+ return segs;
94
+ }
95
+ /** Temperature / phase / transition-fractions at a given heat-added (pure). */
96
+ function derive(segs, totalJ, energyRaw) {
97
+ const e = Math.max(0, Math.min(totalJ, energyRaw));
98
+ let fracMelt = 0, fracBoil = 0;
99
+ for (const s of segs) {
100
+ if (s.which === "melt") fracMelt = s.q > 0 ? clamp01((e - s.qStart) / s.q) : 1;
101
+ if (s.which === "boil") fracBoil = s.q > 0 ? clamp01((e - s.qStart) / s.q) : 1;
102
+ }
103
+ let tempC = segs[0].t0, phase = segs[0].phase;
104
+ for (let i = 0; i < segs.length; i++) {
105
+ const s = segs[i];
106
+ const last = i === segs.length - 1;
107
+ if (e <= s.qStart + s.q || last) {
108
+ const f = s.q > 0 ? clamp01((e - s.qStart) / s.q) : 1;
109
+ tempC = s.kind === "change" ? s.t0 : s.t0 + f * (s.t1 - s.t0);
110
+ phase = s.phase;
111
+ break;
112
+ }
113
+ }
114
+ return {
115
+ tempC,
116
+ phase,
117
+ fracMelt,
118
+ fracBoil
119
+ };
120
+ }
121
+ const ThermalCore = {
122
+ reset(params = {}) {
123
+ const sub = params.substance ?? WATER;
124
+ const mass = Math.max(.001, params.mass ?? 50);
125
+ const tStart = params.tStart ?? -20;
126
+ const tMax = params.tMax ?? sub.tBoil + 40;
127
+ const segs = buildSegs(sub, mass, tStart, tMax);
128
+ const totalJ = segs.reduce((a, s) => a + s.q, 0);
129
+ const d = derive(segs, totalJ, 0);
130
+ return {
131
+ sub,
132
+ mass,
133
+ powerW: params.powerW ?? 100,
134
+ tStart,
135
+ tMax,
136
+ segs,
137
+ totalJ,
138
+ energyJ: 0,
139
+ tempC: d.tempC,
140
+ phase: d.phase,
141
+ fracMelt: 0,
142
+ fracBoil: 0,
143
+ tSec: 0,
144
+ done: false
145
+ };
146
+ },
147
+ step(state, dtSec) {
148
+ const dt = Math.max(0, dtSec);
149
+ const energyJ = Math.max(0, Math.min(state.totalJ, state.energyJ + state.powerW * dt));
150
+ const d = derive(state.segs, state.totalJ, energyJ);
151
+ const done = state.powerW >= 0 && energyJ >= state.totalJ || state.powerW < 0 && energyJ <= 0;
152
+ return {
153
+ ...state,
154
+ energyJ,
155
+ tempC: d.tempC,
156
+ phase: d.phase,
157
+ fracMelt: d.fracMelt,
158
+ fracBoil: d.fracBoil,
159
+ tSec: state.tSec + dt,
160
+ done
161
+ };
162
+ }
163
+ };
164
+ /** Human label for a segment ("warming ice", "melting", "boiling"…). */
165
+ function segLabel(s, sub) {
166
+ if (s.which === "melt") return `melting ${sub.solidName}`;
167
+ if (s.which === "boil") return `boiling ${sub.liquidName}`;
168
+ return `warming ${s.phase === "solid" ? sub.solidName : s.phase === "gas" ? sub.gasName : sub.liquidName}`;
169
+ }
170
+
171
+ //#endregion
172
+ export { ETHANOL, ThermalCore, WATER, segLabel };
@@ -0,0 +1,24 @@
1
+ //#region src/sim/types.d.ts
2
+ /**
3
+ * Sim cores — the "physics engine" half of a lab: pure, time-stepped simulators
4
+ * with NO React and NO pixels. A core is `reset(params) → state`, then
5
+ * `step(state, dt) → state` once per frame. Because a core is a pure function of
6
+ * (params, elapsed time), a lab built on one is deterministic: it replays
7
+ * identically under a Remotion fixed-frame clock (→ video) and is unit-testable
8
+ * with no DOM. Rendering (glyphs) READS the returned state; drawing never lives
9
+ * in here. Live controls write their target fields onto `state` between steps —
10
+ * so a core's mutable inputs (e.g. `volts`, `mode`) live IN its state, which is
11
+ * also why those fields are readable by name from a binding.
12
+ */
13
+ interface SimCore<S, P> {
14
+ /** Build initial state from params. Pure. */
15
+ reset(params: P): S;
16
+ /**
17
+ * Advance `state` by `dtSec` and return the NEXT state. Treat the incoming
18
+ * `state` as immutable (return a fresh object) so the runtime can keep the
19
+ * previous snapshot for change detection.
20
+ */
21
+ step(state: S, dtSec: number): S;
22
+ }
23
+ //#endregion
24
+ export { SimCore };