Fixed canvas visualization not painting sometimes by: - Adding `open` prop check before rendering - Adding `open` to useEffect dependencies - Adding dimension validation for dynamic canvas sizes - Ensures canvas properly renders when dialog becomes visible Affected dialogs: DynamicsParameterDialog, TimeBasedParameterDialog, AdvancedParameterDialog 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
677 lines
24 KiB
TypeScript
677 lines
24 KiB
TypeScript
'use client';
|
|
|
|
import * as React from 'react';
|
|
import { Modal } from '@/components/ui/Modal';
|
|
import { Button } from '@/components/ui/Button';
|
|
import { Slider } from '@/components/ui/Slider';
|
|
import { cn } from '@/lib/utils/cn';
|
|
import type {
|
|
DelayParameters,
|
|
ReverbParameters,
|
|
ChorusParameters,
|
|
FlangerParameters,
|
|
PhaserParameters,
|
|
} from '@/lib/audio/effects/time-based';
|
|
|
|
export type TimeBasedType = 'delay' | 'reverb' | 'chorus' | 'flanger' | 'phaser';
|
|
|
|
export type TimeBasedParameters =
|
|
| (DelayParameters & { type: 'delay' })
|
|
| (ReverbParameters & { type: 'reverb' })
|
|
| (ChorusParameters & { type: 'chorus' })
|
|
| (FlangerParameters & { type: 'flanger' })
|
|
| (PhaserParameters & { type: 'phaser' });
|
|
|
|
export interface EffectPreset {
|
|
name: string;
|
|
parameters: Partial<DelayParameters | ReverbParameters | ChorusParameters | FlangerParameters | PhaserParameters>;
|
|
}
|
|
|
|
export interface TimeBasedParameterDialogProps {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
effectType: TimeBasedType;
|
|
onApply: (params: TimeBasedParameters) => void;
|
|
}
|
|
|
|
const EFFECT_LABELS: Record<TimeBasedType, string> = {
|
|
delay: 'Delay/Echo',
|
|
reverb: 'Reverb',
|
|
chorus: 'Chorus',
|
|
flanger: 'Flanger',
|
|
phaser: 'Phaser',
|
|
};
|
|
|
|
const EFFECT_DESCRIPTIONS: Record<TimeBasedType, string> = {
|
|
delay: 'Creates echo effects by repeating the audio signal',
|
|
reverb: 'Simulates acoustic space and ambience',
|
|
chorus: 'Thickens sound by adding modulated copies',
|
|
flanger: 'Creates sweeping comb-filter effect',
|
|
phaser: 'Creates a phase-shifting swoosh effect',
|
|
};
|
|
|
|
const PRESETS: Record<TimeBasedType, EffectPreset[]> = {
|
|
delay: [
|
|
{ name: 'Short Slap', parameters: { time: 80, feedback: 0.2, mix: 0.3 } },
|
|
{ name: 'Medium Echo', parameters: { time: 250, feedback: 0.4, mix: 0.4 } },
|
|
{ name: 'Long Echo', parameters: { time: 500, feedback: 0.5, mix: 0.5 } },
|
|
{ name: 'Ping Pong', parameters: { time: 375, feedback: 0.6, mix: 0.4 } },
|
|
],
|
|
reverb: [
|
|
{ name: 'Small Room', parameters: { roomSize: 0.3, damping: 0.5, mix: 0.2 } },
|
|
{ name: 'Medium Hall', parameters: { roomSize: 0.6, damping: 0.3, mix: 0.3 } },
|
|
{ name: 'Large Hall', parameters: { roomSize: 0.8, damping: 0.2, mix: 0.4 } },
|
|
{ name: 'Cathedral', parameters: { roomSize: 1.0, damping: 0.1, mix: 0.5 } },
|
|
],
|
|
chorus: [
|
|
{ name: 'Subtle', parameters: { rate: 0.5, depth: 0.2, delay: 20, mix: 0.3 } },
|
|
{ name: 'Classic', parameters: { rate: 1.0, depth: 0.5, delay: 25, mix: 0.5 } },
|
|
{ name: 'Deep', parameters: { rate: 1.5, depth: 0.7, delay: 30, mix: 0.6 } },
|
|
{ name: 'Lush', parameters: { rate: 0.8, depth: 0.6, delay: 35, mix: 0.7 } },
|
|
],
|
|
flanger: [
|
|
{ name: 'Subtle', parameters: { rate: 0.3, depth: 0.3, feedback: 0.2, delay: 2, mix: 0.4 } },
|
|
{ name: 'Classic', parameters: { rate: 0.5, depth: 0.5, feedback: 0.4, delay: 3, mix: 0.5 } },
|
|
{ name: 'Jet', parameters: { rate: 0.2, depth: 0.7, feedback: 0.6, delay: 1.5, mix: 0.6 } },
|
|
{ name: 'Extreme', parameters: { rate: 1.0, depth: 0.8, feedback: 0.7, delay: 2.5, mix: 0.7 } },
|
|
],
|
|
phaser: [
|
|
{ name: 'Gentle', parameters: { rate: 0.4, depth: 0.3, feedback: 0.2, stages: 4, mix: 0.4 } },
|
|
{ name: 'Classic', parameters: { rate: 0.6, depth: 0.5, feedback: 0.4, stages: 6, mix: 0.5 } },
|
|
{ name: 'Deep', parameters: { rate: 0.3, depth: 0.7, feedback: 0.5, stages: 8, mix: 0.6 } },
|
|
{ name: 'Vintage', parameters: { rate: 0.5, depth: 0.6, feedback: 0.6, stages: 4, mix: 0.7 } },
|
|
],
|
|
};
|
|
|
|
export function TimeBasedParameterDialog({
|
|
open,
|
|
onClose,
|
|
effectType,
|
|
onApply,
|
|
}: TimeBasedParameterDialogProps) {
|
|
const [parameters, setParameters] = React.useState<TimeBasedParameters>(() => {
|
|
if (effectType === 'delay') {
|
|
return { type: 'delay', time: 250, feedback: 0.4, mix: 0.4 };
|
|
} else if (effectType === 'reverb') {
|
|
return { type: 'reverb', roomSize: 0.6, damping: 0.3, mix: 0.3 };
|
|
} else if (effectType === 'chorus') {
|
|
return { type: 'chorus', rate: 1.0, depth: 0.5, delay: 25, mix: 0.5 };
|
|
} else if (effectType === 'flanger') {
|
|
return { type: 'flanger', rate: 0.5, depth: 0.5, feedback: 0.4, delay: 3, mix: 0.5 };
|
|
} else {
|
|
return { type: 'phaser', rate: 0.6, depth: 0.5, feedback: 0.4, stages: 6, mix: 0.5 };
|
|
}
|
|
});
|
|
|
|
const canvasRef = React.useRef<HTMLCanvasElement>(null);
|
|
|
|
// Get appropriate presets for this effect type
|
|
const presets = PRESETS[effectType] || [];
|
|
|
|
// Update parameters when effect type changes
|
|
React.useEffect(() => {
|
|
if (effectType === 'delay') {
|
|
setParameters({ type: 'delay', time: 250, feedback: 0.4, mix: 0.4 });
|
|
} else if (effectType === 'reverb') {
|
|
setParameters({ type: 'reverb', roomSize: 0.6, damping: 0.3, mix: 0.3 });
|
|
} else if (effectType === 'chorus') {
|
|
setParameters({ type: 'chorus', rate: 1.0, depth: 0.5, delay: 25, mix: 0.5 });
|
|
} else if (effectType === 'flanger') {
|
|
setParameters({ type: 'flanger', rate: 0.5, depth: 0.5, feedback: 0.4, delay: 3, mix: 0.5 });
|
|
} else {
|
|
setParameters({ type: 'phaser', rate: 0.6, depth: 0.5, feedback: 0.4, stages: 6, mix: 0.5 });
|
|
}
|
|
}, [effectType]);
|
|
|
|
// Draw visualization
|
|
React.useEffect(() => {
|
|
if (!open || !canvasRef.current) return;
|
|
|
|
const canvas = canvasRef.current;
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
// Get actual dimensions
|
|
const rect = canvas.getBoundingClientRect();
|
|
const dpr = window.devicePixelRatio || 1;
|
|
|
|
// Ensure canvas has dimensions before drawing
|
|
if (rect.width === 0 || rect.height === 0) return;
|
|
|
|
// Set actual size in memory
|
|
canvas.width = rect.width * dpr;
|
|
canvas.height = rect.height * dpr;
|
|
|
|
// Normalize coordinate system
|
|
ctx.scale(dpr, dpr);
|
|
|
|
// Clear canvas
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
const width = rect.width;
|
|
const height = rect.height;
|
|
|
|
// Clear with background
|
|
ctx.fillStyle = getComputedStyle(canvas).getPropertyValue('background-color') || '#1a1a1a';
|
|
ctx.fillRect(0, 0, width, height);
|
|
|
|
if (effectType === 'delay') {
|
|
// Draw delay echoes
|
|
const delayParams = parameters as DelayParameters & { type: 'delay' };
|
|
const maxTime = 2000; // ms
|
|
const echoCount = 5;
|
|
|
|
ctx.strokeStyle = 'rgba(128, 128, 128, 0.3)';
|
|
ctx.lineWidth = 1;
|
|
ctx.setLineDash([2, 2]);
|
|
for (let i = 0; i <= 4; i++) {
|
|
const x = (i / 4) * width;
|
|
ctx.beginPath();
|
|
ctx.moveTo(x, 0);
|
|
ctx.lineTo(x, height);
|
|
ctx.stroke();
|
|
}
|
|
ctx.setLineDash([]);
|
|
|
|
let gain = 1.0;
|
|
for (let i = 0; i < echoCount; i++) {
|
|
const x = (i * delayParams.time / maxTime) * width;
|
|
const barHeight = height * gain * 0.8;
|
|
const y = (height - barHeight) / 2;
|
|
|
|
ctx.fillStyle = `rgba(59, 130, 246, ${gain})`;
|
|
ctx.fillRect(x - 3, y, 6, barHeight);
|
|
|
|
gain *= delayParams.feedback;
|
|
if (gain < 0.01) break;
|
|
}
|
|
} else if (effectType === 'reverb') {
|
|
// Draw reverb decay
|
|
const reverbParams = parameters as ReverbParameters & { type: 'reverb' };
|
|
const decayTime = reverbParams.roomSize * 3000; // ms
|
|
|
|
ctx.strokeStyle = '#3b82f6';
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
|
|
for (let x = 0; x < width; x++) {
|
|
const time = (x / width) * 3000;
|
|
const decay = Math.exp(-time / (decayTime * (1 - reverbParams.damping * 0.5)));
|
|
const y = height / 2 + (height / 2 - 20) * (1 - decay);
|
|
|
|
if (x === 0) ctx.moveTo(x, y);
|
|
else ctx.lineTo(x, y);
|
|
}
|
|
ctx.stroke();
|
|
|
|
// Draw reference line
|
|
ctx.strokeStyle = 'rgba(128, 128, 128, 0.3)';
|
|
ctx.lineWidth = 1;
|
|
ctx.setLineDash([5, 5]);
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, height / 2);
|
|
ctx.lineTo(width, height / 2);
|
|
ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
} else {
|
|
// Draw LFO waveform for chorus, flanger, phaser
|
|
let rate = 1.0;
|
|
let depth = 0.5;
|
|
|
|
if (effectType === 'chorus') {
|
|
const chorusParams = parameters as ChorusParameters & { type: 'chorus' };
|
|
rate = chorusParams.rate;
|
|
depth = chorusParams.depth;
|
|
} else if (effectType === 'flanger') {
|
|
const flangerParams = parameters as FlangerParameters & { type: 'flanger' };
|
|
rate = flangerParams.rate;
|
|
depth = flangerParams.depth;
|
|
} else if (effectType === 'phaser') {
|
|
const phaserParams = parameters as PhaserParameters & { type: 'phaser' };
|
|
rate = phaserParams.rate;
|
|
depth = phaserParams.depth;
|
|
}
|
|
|
|
ctx.strokeStyle = '#3b82f6';
|
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
|
|
|
const cycles = rate * 2; // Show 2 seconds worth
|
|
for (let x = 0; x < width; x++) {
|
|
const phase = (x / width) * cycles * 2 * Math.PI;
|
|
const lfo = Math.sin(phase);
|
|
const y = height / 2 - (lfo * depth * height * 0.4);
|
|
|
|
if (x === 0) ctx.moveTo(x, y);
|
|
else ctx.lineTo(x, y);
|
|
}
|
|
ctx.stroke();
|
|
|
|
// Draw center line
|
|
ctx.strokeStyle = 'rgba(128, 128, 128, 0.3)';
|
|
ctx.lineWidth = 1;
|
|
ctx.setLineDash([5, 5]);
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, height / 2);
|
|
ctx.lineTo(width, height / 2);
|
|
ctx.stroke();
|
|
ctx.setLineDash([]);
|
|
}
|
|
|
|
}, [parameters, effectType, open]);
|
|
|
|
const handleApply = () => {
|
|
onApply(parameters);
|
|
onClose();
|
|
};
|
|
|
|
const handlePresetClick = (preset: EffectPreset) => {
|
|
setParameters((prev) => ({
|
|
...prev,
|
|
...preset.parameters,
|
|
}));
|
|
};
|
|
|
|
return (
|
|
<Modal
|
|
open={open}
|
|
onClose={onClose}
|
|
title={EFFECT_LABELS[effectType] || 'Time-Based Effect'}
|
|
description={EFFECT_DESCRIPTIONS[effectType]}
|
|
size="lg"
|
|
footer={
|
|
<>
|
|
<Button variant="outline" onClick={onClose}>
|
|
Cancel
|
|
</Button>
|
|
<Button onClick={handleApply}>
|
|
Apply Effect
|
|
</Button>
|
|
</>
|
|
}
|
|
>
|
|
<div className="space-y-4">
|
|
{/* Visualization */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground">
|
|
{effectType === 'delay' && 'Echo Pattern'}
|
|
{effectType === 'reverb' && 'Reverb Decay'}
|
|
{(effectType === 'chorus' || effectType === 'flanger' || effectType === 'phaser') && 'Modulation (LFO)'}
|
|
</label>
|
|
<canvas
|
|
ref={canvasRef}
|
|
className="w-full h-32 border border-border rounded bg-background"
|
|
/>
|
|
</div>
|
|
|
|
{/* Presets */}
|
|
{presets.length > 0 && (
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground">
|
|
Presets
|
|
</label>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
{presets.map((preset) => (
|
|
<Button
|
|
key={preset.name}
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handlePresetClick(preset)}
|
|
className="justify-start"
|
|
>
|
|
{preset.name}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Effect-specific parameters */}
|
|
{effectType === 'delay' && (
|
|
<>
|
|
{/* Delay Time */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Delay Time</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{((parameters as DelayParameters).time ?? 250).toFixed(0)} ms
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as DelayParameters).time ?? 250)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, time: value }))
|
|
}
|
|
min={10}
|
|
max={2000}
|
|
step={10}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
{/* Feedback */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Feedback</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{(((parameters as DelayParameters).feedback ?? 0.4) * 100).toFixed(0)}%
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as DelayParameters).feedback ?? 0.4)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, feedback: value }))
|
|
}
|
|
min={0}
|
|
max={0.95}
|
|
step={0.01}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{effectType === 'reverb' && (
|
|
<>
|
|
{/* Room Size */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Room Size</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{(((parameters as ReverbParameters).roomSize ?? 0.6) * 100).toFixed(0)}%
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as ReverbParameters).roomSize ?? 0.6)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, roomSize: value }))
|
|
}
|
|
min={0.1}
|
|
max={1}
|
|
step={0.01}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
{/* Damping */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Damping</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{(((parameters as ReverbParameters).damping ?? 0.3) * 100).toFixed(0)}%
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as ReverbParameters).damping ?? 0.3)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, damping: value }))
|
|
}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{effectType === 'chorus' && (
|
|
<>
|
|
{/* Rate */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Rate</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{((parameters as ChorusParameters).rate ?? 1.0).toFixed(2)} Hz
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as ChorusParameters).rate ?? 1.0)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, rate: value }))
|
|
}
|
|
min={0.1}
|
|
max={5}
|
|
step={0.1}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
{/* Depth */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Depth</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{(((parameters as ChorusParameters).depth ?? 0.5) * 100).toFixed(0)}%
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as ChorusParameters).depth ?? 0.5)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, depth: value }))
|
|
}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
{/* Base Delay */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Base Delay</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{((parameters as ChorusParameters).delay ?? 25).toFixed(1)} ms
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as ChorusParameters).delay ?? 25)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, delay: value }))
|
|
}
|
|
min={5}
|
|
max={50}
|
|
step={0.5}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{effectType === 'flanger' && (
|
|
<>
|
|
{/* Rate */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Rate</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{((parameters as FlangerParameters).rate ?? 0.5).toFixed(2)} Hz
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as FlangerParameters).rate ?? 0.5)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, rate: value }))
|
|
}
|
|
min={0.1}
|
|
max={5}
|
|
step={0.1}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
{/* Depth */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Depth</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{(((parameters as FlangerParameters).depth ?? 0.5) * 100).toFixed(0)}%
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as FlangerParameters).depth ?? 0.5)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, depth: value }))
|
|
}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
{/* Feedback */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Feedback</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{(((parameters as FlangerParameters).feedback ?? 0.4) * 100).toFixed(0)}%
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as FlangerParameters).feedback ?? 0.4)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, feedback: value }))
|
|
}
|
|
min={0}
|
|
max={0.95}
|
|
step={0.01}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
{/* Base Delay */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Base Delay</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{((parameters as FlangerParameters).delay ?? 3).toFixed(1)} ms
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as FlangerParameters).delay ?? 3)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, delay: value }))
|
|
}
|
|
min={0.5}
|
|
max={10}
|
|
step={0.1}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{effectType === 'phaser' && (
|
|
<>
|
|
{/* Rate */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Rate</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{((parameters as PhaserParameters).rate ?? 0.6).toFixed(2)} Hz
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as PhaserParameters).rate ?? 0.6)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, rate: value }))
|
|
}
|
|
min={0.1}
|
|
max={5}
|
|
step={0.1}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
{/* Depth */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Depth</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{(((parameters as PhaserParameters).depth ?? 0.5) * 100).toFixed(0)}%
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as PhaserParameters).depth ?? 0.5)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, depth: value }))
|
|
}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
{/* Feedback */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Feedback</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{(((parameters as PhaserParameters).feedback ?? 0.4) * 100).toFixed(0)}%
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as PhaserParameters).feedback ?? 0.4)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, feedback: value }))
|
|
}
|
|
min={0}
|
|
max={0.95}
|
|
step={0.01}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
|
|
{/* Stages */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Stages</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{((parameters as PhaserParameters).stages ?? 6).toFixed(0)}
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[((parameters as PhaserParameters).stages ?? 6)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, stages: Math.floor(value) }))
|
|
}
|
|
min={2}
|
|
max={12}
|
|
step={1}
|
|
className="w-full"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Mix (common to all) */}
|
|
<div className="space-y-2">
|
|
<label className="text-sm font-medium text-foreground flex justify-between">
|
|
<span>Mix (Dry/Wet)</span>
|
|
<span className="text-muted-foreground font-mono">
|
|
{((parameters.mix ?? 0.5) * 100).toFixed(0)}%
|
|
</span>
|
|
</label>
|
|
<Slider
|
|
value={[(parameters.mix ?? 0.5)]}
|
|
onValueChange={([value]) =>
|
|
setParameters((prev) => ({ ...prev, mix: value }))
|
|
}
|
|
min={0}
|
|
max={1}
|
|
step={0.01}
|
|
className="w-full"
|
|
/>
|
|
<div className="flex justify-between text-xs text-muted-foreground">
|
|
<span>0% (Dry)</span>
|
|
<span>100% (Wet)</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
}
|