fuzzyworld

shadow-zone / files / src / earth.ts

earth.ts

9.0 KB · history · blame

1import type { AppState } from './controls';
2
3// Map a P-wave velocity (km/s) to a fill colour.
4// Range 1–15.5 km/s → very dark brown (slow) to bright yellow (fast).
5// Same velocity always produces the same colour, regardless of which layer it
6// comes from — so equal-speed regions look identical on screen.
7function velocityToColor(v: number): string {
8 const t = Math.max(0, Math.min(1, (v - 1.0) / 14.5));
9 const hue = Math.round(22 + t * 26); // 22° (red-orange) → 48° (golden yellow)
10 const sat = Math.round(65 + t * 30); // 65% → 95%
11 const lit = Math.round(14 + t * 48); // 14% → 62%
12 return `hsl(${hue}, ${sat}%, ${lit}%)`;
13}
14
15export interface EarthLayer {
16 rMin: number;
17 rMax: number;
18 vP: (r: number) => number;
19 vS: (r: number) => number;
20 name: string;
21 color: string;
22}
23
24export interface EarthModel {
25 radius: number;
26 layers: EarthLayer[];
27 coreModifier: string; // '' | 'negative-refraction' | 'zero-index' | 'absorber'
28 cmbRadius: number; // outer core radius (km), used by ray tracer for modifier checks
29 vAt(r: number, wave: 'P' | 'S'): number;
30 layerAt(r: number): EarthLayer;
31}
32
33export const R_EARTH = 6371;
34
35export function buildEarthModel(state: AppState): EarthModel {
36 const { outerCoreRadius, outerCoreVTop, outerCoreVBottom, innerCoreRadius, innerCoreV } = state;
37
38 // Clamp inner core so it fits inside outer core
39 const icr = Math.min(innerCoreRadius, outerCoreRadius - 1);
40
41 // Mantle: linear velocity with depth (increases going down, i.e. as r decreases).
42 // Lower mantle: 3480–5701 km v_P = 13.7 → 10.7 (with r)
43 // Upper mantle: 5701–6371 km v_P = 10.5 → 7.5 (with r)
44 // We parameterise generically so the CMB velocity matches outerCoreRadius.
45 const CMB = outerCoreRadius;
46
47 // Fixed mantle model (not user-controlled) referenced from plan.md PREM table.
48 // Lower mantle base at CMB: 13.7 km/s, top at 5701: 10.7 km/s
49 // Upper mantle base at 5701: 10.5 km/s, top at surface: 7.5 km/s
50 const LM_RMIN = CMB;
51 const LM_RMAX = 5701;
52
53 const layers: EarthLayer[] = [];
54
55 // ── Inner core ───────────────────────────────────────────────────────────
56 if (icr > 0) {
57 layers.push({
58 rMin: 0,
59 rMax: icr,
60 vP: () => innerCoreV,
61 vS: () => 3.5,
62 name: 'Inner Core',
63 color: velocityToColor(innerCoreV),
64 });
65 }
66
67 // ── Outer core ───────────────────────────────────────────────────────────
68 // Velocity varies linearly from outerCoreVBottom at r=icr to outerCoreVTop at r=CMB.
69 // Treated as liquid (vS=0) when outerCoreVTop < 10 km/s; solid otherwise.
70 // Exception: spiral-trap uses v(r) = outerCoreVTop * (r/CMB) — zero at center,
71 // producing constant sinI = p*v₀/CMB so rays spiral inward without turning.
72 const ocRange = CMB - icr;
73 const ocMidV = ocRange <= 0 ? outerCoreVTop
74 : (outerCoreVBottom + outerCoreVTop) / 2;
75 const ocIsSolid = outerCoreVTop >= 10;
76 const isSpiralTrap = state.coreModifier === 'spiral-trap';
77 layers.push({
78 rMin: icr,
79 rMax: CMB,
80 vP: isSpiralTrap
81 ? (r) => Math.max(0.001, outerCoreVTop * (r / CMB))
82 : (r) => {
83 if (ocRange <= 0) return outerCoreVTop;
84 const t = (r - icr) / ocRange; // 0 at inner boundary, 1 at CMB
85 return outerCoreVBottom + t * (outerCoreVTop - outerCoreVBottom);
86 },
87 vS: ocIsSolid
88 ? (r) => {
89 if (ocRange <= 0) return outerCoreVTop * 0.6;
90 const t = (r - icr) / ocRange;
91 return (outerCoreVBottom + t * (outerCoreVTop - outerCoreVBottom)) * 0.6;
92 }
93 : () => 0,
94 name: 'Outer Core',
95 color: velocityToColor(ocMidV),
96 });
97
98 // ── Mantle (lower + upper) ───────────────────────────────────────────────
99 // For presets that change outerCoreRadius, the lower mantle simply starts at CMB.
100 // If CMB > 5701 there is no lower mantle, only upper.
101 if (CMB < LM_RMAX) {
102 const lmRmin = CMB;
103 const lmRmax = Math.min(LM_RMAX, R_EARTH);
104 const vLmBot = 13.7;
105 const vLmTop = 10.7;
106 layers.push({
107 rMin: lmRmin,
108 rMax: lmRmax,
109 vP: (r) => {
110 const t = (r - lmRmin) / (lmRmax - lmRmin);
111 return vLmBot + t * (vLmTop - vLmBot);
112 },
113 vS: (r) => {
114 const t = (r - lmRmin) / (lmRmax - lmRmin);
115 return 7.3 + t * (6.5 - 7.3);
116 },
117 name: 'Lower Mantle',
118 color: velocityToColor((vLmBot + vLmTop) / 2),
119 });
120 }
121
122 // Upper mantle (only if CMB < R_EARTH)
123 const umRmin = Math.max(CMB, LM_RMAX);
124 if (umRmin < R_EARTH) {
125 const umRmax = R_EARTH;
126 const vUmBot = CMB >= LM_RMAX ? 13.7 - 3.0 * (CMB - LM_RMIN) / (LM_RMAX - LM_RMIN) : 10.5;
127 const vUmTop = 7.5;
128 layers.push({
129 rMin: umRmin,
130 rMax: umRmax,
131 vP: (r) => {
132 const t = (r - umRmin) / (umRmax - umRmin);
133 return vUmBot + t * (vUmTop - vUmBot);
134 },
135 vS: (r) => {
136 const t = (r - umRmin) / (umRmax - umRmin);
137 return 4.5 + t * (5.5 - 4.5);
138 },
139 name: 'Upper Mantle',
140 color: velocityToColor((vUmBot + vUmTop) / 2),
141 });
142 }
143
144 // Sort layers innermost → outermost
145 layers.sort((a, b) => a.rMin - b.rMin);
146
147 const model: EarthModel = {
148 radius: R_EARTH,
149 layers,
150 coreModifier: state.coreModifier,
151 cmbRadius: CMB,
152 vAt(r: number, wave: 'P' | 'S'): number {
153 const layer = this.layerAt(r);
154 return wave === 'P' ? layer.vP(r) : layer.vS(r);
155 },
156 layerAt(r: number): EarthLayer {
157 // Walk from inner to outer
158 for (const l of this.layers) {
159 if (r <= l.rMax) return l;
160 }
161 return this.layers[this.layers.length - 1];
162 },
163 };
164
165 return model;
166}
167
168// ── Presets ───────────────────────────────────────────────────────────────────
169
170const BASE: AppState = {
171 outerCoreRadius: 3480,
172 outerCoreVTop: 8.0,
173 outerCoreVBottom: 10.4,
174 innerCoreRadius: 1220,
175 innerCoreV: 11.3,
176 waveType: 'P',
177 numRays: 60,
178 preset: 'earth',
179 earthquakeId: '',
180 coreModifier: '',
181};
182
183export const PRESETS: Record<string, AppState> = {
184 earth: { ...BASE, preset: 'earth' },
185 solid: {
186 // Solid (no liquid outer core): velocities increase with depth continuously.
187 // outerCoreVTop matches the lower mantle at the CMB (13.7) — no P-wave discontinuity.
188 // outerCoreVTop >= 10 triggers solid vS behaviour in buildEarthModel.
189 ...BASE,
190 outerCoreVTop: 13.7,
191 outerCoreVBottom: 14.5,
192 innerCoreV: 14.5,
193 preset: 'solid',
194 },
195 'water-core': { ...BASE, outerCoreVTop: 1.5, outerCoreVBottom: 1.8, preset: 'water-core' },
196 'giant-core': { ...BASE, outerCoreRadius: 5000, innerCoreRadius: 1220, preset: 'giant-core' },
197 'tiny-core': { ...BASE, outerCoreRadius: 500, innerCoreRadius: 200, preset: 'tiny-core' },
198 'fast-core': { ...BASE, outerCoreVTop: 15.0, outerCoreVBottom: 14.0, innerCoreV: 14.5, preset: 'fast-core' },
199
200 // ── Impossible / metamaterial cores ─────────────────────────────────────
201 // Ray tracer interprets model.coreModifier to apply non-standard boundary physics.
202
203 // Acoustic metamaterial with negative effective bulk modulus and density.
204 // Snell's law is negated at the CMB: transmitted ray bends to the same side
205 // of the normal as the incident ray. Angular direction reverses inside the core.
206 'neg-refraction': { ...BASE, coreModifier: 'negative-refraction', preset: 'neg-refraction' },
207
208 // v_core → ∞ (zero-index medium): phase velocity is infinite, so sin(θ_t) → 0
209 // for all incident angles. Every transmitted ray is forced radial inside the core
210 // with no angular accumulation. The core acts as a perfect converging lens.
211 'zero-index': { ...BASE, coreModifier: 'zero-index', preset: 'zero-index' },
212
213 // v(r) = v₀·(r/R_CMB): velocity → 0 at centre. Snell parameter sinI = p·v₀/R_CMB
214 // is constant, so rays spiral inward indefinitely — a seismic black hole.
215 // Inner core removed (innerCoreRadius: 0) so the v∝r profile extends to r=0.
216 'spiral-trap': { ...BASE, innerCoreRadius: 0, coreModifier: 'spiral-trap', preset: 'spiral-trap' },
217
218 // Retrograde velocity gradient: v decreases with depth (opposite real Earth).
219 // outerCoreVTop (at CMB) > outerCoreVBottom (at ICB) → v/r falls going inward,
220 // so rays never reach a turning point and are TIR'd at the ICB velocity jump.
221 // The core acts as a concave mirror; the shadow zone expands.
222 'retrograde': { ...BASE, outerCoreVTop: 8.0, outerCoreVBottom: 2.0, coreModifier: '', preset: 'retrograde' },
223
224 // All waves absorbed at the CMB — nothing penetrates the core.
225 // Shadow zone is the pure geometric shadow of the core disk.
226 'absorber': { ...BASE, coreModifier: 'absorber', preset: 'absorber' },
227};