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
| Part | Role | Owned by Input |
|---|---|---|
Mouse, Keyboard, Gamepad, Touchpad | keep their button, key, axis, or touch state | yes |
Screen | fullscreen state of the canvas | yes |
InputCombination | conditions across devices | no, the application creates it |
AxisMap | samples inputs into named scalar values | no, the application creates it |
Devices do not call each other. Each listens on the canvas for what it needs:
Mousefollows the first touch as its left button and position.Screencompletes a pending fullscreen request on a mousedown, mouseup, or touchend.Inputonly tracks the device preference: gamepad, or mouse, keyboard, and touch.
Browser adapters
Adapters give the devices browser APIs through narrow interfaces.
Calling order
| Host | Calls |
|---|---|
| Any | input.connect() once, to attach listeners |
| Variable rate | input.update() each frame, before reading device state |
| Fixed step | sample() 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: thewasJust*flags, typed characters, movement, and wheel values.update()issample()thenpublish("step").
| Frame | Step reader sees | Frame reader sees |
|---|---|---|
| One step | the frame's edges | the same edges |
| Three catch-up steps | every edge on the first step | every edge, once |
| No step | nothing; the edges stay pending | the frame's edges |
| Paused, then one step | every edge since the last step | each frame's edges |
Render skipped by maxFps | as in the rows above | nothing; 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(), andrepeat(). 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:
| Path | Runs | Reads |
|---|---|---|
| Polling | on publish(), once per step or drawn frame | event.code, a physical position |
| Bindings | synchronously on the keydown, outside the frame | key chords |
Matching a binding
KeyBindings keeps two indexes: by key code for physical chords, and by letter for letter chords. On a keydown:
- Resolve the letter from
event.key, or fromevent.codeon non-Latin layouts. - Collect candidates from both indexes.
- Check each candidate's chords with
KeyChord.matches(), exact modifiers included. - Run candidates by priority until one handles the key.
KeyBindingMap
- Sits above the
KeyBindingsregistry. - 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.