spb/satelliteindex
Public
TypeScript 66.5%
Python 30.9%
JavaScript 1.4%
CSS 0.7%
1/**2 * Geometry helpers shared by the globe: geodetic → unit-sphere coordinates (Three.js is Y-up), the compressed3 * altitude scale used for legibility, sun direction for the day/night terminator and CSS-token colour lookup.4 *5 * Altitude scale (documented in the UI tooltip): r = 1 + 0.06 + ln(1 + alt/400) × 0.12 with alt in km.6 * LEO (400 km) → 1.143 · 1 000 km → 1.21 · MEO (20 000 km) → 1.53 · GEO (35 786 km) → 1.60. It exaggerates low7 * shells so that the LEO swarm does not sit on the surface, and compresses MEO/GEO so the GEO ring stays on screen.8 */9import type { Vector3 } from 'three';1011export const EARTH_RADIUS_KM = 6371;12export const DEG = Math.PI / 180;1314export function altToRadius(altKm: number): number {15 const a = Math.max(0, altKm);16 return 1 + 0.06 + Math.log1p(a / 400) * 0.12;17}1819/** lon/lat (deg) at radius r → xyz (Y-up, prime meridian at +X, east positive). Writes into `out` (Float32Array|number[]). */20export function llaToXyz(latDeg: number, lonDeg: number, r: number, out: Float32Array | number[], off = 0): void {21 const lat = latDeg * DEG;22 const lon = lonDeg * DEG;23 const c = Math.cos(lat);24 out[off] = r * c * Math.cos(lon);25 out[off + 1] = r * Math.sin(lat);26 out[off + 2] = -r * c * Math.sin(lon);27}2829export function llaToVec(latDeg: number, lonDeg: number, r: number, v: Vector3): Vector3 {30 const lat = latDeg * DEG;31 const lon = lonDeg * DEG;32 const c = Math.cos(lat);33 return v.set(r * c * Math.cos(lon), r * Math.sin(lat), -r * c * Math.sin(lon));34}3536/**37 * Sub-solar point (approximate, ±0.5°): low-precision solar coordinates (Astronomical Almanac) + GMST.38 * Returns a unit vector in the same Y-up frame as `llaToXyz` — cheap enough to recompute every frame.39 */40export function sunDirection(date: Date, out: Vector3): Vector3 {41 const jd = date.getTime() / 86400000 + 2440587.5;42 const d = jd - 2451545.0;43 const g = ((357.529 + 0.98560028 * d) % 360) * DEG;44 const q = (280.459 + 0.98564736 * d) % 360;45 const L = (q + 1.915 * Math.sin(g) + 0.02 * Math.sin(2 * g)) * DEG;46 const e = (23.439 - 0.00000036 * d) * DEG;47 const ra = Math.atan2(Math.cos(e) * Math.sin(L), Math.cos(L));48 const dec = Math.asin(Math.sin(e) * Math.sin(L));49 const gmst = ((280.46061837 + 360.98564736629 * d) % 360) * DEG;50 const lon = ra - gmst; // sub-solar longitude (rad)51 const c = Math.cos(dec);52 return out.set(c * Math.cos(lon), Math.sin(dec), -c * Math.sin(lon)).normalize();53}5455/** Resolve a CSS custom property (design token) to a colour string usable by Three. */56export function cssVar(name: string, fallback: string): string {57 if (typeof window === 'undefined') return fallback;58 const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim();59 return v || fallback;60}6162export const TOKEN_FALLBACKS: Record<string, string> = {63 '--leo': '#38d3ff',64 '--meo': '#8f7dff',65 '--geo': '#f5b544',66 '--heo': '#ff7ab6',67 '--other': '#7c869e',68 '--accent': '#38d3ff',69 '--accent-2': '#8f7dff',70 '--ink-3': '#6b7694',71 '--rule-strong': 'rgba(160, 180, 230, 0.28)',72 '--plane': '#0b1020',73 '--plane-2': '#111830',74 '--space': '#060912',75};7677export function token(name: keyof typeof TOKEN_FALLBACKS): string {78 return cssVar(name, TOKEN_FALLBACKS[name] ?? '#ffffff');79}8081/** Detect WebGL support without creating a persistent context (headless / old GPUs / blocked GL). */82export function hasWebGL(): boolean {83 if (typeof window === 'undefined') return false;84 try {85 const c = document.createElement('canvas');86 const gl = c.getContext('webgl2') ?? c.getContext('webgl');87 if (!gl) return false;88 const ext = gl.getExtension('WEBGL_lose_context');89 ext?.loseContext();90 return true;91 } catch {92 return false;93 }94}9596export function prefersReducedMotion(): boolean {97 return typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;98}99100/** Low-power heuristic: cap rendered points on small screens / few cores. */101export function isLowPower(): boolean {102 if (typeof window === 'undefined') return false;103 const cores = navigator.hardwareConcurrency ?? 8;104 return cores <= 4 || window.innerWidth < 768;105}106