Skip to content

Controls architecture ​

Input groups the controls for one canvas.

  • It owns five devices and connects their browser listeners.
  • It advances input once per frame, or once per fixed step.
  • Each device also works on its own.

Application and Input ​

PartRoleOwned by Input
Mouse, Keyboard, Gamepad, Touchpadkeep their button, key, axis, or touch stateyes
Screenfullscreen state of the canvasyes
InputCombinationconditions across devicesno, the application creates it
AxisMapsamples inputs into named scalar valuesno, the application creates it

Devices do not call each other. Each listens on the canvas for what it needs:

  • Mouse follows the first touch as its left button and position.
  • Screen completes a pending fullscreen request on a mousedown, mouseup, or touchend.
  • Input only tracks the device preference: gamepad, or mouse, keyboard, and touch.

Browser adapters ​

Adapters give the devices browser APIs through narrow interfaces.

Calling order ​

HostCalls
Anyinput.connect() once, to attach listeners
Variable rateinput.update() each frame, before reading device state
Fixed stepsample() once per frame, then publish(reader), see below

Steps and rendered frames ​

The problem. A fixed-step game reads input twice per frame: before each fixed step, and before the rendered update. A frame runs several steps or none, so a single "since the last update()" view repeats edges or loses them.

The fix. Each device splits sampling from publishing.

  • sample() reads browser changes and turns them into edges.
  • Every edge is kept once for each reader.
  • publish(reader) gives one reader what it has not taken yet: the wasJust* flags, typed characters, movement, and wheel values.
  • update() is sample() then publish("step").
FrameStep reader seesFrame reader sees
One stepthe frame's edgesthe same edges
Three catch-up stepsevery edge on the first stepevery edge, once
No stepnothing; the edges stay pendingthe frame's edges
Paused, then one stepevery edge since the last stepeach frame's edges
Render skipped by maxFpsas in the rows abovenothing; the edges wait for the next drawn frame

Control bank ​

ControlBank in src/devices/ owns the edge rules for every device:

  • DOM handlers and gamepad polls call press(), release(), and repeat(). Held state changes at once; the edge is queued.
  • sample() copies queued edges to the step and frame readers, so a press and release between two samples reports both edges.
  • publish(reader) swaps in that reader's edges for the queries, "ANY" and "NONE" included.
  • A release that is the first edge of a control in a window records the control as held when the window started. wasDownBefore() reads it, which lets aliases tell a tap then a hold from a release then a press.

Each device only maps its controls: key codes, mouse button indexes, touch slots, and gamepad buttons plus axis directions (GamepadSlot).

Every per-reader value goes through one ReaderBuffer that holds queued, step, frame, and published samples. The sample type implements addTo(), clear(), and empty: ControlEdges for edges, MotionSample for mouse movement and wheel deltas, and TypedText for typed characters. Mouse wheel direction is a press and release of scrollUp or scrollDown in the bank, so scrollUp and isDown("scrollUp") read the same edge.

Keyboard ​

Keyboard reads keys two ways:

PathRunsReads
Pollingon publish(), once per step or drawn frameevent.code, a physical position
Bindingssynchronously on the keydown, outside the framekey chords

Matching a binding ​

KeyBindings keeps two indexes: by key code for physical chords, and by letter for letter chords. On a keydown:

  1. Resolve the letter from event.key, or from event.code on non-Latin layouts.
  2. Collect candidates from both indexes.
  3. Check each candidate's chords with KeyChord.matches(), exact modifiers included.
  4. Run candidates by priority until one handles the key.

KeyBindingMap ​

  • Sits above the KeyBindings registry.
  • Resolves each action to its chords once, from the defaults and the overrides.
  • Registers one binding per action.
  • Finds conflicts when the map is built, not on a keydown.

Labels ​

KeyChord.format() builds tooltip labels.

  • A letter chord prints its letter.
  • A physical chord prints the QWERTY character, or the user's keycap when given the map from loadKeyboardLayout().
  • The layout is only read for labels; matching never depends on it.

Layout ​

src/devices/keyboard/ holds the device. chords/ holds KeyChord, letter resolution, and keyboard layouts; bindings/ holds KeyBindings, KeyBindingMap, and KeyBindingSettings.

See also ​