230 lines
7.9 KiB
JavaScript
230 lines
7.9 KiB
JavaScript
|
|
class InputHandler {
|
||
|
|
constructor(canvas, options = {}) {
|
||
|
|
this.canvas = canvas;
|
||
|
|
this.wsUrl = options.wsUrl;
|
||
|
|
this.screenWidth = options.screenWidth || 1280;
|
||
|
|
this.screenHeight = options.screenHeight || 720;
|
||
|
|
|
||
|
|
this.ws = null;
|
||
|
|
this.wsReady = false;
|
||
|
|
|
||
|
|
this.lastMoveTime = 0;
|
||
|
|
this.MOVE_THROTTLE_MS = 33;
|
||
|
|
this.pendingMove = null;
|
||
|
|
this.moveThrottleTimer = null;
|
||
|
|
|
||
|
|
this.pressedKeys = new Set();
|
||
|
|
|
||
|
|
this.codeToKey = {
|
||
|
|
'Enter': 'enter', 'Backspace': 'backspace', 'Tab': 'tab',
|
||
|
|
'Escape': 'escape', 'Delete': 'delete', 'Home': 'home',
|
||
|
|
'End': 'end', 'PageUp': 'pageup', 'PageDown': 'pagedown',
|
||
|
|
'ArrowUp': 'up', 'ArrowDown': 'down', 'ArrowLeft': 'left', 'ArrowRight': 'right',
|
||
|
|
'F1': 'f1', 'F2': 'f2', 'F3': 'f3', 'F4': 'f4',
|
||
|
|
'F5': 'f5', 'F6': 'f6', 'F7': 'f7', 'F8': 'f8',
|
||
|
|
'F9': 'f9', 'F10': 'f10', 'F11': 'f11', 'F12': 'f12',
|
||
|
|
'ControlLeft': 'ctrl', 'ControlRight': 'ctrl',
|
||
|
|
'AltLeft': 'alt', 'AltRight': 'alt',
|
||
|
|
'ShiftLeft': 'shift', 'ShiftRight': 'shift',
|
||
|
|
'MetaLeft': 'win', 'MetaRight': 'win',
|
||
|
|
'Space': 'space',
|
||
|
|
'Digit0': '0', 'Digit1': '1', 'Digit2': '2', 'Digit3': '3', 'Digit4': '4',
|
||
|
|
'Digit5': '5', 'Digit6': '6', 'Digit7': '7', 'Digit8': '8', 'Digit9': '9',
|
||
|
|
'KeyA': 'a', 'KeyB': 'b', 'KeyC': 'c', 'KeyD': 'd', 'KeyE': 'e',
|
||
|
|
'KeyF': 'f', 'KeyG': 'g', 'KeyH': 'h', 'KeyI': 'i', 'KeyJ': 'j',
|
||
|
|
'KeyK': 'k', 'KeyL': 'l', 'KeyM': 'm', 'KeyN': 'n', 'KeyO': 'o',
|
||
|
|
'KeyP': 'p', 'KeyQ': 'q', 'KeyR': 'r', 'KeyS': 's', 'KeyT': 't',
|
||
|
|
'KeyU': 'u', 'KeyV': 'v', 'KeyW': 'w', 'KeyX': 'x', 'KeyY': 'y', 'KeyZ': 'z',
|
||
|
|
'Comma': ',', 'Period': '.', 'Slash': '/', 'Semicolon': ';',
|
||
|
|
'Quote': "'", 'BracketLeft': '[', 'BracketRight': ']',
|
||
|
|
'Backslash': '\\', 'Minus': '-', 'Equal': '=', 'Backquote': '`'
|
||
|
|
};
|
||
|
|
|
||
|
|
this._boundHandlers = {
|
||
|
|
mousemove: this._handleMouseMove.bind(this),
|
||
|
|
mousedown: this._handleMouseDown.bind(this),
|
||
|
|
mouseup: this._handleMouseUp.bind(this),
|
||
|
|
contextmenu: this._handleContextMenu.bind(this),
|
||
|
|
wheel: this._handleWheel.bind(this),
|
||
|
|
keydown: this._handleKeyDown.bind(this),
|
||
|
|
keyup: this._handleKeyUp.bind(this),
|
||
|
|
blur: this._handleBlur.bind(this)
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
init() {
|
||
|
|
this._initWebSocket();
|
||
|
|
this._bindEvents();
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
|
||
|
|
_initWebSocket() {
|
||
|
|
this.ws = createReconnectingWebSocket(this.wsUrl, {
|
||
|
|
onOpen: () => {
|
||
|
|
this.wsReady = true;
|
||
|
|
console.log('Input WebSocket connected');
|
||
|
|
},
|
||
|
|
onClose: () => {
|
||
|
|
this.wsReady = false;
|
||
|
|
console.log('Input WebSocket disconnected');
|
||
|
|
},
|
||
|
|
onMessage: (e) => {
|
||
|
|
try {
|
||
|
|
const msg = JSON.parse(e.data);
|
||
|
|
if (msg.type === 'screenInfo') {
|
||
|
|
this.screenWidth = msg.width;
|
||
|
|
this.screenHeight = msg.height;
|
||
|
|
console.log('Screen resolution:', this.screenWidth, 'x', this.screenHeight);
|
||
|
|
}
|
||
|
|
} catch (err) {}
|
||
|
|
},
|
||
|
|
onError: (err) => {
|
||
|
|
console.error('Input WebSocket error:', err);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
_bindEvents() {
|
||
|
|
this.canvas.addEventListener('mousemove', this._boundHandlers.mousemove);
|
||
|
|
this.canvas.addEventListener('mousedown', this._boundHandlers.mousedown);
|
||
|
|
this.canvas.addEventListener('mouseup', this._boundHandlers.mouseup);
|
||
|
|
this.canvas.addEventListener('contextmenu', this._boundHandlers.contextmenu);
|
||
|
|
this.canvas.addEventListener('wheel', this._boundHandlers.wheel);
|
||
|
|
document.addEventListener('keydown', this._boundHandlers.keydown);
|
||
|
|
document.addEventListener('keyup', this._boundHandlers.keyup);
|
||
|
|
window.addEventListener('blur', this._boundHandlers.blur);
|
||
|
|
}
|
||
|
|
|
||
|
|
_unbindEvents() {
|
||
|
|
this.canvas.removeEventListener('mousemove', this._boundHandlers.mousemove);
|
||
|
|
this.canvas.removeEventListener('mousedown', this._boundHandlers.mousedown);
|
||
|
|
this.canvas.removeEventListener('mouseup', this._boundHandlers.mouseup);
|
||
|
|
this.canvas.removeEventListener('contextmenu', this._boundHandlers.contextmenu);
|
||
|
|
this.canvas.removeEventListener('wheel', this._boundHandlers.wheel);
|
||
|
|
document.removeEventListener('keydown', this._boundHandlers.keydown);
|
||
|
|
document.removeEventListener('keyup', this._boundHandlers.keyup);
|
||
|
|
window.removeEventListener('blur', this._boundHandlers.blur);
|
||
|
|
}
|
||
|
|
|
||
|
|
sendInputEvent(event) {
|
||
|
|
if (this.wsReady && this.ws && this.ws.isReady()) {
|
||
|
|
this.ws.getWebSocket().send(JSON.stringify(event));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_sendMouseMove(x, y) {
|
||
|
|
this.pendingMove = { x, y };
|
||
|
|
|
||
|
|
const now = Date.now();
|
||
|
|
if (now - this.lastMoveTime >= this.MOVE_THROTTLE_MS) {
|
||
|
|
this._flushMouseMove();
|
||
|
|
} else if (!this.moveThrottleTimer) {
|
||
|
|
this.moveThrottleTimer = setTimeout(() => {
|
||
|
|
this._flushMouseMove();
|
||
|
|
this.moveThrottleTimer = null;
|
||
|
|
}, this.MOVE_THROTTLE_MS - (now - this.lastMoveTime));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_flushMouseMove() {
|
||
|
|
if (this.pendingMove) {
|
||
|
|
this.sendInputEvent({
|
||
|
|
type: 'mouseMove',
|
||
|
|
x: this.pendingMove.x,
|
||
|
|
y: this.pendingMove.y
|
||
|
|
});
|
||
|
|
this.lastMoveTime = Date.now();
|
||
|
|
this.pendingMove = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_handleMouseMove(e) {
|
||
|
|
const { x, y } = getScreenCoordinates(e.clientX, e.clientY, this.canvas, this.screenWidth, this.screenHeight);
|
||
|
|
this._sendMouseMove(x, y);
|
||
|
|
}
|
||
|
|
|
||
|
|
_handleMouseDown(e) {
|
||
|
|
const button = e.button === 2 ? 'right' : 'left';
|
||
|
|
this.sendInputEvent({
|
||
|
|
type: 'mouseDown',
|
||
|
|
button: button
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
_handleMouseUp(e) {
|
||
|
|
const button = e.button === 2 ? 'right' : 'left';
|
||
|
|
this.sendInputEvent({
|
||
|
|
type: 'mouseUp',
|
||
|
|
button: button
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
_handleContextMenu(e) {
|
||
|
|
e.preventDefault();
|
||
|
|
}
|
||
|
|
|
||
|
|
_handleWheel(e) {
|
||
|
|
e.preventDefault();
|
||
|
|
const delta = -Math.sign(e.deltaY) * 120;
|
||
|
|
this.sendInputEvent({
|
||
|
|
type: 'mouseWheel',
|
||
|
|
delta: delta
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
_getKeyFromEvent(e) {
|
||
|
|
if (this.codeToKey[e.code]) {
|
||
|
|
return this.codeToKey[e.code];
|
||
|
|
}
|
||
|
|
return e.key;
|
||
|
|
}
|
||
|
|
|
||
|
|
_handleKeyDown(e) {
|
||
|
|
const key = this._getKeyFromEvent(e);
|
||
|
|
const keyId = key.toLowerCase();
|
||
|
|
|
||
|
|
if (!this.pressedKeys.has(keyId)) {
|
||
|
|
this.pressedKeys.add(keyId);
|
||
|
|
this.sendInputEvent({
|
||
|
|
type: 'keyDown',
|
||
|
|
key: key
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
if (e.key === 'Tab' || e.key === ' ' || e.key.startsWith('Arrow')) {
|
||
|
|
e.preventDefault();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
_handleKeyUp(e) {
|
||
|
|
const key = this._getKeyFromEvent(e);
|
||
|
|
const keyId = key.toLowerCase();
|
||
|
|
|
||
|
|
this.pressedKeys.delete(keyId);
|
||
|
|
this.sendInputEvent({
|
||
|
|
type: 'keyUp',
|
||
|
|
key: key
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
_handleBlur() {
|
||
|
|
this.pressedKeys.forEach(keyId => {
|
||
|
|
this.sendInputEvent({
|
||
|
|
type: 'keyUp',
|
||
|
|
key: keyId
|
||
|
|
});
|
||
|
|
});
|
||
|
|
this.pressedKeys.clear();
|
||
|
|
}
|
||
|
|
|
||
|
|
destroy() {
|
||
|
|
this._unbindEvents();
|
||
|
|
if (this.moveThrottleTimer) {
|
||
|
|
clearTimeout(this.moveThrottleTimer);
|
||
|
|
}
|
||
|
|
if (this.ws) {
|
||
|
|
this.ws.close();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|