Zoom to overview selection
This commit is contained in:
parent
712fbd3142
commit
193073015d
|
@ -1,18 +1,28 @@
|
||||||
import { useEffect, useState, useRef, MouseEvent } from 'react';
|
import { useEffect, useState, useRef, MouseEvent } from 'react';
|
||||||
import { Waveform as WaveformOverview } from './Waveform/Overview';
|
import { Waveform as WaveformOverview } from './Waveform/Overview';
|
||||||
import { Canvas as WaveformCanvas } from './Waveform/Canvas';
|
import { Canvas as WaveformCanvas } from './Waveform/Canvas';
|
||||||
|
import {
|
||||||
|
secsToCanvasX,
|
||||||
|
canvasXToFrame,
|
||||||
|
mouseEventToCanvasX,
|
||||||
|
} from './Waveform/Helpers';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
audioContext: AudioContext;
|
audioContext: AudioContext;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AudioFile = {
|
export type AudioFile = {
|
||||||
bytes: number;
|
bytes: number;
|
||||||
channels: number;
|
channels: number;
|
||||||
frames: number;
|
frames: number;
|
||||||
sampleRate: number;
|
sampleRate: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Selection = {
|
||||||
|
x1: number;
|
||||||
|
x2: number;
|
||||||
|
};
|
||||||
|
|
||||||
type ZoomSettings = {
|
type ZoomSettings = {
|
||||||
startFrame: number;
|
startFrame: number;
|
||||||
endFrame: number;
|
endFrame: number;
|
||||||
|
@ -36,31 +46,6 @@ export const Waveform: React.FC<Props> = ({ audioContext }: Props) => {
|
||||||
// TODO: error handling
|
// TODO: error handling
|
||||||
const videoID = new URLSearchParams(window.location.search).get('video_id');
|
const videoID = new URLSearchParams(window.location.search).get('video_id');
|
||||||
|
|
||||||
// helpers
|
|
||||||
|
|
||||||
const mouseEventToCanvasX = (evt: MouseEvent<HTMLCanvasElement>): number => {
|
|
||||||
// TODO: use offsetX/offsetY?
|
|
||||||
const rect = evt.currentTarget.getBoundingClientRect();
|
|
||||||
const elementX = evt.clientX - rect.left;
|
|
||||||
const canvas = evt.target as HTMLCanvasElement;
|
|
||||||
return (elementX * canvas.width) / rect.width;
|
|
||||||
};
|
|
||||||
|
|
||||||
const canvasXToFrame = (x: number): number => {
|
|
||||||
if (audioFile == null) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return Math.floor((x / CanvasLogicalWidth) * audioFile.frames);
|
|
||||||
};
|
|
||||||
|
|
||||||
const secsToCanvasX = (canvasWidth: number, secs: number): number => {
|
|
||||||
if (audioFile == null) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const duration = audioFile.frames / audioFile.sampleRate;
|
|
||||||
return Math.floor(canvasWidth * (secs / duration));
|
|
||||||
};
|
|
||||||
|
|
||||||
// effects
|
// effects
|
||||||
|
|
||||||
// setup player on page load:
|
// setup player on page load:
|
||||||
|
@ -149,7 +134,11 @@ export const Waveform: React.FC<Props> = ({ audioContext }: Props) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const x = secsToCanvasX(canvas.width, currentTime);
|
const x = secsToCanvasX(
|
||||||
|
currentTime,
|
||||||
|
audioFile.sampleRate,
|
||||||
|
audioFile.frames
|
||||||
|
);
|
||||||
|
|
||||||
ctx.strokeStyle = 'red';
|
ctx.strokeStyle = 'red';
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
|
@ -162,8 +151,16 @@ export const Waveform: React.FC<Props> = ({ audioContext }: Props) => {
|
||||||
// callbacks
|
// callbacks
|
||||||
|
|
||||||
const handleMouseMove = (evt: MouseEvent<HTMLCanvasElement>) => {
|
const handleMouseMove = (evt: MouseEvent<HTMLCanvasElement>) => {
|
||||||
|
if (audioFile == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const canvasX = mouseEventToCanvasX(evt);
|
const canvasX = mouseEventToCanvasX(evt);
|
||||||
console.log('mousemove, x =', canvasX, 'frame =', canvasXToFrame(canvasX));
|
console.log(
|
||||||
|
'mousemove, x =',
|
||||||
|
canvasX,
|
||||||
|
'frame =',
|
||||||
|
canvasXToFrame(canvasX, numFrames)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseDown = () => {
|
const handleMouseDown = () => {
|
||||||
|
@ -212,6 +209,17 @@ export const Waveform: React.FC<Props> = ({ audioContext }: Props) => {
|
||||||
setZoomSettings(settings);
|
setZoomSettings(settings);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSelectionChange = (selection: Selection) => {
|
||||||
|
if (audioFile == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const settings: ZoomSettings = {
|
||||||
|
startFrame: canvasXToFrame(selection.x1, audioFile.frames),
|
||||||
|
endFrame: canvasXToFrame(selection.x2, audioFile.frames),
|
||||||
|
};
|
||||||
|
setZoomSettings(settings);
|
||||||
|
};
|
||||||
|
|
||||||
// render component:
|
// render component:
|
||||||
|
|
||||||
const wrapperProps = {
|
const wrapperProps = {
|
||||||
|
@ -249,6 +257,11 @@ export const Waveform: React.FC<Props> = ({ audioContext }: Props) => {
|
||||||
const controlPanelStyles = { margin: '1em' } as React.CSSProperties;
|
const controlPanelStyles = { margin: '1em' } as React.CSSProperties;
|
||||||
const clockTextAreaProps = { color: '#999', width: '400px' };
|
const clockTextAreaProps = { color: '#999', width: '400px' };
|
||||||
|
|
||||||
|
let numFrames = 0;
|
||||||
|
if (audioFile != null) {
|
||||||
|
numFrames = audioFile.frames;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1>clipper</h1>
|
<h1>clipper</h1>
|
||||||
|
@ -271,7 +284,9 @@ export const Waveform: React.FC<Props> = ({ audioContext }: Props) => {
|
||||||
</div>
|
</div>
|
||||||
<WaveformOverview
|
<WaveformOverview
|
||||||
peaks={overviewPeaks}
|
peaks={overviewPeaks}
|
||||||
|
numFrames={numFrames}
|
||||||
style={overviewStyles}
|
style={overviewStyles}
|
||||||
|
onSelectionChange={handleSelectionChange}
|
||||||
></WaveformOverview>
|
></WaveformOverview>
|
||||||
<div style={controlPanelStyles}>
|
<div style={controlPanelStyles}>
|
||||||
<button onClick={handlePlay}>Play</button>
|
<button onClick={handlePlay}>Play</button>
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { CanvasLogicalWidth } from '../Waveform';
|
||||||
|
import { MouseEvent } from 'react';
|
||||||
|
|
||||||
|
// TODO: add tests
|
||||||
|
export const mouseEventToCanvasX = (
|
||||||
|
evt: MouseEvent<HTMLCanvasElement>
|
||||||
|
): number => {
|
||||||
|
// TODO: use offsetX/offsetY?
|
||||||
|
const rect = evt.currentTarget.getBoundingClientRect();
|
||||||
|
const elementX = evt.clientX - rect.left;
|
||||||
|
return (elementX * CanvasLogicalWidth) / rect.width;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: add tests
|
||||||
|
export const canvasXToFrame = (x: number, numFrames: number): number => {
|
||||||
|
return Math.floor((x / CanvasLogicalWidth) * numFrames);
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: add tests
|
||||||
|
export const secsToCanvasX = (
|
||||||
|
secs: number,
|
||||||
|
sampleRate: number,
|
||||||
|
numFrames: number
|
||||||
|
): number => {
|
||||||
|
const duration = numFrames / sampleRate;
|
||||||
|
return Math.floor(CanvasLogicalWidth * (secs / duration));
|
||||||
|
};
|
|
@ -1,26 +1,104 @@
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useState, useRef, MouseEvent } from 'react';
|
||||||
import { Canvas as WaveformCanvas } from './Canvas';
|
import { Canvas as WaveformCanvas } from './Canvas';
|
||||||
import { CanvasLogicalWidth, CanvasLogicalHeight } from '../Waveform';
|
import {
|
||||||
|
CanvasLogicalWidth,
|
||||||
|
CanvasLogicalHeight,
|
||||||
|
Selection,
|
||||||
|
} from '../Waveform';
|
||||||
|
import { mouseEventToCanvasX } from './Helpers';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
peaks: number[][] | null;
|
peaks: number[][] | null;
|
||||||
|
numFrames: number;
|
||||||
style: React.CSSProperties;
|
style: React.CSSProperties;
|
||||||
|
onSelectionChange: (selection: Selection) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum Mode {
|
||||||
|
Normal,
|
||||||
|
Selecting,
|
||||||
|
}
|
||||||
|
|
||||||
export const Waveform: React.FC<Props> = (props: Props) => {
|
export const Waveform: React.FC<Props> = (props: Props) => {
|
||||||
const hudCanvasRef = useRef<HTMLCanvasElement>(null);
|
const hudCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
|
const [mode, setMode] = useState(Mode.Normal);
|
||||||
|
|
||||||
|
const defaultSelection: Selection = { x1: 0, x2: 0 };
|
||||||
|
const [selection, setSelection] = useState(defaultSelection);
|
||||||
|
// TODO: is it needed?
|
||||||
|
const [newStartSelection, setNewStartSelection] = useState(0);
|
||||||
|
|
||||||
|
// effects
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async function () {
|
||||||
|
const canvas = hudCanvasRef.current;
|
||||||
|
if (canvas == null) {
|
||||||
|
console.error('no hud canvas ref available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (ctx == null) {
|
||||||
|
console.error('no hud 2d context available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
if (selection.x1 >= selection.x2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.strokeStyle = 'red';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
|
||||||
|
ctx.rect(selection.x1, 2, selection.x2 - selection.x1, canvas.height - 8);
|
||||||
|
ctx.fill();
|
||||||
|
ctx.stroke();
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
// handlers
|
// handlers
|
||||||
|
|
||||||
const handleMouseDown = () => {
|
const handleMouseDown = (evt: MouseEvent<HTMLCanvasElement>) => {
|
||||||
console.log('mousedown');
|
console.log('mousedown');
|
||||||
|
if (mode != Mode.Normal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMode(Mode.Selecting);
|
||||||
|
setNewStartSelection(mouseEventToCanvasX(evt));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = (evt: MouseEvent<HTMLCanvasElement>) => {
|
||||||
|
console.log('mousemove');
|
||||||
|
if (mode != Mode.Selecting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selection: Selection = {
|
||||||
|
x1: newStartSelection,
|
||||||
|
x2: mouseEventToCanvasX(evt),
|
||||||
|
};
|
||||||
|
setSelection(selection);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
console.log('mouseup');
|
||||||
|
if (mode != Mode.Selecting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setMode(Mode.Normal);
|
||||||
|
props.onSelectionChange(selection);
|
||||||
};
|
};
|
||||||
|
|
||||||
// render component
|
// render component
|
||||||
|
|
||||||
const canvasStyles = {
|
const canvasStyles = {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100px',
|
height: '100%',
|
||||||
margin: '0 auto',
|
margin: '0 auto',
|
||||||
display: 'block',
|
display: 'block',
|
||||||
} as React.CSSProperties;
|
} as React.CSSProperties;
|
||||||
|
@ -51,6 +129,8 @@ export const Waveform: React.FC<Props> = (props: Props) => {
|
||||||
width={CanvasLogicalWidth}
|
width={CanvasLogicalWidth}
|
||||||
height={CanvasLogicalHeight}
|
height={CanvasLogicalHeight}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
></canvas>
|
></canvas>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
Loading…
Reference in New Issue