cosmo/frontend/src/components/Galaxies.tsx

202 lines
6.4 KiB
TypeScript

/**
* Galaxies component - renders distant galaxies as billboards
*/
import { useEffect, useState, useMemo } from 'react';
import { Billboard, Text, useTexture } from '@react-three/drei';
import * as THREE from 'three';
import { fetchStaticData } from '../utils/api';
interface Galaxy {
name: string;
name_zh: string;
type: string;
distance_mly: number; // Distance in millions of light years
ra: number; // Right Ascension in degrees
dec: number; // Declination in degrees
magnitude: number;
diameter_kly: number; // Diameter in thousands of light years
color: string;
}
/**
* Create a procedural galaxy texture
*/
function createGalaxyTexture(color: string, type: string): THREE.Texture {
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
const ctx = canvas.getContext('2d')!;
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const radius = canvas.width / 2;
// Create radial gradient for galaxy glow
const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius);
// Parse color
const tempColor = new THREE.Color(color);
const r = Math.floor(tempColor.r * 255);
const g = Math.floor(tempColor.g * 255);
const b = Math.floor(tempColor.b * 255);
if (type === 'spiral') {
// Spiral galaxy: bright core with arms
gradient.addColorStop(0, `rgba(255, 255, 255, 1.0)`);
gradient.addColorStop(0.1, `rgba(${r}, ${g}, ${b}, 0.9)`);
gradient.addColorStop(0.3, `rgba(${r}, ${g}, ${b}, 0.6)`);
gradient.addColorStop(0.6, `rgba(${r}, ${g}, ${b}, 0.3)`);
gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`);
} else if (type === 'irregular') {
// Irregular galaxy: more diffuse
gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 0.8)`);
gradient.addColorStop(0.4, `rgba(${r}, ${g}, ${b}, 0.5)`);
gradient.addColorStop(0.8, `rgba(${r}, ${g}, ${b}, 0.2)`);
gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`);
} else {
// Galactic center or elliptical: bright concentrated core
gradient.addColorStop(0, `rgba(255, 255, 220, 1.0)`);
gradient.addColorStop(0.2, `rgba(${r}, ${g}, ${b}, 0.9)`);
gradient.addColorStop(0.5, `rgba(${r}, ${g}, ${b}, 0.5)`);
gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`);
}
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Add some star-like points for detail (only for spiral galaxies)
if (type === 'spiral') {
ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
for (let i = 0; i < 50; i++) {
const angle = Math.random() * Math.PI * 2;
const dist = Math.random() * radius * 0.7;
const x = centerX + Math.cos(angle) * dist;
const y = centerY + Math.sin(angle) * dist;
const size = Math.random() * 1.5;
ctx.fillRect(x, y, size, size);
}
}
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
return texture;
}
/**
* Convert RA/Dec to Cartesian coordinates for distant objects
*/
function raDecToCartesian(ra: number, dec: number, distance: number) {
const raRad = (ra * Math.PI) / 180;
const decRad = (dec * Math.PI) / 180;
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);
}
/**
* Calculate visual size based on actual diameter and distance
*/
function calculateAngularSize(diameterKly: number, distanceMly: number): number {
// Angular diameter in radians
const angularDiameter = diameterKly / (distanceMly * 1000);
// Significantly increased multiplier for larger distance
return Math.max(20, angularDiameter * 8000);
}
export function Galaxies() {
const [galaxies, setGalaxies] = useState<Galaxy[]>([]);
useEffect(() => {
// Load galaxy data from API
fetchStaticData('galaxy')
.then((response) => {
// Convert API response to Galaxy format
const galaxyData = response.items.map((item) => ({
name: item.name,
name_zh: item.name_zh,
type: item.data.type,
distance_mly: item.data.distance_mly,
ra: item.data.ra,
dec: item.data.dec,
magnitude: item.data.magnitude,
diameter_kly: item.data.diameter_kly,
color: item.data.color,
}));
setGalaxies(galaxyData);
})
.catch((err) => console.error('Failed to load galaxies:', err));
}, []);
const galaxyData = useMemo(() => {
return galaxies.map((galaxy) => {
// Place galaxies on celestial sphere at fixed distance for visualization
const visualDistance = 5000; // Fixed distance for celestial sphere (was 200)
const position = raDecToCartesian(galaxy.ra, galaxy.dec, visualDistance);
// Calculate visual size based on actual properties
const size = galaxy.type === 'galactic_center'
? 80 // Much larger for Milky Way center
: calculateAngularSize(galaxy.diameter_kly, galaxy.distance_mly);
// Create procedural texture for this galaxy
const texture = createGalaxyTexture(galaxy.color, galaxy.type);
return {
...galaxy,
position,
size,
texture,
};
});
}, [galaxies]);
if (galaxyData.length === 0) {
return null;
}
return (
<group>
{galaxyData.map((galaxy) => (
<group key={galaxy.name}>
<Billboard
position={galaxy.position}
follow={true}
lockX={false}
lockY={false}
lockZ={false}
>
{/* Galaxy texture */}
<mesh>
<planeGeometry args={[galaxy.size * 3, galaxy.size * 3]} />
<meshBasicMaterial
map={galaxy.texture}
transparent
opacity={0.8}
blending={THREE.AdditiveBlending}
depthWrite={false}
/>
</mesh>
</Billboard>
{/* Galaxy name label - positioned slightly outward from galaxy */}
<Billboard position={galaxy.position.clone().multiplyScalar(1.03)}>
<Text
fontSize={60} // Increased from 1.5
color="#DDAAFF"
anchorX="center"
anchorY="middle"
outlineWidth={2}
outlineColor="#000000"
>
{galaxy.name_zh}
</Text>
</Billboard>
</group>
))}
</group>
);
}