/** * Nebulae component - renders nebulae as billboards with procedural textures */ import { useEffect, useState, useMemo } from 'react'; import { Billboard, Text } from '@react-three/drei'; import * as THREE from 'three'; import { fetchStaticData } from '../utils/api'; interface Nebula { name: string; name_zh: string; type: string; // emission, planetary, supernova_remnant, dark distance_ly: number; // Distance in light years ra: number; // Right Ascension in degrees dec: number; // Declination in degrees magnitude: number; diameter_ly: number; // Diameter in light years color: string; } /** * Create a procedural nebula texture based on type */ function createNebulaTexture(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; // 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 === 'emission') { // Emission nebulae: bright, colorful, wispy const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius); gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 0.9)`); gradient.addColorStop(0.3, `rgba(${r}, ${g}, ${b}, 0.7)`); gradient.addColorStop(0.6, `rgba(${r}, ${g}, ${b}, 0.4)`); gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`); ctx.fillStyle = gradient; ctx.fillRect(0, 0, canvas.width, canvas.height); // Add wispy clouds for (let i = 0; i < 30; i++) { const angle = Math.random() * Math.PI * 2; const dist = Math.random() * radius * 0.8; const x = centerX + Math.cos(angle) * dist; const y = centerY + Math.sin(angle) * dist; const cloudGradient = ctx.createRadialGradient(x, y, 0, x, y, 15); cloudGradient.addColorStop(0, `rgba(${r + 50}, ${g + 50}, ${b + 50}, 0.3)`); cloudGradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`); ctx.fillStyle = cloudGradient; ctx.fillRect(x - 15, y - 15, 30, 30); } } else if (type === 'planetary') { // Planetary nebulae: ring-like or spherical structure const innerRadius = radius * 0.3; const outerRadius = radius * 0.9; // Draw ring for (let r_val = innerRadius; r_val < outerRadius; r_val += 1) { const alpha = 1 - ((r_val - innerRadius) / (outerRadius - innerRadius)); ctx.strokeStyle = `rgba(${r}, ${g}, ${b}, ${alpha * 0.6})`; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(centerX, centerY, r_val, 0, Math.PI * 2); ctx.stroke(); } // Add central star const starGradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, 10); starGradient.addColorStop(0, `rgba(255, 255, 255, 1.0)`); starGradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0.5)`); ctx.fillStyle = starGradient; ctx.beginPath(); ctx.arc(centerX, centerY, 10, 0, Math.PI * 2); ctx.fill(); } else if (type === 'supernova_remnant') { // Supernova remnants: filamentary, expanding shell const gradient = ctx.createRadialGradient(centerX, centerY, radius * 0.3, centerX, centerY, radius); gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 0)`); gradient.addColorStop(0.5, `rgba(${r}, ${g}, ${b}, 0.8)`); gradient.addColorStop(0.8, `rgba(${r}, ${g}, ${b}, 0.4)`); gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`); ctx.fillStyle = gradient; ctx.fillRect(0, 0, canvas.width, canvas.height); // Add filaments for (let i = 0; i < 50; i++) { const angle = Math.random() * Math.PI * 2; const dist = radius * 0.5 + Math.random() * radius * 0.4; const x = centerX + Math.cos(angle) * dist; const y = centerY + Math.sin(angle) * dist; const length = Math.random() * 20 + 5; ctx.strokeStyle = `rgba(${r + 50}, ${g + 50}, ${b + 50}, ${Math.random() * 0.6})`; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + Math.cos(angle) * length, y + Math.sin(angle) * length); ctx.stroke(); } } else if (type === 'dark') { // Dark nebulae: darker silhouette against background const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius); gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 0.6)`); gradient.addColorStop(0.5, `rgba(${r}, ${g}, ${b}, 0.4)`); gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`); ctx.fillStyle = gradient; ctx.fillRect(0, 0, canvas.width, canvas.height); // Add darker patches for (let i = 0; i < 15; i++) { const x = centerX + (Math.random() - 0.5) * radius; const y = centerY + (Math.random() - 0.5) * radius; const patchGradient = ctx.createRadialGradient(x, y, 0, x, y, 20); patchGradient.addColorStop(0, `rgba(${r * 0.5}, ${g * 0.5}, ${b * 0.5}, 0.5)`); patchGradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`); ctx.fillStyle = patchGradient; ctx.fillRect(x - 20, y - 20, 40, 40); } } const texture = new THREE.CanvasTexture(canvas); texture.needsUpdate = true; return texture; } /** * Convert RA/Dec to Cartesian coordinates for celestial sphere */ 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 angular diameter * Nebulae are measured in light years of diameter and distance */ function calculateAngularSize(diameterLy: number, distanceLy: number): number { // Angular diameter in radians const angularDiameter = diameterLy / distanceLy; // Scale for visualization (nebulae should be visible but not too large) return Math.max(1.5, Math.min(8, angularDiameter * 3000)); } export function Nebulae() { const [nebulae, setNebulae] = useState([]); useEffect(() => { // Load nebula data from API fetchStaticData('nebula') .then((response) => { // Convert API response to Nebula format const nebulaData = response.items.map((item) => ({ name: item.name, name_zh: item.name_zh, type: item.data.type, distance_ly: item.data.distance_ly, ra: item.data.ra, dec: item.data.dec, magnitude: item.data.magnitude, diameter_ly: item.data.diameter_ly, color: item.data.color, })); setNebulae(nebulaData); }) .catch((err) => console.error('Failed to load nebulae:', err)); }, []); const nebulaData = useMemo(() => { return nebulae.map((nebula) => { // Place nebulae on celestial sphere at fixed distance for visualization const visualDistance = 150; // Between constellations (100) and galaxies (200) const position = raDecToCartesian(nebula.ra, nebula.dec, visualDistance); // Calculate visual size based on angular diameter const size = calculateAngularSize(nebula.diameter_ly, nebula.distance_ly); // Create procedural texture for this nebula const texture = createNebulaTexture(nebula.color, nebula.type); return { ...nebula, position, size, texture, }; }); }, [nebulae]); if (nebulaData.length === 0) { return null; } return ( {nebulaData.map((nebula) => ( {/* Nebula texture */} {/* Nebula name label - positioned slightly outward */} {nebula.name_zh} ))} ); }