@utsp/input
Version:
Comprehensive input management system for UTSP - keyboard, mouse, gamepad, and touch support with unified routing
1,360 lines (1,345 loc) • 42.1 kB
TypeScript
import { Vector2, Vector3, IInputSystem, InputDeviceType, AxisBinding, ButtonBinding } from '@utsp/types';
export { GamepadInput, IInputSystem, InputDeviceType, KeyboardInput, MouseInput, TouchInput, Vector2, Vector3 } from '@utsp/types';
/**
* Base interface for all input management systems
* This provides a common API for keyboard, mouse, gamepad, and touch inputs
*/
interface IInputs {
/**
* Start listening to input events
*/
start(): void;
/**
* Stop listening to input events and cleanup
*/
stop(): void;
/**
* Reset all input states to default
*/
reset(): void;
/**
* Check if the input system is currently active/listening
*/
isListening(): boolean;
/**
* Cleanup and destroy the instance, removing all event listeners
*/
destroy(): void;
/**
* Reset delta values (call at the end of each frame/update)
*/
resetDelta(): void;
/**
* Set or update event callbacks
*/
setCallbacks(callbacks: unknown): void;
/**
* Remove all event callbacks
*/
clearCallbacks(): void;
/**
* Check if a key is currently pressed (Keyboard)
*/
isKeyPressed?(key: string): boolean;
/**
* Check if left mouse button is pressed (Mouse)
*/
isLeftMousePressed?(): boolean;
/**
* Get mouse position (Mouse)
*/
getMousePosition?(): Vector2 | null;
/**
* Get mouse movement delta (Mouse)
*/
getMouseDelta?(): Vector2 | null;
/**
* Get wheel delta (Mouse)
*/
getWheelDelta?(): Vector2 | null;
/**
* Check if a button is pressed (Gamepad, TVRemote)
*/
isButtonPressed?(button: string | number): boolean;
/**
* Get axis value (Gamepad, TVRemote)
*/
getAxis?(axis: number): number | null;
/**
* Get motion/gyroscope data (TVRemote, Mobile)
*/
getMotionData?(): MotionData | null;
}
/**
* Motion/Gyroscope data interface
*/
interface MotionData extends Vector3 {
timestamp?: number;
}
/**
* Common button state interface
*/
interface ButtonState {
pressed: boolean;
justPressed: boolean;
justReleased: boolean;
timestamp?: number;
}
/**
* Input event types enumeration
*/
declare enum InputEventType {
KeyDown = "keydown",
KeyUp = "keyup",
MouseDown = "mousedown",
MouseUp = "mouseup",
MouseMove = "mousemove",
MouseWheel = "mousewheel",
TouchStart = "touchstart",
TouchEnd = "touchend",
TouchMove = "touchmove",
GamepadConnected = "gamepadconnected",
GamepadDisconnected = "gamepaddisconnected",
GamepadButton = "gamepadbutton",
GamepadAxis = "gamepadaxis"
}
/**
* Generic input event interface
*/
interface InputEvent<T = unknown> {
type: InputEventType;
timestamp: number;
data: T;
originalEvent?: Event;
}
/**
* Input device information
*/
interface InputDevice {
type: number;
id: string;
name?: string;
connected: boolean;
timestamp?: number;
}
/**
* Configuration options for input systems
*/
interface InputConfig {
/**
* Target element to attach listeners to (default: window)
*/
targetElement?: HTMLElement | Window;
/**
* Enable/disable specific input types
*/
enabled?: boolean;
/**
* Prevent default browser behaviors
*/
preventDefault?: boolean;
/**
* Stop event propagation
*/
stopPropagation?: boolean;
/**
* Enable debug logging
*/
debug?: boolean;
}
/**
* Abstract base class for input systems
* All input classes should extend this to maintain consistency
*/
declare abstract class BaseInputs implements IInputs {
protected isActive: boolean;
protected targetElement: HTMLElement | Window;
protected config: InputConfig;
protected callbacks: Record<string, ((...args: any[]) => void)[]>;
constructor(targetElement?: HTMLElement | Window, config?: InputConfig);
abstract start(): void;
abstract stop(): void;
abstract reset(): void;
abstract resetDelta(): void;
isListening(): boolean;
destroy(): void;
setCallbacks(callbacks: unknown): void;
clearCallbacks(): void;
/**
* Get the target element
*/
getTargetElement(): HTMLElement | Window;
/**
* Enable the input system
*/
enable(): void;
/**
* Disable the input system
*/
disable(): void;
/**
* Check if the input system is enabled
*/
isEnabled(): boolean;
/**
* Emit an event to all registered callbacks
*/
protected emit(eventType: string, ...args: unknown[]): void;
/**
* Log debug messages if debug mode is enabled
*/
protected log(...args: unknown[]): void;
}
/**
* Type guard to check if an object implements IInputs
*/
declare function isInputs(obj: unknown): obj is IInputs;
/**
* KeyboardInputs - Keyboard input management
* Handles keyboard events with support for vanilla JS, React, and other frameworks
*/
/**
* Keyboard button state with press/release detection
*/
interface KeyButtonState {
pressed: boolean;
justPressed: boolean;
justReleased: boolean;
}
interface KeyState {
[key: string]: KeyButtonState;
}
interface KeyboardCallbacks {
onKeyDown?: (key: string, event: KeyboardEvent) => void;
onKeyUp?: (key: string, event: KeyboardEvent) => void;
}
declare class KeyboardInputs extends BaseInputs {
private keys;
private keyboardCallbacks;
private textInputsThisFrame;
private boundHandlers;
constructor(targetElement?: HTMLElement | Window, config?: InputConfig);
start(): void;
stop(): void;
reset(): void;
resetDelta(): void;
/**
* Poll - Reset transient states (justPressed/justReleased)
* Should be called once per frame AFTER collecting input
*/
poll(): void;
/**
* Get text inputs captured this frame
* Returns actual characters typed (handles keyboard layout, modifiers, etc.)
*
* @returns Array of character strings typed this frame
*
* @example
* ```typescript
* const textInputs = keyboard.getTextInputs();
* textInputs.forEach(char => chatBox.append(char));
* ```
*/
getTextInputs(): string[];
/**
* Check if a key is currently pressed
*
* @param key - Key code (e.g., 'Space', 'KeyA', 'ArrowUp')
* @returns True if key is pressed, false otherwise
*
* @example
* ```typescript
* if (keyboard.isKeyPressed('Space')) {
* player.jump();
* }
*
* if (keyboard.isKeyPressed('KeyW')) {
* player.moveForward();
* }
* ```
*/
isKeyPressed(key: string): boolean;
/**
* Check if a key was just pressed this frame
*
* @param key - Key code (e.g., 'Space', 'KeyA', 'ArrowUp')
* @returns True if key was just pressed (transition false→true), false otherwise
*
* @example
* ```typescript
* if (keyboard.isKeyJustPressed('Space')) {
* player.jump(); // Jump once per press
* }
* ```
*/
isKeyJustPressed(key: string): boolean;
/**
* Check if a key was just released this frame
*
* @param key - Key code (e.g., 'Space', 'KeyA', 'ArrowUp')
* @returns True if key was just released (transition true→false), false otherwise
*
* @example
* ```typescript
* if (keyboard.isKeyJustReleased('Space')) {
* releaseChargedShot();
* }
* ```
*/
isKeyJustReleased(key: string): boolean;
/**
* Get all currently pressed keys
*
* @returns Array of key codes currently pressed
*
* @example
* ```typescript
* const keys = keyboard.getKeysPressed();
* console.log('Pressed keys:', keys); // ['KeyW', 'Space']
* ```
*/
getKeysPressed(): string[];
/**
* Get all keys that were just pressed this frame
*/
getKeysJustPressed(): string[];
/**
* Get all keys that were just released this frame
*/
getKeysJustReleased(): string[];
/**
* Check if any key is currently pressed
*
* @returns True if at least one key is pressed, false otherwise
*
* @example
* ```typescript
* if (keyboard.isAnyKeyPressed()) {
* console.log('Player is active');
* }
* ```
*/
isAnyKeyPressed(): boolean;
/**
* Set keyboard-specific callbacks
*
* @param callbacks - Object containing callback functions
*
* @example
* ```typescript
* keyboard.setKeyboardCallbacks({
* onKeyDown: (key, event) => {
* console.log(`Key pressed: ${key}`);
* },
* onKeyUp: (key, event) => {
* console.log(`Key released: ${key}`);
* },
* });
* ```
*/
setKeyboardCallbacks(callbacks: KeyboardCallbacks): void;
clearKeyboardCallbacks(): void;
setCallbacks(callbacks: KeyboardCallbacks | unknown): void;
private isKeyboardCallbacks;
private handleKeyDown;
private handleKeyUp;
}
/**
* React hook for keyboard inputs
*/
declare function useKeyboardInputs(targetElement?: HTMLElement | Window, callbacks?: KeyboardCallbacks): KeyboardInputs;
/**
* MouseInputs - Mouse input management
* Handles mouse events with support for vanilla JS, React, and other frameworks
*/
/**
* Mouse button state with press/release detection
*/
interface MouseButtonState {
pressed: boolean;
justPressed: boolean;
justReleased: boolean;
}
interface MouseButtons {
left: MouseButtonState;
middle: MouseButtonState;
right: MouseButtonState;
}
interface MouseCallbacks {
onMouseDown?: (button: string, position: Vector2, event: MouseEvent) => void;
onMouseUp?: (button: string, position: Vector2, event: MouseEvent) => void;
onMouseMove?: (position: Vector2, delta: Vector2, event: MouseEvent) => void;
onMouseWheel?: (delta: number, event: WheelEvent) => void;
onMouseEnter?: (event: MouseEvent) => void;
onMouseLeave?: (event: MouseEvent) => void;
}
declare class MouseInputs extends BaseInputs {
private mouseButtons;
private mousePosition;
private mouseDelta;
private wheelDelta;
private mouseCallbacks;
private boundHandlers;
constructor(targetElement?: HTMLElement | Window, config?: InputConfig);
start(): void;
stop(): void;
reset(): void;
/**
* Poll - Reset transient states (justPressed/justReleased)
* Should be called once per frame AFTER collecting input
*/
poll(): void;
resetDelta(): void;
/**
* Check if left mouse button is pressed
*
* @returns True if left button is pressed, false otherwise
*
* @example
* ```typescript
* if (mouse.isLeftMousePressed()) {
* console.log('Left click detected');
* }
* ```
*/
isLeftMousePressed(): boolean;
/**
* Check if left mouse button was just pressed this frame
*
* @returns True if button was just pressed (transition false→true), false otherwise
*
* @example
* ```typescript
* if (mouse.isLeftMouseJustPressed()) {
* fireWeapon(); // Fires once per click
* }
* ```
*/
isLeftMouseJustPressed(): boolean;
/**
* Check if left mouse button was just released this frame
*
* @returns True if button was just released (transition true→false), false otherwise
*
* @example
* ```typescript
* if (mouse.isLeftMouseJustReleased()) {
* releaseChargedAttack();
* }
* ```
*/
isLeftMouseJustReleased(): boolean;
/**
* Check if right mouse button is pressed
*
* @returns True if right button is pressed, false otherwise
*
* @example
* ```typescript
* if (mouse.isRightMousePressed()) {
* showContextMenu();
* }
* ```
*/
isRightMousePressed(): boolean;
/**
* Check if right mouse button was just pressed this frame
*/
isRightMouseJustPressed(): boolean;
/**
* Check if right mouse button was just released this frame
*/
isRightMouseJustReleased(): boolean;
/**
* Check if middle mouse button is pressed
*
* @returns True if middle button is pressed, false otherwise
*/
isMiddleMousePressed(): boolean;
/**
* Check if middle mouse button was just pressed this frame
*/
isMiddleMouseJustPressed(): boolean;
/**
* Check if middle mouse button was just released this frame
*/
isMiddleMouseJustReleased(): boolean;
/**
* Get current mouse position
*
* @returns Vector2 with x and y coordinates in pixels
*
* @example
* ```typescript
* const pos = mouse.getMousePosition();
* console.log(`Mouse at (${pos.x}, ${pos.y})`);
* ```
*/
getMousePosition(): Vector2;
/**
* Get mouse movement delta since last frame
*
* @returns Vector2 with dx and dy in pixels
*
* @example
* ```typescript
* const delta = mouse.getMouseDelta();
* camera.rotate(delta.x * sensitivity, delta.y * sensitivity);
* ```
*/
getMouseDelta(): Vector2;
/**
* Get mouse wheel scroll delta
*
* @returns Wheel delta (positive = scroll down, negative = scroll up)
*
* @example
* ```typescript
* const wheel = mouse.getWheelDelta();
* if (wheel !== 0) {
* camera.zoom(wheel > 0 ? -1 : 1);
* }
* ```
*/
getWheelDelta(): number;
/**
* Set mouse-specific callbacks
*
* @param callbacks - Object containing callback functions
*
* @example
* ```typescript
* mouse.setMouseCallbacks({
* onMouseDown: (button, position, event) => {
* console.log(`${button} button pressed at`, position);
* },
* onMouseMove: (position, delta, event) => {
* console.log('Mouse moved by', delta);
* },
* onMouseWheel: (delta, event) => {
* console.log('Wheel scrolled:', delta);
* },
* });
* ```
*/
setMouseCallbacks(callbacks: MouseCallbacks): void;
clearMouseCallbacks(): void;
setCallbacks(callbacks: MouseCallbacks | unknown): void;
private isMouseCallbacks;
private handleMouseDown;
private handleMouseUp;
private handleMouseMove;
private handleWheel;
private handleMouseEnter;
private handleMouseLeave;
private handleContextMenu;
}
/**
* React hook for mouse inputs
*/
declare function useMouseInputs(targetElement?: HTMLElement | Window, callbacks?: MouseCallbacks): MouseInputs;
/**
* MobileInputs - Touch and mobile device input management
* Handles touch events with support for multi-touch, gestures, and orientation
*/
interface TouchPoint {
id: number;
nativeId: number;
position: Vector2;
startPosition: Vector2;
active: boolean;
justTouched: boolean;
justReleased: boolean;
}
interface MobileCallbacks {
onTouchStart?: (touches: TouchPoint[], event: TouchEvent) => void;
onTouchEnd?: (touches: TouchPoint[], event: TouchEvent) => void;
onTouchMove?: (touches: TouchPoint[], event: TouchEvent) => void;
onTouchCancel?: (touches: TouchPoint[], event: TouchEvent) => void;
onOrientationChange?: (orientation: number) => void;
}
interface MobileInputsConfig extends InputConfig {
/** Prevent default browser behavior (scroll, zoom) - Default: true */
preventDefault?: boolean;
/** Use passive event listeners for better performance - Default: false */
passive?: boolean;
/** Maximum number of simultaneous touches - Default: 10 */
maxTouches?: number;
}
declare class MobileInputs extends BaseInputs {
private touches;
private nativeToInternal;
private mobileCallbacks;
private mobileConfig;
private maxTouches;
private lastLogTime;
private boundHandlers;
constructor(targetElement?: HTMLElement | Window, config?: MobileInputsConfig);
start(): void;
stop(): void;
reset(): void;
resetDelta(): void;
/**
* Poll - Reset transient states (justTouched/justReleased)
* Should be called once per frame AFTER collecting input
*/
poll(): void;
/**
* Check if a touch is active (touchId 0-9)
*
* @param touchId - Touch identifier (0-9)
* @returns True if touch is active, false otherwise
*
* @example
* ```typescript
* if (mobile.isTouchActive(0)) {
* const pos = mobile.getTouchPosition(0);
* console.log('First touch at', pos);
* }
* ```
*/
isTouchActive(touchId: number): boolean;
/**
* Get the position of a touch (touchId 0-9)
*
* @param touchId - Touch identifier (0-9)
* @returns Vector2 with current position, or null if touch is not active
*
* @example
* ```typescript
* const pos = mobile.getTouchPosition(0);
* if (pos) {
* player.moveTo(pos.x, pos.y);
* }
* ```
*/
getTouchPosition(touchId: number): Vector2 | null;
/**
* Get the starting position of a touch (touchId 0-9)
*
* @param touchId - Touch identifier (0-9)
* @returns Vector2 with start position, or null if touch is not active
*
* @example
* ```typescript
* const start = mobile.getTouchStartPosition(0);
* const current = mobile.getTouchPosition(0);
* if (start && current) {
* const swipeDistance = current.subtract(start).length();
* if (swipeDistance > 100) {
* console.log('Swipe detected!');
* }
* }
* ```
*/
getTouchStartPosition(touchId: number): Vector2 | null;
/**
* Get the delta since the beginning of the touch
*
* @param touchId - Touch identifier (0-9)
* @returns Vector2 with delta from start position, or null if touch is not active
*
* @example
* ```typescript
* const delta = mobile.getTouchDelta(0);
* if (delta && delta.x > 50) {
* console.log('Swiped right!');
* }
* ```
*/
getTouchDelta(touchId: number): Vector2 | null;
/**
* Get the number of active touches
*
* @returns Number of simultaneous touches (0-10)
*
* @example
* ```typescript
* const count = mobile.getTouchCount();
* if (count === 2) {
* console.log('Pinch gesture possible');
* }
* ```
*/
getTouchCount(): number;
/**
* Check if a touch just started this frame
*
* @param touchId - Touch identifier (0-9)
* @returns True if touch just started, false otherwise
*
* @example
* ```typescript
* if (mobile.isTouchJustStarted(0)) {
* playSound('tap');
* }
* ```
*/
isTouchJustStarted(touchId: number): boolean;
/**
* Check if a touch just ended this frame
*
* @param touchId - Touch identifier (0-9)
* @returns True if touch just ended, false otherwise
*
* @example
* ```typescript
* if (mobile.isTouchJustReleased(0)) {
* releaseDragObject();
* }
* ```
*/
isTouchJustReleased(touchId: number): boolean;
/**
* Check if any touch just started this frame
*/
isAnyTouchJustStarted(): boolean;
/**
* Get all active touches
*
* @returns Array of all active TouchPoint objects
*
* @example
* ```typescript
* const touches = mobile.getAllTouches();
* touches.forEach(touch => {
* console.log(`Touch ${touch.id} at`, touch.position);
* });
* ```
*/
getAllTouches(): TouchPoint[];
/**
* Set mobile-specific callbacks
*
* @param callbacks - Object containing callback functions
*
* @example
* ```typescript
* mobile.setMobileCallbacks({
* onTouchStart: (touches, event) => {
* console.log(`${touches.length} touches started`);
* },
* onTouchMove: (touches, event) => {
* touches.forEach(t => console.log(`Touch ${t.id}:`, t.position));
* },
* onOrientationChange: (orientation) => {
* console.log('Orientation:', orientation);
* },
* });
* ```
*/
setMobileCallbacks(callbacks: MobileCallbacks): void;
clearMobileCallbacks(): void;
setCallbacks(callbacks: MobileCallbacks | unknown): void;
private isMobileCallbacks;
private handleTouchStart;
private handleTouchEnd;
private handleTouchMove;
private handleTouchCancel;
private handleOrientationChange;
}
/**
* React hook for mobile inputs
*
* ⚠️ IMPORTANT: This function is NOT a proper React hook!
* It creates a new MobileInputs instance on every render, causing memory leaks.
*
* To use correctly in React, wrap with useMemo and useEffect:
*
* @example
* ```typescript
* import { useMemo, useEffect } from 'react';
*
* const inputs = useMemo(() => new MobileInputs(canvasRef.current, config), [config]);
*
* useEffect(() => {
* if (callbacks) inputs.setCallbacks(callbacks);
* inputs.start();
* return () => inputs.stop();
* }, [inputs, callbacks]);
* ```
*
* @deprecated Use the pattern above instead of this function
*/
declare function useMobileInputs(targetElement?: HTMLElement | Window, callbacks?: MobileCallbacks, config?: MobileInputsConfig): MobileInputs;
/**
* GamepadInputs - Gamepad/Controller input management
* Handles gamepad events with support for multiple controllers, buttons, axes, and vibration
*/
/**
* Gamepad button state with press detection
*/
interface GamepadButtonState {
pressed: boolean;
justPressed: boolean;
justReleased: boolean;
value: number;
touched: boolean;
}
/**
* Complete state of a single gamepad
*/
interface GamepadState {
id: string;
index: number;
connected: boolean;
buttons: GamepadButtonState[];
axes: number[];
timestamp: number;
mapping: GamepadMappingType;
hapticActuators?: GamepadHapticActuator[];
}
/**
* Callbacks for gamepad events
*/
interface GamepadCallbacks {
onGamepadConnected?: (gamepad: GamepadState) => void;
onGamepadDisconnected?: (index: number) => void;
onButtonDown?: (gamepadIndex: number, buttonIndex: number, value: number) => void;
onButtonUp?: (gamepadIndex: number, buttonIndex: number) => void;
onAxisMove?: (gamepadIndex: number, axisIndex: number, value: number) => void;
}
/**
* Configuration for GamepadInputs
*/
interface GamepadInputsConfig extends InputConfig {
/** Maximum number of gamepads to support (default: 4) */
maxGamepads?: number;
/** Deadzone for analog axes (default: 0.1) */
axisDeadzone?: number;
/** Polling interval in milliseconds (default: 16 ~= 60fps) */
pollingInterval?: number;
/** Enable automatic polling (default: true) */
autoPolling?: boolean;
}
declare class GamepadInputs extends BaseInputs {
private gamepads;
private previousGamepads;
private gamepadCallbacks;
private gamepadConfig;
private pollingIntervalId;
private rafId;
private boundHandlers;
constructor(config?: GamepadInputsConfig);
start(): void;
stop(): void;
reset(): void;
resetDelta(): void;
/**
* Start automatic polling of gamepad state
*/
startPolling(): void;
/**
* Stop automatic polling
*/
stopPolling(): void;
/**
* Manually poll gamepad state (call this in your game loop if autoPolling is false)
*
* @example
* ```typescript
* const gamepad = new GamepadInputs({ autoPolling: false });
* gamepad.start();
*
* function gameLoop() {
* gamepad.poll(); // Manual polling
* // Check inputs...
* requestAnimationFrame(gameLoop);
* }
* ```
*/
poll(): void;
/**
* Get the number of connected gamepads
*
* @returns Number of gamepads currently connected (0-4)
*
* @example
* ```typescript
* const count = gamepad.getConnectedGamepadCount();
* console.log(`${count} gamepads connected`);
* ```
*/
getConnectedGamepadCount(): number;
/**
* Check if a gamepad is connected at the given index
*
* @param index - Gamepad index (0-3)
* @returns True if gamepad is connected, false otherwise
*
* @example
* ```typescript
* if (gamepad.isGamepadConnected(0)) {
* console.log('Player 1 controller ready!');
* }
* ```
*/
isGamepadConnected(index: number): boolean;
/**
* Get the state of a specific gamepad
*/
getGamepadState(index: number): GamepadState | null;
/**
* Get all connected gamepads
*/
getAllGamepads(): GamepadState[];
/**
* Check if a button is currently pressed
*
* @param gamepadIndex - Gamepad index (0-3)
* @param buttonIndex - Button index (0-16, standard mapping)
* @returns True if button is pressed, false otherwise
* @throws {Error} If gamepadIndex or buttonIndex is invalid
*
* @example
* ```typescript
* // Check A button (Xbox) / Cross (PlayStation)
* if (gamepad.isButtonPressed(0, 0)) {
* console.log('Action button pressed!');
* }
* ```
*/
isButtonPressed(gamepadIndex: number, buttonIndex: number): boolean;
/**
* Check if a button was just pressed this frame
*/
isButtonJustPressed(gamepadIndex: number, buttonIndex: number): boolean;
/**
* Check if a button was just released this frame
*/
isButtonJustReleased(gamepadIndex: number, buttonIndex: number): boolean;
/**
* Get the analog value of a button (0.0 to 1.0)
*/
getButtonValue(gamepadIndex: number, buttonIndex: number): number;
/**
* Get the value of an axis (-1.0 to 1.0, with deadzone applied)
*
* @param gamepadIndex - Gamepad index (0-3)
* @param axisIndex - Axis index (0: left X, 1: left Y, 2: right X, 3: right Y)
* @returns Axis value from -1.0 (left/up) to 1.0 (right/down)
* @throws {Error} If gamepadIndex or axisIndex is invalid
*
* @example
* ```typescript
* const leftStickX = gamepad.getAxis(0, 0);
* const leftStickY = gamepad.getAxis(0, 1);
*
* playerX += leftStickX * speed;
* playerY += leftStickY * speed;
* ```
*/
getAxis(gamepadIndex: number, axisIndex: number): number;
/**
* Get the raw axis value without deadzone
*/
getAxisRaw(gamepadIndex: number, axisIndex: number): number;
/**
* Get left stick as a 2D vector (x: axis 0, y: axis 1)
*
* @param gamepadIndex - Gamepad index (0-3)
* @returns Object with x and y properties (-1.0 to 1.0)
*
* @example
* ```typescript
* const leftStick = gamepad.getLeftStick(0);
*
* if (Math.abs(leftStick.x) > 0.5) {
* console.log('Strong horizontal input');
* }
*
* // Calculate magnitude for movement
* const magnitude = Math.sqrt(leftStick.x ** 2 + leftStick.y ** 2);
* ```
*/
getLeftStick(gamepadIndex: number): {
x: number;
y: number;
};
/**
* Get right stick as a 2D vector (x: axis 2, y: axis 3)
*/
getRightStick(gamepadIndex: number): {
x: number;
y: number;
};
/**
* Set gamepad-specific callbacks
*
* @param callbacks - Object containing callback functions
*
* @example
* ```typescript
* gamepad.setGamepadCallbacks({
* onGamepadConnected: (state) => {
* console.log(`Gamepad connected: ${state.id}`);
* },
* onButtonDown: (gamepadIndex, buttonIndex, value) => {
* console.log(`Button ${buttonIndex} pressed with value ${value}`);
* },
* onAxisMove: (gamepadIndex, axisIndex, value) => {
* console.log(`Axis ${axisIndex} moved to ${value}`);
* },
* });
* ```
*/
setGamepadCallbacks(callbacks: GamepadCallbacks): void;
/**
* Clear all gamepad callbacks
*/
clearGamepadCallbacks(): void;
setCallbacks(callbacks: GamepadCallbacks | unknown): void;
private isGamepadCallbacks;
private updateGamepadState;
private triggerCallbacks;
private handleGamepadConnected;
private handleGamepadDisconnected;
private applyDeadzone;
private cloneGamepadState;
}
/**
* React hook for gamepad inputs
*/
declare function useGamepadInputs(callbacks?: GamepadCallbacks, config?: GamepadInputsConfig): GamepadInputs;
/**
* UnifiedInputRouter - Unified input routing system
* Provides a standardized API to query input states across all devices using numeric enums
*
* @example
* ```ts
* const router = new UnifiedInputRouter();
*
* // Check if space key is pressed
* const jump = router.getButton(InputDeviceType.Keyboard, KeyboardInput.Space);
*
* // Get mouse X position
* const mouseX = router.getAxis(InputDeviceType.Mouse, MouseInput.PositionX);
*
* // Check gamepad button A
* const action = router.getButton(InputDeviceType.Gamepad, GamepadInput.ButtonA);
* ```
*/
/**
* Configuration options for the input router
*/
interface InputRouterConfig {
enableKeyboardMouse?: boolean;
enableGamepad?: boolean;
enableMobile?: boolean;
targetElement?: HTMLElement | Window;
/** Separate target for mobile/touch input (default: uses targetElement) */
mobileTargetElement?: HTMLElement | Window;
debug?: boolean;
/** Keyboard-specific config */
keyboardConfig?: {
preventDefault?: boolean;
stopPropagation?: boolean;
};
/** Mouse-specific config */
mouseConfig?: {
preventDefault?: boolean;
stopPropagation?: boolean;
};
/** Mobile-specific config */
mobileConfig?: {
preventDefault?: boolean;
passive?: boolean;
maxTouches?: number;
};
}
/**
* Unified input router - manages all input devices with a type-safe enum-based API
* Implements IInputSystem interface for dependency injection with core
*/
declare class UnifiedInputRouter implements IInputSystem {
private keyboard;
private mouse;
private gamepad;
private mobile;
private config;
constructor(config?: InputRouterConfig);
private initialize;
/**
* Query a button state (returns boolean)
*/
getButton(device: InputDeviceType, input: number): boolean;
/**
* Query if a button was just pressed this frame (transition false→true)
*/
getButtonJustPressed(device: InputDeviceType, input: number): boolean;
/**
* Query if a button was just released this frame (transition true→false)
*/
getButtonJustReleased(device: InputDeviceType, input: number): boolean;
/**
* Query an axis value (returns number)
*/
getAxis(device: InputDeviceType, input: number): number;
private getKeyboardButton;
private getKeyboardButtonJustPressed;
private getKeyboardButtonJustReleased;
private getMouseButton;
private getMouseButtonJustPressed;
private getMouseButtonJustReleased;
private getGamepadButton;
private getGamepadButtonJustPressed;
private getGamepadButtonJustReleased;
private getTouchButton;
private getTouchButtonJustPressed;
private getTouchButtonJustReleased;
private getMouseAxis;
private getGamepadAxis;
private getTouchAxis;
/**
* Get mouse position (screen coordinates)
*/
getMousePosition(): {
x: number;
y: number;
} | null;
/**
* Reconfigure mobile input target after initialization
* (Used when canvas is created after InputRouter construction)
* Mobile input will ONLY be activated if a valid canvas is provided
*/
setMobileTarget(canvas: HTMLElement | Window): void;
/**
* Get text inputs captured this frame from keyboard
* Returns actual characters typed (handles keyboard layout, modifiers, etc.)
*
* @returns Array of character strings typed this frame
*/
getTextInputs(): string[];
start(): void;
stop(): void;
reset(): void;
/**
* Poll transient states - Reset justPressed/justReleased flags
* Should be called once per frame AFTER collecting input
*/
poll(): void;
isListening(): boolean;
destroy(): void;
private log;
private warn;
}
/**
* InputCollector - Adapter between UnifiedInputRouter and InputBindingRegistry
*
* This module bridges the gap between:
* - @utsp/input (raw device inputs)
* - @utsp/core (logical input evaluation with InputBindingRegistry)
*
* InputCollector's sole responsibility is to read raw values from IInputSystem
* and prepare them as Map<sourceId, value> for InputBindingRegistry.evaluateAxis/Button.
*
* @example
* ```typescript
* // Collect raw source values
* const axisSources = InputCollector.collectAxisSources(axisBindings, inputRouter);
* const buttonSources = InputCollector.collectButtonSources(buttonBindings, inputRouter);
*
* // Let InputBindingRegistry handle scale/sensitivity/invert/OR logic
* axisBindings.forEach(binding => {
* const value = registry.evaluateAxis(binding.bindingId, axisSources);
* user.setAxis(binding.name, value);
* });
* ```
*/
/**
* Button state with transition detection (pressed, justPressed, justReleased)
*/
interface ButtonStateWithTransitions {
pressed: boolean;
justPressed: boolean;
justReleased: boolean;
}
/**
* Input collection utilities
*
* Provides static methods to collect raw input values from an IInputSystem
* and prepare them for InputBindingRegistry evaluation.
*/
declare class InputCollector {
/**
* Collects raw source values for all axis bindings
*
* Returns a Map where:
* - Key: sourceId (from AxisBinding.sources)
* - Value: raw axis value from the input router
*
* This method reads raw device values (keyboard -1/0/+1, mouse delta, gamepad stick, touch position)
* WITHOUT applying scale/sensitivity/invert. InputBindingRegistry.evaluateAxis() will handle that.
*
* @param bindings - Axis bindings from InputBindingRegistry
* @param router - Input system (typically UnifiedInputRouter)
* @returns Map of sourceId → raw value
*
* @example
* ```typescript
* const registry = user.getInputBindingRegistry();
* const axisBindings = registry.getAllAxes();
* const sourceValues = InputCollector.collectAxisSources(axisBindings, inputRouter);
*
* // Then use InputBindingRegistry to evaluate with scale/sensitivity/invert:
* axisBindings.forEach(binding => {
* const finalValue = registry.evaluateAxis(binding.bindingId, sourceValues);
* user.setAxis(binding.name, finalValue);
* });
* ```
*/
static collectAxisSources(bindings: AxisBinding[], router: IInputSystem): Map<number, number>;
/**
* Collects raw source states for all button bindings
*
* Returns a Map where:
* - Key: sourceId (from ButtonBinding.sources)
* - Value: raw button state (pressed/released) from the input router
*
* This method reads raw device button states WITHOUT applying OR logic.
* InputBindingRegistry.evaluateButton() will handle the OR logic across all sources.
*
* @param bindings - Button bindings from InputBindingRegistry
* @param router - Input system (typically UnifiedInputRouter)
* @returns Map of sourceId → pressed state
*
* @example
* ```typescript
* const registry = user.getInputBindingRegistry();
* const buttonBindings = registry.getAllButtons();
* const sourceStates = InputCollector.collectButtonSources(buttonBindings, inputRouter);
*
* // Then use InputBindingRegistry to evaluate with OR logic:
* buttonBindings.forEach(binding => {
* const pressed = registry.evaluateButton(binding.bindingId, sourceStates);
* user.setButton(binding.name, pressed);
* });
* ```
*/
static collectButtonSources(bindings: ButtonBinding[], router: IInputSystem): Map<number, boolean>;
/**
* Collects raw source states with transition detection for all button bindings
*
* Returns a Map where:
* - Key: sourceId (from ButtonBinding.sources)
* - Value: ButtonStateWithTransitions { pressed, justPressed, justReleased }
*
* This method reads raw device button states including transition flags.
* Use this to collect justPressed/justReleased states in addition to pressed.
*
* @param bindings - Button bindings from InputBindingRegistry
* @param router - Input system (typically UnifiedInputRouter with getButtonJustPressed/Released)
* @returns Map of sourceId → ButtonStateWithTransitions
*
* @example
* ```typescript
* const registry = user.getInputBindingRegistry();
* const buttonBindings = registry.getAllButtons();
* const sourceStates = InputCollector.collectButtonSourcesWithTransitions(buttonBindings, inputRouter);
*
* buttonBindings.forEach(binding => {
* // Evaluate each state independently (OR across sources)
* const pressed = registry.evaluateButton(binding.bindingId, new Map([...sourceStates].map(([k,v]) => [k, v.pressed])));
* const justPressed = registry.evaluateButton(binding.bindingId, new Map([...sourceStates].map(([k,v]) => [k, v.justPressed])));
* const justReleased = registry.evaluateButton(binding.bindingId, new Map([...sourceStates].map(([k,v]) => [k, v.justReleased])));
*
* user.setButton(binding.name, pressed);
* user.setButton(\`\${binding.name}_justPressed\`, justPressed);
* user.setButton(\`\${binding.name}_justReleased\`, justReleased);
* });
* ```
*/
static collectButtonSourcesWithTransitions(bindings: ButtonBinding[], router: IInputSystem): Map<number, ButtonStateWithTransitions>;
/**
* Calculate mouse position in grid coordinates
*
* Converts screen-space mouse position to grid cell coordinates (column/row).
* Handles canvas scaling and provides an "over" flag for out-of-bounds detection.
*
* @param router - Input system (typically UnifiedInputRouter)
* @param canvas - Canvas element to calculate relative position
* @param gridWidth - Number of columns in the grid
* @param gridHeight - Number of rows in the grid
* @param rendererOffsets - Optional renderer content offsets (for centered rendering)
* @returns Grid position {x: column, y: row, over: boolean}
*
* @example
* ```typescript
* const offsets = renderer.getOffsets?.() || { offsetX: 0, offsetY: 0 };
* const mousePos = InputCollector.collectMousePosition(
* inputRouter,
* renderer.getCanvas(),
* 80, // columns
* 24, // rows
* offsets
* );
* user.setMousePosition(mousePos.x, mousePos.y, mousePos.over, 0);
* ```
*/
static collectMousePosition(router: IInputSystem, canvas: HTMLCanvasElement, gridWidth: number, gridHeight: number, rendererOffsets?: {
offsetX: number;
offsetY: number;
}): {
x: number;
y: number;
over: boolean;
};
/**
* Calculate touch positions in grid coordinates
*
* Converts screen-space touch positions to grid cell coordinates for all active touches.
* Handles canvas scaling and provides an "over" flag for out-of-bounds detection.
*
* @param router - Input system (typically UnifiedInputRouter)
* @param canvas - Canvas element to calculate relative position
* @param gridWidth - Number of columns in the grid
* @param gridHeight - Number of rows in the grid
* @param maxTouches - Maximum number of touches to check (default: 10)
* @param rendererOffsets - Optional renderer content offsets (for centered rendering)
* @returns Array of touch positions {id, x: column, y: row, over: boolean}
*
* @example
* ```typescript
* const offsets = renderer.getOffsets?.() || { offsetX: 0, offsetY: 0 };
* const touches = InputCollector.collectTouchPositions(
* inputRouter,
* renderer.getCanvas(),
* 80, // columns
* 24, // rows
* 10, // max touches
* offsets
* );
* touches.forEach(touch => {
* user.setTouchPosition(touch.id, touch.x, touch.y, touch.over, 0);
* });
* ```
*/
static collectTouchPositions(router: IInputSystem, canvas: HTMLCanvasElement, gridWidth: number, gridHeight: number, maxTouches?: number, rendererOffsets?: {
offsetX: number;
offsetY: number;
}): Array<{
id: number;
x: number;
y: number;
over: boolean;
}>;
/**
* Collects text input events from keyboard
*
* Returns array of character strings typed this frame.
* Uses event.key to capture actual characters (handles keyboard layout automatically).
*
* @param router - Input system (typically UnifiedInputRouter)
* @returns Array of character strings typed this frame
*
* @example
* ```typescript
* const textInputs = InputCollector.collectTextInputs(inputRouter);
* // textInputs might be: ['a', 'b', 'c', 'é', '@', ' ']
* // Pass to network encoder or process locally
* ```
*/
static collectTextInputs(router: IInputSystem): string[];
}
export { BaseInputs, GamepadInputs, InputCollector, InputEventType, KeyboardInputs, MobileInputs, MouseInputs, UnifiedInputRouter, isInputs, useGamepadInputs, useKeyboardInputs, useMobileInputs, useMouseInputs };
export type { ButtonState, GamepadButtonState, GamepadCallbacks, GamepadInputsConfig, GamepadState, IInputs, InputConfig, InputDevice, InputEvent, InputRouterConfig, KeyState, KeyboardCallbacks, MobileCallbacks, MobileInputsConfig, MotionData, MouseButtonState, MouseButtons, MouseCallbacks, TouchPoint };