Extract OverviewWaveform to its own component

This commit is contained in:
Rob Watson 2021-09-12 09:13:02 +02:00
parent 5a08bd62bf
commit 712fbd3142
3 changed files with 96 additions and 35 deletions

View File

@ -1,5 +1,6 @@
import { useEffect, useState, useRef, MouseEvent } from 'react'; import { useEffect, useState, useRef, MouseEvent } from 'react';
import { WaveformCanvas } from './WaveformCanvas'; import { Waveform as WaveformOverview } from './Waveform/Overview';
import { Canvas as WaveformCanvas } from './Waveform/Canvas';
type Props = { type Props = {
audioContext: AudioContext; audioContext: AudioContext;
@ -19,18 +20,20 @@ type ZoomSettings = {
const defaultZoomSettings: ZoomSettings = { startFrame: 0, endFrame: 0 }; const defaultZoomSettings: ZoomSettings = { startFrame: 0, endFrame: 0 };
export const CanvasLogicalWidth = 2000;
export const CanvasLogicalHeight = 500;
export const Waveform: React.FC<Props> = ({ audioContext }: Props) => { export const Waveform: React.FC<Props> = ({ audioContext }: Props) => {
const [audioFile, setAudioFile] = useState<AudioFile | null>(null); const [audioFile, setAudioFile] = useState<AudioFile | null>(null);
const [currentTime, setCurrentTime] = useState(0); const [currentTime, setCurrentTime] = useState(0);
// TODO: fix linter error
const [audio, setAudio] = useState(new Audio()); const [audio, setAudio] = useState(new Audio());
const [zoomSettings, setZoomSettings] = useState(defaultZoomSettings); const [zoomSettings, setZoomSettings] = useState(defaultZoomSettings);
const [waveformPeaks, setWaveformPeaks] = useState(null); const [waveformPeaks, setWaveformPeaks] = useState(null);
const [overviewPeaks, setOverviewPeaks] = useState(null); const [overviewPeaks, setOverviewPeaks] = useState(null);
const hudCanvasRef = useRef<HTMLCanvasElement>(null); const hudCanvasRef = useRef<HTMLCanvasElement>(null);
const canvasLogicalWidth = 2000;
const canvasLogicalHeight = 500;
// TODO: error handling
const videoID = new URLSearchParams(window.location.search).get('video_id'); const videoID = new URLSearchParams(window.location.search).get('video_id');
// helpers // helpers
@ -47,7 +50,7 @@ export const Waveform: React.FC<Props> = ({ audioContext }: Props) => {
if (audioFile == null) { if (audioFile == null) {
return 0; return 0;
} }
return Math.floor((x / canvasLogicalWidth) * audioFile.frames); return Math.floor((x / CanvasLogicalWidth) * audioFile.frames);
}; };
const secsToCanvasX = (canvasWidth: number, secs: number): number => { const secsToCanvasX = (canvasWidth: number, secs: number): number => {
@ -112,7 +115,7 @@ export const Waveform: React.FC<Props> = ({ audioContext }: Props) => {
} }
const resp = await fetch( const resp = await fetch(
`http://localhost:8888/api/peaks?video_id=${videoID}&start=${zoomSettings.startFrame}&end=${endFrame}&bins=${canvasLogicalWidth}` `http://localhost:8888/api/peaks?video_id=${videoID}&start=${zoomSettings.startFrame}&end=${endFrame}&bins=${CanvasLogicalWidth}`
); );
const peaks = await resp.json(); const peaks = await resp.json();
console.log('respBody from peaks =', peaks); console.log('respBody from peaks =', peaks);
@ -240,13 +243,10 @@ export const Waveform: React.FC<Props> = ({ audioContext }: Props) => {
zIndex: 1, zIndex: 1,
} as React.CSSProperties; } as React.CSSProperties;
const overviewCanvasProps = { const overviewStyles = { ...wrapperProps, height: '90px' };
width: '90%',
height: '90px',
margin: '0 auto',
display: 'block',
} as React.CSSProperties;
// TODO: why is the margin needed?
const controlPanelStyles = { margin: '1em' } as React.CSSProperties;
const clockTextAreaProps = { color: '#999', width: '400px' }; const clockTextAreaProps = { color: '#999', width: '400px' };
return ( return (
@ -255,35 +255,31 @@ export const Waveform: React.FC<Props> = ({ audioContext }: Props) => {
<div style={wrapperProps}> <div style={wrapperProps}>
<WaveformCanvas <WaveformCanvas
peaks={waveformPeaks} peaks={waveformPeaks}
logicalWidth={canvasLogicalWidth}
logicalHeight={canvasLogicalHeight}
fillStyle="black" fillStyle="black"
strokeStyle="green" strokeStyle="green"
style={waveformCanvasProps} style={waveformCanvasProps}
></WaveformCanvas> ></WaveformCanvas>
<canvas <canvas
ref={hudCanvasRef} ref={hudCanvasRef}
width={canvasLogicalWidth}
height={canvasLogicalHeight}
onMouseMove={handleMouseMove} onMouseMove={handleMouseMove}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp} onMouseUp={handleMouseUp}
style={hudCanvasProps} style={hudCanvasProps}
width={CanvasLogicalWidth}
height={CanvasLogicalHeight}
></canvas> ></canvas>
</div> </div>
<WaveformCanvas <WaveformOverview
peaks={overviewPeaks} peaks={overviewPeaks}
logicalWidth={canvasLogicalWidth} style={overviewStyles}
logicalHeight={canvasLogicalHeight} ></WaveformOverview>
fillStyle="grey" <div style={controlPanelStyles}>
strokeStyle="black"
style={overviewCanvasProps}
></WaveformCanvas>
<button onClick={handlePlay}>Play</button> <button onClick={handlePlay}>Play</button>
<button onClick={handlePause}>Pause</button> <button onClick={handlePause}>Pause</button>
<button onClick={handleZoomIn}>+</button> <button onClick={handleZoomIn}>+</button>
<button onClick={handleZoomOut}>-</button> <button onClick={handleZoomOut}>-</button>
<input type="readonly" style={clockTextAreaProps} /> <input type="readonly" style={clockTextAreaProps} />
</div>
</> </>
); );
}; };

View File

@ -1,17 +1,24 @@
import { useEffect, useState, useRef, MouseEvent } from 'react'; import { useEffect, useRef } from 'react';
import { CanvasLogicalWidth, CanvasLogicalHeight } from '../Waveform';
const maxPeakValue = 32768; const maxPeakValue = 32_768;
type Props = { type Props = {
peaks: number[][] | null; peaks: number[][] | null;
logicalWidth: number;
logicalHeight: number;
strokeStyle: string; strokeStyle: string;
fillStyle: string; fillStyle: string;
style: React.CSSProperties; style: React.CSSProperties;
}; };
export const WaveformCanvas: React.FC<Props> = (props: Props) => { // Canvas is a generic component that renders a waveform to a canvas.
//
// Properties:
//
// peaks: a 2d array of uint16s representing the peak values. Each inner array length should match logicalWidth
// strokeStyle: waveform style
// fillStyle: background style
// style: React.CSSProperties applied to canvas element
export const Canvas: React.FC<Props> = (props: Props) => {
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => { useEffect(() => {
@ -57,8 +64,8 @@ export const WaveformCanvas: React.FC<Props> = (props: Props) => {
<> <>
<canvas <canvas
ref={canvasRef} ref={canvasRef}
width={props.logicalWidth} width={CanvasLogicalWidth}
height={props.logicalHeight} height={CanvasLogicalHeight}
style={props.style} style={props.style}
></canvas> ></canvas>
</> </>

View File

@ -0,0 +1,58 @@
import { useEffect, useRef } from 'react';
import { Canvas as WaveformCanvas } from './Canvas';
import { CanvasLogicalWidth, CanvasLogicalHeight } from '../Waveform';
type Props = {
peaks: number[][] | null;
style: React.CSSProperties;
};
export const Waveform: React.FC<Props> = (props: Props) => {
const hudCanvasRef = useRef<HTMLCanvasElement>(null);
// handlers
const handleMouseDown = () => {
console.log('mousedown');
};
// render component
const canvasStyles = {
width: '100%',
height: '100px',
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}
></canvas>
</div>
</>
);
};