clipper/frontend/src/SeekBar.tsx

160 lines
3.7 KiB
TypeScript

import { useRef, useEffect, useState, MouseEvent } from 'react';
import { mouseEventToCanvasPoint } from './Helpers';
interface Props {
position: number;
duration: number;
offsetPixels: number;
onPositionChanged: (posiiton: number) => void;
}
enum Mode {
Normal,
Dragging,
}
const LogicalWidth = 2000;
const LogicalHeight = 100;
const InnerMargin = 40;
export const SeekBar: React.FC<Props> = ({
position,
duration,
offsetPixels,
onPositionChanged,
}: Props) => {
const [mode, setMode] = useState(Mode.Normal);
const [cursor, setCursor] = useState('cursor-auto');
const canvasRef = useRef<HTMLCanvasElement>(null);
// render canvas
useEffect(() => {
const canvas = canvasRef.current;
if (canvas == null) {
console.error('no seekbar canvas ref available');
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);
// background
ctx.fillStyle = 'transparent';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// seek bar
const offset = offsetCanvas(canvas);
const width = canvas.width - offset * 2;
ctx.fillStyle = 'black';
ctx.fillRect(offset, InnerMargin, width, canvas.height - InnerMargin * 2);
// pointer
const positionRatio = position / duration;
const x = offset + width * positionRatio;
const y = canvas.height / 2;
ctx.beginPath();
ctx.arc(x, y, 20, 0, 2 * Math.PI, false);
ctx.fillStyle = 'green';
ctx.fill();
});
// helpers
const emitPositionEvent = (x: number, canvas: HTMLCanvasElement) => {
const pixelRatio = canvas.width / canvas.clientWidth;
const offset = offsetPixels * pixelRatio;
const ratio = (x - offset) / (canvas.width - offset * 2);
onPositionChanged(ratio * duration);
};
const offsetCanvas = (canvas: HTMLCanvasElement): number => {
return Math.round(offsetPixels * (canvas.width / canvas.clientWidth));
};
// handlers
const handleMouseDown = (evt: MouseEvent<HTMLCanvasElement>) => {
if (mode != Mode.Normal) return;
const canvas = evt.currentTarget;
const offset = offsetCanvas(canvas);
const { x } = mouseEventToCanvasPoint(evt);
if (x < offset || x > evt.currentTarget.width - offset) {
return;
}
setMode(Mode.Dragging);
emitPositionEvent(x, canvas);
};
const handleMouseUp = () => {
if (mode != Mode.Dragging) return;
setMode(Mode.Normal);
};
const handleMouseMove = (evt: MouseEvent<HTMLCanvasElement>) => {
const canvas = evt.currentTarget;
const offset = offsetCanvas(canvas);
const coords = mouseEventToCanvasPoint(evt);
const { y } = coords;
let { x } = coords;
// TODO: improve mouse detection around knob.
if (
x >= offset &&
x < canvas.width - offset &&
y > InnerMargin &&
y < LogicalHeight - InnerMargin
) {
setCursor('cursor-pointer');
} else {
setCursor('cursor-auto');
}
if (x < offset) {
x = offset;
}
if (x > canvas.width - offset) {
x = canvas.width - offset;
}
if (mode == Mode.Normal) return;
emitPositionEvent(x, canvas);
};
const handleMouseEnter = () => {
if (mode != Mode.Dragging) return;
setMode(Mode.Normal);
};
// render component
return (
<>
<canvas
className={`w-full bg-gray-700 h-10 mx-0 my-auto ${cursor}`}
ref={canvasRef}
width={LogicalWidth}
height={LogicalHeight}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
onMouseEnter={handleMouseEnter}
></canvas>
</>
);
};