feat: optimize canvas perf

This commit is contained in:
Rohit
2025-06-12 14:14:55 +05:30
parent a5ea68655b
commit 50575d90e7

View File

@@ -29,13 +29,17 @@ export interface Coordinates {
const Canvas = ({ width, height, onCreateRef }: CanvasProps) => { const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const contextRef = useRef<CanvasRenderingContext2D | null>(null);
const imageDataRef = useRef<ImageData | null>(null);
const animationFrameRef = useRef<number | null>(null);
const { socket } = useSocketStore(); const { socket } = useSocketStore();
const { setLastAction, lastAction } = useGlobalInfoStore(); const { setLastAction, lastAction } = useGlobalInfoStore();
const { getText, getList } = useActionContext(); const { getText, getList } = useActionContext();
const getTextRef = useRef(getText); const getTextRef = useRef(getText);
const getListRef = useRef(getList); const getListRef = useRef(getList);
const MOUSE_MOVE_THROTTLE = 16; // ~60fps const MOUSE_MOVE_THROTTLE = 8;
const lastMouseMoveTime = useRef(0); const lastMouseMoveTime = useRef(0);
const [datePickerInfo, setDatePickerInfo] = React.useState<{ const [datePickerInfo, setDatePickerInfo] = React.useState<{
@@ -72,6 +76,22 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
const lastMousePosition = useRef<Coordinates>({ x: 0, y: 0 }); const lastMousePosition = useRef<Coordinates>({ 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(() => { useEffect(() => {
getTextRef.current = getText; getTextRef.current = getText;
getListRef.current = getList; getListRef.current = getList;
@@ -79,115 +99,100 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
useEffect(() => { useEffect(() => {
if (socket) { if (socket) {
socket.on('showDatePicker', (info: { coordinates: Coordinates, selector: string }) => { const handleDatePicker = (info: { coordinates: Coordinates, selector: string }) => {
const canvasCoords = coordinateMapper.mapBrowserToCanvas(info.coordinates); const canvasCoords = coordinateMapper.mapBrowserToCanvas(info.coordinates);
setDatePickerInfo({ setDatePickerInfo({ ...info, coordinates: canvasCoords });
...info, };
coordinates: canvasCoords
});
});
socket.on('showDropdown', (info: { const handleDropdown = (info: {
coordinates: Coordinates, coordinates: Coordinates,
selector: string, selector: string,
options: Array<{ options: Array<{ value: string; text: string; disabled: boolean; selected: boolean; }>;
value: string;
text: string;
disabled: boolean;
selected: boolean;
}>;
}) => { }) => {
const canvasCoords = coordinateMapper.mapBrowserToCanvas(info.coordinates); const canvasCoords = coordinateMapper.mapBrowserToCanvas(info.coordinates);
setDropdownInfo({ setDropdownInfo({ ...info, coordinates: canvasCoords });
...info, };
coordinates: canvasCoords
});
});
socket.on('showTimePicker', (info: { coordinates: Coordinates, selector: string }) => { const handleTimePicker = (info: { coordinates: Coordinates, selector: string }) => {
const canvasCoords = coordinateMapper.mapBrowserToCanvas(info.coordinates); const canvasCoords = coordinateMapper.mapBrowserToCanvas(info.coordinates);
setTimePickerInfo({ setTimePickerInfo({ ...info, coordinates: canvasCoords });
...info, };
coordinates: canvasCoords
});
});
socket.on('showDateTimePicker', (info: { coordinates: Coordinates, selector: string }) => { const handleDateTimePicker = (info: { coordinates: Coordinates, selector: string }) => {
const canvasCoords = coordinateMapper.mapBrowserToCanvas(info.coordinates); const canvasCoords = coordinateMapper.mapBrowserToCanvas(info.coordinates);
setDateTimeLocalInfo({ setDateTimeLocalInfo({ ...info, coordinates: canvasCoords });
...info, };
coordinates: canvasCoords
}); socket.on('showDatePicker', handleDatePicker);
}); socket.on('showDropdown', handleDropdown);
socket.on('showTimePicker', handleTimePicker);
socket.on('showDateTimePicker', handleDateTimePicker);
return () => { return () => {
socket.off('showDatePicker'); socket.off('showDatePicker', handleDatePicker);
socket.off('showDropdown'); socket.off('showDropdown', handleDropdown);
socket.off('showTimePicker'); socket.off('showTimePicker', handleTimePicker);
socket.off('showDateTimePicker'); socket.off('showDateTimePicker', handleDateTimePicker);
}; };
} }
}, [socket]); }, [socket]);
const onMouseEvent = useCallback((event: MouseEvent) => { const onMouseEvent = useCallback((event: MouseEvent) => {
if (socket && canvasRef.current) { if (!socket || !canvasRef.current) return;
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
};
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) { const browserCoordinates = coordinateMapper.mapCanvasToBrowser(clickCoordinates);
case 'mousedown':
if (getTextRef.current === true) { switch (event.type) {
console.log('Capturing Text...'); case 'mousedown':
} else if (getListRef.current === true) { if (getTextRef.current === true) {
console.log('Capturing List...'); console.log('Capturing Text...');
} else { } else if (getListRef.current === true) {
socket.emit('input:mousedown', browserCoordinates); console.log('Capturing List...');
} } else {
notifyLastAction('click'); socket.emit('input:mousedown', browserCoordinates);
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;
} }
notifyLastAction('click');
break;
// Optimize wheel events case 'mousemove': {
case 'wheel': { const now = performance.now();
const wheelEvent = event as WheelEvent; if (now - lastMouseMoveTime.current < MOUSE_MOVE_THROTTLE) {
const deltaX = Math.round(wheelEvent.deltaX / 10) * 10; return;
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;
} }
default: lastMouseMoveTime.current = now;
console.log('Default mouseEvent registered');
return; 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) => { const onKeyboardEvent = useCallback((event: KeyboardEvent) => {
if (socket) { if (socket) {
@@ -206,47 +211,66 @@ const Canvas = ({ width, height, onCreateRef }: CanvasProps) => {
return; return;
} }
} }
}, [socket]); }, [socket, notifyLastAction]);
useEffect(() => { useEffect(() => {
if (canvasRef.current) { const canvas = canvasRef.current;
onCreateRef(canvasRef); if (!canvas) return;
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);
return () => { onCreateRef(canvasRef);
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);
}
}; const options = { passive: true };
} else {
console.log('Canvas not initialized'); 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<React.CSSProperties>(() => ({
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 ( return (
<div style={{ borderRadius: '0px 0px 5px 5px', overflow: 'hidden', backgroundColor: 'white' }}> <div style={containerStyle}>
<canvas <canvas
tabIndex={0} tabIndex={0}
ref={canvasRef} ref={canvasRef}
height={height} height={height}
width={width} width={width}
style={{ style={canvasStyle}
display: 'block',
imageRendering: 'crisp-edges',
willChange: 'transform',
transform: 'translateZ(0)'
}}
/> />
{datePickerInfo && ( {datePickerInfo && (
<DatePicker <DatePicker