Skip to content

Input ​

Input owns the mouse, keyboard, gamepad, touch, and fullscreen controls for one canvas. It connects their browser listeners, advances their frame state, and tracks which input family was active most recently.

ts
import { Input } from "@jolly-pixel/controls";

const canvas = document.querySelector("canvas");
if (!canvas) {
  throw new Error("No canvas element found");
}

const input = new Input(canvas);
input.connect();

function gameLoop() {
  input.update();

  if (input.keyboard.wasJustPressed("Space")) {
    console.log("Jump!");
  }
  if (input.mouse.isDown("left")) {
    const delta = input.mouse.viewportDelta(true);
    console.log("Dragging", delta.x, delta.y);
  }

  requestAnimationFrame(gameLoop);
}

gameLoop();

Constructor ​

ts
interface InputOptions {
  enableOnExit?: boolean;
  windowAdapter?: WindowAdapter;
  documentAdapter?: DocumentAdapter;
}

new Input(
  canvas: CanvasAdapter,
  options?: InputOptions
)

An HTMLCanvasElement satisfies CanvasAdapter. The window and document adapters default to browser-backed implementations and are mainly useful for custom hosts and tests. Their interfaces are referenced by the public options but are not exported from the package root.

enableOnExit defaults to false. When enabled, Input assigns an onbeforeunload handler to the window adapter and emits exit once. The assignment remains installed after disconnect().

Devices ​

ts
interface Input {
  readonly mouse: Mouse;
  readonly keyboard: Keyboard;
  readonly gamepad: Gamepad;
  readonly touchpad: Touchpad;
  readonly screen: Screen;
}

The device instances can also be constructed and used on their own:

Input satisfies InputDevices, the keyboard, mouse, and gamepad members read by input conditions and axes.

Lifecycle ​

ts
type InputReader = "step" | "frame";

interface Input {
  connect(): void;
  disconnect(): void;
  update(): void;
  sample(): void;
  publish(reader: InputReader): void;
}

connect() / disconnect() ​

connect() registers the device listeners plus window blur and contextmenu listeners. Call it before expecting browser events to reach the input state.

disconnect() removes the listeners registered by connect(). It does not clear the onbeforeunload assignment created by enableOnExit.

The context-menu listener prevents the browser menu from opening. A window blur resets mouse, keyboard, gamepad, and touch state so held controls cannot remain stuck, which also clears mouse.hovering.

update() ​

Calls sample() then publish("step"). Call it once before the frame reads isDown, wasJustPressed, wasJustReleased, movement, or typed characters. A host without fixed steps needs nothing else.

sample() ​

Reads what changed in mouse, touchpad, keyboard, and gamepad since the previous sample, and keeps the new transitions, typed characters, wheel input, and mouse movement for both readers. Nothing is visible to queries until a publish().

sample() also changes devicePreference when it sees gamepad activity or activity from mouse, keyboard, or touch input.

publish(reader) ​

Hands one reader everything sampled since that reader's previous publish(), through the wasJust* flags, typed characters, wheel state, and movement. Each reader sees every edge once:

  • "step", before each fixed step. A catch-up frame reports an edge to its first step only. A frame with no step leaves the edges pending, so the next step still sees every press, including both edges of a tap.
  • "frame", before the rendered update. It sees every edge of the frame, whatever number of steps ran.

A fixed-step engine samples once per frame:

ts
input.sample();
for (let step = 0; step < steps; step++) {
  input.publish("step");
  fixedUpdate();
}
input.publish("frame");
update();

Device preference ​

ts
type InputDevicePreference = "default" | "gamepad";

interface Input {
  get devicePreference(): InputDevicePreference;
}

"default" covers mouse, keyboard, and touch input. The initial preference is "default". Input changes it to "gamepad" after gamepad activity and back to "default" after activity from one of the other devices.

Activity ​

ts
interface Input {
  get wasActive(): boolean;
}

wasActive is true while any device reports wasActive: a key held or auto-repeating, a mouse button or touch held, or a gamepad input found by the latest poll.

Exit state ​

ts
interface Input {
  exited: boolean;
}

exited starts as false. The first onbeforeunload callback emits exit and sets it to true; later callbacks do not emit the event again.

Events ​

Input extends Emitter from @openally/emitt.

ts
type InputEvents = {
  exit: () => void;
  devicePreferenceChange: (
    preference: InputDevicePreference
  ) => void;
};

exit requires enableOnExit: true. devicePreferenceChange receives the new preference after the state changes.

Listener types ​

InputListenerType is the dot-path union used by consumers that bind Input and device events through one name, such as @jolly-pixel/engine's @InputListener decorator.

ts
type InputListenerType =
  | "input.devicePreferenceChange"
  | "input.exit"
  | "mouse.down"
  | "mouse.up"
  | "mouse.move"
  | "mouse.wheel"
  | "mouse.enter"
  | "mouse.leave"
  | "mouse.lockStateChange"
  | "gamepad.connect"
  | "gamepad.disconnect"
  | "touchpad.start"
  | "touchpad.move"
  | "touchpad.end"
  | "screen.stateChange"
  | "keyboard.down"
  | "keyboard.up"
  | "keyboard.press"
  | `keyboard.${KeyCode}`;

Vibration ​

ts
vibrate(pattern: VibratePattern): void

Delegates to navigator.vibrate(). This is device vibration, commonly used on phones. Controller haptics are available through input.gamepad.vibration.