This commit is contained in:
gpt-engineer-app[bot] 2025-12-21 13:39:53 +00:00
parent 614811167f
commit e23f6b55fb
3 changed files with 319 additions and 57 deletions

View File

@ -1,27 +1,27 @@
import { Outlet } from 'react-router-dom';
import { Outlet, useLocation } from 'react-router-dom';
import { motion } from 'framer-motion';
import Sidebar from './Sidebar';
import { MiniOscilloscope } from './MiniOscilloscope';
const MainLayout = () => {
return <motion.div initial={{
opacity: 0,
scale: 0.95
}} animate={{
opacity: 1,
scale: 1
}} transition={{
duration: 0.5
}} className="relative z-10 flex flex-col items-center pt-8 md:pt-12 px-4 w-full">
const location = useLocation();
// Don't show mini oscilloscope on the oscilloscope page itself
const showMiniOscilloscope = location.pathname !== '/oscilloscope';
return (
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }}
className="relative z-10 flex flex-col items-center pt-8 md:pt-12 px-4 w-full pb-20"
>
{/* Branding */}
<motion.h1 initial={{
opacity: 0,
y: -20
}} animate={{
opacity: 1,
y: 0
}} transition={{
delay: 0.3,
duration: 0.5
}} className="font-minecraft text-4xl md:text-5xl lg:text-6xl text-primary text-glow-strong mb-6">
<motion.h1
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3, duration: 0.5 }}
className="font-minecraft text-4xl md:text-5xl lg:text-6xl text-primary text-glow-strong mb-6"
>
<span className="inline-block translate-y-[0.35em]">~</span>$ whoami Jory
</motion.h1>
@ -34,6 +34,11 @@ const MainLayout = () => {
<Outlet />
</main>
</div>
</motion.div>;
{/* Mini Oscilloscope Bar */}
{showMiniOscilloscope && <MiniOscilloscope />}
</motion.div>
);
};
export default MainLayout;

View File

@ -0,0 +1,248 @@
import { useEffect, useRef, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useSettings } from '@/contexts/SettingsContext';
export function MiniOscilloscope() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const animationRef = useRef<number>();
const analyzerRef = useRef<AnalyserNode | null>(null);
const audioContextRef = useRef<AudioContext | null>(null);
const sourceNodeRef = useRef<MediaElementAudioSourceNode | null>(null);
const connectedElementsRef = useRef<Set<HTMLMediaElement>>(new Set());
const navigate = useNavigate();
const { playSound } = useSettings();
// Find and connect to all audio elements on the page
const connectToAudioElements = useCallback(() => {
if (!audioContextRef.current || !analyzerRef.current) return;
const audioElements = document.querySelectorAll('audio, video');
audioElements.forEach((element) => {
const mediaElement = element as HTMLMediaElement;
// Skip if already connected
if (connectedElementsRef.current.has(mediaElement)) return;
try {
// Create a source node for this element
const source = audioContextRef.current!.createMediaElementSource(mediaElement);
source.connect(analyzerRef.current!);
source.connect(audioContextRef.current!.destination);
connectedElementsRef.current.add(mediaElement);
} catch (e) {
// Element might already be connected to a different context
console.log('Could not connect audio element:', e);
}
});
}, []);
// Initialize audio context and analyzer
useEffect(() => {
const initAudio = async () => {
try {
audioContextRef.current = new AudioContext();
analyzerRef.current = audioContextRef.current.createAnalyser();
analyzerRef.current.fftSize = 256;
analyzerRef.current.smoothingTimeConstant = 0.8;
// Connect analyzer to destination for pass-through
// We'll connect sources as we find them
} catch (e) {
console.log('Could not initialize audio context:', e);
}
};
initAudio();
// Observe DOM for new audio elements
const observer = new MutationObserver(() => {
connectToAudioElements();
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
// Initial connection attempt
connectToAudioElements();
return () => {
observer.disconnect();
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
if (audioContextRef.current) {
audioContextRef.current.close();
}
};
}, [connectToAudioElements]);
// Draw waveform
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const draw = () => {
const width = canvas.width;
const height = canvas.height;
// Clear with transparent background
ctx.clearRect(0, 0, width, height);
// Draw background
ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
ctx.fillRect(0, 0, width, height);
// Draw grid lines
ctx.strokeStyle = 'hsl(120, 100%, 50%, 0.1)';
ctx.lineWidth = 1;
// Vertical grid lines
for (let x = 0; x < width; x += 20) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.stroke();
}
// Center line
ctx.strokeStyle = 'hsl(120, 100%, 50%, 0.3)';
ctx.beginPath();
ctx.moveTo(0, height / 2);
ctx.lineTo(width, height / 2);
ctx.stroke();
// Draw waveform
if (analyzerRef.current) {
const bufferLength = analyzerRef.current.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
analyzerRef.current.getByteTimeDomainData(dataArray);
// Check if there's actual audio (not just silence)
const hasAudio = dataArray.some(v => Math.abs(v - 128) > 2);
ctx.strokeStyle = 'hsl(120, 100%, 50%)';
ctx.lineWidth = 2;
ctx.shadowColor = 'hsl(120, 100%, 50%)';
ctx.shadowBlur = hasAudio ? 10 : 5;
ctx.beginPath();
const sliceWidth = width / bufferLength;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const v = dataArray[i] / 128.0;
const y = (v * height) / 2;
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
x += sliceWidth;
}
ctx.lineTo(width, height / 2);
ctx.stroke();
ctx.shadowBlur = 0;
// If no audio, draw a subtle idle animation
if (!hasAudio) {
const time = Date.now() / 1000;
ctx.strokeStyle = 'hsl(120, 100%, 50%, 0.5)';
ctx.lineWidth = 1;
ctx.beginPath();
for (let i = 0; i < width; i++) {
const y = height / 2 + Math.sin(i * 0.05 + time * 2) * 3;
if (i === 0) {
ctx.moveTo(i, y);
} else {
ctx.lineTo(i, y);
}
}
ctx.stroke();
}
} else {
// No analyzer - draw idle animation
const time = Date.now() / 1000;
ctx.strokeStyle = 'hsl(120, 100%, 50%, 0.5)';
ctx.lineWidth = 1;
ctx.beginPath();
for (let i = 0; i < width; i++) {
const y = height / 2 + Math.sin(i * 0.05 + time * 2) * 3;
if (i === 0) {
ctx.moveTo(i, y);
} else {
ctx.lineTo(i, y);
}
}
ctx.stroke();
}
animationRef.current = requestAnimationFrame(draw);
};
draw();
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, []);
// Handle resize
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const resizeCanvas = () => {
const container = canvas.parentElement;
if (container) {
canvas.width = container.clientWidth;
canvas.height = container.clientHeight;
}
};
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
return () => {
window.removeEventListener('resize', resizeCanvas);
};
}, []);
const handleClick = () => {
playSound('click');
navigate('/oscilloscope');
};
return (
<div
onClick={handleClick}
className="fixed bottom-4 left-1/2 -translate-x-1/2 w-[300px] md:w-[400px] h-[50px] z-50 cursor-pointer group"
title="Open Oscilloscope"
>
<div className="relative w-full h-full rounded-lg border border-primary/50 overflow-hidden bg-background/80 backdrop-blur-sm transition-all duration-300 group-hover:border-primary group-hover:shadow-[0_0_20px_hsl(var(--primary)/0.4)]">
<canvas
ref={canvasRef}
className="w-full h-full"
/>
{/* Hover overlay */}
<div className="absolute inset-0 flex items-center justify-center bg-primary/0 group-hover:bg-primary/10 transition-colors duration-300">
<span className="opacity-0 group-hover:opacity-100 transition-opacity duration-300 font-crt text-xs text-primary text-glow">
OPEN OSCILLOSCOPE
</span>
</div>
</div>
</div>
);
}

