Skip to content

Event handling

textmode.js includes a unified input system for mouse, keyboard, touch, gesture, and gamepad input. You can use the familiar single-callback helpers such as mousePressed() and keyReleased(), or the newer shared event API with on(), off(), and once(). (•̀ᴗ•́)و

Two ways to register input

There are two event styles in the library:

The single-callback helpers are convenient when you only need one handler for a given event. The shared event API is better when you want multiple listeners or explicit teardown.

Shared event API

js
const t = textmode.create({ width: 800, height: 600 });

const disposeMove = t.on("mouseMoved", (data) => {
  console.log("mouse moved to", data.position.x, data.position.y);
});

t.once("keyPressed", (data) => {
  console.log("first key press:", data.key);
});

function onPress(data) {
  console.log("mouse pressed at", data.position);
}

t.on("mousePressed", onPress);

// Later:
t.off("mousePressed", onPress);
disposeMove();

Event overview

textmode.js exposes the following input event names:

Keyboard

Mouse

Touch lifecycle

Touch gestures

Gamepad

Mouse input

Mouse coordinates in textmode.js use the same center-based grid coordinates as the drawing API:

  • (0, 0) is the center of the grid
  • negative X is left, positive X is right
  • negative Y is up, positive Y is down

When the mouse is outside the grid, x and y are set to Number.NEGATIVE_INFINITY.

Polling mouse state

Use these properties inside draw():

js
const t = textmode.create({ width: 800, height: 600 });

t.draw(() => {
  t.background(0);

  if (!Number.isFinite(t.mouse.x) || !Number.isFinite(t.mouse.y)) {
    return;
  }

  t.push();
  t.translate(t.mouse.x, t.mouse.y);
  t.char("@");
  t.charColor(255, 120, 80);
  t.point();
  t.pop();
});

Mouse events

Mouse callbacks receive:

  • position
  • previousPosition
  • button for click and press events
  • delta for wheel events
  • originalEvent
js
const t = textmode.create({ width: 800, height: 600 });
const trail = [];

t.mouseDragged((data) => {
  if (!Number.isFinite(data.position.x) || !Number.isFinite(data.position.y))
    return;
  trail.push({ x: data.position.x, y: data.position.y, age: 0 });
});

t.draw(() => {
  t.background(0);

  for (let i = trail.length - 1; i >= 0; i--) {
    const p = trail[i];
    p.age++;

    if (p.age > 20) {
      trail.splice(i, 1);
      continue;
    }

    const brightness = Math.round(255 * (1 - p.age / 20));

    t.push();
    t.translate(p.x, p.y);
    t.char("*");
    t.charColor(brightness, brightness, 255);
    t.point();
    t.pop();
  }
});

Cursor and pointer lock

Mouse input also includes:

Pointer lock is useful when you want relative mouse movement through t.movedX and t.movedY, for example in camera controls or first-person navigation.

Keyboard input

Keyboard callbacks receive:

  • key
  • keyCode
  • ctrlKey
  • shiftKey
  • altKey
  • metaKey
  • isPressed
  • originalEvent

Keyboard events

  • keyPressed fires once when a key goes down
  • keyTyped fires for printable characters
  • keyReleased fires when a key goes up

Polling keyboard state

Use these helpers when you want continuous key state:

js
const t = textmode.create({ width: 800, height: 600 });
let x = 0;
let y = 0;

t.draw(() => {
  t.background(0);

  if (t.isKeyPressed("ArrowLeft")) x -= 1;
  if (t.isKeyPressed("ArrowRight")) x += 1;
  if (t.isKeyPressed("ArrowUp")) y -= 1;
  if (t.isKeyPressed("ArrowDown")) y += 1;

  t.push();
  t.translate(x, y);
  t.char("#");
  t.charColor(255, 255, 0);
  t.point();
  t.pop();
});

Touch input

Touch coordinates also use center-based grid coordinates. For per-touch data, each touch includes:

  • id
  • x and y
  • clientX and clientY
  • optional pressure, radiusX, radiusY, and rotationAngle

Touch lifecycle events

The lifecycle events are:

  • touchStarted
  • touchMoved
  • touchEnded
  • touchCancelled

Touch lifecycle callbacks receive:

  • touch
  • previousTouch
  • touches
  • previousTouches
  • changedTouches
  • deltaTime
  • originalEvent

The current active touches are also available through t.touches.

js
const t = textmode.create({ width: 800, height: 600 });

t.draw(() => {
  t.background(0);

  for (const touch of t.touches) {
    if (!Number.isFinite(touch.x) || !Number.isFinite(touch.y)) continue;

    t.push();
    t.translate(touch.x, touch.y);
    t.char("●");
    t.charColor(0, 220, 255);
    t.point();
    t.pop();
  }
});

Touch gestures

textmode.js includes built-in gesture recognition:

Gesture payloads

  • tap and doubleTap include touch and taps
  • longPress includes touch and duration
  • swipe includes touch, direction, distance, and velocity
  • pinch includes touches, scale, deltaScale, and center
  • rotateGesture includes touches, rotation, deltaRotation, and center
js
const t = textmode.create({ width: 800, height: 600 });
let scale = 1;

t.pinch((data) => {
  scale *= data.deltaScale;
  scale = Math.max(0.5, Math.min(3, scale));
});

t.draw(() => {
  t.background(0);

  t.push();
  t.scale(scale, scale);
  t.char("X");
  t.charColor(255, 255, 255);
  t.point();
  t.pop();
});

Gamepad input

textmode.js includes built-in browser Gamepad API support.

Gamepad events

Gamepad callbacks are useful for connection changes and discrete button or axis transitions:

Gamepad event payloads include:

  • gamepad for connection events
  • gamepad, buttonIndex, button, previousButton, and optional standardButtonName for button events
  • gamepad, axisIndex, value, previousValue, delta, and optional standardAxisName for axis events

Polling gamepad state

For continuous control, poll:

t.gamepads returns a compact list of connected controllers without sparse holes. For standard-mapped controllers, each snapshot may also expose standard.leftStick, standard.rightStick, standard.faceButtons, standard.shoulders, standard.center, and standard.dpad.

js
const t = textmode.create({ width: 800, height: 600 });
let x = 0;
let y = 0;

t.gamepadConnected((data) => {
  console.log("connected:", data.gamepad.id);
});

t.draw(() => {
  t.background(0);

  const gp = t.gamepads[0];
  const stick = gp?.standard?.leftStick;

  if (stick) {
    x += stick.x * 0.5;
    y += stick.y * 0.5;
  }

  t.push();
  t.translate(Math.round(x), Math.round(y));
  t.char("█");
  t.charColor(80, 255, 120);
  t.point();
  t.pop();
});

gamepadAxisChanged() is derived from per-frame polling, so for analog sticks and triggers, polling t.gamepads inside draw() is usually the simplest approach.

Choosing an approach

Use event callbacks when you care about edges or transitions:

  • clicks
  • taps
  • button presses
  • gamepad connect and disconnect

Use polling inside draw() when you care about continuous state:

  • held keys
  • current mouse position
  • active touches
  • analog stick movement

In practice, interactive sketches often use both.