feat(waveform): add live hover preview for selection positioning
This commit is contained in:
@@ -19,6 +19,7 @@ export default function WaveformEditor({ audioUrl, startTime, duration, unlockSt
|
|||||||
const [zoom, setZoom] = useState(1); // 1 = full view, higher = zoomed in
|
const [zoom, setZoom] = useState(1); // 1 = full view, higher = zoomed in
|
||||||
const [viewOffset, setViewOffset] = useState(0); // Offset in seconds for panning
|
const [viewOffset, setViewOffset] = useState(0); // Offset in seconds for panning
|
||||||
const [playbackPosition, setPlaybackPosition] = useState<number | null>(null); // Current playback position in seconds
|
const [playbackPosition, setPlaybackPosition] = useState<number | null>(null); // Current playback position in seconds
|
||||||
|
const [hoverPreviewTime, setHoverPreviewTime] = useState<number | null>(null); // Preview position on hover
|
||||||
const audioContextRef = useRef<AudioContext | null>(null);
|
const audioContextRef = useRef<AudioContext | null>(null);
|
||||||
const sourceRef = useRef<AudioBufferSourceNode | null>(null);
|
const sourceRef = useRef<AudioBufferSourceNode | null>(null);
|
||||||
const playbackStartTimeRef = useRef<number>(0); // When playback started
|
const playbackStartTimeRef = useRef<number>(0); // When playback started
|
||||||
@@ -134,6 +135,24 @@ export default function WaveformEditor({ audioUrl, startTime, duration, unlockSt
|
|||||||
});
|
});
|
||||||
ctx.setLineDash([]);
|
ctx.setLineDash([]);
|
||||||
|
|
||||||
|
// Draw hover preview (semi-transparent)
|
||||||
|
if (hoverPreviewTime !== null) {
|
||||||
|
const previewStartPx = ((hoverPreviewTime - visibleStart) / visibleDuration) * width;
|
||||||
|
const previewWidthPx = (duration / visibleDuration) * width;
|
||||||
|
|
||||||
|
if (previewStartPx + previewWidthPx > 0 && previewStartPx < width) {
|
||||||
|
ctx.fillStyle = 'rgba(16, 185, 129, 0.2)'; // Light green
|
||||||
|
ctx.fillRect(Math.max(0, previewStartPx), 0, Math.min(previewWidthPx, width - previewStartPx), height);
|
||||||
|
|
||||||
|
// Draw preview borders
|
||||||
|
ctx.strokeStyle = '#10b981';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.setLineDash([5, 5]);
|
||||||
|
ctx.strokeRect(Math.max(0, previewStartPx), 0, Math.min(previewWidthPx, width - previewStartPx), height);
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Draw playback cursor
|
// Draw playback cursor
|
||||||
if (playbackPosition !== null) {
|
if (playbackPosition !== null) {
|
||||||
const cursorPx = ((playbackPosition - visibleStart) / visibleDuration) * width;
|
const cursorPx = ((playbackPosition - visibleStart) / visibleDuration) * width;
|
||||||
@@ -156,7 +175,7 @@ export default function WaveformEditor({ audioUrl, startTime, duration, unlockSt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}, [audioBuffer, startTime, duration, audioDuration, zoom, viewOffset, unlockSteps, playbackPosition]);
|
}, [audioBuffer, startTime, duration, audioDuration, zoom, viewOffset, unlockSteps, playbackPosition, hoverPreviewTime]);
|
||||||
|
|
||||||
const handleCanvasClick = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
const handleCanvasClick = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||||
if (!canvasRef.current || !audioDuration) return;
|
if (!canvasRef.current || !audioDuration) return;
|
||||||
@@ -176,6 +195,26 @@ export default function WaveformEditor({ audioUrl, startTime, duration, unlockSt
|
|||||||
onStartTimeChange(Math.floor(newStartTime));
|
onStartTimeChange(Math.floor(newStartTime));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCanvasMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||||
|
if (!canvasRef.current || !audioDuration) return;
|
||||||
|
|
||||||
|
const rect = canvasRef.current.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const visibleDuration = audioDuration / zoom;
|
||||||
|
const visibleStart = Math.max(0, Math.min(viewOffset, audioDuration - visibleDuration));
|
||||||
|
const hoveredTime = visibleStart + (x / rect.width) * visibleDuration;
|
||||||
|
|
||||||
|
// Calculate where the selection would be centered on this point
|
||||||
|
let previewStartTime = hoveredTime - (duration / 2);
|
||||||
|
previewStartTime = Math.max(0, Math.min(previewStartTime, audioDuration - duration));
|
||||||
|
|
||||||
|
setHoverPreviewTime(previewStartTime);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCanvasMouseLeave = () => {
|
||||||
|
setHoverPreviewTime(null);
|
||||||
|
};
|
||||||
|
|
||||||
const stopPlayback = () => {
|
const stopPlayback = () => {
|
||||||
sourceRef.current?.stop();
|
sourceRef.current?.stop();
|
||||||
setIsPlaying(false);
|
setIsPlaying(false);
|
||||||
@@ -337,6 +376,8 @@ export default function WaveformEditor({ audioUrl, startTime, duration, unlockSt
|
|||||||
width={800}
|
width={800}
|
||||||
height={150}
|
height={150}
|
||||||
onClick={handleCanvasClick}
|
onClick={handleCanvasClick}
|
||||||
|
onMouseMove={handleCanvasMouseMove}
|
||||||
|
onMouseLeave={handleCanvasMouseLeave}
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
|
|||||||
Reference in New Issue
Block a user