diff --git a/frontend/src/Waveform/Helpers.tsx b/frontend/src/Waveform/Helpers.tsx index 4876e02..42177b6 100644 --- a/frontend/src/Waveform/Helpers.tsx +++ b/frontend/src/Waveform/Helpers.tsx @@ -1,6 +1,11 @@ import { CanvasLogicalWidth } from './Waveform'; import { MouseEvent } from 'react'; +interface Point { + x: number; + y: number; +} + // TODO: add tests export const mouseEventToCanvasX = ( evt: MouseEvent @@ -10,6 +15,20 @@ export const mouseEventToCanvasX = ( return (elementX * CanvasLogicalWidth) / rect.width; }; +// TODO: add tests +export const mouseEventToCanvasPoint = ( + evt: MouseEvent +): Point => { + const rect = evt.currentTarget.getBoundingClientRect(); + const elementX = evt.clientX - rect.left; + const elementY = evt.clientY - rect.top; + + return { + x: (elementX * evt.currentTarget.width) / rect.width, + y: (elementY * evt.currentTarget.height) / rect.height, + }; +}; + // TODO: add tests export const canvasXToFrame = (x: number, numFrames: number): number => { return Math.floor((x / CanvasLogicalWidth) * numFrames); diff --git a/frontend/src/Waveform/SeekBar.tsx b/frontend/src/Waveform/SeekBar.tsx new file mode 100644 index 0000000..debda82 --- /dev/null +++ b/frontend/src/Waveform/SeekBar.tsx @@ -0,0 +1,82 @@ +import { useRef, useEffect, useState, MouseEvent } from 'react'; +import { mouseEventToCanvasPoint } from './Helpers'; + +interface Props { + duration: number; + style: React.CSSProperties; +} + +const LogicalHeight = 200; +const MarginX = 0; +const MarginY = 85; +const KnobRadius = 40; + +export const SeekBar: React.FC = (props: Props) => { + const canvasRef = useRef(null); + const [position, _setPosition] = useState(100); + const [cursor, setCursor] = useState('auto'); + + const secsToCanvasX = (secs: number, width: number): number => { + return (secs / props.duration) * width; + }; + + // draw the canvas + useEffect(() => { + const canvas = canvasRef.current; + if (canvas == null) { + return; + } + + const ctx = canvas.getContext('2d'); + if (ctx == null) { + console.error('no seekbar 2d context available'); + return; + } + + // Set aspect ratio. + canvas.width = canvas.height * (canvas.clientWidth / canvas.clientHeight); + + ctx.fillStyle = '#333333'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.fillStyle = 'black'; + ctx.fillRect( + MarginX, + MarginY, + canvas.width - MarginX * 2, + canvas.height - MarginY * 2 + ); + + const x = secsToCanvasX(position, canvas.width); + const y = LogicalHeight / 2; + ctx.beginPath(); + ctx.arc(x, y, KnobRadius, 0, 2 * Math.PI, false); + ctx.fillStyle = 'red'; + ctx.fill(); + }); + + const style = { ...props.style, cursor: cursor }; + + // handlers + + const handleMouseMove = (evt: MouseEvent) => { + const { x: _x, y: y } = mouseEventToCanvasPoint(evt); + // TODO: improve mouse detection around knob. + if (y > MarginY && y < LogicalHeight - MarginY) { + setCursor('pointer'); + } else { + setCursor('auto'); + } + }; + + return ( + <> + + + ); +}; diff --git a/frontend/src/Waveform/Waveform.tsx b/frontend/src/Waveform/Waveform.tsx index 779effa..e4d4ab2 100644 --- a/frontend/src/Waveform/Waveform.tsx +++ b/frontend/src/Waveform/Waveform.tsx @@ -2,6 +2,7 @@ import { useEffect, useState, useRef, MouseEvent } from 'react'; import { Waveform as WaveformOverview } from './Overview'; import { Thumbnails } from './Thumbnails'; import { Canvas as WaveformCanvas } from './Canvas'; +import { SeekBar } from './SeekBar'; import { canvasXToFrame, mouseEventToCanvasX } from './Helpers'; interface Props { @@ -323,6 +324,12 @@ export const Waveform: React.FC = ({ audioContext }: Props) => { margin: '10px auto 0 auto', display: 'block', }; + const seekBarStyles = { + width: '90%', + height: '50px', + margin: '0 auto', + display: 'block', + }; return ( <> @@ -351,6 +358,10 @@ export const Waveform: React.FC = ({ audioContext }: Props) => { height={CanvasLogicalHeight} > +