cloud-game/web/js/input/pointer.js

102 lines
2.6 KiB
JavaScript

import { pub, MOUSE_PRESSED, MOUSE_MOVED } from "event";
const hasRawPointer = "onpointerrawupdate" in window;
const moveEvent = hasRawPointer ? "pointerrawupdate" : "pointermove";
// Reusable event data objects to avoid allocations
const move = { dx: 0, dy: 0 };
const btn = { b: null, p: false };
// Game resolution for DPI scaling
const gameW = 640;
const gameH = 480;
// Accumulates fractional pixels to prevent drift when scaling
let errX = 0;
let errY = 0;
const scaleDpi = (dx, dy, srcW, srcH) => {
move.dx = dx / (srcW / gameW) + errX;
move.dy = dy / (srcH / gameH) + errY;
errX = move.dx % 1;
errY = move.dy % 1;
move.dx = Math.trunc(move.dx);
move.dy = Math.trunc(move.dy);
return move;
};
const onDown = (e) => {
btn.b = e.button;
btn.p = true;
pub(MOUSE_PRESSED, btn);
};
const onUp = (e) => {
btn.b = e.button;
btn.p = false;
pub(MOUSE_PRESSED, btn);
};
/*
* Tracks pointer movement and publishes events.
* Uses raw pointer events when available for better accuracy.
* Coalesced events are broken in Firefox 120+, so we skip them there.
*/
const track = (el, getDisplay) => {
let off = null;
const handle = (e) => {
const events = e.getCoalescedEvents?.() || [e];
const { w, h, s } = getDisplay();
for (const ev of events) {
move.dx = ev.movementX;
move.dy = ev.movementY;
pub(MOUSE_MOVED, s ? scaleDpi(move.dx, move.dy, w, h) : move);
}
};
return (on) => {
if (on && !off) {
el.addEventListener(moveEvent, handle);
el.onpointerdown = onDown;
el.onpointerup = onUp;
off = () => {
el.removeEventListener(moveEvent, handle);
el.onpointerdown = null;
el.onpointerup = null;
};
} else if (!on && off) {
off();
off = null;
}
};
};
/*
* Auto-hides cursor after inactivity. Movement shows it again.
*/
const autoHide = (el, timeout = 3000) => {
let timer;
const cl = el.classList;
const reset = () => {
cl.remove("no-pointer");
clearTimeout(timer);
timer = setTimeout(() => cl.add("no-pointer"), timeout);
};
return (on) => {
clearTimeout(timer);
el.removeEventListener("pointermove", reset);
cl.remove("no-pointer");
if (on) {
el.addEventListener("pointermove", reset);
reset();
}
};
};
export const pointer = {
lock: (el) => el.requestPointerLock(),
track,
autoHide,
};