Input Guide #
Blit-Tech provides DOM-backed input: pointer (mouse, touch, pen), keyboard (KeyboardEvent.code tracking and
virtual face buttons), gamepad (up to four players via navigator.getGamepads()), and text accumulation for UI
entry (BT.inputString).
All pointer coordinates are returned in logical display space (the displaySize from configure() or
defaultConfig()), independent of the canvas's CSS or backing-buffer size.
Pointer Slot Model #
The engine tracks up to four simultaneous pointers:
| Slot | Source |
|---|---|
| 0 | Mouse (always reserved) |
| 1 | First touch / pen contact (in arrival order) |
| 2 | Second touch / pen contact |
| 3 | Third touch / pen contact |
Overflow contacts beyond slot 3 are dropped silently. The constant POINTER_SLOT_COUNT (exported from the library)
equals 4.
Mouse slot (slot 0) #
- Becomes valid on the first
pointermoveinside the canvas. - Stays valid while the mouse remains over the canvas; cleared on
pointerleave. - Tracks all four buttons (A, B, C, D).
- Position is preserved after the mouse leaves; a new
pointermovere-syncs it.
Touch / pen slots (slots 1-3) #
- Allocated on
pointerdownin the first free slot; freed onpointerup,pointercancel, orpointerleave. - Only
BTN_POINTER_Areports true while the contact is down; B, C, D always return false. - Position and delta are preserved on release so release-frame code can read the final position and flick velocity.
- The engine calls
setPointerCaptureso off-canvas drags continue delivering events.
Position and Delta #
// Current position in display coordinates
const pos = BT.pointerPos(); // slot 0 (mouse)
const touch = BT.pointerPos(1); // slot 1 (first touch)
// Movement since the last frame
const delta = BT.pointerDelta(); // slot 0 (mouse)
const td = BT.pointerDelta(1); // slot 1
// Validity check - false means no live pointer in this slot
if (BT.pointerPosValid()) {
/* mouse is over the canvas */
}
if (BT.pointerPosValid(1)) {
/* touch slot 1 is active */
}
BT.pointerPos() returns Vector2i.zero() when the slot index is out of [0, 3] or the engine is not initialized.
BT.pointerDelta() similarly returns Vector2i.zero() for invalid slots.
Buttons #
Use BT.buttonDown(), BT.buttonPressed(), and BT.buttonReleased() with the BTN_POINTER_* constants. The second
parameter is the pointer slot (defaults to 0):
// Hold detection
BT.buttonDown(BT.BTN_POINTER_A); // left mouse button held
BT.buttonDown(BT.BTN_POINTER_B); // right mouse button held
BT.buttonDown(BT.BTN_POINTER_C); // middle mouse button held
BT.buttonDown(BT.BTN_POINTER_D); // back / forward button held
BT.buttonDown(BT.BTN_POINTER_A, 1); // touch slot 1 in contact
// Edge detection (true only on the transition frame)
BT.buttonPressed(BT.BTN_POINTER_A); // left button just pressed
BT.buttonReleased(BT.BTN_POINTER_A); // left button just released
BT.buttonPressed(BT.BTN_POINTER_A, 2); // touch slot 2 just touched down
Mouse button mapping #
The mapping follows the RetroBlit canonical order, not the DOM PointerEvent.button index:
| Button constant | Mouse button | DOM button index |
|---|---|---|
BTN_POINTER_A |
Left | 0 |
BTN_POINTER_B |
Right | 2 |
BTN_POINTER_C |
Middle / wheel click | 1 |
BTN_POINTER_D |
Back or forward | 3 or 4 |
Keyboard #
Listeners attach to the canvas during BT.init(). For reliable delivery, ensure the canvas can receive focus (for
example canvas.tabIndex = 0 and canvas.focus()). The library follows KeyboardEvent.code (physical-key semantics),
not event.key layout text.
Raw key queries #
Use these for direct key checks:
// Held state
if (BT.keyDown('KeyW')) {
/* W key held */
}
// Edge and optional tick-based repeat (repeat interval in fixed-update ticks)
if (BT.keyPressed('ArrowUp', 10)) {
/* first press or repeat every 10 ticks while held */
}
// Release edge
if (BT.keyReleased('Escape')) {
/* Escape released this frame */
}
Face buttons (BTN_UP through BTN_SELECT) #
Face button constants are bit flags. You can pass a single button or a combined mask; matching uses ANY semantics:
BT.buttonDown(BT.BTN_A | BT.BTN_B) is true when either A or B is down.
For player 0 and player 1, face-button reads merge keyboard maps and gamepad state (logical OR). For player 2 and player 3, face-button reads use gamepad only.
BT.buttonPressed supports optional tick-based repeat via repeatRate (0 or omitted = edge only), matching
BT.keyPressed semantics.
// Player 0 (default: WASD-style + Space / KeyB for A, etc.)
BT.buttonDown(BT.BTN_UP, 0);
BT.buttonPressed(BT.BTN_A, 0, 6); // edge + repeat every 6 ticks while held
// Player 1 (default: arrow keys + alternate bindings)
BT.buttonDown(BT.BTN_LEFT, 1);
Built-in defaults are exposed as read-only tables (same values the engine starts with):
BT.DEFAULT_KEYBOARD_PLAYER1BT.DEFAULT_KEYBOARD_PLAYER2
Remapping (BT.inputMap / BT.inputMapReset) #
Override bindings at runtime. The first argument is the zero-based player index (0 or 1 only; other values are
ignored). The second is the face button constant. Remaining arguments are KeyboardEvent.code strings.
BT.inputMap(0, BT.BTN_A, 'Space');
BT.inputMap(0, BT.BTN_UP, 'ArrowUp', 'KeyW'); // either key triggers UP for player 0
BT.inputMap(1, BT.BTN_START, 'Enter', 'NumpadEnter');
// Restore built-in defaults for both keyboard players (does not affect pointer or future gamepad state)
BT.inputMapReset();
Pass no key codes to clear keyboard bindings for that player and button until you map again:
BT.inputMap(0, BT.BTN_X); // player 0 X has no keyboard keys until remapped
Gamepad API #
BT.gamepadConnected(0); // true when player 0 has a connected gamepad
BT.gamepadCount; // number of connected gamepads (0..4)
BT.getAxis(BT.AXIS_LEFT_X, 0); // -1.0 .. 1.0 (dead-zone filtered)
BT.getAxis(BT.AXIS_TRIGGER_L, 0); // 0.0 .. 1.0
Axis constants:
AXIS_LEFT_X,AXIS_LEFT_YAXIS_RIGHT_X,AXIS_RIGHT_YAXIS_TRIGGER_L,AXIS_TRIGGER_R
Player constants:
PLAYER_ONE(0)PLAYER_TWO(1)PLAYER_THREE(2)PLAYER_FOUR(3)
Default dead zone for analog sticks is 0.75 (GamepadInput.DEFAULT_GAMEPAD_DEAD_ZONE).
Text input buffer #
BT.inputString returns characters accumulated from filtered beforeinput (and Tab / Escape where needed). The buffer
clears after each frame once the engine flushes input at end-of-frame. See public JSDoc on BT.inputString for details.
Scroll Delta #
// Accumulated vertical scroll for the current frame, in pixels
const scroll = BT.pointerScrollDelta;
if (scroll > 0) {
/* scrolled down */
}
if (scroll < 0) {
/* scrolled up */
}
The value aggregates all WheelEvent.deltaY values received since the last frame, normalizing line and page delta modes
to pixels. It resets to zero each frame. The engine also calls preventDefault() on wheel events so the page does not
scroll while the canvas has focus.
Cursor Control #
// In init(): hide the OS cursor and draw your own crosshair / sprite
BT.hideCursor();
// Restore at any time
BT.showCursor();
hideCursor() sets canvas.style.cursor = 'none'. showCursor() restores whatever the cursor was at attach() time.
Both are no-ops before the engine is initialized. The cursor is restored automatically when the engine stops.
Frame-Timing Semantics #
State is snapshotted at the end of each animation frame, after demo.update() and demo.render() have run. This
means:
- Any pointer event that fires between two frames is visible as a transition on the next
update()call. pointerDelta()reflects movement between the lastupdate()and the current one.- Keyboard-held keys and edges follow the same end-of-frame snapshot timing as pointer input (
KeyboardInput.endFramealigns with pointer flush). - Gamepad previous-state rollover also happens at end-of-frame, while current gamepad state is polled from the Gamepad API during button/axis queries.
buttonPressed()/buttonReleased()edges are never lost even when a press and release both arrive in the same inter-frame gap (they appear as pressed-then-released across consecutive frames).
Page-Interaction Guards #
attach() installs three guards on the canvas to prevent browser defaults from interfering:
wheelwith{ passive: false }andpreventDefault()- prevents page scroll on wheel events.canvas.style.touchAction = 'none'- prevents iOS Safari pinch-zoom and double-tap-zoom.contextmenuwithpreventDefault()- prevents the OS context menu on right-click soBTN_POINTER_Bworks.
detach() reverses all three and removes all event listeners. This happens automatically when the engine stops or when
demo.init() throws.
Coordinate Conversion #
Client coordinates from DOM events are converted to display space using the canvas bounding rect:
display_x = floor((clientX - rect.left) / rect.width * displaySize.x)
display_y = floor((clientY - rect.top) / rect.height * displaySize.y)
Coordinates are clamped to [0, displaySize - 1] on each axis. The conversion is skipped when the canvas has zero size
(no layout yet) to avoid NaN coordinates.
Implementation Notes #
PointerInput,KeyboardInput, andGamepadInputare internal; import fromblit-techand access throughBT.*methods.- Default keyboard tables live in
defaultKeyboardMap.ts; runtime remaps are stored in theBTfacade and reset withBT.inputMapReset(). - The
POINTER_SLOT_COUNTconstant (value4) is exported for demos that iterate over slots. PointerInput,KeyboardInput, andGamepadInputare created and attached insideBTAPI.init(), so they are ready beforedemo.init()runs.stop()callsdetach()on all three input subsystems and clears references to prevent listener leaks.