124 lines
3.4 KiB
TypeScript
124 lines
3.4 KiB
TypeScript
|
|
/**
|
||
|
|
* Stars component - renders nearby stars in 3D space
|
||
|
|
*/
|
||
|
|
import { useEffect, useState, useMemo } from 'react';
|
||
|
|
import { Text, Billboard } from '@react-three/drei';
|
||
|
|
import * as THREE from 'three';
|
||
|
|
|
||
|
|
interface Star {
|
||
|
|
name: string;
|
||
|
|
name_zh: string;
|
||
|
|
distance_ly: number;
|
||
|
|
ra: number; // Right Ascension in degrees
|
||
|
|
dec: number; // Declination in degrees
|
||
|
|
magnitude: number;
|
||
|
|
color: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Convert RA/Dec to Cartesian coordinates
|
||
|
|
* RA: Right Ascension (0-360 degrees)
|
||
|
|
* Dec: Declination (-90 to 90 degrees)
|
||
|
|
* Distance: fixed distance for celestial sphere
|
||
|
|
*/
|
||
|
|
function raDecToCartesian(ra: number, dec: number, distance: number = 150) {
|
||
|
|
// Convert to radians
|
||
|
|
const raRad = (ra * Math.PI) / 180;
|
||
|
|
const decRad = (dec * Math.PI) / 180;
|
||
|
|
|
||
|
|
// Convert to Cartesian coordinates
|
||
|
|
const x = distance * Math.cos(decRad) * Math.cos(raRad);
|
||
|
|
const y = distance * Math.cos(decRad) * Math.sin(raRad);
|
||
|
|
const z = distance * Math.sin(decRad);
|
||
|
|
|
||
|
|
return new THREE.Vector3(x, y, z);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Scale star brightness based on magnitude
|
||
|
|
* Lower magnitude = brighter star
|
||
|
|
*/
|
||
|
|
function magnitudeToSize(magnitude: number): number {
|
||
|
|
// Brighter stars (lower magnitude) should be slightly larger
|
||
|
|
// But all stars should be very small compared to planets
|
||
|
|
const normalized = Math.max(-2, Math.min(12, magnitude));
|
||
|
|
return Math.max(0.15, 0.6 - normalized * 0.04);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function Stars() {
|
||
|
|
const [stars, setStars] = useState<Star[]>([]);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
// Load star data
|
||
|
|
fetch('/data/nearby-stars.json')
|
||
|
|
.then((res) => res.json())
|
||
|
|
.then((data) => setStars(data))
|
||
|
|
.catch((err) => console.error('Failed to load stars:', err));
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const starData = useMemo(() => {
|
||
|
|
return stars.map((star) => {
|
||
|
|
// Place all stars on a celestial sphere at fixed distance (150 units)
|
||
|
|
// This way they appear as background objects, similar to constellations
|
||
|
|
const position = raDecToCartesian(star.ra, star.dec, 150);
|
||
|
|
|
||
|
|
// Size based on brightness (magnitude)
|
||
|
|
const size = magnitudeToSize(star.magnitude);
|
||
|
|
|
||
|
|
return {
|
||
|
|
...star,
|
||
|
|
position,
|
||
|
|
size,
|
||
|
|
};
|
||
|
|
});
|
||
|
|
}, [stars]);
|
||
|
|
|
||
|
|
if (starData.length === 0) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<group>
|
||
|
|
{starData.map((star) => (
|
||
|
|
<group key={star.name}>
|
||
|
|
{/* Star sphere */}
|
||
|
|
<mesh position={star.position}>
|
||
|
|
<sphereGeometry args={[star.size, 16, 16]} />
|
||
|
|
<meshBasicMaterial
|
||
|
|
color={star.color}
|
||
|
|
transparent
|
||
|
|
opacity={0.9}
|
||
|
|
blending={THREE.AdditiveBlending}
|
||
|
|
/>
|
||
|
|
</mesh>
|
||
|
|
|
||
|
|
{/* Star glow */}
|
||
|
|
<mesh position={star.position}>
|
||
|
|
<sphereGeometry args={[star.size * 2, 16, 16]} />
|
||
|
|
<meshBasicMaterial
|
||
|
|
color={star.color}
|
||
|
|
transparent
|
||
|
|
opacity={0.2}
|
||
|
|
blending={THREE.AdditiveBlending}
|
||
|
|
/>
|
||
|
|
</mesh>
|
||
|
|
|
||
|
|
{/* Star name label - positioned radially outward from star */}
|
||
|
|
<Billboard position={star.position.clone().multiplyScalar(1.05)}>
|
||
|
|
<Text
|
||
|
|
fontSize={1.2}
|
||
|
|
color="#FFFFFF"
|
||
|
|
anchorX="center"
|
||
|
|
anchorY="middle"
|
||
|
|
outlineWidth={0.08}
|
||
|
|
outlineColor="#000000"
|
||
|
|
>
|
||
|
|
{star.name_zh}
|
||
|
|
</Text>
|
||
|
|
</Billboard>
|
||
|
|
</group>
|
||
|
|
))}
|
||
|
|
</group>
|
||
|
|
);
|
||
|
|
}
|