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(); } } }