4
This commit is contained in:
parent
0c4c4fdf8d
commit
c7397852b7
@ -1,10 +1,10 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import Head from 'next/head';
|
||||
import {
|
||||
mdiClose,
|
||||
mdiTelescope,
|
||||
mdiMagnifyPlusOutline,
|
||||
import {
|
||||
mdiClose,
|
||||
mdiTelescope,
|
||||
mdiMagnifyPlusOutline,
|
||||
mdiMagnifyMinusOutline,
|
||||
mdiOrbitVariant,
|
||||
mdiFlare,
|
||||
@ -13,42 +13,58 @@ import {
|
||||
mdiRecord,
|
||||
mdiStop,
|
||||
mdiDownload,
|
||||
mdiAutoFix
|
||||
mdiAutoFix,
|
||||
mdiAccountGroup,
|
||||
mdiStarCircle,
|
||||
mdiChatProcessingOutline
|
||||
} from '@mdi/js';
|
||||
import BaseIcon from '../components/BaseIcon';
|
||||
import { useAppDispatch } from '../stores/hooks';
|
||||
import { fetch as fetchSkyObjects } from '../stores/sky_objects/sky_objectsSlice';
|
||||
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||
|
||||
const CELEBRITIES = [
|
||||
{ id: 'beyonce', name: 'Beyoncé', category: 'Singer', reaction: 'This view is absolutely flawless! Like a diamond in the sky.' },
|
||||
{ id: 'jackson', name: 'Michael Jackson', category: 'Singer', reaction: 'Hee-hee! Looking at the stars is a thriller!' },
|
||||
{ id: 'mercury', name: 'Freddie Mercury', category: 'Singer', reaction: 'I see a little silhouetto of a galaxy! Magnificent!' },
|
||||
{ id: 'swift', name: 'Taylor Swift', category: 'Singer', reaction: 'I can see the sparks fly in that nebula. Enchanting!' },
|
||||
{ id: 'dicaprio', name: 'Leonardo DiCaprio', category: 'Actor', reaction: 'I\'m the king of the world... or at least this telescope!' },
|
||||
{ id: 'streep', name: 'Meryl Streep', category: 'Actress', reaction: 'The performance of these celestial bodies is award-worthy.' },
|
||||
{ id: 'hanks', name: 'Tom Hanks', category: 'Actor', reaction: 'Houston, we have an incredible view here!' },
|
||||
{ id: 'davinci', name: 'Leonardo da Vinci', category: 'Painter', reaction: 'The proportions of this universe are divine. A true masterpiece.' },
|
||||
{ id: 'vangogh', name: 'Vincent van Gogh', category: 'Painter', reaction: 'The starry night... it\'s even more vibrant than I painted it!' },
|
||||
{ id: 'kahlo', name: 'Frida Kahlo', category: 'Painter', reaction: 'Feet, what do I need them for if I have wings to fly to these stars?' },
|
||||
];
|
||||
|
||||
const PRESET_TARGETS = [
|
||||
{
|
||||
id: 'mars',
|
||||
name: 'Mars',
|
||||
type: 'Planet',
|
||||
{
|
||||
id: 'mars',
|
||||
name: 'Mars',
|
||||
type: 'Planet',
|
||||
img: 'https://images-assets.nasa.gov/image/PIA04591/PIA04591~medium.jpg',
|
||||
dist: '225M km',
|
||||
temp: '210K'
|
||||
},
|
||||
{
|
||||
id: 'jupiter',
|
||||
name: 'Jupiter',
|
||||
type: 'Planet',
|
||||
{
|
||||
id: 'jupiter',
|
||||
name: 'Jupiter',
|
||||
type: 'Planet',
|
||||
img: 'https://images-assets.nasa.gov/image/PIA04866/PIA04866~medium.jpg',
|
||||
dist: '778M km',
|
||||
temp: '110K'
|
||||
},
|
||||
{
|
||||
id: 'orion',
|
||||
name: 'Orion Nebula',
|
||||
type: 'Nebula',
|
||||
{
|
||||
id: 'orion',
|
||||
name: 'Orion Nebula',
|
||||
type: 'Nebula',
|
||||
img: 'https://images-assets.nasa.gov/image/PIA08653/PIA08653~medium.jpg',
|
||||
dist: '1,344 ly',
|
||||
temp: '10,000K'
|
||||
},
|
||||
{
|
||||
id: 'andromeda',
|
||||
name: 'Andromeda',
|
||||
type: 'Galaxy',
|
||||
{
|
||||
id: 'andromeda',
|
||||
name: 'Andromeda',
|
||||
type: 'Galaxy',
|
||||
img: 'https://images-assets.nasa.gov/image/PIA15416/PIA15416~medium.jpg',
|
||||
dist: '2.5M ly',
|
||||
temp: '2.7K'
|
||||
@ -57,7 +73,7 @@ const PRESET_TARGETS = [
|
||||
id: 'pillars',
|
||||
name: 'Pillars of Creation',
|
||||
type: 'Nebula',
|
||||
img: 'https://images-assets.nasa.gov/image/as11-40-5874/as11-40-5874~medium.jpg',
|
||||
img: 'https://images-assets.nasa.gov/image/as11-40-5874/as11-40-5874~medium.jpg',
|
||||
dist: '6,500 ly',
|
||||
temp: '15K'
|
||||
}
|
||||
@ -67,7 +83,7 @@ const ObservationPage = () => {
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const [isCameraActive, setIsCameraActive] = useState(false);
|
||||
const [mode, setMode] = useState<'normal' | 'ir' | 'deep'>('normal');
|
||||
const [zoom, setZoom] = useState(1);
|
||||
const [zoom, setZoom] = useState(1);
|
||||
const [selectedTarget, setSelectedTarget] = useState<any>(null);
|
||||
const [isFocusing, setIsFocusing] = useState(false);
|
||||
const [isSharpnessMax, setIsSharpnessMax] = useState(false);
|
||||
@ -77,6 +93,13 @@ const ObservationPage = () => {
|
||||
focal: 131,
|
||||
});
|
||||
|
||||
// Simulation States
|
||||
const [audienceCount, setAudienceCount] = useState(0);
|
||||
const [isSimActive, setIsSimActive] = useState(false);
|
||||
const [activeCelebrities, setActiveCelebrities] = useState<string[]>([]);
|
||||
const [chatMessages, setChatMessages] = useState<any[]>([]);
|
||||
const [showSimPanel, setShowSimPanel] = useState(false);
|
||||
|
||||
// Recording states
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [mediaRecorder, setMediaRecorder] = useState<MediaRecorder | null>(null);
|
||||
@ -91,13 +114,13 @@ const ObservationPage = () => {
|
||||
|
||||
const startCamera = async () => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: {
|
||||
facingMode: 'environment',
|
||||
width: { ideal: 1920 },
|
||||
height: { ideal: 1080 }
|
||||
},
|
||||
audio: true // Integrate audio for synchronized recording
|
||||
audio: true
|
||||
});
|
||||
if (videoRef.current) {
|
||||
videoRef.current.srcObject = stream;
|
||||
@ -122,6 +145,52 @@ const ObservationPage = () => {
|
||||
return () => stopCamera();
|
||||
}, []);
|
||||
|
||||
// Audience Simulation Logic
|
||||
useEffect(() => {
|
||||
let interval: any;
|
||||
if (isSimActive) {
|
||||
interval = setInterval(() => {
|
||||
setAudienceCount(prev => {
|
||||
const target = 1000000;
|
||||
if (prev < target) {
|
||||
return Math.min(prev + Math.floor(Math.random() * 5000) + 1000, target);
|
||||
}
|
||||
return target;
|
||||
});
|
||||
|
||||
// Generate Chat Messages
|
||||
if (activeCelebrities.length > 0 && Math.random() > 0.7) {
|
||||
const randomCelebId = activeCelebrities[Math.floor(Math.random() * activeCelebrities.length)];
|
||||
const celeb = CELEBRITIES.find(c => c.id === randomCelebId);
|
||||
if (celeb) {
|
||||
const newMessage = {
|
||||
id: Date.now(),
|
||||
name: celeb.name,
|
||||
category: celeb.category,
|
||||
text: celeb.reaction,
|
||||
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
};
|
||||
setChatMessages(prev => [newMessage, ...prev].slice(0, 10));
|
||||
}
|
||||
}
|
||||
}, 2000);
|
||||
} else {
|
||||
setAudienceCount(0);
|
||||
setChatMessages([]);
|
||||
}
|
||||
return () => clearInterval(interval);
|
||||
}, [isSimActive, activeCelebrities]);
|
||||
|
||||
const toggleCelebrity = (id: string) => {
|
||||
setActiveCelebrities(prev =>
|
||||
prev.includes(id) ? prev.filter(c => c !== id) : [...prev, id]
|
||||
);
|
||||
};
|
||||
|
||||
const selectAllCelebrities = () => {
|
||||
setActiveCelebrities(CELEBRITIES.map(c => c.id));
|
||||
};
|
||||
|
||||
const handleZoom = (direction: 'in' | 'out') => {
|
||||
setZoom(prev => {
|
||||
if (direction === 'in') {
|
||||
@ -179,30 +248,17 @@ const ObservationPage = () => {
|
||||
// Recording Logic
|
||||
const startRecording = () => {
|
||||
if (!videoRef.current || !videoRef.current.srcObject) return;
|
||||
|
||||
const stream = videoRef.current.srcObject as MediaStream;
|
||||
|
||||
// Check for supported types with audio
|
||||
const options = { mimeType: 'video/webm;codecs=vp8,opus' };
|
||||
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
|
||||
options.mimeType = 'video/webm';
|
||||
}
|
||||
|
||||
if (!MediaRecorder.isTypeSupported(options.mimeType)) options.mimeType = 'video/webm';
|
||||
const recorder = new MediaRecorder(stream, options);
|
||||
|
||||
const chunks: Blob[] = [];
|
||||
recorder.ondataavailable = (event) => {
|
||||
if (event.data.size > 0) {
|
||||
chunks.push(event.data);
|
||||
}
|
||||
};
|
||||
|
||||
recorder.ondataavailable = (event) => { if (event.data.size > 0) chunks.push(event.data); };
|
||||
recorder.onstop = () => {
|
||||
const blob = new Blob(chunks, { type: 'video/webm' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
setVideoUrl(url);
|
||||
};
|
||||
|
||||
recorder.start();
|
||||
setIsRecording(true);
|
||||
setMediaRecorder(recorder);
|
||||
@ -221,7 +277,7 @@ const ObservationPage = () => {
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = videoUrl;
|
||||
a.download = `JWST-AV-REC-${new Date().getTime()}.webm`;
|
||||
a.download = `JWST-CELEB-REC-${new Date().getTime()}.webm`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(videoUrl);
|
||||
@ -242,7 +298,7 @@ const ObservationPage = () => {
|
||||
return (
|
||||
<div className="relative h-screen w-full bg-black overflow-hidden flex flex-col font-mono text-[#00F2FF]">
|
||||
<Head>
|
||||
<title>JWST | Deep Space Observation</title>
|
||||
<title>JWST | Global Observation Live</title>
|
||||
</Head>
|
||||
|
||||
{/* Main Viewport */}
|
||||
@ -277,7 +333,7 @@ const ObservationPage = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Deep Space High-Res Layer */}
|
||||
{/* Deep Space Layer */}
|
||||
{selectedTarget && (
|
||||
<div
|
||||
className={`absolute inset-0 w-full h-full bg-cover bg-center transition-opacity duration-1000 ${isDeepZoom ? 'opacity-100' : 'opacity-0'}`}
|
||||
@ -290,7 +346,29 @@ const ObservationPage = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Loading Overlay when Focusing */}
|
||||
{/* Audience Visual Simulation Overlay */}
|
||||
{isSimActive && audienceCount > 1000 && (
|
||||
<div className="absolute inset-0 z-10 pointer-events-none overflow-hidden opacity-30">
|
||||
{/* Heatmap/Crowd Particles Effect */}
|
||||
<div className="absolute bottom-0 w-full h-1/4 bg-gradient-to-t from-[#E3B341]/20 to-transparent"></div>
|
||||
{[...Array(20)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute bg-[#00F2FF] rounded-full blur-[2px] animate-pulse"
|
||||
style={{
|
||||
width: Math.random() * 4 + 2,
|
||||
height: Math.random() * 4 + 2,
|
||||
left: `${Math.random() * 100}%`,
|
||||
bottom: `${Math.random() * 20}%`,
|
||||
opacity: Math.random(),
|
||||
animationDelay: `${Math.random() * 5}s`
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loading Overlay */}
|
||||
{isFocusing && (
|
||||
<div className="absolute inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
@ -301,61 +379,55 @@ const ObservationPage = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* JWST Hexagonal Overlay */}
|
||||
{isCameraActive && (
|
||||
<div className="absolute inset-0 z-10 pointer-events-none opacity-20">
|
||||
<svg className="w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="none">
|
||||
<defs>
|
||||
<pattern id="hexagons" width="10" height="10" patternUnits="userSpaceOnUse" patternTransform="scale(2.5)">
|
||||
<path d="M5 0 L10 2.5 L10 7.5 L5 10 L0 7.5 L0 2.5 Z" fill="none" stroke="#E3B341" strokeWidth="0.03" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect width="100%" height="100%" fill="url(#hexagons)" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* UI Controls & Telemetry */}
|
||||
{isCameraActive && (
|
||||
<div className="absolute inset-0 z-20 p-4 flex flex-col justify-between pointer-events-none">
|
||||
{/* Top Bar */}
|
||||
<div className="flex justify-between items-start pointer-events-auto">
|
||||
<div className="bg-black/80 border-l-4 border-[#E3B341] p-4 backdrop-blur-lg">
|
||||
<div className="text-[10px] text-gray-400 uppercase tracking-tighter">Mission Control: NASA-ESA-CSA</div>
|
||||
<div className="bg-black/80 border-l-4 border-[#E3B341] p-4 backdrop-blur-lg min-w-[240px]">
|
||||
<div className="text-[10px] text-gray-400 uppercase tracking-tighter">Global Observation Stream</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className={`w-2 h-2 rounded-full ${isRecording ? 'bg-red-500 animate-pulse' : 'bg-green-500 animate-ping'}`}></div>
|
||||
<div className="font-bold text-[#E3B341] tracking-widest uppercase">
|
||||
{isRecording ? 'A/V REC ACTIVE' : (isFocusing ? 'Acquiring Target' : 'Observation Stable')}
|
||||
<div className={`w-2 h-2 rounded-full ${isRecording ? 'bg-red-500 animate-pulse' : 'bg-green-500'}`}></div>
|
||||
<div className="font-bold text-[#E3B341] tracking-widest uppercase text-xs">
|
||||
{isRecording ? 'A/V REC ACTIVE' : (isFocusing ? 'Acquiring Target' : 'LIVE TRANSMISSION')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-2 gap-x-6 text-[10px]">
|
||||
|
||||
{isSimActive && (
|
||||
<div className="mt-2 flex items-center space-x-2 bg-white/5 p-1 px-2 rounded">
|
||||
<BaseIcon path={mdiAccountGroup} size={14} className="text-[#00F2FF]" />
|
||||
<span className="text-white text-xs font-bold">{audienceCount.toLocaleString()} <span className="text-[10px] text-gray-400 font-normal">WATCHING</span></span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-3 grid grid-cols-2 gap-x-4 text-[9px]">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-gray-500">BUS TEMP</span>
|
||||
<span className="text-white text-sm">{(selectedTarget && isDeepZoom ? 6.5 : telemetry.temp).toFixed(1)}K</span>
|
||||
<span className="text-white">{(selectedTarget && isDeepZoom ? 6.5 : telemetry.temp).toFixed(1)}K</span>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-gray-500">MAGNIFICATION</span>
|
||||
<span className="text-[#00F2FF] text-sm font-bold">{formatZoom(zoom)}</span>
|
||||
</div>
|
||||
<div className="flex flex-col mt-2">
|
||||
<span className="text-gray-500">FOCAL POINT</span>
|
||||
<span className="text-white text-sm">{telemetry.focal.toFixed(1)}mm</span>
|
||||
</div>
|
||||
<div className="flex flex-col mt-2">
|
||||
<span className="text-gray-500">L2 DISTANCE</span>
|
||||
<span className="text-white text-sm">1,502,401.2 km</span>
|
||||
<span className="text-[#00F2FF] font-bold">{formatZoom(zoom)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
{/* Advanced Sim Control */}
|
||||
<button
|
||||
onClick={() => setShowSimPanel(!showSimPanel)}
|
||||
className={`p-3 bg-black/80 border transition-all ${isSimActive ? 'border-[#E3B341] text-[#E3B341]' : 'border-white/20 text-white opacity-60'}`}
|
||||
title="Advanced Simulations"
|
||||
>
|
||||
<BaseIcon path={mdiStarCircle} size={20} />
|
||||
</button>
|
||||
|
||||
{/* Recording System */}
|
||||
<div className="flex space-x-1 bg-black/60 p-1 border border-white/10">
|
||||
{!isRecording ? (
|
||||
<button
|
||||
onClick={startRecording}
|
||||
className="p-3 bg-black/80 border border-white/20 hover:bg-red-600 transition-all text-white"
|
||||
title="Start A/V Recording"
|
||||
>
|
||||
<BaseIcon path={mdiRecord} size={20} className="text-red-500" />
|
||||
</button>
|
||||
@ -363,7 +435,6 @@ const ObservationPage = () => {
|
||||
<button
|
||||
onClick={stopRecording}
|
||||
className="p-3 bg-red-600 border border-white/20 transition-all text-white animate-pulse"
|
||||
title="Stop Recording"
|
||||
>
|
||||
<BaseIcon path={mdiStop} size={20} />
|
||||
</button>
|
||||
@ -372,7 +443,6 @@ const ObservationPage = () => {
|
||||
<button
|
||||
onClick={downloadVideo}
|
||||
className="p-3 bg-blue-600 border border-white/20 transition-all text-white"
|
||||
title="Download A/V Video"
|
||||
>
|
||||
<BaseIcon path={mdiDownload} size={20} />
|
||||
</button>
|
||||
@ -381,17 +451,10 @@ const ObservationPage = () => {
|
||||
|
||||
<button
|
||||
onClick={() => setIsSharpnessMax(!isSharpnessMax)}
|
||||
className={`p-3 bg-black/80 border transition-all ${isSharpnessMax ? 'border-[#00F2FF] text-[#00F2FF] shadow-[0_0_15px_rgba(0,242,255,0.5)]' : 'border-[#E3B341]/40 text-[#E3B341]'}`}
|
||||
title="Maximum Sharpness"
|
||||
className={`p-3 bg-black/80 border transition-all ${isSharpnessMax ? 'border-[#00F2FF] text-[#00F2FF]' : 'border-white/20 text-white opacity-60'}`}
|
||||
>
|
||||
<BaseIcon path={mdiAutoFix} size={20} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setSelectedTarget(null); setZoom(1); }}
|
||||
className="p-3 bg-black/80 border border-[#E3B341]/40 hover:bg-[#E3B341]/20 transition-all text-[#E3B341]"
|
||||
>
|
||||
<BaseIcon path={mdiOrbitVariant} size={20} />
|
||||
</button>
|
||||
<button
|
||||
onClick={stopCamera}
|
||||
className="p-3 bg-black/80 border border-red-500/40 hover:bg-red-500/40 transition-all"
|
||||
@ -401,16 +464,70 @@ const ObservationPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Advanced Simulation Panel */}
|
||||
{showSimPanel && (
|
||||
<div className="absolute top-20 right-4 w-72 bg-black/95 border border-[#E3B341] p-4 backdrop-blur-xl pointer-events-auto z-50">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-[#E3B341] font-bold uppercase text-xs tracking-widest">Audience Simulation</h3>
|
||||
<button onClick={() => setShowSimPanel(false)}><BaseIcon path={mdiClose} size={16} /></button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between bg-white/5 p-2 rounded">
|
||||
<span className="text-[10px] uppercase">Live Audience (1M max)</span>
|
||||
<button
|
||||
onClick={() => setIsSimActive(!isSimActive)}
|
||||
className={`px-3 py-1 text-[9px] uppercase font-bold rounded ${isSimActive ? 'bg-red-600 text-white' : 'bg-[#00F2FF] text-black'}`}
|
||||
>
|
||||
{isSimActive ? 'Disable' : 'Enable'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-white/10 pt-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-[10px] uppercase text-gray-400">Personalities (Famous Artists)</span>
|
||||
<button onClick={selectAllCelebrities} className="text-[8px] text-[#00F2FF] uppercase underline">Select All</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 max-h-40 overflow-y-auto pr-2 custom-scrollbar">
|
||||
{CELEBRITIES.map(celeb => (
|
||||
<button
|
||||
key={celeb.id}
|
||||
onClick={() => toggleCelebrity(celeb.id)}
|
||||
className={`p-1.5 text-left text-[9px] rounded transition-all border ${activeCelebrities.includes(celeb.id) ? 'bg-[#E3B341] text-black border-[#E3B341]' : 'bg-white/5 text-white border-white/10'}`}
|
||||
>
|
||||
{celeb.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Simulation Chat / Interaction Overlay */}
|
||||
{isSimActive && chatMessages.length > 0 && (
|
||||
<div className="absolute left-4 top-1/2 -translate-y-1/2 w-64 flex flex-col space-y-2 pointer-events-none">
|
||||
{chatMessages.map(msg => (
|
||||
<div key={msg.id} className="bg-black/60 backdrop-blur-md p-2 border-l-2 border-[#E3B341] animate-fade-in-right">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-[#E3B341] font-bold text-[9px] uppercase">{msg.name}</span>
|
||||
<span className="text-[8px] text-gray-500">{msg.category}</span>
|
||||
</div>
|
||||
<p className="text-white text-[10px] leading-tight mt-1 italic">"{msg.text}"</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Zoom Controls */}
|
||||
<div className="absolute right-4 top-1/2 -translate-y-1/2 flex flex-col space-y-4 pointer-events-auto items-center">
|
||||
<div className="text-[10px] text-[#00F2FF] font-bold mb-2 uppercase rotate-90 w-20 text-center">Magnify</div>
|
||||
<button
|
||||
onClick={() => handleZoom('in')}
|
||||
className="w-12 h-12 bg-black/80 border border-[#00F2FF] flex items-center justify-center hover:bg-[#00F2FF] hover:text-black transition-all rounded-full shadow-[0_0_15px_rgba(0,242,255,0.3)]"
|
||||
>
|
||||
<BaseIcon path={mdiMagnifyPlusOutline} size={24} />
|
||||
</button>
|
||||
<div className="h-40 w-1 bg-gray-800 relative rounded-full overflow-hidden border border-white/10">
|
||||
<div className="h-40 w-1 bg-gray-800 relative rounded-full overflow-hidden">
|
||||
<div
|
||||
className="absolute bottom-0 left-0 w-full bg-[#00F2FF] transition-all duration-300"
|
||||
style={{ height: `${(Math.log10(zoom) / 14) * 100}%` }}
|
||||
@ -424,26 +541,10 @@ const ObservationPage = () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Targeting HUD */}
|
||||
{isDeepZoom && (
|
||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<div className="w-80 h-80 border border-[#00F2FF]/20 rounded-lg relative animate-pulse">
|
||||
<div className="absolute -top-1 -left-1 w-4 h-4 border-t-2 border-l-2 border-[#00F2FF]"></div>
|
||||
<div className="absolute -top-1 -right-1 w-4 h-4 border-t-2 border-r-2 border-[#00F2FF]"></div>
|
||||
<div className="absolute -bottom-1 -left-1 w-4 h-4 border-b-2 border-l-2 border-[#00F2FF]"></div>
|
||||
<div className="absolute -bottom-1 -right-1 w-4 h-4 border-b-2 border-r-2 border-[#00F2FF]"></div>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<BaseIcon path={mdiCrosshairsGps} size={32} className="text-[#00F2FF]/40" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bottom Area: Controls */}
|
||||
<div className="flex flex-col items-center space-y-6">
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
{/* Target Selector */}
|
||||
<div className="flex flex-wrap justify-center gap-2 pointer-events-auto bg-black/60 p-3 backdrop-blur-md rounded-xl border border-white/10 max-w-lg">
|
||||
<div className="w-full text-[8px] text-gray-500 uppercase tracking-widest text-center mb-1">Select Astronomical Target</div>
|
||||
{PRESET_TARGETS.map(target => (
|
||||
<button
|
||||
key={target.id}
|
||||
@ -457,7 +558,7 @@ const ObservationPage = () => {
|
||||
|
||||
{/* Sensor Modes */}
|
||||
<div className="flex justify-center space-x-6 pointer-events-auto pb-4">
|
||||
{[
|
||||
{[
|
||||
{ id: 'normal', label: 'VIS', icon: mdiWeatherNight, color: 'text-white' },
|
||||
{ id: 'ir', label: 'NIRSpec', icon: mdiFlare, color: 'text-[#E3B341]' },
|
||||
{ id: 'deep', label: 'MIRI', icon: mdiOrbitVariant, color: 'text-purple-400' }
|
||||
@ -478,32 +579,28 @@ const ObservationPage = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Target Info Overlay */}
|
||||
{selectedTarget && isDeepZoom && (
|
||||
<div className="absolute bottom-28 left-6 z-30 pointer-events-none">
|
||||
<div className="bg-black/90 border-l-4 border-[#00F2FF] p-6 backdrop-blur-2xl max-w-xs animate-fade-in shadow-[0_0_30px_rgba(0,0,0,0.5)]">
|
||||
<div className="text-[10px] text-[#00F2FF] uppercase tracking-[0.3em] font-bold">Spectral Classification</div>
|
||||
<h2 className="text-3xl font-bold text-white uppercase tracking-tighter my-1">{selectedTarget.name}</h2>
|
||||
<div className="mt-4 space-y-2 text-[10px] text-gray-400 border-t border-white/10 pt-4">
|
||||
<div className="flex justify-between">
|
||||
<span>DISTANCE</span>
|
||||
<span className="text-white font-bold">{selectedTarget.dist}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>THERMAL SIG</span>
|
||||
<span className="text-white font-bold">{selectedTarget.temp}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>COORD</span>
|
||||
<span className="text-white font-bold">RA 5h 35m 17s | Dec -5° 23' 28"</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Futuristic Scanline */}
|
||||
<div className="absolute inset-0 pointer-events-none z-40 bg-scanline opacity-10"></div>
|
||||
|
||||
<style jsx global>{`
|
||||
@keyframes fade-in-right {
|
||||
from { opacity: 0; transform: translateX(-20px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
.animate-fade-in-right {
|
||||
animation: fade-in-right 0.5s ease-out forwards;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: #E3B341;
|
||||
border-radius: 10px;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -512,4 +609,4 @@ ObservationPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||
};
|
||||
|
||||
export default ObservationPage;
|
||||
export default ObservationPage;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user