Files
parcer/src/components/atoms/canvas.tsx

332 lines
11 KiB
TypeScript
Raw Normal View History

2025-01-05 22:23:52 +05:30
// Canvas.tsx
import React, { useCallback, useEffect, useRef, useMemo } from 'react';
2024-06-14 21:56:50 +05:30
import { useSocketStore } from '../../context/socket';
import { useGlobalInfoStore } from "../../context/globalInfo";
2024-07-25 22:44:18 +05:30
import { useActionContext } from '../../context/browserActions';
import DatePicker from './DatePicker';
import Dropdown from './Dropdown';
import TimePicker from './TimePicker';
import DateTimeLocalPicker from './DateTimeLocalPicker';
import { FrontendPerformanceMonitor } from '../../../perf/performance';
2024-06-14 21:56:50 +05:30
2025-01-05 22:23:52 +05:30
// Types
2024-06-14 21:56:50 +05:30
interface CreateRefCallback {
(ref: React.RefObject<HTMLCanvasElement>): void;
}
2024-06-14 21:56:50 +05:30
interface CanvasProps {
width: number;
height: number;
onCreateRef: CreateRefCallback;
}
export interface Coordinates {
x: number;
y: number;
2025-01-05 22:23:52 +05:30
}
interface DropdownOption {
value: string;
text: string;
disabled: boolean;
selected: boolean;
}
interface CanvasState {
datePickerInfo: {
coordinates: Coordinates;
selector: string;
} | null;
dropdownInfo: {
coordinates: Coordinates;
selector: string;
options: DropdownOption[];
} | null;
timePickerInfo: {
coordinates: Coordinates;
selector: string;
} | null;
dateTimeLocalInfo: {
coordinates: Coordinates;
selector: string;
} | null;
}
type CanvasAction =
| { type: 'SET_DATE_PICKER'; payload: CanvasState['datePickerInfo'] }
| { type: 'SET_DROPDOWN'; payload: CanvasState['dropdownInfo'] }
| { type: 'SET_TIME_PICKER'; payload: CanvasState['timePickerInfo'] }
| { type: 'SET_DATETIME_PICKER'; payload: CanvasState['dateTimeLocalInfo'] };
// Helper functions
const throttle = <T extends (...args: any[]) => any>(func: T, limit: number): T => {
let inThrottle = false;
return ((...args: Parameters<T>): ReturnType<T> | void => {
if (!inThrottle) {
func.apply(null, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
}) as T;
};
const createOffscreenCanvas = (width: number, height: number) => {
if (typeof OffscreenCanvas !== 'undefined') {
return new OffscreenCanvas(width, height);
}
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
return canvas;
2024-07-23 10:19:11 +05:30
};
2024-06-14 21:56:50 +05:30
2025-01-05 22:23:52 +05:30
// Reducer
const canvasReducer = (state: CanvasState, action: CanvasAction): CanvasState => {
switch (action.type) {
case 'SET_DATE_PICKER':
return { ...state, datePickerInfo: action.payload };
case 'SET_DROPDOWN':
return { ...state, dropdownInfo: action.payload };
case 'SET_TIME_PICKER':
return { ...state, timePickerInfo: action.payload };
case 'SET_DATETIME_PICKER':
return { ...state, dateTimeLocalInfo: action.payload };
default:
return state;
}
};
2025-01-05 22:23:52 +05:30
// Main Component
const Canvas = React.memo(({ width, height, onCreateRef }: CanvasProps) => {
// Refs
const performanceMonitor = useRef(new FrontendPerformanceMonitor());
2024-06-14 21:56:50 +05:30
const canvasRef = useRef<HTMLCanvasElement>(null);
2025-01-05 22:23:52 +05:30
const lastMousePosition = useRef<Coordinates>({ x: 0, y: 0 });
const frameRequest = useRef<number>();
const renderingContext = useRef<CanvasRenderingContext2D | null>(null);
const offscreenCanvas = useRef<HTMLCanvasElement | OffscreenCanvas>(
createOffscreenCanvas(width || 900, height || 400)
);
// Hooks
2024-06-14 21:56:50 +05:30
const { socket } = useSocketStore();
2024-07-23 20:08:40 +05:30
const { setLastAction, lastAction } = useGlobalInfoStore();
2024-08-08 00:41:32 +05:30
const { getText, getList } = useActionContext();
const getTextRef = useRef(getText);
2024-08-08 00:41:32 +05:30
const getListRef = useRef(getList);
2024-06-14 21:56:50 +05:30
2025-01-05 22:23:52 +05:30
// State
const [state, dispatch] = React.useReducer(canvasReducer, {
datePickerInfo: null,
dropdownInfo: null,
timePickerInfo: null,
dateTimeLocalInfo: null
});
2025-01-05 22:23:52 +05:30
// Memoized values
const canvasSize = useMemo(() => ({
width: width || 900,
height: height || 400
}), [width, height]);
2025-01-05 22:23:52 +05:30
const notifyLastAction = useCallback((action: string) => {
2024-07-23 20:08:40 +05:30
if (lastAction !== action) {
setLastAction(action);
}
2025-01-05 22:23:52 +05:30
}, [lastAction, setLastAction]);
2024-07-23 20:08:40 +05:30
2025-01-05 22:23:52 +05:30
// Socket event handlers
const socketHandlers = useMemo(() => ({
showDatePicker: (info: CanvasState['datePickerInfo']) => {
dispatch({ type: 'SET_DATE_PICKER', payload: info });
},
showDropdown: (info: CanvasState['dropdownInfo']) => {
dispatch({ type: 'SET_DROPDOWN', payload: info });
},
showTimePicker: (info: CanvasState['timePickerInfo']) => {
dispatch({ type: 'SET_TIME_PICKER', payload: info });
},
showDateTimePicker: (info: CanvasState['dateTimeLocalInfo']) => {
dispatch({ type: 'SET_DATETIME_PICKER', payload: info });
}
}), []);
2024-07-25 22:44:50 +05:30
2025-01-05 22:23:52 +05:30
// Event handlers
const handleMouseMove = useCallback(
throttle((coordinates: Coordinates) => {
if (!socket) return;
2025-01-05 22:23:52 +05:30
if (
lastMousePosition.current.x !== coordinates.x ||
lastMousePosition.current.y !== coordinates.y
) {
lastMousePosition.current = coordinates;
socket.emit('input:mousemove', coordinates);
notifyLastAction('move');
}
}, 16),
[socket, notifyLastAction]
);
2025-01-05 22:23:52 +05:30
const onMouseEvent = useCallback((event: MouseEvent) => {
performanceMonitor.current.measureEventLatency(event);
if (!socket || !canvasRef.current) return;
2025-01-05 22:23:52 +05:30
const rect = canvasRef.current.getBoundingClientRect();
const clickCoordinates = {
x: event.clientX - rect.left,
y: event.clientY - rect.top,
};
2025-01-05 22:23:52 +05:30
switch (event.type) {
case 'mousedown':
if (getTextRef.current) {
console.log('Capturing Text...');
} else if (getListRef.current) {
console.log('Capturing List...');
} else {
socket.emit('input:mousedown', clickCoordinates);
}
notifyLastAction('click');
break;
2025-01-05 22:23:52 +05:30
case 'mousemove':
handleMouseMove(clickCoordinates);
break;
2025-01-05 22:23:52 +05:30
case 'wheel':
if (frameRequest.current) {
cancelAnimationFrame(frameRequest.current);
}
frameRequest.current = requestAnimationFrame(() => {
2024-06-14 21:56:50 +05:30
const wheelEvent = event as WheelEvent;
2025-01-05 22:23:52 +05:30
socket.emit('input:wheel', {
2024-06-14 21:56:50 +05:30
deltaX: Math.round(wheelEvent.deltaX),
deltaY: Math.round(wheelEvent.deltaY),
2025-01-05 22:23:52 +05:30
});
2024-07-23 20:09:30 +05:30
notifyLastAction('scroll');
2025-01-05 22:23:52 +05:30
});
break;
2024-06-14 21:56:50 +05:30
}
2025-01-05 22:23:52 +05:30
}, [socket, handleMouseMove, notifyLastAction]);
2024-06-14 21:56:50 +05:30
const onKeyboardEvent = useCallback((event: KeyboardEvent) => {
2025-01-05 22:23:52 +05:30
if (!socket) return;
switch (event.type) {
case 'keydown':
socket.emit('input:keydown', {
key: event.key,
coordinates: lastMousePosition.current
});
notifyLastAction(`${event.key} pressed`);
break;
case 'keyup':
socket.emit('input:keyup', event.key);
break;
2024-06-14 21:56:50 +05:30
}
2025-01-05 22:23:52 +05:30
}, [socket, notifyLastAction]);
2024-07-23 10:19:11 +05:30
2025-01-05 22:23:52 +05:30
// Effects
useEffect(() => {
2025-01-05 22:23:52 +05:30
getTextRef.current = getText;
getListRef.current = getList;
}, [getText, getList]);
useEffect(() => {
if (!socket) return;
Object.entries(socketHandlers).forEach(([event, handler]) => {
socket.on(event, handler);
});
return () => {
Object.keys(socketHandlers).forEach(event => {
socket.off(event);
});
};
}, [socket, socketHandlers]);
useEffect(() => {
const monitor = performanceMonitor.current;
const intervalId = setInterval(() => {
2025-01-05 22:23:52 +05:30
const report = monitor.getPerformanceReport();
console.log('Frontend Performance Report:', report);
2025-01-05 22:23:52 +05:30
}, 10000);
2025-01-05 22:23:52 +05:30
return () => {
clearInterval(intervalId);
if (frameRequest.current) {
cancelAnimationFrame(frameRequest.current);
}
};
}, []);
2024-06-14 21:56:50 +05:30
useEffect(() => {
2025-01-05 22:23:52 +05:30
if (!canvasRef.current) return;
2025-01-05 22:23:52 +05:30
renderingContext.current = canvasRef.current.getContext('2d');
onCreateRef(canvasRef);
const canvas = canvasRef.current;
canvas.addEventListener('mousedown', onMouseEvent);
canvas.addEventListener('mousemove', onMouseEvent);
canvas.addEventListener('wheel', onMouseEvent, { passive: true });
canvas.addEventListener('keydown', onKeyboardEvent);
canvas.addEventListener('keyup', onKeyboardEvent);
2025-01-05 22:23:52 +05:30
return () => {
canvas.removeEventListener('mousedown', onMouseEvent);
canvas.removeEventListener('mousemove', onMouseEvent);
canvas.removeEventListener('wheel', onMouseEvent);
canvas.removeEventListener('keydown', onKeyboardEvent);
canvas.removeEventListener('keyup', onKeyboardEvent);
};
}, [onMouseEvent, onKeyboardEvent, onCreateRef]);
2024-06-14 21:56:50 +05:30
return (
2025-01-05 22:23:52 +05:30
<div className="relative bg-white rounded-b-md overflow-hidden">
2024-10-23 08:01:38 +05:30
<canvas
tabIndex={0}
ref={canvasRef}
2025-01-05 22:23:52 +05:30
height={canvasSize.height}
width={canvasSize.width}
className="block"
/>
2025-01-05 22:23:52 +05:30
{state.datePickerInfo && (
<DatePicker
2025-01-05 22:23:52 +05:30
coordinates={state.datePickerInfo.coordinates}
selector={state.datePickerInfo.selector}
onClose={() => dispatch({ type: 'SET_DATE_PICKER', payload: null })}
/>
)}
2025-01-05 22:23:52 +05:30
{state.dropdownInfo && (
<Dropdown
2025-01-05 22:23:52 +05:30
coordinates={state.dropdownInfo.coordinates}
selector={state.dropdownInfo.selector}
options={state.dropdownInfo.options}
onClose={() => dispatch({ type: 'SET_DROPDOWN', payload: null })}
/>
)}
2025-01-05 22:23:52 +05:30
{state.timePickerInfo && (
<TimePicker
2025-01-05 22:23:52 +05:30
coordinates={state.timePickerInfo.coordinates}
selector={state.timePickerInfo.selector}
onClose={() => dispatch({ type: 'SET_TIME_PICKER', payload: null })}
/>
)}
2025-01-05 22:23:52 +05:30
{state.dateTimeLocalInfo && (
<DateTimeLocalPicker
2025-01-05 22:23:52 +05:30
coordinates={state.dateTimeLocalInfo.coordinates}
selector={state.dateTimeLocalInfo.selector}
onClose={() => dispatch({ type: 'SET_DATETIME_PICKER', payload: null })}
/>
)}
2024-10-23 08:01:38 +05:30
</div>
2024-06-14 21:56:50 +05:30
);
2025-01-05 22:23:52 +05:30
});
2025-01-05 22:23:52 +05:30
Canvas.displayName = 'Canvas';
2024-07-13 22:21:42 +05:30
export default Canvas;