@jjlmoya/utils-science 1.28.0 → 1.30.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 +2 -1
- package/src/entries.ts +3 -1
- package/src/index.ts +1 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- 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 +557 -0
- package/src/tool/phase-diagram-critical-points/seo.astro +15 -0
- package/src/tools.ts +2 -0
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -11,10 +11,11 @@ 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
13
|
import { entropySecondLaw } from '../tool/entropy-second-law/index';
|
|
14
|
+
import { phaseDiagramCriticalPoints } from '../tool/phase-diagram-critical-points/index';
|
|
14
15
|
|
|
15
16
|
export const scienceCategory: ScienceCategoryEntry = {
|
|
16
17
|
icon: 'mdi:flask',
|
|
17
|
-
tools: [colonyCounter, asteroidImpact, microwaveDetector, simulationProbability, cellularRenewal, cosmicInflation, temperatureTimeline, lorenzAttractor, stellarHabitabilityZone, radioactiveDecay, naturalSelectionDrift, entropySecondLaw],
|
|
18
|
+
tools: [colonyCounter, asteroidImpact, microwaveDetector, simulationProbability, cellularRenewal, cosmicInflation, temperatureTimeline, lorenzAttractor, stellarHabitabilityZone, radioactiveDecay, naturalSelectionDrift, entropySecondLaw, phaseDiagramCriticalPoints],
|
|
18
19
|
i18n: {
|
|
19
20
|
es: () => import('./i18n/es').then((m) => m.content),
|
|
20
21
|
en: () => import('./i18n/en').then((m) => m.content),
|
package/src/entries.ts
CHANGED
|
@@ -11,6 +11,7 @@ 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
13
|
export { entropySecondLaw } from './tool/entropy-second-law/entry';
|
|
14
|
+
export { phaseDiagramCriticalPoints } from './tool/phase-diagram-critical-points/entry';
|
|
14
15
|
export { scienceCategory } from './category';
|
|
15
16
|
import { asteroidImpact } from './tool/asteroid-impact/entry';
|
|
16
17
|
import { cellularRenewal } from './tool/cellular-renewal/entry';
|
|
@@ -24,4 +25,5 @@ import { stellarHabitabilityZone } from './tool/stellar-habitability-zone/entry'
|
|
|
24
25
|
import { radioactiveDecay } from './tool/radioactive-decay/entry';
|
|
25
26
|
import { naturalSelectionDrift } from './tool/natural-selection-drift/entry';
|
|
26
27
|
import { entropySecondLaw } from './tool/entropy-second-law/entry';
|
|
27
|
-
|
|
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
|
@@ -12,6 +12,7 @@ export { STELLAR_HABITABILITY_ZONE_TOOL } from './tool/stellar-habitability-zone
|
|
|
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
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';
|
|
15
16
|
|
|
16
17
|
export type {
|
|
17
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 { phaseDiagramCriticalPoints } 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 phaseDiagramCriticalPoints.i18n[locale]?.();
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{content && <SharedBibliography links={content.bibliography} />}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
|
+
{
|
|
5
|
+
name: 'NIST Chemistry WebBook',
|
|
6
|
+
url: 'https://webbook.nist.gov/chemistry/',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'OpenStax Chemistry 2e, Phase Diagrams',
|
|
10
|
+
url: 'https://openstax.org/books/chemistry-2e/pages/10-4-phase-diagrams',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'IUPAC Gold Book, Critical Point',
|
|
14
|
+
url: 'https://goldbook.iupac.org/terms/view/C01396',
|
|
15
|
+
},
|
|
16
|
+
];
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
---
|
|
2
|
+
import './phase-diagram-critical-points-visualizer.css';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
ui: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { ui } = Astro.props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<div class="phase-lab" id="phase-lab">
|
|
12
|
+
<section class="phase-panel phase-controls" aria-label={ui.controls}>
|
|
13
|
+
<label class="phase-field" for="phase-substance">
|
|
14
|
+
<span>{ui.substance}</span>
|
|
15
|
+
<select id="phase-substance">
|
|
16
|
+
<option value="water">Water</option>
|
|
17
|
+
<option value="carbon-dioxide">Carbon dioxide</option>
|
|
18
|
+
<option value="nitrogen">Nitrogen</option>
|
|
19
|
+
</select>
|
|
20
|
+
</label>
|
|
21
|
+
|
|
22
|
+
<label class="phase-field" for="phase-units">
|
|
23
|
+
<span>{ui.units}</span>
|
|
24
|
+
<select id="phase-units">
|
|
25
|
+
<option value="scientific">{ui.scientificUnits}</option>
|
|
26
|
+
<option value="metric">{ui.metricUnits}</option>
|
|
27
|
+
<option value="imperial">{ui.imperialUnits}</option>
|
|
28
|
+
</select>
|
|
29
|
+
</label>
|
|
30
|
+
|
|
31
|
+
<label class="phase-field" for="phase-temperature">
|
|
32
|
+
<span>{ui.temperature}</span>
|
|
33
|
+
<output id="phase-temperature-output">373 K</output>
|
|
34
|
+
<input id="phase-temperature" type="range" min="40" max="720" step="1" value="373" />
|
|
35
|
+
</label>
|
|
36
|
+
|
|
37
|
+
<label class="phase-field" for="phase-pressure">
|
|
38
|
+
<span>{ui.pressure}</span>
|
|
39
|
+
<output id="phase-pressure-output">0.10 MPa</output>
|
|
40
|
+
<input id="phase-pressure" type="range" min="-3" max="1.45" step="0.01" value="-1" />
|
|
41
|
+
</label>
|
|
42
|
+
|
|
43
|
+
<button type="button" id="phase-reset">{ui.reset}</button>
|
|
44
|
+
</section>
|
|
45
|
+
|
|
46
|
+
<section class="phase-map-shell" aria-label={ui.diagram}>
|
|
47
|
+
<div class="phase-map-frame">
|
|
48
|
+
<span class="phase-axis-label phase-axis-label-y" aria-hidden="true">{ui.pressure}</span>
|
|
49
|
+
<svg class="phase-map" id="phase-map" viewBox="0 0 820 560" role="img" aria-label={ui.diagram}>
|
|
50
|
+
<defs>
|
|
51
|
+
<linearGradient id="phase-supercritical-glow" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
52
|
+
<stop offset="0%" stop-color="#ffcf5a" stop-opacity="0.18"></stop>
|
|
53
|
+
<stop offset="100%" stop-color="#4fd6b5" stop-opacity="0.3"></stop>
|
|
54
|
+
</linearGradient>
|
|
55
|
+
<filter id="phase-soft-glow" x="-30%" y="-30%" width="160%" height="160%">
|
|
56
|
+
<feGaussianBlur stdDeviation="8" result="glow"></feGaussianBlur>
|
|
57
|
+
<feMerge>
|
|
58
|
+
<feMergeNode in="glow"></feMergeNode>
|
|
59
|
+
<feMergeNode in="SourceGraphic"></feMergeNode>
|
|
60
|
+
</feMerge>
|
|
61
|
+
</filter>
|
|
62
|
+
</defs>
|
|
63
|
+
<rect class="phase-region phase-region-gas" x="46" y="72" width="730" height="410" rx="14"></rect>
|
|
64
|
+
<path class="phase-region phase-region-solid" id="phase-solid-region"></path>
|
|
65
|
+
<path class="phase-region phase-region-liquid" id="phase-liquid-region"></path>
|
|
66
|
+
<path class="phase-region phase-region-supercritical" id="phase-super-region"></path>
|
|
67
|
+
<path class="phase-grid" d="M46 482 H776 M46 379.5 H776 M46 277 H776 M46 174.5 H776 M46 72 H776 M46 72 V482 M228.5 72 V482 M411 72 V482 M593.5 72 V482 M776 72 V482"></path>
|
|
68
|
+
<path class="phase-boundary phase-vapor" id="phase-vapor-curve"></path>
|
|
69
|
+
<path class="phase-boundary phase-melt" id="phase-melt-line"></path>
|
|
70
|
+
<g class="phase-point phase-triple" id="phase-triple-point">
|
|
71
|
+
<circle r="7"></circle>
|
|
72
|
+
<text x="15" y="24">{ui.triplePoint}</text>
|
|
73
|
+
</g>
|
|
74
|
+
<g class="phase-point phase-critical" id="phase-critical-point">
|
|
75
|
+
<circle r="9"></circle>
|
|
76
|
+
<text x="-106" y="-24">{ui.criticalPoint}</text>
|
|
77
|
+
</g>
|
|
78
|
+
<g class="phase-sample-marker" id="phase-sample-marker">
|
|
79
|
+
<circle class="phase-sample-trail phase-sample-trail-outer" r="38"></circle>
|
|
80
|
+
<circle class="phase-sample-trail phase-sample-trail-inner" r="22"></circle>
|
|
81
|
+
<circle class="phase-sample-dot" r="7"></circle>
|
|
82
|
+
</g>
|
|
83
|
+
<text class="phase-region-label phase-label-solid" id="phase-label-solid">{ui.solid}</text>
|
|
84
|
+
<text class="phase-region-label phase-label-liquid" id="phase-label-liquid">{ui.liquid}</text>
|
|
85
|
+
<text class="phase-region-label phase-label-gas" id="phase-label-gas">{ui.gas}</text>
|
|
86
|
+
<text class="phase-region-label phase-label-super" id="phase-label-super">{ui.supercritical}</text>
|
|
87
|
+
</svg>
|
|
88
|
+
<span class="phase-axis-label phase-axis-label-x" aria-hidden="true">{ui.temperature}</span>
|
|
89
|
+
</div>
|
|
90
|
+
</section>
|
|
91
|
+
|
|
92
|
+
<section class="phase-panel phase-readout" aria-label={ui.sample}>
|
|
93
|
+
<span>{ui.phase}</span>
|
|
94
|
+
<strong id="phase-state">{ui.liquid}</strong>
|
|
95
|
+
<p id="phase-coordinates">{ui.coordinates}: 373 K / 0.10 MPa</p>
|
|
96
|
+
|
|
97
|
+
<div class="phase-meter">
|
|
98
|
+
<div>
|
|
99
|
+
<span>{ui.latentHeat}</span>
|
|
100
|
+
<output id="phase-latent-output">0.82</output>
|
|
101
|
+
</div>
|
|
102
|
+
<i id="phase-latent-bar"></i>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div class="phase-meter">
|
|
106
|
+
<div>
|
|
107
|
+
<span>{ui.criticalProximity}</span>
|
|
108
|
+
<output id="phase-critical-output">0.18</output>
|
|
109
|
+
</div>
|
|
110
|
+
<i id="phase-critical-bar"></i>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<div class="phase-coexistence" aria-label={ui.interpretation}>
|
|
114
|
+
<div class="phase-coexistence-head">
|
|
115
|
+
<span>{ui.purePhase}</span>
|
|
116
|
+
<output id="phase-coexistence-output">12%</output>
|
|
117
|
+
<span>{ui.coexistence}</span>
|
|
118
|
+
</div>
|
|
119
|
+
<div class="phase-coexistence-axis">
|
|
120
|
+
<i id="phase-coexistence-marker"></i>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</section>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<script>
|
|
127
|
+
import {
|
|
128
|
+
buildPhaseCurve,
|
|
129
|
+
classifyPhase,
|
|
130
|
+
formatPressureForUnits,
|
|
131
|
+
formatTemperatureForUnits,
|
|
132
|
+
getSubstance,
|
|
133
|
+
meltingPressureAt,
|
|
134
|
+
vaporPressureAt,
|
|
135
|
+
} from './logic';
|
|
136
|
+
|
|
137
|
+
const substanceSelect = document.getElementById('phase-substance') as HTMLSelectElement | null;
|
|
138
|
+
const unitsSelect = document.getElementById('phase-units') as HTMLSelectElement | null;
|
|
139
|
+
const temperatureInput = document.getElementById('phase-temperature') as HTMLInputElement | null;
|
|
140
|
+
const pressureInput = document.getElementById('phase-pressure') as HTMLInputElement | null;
|
|
141
|
+
const temperatureOutput = document.getElementById('phase-temperature-output');
|
|
142
|
+
const pressureOutput = document.getElementById('phase-pressure-output');
|
|
143
|
+
const resetButton = document.getElementById('phase-reset');
|
|
144
|
+
const vaporCurve = document.getElementById('phase-vapor-curve');
|
|
145
|
+
const meltLine = document.getElementById('phase-melt-line');
|
|
146
|
+
const solidRegion = document.getElementById('phase-solid-region');
|
|
147
|
+
const liquidRegion = document.getElementById('phase-liquid-region');
|
|
148
|
+
const superRegion = document.getElementById('phase-super-region');
|
|
149
|
+
const triplePoint = document.getElementById('phase-triple-point');
|
|
150
|
+
const criticalPoint = document.getElementById('phase-critical-point');
|
|
151
|
+
const sampleMarker = document.getElementById('phase-sample-marker');
|
|
152
|
+
const stateOutput = document.getElementById('phase-state');
|
|
153
|
+
const coordinatesOutput = document.getElementById('phase-coordinates');
|
|
154
|
+
const latentOutput = document.getElementById('phase-latent-output');
|
|
155
|
+
const criticalOutput = document.getElementById('phase-critical-output');
|
|
156
|
+
const latentBar = document.getElementById('phase-latent-bar');
|
|
157
|
+
const criticalBar = document.getElementById('phase-critical-bar');
|
|
158
|
+
const coexistenceOutput = document.getElementById('phase-coexistence-output');
|
|
159
|
+
const coexistenceMarker = document.getElementById('phase-coexistence-marker');
|
|
160
|
+
const coexistenceAxis = document.querySelector('.phase-coexistence-axis') as HTMLElement | null;
|
|
161
|
+
const storageKey = 'phase-diagram-critical-points:last-state';
|
|
162
|
+
|
|
163
|
+
const plot = {
|
|
164
|
+
left: 46,
|
|
165
|
+
top: 72,
|
|
166
|
+
width: 730,
|
|
167
|
+
height: 410,
|
|
168
|
+
};
|
|
169
|
+
const phaseLabelNodes = {
|
|
170
|
+
solid: document.getElementById('phase-label-solid'),
|
|
171
|
+
liquid: document.getElementById('phase-label-liquid'),
|
|
172
|
+
gas: document.getElementById('phase-label-gas'),
|
|
173
|
+
supercritical: document.getElementById('phase-label-super'),
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const labels = {
|
|
177
|
+
solid: document.getElementById('phase-label-solid')?.textContent ?? 'Solid',
|
|
178
|
+
liquid: document.getElementById('phase-label-liquid')?.textContent ?? 'Liquid',
|
|
179
|
+
gas: document.getElementById('phase-label-gas')?.textContent ?? 'Gas',
|
|
180
|
+
supercritical: document.getElementById('phase-label-super')?.textContent ?? 'Supercritical',
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
function pressureFromSlider() {
|
|
184
|
+
return 10 ** Number(pressureInput?.value ?? -1);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function currentSubstance() {
|
|
188
|
+
return getSubstance(substanceSelect?.value ?? 'water');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function currentUnits() {
|
|
192
|
+
return (unitsSelect?.value ?? 'scientific') as 'scientific' | 'metric' | 'imperial';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function xForTemperature(temperature: number, minTemperature: number, maxTemperature: number) {
|
|
196
|
+
return plot.left + ((temperature - minTemperature) / (maxTemperature - minTemperature)) * plot.width;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function yForPressure(pressure: number, minPressure: number, maxPressure: number) {
|
|
200
|
+
const logMin = Math.log10(minPressure);
|
|
201
|
+
const logMax = Math.log10(maxPressure);
|
|
202
|
+
const progress = (Math.log10(Math.max(minPressure, pressure)) - logMin) / (logMax - logMin);
|
|
203
|
+
|
|
204
|
+
return plot.top + plot.height - progress * plot.height;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function pathFromPoints(points: { x: number; y: number }[]) {
|
|
208
|
+
return points.map((point, index) => `${index === 0 ? 'M' : 'L'}${point.x.toFixed(1)} ${point.y.toFixed(1)}`).join(' ');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function setAttribute(node: Element | null, name: string, value: string) {
|
|
212
|
+
node?.setAttribute(name, value);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function setText(node: Element | null, value: string) {
|
|
216
|
+
if (node) node.textContent = value;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function setCssValue(node: HTMLElement | null, name: string, value: string) {
|
|
220
|
+
node?.style.setProperty(name, value);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function updateSliderFill(input: HTMLInputElement | null) {
|
|
224
|
+
if (!input) return;
|
|
225
|
+
const min = Number(input.min);
|
|
226
|
+
const max = Number(input.max);
|
|
227
|
+
const value = Number(input.value);
|
|
228
|
+
input.style.setProperty('--fill', `${((value - min) / (max - min)) * 100}%`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function boundaryCoexistence(substance: ReturnType<typeof getSubstance>, temperature: number, pressure: number) {
|
|
232
|
+
const vaporPressure = vaporPressureAt(substance, temperature);
|
|
233
|
+
const vaporDistance = Math.abs(Math.log10(Math.max(0.001, pressure)) - Math.log10(Math.max(0.001, vaporPressure)));
|
|
234
|
+
const meltPressure = meltingPressureAt(substance, temperature);
|
|
235
|
+
const meltDistance = Math.abs(Math.log10(Math.max(0.001, pressure)) - Math.log10(Math.max(0.001, meltPressure)));
|
|
236
|
+
const temperatureWindow = Math.abs(temperature - substance.tripleTemperature) < substance.criticalTemperature * 0.5;
|
|
237
|
+
const nearestDistance = Math.min(vaporDistance, temperatureWindow ? meltDistance : 99);
|
|
238
|
+
|
|
239
|
+
return Math.max(0, Math.min(1, 1 - nearestDistance / 0.22));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function readSavedState() {
|
|
243
|
+
try {
|
|
244
|
+
const rawState = window.localStorage.getItem(storageKey);
|
|
245
|
+
return rawState ? JSON.parse(rawState) : null;
|
|
246
|
+
} catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function saveState() {
|
|
252
|
+
try {
|
|
253
|
+
window.localStorage.setItem(storageKey, JSON.stringify({
|
|
254
|
+
substance: substanceSelect?.value,
|
|
255
|
+
units: unitsSelect?.value,
|
|
256
|
+
temperature: temperatureInput?.value,
|
|
257
|
+
pressure: pressureInput?.value,
|
|
258
|
+
}));
|
|
259
|
+
} catch {
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function getPlotBounds(substance: ReturnType<typeof getSubstance>) {
|
|
264
|
+
return {
|
|
265
|
+
minTemperature: Math.max(35, substance.tripleTemperature * 0.55),
|
|
266
|
+
maxTemperature: substance.criticalTemperature * 1.18,
|
|
267
|
+
minPressure: Math.max(0.001, substance.triplePressure * 0.2),
|
|
268
|
+
maxPressure: substance.criticalPressure * 1.45,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function pointForValues(temperature: number, pressure: number, bounds: ReturnType<typeof getPlotBounds>) {
|
|
273
|
+
return {
|
|
274
|
+
x: xForTemperature(temperature, bounds.minTemperature, bounds.maxTemperature),
|
|
275
|
+
y: yForPressure(pressure, bounds.minPressure, bounds.maxPressure),
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function buildGeometry(substance: ReturnType<typeof getSubstance>) {
|
|
280
|
+
const bounds = getPlotBounds(substance);
|
|
281
|
+
const minTemperature = Math.max(35, substance.tripleTemperature * 0.55);
|
|
282
|
+
const maxTemperature = substance.criticalTemperature * 1.18;
|
|
283
|
+
const curve = buildPhaseCurve(substance).map((point) => ({
|
|
284
|
+
x: xForTemperature(point.temperature, bounds.minTemperature, bounds.maxTemperature),
|
|
285
|
+
y: yForPressure(point.pressure, bounds.minPressure, bounds.maxPressure),
|
|
286
|
+
}));
|
|
287
|
+
const triple = pointForValues(substance.tripleTemperature, substance.triplePressure, bounds);
|
|
288
|
+
const critical = pointForValues(substance.criticalTemperature, substance.criticalPressure, bounds);
|
|
289
|
+
const meltTopTemperature = substance.tripleTemperature
|
|
290
|
+
+ Math.sign(substance.fusionSlope) * (maxTemperature - minTemperature) * 0.24;
|
|
291
|
+
const meltTop = pointForValues(meltTopTemperature, bounds.maxPressure, bounds);
|
|
292
|
+
const meltLower = pointForValues(substance.tripleTemperature - 46, bounds.minPressure, bounds);
|
|
293
|
+
|
|
294
|
+
return { bounds, curve, triple, critical, meltTop, meltLower };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function drawPhaseGeometry(geometry: ReturnType<typeof buildGeometry>) {
|
|
298
|
+
const { curve, triple, critical, meltTop, meltLower } = geometry;
|
|
299
|
+
vaporCurve?.setAttribute('d', pathFromPoints(curve));
|
|
300
|
+
meltLine?.setAttribute('d', pathFromPoints([meltLower, triple, meltTop]));
|
|
301
|
+
solidRegion?.setAttribute('d', `M${plot.left} ${plot.top} H${meltTop.x.toFixed(1)} L${triple.x.toFixed(1)} ${triple.y.toFixed(1)} L${meltLower.x.toFixed(1)} ${plot.top + plot.height} H${plot.left} Z`);
|
|
302
|
+
liquidRegion?.setAttribute('d', `M${triple.x.toFixed(1)} ${triple.y.toFixed(1)} L${meltTop.x.toFixed(1)} ${plot.top} H${plot.left + plot.width} V${critical.y.toFixed(1)} L${critical.x.toFixed(1)} ${critical.y.toFixed(1)} ${pathFromPoints(curve.slice().reverse()).replace('M', 'L')} Z`);
|
|
303
|
+
superRegion?.setAttribute('d', `M${critical.x.toFixed(1)} ${plot.top} H${plot.left + plot.width} V${critical.y.toFixed(1)} H${critical.x.toFixed(1)} Z`);
|
|
304
|
+
triplePoint?.setAttribute('transform', `translate(${triple.x.toFixed(1)} ${triple.y.toFixed(1)})`);
|
|
305
|
+
criticalPoint?.setAttribute('transform', `translate(${critical.x.toFixed(1)} ${critical.y.toFixed(1)})`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function positionRegionLabels(critical: { x: number; y: number }) {
|
|
309
|
+
setAttribute(phaseLabelNodes.solid, 'x', `${plot.left + 64}`);
|
|
310
|
+
setAttribute(phaseLabelNodes.solid, 'y', `${plot.top + 80}`);
|
|
311
|
+
setAttribute(phaseLabelNodes.liquid, 'x', `${Math.min(plot.left + plot.width - 162, critical.x - 118)}`);
|
|
312
|
+
setAttribute(phaseLabelNodes.liquid, 'y', `${plot.top + 126}`);
|
|
313
|
+
setAttribute(phaseLabelNodes.gas, 'x', `${plot.left + plot.width - 124}`);
|
|
314
|
+
setAttribute(phaseLabelNodes.gas, 'y', `${plot.top + plot.height - 54}`);
|
|
315
|
+
setAttribute(phaseLabelNodes.supercritical, 'x', `${Math.min(critical.x + 14, plot.left + plot.width - 168)}`);
|
|
316
|
+
setAttribute(phaseLabelNodes.supercritical, 'y', `${Math.max(plot.top + 42, critical.y + 38)}`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function updateSampleReadout(substance: ReturnType<typeof getSubstance>, geometry: ReturnType<typeof buildGeometry>) {
|
|
320
|
+
const temperature = Number(temperatureInput?.value ?? substance.normalBoilingPoint);
|
|
321
|
+
const pressure = pressureFromSlider();
|
|
322
|
+
const sample = classifyPhase(substance, { temperature, pressure });
|
|
323
|
+
const samplePoint = pointForValues(temperature, pressure, geometry.bounds);
|
|
324
|
+
const meltPressure = meltingPressureAt(substance, temperature);
|
|
325
|
+
const coexistence = boundaryCoexistence(substance, temperature, pressure);
|
|
326
|
+
|
|
327
|
+
setAttribute(sampleMarker, 'transform', `translate(${samplePoint.x.toFixed(1)} ${samplePoint.y.toFixed(1)})`);
|
|
328
|
+
setAttribute(sampleMarker, 'data-phase', sample.phase);
|
|
329
|
+
setText(stateOutput, labels[sample.phase]);
|
|
330
|
+
setText(coordinatesOutput, `${formatTemperatureForUnits(temperature, currentUnits())} / ${formatPressureForUnits(pressure, currentUnits())}`);
|
|
331
|
+
setText(temperatureOutput, formatTemperatureForUnits(temperature, currentUnits()));
|
|
332
|
+
setText(pressureOutput, formatPressureForUnits(pressure, currentUnits()));
|
|
333
|
+
setText(latentOutput, sample.latentHeatIndex.toFixed(2));
|
|
334
|
+
setText(criticalOutput, sample.proximityToCritical.toFixed(2));
|
|
335
|
+
setText(coexistenceOutput, `${Math.round(coexistence * 100)}%`);
|
|
336
|
+
setCssValue(latentBar, '--value', `${sample.latentHeatIndex}`);
|
|
337
|
+
setCssValue(criticalBar, '--value', `${sample.proximityToCritical}`);
|
|
338
|
+
setCssValue(coexistenceMarker, '--value', `${coexistence}`);
|
|
339
|
+
setCssValue(coexistenceAxis, '--coexistence', `${coexistence}`);
|
|
340
|
+
document.getElementById('phase-lab')?.style.setProperty('--melt-pressure', `${meltPressure}`);
|
|
341
|
+
updateSliderFill(temperatureInput);
|
|
342
|
+
updateSliderFill(pressureInput);
|
|
343
|
+
saveState();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function drawDiagram() {
|
|
347
|
+
const substance = currentSubstance();
|
|
348
|
+
const geometry = buildGeometry(substance);
|
|
349
|
+
|
|
350
|
+
drawPhaseGeometry(geometry);
|
|
351
|
+
positionRegionLabels(geometry.critical);
|
|
352
|
+
updateSampleReadout(substance, geometry);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function resetForSubstance(savedState?: { temperature?: string; pressure?: string }) {
|
|
356
|
+
const substance = currentSubstance();
|
|
357
|
+
if (temperatureInput) {
|
|
358
|
+
temperatureInput.min = `${Math.max(40, Math.floor(substance.tripleTemperature * 0.55))}`;
|
|
359
|
+
temperatureInput.max = `${Math.ceil(substance.criticalTemperature * 1.18)}`;
|
|
360
|
+
temperatureInput.value = savedState?.temperature ?? `${Math.round(substance.normalBoilingPoint)}`;
|
|
361
|
+
}
|
|
362
|
+
if (pressureInput) {
|
|
363
|
+
pressureInput.min = `${Math.log10(Math.max(0.001, substance.triplePressure * 0.2)).toFixed(2)}`;
|
|
364
|
+
pressureInput.max = `${Math.log10(substance.criticalPressure * 1.45).toFixed(2)}`;
|
|
365
|
+
pressureInput.value = savedState?.pressure ?? `${Math.log10(Math.min(0.101, substance.criticalPressure * 0.72)).toFixed(2)}`;
|
|
366
|
+
}
|
|
367
|
+
drawDiagram();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function hydrateSavedState() {
|
|
371
|
+
const savedState = readSavedState();
|
|
372
|
+
if (!savedState) {
|
|
373
|
+
resetForSubstance();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (savedState.substance && substanceSelect) {
|
|
378
|
+
substanceSelect.value = savedState.substance;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (savedState.units && unitsSelect) {
|
|
382
|
+
unitsSelect.value = savedState.units;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
resetForSubstance({
|
|
386
|
+
temperature: savedState.temperature,
|
|
387
|
+
pressure: savedState.pressure,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
substanceSelect?.addEventListener('change', () => resetForSubstance());
|
|
392
|
+
unitsSelect?.addEventListener('change', drawDiagram);
|
|
393
|
+
temperatureInput?.addEventListener('input', drawDiagram);
|
|
394
|
+
pressureInput?.addEventListener('input', drawDiagram);
|
|
395
|
+
resetButton?.addEventListener('click', () => resetForSubstance());
|
|
396
|
+
hydrateSavedState();
|
|
397
|
+
</script>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ScienceToolEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const phaseDiagramCriticalPoints: ScienceToolEntry = {
|
|
4
|
+
id: 'phase-diagram-critical-points',
|
|
5
|
+
icons: {
|
|
6
|
+
bg: 'mdi:chart-bell-curve-cumulative',
|
|
7
|
+
fg: 'mdi:state-machine',
|
|
8
|
+
},
|
|
9
|
+
i18n: {
|
|
10
|
+
de: () => import('./i18n/de').then((m) => m.content),
|
|
11
|
+
en: () => import('./i18n/en').then((m) => m.content),
|
|
12
|
+
es: () => import('./i18n/es').then((m) => m.content),
|
|
13
|
+
fr: () => import('./i18n/fr').then((m) => m.content),
|
|
14
|
+
id: () => import('./i18n/id').then((m) => m.content),
|
|
15
|
+
it: () => import('./i18n/it').then((m) => m.content),
|
|
16
|
+
ja: () => import('./i18n/ja').then((m) => m.content),
|
|
17
|
+
ko: () => import('./i18n/ko').then((m) => m.content),
|
|
18
|
+
nl: () => import('./i18n/nl').then((m) => m.content),
|
|
19
|
+
pl: () => import('./i18n/pl').then((m) => m.content),
|
|
20
|
+
pt: () => import('./i18n/pt').then((m) => m.content),
|
|
21
|
+
ru: () => import('./i18n/ru').then((m) => m.content),
|
|
22
|
+
sv: () => import('./i18n/sv').then((m) => m.content),
|
|
23
|
+
tr: () => import('./i18n/tr').then((m) => m.content),
|
|
24
|
+
zh: () => import('./i18n/zh').then((m) => m.content),
|
|
25
|
+
},
|
|
26
|
+
};
|