From 50575d90e7deb61dad2d9f48dfcf608e7ba7dd65 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 12 Jun 2025 14:14:55 +0530 Subject: [PATCH] feat: optimize canvas perf --- src/components/recorder/canvas.tsx | 252 ++++++++++++++++------------- 1 file changed, 138 insertions(+), 114 deletions(-) diff --git a/src/components/recorder/canvas.tsx b/src/components/recorder/canvas.tsx index 52d29b00..c8c7502c 100644 --- a/src/components/recorder/canvas.tsx +++ b/src/components/recorder/canvas.tsx @@ -29,13 +29,17 @@ export interface Coordinates { 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 = 16; // ~60fps + const MOUSE_MOVE_THROTTLE = 8; const lastMouseMoveTime = useRef(0); const [datePickerInfo, setDatePickerInfo] = React.useState<{ @@ -72,6 +76,22 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { 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; @@ -79,115 +99,100 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { useEffect(() => { if (socket) { - socket.on('showDatePicker', (info: { coordinates: Coordinates, selector: string }) => { + const handleDatePicker = (info: { coordinates: Coordinates, selector: string }) => { const canvasCoords = coordinateMapper.mapBrowserToCanvas(info.coordinates); - setDatePickerInfo({ - ...info, - coordinates: canvasCoords - }); - }); + setDatePickerInfo({ ...info, coordinates: canvasCoords }); + }; - socket.on('showDropdown', (info: { + const handleDropdown = (info: { coordinates: Coordinates, selector: string, - options: Array<{ - value: string; - text: string; - disabled: boolean; - selected: boolean; - }>; + options: Array<{ value: string; text: string; disabled: boolean; selected: boolean; }>; }) => { const canvasCoords = coordinateMapper.mapBrowserToCanvas(info.coordinates); - setDropdownInfo({ - ...info, - coordinates: canvasCoords - }); - }); + setDropdownInfo({ ...info, coordinates: canvasCoords }); + }; - socket.on('showTimePicker', (info: { coordinates: Coordinates, selector: string }) => { + const handleTimePicker = (info: { coordinates: Coordinates, selector: string }) => { const canvasCoords = coordinateMapper.mapBrowserToCanvas(info.coordinates); - setTimePickerInfo({ - ...info, - coordinates: canvasCoords - }); - }); + setTimePickerInfo({ ...info, coordinates: canvasCoords }); + }; - socket.on('showDateTimePicker', (info: { coordinates: Coordinates, selector: string }) => { + const handleDateTimePicker = (info: { coordinates: Coordinates, selector: string }) => { const canvasCoords = coordinateMapper.mapBrowserToCanvas(info.coordinates); - setDateTimeLocalInfo({ - ...info, - coordinates: canvasCoords - }); - }); + setDateTimeLocalInfo({ ...info, coordinates: canvasCoords }); + }; + + socket.on('showDatePicker', handleDatePicker); + socket.on('showDropdown', handleDropdown); + socket.on('showTimePicker', handleTimePicker); + socket.on('showDateTimePicker', handleDateTimePicker); return () => { - socket.off('showDatePicker'); - socket.off('showDropdown'); - socket.off('showTimePicker'); - socket.off('showDateTimePicker'); + 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) { - const rect = canvasRef.current.getBoundingClientRect(); - const clickCoordinates = { - x: event.clientX - rect.left, // Use relative x coordinate - y: event.clientY - rect.top, // Use relative y coordinate - }; + if (!socket || !canvasRef.current) return; - const browserCoordinates = coordinateMapper.mapCanvasToBrowser(clickCoordinates); + const rect = canvasRef.current.getBoundingClientRect(); + const clickCoordinates = { + x: event.clientX - rect.left, + y: event.clientY - rect.top, + }; - 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 > 1 || dy > 1) { - lastMousePosition.current = { - x: clickCoordinates.x, - y: clickCoordinates.y, - }; - socket.emit('input:mousemove', browserCoordinates); - notifyLastAction('move'); - } - break; + 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; - // Optimize wheel events - case 'wheel': { - const wheelEvent = event as WheelEvent; - const deltaX = Math.round(wheelEvent.deltaX / 10) * 10; - const deltaY = Math.round(wheelEvent.deltaY / 10) * 10; - - if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) { - socket.emit('input:wheel', { deltaX, deltaY }); - notifyLastAction('scroll'); - } - break; + case 'mousemove': { + const now = performance.now(); + if (now - lastMouseMoveTime.current < MOUSE_MOVE_THROTTLE) { + return; } - default: - console.log('Default mouseEvent registered'); - 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]); + }, [socket, notifyLastAction]); const onKeyboardEvent = useCallback((event: KeyboardEvent) => { if (socket) { @@ -206,47 +211,66 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { return; } } - }, [socket]); + }, [socket, notifyLastAction]); useEffect(() => { - if (canvasRef.current) { - onCreateRef(canvasRef); - canvasRef.current.addEventListener('mousedown', onMouseEvent); - canvasRef.current.addEventListener('mousemove', onMouseEvent); - canvasRef.current.addEventListener('wheel', onMouseEvent, { passive: true }); - canvasRef.current.addEventListener('keydown', onKeyboardEvent); - canvasRef.current.addEventListener('keyup', onKeyboardEvent); + const canvas = canvasRef.current; + if (!canvas) return; - return () => { - if (canvasRef.current) { - canvasRef.current.removeEventListener('mousedown', onMouseEvent); - canvasRef.current.removeEventListener('mousemove', onMouseEvent); - canvasRef.current.removeEventListener('wheel', onMouseEvent); - canvasRef.current.removeEventListener('keydown', onKeyboardEvent); - canvasRef.current.removeEventListener('keyup', onKeyboardEvent); - } + onCreateRef(canvasRef); - }; - } else { - console.log('Canvas not initialized'); - } + 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); - }, [onMouseEvent]); + 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 && (