2
This commit is contained in:
parent
c8a5aca21b
commit
ee308ed2f9
@ -1,23 +1,22 @@
|
|||||||
|
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import type { ReactElement } from 'react';
|
import type { ReactElement } from 'react';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import {
|
import {
|
||||||
mdiClose,
|
mdiClose,
|
||||||
mdiCamera,
|
|
||||||
mdiTarget,
|
|
||||||
mdiInformationOutline,
|
|
||||||
mdiTelescope,
|
mdiTelescope,
|
||||||
mdiMagnifyPlusOutline,
|
mdiMagnifyPlusOutline,
|
||||||
mdiMagnifyMinusOutline,
|
mdiMagnifyMinusOutline,
|
||||||
mdiOrbitVariant,
|
mdiOrbitVariant,
|
||||||
mdiFlare,
|
mdiFlare,
|
||||||
mdiWeatherNight,
|
mdiWeatherNight,
|
||||||
mdiCrosshairsGps
|
mdiCrosshairsGps,
|
||||||
|
mdiRecord,
|
||||||
|
mdiStop,
|
||||||
|
mdiDownload,
|
||||||
|
mdiAutoFix
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import BaseIcon from '../components/BaseIcon';
|
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 { fetch as fetchSkyObjects } from '../stores/sky_objects/sky_objectsSlice';
|
||||||
import LayoutAuthenticated from '../layouts/Authenticated';
|
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||||
|
|
||||||
@ -58,7 +57,7 @@ const PRESET_TARGETS = [
|
|||||||
id: 'pillars',
|
id: 'pillars',
|
||||||
name: 'Pillars of Creation',
|
name: 'Pillars of Creation',
|
||||||
type: 'Nebula',
|
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',
|
dist: '6,500 ly',
|
||||||
temp: '15K'
|
temp: '15K'
|
||||||
}
|
}
|
||||||
@ -71,12 +70,19 @@ const ObservationPage = () => {
|
|||||||
const [zoom, setZoom] = useState(1);
|
const [zoom, setZoom] = useState(1);
|
||||||
const [selectedTarget, setSelectedTarget] = useState<any>(null);
|
const [selectedTarget, setSelectedTarget] = useState<any>(null);
|
||||||
const [isFocusing, setIsFocusing] = useState(false);
|
const [isFocusing, setIsFocusing] = useState(false);
|
||||||
|
const [isSharpnessMax, setIsSharpnessMax] = useState(false);
|
||||||
const [telemetry, setTelemetry] = useState({
|
const [telemetry, setTelemetry] = useState({
|
||||||
temp: -233,
|
temp: -233,
|
||||||
dist: 1.5,
|
dist: 1.5,
|
||||||
focal: 131,
|
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();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -86,7 +92,11 @@ const ObservationPage = () => {
|
|||||||
const startCamera = async () => {
|
const startCamera = async () => {
|
||||||
try {
|
try {
|
||||||
const stream = await navigator.mediaDevices.getUserMedia({
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
video: { facingMode: 'environment' }
|
video: {
|
||||||
|
facingMode: 'environment',
|
||||||
|
width: { ideal: 1920 },
|
||||||
|
height: { ideal: 1080 }
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if (videoRef.current) {
|
if (videoRef.current) {
|
||||||
videoRef.current.srcObject = stream;
|
videoRef.current.srcObject = stream;
|
||||||
@ -103,6 +113,7 @@ const ObservationPage = () => {
|
|||||||
const tracks = (videoRef.current.srcObject as MediaStream).getTracks();
|
const tracks = (videoRef.current.srcObject as MediaStream).getTracks();
|
||||||
tracks.forEach(track => track.stop());
|
tracks.forEach(track => track.stop());
|
||||||
setIsCameraActive(false);
|
setIsCameraActive(false);
|
||||||
|
if (isRecording) stopRecording();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -124,7 +135,6 @@ const ObservationPage = () => {
|
|||||||
setSelectedTarget(target);
|
setSelectedTarget(target);
|
||||||
setIsFocusing(true);
|
setIsFocusing(true);
|
||||||
|
|
||||||
// Animate zoom in
|
|
||||||
let currentZoom = zoom;
|
let currentZoom = zoom;
|
||||||
const targetZoom = 1000000;
|
const targetZoom = 1000000;
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
@ -139,7 +149,6 @@ const ObservationPage = () => {
|
|||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Simulate telemetry fluctuations
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setTelemetry(prev => ({
|
setTelemetry(prev => ({
|
||||||
@ -152,10 +161,65 @@ const ObservationPage = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getFilter = () => {
|
const getFilter = () => {
|
||||||
|
let base = 'none';
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case 'ir': return 'contrast(1.2) brightness(1.1) hue-rotate(180deg) saturate(1.5)';
|
case 'ir': base = 'contrast(1.2) brightness(1.1) hue-rotate(180deg) saturate(1.5)'; break;
|
||||||
case 'deep': return 'contrast(1.5) brightness(0.8) saturate(0.5) blur(0.5px)';
|
case 'deep': base = 'contrast(1.5) brightness(0.8) saturate(0.5) blur(0.5px)'; break;
|
||||||
default: return 'none';
|
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={{
|
style={{
|
||||||
filter: getFilter(),
|
filter: getFilter(),
|
||||||
transform: `scale(${1 + Math.log10(zoom)})`,
|
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={{
|
style={{
|
||||||
backgroundImage: `url(${selectedTarget.img})`,
|
backgroundImage: `url(${selectedTarget.img})`,
|
||||||
transform: `scale(${1 + (Math.log10(zoom) - 3) / 20})`,
|
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="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="text-[10px] text-gray-400 uppercase tracking-tighter">Mission Control: NASA-ESA-CSA</div>
|
||||||
<div className="flex items-center space-x-2">
|
<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">
|
<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>
|
</div>
|
||||||
<div className="mt-3 grid grid-cols-2 gap-x-6 text-[10px]">
|
<div className="mt-3 grid grid-cols-2 gap-x-6 text-[10px]">
|
||||||
@ -277,6 +343,43 @@ const ObservationPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex space-x-2">
|
<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
|
<button
|
||||||
onClick={() => { setSelectedTarget(null); setZoom(1); }}
|
onClick={() => { setSelectedTarget(null); setZoom(1); }}
|
||||||
className="p-3 bg-black/80 border border-[#E3B341]/40 hover:bg-[#E3B341]/20 transition-all text-[#E3B341]"
|
className="p-3 bg-black/80 border border-[#E3B341]/40 hover:bg-[#E3B341]/20 transition-all text-[#E3B341]"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user