import React, { memo, useCallback, useEffect, useRef } from 'react'; import { useSocketStore } from '../../context/socket'; import { useGlobalInfoStore } from "../../context/globalInfo"; import { useActionContext } from '../../context/browserActions'; import DatePicker from '../pickers/DatePicker'; import Dropdown from '../pickers/Dropdown'; import TimePicker from '../pickers/TimePicker'; import DateTimeLocalPicker from '../pickers/DateTimeLocalPicker'; import { coordinateMapper } from '../../helpers/coordinateMapper'; interface CreateRefCallback { (ref: React.RefObject): void; } interface CanvasProps { width: number; height: number; onCreateRef: CreateRefCallback; } /** * Interface for mouse's x,y coordinates */ export interface Coordinates { x: number; y: number; }; const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { const canvasRef = useRef(null); const contextRef = useRef(null); const imageDataRef = useRef(null); const animationFrameRef = useRef(null); const { socket } = useSocketStore(); const { setLastAction, lastAction } = useGlobalInfoStore(); const { getText, getList } = useActionContext(); const getTextRef = useRef(getText); const getListRef = useRef(getList); const MOUSE_MOVE_THROTTLE = 8; const lastMouseMoveTime = useRef(0); const [datePickerInfo, setDatePickerInfo] = React.useState<{ coordinates: Coordinates; selector: string; } | null>(null); const [dropdownInfo, setDropdownInfo] = React.useState<{ coordinates: Coordinates; selector: string; options: Array<{ value: string; text: string; disabled: boolean; selected: boolean; }>; } | null>(null); const [timePickerInfo, setTimePickerInfo] = React.useState<{ coordinates: Coordinates; selector: string; } | null>(null); const [dateTimeLocalInfo, setDateTimeLocalInfo] = React.useState<{ coordinates: Coordinates; selector: string; } | null>(null); const notifyLastAction = (action: string) => { if (lastAction !== action) { setLastAction(action); } }; const lastMousePosition = useRef({ x: 0, y: 0 }); useEffect(() => { if (canvasRef.current && !contextRef.current) { const ctx = canvasRef.current.getContext('2d', { alpha: false, desynchronized: true, willReadFrequently: false }); if (ctx) { contextRef.current = ctx; imageDataRef.current = ctx.createImageData(width, height); } } }, [width, height]); useEffect(() => { getTextRef.current = getText; getListRef.current = getList; }, [getText, getList]); useEffect(() => { if (socket) { const handleDatePicker = (info: { coordinates: Coordinates, selector: string }) => { const canvasCoords = coordinateMapper.mapBrowserToCanvas(info.coordinates); setDatePickerInfo({ ...info, coordinates: canvasCoords }); }; const handleDropdown = (info: { coordinates: Coordinates, selector: string, options: Array<{ value: string; text: string; disabled: boolean; selected: boolean; }>; }) => { const canvasCoords = coordinateMapper.mapBrowserToCanvas(info.coordinates); setDropdownInfo({ ...info, coordinates: canvasCoords }); }; const handleTimePicker = (info: { coordinates: Coordinates, selector: string }) => { const canvasCoords = coordinateMapper.mapBrowserToCanvas(info.coordinates); setTimePickerInfo({ ...info, coordinates: canvasCoords }); }; const handleDateTimePicker = (info: { coordinates: Coordinates, selector: string }) => { const canvasCoords = coordinateMapper.mapBrowserToCanvas(info.coordinates); setDateTimeLocalInfo({ ...info, coordinates: canvasCoords }); }; socket.on('showDatePicker', handleDatePicker); socket.on('showDropdown', handleDropdown); socket.on('showTimePicker', handleTimePicker); socket.on('showDateTimePicker', handleDateTimePicker); return () => { socket.off('showDatePicker', handleDatePicker); socket.off('showDropdown', handleDropdown); socket.off('showTimePicker', handleTimePicker); socket.off('showDateTimePicker', handleDateTimePicker); }; } }, [socket]); const onMouseEvent = useCallback((event: MouseEvent) => { if (!socket || !canvasRef.current) return; const rect = canvasRef.current.getBoundingClientRect(); const clickCoordinates = { x: event.clientX - rect.left, y: event.clientY - rect.top, }; const browserCoordinates = coordinateMapper.mapCanvasToBrowser(clickCoordinates); switch (event.type) { case 'mousedown': if (getTextRef.current === true) { console.log('Capturing Text...'); } else if (getListRef.current === true) { console.log('Capturing List...'); } else { socket.emit('input:mousedown', browserCoordinates); } notifyLastAction('click'); break; case 'mousemove': { const now = performance.now(); if (now - lastMouseMoveTime.current < MOUSE_MOVE_THROTTLE) { return; } lastMouseMoveTime.current = now; const dx = Math.abs(lastMousePosition.current.x - clickCoordinates.x); const dy = Math.abs(lastMousePosition.current.y - clickCoordinates.y); if (dx > 0.5 || dy > 0.5) { lastMousePosition.current = clickCoordinates; socket.emit('input:mousemove', browserCoordinates); notifyLastAction('move'); } break; } case 'wheel': { const wheelEvent = event as WheelEvent; const deltaX = Math.round(wheelEvent.deltaX / 5) * 5; const deltaY = Math.round(wheelEvent.deltaY / 5) * 5; if (Math.abs(deltaX) > 2 || Math.abs(deltaY) > 2) { socket.emit('input:wheel', { deltaX, deltaY }); notifyLastAction('scroll'); } break; } default: return; } }, [socket, notifyLastAction]); const onKeyboardEvent = useCallback((event: KeyboardEvent) => { if (socket) { const browserCoordinates = coordinateMapper.mapCanvasToBrowser(lastMousePosition.current); switch (event.type) { case 'keydown': socket.emit('input:keydown', { key: event.key, coordinates: browserCoordinates }); notifyLastAction(`${event.key} pressed`); break; case 'keyup': socket.emit('input:keyup', event.key); break; default: console.log('Default keyEvent registered'); return; } } }, [socket, notifyLastAction]); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; onCreateRef(canvasRef); const options = { passive: true }; canvas.addEventListener('mousedown', onMouseEvent, options); canvas.addEventListener('mousemove', onMouseEvent, options); canvas.addEventListener('wheel', onMouseEvent, options); canvas.addEventListener('keydown', onKeyboardEvent); canvas.addEventListener('keyup', onKeyboardEvent); return () => { canvas.removeEventListener('mousedown', onMouseEvent); canvas.removeEventListener('mousemove', onMouseEvent); canvas.removeEventListener('wheel', onMouseEvent); canvas.removeEventListener('keydown', onKeyboardEvent); canvas.removeEventListener('keyup', onKeyboardEvent); }; }, [onMouseEvent, onKeyboardEvent, onCreateRef]); useEffect(() => { return () => { if (animationFrameRef.current) { cancelAnimationFrame(animationFrameRef.current); } }; }, []); const containerStyle = React.useMemo(() => ({ borderRadius: '0px 0px 5px 5px', overflow: 'hidden', backgroundColor: 'white', contain: 'layout style paint', isolation: 'isolate' as React.CSSProperties['isolation'] }), []); const canvasStyle = React.useMemo(() => ({ display: 'block', imageRendering: 'crisp-edges' as const, willChange: 'contents', backfaceVisibility: 'hidden' as const, transform: 'translateZ(0)', maxWidth: '100%', maxHeight: '100%' }), []); return (
{datePickerInfo && ( setDatePickerInfo(null)} /> )} {dropdownInfo && ( setDropdownInfo(null)} /> )} {timePickerInfo && ( setTimePickerInfo(null)} /> )} {dateTimeLocalInfo && ( setDateTimeLocalInfo(null)} /> )}
); }; export default memo(Canvas);