shadow-zone / commit
Add five impossible metamaterial core presets
README.md | 12 ++++++++++ src/earth.ts | 48 +++++++++++++++++++++++++++++++++---- src/rayTracer.ts | 73 ++++++++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 115 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 6b6257b..4e23c0a 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,18 @@ Core*, *Giant Core*, *Solid Earth* — to explore non-Earth-like scenarios. - Velocity-based layer colouring — equal speed → equal colour - Hover over any red arc on the surface to see the exact shadow zone range - Collapsible description and Wikipedia reference in the sidebar +- **Impossible core presets** — five physically impossible metamaterial-style + cores (marked ⚠️) that go beyond what sliders alone can express: + - *Negative Refraction* — Snell's law negated at the CMB; rays bend to the + same side of the normal as the incident ray + - *Zero-Index Core* — all transmitted rays forced radial; core acts as a + perfect converging lens with no angular phase accumulation + - *Spiral Trap* — v∝r profile (zero at centre); sinI is constant, so rays + spiral inward indefinitely — a seismic black hole + - *Retrograde Core* — velocity decreases with depth; rays reach the ICB + and total-internally-reflect, expanding the shadow zone + - *Perfect Absorber* — all waves absorbed at the CMB; shadow zone equals + the pure geometric shadow of the core disk ## Tech stack diff --git a/src/earth.ts b/src/earth.ts index 464cdf7..27c4e47 100644 --- a/src/earth.ts +++ b/src/earth.ts @@ -24,6 +24,8 @@ export interface EarthLayer { export interface EarthModel { radius: number; layers: EarthLayer[]; + coreModifier: string; // '' | 'negative-refraction' | 'zero-index' | 'absorber' + cmbRadius: number; // outer core radius (km), used by ray tracer for modifier checks vAt(r: number, wave: 'P' | 'S'): number; layerAt(r: number): EarthLayer; } @@ -65,18 +67,23 @@ export function buildEarthModel(state: AppState): EarthModel { // ── Outer core ─────────────────────────────────────────────────────────── // Velocity varies linearly from outerCoreVBottom at r=icr to outerCoreVTop at r=CMB. // Treated as liquid (vS=0) when outerCoreVTop < 10 km/s; solid otherwise. + // Exception: spiral-trap uses v(r) = outerCoreVTop * (r/CMB) — zero at center, + // producing constant sinI = p*v₀/CMB so rays spiral inward without turning. const ocRange = CMB - icr; const ocMidV = ocRange <= 0 ? outerCoreVTop : (outerCoreVBottom + outerCoreVTop) / 2; const ocIsSolid = outerCoreVTop >= 10; + const isSpiralTrap = state.coreModifier === 'spiral-trap'; layers.push({ rMin: icr, rMax: CMB, - vP: (r) => { - if (ocRange <= 0) return outerCoreVTop; - const t = (r - icr) / ocRange; // 0 at inner boundary, 1 at CMB - return outerCoreVBottom + t * (outerCoreVTop - outerCoreVBottom); - }, + vP: isSpiralTrap + ? (r) => Math.max(0.001, outerCoreVTop * (r / CMB)) + : (r) => { + if (ocRange <= 0) return outerCoreVTop; + const t = (r - icr) / ocRange; // 0 at inner boundary, 1 at CMB + return outerCoreVBottom + t * (outerCoreVTop - outerCoreVBottom); + }, vS: ocIsSolid ? (r) => { if (ocRange <= 0) return outerCoreVTop * 0.6; @@ -140,6 +147,8 @@ export function buildEarthModel(state: AppState): EarthModel { const model: EarthModel = { radius: R_EARTH, layers, + coreModifier: state.coreModifier, + cmbRadius: CMB, vAt(r: number, wave: 'P' | 'S'): number { const layer = this.layerAt(r); return wave === 'P' ? layer.vP(r) : layer.vS(r); @@ -168,6 +177,7 @@ const BASE: AppState = { numRays: 60, preset: 'earth', earthquakeId: '', + coreModifier: '', }; export const PRESETS: Record<string, AppState> = { @@ -186,4 +196,32 @@ export const PRESETS: Record<string, AppState> = { 'giant-core': { ...BASE, outerCoreRadius: 5000, innerCoreRadius: 1220, preset: 'giant-core' }, 'tiny-core': { ...BASE, outerCoreRadius: 500, innerCoreRadius: 200, preset: 'tiny-core' }, 'fast-core': { ...BASE, outerCoreVTop: 15.0, outerCoreVBottom: 14.0, innerCoreV: 14.5, preset: 'fast-core' }, + + // ── Impossible / metamaterial cores ───────────────────────────────────── + // Ray tracer interprets model.coreModifier to apply non-standard boundary physics. + + // Acoustic metamaterial with negative effective bulk modulus and density. + // Snell's law is negated at the CMB: transmitted ray bends to the same side + // of the normal as the incident ray. Angular direction reverses inside the core. + 'neg-refraction': { ...BASE, coreModifier: 'negative-refraction', preset: 'neg-refraction' }, + + // v_core → ∞ (zero-index medium): phase velocity is infinite, so sin(θ_t) → 0 + // for all incident angles. Every transmitted ray is forced radial inside the core + // with no angular accumulation. The core acts as a perfect converging lens. + 'zero-index': { ...BASE, coreModifier: 'zero-index', preset: 'zero-index' }, + + // v(r) = v₀·(r/R_CMB): velocity → 0 at centre. Snell parameter sinI = p·v₀/R_CMB + // is constant, so rays spiral inward indefinitely — a seismic black hole. + // Inner core removed (innerCoreRadius: 0) so the v∝r profile extends to r=0. + 'spiral-trap': { ...BASE, innerCoreRadius: 0, coreModifier: 'spiral-trap', preset: 'spiral-trap' }, + + // Retrograde velocity gradient: v decreases with depth (opposite real Earth). + // outerCoreVTop (at CMB) > outerCoreVBottom (at ICB) → v/r falls going inward, + // so rays never reach a turning point and are TIR'd at the ICB velocity jump. + // The core acts as a concave mirror; the shadow zone expands. + 'retrograde': { ...BASE, outerCoreVTop: 8.0, outerCoreVBottom: 2.0, coreModifier: '', preset: 'retrograde' }, + + // All waves absorbed at the CMB — nothing penetrates the core. + // Shadow zone is the pure geometric shadow of the core disk. + 'absorber': { ...BASE, coreModifier: 'absorber', preset: 'absorber' }, }; diff --git a/src/rayTracer.ts b/src/rayTracer.ts index ac47935..e0ddc5b 100644 --- a/src/rayTracer.ts +++ b/src/rayTracer.ts @@ -36,6 +36,12 @@ export function traceRay( let theta = 0; let sign = -1; // -1 = going down + // Impossible-core modifier state + // thetaSign: +1 normal angular direction, -1 for negative-refraction inside core + // forceRadial: true inside zero-index core — ray travels radially, no θ accumulation + let thetaSign = 1; + let forceRadial = false; + const v0 = model.vAt(r, wave); if (v0 === 0) { return { points, rayParameter: p, takeoffAngle, epicentralDistance: null, travelTimeSec: null, wave, blocked: false }; @@ -62,16 +68,17 @@ export function traceRay( const sinI = p * v / r; - if (sinI >= 1) { + // For zero-index core the ray is forced radial — skip the Bouguer turning check. + if (!forceRadial && sinI >= 1) { sign = 1; } - // cosI → 0 at turning point (sinI = 1). Without a minimum, dr = 0 forever. - // A tiny minimum upward velocity lets the ray escape the turning point. const rawCosI = Math.sqrt(Math.max(0, 1 - sinI * sinI)); - const cosI = sinI >= 1 ? Math.max(rawCosI, 0.001) : rawCosI; + // Zero-index core: ray is perfectly radial regardless of ray parameter. + const cosI = forceRadial ? 1 : (sinI >= 1 ? Math.max(rawCosI, 0.001) : rawCosI); const dr = sign * cosI * STEP; - const dtheta = (p * v / (r * r)) * STEP; + // Zero-index: no angular accumulation; negative-refraction: reverse angular direction. + const dtheta = forceRadial ? 0 : thetaSign * (p * v / (r * r)) * STEP; const rNext = r + dr; const layerNow = model.layerAt(r); @@ -82,13 +89,12 @@ export function traceRay( if (layerNow !== layerNext) { const bR = sign < 0 ? layerNow.rMin : layerNow.rMax; + const isCMB = model.cmbRadius > 0 && Math.abs(bR - model.cmbRadius) < 2; const vOld = model.vAt(bR - 0.001 * sign, wave); const vNew = model.vAt(bR + 0.001 * sign, wave); - const sinIBound = p * vOld / bR; - const sinINew = sinIBound * (vNew / vOld); - + // S-wave absorbed by liquid outer core if (wave === 'S' && vNew === 0) { const fracStep = Math.abs(bR - r) / (Math.abs(dr) + 1e-10); travelTimeSec += fracStep * STEP / v; @@ -97,9 +103,38 @@ export function traceRay( break; } - if (Math.abs(sinINew) > 1) { - newSign = -sign; - newR = r + newSign * cosI * STEP; + // Perfect absorber: all waves stopped at CMB + if (isCMB && sign < 0 && model.coreModifier === 'absorber') { + const fracStep = Math.abs(bR - r) / (Math.abs(dr) + 1e-10); + travelTimeSec += fracStep * STEP / v; + points.push({ r: bR, theta: theta + dtheta * fracStep }); + blocked = true; + break; + } + + // Normal Snell's law TIR check — skipped inside zero-index core since the + // ray is perfectly radial (angle = 0) and passes through any inner boundary. + let didTIR = false; + if (!forceRadial) { + const sinIBound = p * vOld / bR; + const sinINew = sinIBound * (vNew / vOld); + if (Math.abs(sinINew) > 1) { + newSign = -sign; + newR = r + newSign * cosI * STEP; + didTIR = true; + } + } + + // CMB impossible-core modifiers (only when ray actually crosses, not TIR'd) + if (isCMB && !didTIR) { + if (model.coreModifier === 'negative-refraction') { + // Negate tangential component: flip angular direction on each CMB crossing. + thetaSign = sign < 0 ? -1 : 1; + } + if (model.coreModifier === 'zero-index') { + // Inside core: radial-only travel. Restore normal Bouguer on exit. + forceRadial = sign < 0; + } } } @@ -112,11 +147,23 @@ export function traceRay( if (r >= R_EARTH) { r = R_EARTH; points.push({ r, theta }); - epicentralDistance = (theta * 180) / Math.PI; + // Use absolute value: negative theta means the ray curved backward + // (negative refraction), but distance is always a positive angle. + epicentralDistance = Math.abs((theta * 180) / Math.PI); break; } - if (r <= 1) break; + if (r <= 1) { + if (forceRadial) { + // Zero-index core: bounce off centre and head back out radially. + sign = 1; + r = 2; + continue; + } + // Spiral-trap and other rays that reach the centre are absorbed. + blocked = true; + break; + } } return {