clipper/frontend/src/HudCanvas.tsx

212 lines
5.2 KiB
TypeScript
Raw Normal View History

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';
interface Styles {
borderLineWidth: number;
borderStrokeStyle: string;
positionLineWidth: number;
positionStrokeStyle: string;
2022-01-17 17:58:50 +00:00
hoverPositionStrokeStyle: string;
}
2021-12-06 22:52:24 +00:00
interface Props {
width: number;
height: number;
2021-12-12 10:04:23 +00:00
emptySelectionAction: EmptySelectionAction;
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,
styles: {
borderLineWidth,
borderStrokeStyle,
positionLineWidth,
positionStrokeStyle,
2022-01-17 17:58:50 +00:00
hoverPositionStrokeStyle,
},
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.
useEffect(() => {
2022-01-24 19:33:16 +00:00
if (!state.shouldPublish) {
return;
}
onSelectionChange({
selection: state.selection,
mode: state.mode,
prevMode: state.prevMode,
});
}, [state]);
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();
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,
borderLineWidth,
2021-12-06 22:52:24 +00:00
currentSelection.end - currentSelection.start,
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
if (position == null) {
return;
}
2021-12-06 22:52:24 +00:00
ctx.beginPath();
ctx.strokeStyle = positionStrokeStyle;
ctx.lineWidth = positionLineWidth;
ctx.moveTo(position, 0);
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
const handleMouseDown = (evt: MouseEvent<HTMLCanvasElement>) => {
2022-01-24 19:33:16 +00:00
if (state.mode != SelectionMode.Normal) {
return;
}
2022-01-24 19:33:16 +00:00
dispatch({ x: getCanvasX(evt), type: 'mousedown' });
};
2021-12-06 22:52:24 +00:00
const handleMouseMove = (evt: MouseEvent<HTMLCanvasElement>) => {
2022-01-24 19:33:16 +00:00
dispatch({ x: getCanvasX(evt), type: 'mousemove' });
};
2021-12-06 22:52:24 +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' });
};
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>
</>
);
};