clipper/frontend/src/SeekBar.tsx

134 lines
3.2 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 = '#444444';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// seek bar
const pixelRatio = canvas.width / canvas.clientWidth;
const offset = offsetPixels * pixelRatio;
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 = (evt: MouseEvent<HTMLCanvasElement>) => {
const canvas = evt.currentTarget;
const { x } = mouseEventToCanvasPoint(evt);
const pixelRatio = canvas.width / canvas.clientWidth;
const offset = offsetPixels * pixelRatio;
const ratio = (x - offset) / (canvas.width - offset * 2);
onPositionChanged(ratio * duration);
};
// handlers
const handleMouseDown = (evt: MouseEvent<HTMLCanvasElement>) => {
if (mode != Mode.Normal) return;
setMode(Mode.Dragging);
emitPositionEvent(evt);
};
const handleMouseUp = () => {
if (mode != Mode.Dragging) return;
setMode(Mode.Normal);
};
const handleMouseMove = (evt: MouseEvent<HTMLCanvasElement>) => {
const { y } = mouseEventToCanvasPoint(evt);
// TODO: improve mouse detection around knob.
if (y > InnerMargin && y < LogicalHeight - InnerMargin) {
setCursor('cursor-pointer');
} else {
setCursor('cursor-auto');
}
if (mode == Mode.Normal) return;
emitPositionEvent(evt);
};
const handleMouseEnter = () => {
if (mode != Mode.Dragging) return;
setMode(Mode.Normal);
};
// render component
return (
<>
<canvas
className={`w-full h-[30px] mx-0 my-auto ${cursor}`}
ref={canvasRef}
width={LogicalWidth}
height={LogicalHeight}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
onMouseEnter={handleMouseEnter}
></canvas>
</>
);
};