feat: add replayer func client side

This commit is contained in:
Rohit Rajan
2026-01-09 17:59:18 +05:30
parent d7830ee6ef
commit dddac2d966
15 changed files with 308 additions and 732 deletions

310
legacy/src/Canvas.tsx Normal file
View File

@@ -0,0 +1,310 @@
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<HTMLCanvasElement>): 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<HTMLCanvasElement>(null);
const contextRef = useRef<CanvasRenderingContext2D | null>(null);
const imageDataRef = useRef<ImageData | null>(null);
const animationFrameRef = useRef<number | null>(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<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(() => {
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<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 (
<div style={containerStyle}>
<canvas
tabIndex={0}
ref={canvasRef}
height={height}
width={width}
style={canvasStyle}
/>
{datePickerInfo && (
<DatePicker
coordinates={datePickerInfo.coordinates}
selector={datePickerInfo.selector}
onClose={() => setDatePickerInfo(null)}
/>
)}
{dropdownInfo && (
<Dropdown
coordinates={dropdownInfo.coordinates}
selector={dropdownInfo.selector}
options={dropdownInfo.options}
onClose={() => setDropdownInfo(null)}
/>
)}
{timePickerInfo && (
<TimePicker
coordinates={timePickerInfo.coordinates}
selector={timePickerInfo.selector}
onClose={() => setTimePickerInfo(null)}
/>
)}
{dateTimeLocalInfo && (
<DateTimeLocalPicker
coordinates={dateTimeLocalInfo.coordinates}
selector={dateTimeLocalInfo.selector}
onClose={() => setDateTimeLocalInfo(null)}
/>
)}
</div>
);
};
export default memo(Canvas);

View File

@@ -0,0 +1,87 @@
import React, { useMemo } from 'react';
import styled from "styled-components";
import { coordinateMapper } from '../../helpers/coordinateMapper';
interface HighlighterProps {
unmodifiedRect: DOMRect;
displayedSelector: string;
width: number;
height: number;
canvasRect: DOMRect;
};
const HighlighterComponent = ({ unmodifiedRect, displayedSelector = '', width, height, canvasRect }: HighlighterProps) => {
if (!unmodifiedRect) {
return null;
} else {
const rect = useMemo(() => {
const mappedRect = coordinateMapper.mapBrowserRectToCanvas(unmodifiedRect);
return {
top: mappedRect.top + canvasRect.top + window.scrollY,
left: mappedRect.left + canvasRect.left + window.scrollX,
width: mappedRect.width,
height: mappedRect.height,
};
}, [unmodifiedRect, canvasRect.top, canvasRect.left]);
return (
<div>
<HighlighterOutline
id="Highlighter-outline"
top={rect.top}
left={rect.left}
width={rect.width}
height={rect.height}
/>
{/* <HighlighterLabel
id="Highlighter-label"
top={rect.top + rect.height + 8}
left={rect.left}
>
{displayedSelector}
</HighlighterLabel> */}
</div>
);
}
}
export const Highlighter = React.memo(HighlighterComponent);
const HighlighterOutline = styled.div<HighlighterOutlineProps>`
box-sizing: border-box;
pointer-events: none !important;
position: fixed !important;
background: #ff5d5b26 !important;
outline: 2px solid #ff00c3 !important;
z-index: 2147483647 !important;
top: ${(p: HighlighterOutlineProps) => p.top}px;
left: ${(p: HighlighterOutlineProps) => p.left}px;
width: ${(p: HighlighterOutlineProps) => p.width}px;
height: ${(p: HighlighterOutlineProps) => p.height}px;
`;
const HighlighterLabel = styled.div<HighlighterLabelProps>`
pointer-events: none !important;
position: fixed !important;
background: #080a0b !important;
color: white !important;
padding: 8px !important;
font-family: monospace !important;
border-radius: 5px !important;
z-index: 2147483647 !important;
top: ${(p: HighlighterLabelProps) => p.top}px;
left: ${(p: HighlighterLabelProps) => p.left}px;
`;
interface HighlighterLabelProps {
top: number;
left: number;
}
interface HighlighterOutlineProps {
top: number;
left: number;
width: number;
height: number;
}

View File

@@ -0,0 +1,94 @@
import { BROWSER_DEFAULT_HEIGHT, BROWSER_DEFAULT_WIDTH } from "../constants/const";
import { getResponsiveDimensions } from "./dimensionUtils";
export class CoordinateMapper {
private canvasWidth: number;
private canvasHeight: number;
private browserWidth: number;
private browserHeight: number;
private lastBrowserRect: { left: number, top: number, right: number, bottom: number } | null = null;
private lastCanvasRect: DOMRect | null = null;
constructor() {
const dimensions = getResponsiveDimensions();
this.canvasWidth = dimensions.canvasWidth;
this.canvasHeight = dimensions.canvasHeight;
this.browserWidth = BROWSER_DEFAULT_WIDTH;
this.browserHeight = BROWSER_DEFAULT_HEIGHT;
}
mapCanvasToBrowser(coord: { x: number, y: number }): { x: number, y: number } {
return {
x: (coord.x / this.canvasWidth) * this.browserWidth,
y: (coord.y / this.canvasHeight) * this.browserHeight
};
}
mapBrowserToCanvas(coord: { x: number, y: number }): { x: number, y: number } {
return {
x: (coord.x / this.browserWidth) * this.canvasWidth,
y: (coord.y / this.browserHeight) * this.canvasHeight
};
}
mapBrowserRectToCanvas(rect: DOMRect): DOMRect {
if (this.lastBrowserRect &&
this.lastBrowserRect.left === rect.left &&
this.lastBrowserRect.top === rect.top &&
this.lastBrowserRect.right === rect.right &&
this.lastBrowserRect.bottom === rect.bottom) {
return this.lastCanvasRect!;
}
const topLeft = this.mapBrowserToCanvas({ x: rect.left, y: rect.top });
const bottomRight = this.mapBrowserToCanvas({ x: rect.right, y: rect.bottom });
const width = bottomRight.x - topLeft.x;
const height = bottomRight.y - topLeft.y;
const result = new DOMRect(
topLeft.x,
topLeft.y,
width,
height
);
this.lastBrowserRect = {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom
};
this.lastCanvasRect = result;
return result;
}
mapCanvasRectToBrowser(rect: DOMRect): DOMRect {
const topLeft = this.mapCanvasToBrowser({ x: rect.left, y: rect.top });
const bottomRight = this.mapCanvasToBrowser({ x: rect.right, y: rect.bottom });
const width = bottomRight.x - topLeft.x;
const height = bottomRight.y - topLeft.y;
return new DOMRect(
topLeft.x,
topLeft.y,
width,
height
);
}
updateDimensions(canvasWidth?: number, canvasHeight?: number, browserWidth?: number, browserHeight?: number) {
if (canvasWidth) this.canvasWidth = canvasWidth;
if (canvasHeight) this.canvasHeight = canvasHeight;
if (browserWidth) this.browserWidth = browserWidth;
if (browserHeight) this.browserHeight = browserHeight;
this.lastBrowserRect = null;
this.lastCanvasRect = null;
}
}
export const coordinateMapper = new CoordinateMapper();

139
legacy/src/inputHelpers.ts Normal file
View File

@@ -0,0 +1,139 @@
import {
ONE_PERCENT_OF_VIEWPORT_H,
ONE_PERCENT_OF_VIEWPORT_W,
} from "../constants/const";
import { Coordinates } from '../components/recorder/Canvas';
export const throttle = (callback: any, limit: number) => {
let wait = false;
return (...args: any[]) => {
if (!wait) {
callback(...args);
wait = true;
setTimeout(function () {
wait = false;
}, limit);
}
}
}
export const getMappedCoordinates = (
event: MouseEvent,
canvas: HTMLCanvasElement | null,
browserWidth: number,
browserHeight: number,
): Coordinates => {
const clientCoordinates = getCoordinates(event, canvas);
const mappedX = mapPixelFromSmallerToLarger(
browserWidth / 100,
ONE_PERCENT_OF_VIEWPORT_W,
clientCoordinates.x,
);
const mappedY = mapPixelFromSmallerToLarger(
browserHeight / 100,
ONE_PERCENT_OF_VIEWPORT_H,
clientCoordinates.y,
);
return {
x: mappedX,
y: mappedY
};
};
const getCoordinates = (event: MouseEvent, canvas: HTMLCanvasElement | null): Coordinates => {
if (!canvas) {
return { x: 0, y: 0 };
}
return {
x: event.pageX - canvas.offsetLeft,
y: event.pageY - canvas.offsetTop
};
};
export const mapRect = (
rect: DOMRect,
browserWidth: number,
browserHeight: number,
) => {
const mappedX = mapPixelFromSmallerToLarger(
browserWidth / 100,
ONE_PERCENT_OF_VIEWPORT_W,
rect.x,
);
const mappedLeft = mapPixelFromSmallerToLarger(
browserWidth / 100,
ONE_PERCENT_OF_VIEWPORT_W,
rect.left,
);
const mappedRight = mapPixelFromSmallerToLarger(
browserWidth / 100,
ONE_PERCENT_OF_VIEWPORT_W,
rect.right,
);
const mappedWidth = mapPixelFromSmallerToLarger(
browserWidth / 100,
ONE_PERCENT_OF_VIEWPORT_W,
rect.width,
);
const mappedY = mapPixelFromSmallerToLarger(
browserHeight / 100,
ONE_PERCENT_OF_VIEWPORT_H,
rect.y,
);
const mappedTop = mapPixelFromSmallerToLarger(
browserHeight / 100,
ONE_PERCENT_OF_VIEWPORT_H,
rect.top,
);
const mappedBottom = mapPixelFromSmallerToLarger(
browserHeight / 100,
ONE_PERCENT_OF_VIEWPORT_H,
rect.bottom,
);
const mappedHeight = mapPixelFromSmallerToLarger(
browserHeight / 100,
ONE_PERCENT_OF_VIEWPORT_H,
rect.height,
);
console.log('Mapped:', {
x: mappedX,
y: mappedY,
width: mappedWidth,
height: mappedHeight,
top: mappedTop,
right: mappedRight,
bottom: mappedBottom,
left: mappedLeft,
})
return {
x: mappedX,
y: mappedY,
width: mappedWidth,
height: mappedHeight,
top: mappedTop,
right: mappedRight,
bottom: mappedBottom,
left: mappedLeft,
};
};
const mapPixelFromSmallerToLarger = (
onePercentOfSmallerScreen: number,
onePercentOfLargerScreen: number,
pixel: number
): number => {
const xPercentOfScreen = pixel / onePercentOfSmallerScreen;
return xPercentOfScreen * onePercentOfLargerScreen;
};
const mapPixelFromLargerToSmaller = (
onePercentOfSmallerScreen: number,
onePercentOfLargerScreen: number,
pixel: number
): number => {
const xPercentOfScreen = pixel / onePercentOfLargerScreen;
return Math.round(xPercentOfScreen * onePercentOfSmallerScreen);
};