121 lines
4.5 KiB
TypeScript
121 lines
4.5 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react'
|
|
import { Canvas } from '@react-three/fiber'
|
|
import { OrbitControls } from '@react-three/drei'
|
|
import ThreeGlobe from 'three-globe'
|
|
import { Color } from 'three'
|
|
|
|
type Arc = {
|
|
order: number
|
|
startLat: number; startLng: number
|
|
endLat: number; endLng: number
|
|
arcAlt: number; color: string
|
|
}
|
|
|
|
const COLORS = ['#22ade0', '#3b82f6', '#CD9E53']
|
|
|
|
/* steel-trade arcs centred on Tehran/Shanghai */
|
|
const ARCS: Arc[] = [
|
|
{ order: 1, startLat: 35.69, startLng: 51.39, endLat: 31.23, endLng: 121.47, arcAlt: 0.3, color: COLORS[0] },
|
|
{ order: 1, startLat: 35.69, startLng: 51.39, endLat: 33.31, endLng: 44.36, arcAlt: 0.1, color: COLORS[2] },
|
|
{ order: 2, startLat: 35.69, startLng: 51.39, endLat: 51.92, endLng: 4.47, arcAlt: 0.3, color: COLORS[1] },
|
|
{ order: 2, startLat: 31.23, startLng: 121.47, endLat: 35.69, endLng: 51.39, arcAlt: 0.2, color: COLORS[0] },
|
|
{ order: 3, startLat: 25.20, startLng: 55.27, endLat: 35.69, endLng: 51.39, arcAlt: 0.2, color: COLORS[2] },
|
|
{ order: 3, startLat: 19.07, startLng: 72.88, endLat: 35.69, endLng: 51.39, arcAlt: 0.3, color: COLORS[1] },
|
|
{ order: 4, startLat: 41.01, startLng: 28.97, endLat: 35.69, endLng: 51.39, arcAlt: 0.2, color: COLORS[0] },
|
|
{ order: 4, startLat: -23.55, startLng: -46.63, endLat: 31.23, endLng: 121.47, arcAlt: 0.5, color: COLORS[2] },
|
|
{ order: 5, startLat: 55.75, startLng: 37.62, endLat: 35.69, endLng: 51.39, arcAlt: 0.2, color: COLORS[1] },
|
|
{ order: 5, startLat: 37.56, startLng: 126.97, endLat: 31.23, endLng: 121.47, arcAlt: 0.2, color: COLORS[0] },
|
|
]
|
|
|
|
function GlobeObject({ countries }: { countries: { features: object[] } }) {
|
|
const globe = useMemo(() => {
|
|
const g = new ThreeGlobe()
|
|
.hexPolygonsData(countries.features)
|
|
.hexPolygonResolution(3)
|
|
.hexPolygonMargin(0.62)
|
|
.hexPolygonUseDots(true)
|
|
.hexPolygonColor(() => 'rgba(150,200,255,0.95)')
|
|
.showAtmosphere(true)
|
|
.atmosphereColor('#7cc4ff')
|
|
.atmosphereAltitude(0.22)
|
|
|
|
g.arcsData(ARCS)
|
|
.arcStartLat((d: object) => (d as Arc).startLat)
|
|
.arcStartLng((d: object) => (d as Arc).startLng)
|
|
.arcEndLat((d: object) => (d as Arc).endLat)
|
|
.arcEndLng((d: object) => (d as Arc).endLng)
|
|
.arcColor((d: object) => (d as Arc).color)
|
|
.arcAltitude((d: object) => (d as Arc).arcAlt)
|
|
.arcStroke(0.5)
|
|
.arcDashLength(0.9)
|
|
.arcDashGap(4)
|
|
.arcDashInitialGap((d: object) => (d as Arc).order)
|
|
.arcDashAnimateTime(1400)
|
|
|
|
const points = ARCS.flatMap((a) => [
|
|
{ lat: a.startLat, lng: a.startLng, color: a.color },
|
|
{ lat: a.endLat, lng: a.endLng, color: a.color },
|
|
])
|
|
g.pointsData(points)
|
|
.pointColor((d: object) => (d as { color: string }).color)
|
|
.pointAltitude(0)
|
|
.pointRadius(0.55)
|
|
.pointsMerge(true)
|
|
|
|
// material → lighter, distinguishable blue sphere
|
|
const mat = g.globeMaterial() as unknown as {
|
|
color: Color; emissive: Color; emissiveIntensity: number; shininess: number
|
|
}
|
|
mat.color = new Color('#1b386e')
|
|
mat.emissive = new Color('#23508f')
|
|
mat.emissiveIntensity = 0.45
|
|
mat.shininess = 0.6
|
|
|
|
return g
|
|
}, [countries])
|
|
|
|
return <primitive object={globe} />
|
|
}
|
|
|
|
export function World() {
|
|
const [countries, setCountries] = useState<{ features: object[] } | null>(null)
|
|
|
|
useEffect(() => {
|
|
let alive = true
|
|
fetch('/globe-countries.geojson')
|
|
.then((r) => r.json())
|
|
.then((geo) => { if (alive) setCountries(geo) })
|
|
.catch(() => {})
|
|
return () => { alive = false }
|
|
}, [])
|
|
|
|
return (
|
|
<Canvas camera={{ fov: 50, near: 180, far: 1800, position: [0, 0, 320] }} gl={{ alpha: true, antialias: true }}>
|
|
<ambientLight color="#dce9ff" intensity={1.6} />
|
|
<directionalLight color="#ffffff" position={[-400, 200, 400]} intensity={1.6} />
|
|
<directionalLight color="#6cc4ff" position={[300, 200, 200]} intensity={1} />
|
|
<pointLight color="#ffffff" position={[-200, 500, 200]} intensity={1.1} />
|
|
{countries && <GlobeObject countries={countries} />}
|
|
<OrbitControls
|
|
enablePan={false}
|
|
enableZoom={false}
|
|
minDistance={320}
|
|
maxDistance={320}
|
|
autoRotate
|
|
autoRotateSpeed={0.6}
|
|
minPolarAngle={Math.PI / 3.5}
|
|
maxPolarAngle={Math.PI - Math.PI / 3}
|
|
/>
|
|
</Canvas>
|
|
)
|
|
}
|
|
|
|
/* default wrapper sized to fit the section */
|
|
export default function Globe({ size = 600 }: { size?: number }) {
|
|
return (
|
|
<div style={{ width: '100%', maxWidth: size, aspectRatio: '1', margin: '0 auto', position: 'relative' }}>
|
|
<World />
|
|
</div>
|
|
)
|
|
}
|