@jjlmoya/utils-science 1.36.0 → 1.38.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 +4 -1
- package/src/entries.ts +7 -1
- package/src/index.ts +3 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/double-slit-decoherence/bibliography.astro +14 -0
- package/src/tool/double-slit-decoherence/bibliography.ts +12 -0
- package/src/tool/double-slit-decoherence/component.astro +235 -0
- package/src/tool/double-slit-decoherence/double-slit-decoherence-simulator.css +344 -0
- package/src/tool/double-slit-decoherence/entry.ts +26 -0
- package/src/tool/double-slit-decoherence/i18n/de.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/en.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/es.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/fr.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/id.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/it.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/ja.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/ko.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/nl.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/pl.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/pt.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/ru.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/sv.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/tr.ts +181 -0
- package/src/tool/double-slit-decoherence/i18n/zh.ts +181 -0
- package/src/tool/double-slit-decoherence/index.ts +11 -0
- package/src/tool/double-slit-decoherence/logic.ts +77 -0
- package/src/tool/double-slit-decoherence/seo.astro +15 -0
- package/src/tool/dyson-sphere-energy-capture/bibliography.astro +14 -0
- package/src/tool/dyson-sphere-energy-capture/bibliography.ts +16 -0
- package/src/tool/dyson-sphere-energy-capture/component.astro +253 -0
- package/src/tool/dyson-sphere-energy-capture/dyson-sphere-energy-capture.css +502 -0
- package/src/tool/dyson-sphere-energy-capture/entry.ts +26 -0
- package/src/tool/dyson-sphere-energy-capture/i18n/de.ts +195 -0
- package/src/tool/dyson-sphere-energy-capture/i18n/en.ts +195 -0
- package/src/tool/dyson-sphere-energy-capture/i18n/es.ts +195 -0
- package/src/tool/dyson-sphere-energy-capture/i18n/fr.ts +195 -0
- package/src/tool/dyson-sphere-energy-capture/i18n/id.ts +195 -0
- package/src/tool/dyson-sphere-energy-capture/i18n/it.ts +195 -0
- package/src/tool/dyson-sphere-energy-capture/i18n/ja.ts +71 -0
- package/src/tool/dyson-sphere-energy-capture/i18n/ko.ts +71 -0
- package/src/tool/dyson-sphere-energy-capture/i18n/nl.ts +197 -0
- package/src/tool/dyson-sphere-energy-capture/i18n/pl.ts +197 -0
- package/src/tool/dyson-sphere-energy-capture/i18n/pt.ts +195 -0
- package/src/tool/dyson-sphere-energy-capture/i18n/ru.ts +195 -0
- package/src/tool/dyson-sphere-energy-capture/i18n/sv.ts +195 -0
- package/src/tool/dyson-sphere-energy-capture/i18n/tr.ts +195 -0
- package/src/tool/dyson-sphere-energy-capture/i18n/zh.ts +71 -0
- package/src/tool/dyson-sphere-energy-capture/index.ts +11 -0
- package/src/tool/dyson-sphere-energy-capture/logic.ts +120 -0
- package/src/tool/dyson-sphere-energy-capture/seo.astro +15 -0
- package/src/tool/global-albedo-snowball-simulator/bibliography.astro +14 -0
- package/src/tool/global-albedo-snowball-simulator/bibliography.ts +16 -0
- package/src/tool/global-albedo-snowball-simulator/component.astro +278 -0
- package/src/tool/global-albedo-snowball-simulator/entry.ts +26 -0
- package/src/tool/global-albedo-snowball-simulator/global-albedo-snowball-simulator.css +530 -0
- package/src/tool/global-albedo-snowball-simulator/i18n/de.ts +169 -0
- package/src/tool/global-albedo-snowball-simulator/i18n/en.ts +169 -0
- package/src/tool/global-albedo-snowball-simulator/i18n/es.ts +169 -0
- package/src/tool/global-albedo-snowball-simulator/i18n/fr.ts +169 -0
- package/src/tool/global-albedo-snowball-simulator/i18n/id.ts +169 -0
- package/src/tool/global-albedo-snowball-simulator/i18n/it.ts +87 -0
- package/src/tool/global-albedo-snowball-simulator/i18n/ja.ts +87 -0
- package/src/tool/global-albedo-snowball-simulator/i18n/ko.ts +169 -0
- package/src/tool/global-albedo-snowball-simulator/i18n/nl.ts +169 -0
- package/src/tool/global-albedo-snowball-simulator/i18n/pl.ts +169 -0
- package/src/tool/global-albedo-snowball-simulator/i18n/pt.ts +169 -0
- package/src/tool/global-albedo-snowball-simulator/i18n/ru.ts +169 -0
- package/src/tool/global-albedo-snowball-simulator/i18n/sv.ts +169 -0
- package/src/tool/global-albedo-snowball-simulator/i18n/tr.ts +169 -0
- package/src/tool/global-albedo-snowball-simulator/i18n/zh.ts +169 -0
- package/src/tool/global-albedo-snowball-simulator/index.ts +11 -0
- package/src/tool/global-albedo-snowball-simulator/logic.ts +88 -0
- package/src/tool/global-albedo-snowball-simulator/seo.astro +15 -0
- package/src/tools.ts +6 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { dysonSphereEnergyCapture } from './entry';
|
|
2
|
+
import type { ToolDefinition } from '../../types';
|
|
3
|
+
|
|
4
|
+
export * from './entry';
|
|
5
|
+
|
|
6
|
+
export const DYSON_SPHERE_ENERGY_CAPTURE_TOOL: ToolDefinition = {
|
|
7
|
+
entry: dysonSphereEnergyCapture,
|
|
8
|
+
Component: () => import('./component.astro'),
|
|
9
|
+
SEOComponent: () => import('./seo.astro'),
|
|
10
|
+
BibliographyComponent: () => import('./bibliography.astro'),
|
|
11
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export type StarType = 'm-dwarf' | 'sun' | 'a-star' | 'red-giant' | 'blue-giant';
|
|
2
|
+
export type StructureType = 'swarm' | 'ring' | 'shell' | 'statite-cloud';
|
|
3
|
+
|
|
4
|
+
export interface StarPreset {
|
|
5
|
+
id: StarType;
|
|
6
|
+
name: string;
|
|
7
|
+
luminositySolar: number;
|
|
8
|
+
massSolar: number;
|
|
9
|
+
radiusSolar: number;
|
|
10
|
+
temperatureK: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface StructurePreset {
|
|
14
|
+
id: StructureType;
|
|
15
|
+
name: string;
|
|
16
|
+
coverageEfficiency: number;
|
|
17
|
+
arealDensityKgM2: number;
|
|
18
|
+
thermalLimitK: number;
|
|
19
|
+
stabilityFactor: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DysonInput {
|
|
23
|
+
star: StarType;
|
|
24
|
+
structure: StructureType;
|
|
25
|
+
coveragePercent: number;
|
|
26
|
+
operatingTempK: number;
|
|
27
|
+
kardashevTarget: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DysonResult {
|
|
31
|
+
luminosityWatts: number;
|
|
32
|
+
capturedWatts: number;
|
|
33
|
+
optimalRadiusAu: number;
|
|
34
|
+
orbitalPeriodDays: number;
|
|
35
|
+
collectorAreaM2: number;
|
|
36
|
+
materialMassKg: number;
|
|
37
|
+
mercuryMasses: number;
|
|
38
|
+
earthMasses: number;
|
|
39
|
+
kardashevRating: number;
|
|
40
|
+
targetCoveragePercent: number;
|
|
41
|
+
status: 'underbuilt' | 'balanced' | 'extreme';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const SOLAR_LUMINOSITY_W = 3.828e26;
|
|
45
|
+
const SOLAR_MASS_KG = 1.98847e30;
|
|
46
|
+
const AU_M = 1.495978707e11;
|
|
47
|
+
const STEFAN_BOLTZMANN = 5.670374419e-8;
|
|
48
|
+
const MERCURY_MASS_KG = 3.3011e23;
|
|
49
|
+
const EARTH_MASS_KG = 5.9722e24;
|
|
50
|
+
const DAY_SECONDS = 86400;
|
|
51
|
+
const GRAVITATIONAL_CONSTANT = 6.6743e-11;
|
|
52
|
+
|
|
53
|
+
export const STAR_PRESETS: StarPreset[] = [
|
|
54
|
+
{ id: 'm-dwarf', name: 'M dwarf', luminositySolar: 0.035, massSolar: 0.35, radiusSolar: 0.42, temperatureK: 3400 },
|
|
55
|
+
{ id: 'sun', name: 'Sun-like G star', luminositySolar: 1, massSolar: 1, radiusSolar: 1, temperatureK: 5772 },
|
|
56
|
+
{ id: 'a-star', name: 'A-type main sequence', luminositySolar: 25, massSolar: 2.1, radiusSolar: 1.9, temperatureK: 9000 },
|
|
57
|
+
{ id: 'red-giant', name: 'Red giant', luminositySolar: 900, massSolar: 1.5, radiusSolar: 60, temperatureK: 3800 },
|
|
58
|
+
{ id: 'blue-giant', name: 'Blue giant', luminositySolar: 20000, massSolar: 12, radiusSolar: 7, temperatureK: 22000 },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
export const STRUCTURE_PRESETS: StructurePreset[] = [
|
|
62
|
+
{ id: 'swarm', name: 'Dyson swarm', coverageEfficiency: 0.82, arealDensityKgM2: 0.35, thermalLimitK: 1100, stabilityFactor: 1.15 },
|
|
63
|
+
{ id: 'ring', name: 'Equatorial ring', coverageEfficiency: 0.22, arealDensityKgM2: 1.4, thermalLimitK: 900, stabilityFactor: 0.62 },
|
|
64
|
+
{ id: 'shell', name: 'Rigid shell', coverageEfficiency: 0.96, arealDensityKgM2: 8.5, thermalLimitK: 750, stabilityFactor: 0.28 },
|
|
65
|
+
{ id: 'statite-cloud', name: 'Statite mirror cloud', coverageEfficiency: 0.68, arealDensityKgM2: 0.08, thermalLimitK: 650, stabilityFactor: 0.88 },
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
function clamp(value: number, min: number, max: number): number {
|
|
69
|
+
return Math.min(max, Math.max(min, value));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function findStar(id: StarType): StarPreset {
|
|
73
|
+
return STAR_PRESETS.find((star) => star.id === id) ?? STAR_PRESETS[1];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function findStructure(id: StructureType): StructurePreset {
|
|
77
|
+
return STRUCTURE_PRESETS.find((structure) => structure.id === id) ?? STRUCTURE_PRESETS[0];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function kardashevPower(rating: number): number {
|
|
81
|
+
return Math.pow(10, (rating * 10) + 6);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getDysonStatus(ratio: number): DysonResult['status'] {
|
|
85
|
+
if (ratio < 0.85) return 'underbuilt';
|
|
86
|
+
if (ratio > 2.5) return 'extreme';
|
|
87
|
+
return 'balanced';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function calculateDyson(input: DysonInput): DysonResult {
|
|
91
|
+
const star = findStar(input.star);
|
|
92
|
+
const structure = findStructure(input.structure);
|
|
93
|
+
const luminosityWatts = star.luminositySolar * SOLAR_LUMINOSITY_W;
|
|
94
|
+
const safeTemp = clamp(Math.min(input.operatingTempK, structure.thermalLimitK), 120, 1600);
|
|
95
|
+
const optimalRadiusM = Math.sqrt(luminosityWatts / (16 * Math.PI * STEFAN_BOLTZMANN * Math.pow(safeTemp, 4)));
|
|
96
|
+
const optimalRadiusAu = optimalRadiusM / AU_M;
|
|
97
|
+
const coverageFraction = clamp(input.coveragePercent / 100, 0.001, 1);
|
|
98
|
+
const capturedWatts = luminosityWatts * coverageFraction * structure.coverageEfficiency;
|
|
99
|
+
const collectorAreaM2 = 4 * Math.PI * optimalRadiusM * optimalRadiusM * coverageFraction / Math.max(0.05, structure.coverageEfficiency);
|
|
100
|
+
const materialMassKg = collectorAreaM2 * structure.arealDensityKgM2 * structure.stabilityFactor;
|
|
101
|
+
const orbitalPeriodDays = (2 * Math.PI * Math.sqrt(Math.pow(optimalRadiusM, 3) / (GRAVITATIONAL_CONSTANT * star.massSolar * SOLAR_MASS_KG))) / DAY_SECONDS;
|
|
102
|
+
const kardashevRating = (Math.log10(capturedWatts) - 6) / 10;
|
|
103
|
+
const targetCoveragePercent = clamp((kardashevPower(input.kardashevTarget) / (luminosityWatts * structure.coverageEfficiency)) * 100, 0, 100);
|
|
104
|
+
const ratio = input.coveragePercent / Math.max(0.01, targetCoveragePercent);
|
|
105
|
+
const status = getDysonStatus(ratio);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
luminosityWatts,
|
|
109
|
+
capturedWatts,
|
|
110
|
+
optimalRadiusAu,
|
|
111
|
+
orbitalPeriodDays,
|
|
112
|
+
collectorAreaM2,
|
|
113
|
+
materialMassKg,
|
|
114
|
+
mercuryMasses: materialMassKg / MERCURY_MASS_KG,
|
|
115
|
+
earthMasses: materialMassKg / EARTH_MASS_KG,
|
|
116
|
+
kardashevRating,
|
|
117
|
+
targetCoveragePercent,
|
|
118
|
+
status,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { dysonSphereEnergyCapture } 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 dysonSphereEnergyCapture.i18n[locale]?.();
|
|
12
|
+
if (!content) return null;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
{content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import { globalAlbedoSnowballSimulator } 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 globalAlbedoSnowballSimulator.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: 'Budyko, The effect of solar radiation variations on the climate of the Earth',
|
|
6
|
+
url: 'https://onlinelibrary.wiley.com/doi/abs/10.1111/j.2153-3490.1969.tb00466.x',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'A global climatic model based on the energy balance of the Earth-atmosphere system',
|
|
10
|
+
url: 'https://journals.ametsoc.org/view/journals/apme/8/3/1520-0450_1969_008_0392_agcmbo_2_0_co_2.xml',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'NOAA Global Monitoring Laboratory, Trends in atmospheric carbon dioxide',
|
|
14
|
+
url: 'https://gml.noaa.gov/ccgg/trends/',
|
|
15
|
+
},
|
|
16
|
+
];
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
---
|
|
2
|
+
import './global-albedo-snowball-simulator.css';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
ui: Record<string, string>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const { ui } = Astro.props;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<div
|
|
12
|
+
class="albedo-lab"
|
|
13
|
+
id="albedo-lab"
|
|
14
|
+
data-snowball={ui.snowball}
|
|
15
|
+
data-temperate={ui.temperate}
|
|
16
|
+
data-hothouse={ui.hothouse}
|
|
17
|
+
data-retreating={ui.retreating}
|
|
18
|
+
data-advancing={ui.advancing}
|
|
19
|
+
data-stable={ui.stable}
|
|
20
|
+
data-watts={ui.watts}
|
|
21
|
+
data-ppm={ui.ppm}
|
|
22
|
+
data-percent={ui.percent}
|
|
23
|
+
data-celsius={ui.celsius}
|
|
24
|
+
>
|
|
25
|
+
<section class="albedo-planet" aria-label={ui.timeline}>
|
|
26
|
+
<div class="albedo-guide albedo-guide-horizontal" aria-hidden="true"></div>
|
|
27
|
+
<div class="albedo-guide albedo-guide-vertical" aria-hidden="true"></div>
|
|
28
|
+
<div class="albedo-sun" id="albedo-sun" aria-hidden="true"></div>
|
|
29
|
+
<div class="albedo-orbit">
|
|
30
|
+
<div class="albedo-earth" id="albedo-earth">
|
|
31
|
+
<span class="albedo-atmosphere"></span>
|
|
32
|
+
<span class="albedo-ice albedo-ice-north"></span>
|
|
33
|
+
<span class="albedo-ice albedo-ice-mid"></span>
|
|
34
|
+
<span class="albedo-ice albedo-ice-south"></span>
|
|
35
|
+
<span class="albedo-glint"></span>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="albedo-state-pill">
|
|
39
|
+
<span>{ui.state}</span>
|
|
40
|
+
<strong id="albedo-state">---</strong>
|
|
41
|
+
</div>
|
|
42
|
+
</section>
|
|
43
|
+
|
|
44
|
+
<section class="albedo-controls" aria-label={ui.controls}>
|
|
45
|
+
<label class="albedo-slider albedo-slider-solar" for="albedo-solar">
|
|
46
|
+
<span>{ui.solarConstant}</span>
|
|
47
|
+
<output id="albedo-solar-output">1361 W/m<sup>2</sup></output>
|
|
48
|
+
<input id="albedo-solar" type="range" min="1180" max="1500" value="1361" step="1" />
|
|
49
|
+
</label>
|
|
50
|
+
<label class="albedo-slider albedo-slider-greenhouse" for="albedo-greenhouse">
|
|
51
|
+
<span>{ui.greenhouse}</span>
|
|
52
|
+
<output id="albedo-greenhouse-output">420 ppm</output>
|
|
53
|
+
<input id="albedo-greenhouse" type="range" min="120" max="1800" value="420" step="10" />
|
|
54
|
+
</label>
|
|
55
|
+
<label class="albedo-slider albedo-slider-ice" for="albedo-ice">
|
|
56
|
+
<span>{ui.initialIce}</span>
|
|
57
|
+
<output id="albedo-ice-output">22%</output>
|
|
58
|
+
<input id="albedo-ice" type="range" min="0" max="100" value="22" step="1" />
|
|
59
|
+
</label>
|
|
60
|
+
</section>
|
|
61
|
+
|
|
62
|
+
<section class="albedo-readouts" aria-label={ui.diagnostics}>
|
|
63
|
+
<article>
|
|
64
|
+
<span>{ui.temperature}</span>
|
|
65
|
+
<strong id="albedo-temperature">--- °C</strong>
|
|
66
|
+
</article>
|
|
67
|
+
<article>
|
|
68
|
+
<span>{ui.absorbed}</span>
|
|
69
|
+
<strong id="albedo-absorbed">--- W/m<sup>2</sup></strong>
|
|
70
|
+
</article>
|
|
71
|
+
<article>
|
|
72
|
+
<span>{ui.finalIce}</span>
|
|
73
|
+
<strong id="albedo-final-ice">---</strong>
|
|
74
|
+
</article>
|
|
75
|
+
<article>
|
|
76
|
+
<span>{ui.albedo}</span>
|
|
77
|
+
<strong id="albedo-final-albedo">---</strong>
|
|
78
|
+
</article>
|
|
79
|
+
</section>
|
|
80
|
+
|
|
81
|
+
<section class="albedo-chart-panel" aria-label={ui.timeline}>
|
|
82
|
+
<div class="albedo-chart-head">
|
|
83
|
+
<span>{ui.timeline}</span>
|
|
84
|
+
<strong id="albedo-trend">---</strong>
|
|
85
|
+
</div>
|
|
86
|
+
<svg class="albedo-chart" viewBox="0 0 720 280" role="img" aria-label={ui.timeline}>
|
|
87
|
+
<defs>
|
|
88
|
+
<linearGradient id="albedo-ice-gradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
89
|
+
<stop id="albedo-ice-gradient-top" offset="0%" stop-color="#dff9ff" stop-opacity="0.62"></stop>
|
|
90
|
+
<stop id="albedo-ice-gradient-bottom" offset="100%" stop-color="#1f8aa5" stop-opacity="0.02"></stop>
|
|
91
|
+
</linearGradient>
|
|
92
|
+
</defs>
|
|
93
|
+
<path class="albedo-chart-grid" d="M44 28 H684 M44 84 H684 M44 140 H684 M44 196 H684 M44 252 H684"></path>
|
|
94
|
+
<path class="albedo-chart-band" id="albedo-chart-band"></path>
|
|
95
|
+
<path class="albedo-chart-line" id="albedo-chart-line"></path>
|
|
96
|
+
<circle class="albedo-chart-dot" id="albedo-chart-dot" r="7"></circle>
|
|
97
|
+
<text x="44" y="270">0</text>
|
|
98
|
+
<text x="626" y="270">80 {ui.years}</text>
|
|
99
|
+
<text x="8" y="34">100%</text>
|
|
100
|
+
<text x="20" y="252">0%</text>
|
|
101
|
+
</svg>
|
|
102
|
+
</section>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<script>
|
|
106
|
+
import { simulateAlbedo } from './logic';
|
|
107
|
+
|
|
108
|
+
const root = document.getElementById('albedo-lab');
|
|
109
|
+
|
|
110
|
+
if (root) {
|
|
111
|
+
const labRoot = root;
|
|
112
|
+
const solarInput = document.getElementById('albedo-solar') as HTMLInputElement;
|
|
113
|
+
const greenhouseInput = document.getElementById('albedo-greenhouse') as HTMLInputElement;
|
|
114
|
+
const iceInput = document.getElementById('albedo-ice') as HTMLInputElement;
|
|
115
|
+
const chartLine = document.getElementById('albedo-chart-line');
|
|
116
|
+
const chartBand = document.getElementById('albedo-chart-band');
|
|
117
|
+
const chartDot = document.getElementById('albedo-chart-dot');
|
|
118
|
+
const gradientTop = document.getElementById('albedo-ice-gradient-top');
|
|
119
|
+
const gradientBottom = document.getElementById('albedo-ice-gradient-bottom');
|
|
120
|
+
|
|
121
|
+
const labelForState = {
|
|
122
|
+
snowball: root.dataset.snowball || 'Snowball lock-in',
|
|
123
|
+
temperate: root.dataset.temperate || 'Temperate balance',
|
|
124
|
+
hothouse: root.dataset.hothouse || 'Hothouse retreat',
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
function template(name: string, value: string) {
|
|
128
|
+
return (labRoot.dataset[name] || '{value}').replace('{value}', value);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function setText(id: string, value: string) {
|
|
132
|
+
const node = document.getElementById(id);
|
|
133
|
+
if (node) node.textContent = value;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function setHtml(id: string, value: string) {
|
|
137
|
+
const node = document.getElementById(id);
|
|
138
|
+
if (node) node.innerHTML = value;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function formatWatts(value: number) {
|
|
142
|
+
return `${value.toFixed(0)} W/m<sup>2</sup>`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function formatWattsDecimal(value: number) {
|
|
146
|
+
return `${value.toFixed(1)} W/m<sup>2</sup>`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function formatCelsius(value: number) {
|
|
150
|
+
return `${value.toFixed(1)} °C`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function formatPath(points: { x: number; y: number }[]) {
|
|
154
|
+
return points.map((point, index) => `${index === 0 ? 'M' : 'L'}${point.x.toFixed(1)} ${point.y.toFixed(1)}`).join(' ');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function drawChart(result: ReturnType<typeof simulateAlbedo>) {
|
|
158
|
+
const left = 44;
|
|
159
|
+
const top = 28;
|
|
160
|
+
const width = 640;
|
|
161
|
+
const height = 224;
|
|
162
|
+
const points = result.steps.map((step) => ({
|
|
163
|
+
x: left + (step.year / 80) * width,
|
|
164
|
+
y: top + height - (step.iceCover / 100) * height,
|
|
165
|
+
}));
|
|
166
|
+
const last = points[points.length - 1]!;
|
|
167
|
+
const band = `${formatPath(points)} L${left + width} ${top + height} L${left} ${top + height} Z`;
|
|
168
|
+
|
|
169
|
+
chartLine?.setAttribute('d', formatPath(points));
|
|
170
|
+
chartBand?.setAttribute('d', band);
|
|
171
|
+
chartDot?.setAttribute('cx', last.x.toFixed(1));
|
|
172
|
+
chartDot?.setAttribute('cy', last.y.toFixed(1));
|
|
173
|
+
gradientTop?.setAttribute('stop-opacity', `${0.18 + result.finalIceCover / 140}`);
|
|
174
|
+
gradientBottom?.setAttribute('stop-opacity', `${Math.max(0.02, result.finalIceCover / 360)}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function updateSlider(input: HTMLInputElement) {
|
|
178
|
+
const min = Number(input.min);
|
|
179
|
+
const max = Number(input.max);
|
|
180
|
+
const value = Number(input.value);
|
|
181
|
+
input.style.setProperty('--fill', `${((value - min) / (max - min)) * 100}%`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function setIceStyle(result: ReturnType<typeof simulateAlbedo>) {
|
|
185
|
+
const midIceOpacity = Math.max(0, Math.min(0.72, (result.finalIceCover - 36) / 56));
|
|
186
|
+
|
|
187
|
+
labRoot.style.setProperty('--ice-cover', `${result.finalIceCover}%`);
|
|
188
|
+
labRoot.style.setProperty('--initial-ice-cover', `${Number(iceInput.value)}%`);
|
|
189
|
+
labRoot.style.setProperty('--north-ice-height', `${8 + result.finalIceCover * 0.39}%`);
|
|
190
|
+
labRoot.style.setProperty('--south-ice-height', `${9 + result.finalIceCover * 0.43}%`);
|
|
191
|
+
labRoot.style.setProperty('--mid-ice-height', `${result.finalIceCover * 0.13}%`);
|
|
192
|
+
labRoot.style.setProperty('--mid-ice-opacity', `${midIceOpacity}`);
|
|
193
|
+
labRoot.style.setProperty('--north-ice-tilt', `${-4 + result.finalIceCover * 0.025}deg`);
|
|
194
|
+
labRoot.style.setProperty('--south-ice-tilt', `${5 - result.finalIceCover * 0.026}deg`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function setLightStyle(result: ReturnType<typeof simulateAlbedo>, solarIntensity: number, atmosphere: number) {
|
|
198
|
+
const albedoGlow = Math.min(1, result.finalAlbedo * 1.45);
|
|
199
|
+
|
|
200
|
+
labRoot.style.setProperty('--solar-intensity', solarIntensity.toFixed(3));
|
|
201
|
+
labRoot.style.setProperty('--atmosphere-intensity', atmosphere.toFixed(3));
|
|
202
|
+
labRoot.style.setProperty('--albedo-glow', `${albedoGlow}`);
|
|
203
|
+
labRoot.style.setProperty('--chart-ice-opacity', `${Math.max(0.08, result.finalIceCover / 100)}`);
|
|
204
|
+
labRoot.style.setProperty('--solar-panel-alpha', `${0.1 + solarIntensity * 0.22}`);
|
|
205
|
+
labRoot.style.setProperty('--solar-panel-alpha-dark', `${0.12 + solarIntensity * 0.34}`);
|
|
206
|
+
labRoot.style.setProperty('--atmosphere-panel-alpha', `${0.07 + atmosphere * 0.1}`);
|
|
207
|
+
labRoot.style.setProperty('--atmosphere-panel-alpha-dark', `${0.08 + atmosphere * 0.12}`);
|
|
208
|
+
labRoot.style.setProperty('--glint-alpha', `${0.12 + albedoGlow * 0.26}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function setBodyStyle(result: ReturnType<typeof simulateAlbedo>, solarIntensity: number, greenhouseIntensity: number, atmosphere: number) {
|
|
212
|
+
const albedoGlow = Math.min(1, result.finalAlbedo * 1.45);
|
|
213
|
+
|
|
214
|
+
labRoot.style.setProperty('--greenhouse-intensity', greenhouseIntensity.toFixed(3));
|
|
215
|
+
labRoot.style.setProperty('--sun-size', `${168 + solarIntensity * 76}px`);
|
|
216
|
+
labRoot.style.setProperty('--sun-glow-near', `${42 + solarIntensity * 86}px`);
|
|
217
|
+
labRoot.style.setProperty('--sun-glow-far', `${120 + solarIntensity * 170}px`);
|
|
218
|
+
labRoot.style.setProperty('--sun-glow-alpha-near', `${0.42 + solarIntensity * 0.28}`);
|
|
219
|
+
labRoot.style.setProperty('--sun-glow-alpha-far', `${0.14 + solarIntensity * 0.18}`);
|
|
220
|
+
labRoot.style.setProperty('--sun-saturation', `${0.95 + solarIntensity * 0.34}`);
|
|
221
|
+
labRoot.style.setProperty('--earth-atmosphere-glow', `${22 + atmosphere * 74}px`);
|
|
222
|
+
labRoot.style.setProperty('--earth-atmosphere-alpha', `${0.18 + atmosphere * 0.32}`);
|
|
223
|
+
labRoot.style.setProperty('--earth-albedo-glow', `${20 + albedoGlow * 52}px`);
|
|
224
|
+
labRoot.style.setProperty('--earth-albedo-alpha', `${0.12 + albedoGlow * 0.22}`);
|
|
225
|
+
labRoot.style.setProperty('--earth-saturation', `${0.78 + greenhouseIntensity * 0.45}`);
|
|
226
|
+
labRoot.style.setProperty('--earth-brightness', `${0.9 + solarIntensity * 0.18}`);
|
|
227
|
+
labRoot.style.setProperty('--atmosphere-inset', `${-7 - atmosphere * 13}px`);
|
|
228
|
+
labRoot.style.setProperty('--atmosphere-border-width', `${2 + atmosphere * 5}px`);
|
|
229
|
+
labRoot.style.setProperty('--atmosphere-border-alpha', `${0.18 + atmosphere * 0.22}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function setVisualPhysics(result: ReturnType<typeof simulateAlbedo>, solarConstant: number, greenhousePpm: number) {
|
|
233
|
+
const solarIntensity = (solarConstant - 1180) / (1500 - 1180);
|
|
234
|
+
const greenhouseIntensity = (greenhousePpm - 120) / (1800 - 120);
|
|
235
|
+
const iceFraction = result.finalIceCover / 100;
|
|
236
|
+
const atmosphere = Math.max(0.1, Math.min(1, greenhouseIntensity * 0.72 + (1 - iceFraction) * 0.28));
|
|
237
|
+
|
|
238
|
+
setIceStyle(result);
|
|
239
|
+
setLightStyle(result, solarIntensity, atmosphere);
|
|
240
|
+
setBodyStyle(result, solarIntensity, greenhouseIntensity, atmosphere);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function trendLabel(result: ReturnType<typeof simulateAlbedo>) {
|
|
244
|
+
const firstIce = result.steps[0]!.iceCover;
|
|
245
|
+
|
|
246
|
+
if (result.finalIceCover > firstIce + 3) return labRoot.dataset.advancing;
|
|
247
|
+
if (result.finalIceCover < firstIce - 3) return labRoot.dataset.retreating;
|
|
248
|
+
return labRoot.dataset.stable;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function update() {
|
|
252
|
+
const solarConstant = Number(solarInput.value);
|
|
253
|
+
const greenhousePpm = Number(greenhouseInput.value);
|
|
254
|
+
const initialIceCover = Number(iceInput.value);
|
|
255
|
+
const result = simulateAlbedo({ solarConstant, greenhousePpm, initialIceCover });
|
|
256
|
+
const trend = trendLabel(result);
|
|
257
|
+
|
|
258
|
+
setHtml('albedo-solar-output', formatWatts(solarConstant));
|
|
259
|
+
setText('albedo-greenhouse-output', template('ppm', greenhousePpm.toFixed(0)));
|
|
260
|
+
setText('albedo-ice-output', template('percent', initialIceCover.toFixed(0)));
|
|
261
|
+
setText('albedo-state', labelForState[result.stability]);
|
|
262
|
+
setHtml('albedo-temperature', formatCelsius(result.equilibriumTemperatureC));
|
|
263
|
+
setHtml('albedo-absorbed', formatWattsDecimal(result.absorbedWattsM2));
|
|
264
|
+
setText('albedo-final-ice', template('percent', result.finalIceCover.toFixed(0)));
|
|
265
|
+
setText('albedo-final-albedo', result.finalAlbedo.toFixed(2));
|
|
266
|
+
setText('albedo-trend', trend || '');
|
|
267
|
+
|
|
268
|
+
labRoot.dataset.state = result.stability;
|
|
269
|
+
setVisualPhysics(result, solarConstant, greenhousePpm);
|
|
270
|
+
drawChart(result);
|
|
271
|
+
[solarInput, greenhouseInput, iceInput].forEach(updateSlider);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
[solarInput, greenhouseInput, iceInput].forEach((input) => input.addEventListener('input', update));
|
|
275
|
+
update();
|
|
276
|
+
}
|
|
277
|
+
</script>
|
|
278
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ScienceToolEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const globalAlbedoSnowballSimulator: ScienceToolEntry = {
|
|
4
|
+
id: 'global-albedo-snowball-simulator',
|
|
5
|
+
icons: {
|
|
6
|
+
bg: 'mdi:snowflake',
|
|
7
|
+
fg: 'mdi:earth',
|
|
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
|
+
};
|