View File

@ -95,42 +95,6 @@ export const useOfflineVideoExport = () => {
throw new Error('No video codec supported');
}
// Create audio context for recording
const recordingAudioContext = new AudioContext();
// Resume audio context if suspended
if (recordingAudioContext.state === 'suspended') {
await recordingAudioContext.resume();
}
// Create audio source and destination
const recordingAudioSource = recordingAudioContext.createBufferSource();
recordingAudioSource.buffer = audioBuffer;
recordingAudioSource.loop = false;
const audioDestination = recordingAudioContext.createMediaStreamDestination();
recordingAudioSource.connect(audioDestination);
recordingAudioSource.connect(recordingAudioContext.destination);
// Combine video and audio streams
const combinedStream = new MediaStream();
canvas.captureStream(fps).getVideoTracks().forEach(track => combinedStream.addTrack(track));
audioDestination.stream.getAudioTracks().forEach(track => combinedStream.addTrack(track));
console.log(`✅ Combined stream: ${combinedStream.getVideoTracks().length} video, ${combinedStream.getAudioTracks().length} audio tracks`);
const recorder = new MediaRecorder(combinedStream, {
mimeType: selectedCodec,
videoBitsPerSecond: videoBitsPerSecond,
});
console.log('✅ MediaRecorder created with audio and video');
recorder.start(1000); // 1 second chunks
// Start audio playback synchronized with recording
recordingAudioSource.start(0);
console.log('🔊 Audio playback started for recording');
// Use real audio data if available, otherwise generate mock data
let audioBuffer: AudioBuffer;
let sampleRate: number;
@ -171,6 +135,51 @@ export const useOfflineVideoExport = () => {
console.log(`📊 Using mock audio: ${duration.toFixed(1)}s, ${totalSamples} samples`);
}
// Create audio context for recording
const recordingAudioContext = new AudioContext();
// Resume audio context if suspended
if (recordingAudioContext.state === 'suspended') {
await recordingAudioContext.resume();
}
// Create audio source and destination
const recordingAudioSource = recordingAudioContext.createBufferSource();
recordingAudioSource.buffer = audioBuffer;
recordingAudioSource.loop = false;
const audioDestination = recordingAudioContext.createMediaStreamDestination();
recordingAudioSource.connect(audioDestination);
recordingAudioSource.connect(recordingAudioContext.destination);
// Combine video and audio streams
const combinedStream = new MediaStream();
canvas.captureStream(fps).getVideoTracks().forEach(track => combinedStream.addTrack(track));
audioDestination.stream.getAudioTracks().forEach(track => combinedStream.addTrack(track));
console.log(`✅ Combined stream: ${combinedStream.getVideoTracks().length} video, ${combinedStream.getAudioTracks().length} audio tracks`);
// Chunks array to collect recorded data
const chunks: Blob[] = [];
const recorder = new MediaRecorder(combinedStream, {
mimeType: selectedCodec,
videoBitsPerSecond: videoBitsPerSecond,
});
recorder.ondataavailable = (e) => {
if (e.data.size > 0) {
chunks.push(e.data);
}
};
console.log('✅ MediaRecorder created with audio and video');
recorder.start(1000); // 1 second chunks
// Start audio playback synchronized with recording
recordingAudioSource.start(0);
console.log('🔊 Audio playback started for recording');
// Generate animation frames for full audio duration
const totalFrames = Math.ceil(duration * fps);
const samplesPerFrame = Math.min(qualityConfig.samplesPerFrame, Math.floor(totalSamples / totalFrames));