157 lines
3.8 KiB
TypeScript
157 lines
3.8 KiB
TypeScript
import { useEffect, useState, useRef, MouseEvent } from 'react';
|
|
import { Canvas as WaveformCanvas } from './Canvas';
|
|
import { CanvasLogicalWidth, CanvasLogicalHeight, Selection } from './Waveform';
|
|
import { mouseEventToCanvasX } from './Helpers';
|
|
|
|
interface Props {
|
|
peaks: number[][] | null;
|
|
numFrames: number;
|
|
style: React.CSSProperties;
|
|
onSelectionStart: (x1: number) => void;
|
|
onSelectionChange: (selection: Selection) => void;
|
|
}
|
|
|
|
enum Mode {
|
|
Normal,
|
|
Selecting,
|
|
}
|
|
|
|
// TODO: render position marker during playback
|
|
export const Waveform: React.FC<Props> = (props: Props) => {
|
|
const hudCanvasRef = useRef<HTMLCanvasElement>(null);
|
|
const [mode, setMode] = useState(Mode.Normal);
|
|
|
|
const defaultSelection: Selection = { x1: 0, x2: 0 };
|
|
// selection is the current selection in canvas coordinates:
|
|
const [selection, setSelection] = useState(defaultSelection);
|
|
// newSelection is a new selection in the process of being drawn by the user.
|
|
// It is only useful if Mode.Selecting is active.
|
|
const [newSelection, setNewSelection] = useState(defaultSelection);
|
|
|
|
// effects
|
|
|
|
// draw the overview waveform
|
|
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);
|
|
|
|
let currentSelection: Selection;
|
|
if (mode == Mode.Selecting) {
|
|
currentSelection = newSelection;
|
|
} else {
|
|
currentSelection = selection;
|
|
}
|
|
if (currentSelection.x1 >= currentSelection.x2) {
|
|
return;
|
|
}
|
|
|
|
ctx.beginPath();
|
|
ctx.strokeStyle = 'red';
|
|
ctx.lineWidth = 2;
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
|
|
ctx.rect(
|
|
currentSelection.x1,
|
|
2,
|
|
currentSelection.x2 - currentSelection.x1,
|
|
canvas.height - 8
|
|
);
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
})();
|
|
});
|
|
|
|
// handlers
|
|
|
|
const handleMouseDown = (evt: MouseEvent<HTMLCanvasElement>) => {
|
|
if (mode != Mode.Normal) {
|
|
return;
|
|
}
|
|
|
|
setMode(Mode.Selecting);
|
|
|
|
const x = mouseEventToCanvasX(evt);
|
|
setNewSelection({ x1: x, x2: x });
|
|
props.onSelectionStart(x);
|
|
};
|
|
|
|
const handleMouseMove = (evt: MouseEvent<HTMLCanvasElement>) => {
|
|
if (mode != Mode.Selecting) {
|
|
return;
|
|
}
|
|
|
|
const x = mouseEventToCanvasX(evt);
|
|
if (x == newSelection.x2) {
|
|
return;
|
|
}
|
|
|
|
setNewSelection({ ...newSelection, x2: x });
|
|
};
|
|
|
|
const handleMouseUp = () => {
|
|
if (mode != Mode.Selecting) {
|
|
return;
|
|
}
|
|
setMode(Mode.Normal);
|
|
|
|
// TODO: better shallow equality check?
|
|
if (selection.x1 !== newSelection.x1 || selection.x2 !== newSelection.x2) {
|
|
setSelection(newSelection);
|
|
props.onSelectionChange(newSelection);
|
|
}
|
|
};
|
|
|
|
// render component
|
|
|
|
const canvasStyles = {
|
|
width: '100%',
|
|
height: '100%',
|
|
margin: '0 auto',
|
|
display: 'block',
|
|
} as React.CSSProperties;
|
|
|
|
const hudCanvasStyles = {
|
|
width: '100%',
|
|
height: '100%',
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
zIndex: 1,
|
|
} as React.CSSProperties;
|
|
|
|
return (
|
|
<>
|
|
<div style={props.style}>
|
|
<WaveformCanvas
|
|
peaks={props.peaks}
|
|
fillStyle="grey"
|
|
strokeStyle="black"
|
|
style={canvasStyles}
|
|
></WaveformCanvas>
|
|
<canvas
|
|
ref={hudCanvasRef}
|
|
style={hudCanvasStyles}
|
|
width={CanvasLogicalWidth}
|
|
height={CanvasLogicalHeight}
|
|
onMouseDown={handleMouseDown}
|
|
onMouseMove={handleMouseMove}
|
|
onMouseUp={handleMouseUp}
|
|
></canvas>
|
|
</div>
|
|
</>
|
|
);
|
|
};
|