@jjlmoya/utils-science 1.27.0 → 1.29.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 (53) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +3 -1
  3. package/src/entries.ts +5 -1
  4. package/src/index.ts +2 -0
  5. package/src/tests/locale_completeness.test.ts +2 -2
  6. package/src/tests/tool_validation.test.ts +2 -2
  7. package/src/tool/entropy-second-law/bibliography.astro +14 -0
  8. package/src/tool/entropy-second-law/bibliography.ts +12 -0
  9. package/src/tool/entropy-second-law/component.astro +366 -0
  10. package/src/tool/entropy-second-law/entropy-second-law-simulator.css +445 -0
  11. package/src/tool/entropy-second-law/entry.ts +26 -0
  12. package/src/tool/entropy-second-law/i18n/de.ts +210 -0
  13. package/src/tool/entropy-second-law/i18n/en.ts +210 -0
  14. package/src/tool/entropy-second-law/i18n/es.ts +210 -0
  15. package/src/tool/entropy-second-law/i18n/fr.ts +210 -0
  16. package/src/tool/entropy-second-law/i18n/id.ts +210 -0
  17. package/src/tool/entropy-second-law/i18n/it.ts +210 -0
  18. package/src/tool/entropy-second-law/i18n/ja.ts +210 -0
  19. package/src/tool/entropy-second-law/i18n/ko.ts +210 -0
  20. package/src/tool/entropy-second-law/i18n/nl.ts +210 -0
  21. package/src/tool/entropy-second-law/i18n/pl.ts +210 -0
  22. package/src/tool/entropy-second-law/i18n/pt.ts +210 -0
  23. package/src/tool/entropy-second-law/i18n/ru.ts +210 -0
  24. package/src/tool/entropy-second-law/i18n/sv.ts +210 -0
  25. package/src/tool/entropy-second-law/i18n/tr.ts +210 -0
  26. package/src/tool/entropy-second-law/i18n/zh.ts +210 -0
  27. package/src/tool/entropy-second-law/index.ts +11 -0
  28. package/src/tool/entropy-second-law/logic.ts +208 -0
  29. package/src/tool/entropy-second-law/seo.astro +15 -0
  30. package/src/tool/phase-diagram-critical-points/bibliography.astro +14 -0
  31. package/src/tool/phase-diagram-critical-points/bibliography.ts +16 -0
  32. package/src/tool/phase-diagram-critical-points/component.astro +397 -0
  33. package/src/tool/phase-diagram-critical-points/entry.ts +26 -0
  34. package/src/tool/phase-diagram-critical-points/i18n/de.ts +179 -0
  35. package/src/tool/phase-diagram-critical-points/i18n/en.ts +181 -0
  36. package/src/tool/phase-diagram-critical-points/i18n/es.ts +179 -0
  37. package/src/tool/phase-diagram-critical-points/i18n/fr.ts +179 -0
  38. package/src/tool/phase-diagram-critical-points/i18n/id.ts +179 -0
  39. package/src/tool/phase-diagram-critical-points/i18n/it.ts +179 -0
  40. package/src/tool/phase-diagram-critical-points/i18n/ja.ts +179 -0
  41. package/src/tool/phase-diagram-critical-points/i18n/ko.ts +179 -0
  42. package/src/tool/phase-diagram-critical-points/i18n/nl.ts +179 -0
  43. package/src/tool/phase-diagram-critical-points/i18n/pl.ts +179 -0
  44. package/src/tool/phase-diagram-critical-points/i18n/pt.ts +179 -0
  45. package/src/tool/phase-diagram-critical-points/i18n/ru.ts +179 -0
  46. package/src/tool/phase-diagram-critical-points/i18n/sv.ts +179 -0
  47. package/src/tool/phase-diagram-critical-points/i18n/tr.ts +179 -0
  48. package/src/tool/phase-diagram-critical-points/i18n/zh.ts +179 -0
  49. package/src/tool/phase-diagram-critical-points/index.ts +11 -0
  50. package/src/tool/phase-diagram-critical-points/logic.ts +179 -0
  51. package/src/tool/phase-diagram-critical-points/phase-diagram-critical-points-visualizer.css +542 -0
  52. package/src/tool/phase-diagram-critical-points/seo.astro +15 -0
  53. package/src/tools.ts +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-science",
