2
This commit is contained in:
parent
c8a5aca21b
commit
ee308ed2f9
@ -1,23 +1,22 @@
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import type { ReactElement } from 'react';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import {
|
||||
mdiClose,
|
||||
mdiCamera,
|
||||
mdiTarget,
|
||||
mdiInformationOutline,
|
||||
mdiTelescope,
|
||||
mdiMagnifyPlusOutline,
|
||||
mdiMagnifyMinusOutline,
|
||||
mdiOrbitVariant,
|
||||
mdiFlare,
|
||||
mdiWeatherNight,
|
||||
mdiCrosshairsGps
|
||||
mdiCrosshairsGps,
|
||||
mdiRecord,
|
||||
mdiStop,
|
||||
mdiDownload,
|
||||
mdiAutoFix
|
||||
} from '@mdi/js';
|
||||
import BaseIcon from '../components/BaseIcon';
|
||||
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||
import { useAppDispatch } from '../stores/hooks';
|
||||
import { fetch as fetchSkyObjects } from '../stores/sky_objects/sky_objectsSlice';
|
||||
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||
|
||||
@ -58,7 +57,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', // Using a generic high-res space image placeholder
|
||||
img: 'https://images-assets.nasa.gov/image/as11-40-5874/as11-40-5874~medium.jpg',
|
||||
dist: '6,500 ly',
|
||||
temp: '15K'
|
||||
}
|
||||
@ -71,12 +70,19 @@ const ObservationPage = () => {
|
||||
const [zoom, setZoom] = useState(1);
|
||||
const [selectedTarget, setSelectedTarget] = useState<any>(null);
|
||||
const [isFocusing, setIsFocusing] = useState(false);
|
||||
const [isSharpnessMax, setIsSharpnessMax] = useState(false);
|
||||
const [telemetry, setTelemetry] = useState({
|
||||
temp: -233,
|
||||
dist: 1.5,
|
||||
focal: 131,
|
||||
});
|
||||
|
||||
// Recording states
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [mediaRecorder, setMediaRecorder] = useState<MediaRecorder | null>(null);
|
||||
const [recordedChunks, setRecordedChunks] = useState<Blob[]>([]);
|
||||
const [videoUrl, setVideoUrl] = useState<string | null>(null);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
@ -86,7 +92,11 @@ const ObservationPage = () => {
|
||||
const startCamera = async () => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: 'environment' }
|
||||
video: {
|
||||
facingMode: 'environment',
|
||||
width: { ideal: 1920 },
|
||||
height: { ideal: 1080 }
|
||||
}
|
||||
});
|
||||
if (videoRef.current) {
|
||||
videoRef.current.srcObject = stream;
|
||||
@ -103,6 +113,7 @@ const ObservationPage = () => {
|
||||
const tracks = (videoRef.current.srcObject as MediaStream).getTracks();
|
||||
tracks.forEach(track => track.stop());
|
||||
setIsCameraActive(false);
|
||||
if (isRecording) stopRecording();
|
||||
}
|
||||
};
|
||||
|
||||
@ -124,7 +135,6 @@ const ObservationPage = () => {
|
||||
setSelectedTarget(target);
|
||||
setIsFocusing(true);
|
||||
|
||||
// Animate zoom in
|
||||
let currentZoom = zoom;
|
||||
const targetZoom = 1000000;
|
||||
const interval = setInterval(() => {
|
||||
@ -139,7 +149,6 @@ const ObservationPage = () => {
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// Simulate telemetry fluctuations
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setTelemetry(prev => ({
|
||||
@ -152,10 +161,65 @@ const ObservationPage = () => {
|
||||
}, []);
|
||||
|
||||
const getFilter = () => {
|
||||
let base = 'none';
|
||||
switch (mode) {
|
||||
case 'ir': return 'contrast(1.2) brightness(1.1) hue-rotate(180deg) saturate(1.5)';
|
||||
case 'deep': return 'contrast(1.5) brightness(0.8) saturate(0.5) blur(0.5px)';
|
||||
default: return 'none';
|
||||
case 'ir': base = 'contrast(1.2) brightness(1.1) hue-rotate(180deg) saturate(1.5)'; break;
|
||||
case 'deep': base = 'contrast(1.5) brightness(0.8) saturate(0.5) blur(0.5px)'; break;
|
||||
default: base = 'none';
|
||||
}
|
||||
|
||||
if (isSharpnessMax) {
|
||||
base += ' contrast(1.4) brightness(1.1) saturate(1.2) drop-shadow(0 0 1px white)';
|
||||
}
|
||||
|
||||
return base;
|
||||
};
|
||||
|
||||
// Recording Logic
|
||||
const startRecording = () => {
|
||||
if (!videoRef.current || !videoRef.current.srcObject) return;
|
||||
|
||||
const stream = videoRef.current.srcObject as MediaStream;
|
||||
const recorder = new MediaRecorder(stream, {
|
||||
mimeType: 'video/webm'
|
||||
});
|
||||
|
||||
const chunks: Blob[] = [];
|
||||
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);
|
||||
setVideoUrl(null);
|
||||
};
|
||||
|
||||
const stopRecording = () => {
|
||||
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
|
||||
mediaRecorder.stop();
|
||||
setIsRecording(false);
|
||||
}
|
||||
};
|
||||
|
||||
const downloadVideo = () => {
|
||||
if (videoUrl) {
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = videoUrl;
|
||||
a.download = `JWST-REC-${new Date().getTime()}.webm`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(videoUrl);
|
||||
setVideoUrl(null);
|
||||
}
|
||||
};
|
||||
|
||||
@ -202,7 +266,8 @@ const ObservationPage = () => {
|
||||
style={{
|
||||
filter: getFilter(),
|
||||
transform: `scale(${1 + Math.log10(zoom)})`,
|
||||
transition: 'transform 0.5s ease-out, opacity 1.5s ease-in-out'
|
||||
transition: 'transform 0.5s ease-out, opacity 1.5s ease-in-out',
|
||||
imageRendering: isSharpnessMax ? 'crisp-edges' : 'auto'
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -213,7 +278,8 @@ const ObservationPage = () => {
|
||||
style={{
|
||||
backgroundImage: `url(${selectedTarget.img})`,
|
||||
transform: `scale(${1 + (Math.log10(zoom) - 3) / 20})`,
|
||||
filter: getFilter()
|
||||
filter: getFilter(),
|
||||
imageRendering: isSharpnessMax ? 'crisp-edges' : 'auto'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@ -251,9 +317,9 @@ const ObservationPage = () => {
|
||||
<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="flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-ping"></div>
|
||||
<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">
|
||||
{isFocusing ? 'Acquiring Target' : 'Observation Stable'}
|
||||
{isRecording ? 'REC ACTIVE' : (isFocusing ? 'Acquiring Target' : 'Observation Stable')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 grid grid-cols-2 gap-x-6 text-[10px]">
|
||||
@ -277,6 +343,43 @@ const ObservationPage = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
{/* 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 Recording"
|
||||
>
|
||||
<BaseIcon path={mdiRecord} size={20} className="text-red-500" />
|
||||
</button>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
{videoUrl && (
|
||||
<button
|
||||
onClick={downloadVideo}
|
||||
className="p-3 bg-blue-600 border border-white/20 transition-all text-white"
|
||||
title="Download Video"
|
||||
>
|
||||
<BaseIcon path={mdiDownload} size={20} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<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"
|
||||
>
|
||||
<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]"
|
||||
@ -403,4 +506,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