2022-01-24 19:33:16 +00:00
|
|
|
import { useEffect, useRef, useReducer, MouseEvent } from 'react';
|
|
|
|
import {
|
|
|
|
stateReducer,
|
|
|
|
State,
|
|
|
|
SelectionMode,
|
|
|
|
HoverState,
|
|
|
|
Selection,
|
|
|
|
EmptySelectionAction,
|
|
|
|
} from './HudCanvasState';
|
|
|
|
import constrainNumeric from './helpers/constrainNumeric';
|
|
|
|
export { EmptySelectionAction } from './HudCanvasState';
|
2021-12-11 16:25:43 +00:00
|
|
|
|
|
|
|
interface Styles {
|
|
|
|
borderLineWidth: number;
|
|
|
|
borderStrokeStyle: string;
|
|
|
|
positionLineWidth: number;
|
|
|
|
positionStrokeStyle: string;
|
2022-01-17 17:58:50 +00:00
|
|
|
hoverPositionStrokeStyle: string;
|
2021-12-11 16:25:43 +00:00
|
|
|
}
|
2021-12-06 22:52:24 +00:00
|
|
|
|
|
|
|
interface Props {
|
|
|
|
width: number;
|
|
|
|
height: number;
|
2021-12-12 10:04:23 +00:00
|
|
|
emptySelectionAction: EmptySelectionAction;
|
2021-12-11 16:25:43 +00:00
|
|
|
styles: Styles;
|
|
|
|
position: number | null;
|
2021-12-06 22:52:24 +00:00
|
|
|
selection: Selection;
|
2022-01-24 19:33:16 +00:00
|
|
|
onSelectionChange: (selectionState: SelectionChangeEvent) => void;
|
2021-12-06 22:52:24 +00:00
|
|
|
}
|
|
|
|
|
2022-01-24 19:33:16 +00:00
|
|
|
export interface SelectionChangeEvent {
|
|
|
|
selection: Selection;
|
|
|
|
mode: SelectionMode;
|
|
|
|
prevMode: SelectionMode;
|
2021-12-06 22:52:24 +00:00
|
|
|
}
|
|
|
|
|
2022-01-24 19:33:16 +00:00
|
|
|
const emptySelection: Selection = { start: 0, end: 0 };
|
2021-12-12 10:04:23 +00:00
|
|
|
|
2022-01-24 19:33:16 +00:00
|
|
|
const initialState: State = {
|
|
|
|
width: 0,
|
|
|
|
emptySelectionAction: EmptySelectionAction.SelectNothing,
|
|
|
|
hoverX: 0,
|
|
|
|
selection: emptySelection,
|
|
|
|
origSelection: emptySelection,
|
|
|
|
mousedownX: 0,
|
|
|
|
mode: SelectionMode.Normal,
|
|
|
|
prevMode: SelectionMode.Normal,
|
|
|
|
cursorClass: 'cursor-auto',
|
|
|
|
hoverState: HoverState.Normal,
|
|
|
|
shouldPublish: false,
|
|
|
|
};
|
2021-12-06 22:52:24 +00:00
|
|
|
|
2022-01-24 19:33:16 +00:00
|
|
|
const getCanvasX = (evt: MouseEvent<HTMLCanvasElement>): number => {
|
|
|
|
const rect = evt.currentTarget.getBoundingClientRect();
|
|
|
|
const x = Math.round(
|
|
|
|
((evt.clientX - rect.left) / rect.width) * evt.currentTarget.width
|
|
|
|
);
|
|
|
|
return constrainNumeric(x, evt.currentTarget.width);
|
|
|
|
};
|
2021-12-06 22:52:24 +00:00
|
|
|
|
|
|
|
export const HudCanvas: React.FC<Props> = ({
|
|
|
|
width,
|
|
|
|
height,
|
2021-12-12 10:04:23 +00:00
|
|
|
emptySelectionAction,
|
2021-12-11 16:25:43 +00:00
|
|
|
styles: {
|
|
|
|
borderLineWidth,
|
|
|
|
borderStrokeStyle,
|
|
|
|
positionLineWidth,
|
|
|
|
positionStrokeStyle,
|
2022-01-17 17:58:50 +00:00
|
|
|
hoverPositionStrokeStyle,
|
2021-12-11 16:25:43 +00:00
|
|
|
},
|
2021-12-06 22:52:24 +00:00
|
|
|
position,
|
2022-01-24 19:33:16 +00:00
|
|
|
selection: initialSelection,
|
2021-12-06 22:52:24 +00:00
|
|
|
onSelectionChange,
|
|
|
|
}: Props) => {
|
|
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
2022-01-24 19:33:16 +00:00
|
|
|
|
|
|
|
const [state, dispatch] = useReducer(stateReducer, {
|
|
|
|
...initialState,
|
|
|
|
width,
|
|
|
|
emptySelectionAction,
|
|
|
|
selection: initialSelection,
|
|
|
|
});
|
2021-12-06 22:52:24 +00:00
|
|
|
|
|
|
|
// side effects
|
|
|
|
|
2022-01-24 19:33:16 +00:00
|
|
|
useEffect(() => {
|
|
|
|
dispatch({ selection: initialSelection, x: 0, type: 'setselection' });
|
|
|
|
}, [initialSelection]);
|
|
|
|
|
2021-12-06 22:52:24 +00:00
|
|
|
// handle global mouse up
|
|
|
|
useEffect(() => {
|
|
|
|
window.addEventListener('mouseup', handleMouseUp);
|
|
|
|
return () => {
|
|
|
|
window.removeEventListener('mouseup', handleMouseUp);
|
|
|
|
};
|
2022-01-24 19:33:16 +00:00
|
|
|
}, [state]);
|
2021-12-06 22:52:24 +00:00
|
|
|
|
2022-01-24 19:33:16 +00:00
|
|
|
// trigger onSelectionChange callback.
|
2022-01-15 17:14:16 +00:00
|
|
|
useEffect(() => {
|
2022-01-24 19:33:16 +00:00
|
|
|
if (!state.shouldPublish) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
onSelectionChange({
|
|
|
|
selection: state.selection,
|
|
|
|
mode: state.mode,
|
|
|
|
prevMode: state.prevMode,
|
|
|
|
});
|
|
|
|
}, [state]);
|
2022-01-15 17:14:16 +00:00
|
|
|
|
2021-12-06 22:52:24 +00:00
|
|
|
// draw the overview HUD
|
|
|
|
useEffect(() => {
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
const canvas = canvasRef.current;
|
|
|
|
if (canvas == null) {
|
|
|
|
console.error('no hud canvas ref available');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
if (ctx == null) {
|
|
|
|
console.error('no hud 2d context available');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
|
|
|
|
// draw selection
|
|
|
|
|
2022-01-24 19:33:16 +00:00
|
|
|
const currentSelection = state.selection;
|
2021-12-06 22:52:24 +00:00
|
|
|
|
|
|
|
ctx.beginPath();
|
2021-12-11 16:25:43 +00:00
|
|
|
ctx.strokeStyle = borderStrokeStyle;
|
|
|
|
ctx.lineWidth = borderLineWidth;
|
2022-01-24 19:33:16 +00:00
|
|
|
const alpha =
|
|
|
|
state.hoverState == HoverState.OverSelection ? '0.15' : '0.13';
|
2021-12-06 22:52:24 +00:00
|
|
|
ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
|
|
|
|
ctx.rect(
|
|
|
|
currentSelection.start,
|
2021-12-11 16:25:43 +00:00
|
|
|
borderLineWidth,
|
2021-12-06 22:52:24 +00:00
|
|
|
currentSelection.end - currentSelection.start,
|
2021-12-11 16:25:43 +00:00
|
|
|
canvas.height - borderLineWidth * 2
|
2021-12-06 22:52:24 +00:00
|
|
|
);
|
|
|
|
ctx.fill();
|
|
|
|
ctx.stroke();
|
|
|
|
|
2022-01-17 17:58:50 +00:00
|
|
|
// draw hover position
|
2022-01-24 19:33:16 +00:00
|
|
|
|
|
|
|
const hoverX = state.hoverX;
|
2022-01-17 17:58:50 +00:00
|
|
|
if (
|
2022-01-24 19:33:16 +00:00
|
|
|
hoverX != null &&
|
|
|
|
(hoverX < currentSelection.start || hoverX > currentSelection.end)
|
2022-01-17 17:58:50 +00:00
|
|
|
) {
|
|
|
|
ctx.beginPath();
|
|
|
|
ctx.strokeStyle = hoverPositionStrokeStyle;
|
|
|
|
ctx.lineWidth = 2;
|
2022-01-24 19:33:16 +00:00
|
|
|
ctx.moveTo(hoverX, 0);
|
|
|
|
ctx.lineTo(hoverX, canvas.height);
|
2022-01-17 17:58:50 +00:00
|
|
|
ctx.stroke();
|
|
|
|
}
|
|
|
|
|
2021-12-06 22:52:24 +00:00
|
|
|
// draw position marker
|
|
|
|
|
2021-12-11 16:25:43 +00:00
|
|
|
if (position == null) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-12-06 22:52:24 +00:00
|
|
|
ctx.beginPath();
|
2021-12-11 16:25:43 +00:00
|
|
|
ctx.strokeStyle = positionStrokeStyle;
|
|
|
|
ctx.lineWidth = positionLineWidth;
|
|
|
|
ctx.moveTo(position, 0);
|
2022-01-17 17:58:13 +00:00
|
|
|
ctx.lineWidth = position == 0 ? 8 : 4;
|
|
|
|
ctx.lineTo(position, canvas.height);
|
2021-12-06 22:52:24 +00:00
|
|
|
ctx.stroke();
|
|
|
|
});
|
2022-01-24 19:33:16 +00:00
|
|
|
}, [state, position]);
|
2021-12-06 22:52:24 +00:00
|
|
|
|
2022-01-14 11:24:59 +00:00
|
|
|
const handleMouseDown = (evt: MouseEvent<HTMLCanvasElement>) => {
|
2022-01-24 19:33:16 +00:00
|
|
|
if (state.mode != SelectionMode.Normal) {
|
2022-01-14 11:24:59 +00:00
|
|
|
return;
|
|
|
|
}
|
2022-01-24 19:33:16 +00:00
|
|
|
dispatch({ x: getCanvasX(evt), type: 'mousedown' });
|
2022-01-14 11:24:59 +00:00
|
|
|
};
|
2021-12-06 22:52:24 +00:00
|
|
|
|
2022-01-14 11:24:59 +00:00
|
|
|
const handleMouseMove = (evt: MouseEvent<HTMLCanvasElement>) => {
|
2022-01-24 19:33:16 +00:00
|
|
|
dispatch({ x: getCanvasX(evt), type: 'mousemove' });
|
2022-01-14 11:24:59 +00:00
|
|
|
};
|
2021-12-06 22:52:24 +00:00
|
|
|
|
2022-01-14 11:24:59 +00:00
|
|
|
const handleMouseUp = () => {
|
2022-01-24 19:33:16 +00:00
|
|
|
if (state.mode == SelectionMode.Normal) {
|
2021-12-06 22:52:24 +00:00
|
|
|
return;
|
|
|
|
}
|
2022-01-24 19:33:16 +00:00
|
|
|
dispatch({ x: state.hoverX, type: 'mouseup' });
|
2022-01-14 11:24:59 +00:00
|
|
|
};
|
2021-12-12 10:04:23 +00:00
|
|
|
|
2022-01-24 19:33:16 +00:00
|
|
|
const handleMouseLeave = () => {
|
|
|
|
dispatch({ x: state.hoverX, type: 'mouseleave' });
|
2021-12-06 22:52:24 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<canvas
|
|
|
|
ref={canvasRef}
|
2022-01-24 19:33:16 +00:00
|
|
|
className={`block absolute w-full h-full ${state.cursorClass} z-20`}
|
2021-12-06 22:52:24 +00:00
|
|
|
width={width}
|
|
|
|
height={height}
|
|
|
|
onMouseDown={handleMouseDown}
|
|
|
|
onMouseMove={handleMouseMove}
|
|
|
|
onMouseLeave={handleMouseLeave}
|
|
|
|
></canvas>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|