cosmo/frontend/src/components/Stars.tsx

124 lines
3.4 KiB
TypeScript
Raw Normal View History

2025-11-27 10:14:25 +00:00
/**
* 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>
);
}