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; hoverPositionStrokeStyle: string; } interface Props { width: number; height: number; emptySelectionAction: EmptySelectionAction; styles: Styles; position: number | null; selection: Selection; onSelectionChange: (selectionState: SelectionChangeEvent) => void; } export interface SelectionChangeEvent { selection: Selection; mode: SelectionMode; prevMode: SelectionMode; } const emptySelection: Selection = { start: 0, end: 0 }; 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, }; const getCanvasX = (evt: MouseEvent): 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); }; export const HudCanvas: React.FC = ({ width, height, emptySelectionAction, styles: { borderLineWidth, borderStrokeStyle, positionLineWidth, positionStrokeStyle, hoverPositionStrokeStyle, }, position, selection: initialSelection, onSelectionChange, }: Props) => { const canvasRef = useRef(null); const [state, dispatch] = useReducer(stateReducer, { ...initialState, width, emptySelectionAction, selection: initialSelection, }); // side effects useEffect(() => { dispatch({ selection: initialSelection, x: 0, type: 'setselection' }); }, [initialSelection]); // handle global mouse up useEffect(() => { window.addEventListener('mouseup', handleMouseUp); return () => { window.removeEventListener('mouseup', handleMouseUp); }; }, [state]); // trigger onSelectionChange callback. useEffect(() => { // really we only care if hoverX hasn't changed, not checking this will // result in a flood of events being published. if (!state.shouldPublish) { return; } onSelectionChange({ selection: state.selection, mode: state.mode, prevMode: state.prevMode, }); }, [state]); // 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 const currentSelection = state.selection; ctx.beginPath(); ctx.strokeStyle = borderStrokeStyle; ctx.lineWidth = borderLineWidth; const alpha = state.hoverState == HoverState.OverSelection ? '0.15' : '0.13'; ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`; ctx.rect( currentSelection.start, borderLineWidth, currentSelection.end - currentSelection.start, canvas.height - borderLineWidth * 2 ); ctx.fill(); ctx.stroke(); // draw hover position const hoverX = state.hoverX; if ( hoverX != null && (hoverX < currentSelection.start || hoverX > currentSelection.end) ) { ctx.beginPath(); ctx.strokeStyle = hoverPositionStrokeStyle; ctx.lineWidth = 2; ctx.moveTo(hoverX, 0); ctx.lineTo(hoverX, canvas.height); ctx.stroke(); } // draw position marker if (position == null) { return; } ctx.beginPath(); ctx.strokeStyle = positionStrokeStyle; ctx.lineWidth = positionLineWidth; ctx.moveTo(position, 0); ctx.lineWidth = position == 0 ? 8 : 4; ctx.lineTo(position, canvas.height); ctx.stroke(); }); }, [state, position]); const handleMouseDown = (evt: MouseEvent) => { if (state.mode != SelectionMode.Normal) { return; } dispatch({ x: getCanvasX(evt), type: 'mousedown' }); }; const handleMouseMove = (evt: MouseEvent) => { dispatch({ x: getCanvasX(evt), type: 'mousemove' }); }; const handleMouseUp = () => { if (state.mode == SelectionMode.Normal) { return; } dispatch({ x: state.hoverX, type: 'mouseup' }); }; const handleMouseLeave = () => { dispatch({ x: state.hoverX, type: 'mouseleave' }); }; return ( <> ); };