feat(frontend): useCursorSender hook with letterbox mapping and 50 Hz throttle
This commit is contained in:
61
Frontend/src/hooks/useCursorSender.ts
Normal file
61
Frontend/src/hooks/useCursorSender.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
/**
|
||||
* Convert a viewport point to world coords in [0,1] × [0,1] using
|
||||
* letterbox mapping (preserves world aspect ratio 1:1).
|
||||
*/
|
||||
export function viewportToWorld(
|
||||
viewportPx: { x: number; y: number },
|
||||
viewportSize: { w: number; h: number },
|
||||
): { x: number; y: number } {
|
||||
// World is 1×1. Fit inside viewport with black bars on the wider axis.
|
||||
const scale = Math.min(viewportSize.w, viewportSize.h);
|
||||
const offsetX = (viewportSize.w - scale) / 2;
|
||||
const offsetY = (viewportSize.h - scale) / 2;
|
||||
return {
|
||||
x: Math.max(0, Math.min(1, (viewportPx.x - offsetX) / scale)),
|
||||
y: Math.max(0, Math.min(1, (viewportPx.y - offsetY) / scale)),
|
||||
};
|
||||
}
|
||||
|
||||
const SEND_INTERVAL_MS = 20; // 50 Hz cap
|
||||
|
||||
export function useCursorSender(
|
||||
targetRef: React.RefObject<HTMLElement>,
|
||||
onSend: (x: number, y: number) => void,
|
||||
) {
|
||||
const lastCursorRef = useRef<{ x: number; y: number } | null>(null);
|
||||
const lastSentAtRef = useRef<number>(0);
|
||||
const rafRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = targetRef.current;
|
||||
if (!el) return;
|
||||
|
||||
const handleMove = (ev: MouseEvent) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const world = viewportToWorld(
|
||||
{ x: ev.clientX - rect.left, y: ev.clientY - rect.top },
|
||||
{ w: rect.width, h: rect.height },
|
||||
);
|
||||
lastCursorRef.current = world;
|
||||
};
|
||||
|
||||
const tick = (now: number) => {
|
||||
const c = lastCursorRef.current;
|
||||
if (c && now - lastSentAtRef.current >= SEND_INTERVAL_MS) {
|
||||
onSend(c.x, c.y);
|
||||
lastSentAtRef.current = now;
|
||||
}
|
||||
rafRef.current = requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
el.addEventListener('mousemove', handleMove);
|
||||
rafRef.current = requestAnimationFrame(tick);
|
||||
|
||||
return () => {
|
||||
el.removeEventListener('mousemove', handleMove);
|
||||
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
|
||||
};
|
||||
}, [targetRef, onSend]);
|
||||
}
|
||||
Reference in New Issue
Block a user