mirror of
https://github.com/JorySeverijnse/ui-fixer-supreme.git
synced 2026-01-29 16:18:38 +00:00
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:
commit
cc2612918c
@ -5,6 +5,13 @@ import { Progress } from '@/components/ui/progress';
|
||||
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
|
||||
import type { OscilloscopeMode } from '@/hooks/useOscilloscopeRenderer';
|
||||
|
||||
export interface LiveDisplaySettings {
|
||||
lineThickness: number;
|
||||
showGrid: boolean;
|
||||
glowIntensity: number;
|
||||
displayMode: OscilloscopeMode;
|
||||
}
|
||||
|
||||
interface ControlPanelProps {
|
||||
mode: OscilloscopeMode;
|
||||
onModeChange: (mode: OscilloscopeMode) => void;
|
||||
@ -29,6 +36,8 @@ interface ControlPanelProps {
|
||||
onExportFormatChange: (format: string) => void;
|
||||
exportQuality: string;
|
||||
onExportQualityChange: (quality: string) => void;
|
||||
liveSettings: LiveDisplaySettings;
|
||||
onLiveSettingsChange: (settings: LiveDisplaySettings) => void;
|
||||
}
|
||||
|
||||
export function ControlPanel({
|
||||
@ -55,36 +64,97 @@ export function ControlPanel({
|
||||
onExportFormatChange,
|
||||
exportQuality,
|
||||
onExportQualityChange,
|
||||
liveSettings,
|
||||
onLiveSettingsChange,
|
||||
}: ControlPanelProps) {
|
||||
return (
|
||||
<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">
|
||||
<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
|
||||
value={mode}
|
||||
onValueChange={(value) => onModeChange(value as OscilloscopeMode)}
|
||||
value={liveSettings.displayMode}
|
||||
onValueChange={(value) => onLiveSettingsChange({ ...liveSettings, displayMode: value as OscilloscopeMode })}
|
||||
className="space-y-2"
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<RadioGroupItem value="combined" id="combined" className="border-primary" />
|
||||
<Label htmlFor="combined" className="font-mono-crt text-sm cursor-pointer">
|
||||
<RadioGroupItem value="combined" id="live-combined" className="border-primary" />
|
||||
<Label htmlFor="live-combined" className="font-mono-crt text-sm cursor-pointer">
|
||||
Combined (L+R merged)
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<RadioGroupItem value="separate" id="separate" className="border-primary" />
|
||||
<Label htmlFor="separate" className="font-mono-crt text-sm cursor-pointer">
|
||||
<RadioGroupItem value="separate" id="live-separate" className="border-primary" />
|
||||
<Label htmlFor="live-separate" className="font-mono-crt text-sm cursor-pointer">
|
||||
Separate (L/R stacked)
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<RadioGroupItem value="all" id="all" className="border-primary" />
|
||||
<Label htmlFor="all" className="font-mono-crt text-sm cursor-pointer">
|
||||
<RadioGroupItem value="all" id="live-all" className="border-primary" />
|
||||
<Label htmlFor="live-all" className="font-mono-crt text-sm cursor-pointer">
|
||||
All (L/R + XY below)
|
||||
</Label>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
{/* Playback Controls */}
|
||||
@ -141,8 +211,23 @@ export function ControlPanel({
|
||||
</Button>
|
||||
|
||||
{/* 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>
|
||||
<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 */}
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { AudioUploader } from './AudioUploader';
|
||||
import { ControlPanel } from './ControlPanel';
|
||||
import { ControlPanel, LiveDisplaySettings } from './ControlPanel';
|
||||
import { OscilloscopeDisplay } from './OscilloscopeDisplay';
|
||||
import { useAudioAnalyzer } from '@/hooks/useAudioAnalyzer';
|
||||
import { useOscilloscopeRenderer } from '@/hooks/useOscilloscopeRenderer';
|
||||
@ -11,6 +11,12 @@ import { Button } from '@/components/ui/button';
|
||||
|
||||
export function Oscilloscope() {
|
||||
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 [isPlaying, setIsPlaying] = useState(false);
|
||||
const [currentSample, setCurrentSample] = useState(0);
|
||||
@ -249,6 +255,8 @@ export function Oscilloscope() {
|
||||
onExportFormatChange={handleExportFormatChange}
|
||||
exportQuality={exportQuality}
|
||||
onExportQualityChange={handleExportQualityChange}
|
||||
liveSettings={liveSettings}
|
||||
onLiveSettingsChange={setLiveSettings}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -257,13 +265,14 @@ export function Oscilloscope() {
|
||||
<OscilloscopeDisplay
|
||||
audioData={audioData}
|
||||
micAnalyzer={micAnalyzer}
|
||||
mode={mode}
|
||||
mode={liveSettings.displayMode}
|
||||
isPlaying={isPlaying}
|
||||
playbackSpeed={playbackSpeed}
|
||||
isLooping={isLooping}
|
||||
seekPosition={seekPosition}
|
||||
onPlaybackEnd={() => setIsPlaying(false)}
|
||||
onSeek={handleSeek}
|
||||
liveSettings={liveSettings}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import { useEffect, useRef, useCallback } from 'react';
|
||||
import type { AudioData } from '@/hooks/useAudioAnalyzer';
|
||||
import type { OscilloscopeMode } from '@/hooks/useOscilloscopeRenderer';
|
||||
import { useAudioAnalyzer as useSharedAudioAnalyzer } from '@/contexts/AudioAnalyzerContext';
|
||||
import type { LiveDisplaySettings } from './ControlPanel';
|
||||
|
||||
interface OscilloscopeDisplayProps {
|
||||
audioData: AudioData | null;
|
||||
@ -13,12 +14,24 @@ interface OscilloscopeDisplayProps {
|
||||
seekPosition: number;
|
||||
onPlaybackEnd?: () => void;
|
||||
onSeek?: (position: number) => void;
|
||||
liveSettings?: LiveDisplaySettings;
|
||||
}
|
||||
|
||||
const WIDTH = 800;
|
||||
const HEIGHT = 600;
|
||||
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({
|
||||
audioData,
|
||||
micAnalyzer,
|
||||
@ -28,7 +41,8 @@ export function OscilloscopeDisplay({
|
||||
isLooping,
|
||||
seekPosition,
|
||||
onPlaybackEnd,
|
||||
onSeek
|
||||
onSeek,
|
||||
liveSettings
|
||||
}: OscilloscopeDisplayProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const animationRef = useRef<number | null>(null);
|
||||
@ -39,8 +53,17 @@ export function OscilloscopeDisplay({
|
||||
// Use shared analyzer for live audio (music player, sound effects)
|
||||
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) => {
|
||||
ctx.strokeStyle = '#00ff00';
|
||||
if (!showGrid) return;
|
||||
|
||||
const primaryColor = getThemeColor('--primary', '#00ff00');
|
||||
ctx.strokeStyle = primaryColor;
|
||||
ctx.globalAlpha = 0.3;
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
// Horizontal center line (X axis)
|
||||
@ -54,7 +77,9 @@ export function OscilloscopeDisplay({
|
||||
ctx.moveTo(WIDTH / 2, 0);
|
||||
ctx.lineTo(WIDTH / 2, HEIGHT);
|
||||
ctx.stroke();
|
||||
}, []);
|
||||
|
||||
ctx.globalAlpha = 1;
|
||||
}, [showGrid]);
|
||||
|
||||
const drawFrame = useCallback(() => {
|
||||
if (!canvasRef.current) return;
|
||||
@ -67,6 +92,9 @@ export function OscilloscopeDisplay({
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const primaryColor = getThemeColor('--primary', '#00ff00');
|
||||
const backgroundColor = getThemeColor('--background', '#000000');
|
||||
|
||||
let samplesPerFrame: number;
|
||||
let startSample: number;
|
||||
let endSample: number;
|
||||
@ -81,8 +109,8 @@ export function OscilloscopeDisplay({
|
||||
const dataArray = new Uint8Array(bufferLength);
|
||||
activeAnalyzer.getByteTimeDomainData(dataArray);
|
||||
|
||||
// Clear to pure black
|
||||
ctx.fillStyle = '#000000';
|
||||
// Clear to background color
|
||||
ctx.fillStyle = backgroundColor;
|
||||
ctx.fillRect(0, 0, WIDTH, HEIGHT);
|
||||
|
||||
// Draw graticule first
|
||||
@ -98,9 +126,17 @@ export function OscilloscopeDisplay({
|
||||
startSample = 0;
|
||||
endSample = liveData.length;
|
||||
|
||||
// Apply glow effect
|
||||
if (glowIntensity > 0) {
|
||||
ctx.shadowColor = primaryColor;
|
||||
ctx.shadowBlur = glowIntensity * 8;
|
||||
} else {
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
|
||||
// Draw live data directly
|
||||
ctx.strokeStyle = '#00ff00';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeStyle = primaryColor;
|
||||
ctx.lineWidth = lineThickness;
|
||||
ctx.beginPath();
|
||||
|
||||
const sliceWidth = WIDTH / samplesPerFrame;
|
||||
@ -120,6 +156,7 @@ export function OscilloscopeDisplay({
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Request next frame for real-time
|
||||
animationRef.current = requestAnimationFrame(drawFrame);
|
||||
@ -149,20 +186,28 @@ export function OscilloscopeDisplay({
|
||||
|
||||
endSample = Math.min(startSample + samplesPerFrame, audioData.leftChannel.length);
|
||||
|
||||
// Clear to pure black
|
||||
ctx.fillStyle = '#000000';
|
||||
// Clear to background color
|
||||
ctx.fillStyle = backgroundColor;
|
||||
ctx.fillRect(0, 0, WIDTH, HEIGHT);
|
||||
|
||||
// Draw graticule first
|
||||
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';
|
||||
|
||||
const leftColor = '#00ff00';
|
||||
const rightColor = '#00ccff';
|
||||
const xyColor = '#ff8800';
|
||||
const dividerColor = '#333333';
|
||||
const leftColor = primaryColor;
|
||||
const rightColor = getThemeColor('--accent', '#00ccff');
|
||||
const xyColor = getThemeColor('--secondary', '#ff8800');
|
||||
const dividerColor = 'rgba(255,255,255,0.1)';
|
||||
|
||||
if (mode === 'combined') {
|
||||
// Combined: both channels merged
|
||||
@ -280,6 +325,7 @@ export function OscilloscopeDisplay({
|
||||
}
|
||||
|
||||
currentSampleRef.current = endSample;
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
if (endSample >= audioData.leftChannel.length) {
|
||||
if (isLooping) {
|
||||
@ -291,7 +337,7 @@ export function OscilloscopeDisplay({
|
||||
}
|
||||
|
||||
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
|
||||
useEffect(() => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user