3
- "version": "1.27.0",
3
+ "version": "1.29.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -10,10 +10,12 @@ import { lorenzAttractor } from '../tool/lorenz-attractor/index';
10
10
  import { stellarHabitabilityZone } from '../tool/stellar-habitability-zone/index';
11
11
  import { radioactiveDecay } from '../tool/radioactive-decay/index';
12
12
  import { naturalSelectionDrift } from '../tool/natural-selection-drift/index';
13
+ import { entropySecondLaw } from '../tool/entropy-second-law/index';
14
+ import { phaseDiagramCriticalPoints } from '../tool/phase-diagram-critical-points/index';
13
15
 
14
16
  export const scienceCategory: ScienceCategoryEntry = {
15
17
  icon: 'mdi:flask',
16
- tools: [colonyCounter, asteroidImpact, microwaveDetector, simulationProbability, cellularRenewal, cosmicInflation, temperatureTimeline, lorenzAttractor, stellarHabitabilityZone, radioactiveDecay, naturalSelectionDrift],
18
+ tools: [colonyCounter, asteroidImpact, microwaveDetector, simulationProbability, cellularRenewal, cosmicInflation, temperatureTimeline, lorenzAttractor, stellarHabitabilityZone, radioactiveDecay, naturalSelectionDrift, entropySecondLaw, phaseDiagramCriticalPoints],
17
19
  i18n: {
18
20
  es: () => import('./i18n/es').then((m) => m.content),
19
21
  en: () => import('./i18n/en').then((m) => m.content),
package/src/entries.ts CHANGED
@@ -10,6 +10,8 @@ export { lorenzAttractor } from './tool/lorenz-attractor/entry';
10
10
  export { stellarHabitabilityZone } from './tool/stellar-habitability-zone/entry';
11
11
  export { radioactiveDecay } from './tool/radioactive-decay/entry';
12
12
  export { naturalSelectionDrift } from './tool/natural-selection-drift/entry';
13
+ export { entropySecondLaw } from './tool/entropy-second-law/entry';
14
+ export { phaseDiagramCriticalPoints } from './tool/phase-diagram-critical-points/entry';
13
15
  export { scienceCategory } from './category';
14
16
  import { asteroidImpact } from './tool/asteroid-impact/entry';
15
17
  import { cellularRenewal } from './tool/cellular-renewal/entry';
@@ -22,4 +24,6 @@ import { lorenzAttractor } from './tool/lorenz-attractor/entry';
22
24
  import { stellarHabitabilityZone } from './tool/stellar-habitability-zone/entry';
23
25
  import { radioactiveDecay } from './tool/radioactive-decay/entry';
24
26
  import { naturalSelectionDrift } from './tool/natural-selection-drift/entry';
25
- export const ALL_ENTRIES = [asteroidImpact, cellularRenewal, colonyCounter, microwaveDetector, simulationProbability, cosmicInflation, temperatureTimeline, lorenzAttractor, stellarHabitabilityZone, radioactiveDecay, naturalSelectionDrift];
27
+ import { entropySecondLaw } from './tool/entropy-second-law/entry';
28
+ import { phaseDiagramCriticalPoints } from './tool/phase-diagram-critical-points/entry';
29
+ export const ALL_ENTRIES = [asteroidImpact, cellularRenewal, colonyCounter, microwaveDetector, simulationProbability, cosmicInflation, temperatureTimeline, lorenzAttractor, stellarHabitabilityZone, radioactiveDecay, naturalSelectionDrift, entropySecondLaw, phaseDiagramCriticalPoints];
package/src/index.ts CHANGED
@@ -11,6 +11,8 @@ export { LORENZ_ATTRACTOR_TOOL } from './tool/lorenz-attractor/index';
11
11
  export { STELLAR_HABITABILITY_ZONE_TOOL } from './tool/stellar-habitability-zone/index';
12
12
  export { RADIOACTIVE_DECAY_TOOL } from './tool/radioactive-decay/index';
13
13
  export { NATURAL_SELECTION_DRIFT_TOOL } from './tool/natural-selection-drift/index';
14
+ export { ENTROPY_SECOND_LAW_TOOL } from './tool/entropy-second-law/index';
15
+ export { PHASE_DIAGRAM_CRITICAL_POINTS_TOOL } from './tool/phase-diagram-critical-points/index';
14
16
 
15
17
  export type {
16
18
  KnownLocale,
@@ -18,7 +18,7 @@ describe('Locale Completeness Validation', () => {
18
18
  });
19
19
  });
20
20
 
21
- it('all 11 tools registered', () => {
22
- expect(ALL_TOOLS.length).toBe(11);
21
+ it('all 13 tools registered', () => {
22
+ expect(ALL_TOOLS.length).toBe(13);
23
23
  });
24
24
  });
@@ -4,8 +4,8 @@ import { scienceCategory } from '../data';
4
4
 
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
- it('should have 11 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(11);
7
+ it('should have 13 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(13);
9
9
  });
10
10
 
11
11
  it('scienceCategory should be defined', () => {
@@ -0,0 +1,14 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { entropySecondLaw } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props;
11
+ const content = await entropySecondLaw.i18n[locale]?.();
12
+ ---
13
+
14
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,12 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'Entropy and the Second Law of Thermodynamics',
6
+ url: 'https://www.mdpi.com/1099-4300/22/7/793',
7
+ },
8
+ {
9
+ name: 'OpenStax University Physics Volume 2, The Second Law of Thermodynamics',
10
+ url: 'https://openstax.org/books/university-physics-volume-2/pages/4-introduction',
11
+ }
12
+ ];
@@ -0,0 +1,366 @@
1
+ ---
2
+ import './entropy-second-law-simulator.css';
3
+
4
+ interface Props {
5
+ ui: Record<string, string>;
6
+ }
7
+
8
+ const { ui } = Astro.props;
9
+ ---
10
+
11
+ <div class="entropy-lab" id="entropy-lab">
12
+ <section class="entropy-controls" aria-label={ui.controls}>
13
+ <p class="entropy-status">{ui.status}</p>
14
+ <div class="entropy-control-grid">
15
+ <label class="entropy-field" for="entropy-left-temp">
16
+ <span>{ui.leftTemperature}</span>
17
+ <output id="entropy-left-temp-output">280 K</output>
18
+ <input id="entropy-left-temp" type="range" min="180" max="800" step="10" value="280" />
19
+ </label>
20
+
21
+ <label class="entropy-field" for="entropy-right-temp">
22
+ <span>{ui.rightTemperature}</span>
23
+ <output id="entropy-right-temp-output">480 K</output>
24
+ <input id="entropy-right-temp" type="range" min="180" max="800" step="10" value="480" />
25
+ </label>
26
+
27
+ <label class="entropy-field entropy-field-barrier" for="entropy-gate-open">
28
+ <span>{ui.gateAperture}</span>
29
+ <output id="entropy-gate-output">55%</output>
30
+ <input id="entropy-gate-open" type="range" min="20" max="92" step="1" value="55" />
31
+ </label>
32
+ </div>
33
+
34
+ <div class="entropy-actions">
35
+ <button type="button" id="entropy-toggle" data-pause={ui.pause} data-resume={ui.resume}>{ui.pause}</button>
36
+ <button type="button" id="entropy-reset">{ui.reset}</button>
37
+ </div>
38
+ </section>
39
+
40
+ <section class="entropy-stage" aria-label={ui.particleBox}>
41
+ <div class="entropy-canvas-head">
42
+ <div class="entropy-divider" aria-hidden="true">
43
+ <i class="entropy-divider-line"></i>
44
+ <span>{ui.barrier}</span>
45
+ <i class="entropy-divider-line"></i>
46
+ </div>
47
+ </div>
48
+
49
+ <div class="entropy-canvas-shell">
50
+ <canvas id="entropy-canvas" width="720" height="440"></canvas>
51
+ </div>
52
+
53
+ <div class="entropy-graph-shell">
54
+ <div class="entropy-graph-meta">
55
+ <span>{ui.entropyGraph}</span>
56
+ <span>{ui.liveTrace}</span>
57
+ </div>
58
+ <svg class="entropy-graph" viewBox="0 0 760 180" role="img" aria-label={ui.entropyGraph}>
59
+ <defs>
60
+ <linearGradient id="entropy-fill" x1="0%" y1="0%" x2="100%" y2="0%">
61
+ <stop offset="0%" stop-color="#56d7ff"></stop>
62
+ <stop offset="100%" stop-color="#ffd36a"></stop>
63
+ </linearGradient>
64
+ <filter id="entropy-line-glow" x="-20%" y="-80%" width="140%" height="240%">
65
+ <feGaussianBlur stdDeviation="4" result="glow"></feGaussianBlur>
66
+ <feMerge>
67
+ <feMergeNode in="glow"></feMergeNode>
68
+ <feMergeNode in="SourceGraphic"></feMergeNode>
69
+ </feMerge>
70
+ </filter>
71
+ </defs>
72
+ <path class="entropy-graph-area" id="entropy-graph-area" d=""></path>
73
+ <path class="entropy-graph-line" id="entropy-graph-line" d=""></path>
74
+ <path class="entropy-graph-guide" d="M56 24 L56 150 L740 150"></path>
75
+ <path class="entropy-graph-guide entropy-graph-guide-mid" d="M56 87 L740 87"></path>
76
+ <text x="8" y="30">{ui.highEntropy}</text>
77
+ <text x="12" y="91">{ui.midEntropy}</text>
78
+ <text x="12" y="146">{ui.lowEntropy}</text>
79
+ </svg>
80
+ </div>
81
+ </section>
82
+
83
+ <section class="entropy-console" aria-label={ui.results}>
84
+ <div class="entropy-readout">
85
+ <span>{ui.totalEntropy}</span>
86
+ <strong id="entropy-total-value">0.00</strong>
87
+ </div>
88
+
89
+ <div class="entropy-metrics">
90
+ <article>
91
+ <span>{ui.particleBalance}</span>
92
+ <strong id="entropy-balance-value">48 / 48</strong>
93
+ </article>
94
+ <article>
95
+ <span>{ui.spatialEntropy}</span>
96
+ <strong id="entropy-spatial-value">0.00</strong>
97
+ </article>
98
+ <article>
99
+ <span>{ui.thermalEntropy}</span>
100
+ <strong id="entropy-thermal-value">0.00</strong>
101
+ </article>
102
+ <article>
103
+ <span>{ui.energyGap}</span>
104
+ <strong id="entropy-gap-value">0.00</strong>
105
+ </article>
106
+ </div>
107
+
108
+ <div class="entropy-note">
109
+ <span>{ui.noteLabel}</span>
110
+ <div class="entropy-stateboard">
111
+ <div class="entropy-state-dial" id="entropy-state-dial" aria-hidden="true">
112
+ <i class="entropy-state-track"></i>
113
+ <i class="entropy-state-node"></i>
114
+ </div>
115
+ <div class="entropy-state-labels">
116
+ <strong id="entropy-state-gradient" class="is-active">{ui.stateGradient}</strong>
117
+ <strong id="entropy-state-mixing">{ui.stateMixing}</strong>
118
+ <strong id="entropy-state-equilibrium">{ui.stateEquilibrium}</strong>
119
+ </div>
120
+ </div>
121
+ <p>{ui.note}</p>
122
+ </div>
123
+ </section>
124
+ </div>
125
+
126
+ <script>
127
+ import { createParticles, measureSystem, stepParticles } from './logic';
128
+
129
+ const canvas = document.getElementById('entropy-canvas') as HTMLCanvasElement | null;
130
+ const context = canvas?.getContext('2d');
131
+ const leftTemperatureInput = document.getElementById('entropy-left-temp') as HTMLInputElement | null;
132
+ const rightTemperatureInput = document.getElementById('entropy-right-temp') as HTMLInputElement | null;
133
+ const gateInput = document.getElementById('entropy-gate-open') as HTMLInputElement | null;
134
+ const leftTemperatureOutput = document.getElementById('entropy-left-temp-output');
135
+ const rightTemperatureOutput = document.getElementById('entropy-right-temp-output');
136
+ const gateOutput = document.getElementById('entropy-gate-output');
137
+ const balanceValue = document.getElementById('entropy-balance-value');
138
+ const spatialValue = document.getElementById('entropy-spatial-value');
139
+ const thermalValue = document.getElementById('entropy-thermal-value');
140
+ const totalValue = document.getElementById('entropy-total-value');
141
+ const gapValue = document.getElementById('entropy-gap-value');
142
+ const graphLine = document.getElementById('entropy-graph-line');
143
+ const graphArea = document.getElementById('entropy-graph-area');
144
+ const stateDial = document.getElementById('entropy-state-dial');
145
+ const stateGradient = document.getElementById('entropy-state-gradient');
146
+ const stateMixing = document.getElementById('entropy-state-mixing');
147
+ const stateEquilibrium = document.getElementById('entropy-state-equilibrium');
148
+ const toggleButton = document.getElementById('entropy-toggle');
149
+ const resetButton = document.getElementById('entropy-reset');
150
+
151
+ const scene = {
152
+ width: 720,
153
+ height: 440,
154
+ leftCount: 48,
155
+ rightCount: 48,
156
+ gateOpen: 0.55,
157
+ leftTemperature: 280,
158
+ rightTemperature: 480,
159
+ particles: [],
160
+ entropyHistory: [],
161
+ isPaused: false,
162
+ seed: 20260617,
163
+ lastTime: 0,
164
+ raf: 0,
165
+ };
166
+
167
+ function colorForEnergy(energy: number) {
168
+ const hue = 198 - energy * 148;
169
+ const saturation = 88;
170
+ const lightness = 64 - energy * 18;
171
+
172
+ return `hsl(${hue} ${saturation}% ${lightness}%)`;
173
+ }
174
+
175
+ function resetSimulation() {
176
+ scene.seed += 137;
177
+ scene.particles = createParticles({
178
+ width: scene.width,
179
+ height: scene.height,
180
+ gateOpen: scene.gateOpen,
181
+ seed: scene.seed,
182
+ left: { count: scene.leftCount, temperature: scene.leftTemperature },
183
+ right: { count: scene.rightCount, temperature: scene.rightTemperature },
184
+ });
185
+ scene.entropyHistory = [];
186
+ scene.lastTime = 0;
187
+ updateReadout();
188
+ renderFrame();
189
+ }
190
+
191
+ function updateControlOutputs() {
192
+ leftTemperatureOutput!.textContent = `${scene.leftTemperature} K`;
193
+ rightTemperatureOutput!.textContent = `${scene.rightTemperature} K`;
194
+ gateOutput!.textContent = `${Math.round(scene.gateOpen * 100)}%`;
195
+ }
196
+
197
+ function updateSliderFills() {
198
+ leftTemperatureInput?.style.setProperty('--fill', `${((scene.leftTemperature - 180) / (800 - 180)) * 100}%`);
199
+ rightTemperatureInput?.style.setProperty('--fill', `${((scene.rightTemperature - 180) / (800 - 180)) * 100}%`);
200
+ gateInput?.style.setProperty('--fill', `${((scene.gateOpen * 100 - 20) / (92 - 20)) * 100}%`);
201
+ }
202
+
203
+ function updateMetricOutputs(metrics: ReturnType<typeof measureSystem>) {
204
+ balanceValue!.textContent = `${metrics.leftCount} / ${metrics.rightCount}`;
205
+ spatialValue!.textContent = metrics.spatialEntropy.toFixed(2);
206
+ thermalValue!.textContent = metrics.thermalEntropy.toFixed(2);
207
+ totalValue!.textContent = metrics.totalEntropy.toFixed(2);
208
+ gapValue!.textContent = Math.abs(metrics.leftAverageEnergy - metrics.rightAverageEnergy).toFixed(2);
209
+ }
210
+
211
+ function updateMacroState(metrics: ReturnType<typeof measureSystem>) {
212
+ stateDial?.style.setProperty('--entropy-progress', `${metrics.totalEntropy}`);
213
+
214
+ const gradientActive = metrics.totalEntropy < 0.42;
215
+ const mixingActive = metrics.totalEntropy >= 0.42 && metrics.totalEntropy < 0.9;
216
+ const equilibriumActive = metrics.totalEntropy >= 0.9;
217
+
218
+ stateGradient?.classList.toggle('is-active', gradientActive);
219
+ stateMixing?.classList.toggle('is-active', mixingActive);
220
+ stateEquilibrium?.classList.toggle('is-active', equilibriumActive);
221
+ stateDial?.classList.toggle('is-equilibrium', equilibriumActive);
222
+ }
223
+
224
+ function updateReadout() {
225
+ updateControlOutputs();
226
+ updateSliderFills();
227
+ const metrics = measureSystem(scene.particles, scene.width, {
228
+ left: scene.leftTemperature,
229
+ right: scene.rightTemperature,
230
+ });
231
+ updateMetricOutputs(metrics);
232
+ updateMacroState(metrics);
233
+
234
+ scene.entropyHistory.push(metrics.totalEntropy);
235
+ if (scene.entropyHistory.length > 90) {
236
+ scene.entropyHistory.shift();
237
+ }
238
+
239
+ drawGraph();
240
+ }
241
+
242
+ function drawGraph() {
243
+ const left = 56;
244
+ const right = 740;
245
+ const bottom = 150;
246
+ const top = 24;
247
+ const width = right - left;
248
+ const height = bottom - top;
249
+
250
+ if (scene.entropyHistory.length === 0) {
251
+ graphLine?.setAttribute('d', '');
252
+ graphArea?.setAttribute('d', '');
253
+ return;
254
+ }
255
+
256
+ const points = scene.entropyHistory.map((value, index) => {
257
+ const x = left + (index / Math.max(1, scene.entropyHistory.length - 1)) * width;
258
+ const y = bottom - value * height;
259
+ return `${index === 0 ? 'M' : 'L'}${x.toFixed(2)} ${y.toFixed(2)}`;
260
+ }).join(' ');
261
+
262
+ graphLine?.setAttribute('d', points);
263
+ graphArea?.setAttribute('d', `${points} L ${right} ${bottom} L ${left} ${bottom} Z`);
264
+ }
265
+
266
+ function drawDivider() {
267
+ if (!context) return;
268
+
269
+ const dividerX = scene.width / 2;
270
+ const aperture = scene.height * scene.gateOpen;
271
+ const gateTop = (scene.height - aperture) / 2;
272
+ const gateBottom = gateTop + aperture;
273
+
274
+ context.strokeStyle = 'rgba(255, 244, 220, 0.3)';
275
+ context.lineWidth = 2;
276
+ context.beginPath();
277
+ context.moveTo(dividerX, 0);
278
+ context.lineTo(dividerX, gateTop);
279
+ context.moveTo(dividerX, gateBottom);
280
+ context.lineTo(dividerX, scene.height);
281
+ context.stroke();
282
+
283
+ context.fillStyle = 'rgba(255, 211, 106, 0.12)';
284
+ context.fillRect(dividerX - 3, gateTop, 6, aperture);
285
+ }
286
+
287
+ function renderFrame() {
288
+ if (!context || !canvas) return;
289
+
290
+ context.clearRect(0, 0, canvas.width, canvas.height);
291
+
292
+ const coldGlow = context.createRadialGradient(scene.width * 0.23, scene.height * 0.5, 0, scene.width * 0.23, scene.height * 0.5, scene.width * 0.28);
293
+ coldGlow.addColorStop(0, 'rgba(86, 215, 255, 0.12)');
294
+ coldGlow.addColorStop(1, 'rgba(86, 215, 255, 0)');
295
+ context.fillStyle = coldGlow;
296
+ context.fillRect(0, 0, scene.width, scene.height);
297
+
298
+ const hotGlow = context.createRadialGradient(scene.width * 0.77, scene.height * 0.5, 0, scene.width * 0.77, scene.height * 0.5, scene.width * 0.28);
299
+ hotGlow.addColorStop(0, 'rgba(255, 163, 71, 0.12)');
300
+ hotGlow.addColorStop(1, 'rgba(255, 163, 71, 0)');
301
+ context.fillStyle = hotGlow;
302
+ context.fillRect(0, 0, scene.width, scene.height);
303
+
304
+ drawDivider();
305
+
306
+ context.globalCompositeOperation = 'lighter';
307
+ scene.particles.forEach((particle) => {
308
+ context.beginPath();
309
+ context.fillStyle = colorForEnergy(particle.energy);
310
+ context.shadowBlur = 16;
311
+ context.shadowColor = colorForEnergy(particle.energy);
312
+ context.arc(particle.x, particle.y, 4.8 + particle.energy * 2.2, 0, Math.PI * 2);
313
+ context.fill();
314
+ });
315
+ context.globalCompositeOperation = 'source-over';
316
+ context.shadowBlur = 0;
317
+ }
318
+
319
+ function frame(time: number) {
320
+ if (!scene.isPaused) {
321
+ if (!scene.lastTime) scene.lastTime = time;
322
+ const delta = Math.min(32, time - scene.lastTime);
323
+ scene.lastTime = time;
324
+
325
+ stepParticles(scene.particles, {
326
+ width: scene.width,
327
+ height: scene.height,
328
+ gateOpen: scene.gateOpen,
329
+ left: { count: scene.leftCount, temperature: scene.leftTemperature },
330
+ right: { count: scene.rightCount, temperature: scene.rightTemperature },
331
+ }, delta);
332
+
333
+ updateReadout();
334
+ renderFrame();
335
+ }
336
+
337
+ scene.raf = window.requestAnimationFrame(frame);
338
+ }
339
+
340
+ leftTemperatureInput?.addEventListener('input', () => {
341
+ scene.leftTemperature = Number(leftTemperatureInput.value);
342
+ resetSimulation();
343
+ });
344
+
345
+ rightTemperatureInput?.addEventListener('input', () => {
346
+ scene.rightTemperature = Number(rightTemperatureInput.value);
347
+ resetSimulation();
348
+ });
349
+
350
+ gateInput?.addEventListener('input', () => {
351
+ scene.gateOpen = Number(gateInput.value) / 100;
352
+ resetSimulation();
353
+ });
354
+
355
+ toggleButton?.addEventListener('click', () => {
356
+ scene.isPaused = !scene.isPaused;
357
+ toggleButton.textContent = scene.isPaused
358
+ ? toggleButton.getAttribute('data-resume')
359
+ : toggleButton.getAttribute('data-pause');
360
+ });
361
+
362
+ resetButton?.addEventListener('click', resetSimulation);
363
+
364
+ resetSimulation();
365
+ scene.raf = window.requestAnimationFrame(frame);
366
+ </script>