141 lines
3.2 KiB
TypeScript
141 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('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('pointer');
|
|
} else {
|
|
setCursor('auto');
|
|
}
|
|
|
|
if (mode == Mode.Normal) return;
|
|
|
|
emitPositionEvent(evt);
|
|
};
|
|
|
|
const handleMouseEnter = () => {
|
|
if (mode != Mode.Dragging) return;
|
|
|
|
setMode(Mode.Normal);
|
|
};
|
|
|
|
// render component
|
|
|
|
const styles = {
|
|
width: '100%',
|
|
height: '30px',
|
|
margin: '0 auto',
|
|
cursor: cursor,
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<canvas
|
|
style={styles}
|
|
ref={canvasRef}
|
|
width={LogicalWidth}
|
|
height={LogicalHeight}
|
|
onMouseDown={handleMouseDown}
|
|
onMouseUp={handleMouseUp}
|
|
onMouseMove={handleMouseMove}
|
|
onMouseEnter={handleMouseEnter}
|
|
></canvas>
|
|
</>
|
|
);
|
|
};
|