import { getRealPoint } from "./dom"; import type { Point } from "./geometry"; interface IKeyState { state: boolean; prevState?: boolean; released?: boolean; pressed?: boolean; repeated?: boolean; held?: boolean; changedAt: number; eventAt: number; repeatAt?: number; } namespace Input { export enum KeyCode { UP = 'ArrowUp', DOWN = 'ArrowDown', LEFT = 'ArrowLeft', RIGHT = 'ArrowRight', SPACE = 'Space', SHIFT = 'Shift', SHIFT_LEFT = 'ShiftLeft', SHIFT_RIGHT = 'ShiftRight', A = 'KeyA', B = 'KeyB', C = 'KeyC', D = 'KeyD', E = 'KeyE', F = 'KeyF', G = 'KeyG', H = 'KeyH', I = 'KeyI', J = 'KeyJ', K = 'KeyK', L = 'KeyL', M = 'KeyM', N = 'KeyN', O = 'KeyO', P = 'KeyP', Q = 'KeyQ', R = 'KeyR', S = 'KeyS', T = 'KeyT', U = 'KeyU', V = 'KeyV', W = 'KeyW', X = 'KeyX', Y = 'KeyY', Z = 'KeyZ', NUM_0 = 'Digit0', NUM_1 = 'Digit1', NUM_2 = 'Digit2', NUM_3 = 'Digit3', NUM_4 = 'Digit4', NUM_5 = 'Digit5', NUM_6 = 'Digit6', NUM_7 = 'Digit7', NUM_8 = 'Digit8', NUM_9 = 'Digit9', MOUSE_LEFT = 'Mouse0', MOUSE_MIDDLE = 'Mouse1', MOUSE_RIGHT = 'Mouse2', }; export enum GamepadAxis { LX = 0, LY = 1, RX = 2, RY = 3, } export enum GamepadButton { A = 0, B = 1, X = 2, Y = 3, LB = 4, RB = 5, LT = 6, RT = 7, SELECT = 8, START = 9, L3 = 10, R3 = 11, D_UP = 12, D_DOWN = 13, D_LEFT = 14, D_RIGHT = 15, HOME = 16, } const DEAD_ZONE = 0.05; const KEYS: Partial> = {}; const mousePosition: Point = { x: 0, y: 0 }; let mainCanvas: HTMLCanvasElement | null = null; let repeatIntervalMs = 10; let repeatDelayMs = 300; const onStateChange = (keyId: string, state: boolean) => { console.debug(`[Input] ${state ? 'Pressed' : 'Released'} ${keyId}`); const now = Date.now(); if (KEYS[keyId]) { if (state !== KEYS[keyId].state) { KEYS[keyId].changedAt = now; } KEYS[keyId].state = state; KEYS[keyId].eventAt = now; } else { KEYS[keyId] = { state, changedAt: now, eventAt: now }; } if (keyId === KeyCode.SHIFT_LEFT || keyId === KeyCode.SHIFT_RIGHT) { const shiftState = Boolean(KEYS[KeyCode.SHIFT_LEFT]?.state || KEYS[KeyCode.SHIFT_RIGHT]?.state); onStateChange(KeyCode.SHIFT, shiftState); } }; const onMouseChange = (e: MouseEvent) => { if (mainCanvas) { const point = getRealPoint(mainCanvas, e); mousePosition.x = point.x; mousePosition.y = point.y; } else { mousePosition.x = e.clientX; mousePosition.y = e.clientY; } } document.body.addEventListener('keydown', (e) => onStateChange(e.code as KeyCode, true)); document.body.addEventListener('keyup', (e) => onStateChange(e.code as KeyCode, false)); document.body.addEventListener('mousedown', (e) => { onStateChange(`Mouse${e.button}`, true); onMouseChange(e); }); document.body.addEventListener('mouseup', (e) => { onStateChange(`Mouse${e.button}`, false); onMouseChange(e); }); document.body.addEventListener('mousemove', (e) => onMouseChange(e)); document.body.addEventListener('contextmenu', (e) => e.preventDefault()); export const isPressed = (...keys: KeyCode[]) => keys.some(key => KEYS[key]?.pressed); export const isReleased = (...keys: KeyCode[]) => keys.some(key => KEYS[key]?.released); export const isRepeated = (...keys: KeyCode[]) => keys.some(key => KEYS[key]?.repeated); export const isHeld = (...keys: KeyCode[]) => keys.some(key => KEYS[key]?.held); export const hasPressed = () => Object.values(KEYS).some(k => k?.pressed); export const hasReleased = () => Object.values(KEYS).some(k => k?.released); export const hasRepeated = () => Object.values(KEYS).some(k => k?.repeated); export const hasHeld = () => Object.values(KEYS).some(k => k?.held); export const getGamepad = () => navigator.getGamepads().find(g => g != null); export const getGamepadAxis = (i: GamepadAxis) => { const value = getGamepad()?.axes[i] ?? 0; return Math.abs(value) < DEAD_ZONE ? 0 : value; } export const getGamepadButton = (btn: GamepadButton) => getGamepad()?.buttons[btn]?.value ?? 0; export const getLeft = () => isHeld(KeyCode.LEFT) ? 1 : Math.max(0, -getGamepadAxis(GamepadAxis.LX)); export const getRight = () => isHeld(KeyCode.RIGHT) ? 1 : Math.max(0, getGamepadAxis(GamepadAxis.LX)); export const getUp = () => isHeld(KeyCode.UP) ? 1 : Math.max(0, -getGamepadAxis(GamepadAxis.LY)); export const getDown = () => isHeld(KeyCode.DOWN) ? 1 : Math.max(0, getGamepadAxis(GamepadAxis.LY)); export const getHorizontal = () => ( (Number(isHeld(KeyCode.LEFT)) - Number(isHeld(KeyCode.RIGHT))) + getGamepadAxis(GamepadAxis.LX) ); export const getVertical = () => ( (Number(isHeld(KeyCode.UP)) - Number(isHeld(KeyCode.DOWN))) + getGamepadAxis(GamepadAxis.LY) ); export const getMousePosition = (): Point => ({ ...mousePosition }); export const setRepeatInterval = (intervalMs: number) => { repeatIntervalMs = intervalMs; } export const setRepeatDelay = (delayMs: number) => { repeatDelayMs = delayMs; } export const setMainCanvas = (canvas: HTMLCanvasElement | null) => { mainCanvas = canvas; } export function updateKeys() { for (const key of Object.values(KEYS)) { if (!key) continue; key.released = false; key.pressed = false; if (key.state) { key.pressed = !key.held; key.held = true; } else { key.released = key.held; key.held = false; } const now = Date.now(); key.repeated = key.held && (now - (key.repeatAt ?? 0) > repeatIntervalMs) && (now - key.changedAt > repeatDelayMs); if (key.repeated) { key.repeatAt = now; } key.prevState = key.state; } } } export default Input;