spb/satelliteindex
Public
TypeScript 66.5%
Python 30.9%
JavaScript 1.4%
CSS 0.7%
1'use client';2/**3 * Vector Earth: dark sphere shaded by the real sun direction (day/night terminator), fresnel atmosphere, and4 * Natural Earth 110 m land + country outlines drawn as GPU line segments (no raster texture — a premium, data-first look).5 */6import { useFrame } from '@react-three/fiber';7import { useMemo, useRef } from 'react';8import * as THREE from 'three';9import { mesh as topoMesh } from 'topojson-client';10import type { Topology } from 'topojson-specification';11import countries110 from 'world-atlas/countries-110m.json';12import land110 from 'world-atlas/land-110m.json';13import { llaToXyz, prefersReducedMotion, sunDirection, token } from './geo';1415const R_LAND = 1.003;1617type MultiLine = { type: string; coordinates: number[][][] };1819function linesToSegments(ml: MultiLine, r: number): Float32Array {20 let segs = 0;21 for (const line of ml.coordinates) segs += Math.max(0, line.length - 1);22 const out = new Float32Array(segs * 6);23 let o = 0;24 const a = new Float32Array(3);25 const b = new Float32Array(3);26 for (const line of ml.coordinates) {27 for (let i = 0; i < line.length - 1; i++) {28 const p = line[i]!;29 const q = line[i + 1]!;30 llaToXyz(p[1] ?? 0, p[0] ?? 0, r, a);31 llaToXyz(q[1] ?? 0, q[0] ?? 0, r, b);32 out.set(a, o);33 out.set(b, o + 3);34 o += 6;35 }36 }37 return out;38}3940function graticule(stepDeg: number, r: number): Float32Array {41 const pts: number[] = [];42 const v = new Float32Array(3);43 const push = (lat: number, lon: number) => {44 llaToXyz(lat, lon, r, v);45 pts.push(v[0]!, v[1]!, v[2]!);46 };47 for (let lon = -180; lon < 180; lon += stepDeg) {48 for (let lat = -90; lat < 90; lat += 3) {49 push(lat, lon);50 push(lat + 3, lon);51 }52 }53 for (let lat = -60; lat <= 60; lat += stepDeg) {54 for (let lon = -180; lon < 180; lon += 3) {55 push(lat, lon);56 push(lat, lon + 3);57 }58 }59 return new Float32Array(pts);60}6162function lineGeometry(arr: Float32Array): THREE.BufferGeometry {63 const g = new THREE.BufferGeometry();64 g.setAttribute('position', new THREE.BufferAttribute(arr, 3));65 return g;66}6768const EARTH_VERT = /* glsl */ `69 varying vec3 vNormalW;70 varying vec3 vPosW;71 void main() {72 vNormalW = normalize(mat3(modelMatrix) * normal);73 vPosW = (modelMatrix * vec4(position, 1.0)).xyz;74 gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);75 }76`;77const EARTH_FRAG = /* glsl */ `78 uniform vec3 uSun;79 uniform vec3 uNight;80 uniform vec3 uDay;81 uniform vec3 uRim;82 varying vec3 vNormalW;83 varying vec3 vPosW;84 void main() {85 vec3 n = normalize(vNormalW);86 float l = dot(n, uSun);87 float day = smoothstep(-0.12, 0.28, l);88 vec3 col = mix(uNight, uDay, day);89 vec3 viewDir = normalize(cameraPosition - vPosW);90 float rim = pow(1.0 - max(dot(n, viewDir), 0.0), 3.0);91 col += uRim * rim * 0.35;92 gl_FragColor = vec4(col, 1.0);93 }94`;95const ATMO_VERT = /* glsl */ `96 varying vec3 vNormalW;97 varying vec3 vPosW;98 void main() {99 vNormalW = normalize(mat3(modelMatrix) * normal);100 vPosW = (modelMatrix * vec4(position, 1.0)).xyz;101 gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);102 }103`;104const ATMO_FRAG = /* glsl */ `105 uniform vec3 uColor;106 varying vec3 vNormalW;107 varying vec3 vPosW;108 void main() {109 vec3 viewDir = normalize(cameraPosition - vPosW);110 float f = pow(max(dot(normalize(vNormalW), viewDir), 0.0), 2.6);111 gl_FragColor = vec4(uColor, f * 0.55);112 }113`;114115export function Earth({ quality = 'high' }: { quality?: 'high' | 'low' }) {116 const sunRef = useRef(new THREE.Vector3(1, 0, 0));117 const lastSun = useRef(0);118 const reduced = prefersReducedMotion();119120 const { landGeo, borderGeo, gratGeo, earthMat, atmoMat, landColor, borderColor, gratColor } = useMemo(() => {121 const landTopo = land110 as unknown as Topology;122 const cTopo = countries110 as unknown as Topology;123 const land = topoMesh(landTopo, landTopo.objects.land as never) as unknown as MultiLine;124 const borders = topoMesh(cTopo, cTopo.objects.countries as never, ((a: { id: string }, b: { id: string }) => a !== b) as never) as unknown as MultiLine;125 const accent = new THREE.Color(token('--accent'));126 const night = new THREE.Color(token('--plane')).multiplyScalar(0.55);127 const day = new THREE.Color(token('--plane-3')).lerp(new THREE.Color(token('--accent')), 0.05);128 const rim = new THREE.Color(token('--accent')).multiplyScalar(0.6);129 return {130 landGeo: lineGeometry(linesToSegments(land, R_LAND)),131 borderGeo: lineGeometry(linesToSegments(borders, R_LAND)),132 gratGeo: lineGeometry(graticule(30, 1.001)),133 earthMat: new THREE.ShaderMaterial({134 vertexShader: EARTH_VERT,135 fragmentShader: EARTH_FRAG,136 uniforms: { uSun: { value: sunRef.current }, uNight: { value: night }, uDay: { value: day }, uRim: { value: rim } },137 }),138 atmoMat: new THREE.ShaderMaterial({139 vertexShader: ATMO_VERT,140 fragmentShader: ATMO_FRAG,141 uniforms: { uColor: { value: accent } },142 transparent: true,143 side: THREE.BackSide,144 depthWrite: false,145 blending: THREE.AdditiveBlending,146 }),147 landColor: new THREE.Color(token('--accent')).lerp(new THREE.Color('#ffffff'), 0.25),148 borderColor: new THREE.Color(token('--ink-3')),149 gratColor: new THREE.Color(token('--ink-3')),150 };151 }, []);152153 useFrame(() => {154 const now = Date.now();155 if (now - lastSun.current > (reduced ? 60_000 : 1_000)) {156 lastSun.current = now;157 sunDirection(new Date(now), sunRef.current);158 }159 });160161 const seg = quality === 'high' ? 96 : 48;162 return (163 <group>164 <mesh material={earthMat} renderOrder={0}>165 <sphereGeometry args={[1, seg, seg / 2]} />166 </mesh>167 <mesh material={atmoMat} renderOrder={1} scale={1.045}>168 <sphereGeometry args={[1, 64, 32]} />169 </mesh>170 <lineSegments geometry={gratGeo} renderOrder={2}>171 <lineBasicMaterial color={gratColor} transparent opacity={0.16} depthWrite={false} />172 </lineSegments>173 <lineSegments geometry={landGeo} renderOrder={3}>174 <lineBasicMaterial color={landColor} transparent opacity={0.85} depthWrite={false} />175 </lineSegments>176 <lineSegments geometry={borderGeo} renderOrder={3}>177 <lineBasicMaterial color={borderColor} transparent opacity={0.35} depthWrite={false} />178 </lineSegments>179 {/* Equator hairline helps read the GEO ring geometry */}180 <mesh rotation={[Math.PI / 2, 0, 0]} renderOrder={2}>181 <ringGeometry args={[1.0005, 1.0035, 128]} />182 <meshBasicMaterial color={gratColor} transparent opacity={0.25} side={THREE.DoubleSide} depthWrite={false} />183 </mesh>184 </group>185 );186}187