Implement basic zoom in/out
This commit is contained in:
parent
06e4b7f550
commit
faf818e4ae
|
@ -11,12 +11,20 @@ type AudioFile = {
|
||||||
sampleRate: number;
|
sampleRate: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ZoomSettings = {
|
||||||
|
startFrame: number;
|
||||||
|
endFrame: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultZoomSettings: ZoomSettings = { startFrame: 0, endFrame: 0 };
|
||||||
|
|
||||||
export const Waveform: React.FC<WaveformProps> = ({
|
export const Waveform: React.FC<WaveformProps> = ({
|
||||||
audioContext,
|
audioContext,
|
||||||
}: WaveformProps) => {
|
}: WaveformProps) => {
|
||||||
const [audioFile, setAudioFile] = useState<AudioFile | null>(null);
|
const [audioFile, setAudioFile] = useState<AudioFile | null>(null);
|
||||||
const [currentTime, setCurrentTime] = useState(0);
|
const [currentTime, setCurrentTime] = useState(0);
|
||||||
const [audio, setAudio] = useState(new Audio());
|
const [audio, setAudio] = useState(new Audio());
|
||||||
|
const [zoomSettings, setZoomSettings] = useState(defaultZoomSettings);
|
||||||
|
|
||||||
const waveformCanvasRef = useRef<HTMLCanvasElement>(null);
|
const waveformCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const hudCanvasRef = useRef<HTMLCanvasElement>(null);
|
const hudCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
@ -41,13 +49,13 @@ export const Waveform: React.FC<WaveformProps> = ({
|
||||||
return Math.floor((x / canvasLogicalWidth) * audioFile.frames);
|
return Math.floor((x / canvasLogicalWidth) * audioFile.frames);
|
||||||
};
|
};
|
||||||
|
|
||||||
const canvasXToSecs = (x: number): number => {
|
// const canvasXToSecs = (x: number): number => {
|
||||||
if (audioFile == null) {
|
// if (audioFile == null) {
|
||||||
return 0;
|
// return 0;
|
||||||
}
|
// }
|
||||||
const duration = audioFile.frames / audioFile.sampleRate;
|
// const duration = audioFile.frames / audioFile.sampleRate;
|
||||||
return (canvasXToFrame(x) / audioFile.frames) * duration;
|
// return (canvasXToFrame(x) / audioFile.frames) * duration;
|
||||||
};
|
// };
|
||||||
|
|
||||||
const secsToCanvasX = (canvasWidth: number, secs: number): number => {
|
const secsToCanvasX = (canvasWidth: number, secs: number): number => {
|
||||||
if (audioFile == null) {
|
if (audioFile == null) {
|
||||||
|
@ -92,10 +100,11 @@ export const Waveform: React.FC<WaveformProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
setAudioFile(audioFile);
|
setAudioFile(audioFile);
|
||||||
|
setZoomSettings({ startFrame: 0, endFrame: audioFile.frames });
|
||||||
})();
|
})();
|
||||||
}, [audioContext]);
|
}, [audioContext]);
|
||||||
|
|
||||||
// render waveform to canvas when audioData is updated:
|
// render waveform to canvas when zoom settings are updated:
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async function () {
|
(async function () {
|
||||||
if (audioFile == null) {
|
if (audioFile == null) {
|
||||||
|
@ -116,18 +125,21 @@ export const Waveform: React.FC<WaveformProps> = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.strokeStyle = '#00aa00';
|
let endFrame = zoomSettings.endFrame;
|
||||||
ctx.fillStyle = 'black';
|
if (endFrame <= zoomSettings.startFrame) {
|
||||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
endFrame = audioFile.frames;
|
||||||
|
}
|
||||||
|
|
||||||
const resp = await fetch(
|
const resp = await fetch(
|
||||||
`http://localhost:8888/api/peaks?video_id=${videoID}&start=0&end=${Math.round(
|
`http://localhost:8888/api/peaks?video_id=${videoID}&start=${zoomSettings.startFrame}&end=${endFrame}&bins=${canvas.width}`
|
||||||
audioFile.frames
|
|
||||||
)}&bins=${canvas.width}`
|
|
||||||
);
|
);
|
||||||
const peaks = await resp.json();
|
const peaks = await resp.json();
|
||||||
console.log('respBody from peaks =', peaks);
|
console.log('respBody from peaks =', peaks);
|
||||||
|
|
||||||
|
ctx.strokeStyle = '#00aa00';
|
||||||
|
ctx.fillStyle = 'black';
|
||||||
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
const numChannels = peaks.length;
|
const numChannels = peaks.length;
|
||||||
const chanHeight = canvas.height / numChannels;
|
const chanHeight = canvas.height / numChannels;
|
||||||
for (let c = 0; c < numChannels; c++) {
|
for (let c = 0; c < numChannels; c++) {
|
||||||
|
@ -144,7 +156,7 @@ export const Waveform: React.FC<WaveformProps> = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, [audioFile]);
|
}, [zoomSettings]);
|
||||||
|
|
||||||
// redraw HUD
|
// redraw HUD
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -163,6 +175,10 @@ export const Waveform: React.FC<WaveformProps> = ({
|
||||||
|
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
if (audioFile == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const x = secsToCanvasX(canvas.width, currentTime);
|
const x = secsToCanvasX(canvas.width, currentTime);
|
||||||
|
|
||||||
ctx.strokeStyle = 'red';
|
ctx.strokeStyle = 'red';
|
||||||
|
@ -180,14 +196,8 @@ export const Waveform: React.FC<WaveformProps> = ({
|
||||||
console.log('mousemove, x =', canvasX, 'frame =', canvasXToFrame(canvasX));
|
console.log('mousemove, x =', canvasX, 'frame =', canvasXToFrame(canvasX));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseDown = (evt: MouseEvent<HTMLCanvasElement>) => {
|
const handleMouseDown = () => {
|
||||||
if (audioFile == null) {
|
return null;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const canvasX = mouseEventToCanvasX(evt);
|
|
||||||
audio.currentTime = canvasXToSecs(canvasX);
|
|
||||||
console.log('currentTime now', canvasXToSecs(canvasX));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
const handleMouseUp = () => {
|
||||||
|
@ -207,11 +217,29 @@ export const Waveform: React.FC<WaveformProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleZoomIn = () => {
|
const handleZoomIn = () => {
|
||||||
|
if (audioFile == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.log('zoom in');
|
console.log('zoom in');
|
||||||
|
const diff = zoomSettings.endFrame - zoomSettings.startFrame;
|
||||||
|
const endFrame = zoomSettings.startFrame + Math.floor(diff / 2);
|
||||||
|
const settings = { ...zoomSettings, endFrame: endFrame };
|
||||||
|
setZoomSettings(settings);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleZoomOut = () => {
|
const handleZoomOut = () => {
|
||||||
|
if (audioFile == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.log('zoom out');
|
console.log('zoom out');
|
||||||
|
const diff = zoomSettings.endFrame - zoomSettings.startFrame;
|
||||||
|
const newDiff = diff * 2;
|
||||||
|
const endFrame = Math.min(
|
||||||
|
zoomSettings.endFrame + newDiff,
|
||||||
|
audioFile.frames
|
||||||
|
);
|
||||||
|
const settings = { ...zoomSettings, endFrame: endFrame };
|
||||||
|
setZoomSettings(settings);
|
||||||
};
|
};
|
||||||
|
|
||||||
// render component:
|
// render component:
|
||||||
|
@ -222,6 +250,7 @@ export const Waveform: React.FC<WaveformProps> = ({
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
margin: '0 auto',
|
margin: '0 auto',
|
||||||
} as React.CSSProperties;
|
} as React.CSSProperties;
|
||||||
|
|
||||||
const waveformCanvasProps = {
|
const waveformCanvasProps = {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
@ -231,6 +260,7 @@ export const Waveform: React.FC<WaveformProps> = ({
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
zIndex: 0,
|
zIndex: 0,
|
||||||
} as React.CSSProperties;
|
} as React.CSSProperties;
|
||||||
|
|
||||||
const hudCanvasProps = {
|
const hudCanvasProps = {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
@ -240,6 +270,7 @@ export const Waveform: React.FC<WaveformProps> = ({
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
} as React.CSSProperties;
|
} as React.CSSProperties;
|
||||||
|
|
||||||
const clockTextAreaProps = { color: '#999', width: '400px' };
|
const clockTextAreaProps = { color: '#999', width: '400px' };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
Loading…
Reference in New Issue