Clarify oscilloscope modes

- Separate live display controls from export display mode
- Allow live visualization options (line thickness, grid, glow) to affect the real-time graph
- Update Oscilloscope to pass new liveSettings to panels and display components
- Fix duplicate variable declarations in OscilloscopeDisplay drawFrame logic

X-Lovable-Edit-ID: edt-fc124d7f-b9d9-4269-96b8-0925004fe070
This commit is contained in:
gpt-engineer-app[bot] 2025-12-21 14:24:16 +00:00
commit cc2612918c
3 changed files with 168 additions and 28 deletions

View File

@ -5,6 +5,13 @@ import { Progress } from '@/components/ui/progress';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import type { OscilloscopeMode } from '@/hooks/useOscilloscopeRenderer'; import type { OscilloscopeMode } from '@/hooks/useOscilloscopeRenderer';
export interface LiveDisplaySettings {
lineThickness: number;
showGrid: boolean;
glowIntensity: number;
displayMode: OscilloscopeMode;
}
interface ControlPanelProps { interface ControlPanelProps {
mode: OscilloscopeMode; mode: OscilloscopeMode;
onModeChange: (mode: OscilloscopeMode) => void; onModeChange: (mode: OscilloscopeMode) => void;
@ -29,6 +36,8 @@ interface ControlPanelProps {
onExportFormatChange: (format: string) => void; onExportFormatChange: (format: string) => void;
exportQuality: string; exportQuality: string;
onExportQualityChange: (quality: string) => void; onExportQualityChange: (quality: string) => void;
liveSettings: LiveDisplaySettings;
onLiveSettingsChange: (settings: LiveDisplaySettings) => void;
} }
export function ControlPanel({ export function ControlPanel({
@ -55,36 +64,97 @@ export function ControlPanel({
onExportFormatChange, onExportFormatChange,
exportQuality, exportQuality,
onExportQualityChange, onExportQualityChange,
liveSettings,
onLiveSettingsChange,
}: ControlPanelProps) { }: ControlPanelProps) {
return ( return (
<div className="flex flex-col gap-6 p-6 bg-card border border-border rounded-lg"> <div className="flex flex-col gap-6 p-6 bg-card border border-border rounded-lg">
{/* Mode Selection */} {/* Live Display Options */}
<div className="space-y-3"> <div className="space-y-3">
<Label className="font-crt text-lg text-primary text-glow">DISPLAY MODE</Label> <Label className="font-crt text-lg text-primary text-glow">LIVE DISPLAY</Label>
{/* Display Mode */}
<RadioGroup <RadioGroup
value={mode} value={liveSettings.displayMode}
onValueChange={(value) => onModeChange(value as OscilloscopeMode)} onValueChange={(value) => onLiveSettingsChange({ ...liveSettings, displayMode: value as OscilloscopeMode })}
className="space-y-2" className="space-y-2"
> >
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<RadioGroupItem value="combined" id="combined" className="border-primary" /> <RadioGroupItem value="combined" id="live-combined" className="border-primary" />
<Label htmlFor="combined" className="font-mono-crt text-sm cursor-pointer"> <Label htmlFor="live-combined" className="font-mono-crt text-sm cursor-pointer">
Combined (L+R merged) Combined (L+R merged)
</Label> </Label>
</div> </div>
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<RadioGroupItem value="separate" id="separate" className="border-primary" /> <RadioGroupItem value="separate" id="live-separate" className="border-primary" />
<Label htmlFor="separate" className="font-mono-crt text-sm cursor-pointer"> <Label htmlFor="live-separate" className="font-mono-crt text-sm cursor-pointer">
Separate (L/R stacked) Separate (L/R stacked)
</Label> </Label>
</div> </div>
<div className="flex items-center space-x-3"> <div className="flex items-center space-x-3">
<RadioGroupItem value="all" id="all" className="border-primary" /> <RadioGroupItem value="all" id="live-all" className="border-primary" />
<Label htmlFor="all" className="font-mono-crt text-sm cursor-pointer"> <Label htmlFor="live-all" className="font-mono-crt text-sm cursor-pointer">
All (L/R + XY below) All (L/R + XY below)
</Label> </Label>
</div> </div>
</RadioGroup> </RadioGroup>
{/* Line Thickness */}
<div className="flex items-center justify-between">
<span className="font-mono-crt text-sm text-foreground/90">Line Thickness</span>
<div className="flex gap-1">
{[1, 2, 3, 4].map((thickness) => (
<button
key={thickness}
onClick={() => onLiveSettingsChange({ ...liveSettings, lineThickness: thickness })}
className={`px-2 py-1 text-xs font-mono-crt border transition-all duration-300 ${
liveSettings.lineThickness === thickness
? 'border-primary text-primary bg-primary/10'
: 'border-primary/50 text-primary/70 hover:border-primary hover:text-primary'
}`}
>
{thickness}px
</button>
))}
</div>
</div>
{/* Show Grid */}
<div className="flex items-center justify-between">
<span className="font-mono-crt text-sm text-foreground/90">Show Grid</span>
<button
onClick={() => onLiveSettingsChange({ ...liveSettings, showGrid: !liveSettings.showGrid })}
className={`w-12 h-6 rounded-full border border-primary transition-all duration-300 ${
liveSettings.showGrid ? 'bg-primary' : 'bg-transparent'
}`}
>
<div
className={`w-4 h-4 rounded-full bg-background border border-primary transition-transform duration-300 ${
liveSettings.showGrid ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</div>
{/* Glow Intensity */}
<div className="flex items-center justify-between">
<span className="font-mono-crt text-sm text-foreground/90">Glow</span>
<div className="flex gap-1">
{[0, 1, 2, 3].map((glow) => (
<button
key={glow}
onClick={() => onLiveSettingsChange({ ...liveSettings, glowIntensity: glow })}
className={`px-2 py-1 text-xs font-mono-crt border transition-all duration-300 ${
liveSettings.glowIntensity === glow
? 'border-primary text-primary bg-primary/10'
: 'border-primary/50 text-primary/70 hover:border-primary hover:text-primary'
}`}
>
{glow === 0 ? 'Off' : glow}
</button>
))}
</div>
</div>
</div> </div>
{/* Playback Controls */} {/* Playback Controls */}
@ -141,8 +211,23 @@ export function ControlPanel({
</Button> </Button>
{/* Export Options */} {/* Export Options */}
<div className="space-y-3"> <div className="space-y-3 pt-4 border-t border-border">
<Label className="font-crt text-lg text-primary text-glow">EXPORT OPTIONS</Label> <Label className="font-crt text-lg text-primary text-glow">EXPORT OPTIONS</Label>
<p className="font-mono-crt text-xs text-muted-foreground">Settings for video export only</p>
{/* Export Display Mode */}
<div className="flex items-center justify-between">
<span className="font-mono-crt text-sm text-foreground/90">Mode</span>
<select
value={mode}
onChange={(e) => onModeChange(e.target.value as OscilloscopeMode)}
className="bg-background border border-primary/50 text-primary font-mono-crt text-sm px-2 py-1"
>
<option value="combined">Combined</option>
<option value="separate">Separate</option>
<option value="all">All</option>
</select>
</div>
{/* Resolution */} {/* Resolution */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">

View File

@ -1,6 +1,6 @@
import { useState, useCallback, useEffect, useRef } from 'react'; import { useState, useCallback, useEffect, useRef } from 'react';
import { AudioUploader } from './AudioUploader'; import { AudioUploader } from './AudioUploader';
import { ControlPanel } from './ControlPanel'; import { ControlPanel, LiveDisplaySettings } from './ControlPanel';
import { OscilloscopeDisplay } from './OscilloscopeDisplay'; import { OscilloscopeDisplay } from './OscilloscopeDisplay';
import { useAudioAnalyzer } from '@/hooks/useAudioAnalyzer'; import { useAudioAnalyzer } from '@/hooks/useAudioAnalyzer';
import { useOscilloscopeRenderer } from '@/hooks/useOscilloscopeRenderer'; import { useOscilloscopeRenderer } from '@/hooks/useOscilloscopeRenderer';
@ -11,6 +11,12 @@ import { Button } from '@/components/ui/button';
export function Oscilloscope() { export function Oscilloscope() {
const [mode, setMode] = useState<'combined' | 'separate' | 'all'>('combined'); const [mode, setMode] = useState<'combined' | 'separate' | 'all'>('combined');
const [liveSettings, setLiveSettings] = useState<LiveDisplaySettings>({
lineThickness: 2,
showGrid: true,
glowIntensity: 1,
displayMode: 'combined',
});
const [isMicActive, setIsMicActive] = useState(false); const [isMicActive, setIsMicActive] = useState(false);
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
const [currentSample, setCurrentSample] = useState(0); const [currentSample, setCurrentSample] = useState(0);
@ -249,6 +255,8 @@ export function Oscilloscope() {
onExportFormatChange={handleExportFormatChange} onExportFormatChange={handleExportFormatChange}
exportQuality={exportQuality} exportQuality={exportQuality}
onExportQualityChange={handleExportQualityChange} onExportQualityChange={handleExportQualityChange}
liveSettings={liveSettings}
onLiveSettingsChange={setLiveSettings}
/> />
</div> </div>
@ -257,13 +265,14 @@ export function Oscilloscope() {
<OscilloscopeDisplay <OscilloscopeDisplay
audioData={audioData} audioData={audioData}
micAnalyzer={micAnalyzer} micAnalyzer={micAnalyzer}
mode={mode} mode={liveSettings.displayMode}
isPlaying={isPlaying} isPlaying={isPlaying}
playbackSpeed={playbackSpeed} playbackSpeed={playbackSpeed}
isLooping={isLooping} isLooping={isLooping}
seekPosition={seekPosition} seekPosition={seekPosition}
onPlaybackEnd={() => setIsPlaying(false)} onPlaybackEnd={() => setIsPlaying(false)}
onSeek={handleSeek} onSeek={handleSeek}
liveSettings={liveSettings}
/> />
</div> </div>

View File

@ -2,6 +2,7 @@ import { useEffect, useRef, useCallback } from 'react';
import type { AudioData } from '@/hooks/useAudioAnalyzer'; import type { AudioData } from '@/hooks/useAudioAnalyzer';
import type { OscilloscopeMode } from '@/hooks/useOscilloscopeRenderer'; import type { OscilloscopeMode } from '@/hooks/useOscilloscopeRenderer';
import { useAudioAnalyzer as useSharedAudioAnalyzer } from '@/contexts/AudioAnalyzerContext'; import { useAudioAnalyzer as useSharedAudioAnalyzer } from '@/contexts/AudioAnalyzerContext';
import type { LiveDisplaySettings } from './ControlPanel';
interface OscilloscopeDisplayProps { interface OscilloscopeDisplayProps {
audioData: AudioData | null; audioData: AudioData | null;
@ -13,12 +14,24 @@ interface OscilloscopeDisplayProps {
seekPosition: number; seekPosition: number;
onPlaybackEnd?: () => void; onPlaybackEnd?: () => void;
onSeek?: (position: number) => void; onSeek?: (position: number) => void;
liveSettings?: LiveDisplaySettings;
} }
const WIDTH = 800; const WIDTH = 800;
const HEIGHT = 600; const HEIGHT = 600;
const FPS = 60; const FPS = 60;
// Get computed CSS color from theme
const getThemeColor = (cssVar: string, fallback: string): string => {
if (typeof window === 'undefined') return fallback;
const root = document.documentElement;
const value = getComputedStyle(root).getPropertyValue(cssVar).trim();
if (value) {
return `hsl(${value})`;
}
return fallback;
};
export function OscilloscopeDisplay({ export function OscilloscopeDisplay({
audioData, audioData,
micAnalyzer, micAnalyzer,
@ -28,7 +41,8 @@ export function OscilloscopeDisplay({
isLooping, isLooping,
seekPosition, seekPosition,
onPlaybackEnd, onPlaybackEnd,
onSeek onSeek,
liveSettings
}: OscilloscopeDisplayProps) { }: OscilloscopeDisplayProps) {
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const animationRef = useRef<number | null>(null); const animationRef = useRef<number | null>(null);
@ -39,8 +53,17 @@ export function OscilloscopeDisplay({
// Use shared analyzer for live audio (music player, sound effects) // Use shared analyzer for live audio (music player, sound effects)
const liveAnalyzer = sharedAnalyzer || micAnalyzer; const liveAnalyzer = sharedAnalyzer || micAnalyzer;
// Get settings with defaults
const lineThickness = liveSettings?.lineThickness ?? 2;
const showGrid = liveSettings?.showGrid ?? true;
const glowIntensity = liveSettings?.glowIntensity ?? 1;
const drawGraticule = useCallback((ctx: CanvasRenderingContext2D) => { const drawGraticule = useCallback((ctx: CanvasRenderingContext2D) => {
ctx.strokeStyle = '#00ff00'; if (!showGrid) return;
const primaryColor = getThemeColor('--primary', '#00ff00');
ctx.strokeStyle = primaryColor;
ctx.globalAlpha = 0.3;
ctx.lineWidth = 1; ctx.lineWidth = 1;
// Horizontal center line (X axis) // Horizontal center line (X axis)
@ -54,7 +77,9 @@ export function OscilloscopeDisplay({
ctx.moveTo(WIDTH / 2, 0); ctx.moveTo(WIDTH / 2, 0);
ctx.lineTo(WIDTH / 2, HEIGHT); ctx.lineTo(WIDTH / 2, HEIGHT);
ctx.stroke(); ctx.stroke();
}, []);
ctx.globalAlpha = 1;
}, [showGrid]);
const drawFrame = useCallback(() => { const drawFrame = useCallback(() => {
if (!canvasRef.current) return; if (!canvasRef.current) return;
@ -67,6 +92,9 @@ export function OscilloscopeDisplay({
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
if (!ctx) return; if (!ctx) return;
const primaryColor = getThemeColor('--primary', '#00ff00');
const backgroundColor = getThemeColor('--background', '#000000');
let samplesPerFrame: number; let samplesPerFrame: number;
let startSample: number; let startSample: number;
let endSample: number; let endSample: number;
@ -81,8 +109,8 @@ export function OscilloscopeDisplay({
const dataArray = new Uint8Array(bufferLength); const dataArray = new Uint8Array(bufferLength);
activeAnalyzer.getByteTimeDomainData(dataArray); activeAnalyzer.getByteTimeDomainData(dataArray);
// Clear to pure black // Clear to background color
ctx.fillStyle = '#000000'; ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, WIDTH, HEIGHT); ctx.fillRect(0, 0, WIDTH, HEIGHT);
// Draw graticule first // Draw graticule first
@ -98,9 +126,17 @@ export function OscilloscopeDisplay({
startSample = 0; startSample = 0;
endSample = liveData.length; endSample = liveData.length;
// Apply glow effect
if (glowIntensity > 0) {
ctx.shadowColor = primaryColor;
ctx.shadowBlur = glowIntensity * 8;
} else {
ctx.shadowBlur = 0;
}
// Draw live data directly // Draw live data directly
ctx.strokeStyle = '#00ff00'; ctx.strokeStyle = primaryColor;
ctx.lineWidth = 2; ctx.lineWidth = lineThickness;
ctx.beginPath(); ctx.beginPath();
const sliceWidth = WIDTH / samplesPerFrame; const sliceWidth = WIDTH / samplesPerFrame;
@ -120,6 +156,7 @@ export function OscilloscopeDisplay({
} }
ctx.stroke(); ctx.stroke();
ctx.shadowBlur = 0;
// Request next frame for real-time // Request next frame for real-time
animationRef.current = requestAnimationFrame(drawFrame); animationRef.current = requestAnimationFrame(drawFrame);
@ -149,20 +186,28 @@ export function OscilloscopeDisplay({
endSample = Math.min(startSample + samplesPerFrame, audioData.leftChannel.length); endSample = Math.min(startSample + samplesPerFrame, audioData.leftChannel.length);
// Clear to pure black // Clear to background color
ctx.fillStyle = '#000000'; ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, WIDTH, HEIGHT); ctx.fillRect(0, 0, WIDTH, HEIGHT);
// Draw graticule first // Draw graticule first
drawGraticule(ctx); drawGraticule(ctx);
ctx.lineWidth = 2; // Apply glow effect
if (glowIntensity > 0) {
ctx.shadowColor = primaryColor;
ctx.shadowBlur = glowIntensity * 8;
} else {
ctx.shadowBlur = 0;
}
ctx.lineWidth = lineThickness;
ctx.lineCap = 'round'; ctx.lineCap = 'round';
const leftColor = '#00ff00'; const leftColor = primaryColor;
const rightColor = '#00ccff'; const rightColor = getThemeColor('--accent', '#00ccff');
const xyColor = '#ff8800'; const xyColor = getThemeColor('--secondary', '#ff8800');
const dividerColor = '#333333'; const dividerColor = 'rgba(255,255,255,0.1)';
if (mode === 'combined') { if (mode === 'combined') {
// Combined: both channels merged // Combined: both channels merged
@ -280,6 +325,7 @@ export function OscilloscopeDisplay({
} }
currentSampleRef.current = endSample; currentSampleRef.current = endSample;
ctx.shadowBlur = 0;
if (endSample >= audioData.leftChannel.length) { if (endSample >= audioData.leftChannel.length) {
if (isLooping) { if (isLooping) {
@ -291,7 +337,7 @@ export function OscilloscopeDisplay({
} }
animationRef.current = requestAnimationFrame(drawFrame); animationRef.current = requestAnimationFrame(drawFrame);
}, [audioData, micAnalyzer, liveAnalyzer, mode, drawGraticule, onPlaybackEnd, isPlaying, playbackSpeed, isLooping, seekPosition]); }, [audioData, micAnalyzer, liveAnalyzer, mode, drawGraticule, onPlaybackEnd, isPlaying, playbackSpeed, isLooping, seekPosition, lineThickness, glowIntensity]);
// Initialize canvas // Initialize canvas
useEffect(() => { useEffect(() => {