@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.
- package/package.json +1 -1
- package/src/category/index.ts +3 -1
- package/src/entries.ts +5 -1
- package/src/index.ts +2 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/entropy-second-law/bibliography.astro +14 -0
- package/src/tool/entropy-second-law/bibliography.ts +12 -0
- package/src/tool/entropy-second-law/component.astro +366 -0
- package/src/tool/entropy-second-law/entropy-second-law-simulator.css +445 -0
- package/src/tool/entropy-second-law/entry.ts +26 -0
- package/src/tool/entropy-second-law/i18n/de.ts +210 -0
- package/src/tool/entropy-second-law/i18n/en.ts +210 -0
- package/src/tool/entropy-second-law/i18n/es.ts +210 -0
- package/src/tool/entropy-second-law/i18n/fr.ts +210 -0
- package/src/tool/entropy-second-law/i18n/id.ts +210 -0
- package/src/tool/entropy-second-law/i18n/it.ts +210 -0
- package/src/tool/entropy-second-law/i18n/ja.ts +210 -0
- package/src/tool/entropy-second-law/i18n/ko.ts +210 -0
- package/src/tool/entropy-second-law/i18n/nl.ts +210 -0
- package/src/tool/entropy-second-law/i18n/pl.ts +210 -0
- package/src/tool/entropy-second-law/i18n/pt.ts +210 -0
- package/src/tool/entropy-second-law/i18n/ru.ts +210 -0
- package/src/tool/entropy-second-law/i18n/sv.ts +210 -0
- package/src/tool/entropy-second-law/i18n/tr.ts +210 -0
- package/src/tool/entropy-second-law/i18n/zh.ts +210 -0
- package/src/tool/entropy-second-law/index.ts +11 -0
- package/src/tool/entropy-second-law/logic.ts +208 -0
- package/src/tool/entropy-second-law/seo.astro +15 -0
- package/src/tool/phase-diagram-critical-points/bibliography.astro +14 -0
- package/src/tool/phase-diagram-critical-points/bibliography.ts +16 -0
- package/src/tool/phase-diagram-critical-points/component.astro +397 -0
- package/src/tool/phase-diagram-critical-points/entry.ts +26 -0
- package/src/tool/phase-diagram-critical-points/i18n/de.ts +179 -0
- package/src/tool/phase-diagram-critical-points/i18n/en.ts +181 -0
- package/src/tool/phase-diagram-critical-points/i18n/es.ts +179 -0
- package/src/tool/phase-diagram-critical-points/i18n/fr.ts +179 -0
- package/src/tool/phase-diagram-critical-points/i18n/id.ts +179 -0
- package/src/tool/phase-diagram-critical-points/i18n/it.ts +179 -0
- package/src/tool/phase-diagram-critical-points/i18n/ja.ts +179 -0
- package/src/tool/phase-diagram-critical-points/i18n/ko.ts +179 -0
- package/src/tool/phase-diagram-critical-points/i18n/nl.ts +179 -0
- package/src/tool/phase-diagram-critical-points/i18n/pl.ts +179 -0
- package/src/tool/phase-diagram-critical-points/i18n/pt.ts +179 -0
- package/src/tool/phase-diagram-critical-points/i18n/ru.ts +179 -0
- package/src/tool/phase-diagram-critical-points/i18n/sv.ts +179 -0
- package/src/tool/phase-diagram-critical-points/i18n/tr.ts +179 -0
- package/src/tool/phase-diagram-critical-points/i18n/zh.ts +179 -0
- package/src/tool/phase-diagram-critical-points/index.ts +11 -0
- package/src/tool/phase-diagram-critical-points/logic.ts +179 -0
- package/src/tool/phase-diagram-critical-points/phase-diagram-critical-points-visualizer.css +542 -0
- package/src/tool/phase-diagram-critical-points/seo.astro +15 -0
- package/src/tools.ts +4 -0
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
@@ -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
|
|
8
|
-
expect(ALL_TOOLS.length).toBe(
|
|
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>
|