clipper/frontend/src/Overview.tsx

253 lines
6.2 KiB
TypeScript
Raw Normal View History

2021-10-08 14:38:35 +00:00
import { useState, useEffect, useRef, MouseEvent } from 'react';
2021-11-06 20:52:47 +00:00
import { MediaSetServiceClientImpl, MediaSet } from './generated/media_set';
import { Frames, newRPC } from './App';
2021-10-08 14:38:35 +00:00
import { WaveformCanvas } from './WaveformCanvas';
import { mouseEventToCanvasX } from './Helpers';
import { secsToCanvasX } from './Helpers';
2021-11-06 20:52:47 +00:00
import { from, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
2021-10-08 14:38:35 +00:00
interface Props {
mediaSet: MediaSet;
height: number;
offsetPixels: number;
position: number;
selection: Frames;
2021-10-08 14:38:35 +00:00
onSelectionStart: (x1: number) => void;
onSelectionChange: (selection: Frames) => void;
}
enum Mode {
Normal,
Selecting,
Dragging,
}
2021-11-06 20:52:47 +00:00
const CanvasLogicalWidth = 2_000;
2021-10-08 14:38:35 +00:00
const CanvasLogicalHeight = 500;
const emptySelection = { start: 0, end: 0 };
// TODO: render position marker during playback
export const Overview: React.FC<Props> = ({
mediaSet,
height,
offsetPixels,
position,
selection,
2021-10-08 14:38:35 +00:00
onSelectionStart,
onSelectionChange,
}: Props) => {
const hudCanvasRef = useRef<HTMLCanvasElement>(null);
2021-11-06 20:52:47 +00:00
const [peaks, setPeaks] = useState<Observable<number[]>>(from([]));
2021-10-08 14:38:35 +00:00
const [mode, setMode] = useState(Mode.Normal);
const [newSelection, setNewSelection] = useState({ ...emptySelection });
const [dragStart, setDragStart] = useState(0);
// effects
// load peaks on mediaset change
useEffect(() => {
(async function () {
if (mediaSet == null) {
return;
}
2021-11-06 20:52:47 +00:00
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;
}
console.log('fetching audio...');
const service = new MediaSetServiceClientImpl(newRPC());
2021-11-06 20:52:47 +00:00
const audioProgressStream = service.GetAudio({
id: mediaSet.id,
numBins: CanvasLogicalWidth,
});
2021-11-06 20:52:47 +00:00
const peaks = audioProgressStream.pipe(map((progress) => progress.peaks));
setPeaks(peaks);
2021-10-08 14:38:35 +00:00
})();
}, [mediaSet]);
2021-11-06 20:52:47 +00:00
// draw the overview HUD
2021-10-08 14:38:35 +00:00
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);
// draw selection:
let currentSelection: Frames;
if (mode == Mode.Selecting || mode == Mode.Dragging) {
currentSelection = newSelection;
} else {
currentSelection = selection;
}
if (currentSelection.start < currentSelection.end) {
const x1 =
2021-11-02 16:20:47 +00:00
(currentSelection.start / mediaSet.audioFrames) * CanvasLogicalWidth;
2021-10-08 14:38:35 +00:00
const x2 =
2021-11-02 16:20:47 +00:00
(currentSelection.end / mediaSet.audioFrames) * CanvasLogicalWidth;
2021-10-08 14:38:35 +00:00
ctx.beginPath();
ctx.strokeStyle = 'red';
ctx.lineWidth = 4;
ctx.fillStyle = 'rgba(255, 255, 255, 0.15)';
ctx.rect(x1, 2, x2 - x1, canvas.height - 10);
ctx.fill();
ctx.stroke();
}
// draw position marker:
2021-11-02 16:20:47 +00:00
const fullSelection = { start: 0, end: mediaSet.audioFrames }; // constantize?
2021-10-08 14:38:35 +00:00
const x = secsToCanvasX(
position,
2021-11-02 16:20:47 +00:00
mediaSet.audioSampleRate,
2021-10-08 14:38:35 +00:00
fullSelection
);
// should never happen:
if (x == null) {
return;
}
ctx.strokeStyle = 'red';
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineWidth = 4;
ctx.lineTo(x, canvas.height - 4);
ctx.stroke();
})();
});
// publish event on new selection start
useEffect(() => {
onSelectionStart(newSelection.start);
}, [newSelection]);
// handlers
const handleMouseDown = (evt: MouseEvent<HTMLCanvasElement>) => {
if (mode != Mode.Normal) {
return;
}
const frame = Math.floor(
2021-11-02 16:20:47 +00:00
mediaSet.audioFrames *
2021-10-08 14:38:35 +00:00
(mouseEventToCanvasX(evt) / evt.currentTarget.width)
);
if (frame >= selection.start && frame < selection.end) {
setMode(Mode.Dragging);
setDragStart(frame);
return;
}
setMode(Mode.Selecting);
setNewSelection({ start: frame, end: frame });
};
const handleMouseMove = (evt: MouseEvent<HTMLCanvasElement>) => {
if (mode == Mode.Normal) {
return;
}
const frame = Math.floor(
2021-11-02 16:20:47 +00:00
mediaSet.audioFrames *
2021-10-08 14:38:35 +00:00
(mouseEventToCanvasX(evt) / evt.currentTarget.width)
);
if (mode == Mode.Dragging) {
const diff = frame - dragStart;
const frameCount = selection.end - selection.start;
let start = Math.max(0, selection.start + diff);
let end = start + frameCount;
2021-11-02 16:20:47 +00:00
if (end > mediaSet.audioFrames) {
end = mediaSet.audioFrames;
2021-10-08 14:38:35 +00:00
start = end - frameCount;
}
setNewSelection({
start: start,
end: end,
});
return;
}
if (frame == newSelection.end) {
return;
}
setNewSelection({ ...newSelection, end: frame });
};
const handleMouseUp = () => {
if (mode == Mode.Normal) {
return;
}
setMode(Mode.Normal);
onSelectionChange({ ...newSelection });
2021-10-08 14:38:35 +00:00
};
// render component
const containerStyles = {
flexGrow: 0,
position: 'relative',
margin: `0 ${offsetPixels}px`,
height: `${height}px`,
} as React.CSSProperties;
const hudCanvasStyles = {
position: 'absolute',
width: '100%',
height: '100%',
display: 'block',
zIndex: 2,
} as React.CSSProperties;
return (
<>
<div style={containerStyles}>
<WaveformCanvas
peaks={peaks}
2021-11-06 20:52:47 +00:00
channels={mediaSet.audioChannels}
2021-10-08 14:38:35 +00:00
width={CanvasLogicalWidth}
height={CanvasLogicalHeight}
strokeStyle="black"
fillStyle="#003300"
zIndex={1}
alpha={1}
></WaveformCanvas>
<canvas
ref={hudCanvasRef}
width={CanvasLogicalWidth}
height={CanvasLogicalHeight}
style={hudCanvasStyles}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
></canvas>
</div>
</>
);
};