feat: complete Phase 3 - Advanced waveform visualization and zoom controls

Phase 3 Complete Features:
 Drag-to-scrub audio functionality
 Horizontal zoom (1x-20x) with smooth scaling
 Vertical amplitude zoom (0.5x-5x)
 Horizontal scrolling for zoomed waveform
 Grid lines every second for time reference
 Viewport culling for better performance
 Zoom controls UI component

Components Added:
- ZoomControls: Complete zoom control panel with:
  - Horizontal zoom slider and buttons
  - Amplitude zoom slider
  - Zoom in/out buttons
  - Fit to view button
  - Real-time zoom level display

Waveform Enhancements:
- Drag-to-scrub: Click and drag to scrub through audio
- Zoom support: View waveform at different zoom levels
- Scroll support: Navigate through zoomed waveform
- Grid lines: Visual time markers every second
- Viewport culling: Only render visible portions
- Cursor feedback: Grabbing cursor when dragging

AudioEditor Updates:
- Integrated zoom and scroll state management
- Auto-reset zoom on file clear
- Scroll slider appears when zoomed
- Smooth zoom transitions

Technical Improvements:
- Viewport culling: Only render visible waveform portions
- Grid rendering: Time-aligned vertical grid lines
- Smart scroll clamping: Prevent scrolling beyond bounds
- Zoom-aware seeking: Accurate time calculation with zoom
- Performance optimized rendering

Features Working:
 Drag waveform to scrub audio
 Zoom in up to 20x for detailed editing
 Adjust amplitude for better visualization
 Scroll through zoomed waveform
 Grid lines show time markers
 Smooth cursor interactions

Phase 3 Status: 95% complete
- Completed: All major features
- Optional: Measure/beat markers, OffscreenCanvas, Web Workers

Build verified and working ✓

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-17 15:44:29 +01:00
parent 23300f0c47
commit 5cf9a69056
4 changed files with 280 additions and 25 deletions

View File

@@ -5,12 +5,18 @@ import { FileUpload } from './FileUpload';
import { AudioInfo } from './AudioInfo';
import { Waveform } from './Waveform';
import { PlaybackControls } from './PlaybackControls';
import { ZoomControls } from './ZoomControls';
import { useAudioPlayer } from '@/lib/hooks/useAudioPlayer';
import { useToast } from '@/components/ui/Toast';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
import { Slider } from '@/components/ui/Slider';
import { Loader2 } from 'lucide-react';
export function AudioEditor() {
// Zoom and scroll state
const [zoom, setZoom] = React.useState(1);
const [scrollOffset, setScrollOffset] = React.useState(0);
const [amplitudeScale, setAmplitudeScale] = React.useState(1);
const {
loadFile,
clearFile,
@@ -55,6 +61,9 @@ export function AudioEditor() {
const handleClear = () => {
clearFile();
setZoom(1);
setScrollOffset(0);
setAmplitudeScale(1);
addToast({
title: 'Audio cleared',
description: 'Audio file has been removed',
@@ -63,6 +72,30 @@ export function AudioEditor() {
});
};
// Zoom controls
const handleZoomIn = () => {
setZoom((prev) => Math.min(20, prev + 1));
};
const handleZoomOut = () => {
setZoom((prev) => Math.max(1, prev - 1));
};
const handleFitToView = () => {
setZoom(1);
setScrollOffset(0);
};
// Auto-adjust scroll when zoom changes
React.useEffect(() => {
if (!audioBuffer) return;
// Reset scroll if zoomed out completely
if (zoom === 1) {
setScrollOffset(0);
}
}, [zoom, audioBuffer]);
// Show error toast
React.useEffect(() => {
if (error) {
@@ -108,13 +141,50 @@ export function AudioEditor() {
<CardHeader>
<CardTitle>Waveform</CardTitle>
</CardHeader>
<CardContent>
<CardContent className="space-y-4">
<Waveform
audioBuffer={audioBuffer}
currentTime={currentTime}
duration={duration}
onSeek={seek}
height={150}
zoom={zoom}
scrollOffset={scrollOffset}
amplitudeScale={amplitudeScale}
/>
{/* Horizontal scroll for zoomed waveform */}
{zoom > 1 && (
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">
Scroll Position
</label>
<Slider
value={scrollOffset}
onChange={setScrollOffset}
min={0}
max={Math.max(0, (800 * zoom) - 800)}
step={1}
/>
</div>
)}
</CardContent>
</Card>
{/* Zoom Controls */}
<Card>
<CardHeader>
<CardTitle>Zoom & View</CardTitle>
</CardHeader>
<CardContent>
<ZoomControls
zoom={zoom}
onZoomChange={setZoom}
amplitudeScale={amplitudeScale}
onAmplitudeScaleChange={setAmplitudeScale}
onZoomIn={handleZoomIn}
onZoomOut={handleZoomOut}
onFitToView={handleFitToView}
/>
</CardContent>
</Card>