added cut and mute zones
This commit is contained in:
@ -24,7 +24,7 @@ function pickInterval(pxPerSec: number): { major: number; minor: number } {
|
||||
return { major, minor };
|
||||
}
|
||||
|
||||
export default function WaveformTimeline() {
|
||||
export default function WaveformTimeline({ cutMode, muteMode }: { cutMode: boolean; muteMode: boolean }) {
|
||||
const waveCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const headCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@ -35,7 +35,11 @@ export default function WaveformTimeline() {
|
||||
const backendUrl = useEditorStore((s) => s.backendUrl);
|
||||
const duration = useEditorStore((s) => s.duration);
|
||||
const deletedRanges = useEditorStore((s) => s.deletedRanges);
|
||||
const cutRanges = useEditorStore((s) => s.cutRanges);
|
||||
const muteRanges = useEditorStore((s) => s.muteRanges);
|
||||
const setCurrentTime = useEditorStore((s) => s.setCurrentTime);
|
||||
const addCutRange = useEditorStore((s) => s.addCutRange);
|
||||
const addMuteRange = useEditorStore((s) => s.addMuteRange);
|
||||
|
||||
const audioContextRef = useRef<AudioContext | null>(null);
|
||||
const audioBufferRef = useRef<AudioBuffer | null>(null);
|
||||
@ -46,6 +50,9 @@ export default function WaveformTimeline() {
|
||||
const drawStaticWaveformRef = useRef<() => void>(() => {});
|
||||
const isDraggingRef = useRef(false);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const selectionStartRef = useRef<number | null>(null);
|
||||
const [selectionStart, setSelectionStart] = useState<number | null>(null);
|
||||
const [selectionEnd, setSelectionEnd] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!videoUrl || !videoPath) return;
|
||||
@ -205,6 +212,35 @@ export default function WaveformTimeline() {
|
||||
ctx.fillRect(x1, waveTop, x2 - x1, waveH);
|
||||
}
|
||||
|
||||
// Draw cut ranges (red overlays)
|
||||
for (const range of cutRanges) {
|
||||
const x1 = (range.start - scroll) * pxPerSec;
|
||||
const x2 = (range.end - scroll) * pxPerSec;
|
||||
ctx.fillStyle = 'rgba(239, 68, 68, 0.3)';
|
||||
ctx.fillRect(x1, waveTop, x2 - x1, waveH);
|
||||
}
|
||||
|
||||
// Draw mute ranges (blue overlays)
|
||||
for (const range of muteRanges) {
|
||||
const x1 = (range.start - scroll) * pxPerSec;
|
||||
const x2 = (range.end - scroll) * pxPerSec;
|
||||
ctx.fillStyle = 'rgba(59, 130, 246, 0.3)';
|
||||
ctx.fillRect(x1, waveTop, x2 - x1, waveH);
|
||||
}
|
||||
|
||||
// Draw selection overlay (when in cut/mute mode)
|
||||
if ((cutMode || muteMode) && selectionStart !== null && selectionEnd !== null) {
|
||||
const x1 = (Math.min(selectionStart, selectionEnd) - scroll) * pxPerSec;
|
||||
const x2 = (Math.max(selectionStart, selectionEnd) - scroll) * pxPerSec;
|
||||
ctx.fillStyle = cutMode ? 'rgba(239, 68, 68, 0.5)' : 'rgba(59, 130, 246, 0.5)';
|
||||
ctx.fillRect(x1, waveTop, x2 - x1, waveH);
|
||||
|
||||
// Add border
|
||||
ctx.strokeStyle = cutMode ? '#ef4444' : '#3b82f6';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(x1, waveTop, x2 - x1, waveH);
|
||||
}
|
||||
|
||||
const mid = waveTop + waveH / 2;
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#4a4d5e';
|
||||
@ -228,7 +264,7 @@ export default function WaveformTimeline() {
|
||||
ctx.lineTo(x, mid + max * amp);
|
||||
}
|
||||
ctx.stroke();
|
||||
}, [deletedRanges]);
|
||||
}, [deletedRanges, cutRanges, muteRanges, selectionStart, selectionEnd, cutMode, muteMode]);
|
||||
|
||||
// Keep the ref in sync with the latest drawStaticWaveform closure
|
||||
useEffect(() => {
|
||||
@ -347,27 +383,82 @@ export default function WaveformTimeline() {
|
||||
if (video) video.currentTime = newTime;
|
||||
}, [setCurrentTime]);
|
||||
|
||||
const clientXToTime = useCallback((clientX: number): number => {
|
||||
const buffer = audioBufferRef.current;
|
||||
const canvas = headCanvasRef.current;
|
||||
if (!canvas || !buffer) return 0;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = clientX - rect.left;
|
||||
const pxPerSec = (rect.width * zoomRef.current) / buffer.duration;
|
||||
return Math.max(0, Math.min(buffer.duration, scrollSecsRef.current + x / pxPerSec));
|
||||
}, []);
|
||||
|
||||
const handleMouseDown = useCallback(
|
||||
(e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
e.preventDefault();
|
||||
isDraggingRef.current = true;
|
||||
setIsDragging(true);
|
||||
seekToClientX(e.clientX);
|
||||
|
||||
if (cutMode || muteMode) {
|
||||
// Range selection mode
|
||||
const startTime = clientXToTime(e.clientX);
|
||||
selectionStartRef.current = startTime;
|
||||
setSelectionStart(startTime);
|
||||
setSelectionEnd(startTime);
|
||||
isDraggingRef.current = true;
|
||||
setIsDragging(true);
|
||||
|
||||
const onMove = (ev: MouseEvent) => {
|
||||
if (!isDraggingRef.current) return;
|
||||
seekToClientX(ev.clientX);
|
||||
};
|
||||
const onUp = () => {
|
||||
isDraggingRef.current = false;
|
||||
setIsDragging(false);
|
||||
window.removeEventListener('mousemove', onMove);
|
||||
window.removeEventListener('mouseup', onUp);
|
||||
};
|
||||
window.addEventListener('mousemove', onMove);
|
||||
window.addEventListener('mouseup', onUp);
|
||||
const onMove = (ev: MouseEvent) => {
|
||||
if (!isDraggingRef.current) return;
|
||||
const currentTime = clientXToTime(ev.clientX);
|
||||
setSelectionEnd(currentTime);
|
||||
};
|
||||
|
||||
const onUp = () => {
|
||||
isDraggingRef.current = false;
|
||||
setIsDragging(false);
|
||||
|
||||
if (selectionStartRef.current !== null && selectionEnd !== null) {
|
||||
const start = Math.min(selectionStartRef.current, selectionEnd);
|
||||
const end = Math.max(selectionStartRef.current, selectionEnd);
|
||||
|
||||
if (cutMode) {
|
||||
addCutRange(start, end);
|
||||
} else if (muteMode) {
|
||||
addMuteRange(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset selection
|
||||
selectionStartRef.current = null;
|
||||
setSelectionStart(null);
|
||||
setSelectionEnd(null);
|
||||
|
||||
window.removeEventListener('mousemove', onMove);
|
||||
window.removeEventListener('mouseup', onUp);
|
||||
};
|
||||
|
||||
window.addEventListener('mousemove', onMove);
|
||||
window.addEventListener('mouseup', onUp);
|
||||
} else {
|
||||
// Normal seek mode
|
||||
isDraggingRef.current = true;
|
||||
setIsDragging(true);
|
||||
seekToClientX(e.clientX);
|
||||
|
||||
const onMove = (ev: MouseEvent) => {
|
||||
if (!isDraggingRef.current) return;
|
||||
seekToClientX(ev.clientX);
|
||||
};
|
||||
const onUp = () => {
|
||||
isDraggingRef.current = false;
|
||||
setIsDragging(false);
|
||||
window.removeEventListener('mousemove', onMove);
|
||||
window.removeEventListener('mouseup', onUp);
|
||||
};
|
||||
window.addEventListener('mousemove', onMove);
|
||||
window.addEventListener('mouseup', onUp);
|
||||
}
|
||||
},
|
||||
[seekToClientX],
|
||||
[cutMode, muteMode, clientXToTime, seekToClientX, addCutRange, addMuteRange, selectionEnd],
|
||||
);
|
||||
|
||||
if (!videoUrl) {
|
||||
|
||||
Reference in New Issue
Block a user