hud-gamepad
Version:
A Heads Up Display (HUD) for Gamepads, Keyboards, and more
401 lines (350 loc) • 11.4 kB
JavaScript
import { stage } from '../components/stage.js';
export class EventHandler {
constructor(controller, config = {}) {
if (!controller) throw new Error('Controller is required for EventHandler');
this.controller = controller;
this.touches = {};
this.observerFunction = config.observerFunction;
this.hint = config.hint;
this.bind();
this.isBinding = false;
this.stickPressed = false;
// Add key repeat handling
this.keyStates = new Map();
this.repeatDelay = 300; // Initial delay before repeat starts
this.repeatRate = 50; // How often to repeat (ms)
}
bind() {
document.addEventListener('touchmove', function(e) { e.preventDefault(); }, false);
const canvas = stage.getCanvas();
if (!canvas) {
console.error('Canvas not found');
return;
}
canvas.oncontextmenu = function(e) { e.preventDefault(); };
let events = {
browser:["mousedown", "mouseup", "mousemove"],
app:["touchstart", "touchend", "touchmove"]
}
events = document.querySelector('.HudGamePadObserver').style.display === '' ? events.app : events.browser;
events.forEach(event => canvas.addEventListener(event, (e) => this.listen(e), { passive: false }));
// Update the resize observer to properly handle window resizing
const resizeObserver = new ResizeObserver(() => {
stage.updateDimensions(
window.innerWidth,
window.innerHeight
);
this.controller.init(this.controller.config);
});
resizeObserver.observe(document.body);
// Cleanup on page unload
window.addEventListener('unload', () => {
this.keyStates.forEach((timer) => {
clearTimeout(timer.timeout);
clearInterval(timer.interval);
});
this.keyStates.clear();
});
}
listen(e) {
if (!e) return this.controller.getState();
if (e.type) {
this.handlePointerEvent(e);
this.dispatch(e);
} else {
if(!this.stickPressed) {
this.handleKeyboardEvent(e);
}
}
if (this.observerFunction) {
this.observerFunction(this.controller.getState());
}
return this.controller.getState();
}
dispatch(e) {
const states = this.controller.getState();
const dispatchKey = (key, code, isActive) => {
if (isActive) {
if (!this.keyStates.has(key)) {
// Initial keydown
window.dispatchEvent(new KeyboardEvent("keydown", {
key,
keyCode: code,
which: code,
bubbles: true,
repeat: false
}));
// Setup repeat behavior
const timeout = setTimeout(() => {
const interval = setInterval(() => {
window.dispatchEvent(new KeyboardEvent("keydown", {
key,
keyCode: code,
which: code,
bubbles: true,
repeat: true
}));
}, this.repeatRate);
this.keyStates.set(key, { timeout, interval });
}, this.repeatDelay);
this.keyStates.set(key, { timeout, interval: null });
}
} else if (this.keyStates.has(key)) {
// Cleanup timers
const timers = this.keyStates.get(key);
clearTimeout(timers.timeout);
if (timers.interval) clearInterval(timers.interval);
this.keyStates.delete(key);
// Send keyup
window.dispatchEvent(new KeyboardEvent("keyup", {
key,
keyCode: code,
which: code,
bubbles: true
}));
}
};
const arrows = {
up: {
key: "ArrowUp",
code: 38,
active: states['y-dir'] === -1
},
down: {
key: "ArrowDown",
code: 40,
active: states['y-dir'] === 1
},
left: {
key: "ArrowLeft",
code: 37,
active: states['x-dir'] === -1
},
right: {
key: "ArrowRight",
code: 39,
active: states['x-dir'] === 1
}
};
const buttons = this.controller.buttons;
buttons.forEach(button => {
const { key, name } = button.config;
const isActive = states[name] === 1;
button.config.hit.active = isActive;
dispatchKey(key, key.charCodeAt(0), isActive);
});
Object.values(arrows).forEach(({ key, code, active }) => {
dispatchKey(key, code, active);
});
}
handlePointerEvent(e) {
const type = e.type;
if (type.includes("mouse")) {
e.identifier = "desktop";
e = { touches: [e] };
}
// Track up to 5 touches
Array.from(e.touches).slice(0, 5).forEach(touch => {
const { identifier: id, pageX: x, pageY: y } = touch;
this.touches[id] = { x, y };
});
for(let id in this.touches) {
switch(type)
{
case "touchstart":
case "touchmove":
case "touchend":
this.handleJoystickTouch(id, this.touches[id], type);
this.handleButtonTouch(id, this.touches[id], type);
break;
case "mousedown":
case "mousemove":
case "mouseup":
break;
}
}
if (type === "touchend") {
this.handleTouchEnd(e);
this.stickPressed = false;
} else {
this.handleActiveTouches(type);
}
if(type === "mouseup") {
this.stickPressed = false;
}
}
handleTouchEnd(e) {
const id = e.changedTouches[0].identifier;
// Reset joystick if it was being controlled by this touch
if (this.touches[id]?.id === "stick") {
this.controller.joystick?.reset();
this.controller.updateState({
"x-axis": 0,
"y-axis": 0,
"x-dir": 0,
"y-dir": 0
});
}
delete this.touches[id];
// Handle multiple touch ends
if (e.changedTouches.length > e.touches.length) {
const delta = e.changedTouches.length - e.touches.length;
Object.keys(this.touches).slice(0, delta).forEach(id => delete this.touches[id]);
}
// Reset all states if no touches remain
if (e.touches.length === 0) {
this.touches = {};
this.controller.resetStates();
}
}
handleActiveTouches(type) {
Object.keys(this.touches).forEach(id => {
const touch = this.touches[id];
if (this.controller.joystick && !touch.id) {
this.handleJoystickTouch(id, touch, type);
}
this.handleButtonTouch(id, touch, type);
});
}
handleJoystickTouch(id, touch, type) {
const stick = this.controller.joystick;
// Convert touch coordinates to integers for consistent behavior
const dx = parseInt(touch.x - stick.x);
const dy = parseInt(touch.y - stick.y);
const dist = parseInt(Math.sqrt(dx * dx + dy * dy));
// Check if touch is within joystick area
if (dist < stick.radius * 1.5) {
if (!type || type === "mousedown" || type === "touchstart") {
this.touches[id].id = "stick";
this.stickPressed = true;
}
}
if (this.touches[id].id === "stick" || this.stickPressed) {
// Update joystick position with integer values and radius constraints
if (Math.abs(parseInt(dx)) < (stick.radius / 2)) {
stick.dx = stick.x + dx;
}
if (Math.abs(parseInt(dy)) < (stick.radius / 2)) {
stick.dy = stick.y + dy;
}
// Update state map with normalized values
const newState = {
"x-axis": (stick.dx - stick.x) / (stick.radius / 2),
"y-axis": (stick.dy - stick.y) / (stick.radius / 2)
};
// Add rounded directions
newState["x-dir"] = Math.round(newState["x-axis"]);
newState["y-dir"] = Math.round(newState["y-axis"]);
this.controller.updateState(newState);
}
if (type === "mouseup" || type === "touchend") {
stick.reset();
this.controller.updateState({
"x-axis": 0,
"y-axis": 0,
"x-dir": 0,
"y-dir": 0
});
delete this.touches[id].id;
}
}
handleButtonTouch(id, touch, type) {
if (this.touches[id].id === "stick") return;
this.controller.buttons.forEach((button, index) => {
const { hit, name, r } = button.config;
const dx = touch.x - button.config.x;
const dy = touch.y - button.config.y;
let dist = Infinity;
if (r) {
dist = Math.sqrt(dx * dx + dy * dy);
} else if (touch.x > hit.x[0] && touch.x < hit.x[1] &&
touch.y > hit.y[0] && touch.y < hit.y[1]) {
dist = 0;
}
if (dist < (r || 25)) {
if (!type || type === "mousedown" || type === "touchstart") {
this.touches[id].id = name;
}
}
if (this.touches[id].id === name) {
button.config.hit.active = true;
this.controller.updateState({ [name]: 1 });
}
if (type === "mouseup" || type === "touchend") {
delete this.touches[id].id;
button.config.hit.active = false;
this.controller.updateState({ [name]: 0 });
}
});
}
handleKeyboardEvent(keys) {
if (!this.controller.joystick) return;
let dir = 0;
if (keys.left) dir |= 1; // 0001
if (keys.up) dir |= 2; // 0010
if (keys.right) dir |= 4; // 0100
if (keys.down) dir |= 8; // 1000
const stick = this.controller.joystick;
const halfRadius = stick.radius / 2;
// Reset joystick position
stick.dx = stick.x;
stick.dy = stick.y;
const directions = {
1: [-halfRadius, 0], // left
2: [0, -halfRadius], // up
3: [-halfRadius, -halfRadius], // up + left
4: [halfRadius, 0], // right
6: [halfRadius, -halfRadius], // up + right
8: [0, halfRadius], // down
9: [-halfRadius, halfRadius], // down + left
12: [halfRadius, halfRadius] // down + right
};
if (dir in directions) {
const [dx, dy] = directions[dir];
stick.dx = stick.x + dx;
stick.dy = stick.y + dy;
this.controller.updateState({
"x-axis": (stick.dx - stick.x) / halfRadius,
"y-axis": (stick.dy - stick.y) / halfRadius,
"x-dir": Math.sign(stick.dx - stick.x),
"y-dir": Math.sign(stick.dy - stick.y)
});
this.touches.stick = { id: "stick" };
} else {
stick.reset();
delete this.touches.stick;
this.controller.updateState({
"x-axis": 0,
"y-axis": 0,
"x-dir": 0,
"y-dir": 0
});
}
// Handle button keys
Object.entries(keys).forEach(([key, value]) => {
if (!["left", "right", "up", "down"].includes(key)) {
this.handleButtonKey(key, value);
}
});
}
handleButtonKey(key, isPressed) {
const buttons = this.controller.buttons;
buttons.forEach((button, index) => {
if (button.config.key === key) {
if (isPressed) {
this.touches[button.config.name] = {
id: button.config.name,
x: button.config.hit.x[0] + (button.config.w || button.config.r * 2) / 2,
y: button.config.hit.y[0] + (button.config.h || button.config.r * 2) / 2
};
button.config.hit.active = true;
this.controller.updateState({ [button.config.name]: 1 });
} else {
button.config.hit.active = false;
this.controller.updateState({ [button.config.name]: 0 });
delete this.touches[button.config.name];
}
}
});
}
}