# Chapter 7: Mouse and keyboard input (/guides/handmade-web-games/mouse-and-keyboard-input)







JavaScript uses an event system with callback functions to handle user inputs.

```ts twoslash
const output = document.createElement("output");
// ---cut---
addEventListener("keydown", (event) => {
  output.textContent = `key: "${event.key}"`;
});
```

<KeyboardInputDemo />

This creates a bit of a challenge. If our game loop is something like this...

```ts
function loop() {
  update();
  draw();
  requestAnimationFrame(loop);
}

loop();
```

...then where in that loop is the event firing? What is the order of operations when user inputs can come in at any time?

<details>
  <summary>
    <strong>Reveal the answer</strong>
  </summary>

  The answer is that they don't fire *inside* the loop at all. Event listeners run in the background, firing between frames whenever the player does something. The loop's job is just to read whatever state those handlers have written by the time `fixedUpdate()` runs.
</details>

<Callout type="info" title="Before we begin...">
  These examples assume a game loop with `draw` and `fixedUpdate` functions, as well as a DPI-scaled
  `canvas` and its boundingClientRect and CanvasRenderingContext2D. If that sounds unfamiliar to
  you, take a look at [Chapter 3](/guides/handmade-web-games/game-loops).
</Callout>

## Keyboard events [#keyboard-events]

<Scrollycoding preview="canvas2d">
  <slot>
    <>    </>
  </slot>

  <slot path="steps">
    <>
      To bring keyboard events into the game loop, we need to change them from momentary actions into state that the loop can poll.

      Some states span many frames. For example, this demo reads which keys are currently pressed. The state only updates when events fire.

      <details>
        <summary>
          <i>Aside--where did `{ signal }` in the code example come from?</i>
        </summary>

        Because many of the demos on this page register events on the global `window` object, they'll interfere with one another if we don't tear down the event listeners when each preview unmounts.

        One way to do this is with `removeEventListener`:

        ```ts twoslash
        const callback = (event: KeyboardEvent) => {
          const { key, ctrlKey } = event;
          console.log(key);
          if (ctrlKey && key === "c") {
            removeEventListener("keydown", callback);
          }
        };
        addEventListener("keydown", callback);
        ```

        But because it requires holding onto a reference to the `callback` function, `removeEventListener` always feels a bit clunky to use. And in our particular case for these demos, coming up with a way to figure out which event each callback in every demo is tied to sounds awful.

        Thankfully, a much nicer alternative is provided by `AbortController`:

        ```ts twoslash
        const {
          abort, // a function you can call anywhere
          signal, // one signal can abort many things
        } = new AbortController();

        // log keydown events until "ctrl+c"
        addEventListener(
          "keydown",
          (event) => {
            const { key, ctrlKey } = event;
            console.log(key);
            if (ctrlKey && key === "c") {
              abort();
            }
          },
          { signal },
        );
        ```

        You can pass the same abort `signal` to as many event listeners as you want, then call `abort()` from anywhere. This `abort` is totally unaware of the event types and fully decoupled from the event handlers, making it great for this use case.

        A small downside we'll have to live with is that in each of the demos, you'll see a `{ signal }` passed in to the `addEventListener` but you won't see where it comes from, as we need a way to call `abort()` on unmount outside of the demo.

        For a deeper dive on this, see [Don't Sleep on AbortController](https://kettanaito.com/blog/dont-sleep-on-abort-controller) by Artem Zakharchenko.
      </details>
    </>
  </slot>

  <slot path="steps">
    <>
      The previous demo showed an example of continuous, pollable state. But sometimes we need events to behave like one-shot blips that fire just when the user interacts. How can our pollable state object handle these?

      The answer is to use event callbacks to set temporary event-related state (like which keys were *just* pressed this tick). Then `reset()` that state at the end of each `fixedUpdate()`.
    </>
  </slot>
</Scrollycoding>

## Mouse events [#mouse-events]

Let's look at how mouse events are usually handled for DOM elements, like an HTML `<button>`:

```ts twoslash
const button = document.createElement("button");
const output = document.createElement("output");
// ---cut---
button.onclick = (event) => {
  output.textContent = `x: ${event.x}, y: ${event.y}`;
};
```

<ButtonClickCoordsDemo />

Unlike this example, the items in canvas games aren't DOM elements and they don't have event handlers. So how can we make them clickable?

Before we get there, let's make the canvas itself capable of responding to <s>mouse</s> *pointer* events.

<details>
  <summary>
    <strong>Why not *mouse* events?</strong>
  </summary>

  Because not everyone clicks stuff using a mouse! Mobile devices tend to use touch events, tablets might use a stylus, and there are also things like trackpads or assistive devices that support multi-finger gestures.

  Pointer events are a hardware-agnostic way to model all of these.

  There are *many* events for mouse-ish things the browser responds to. But if you want to focus on just a handful, the ones with links are worth knowing for browser games.

  * **Legacy mouse events**
    * `mousedown`, `mouseup`, `mousemove`, `mouseenter`, `mouseleave`, `mouseover`, `mouseout`
  * **Pointer events** (the "modern" api for click-like events)
    * [`pointerdown`](https://developer.mozilla.org/en-US/docs/Web/API/Element/pointerdown_event), [`pointerup`](https://developer.mozilla.org/en-US/docs/Web/API/Element/pointerup_event), [`pointermove`](https://developer.mozilla.org/en-US/docs/Web/API/Element/pointermove_event), `pointerenter`, `pointerleave`, `pointerover`, `pointerout`, `pointercancel`, `gotpointercapture`, `lostpointercapture`
    * [`click`](https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event) is an odd one--it's a `PointerEvent` in modern browsers, but a `MouseEvent` in old ones. It generally fires right after `pointerup`, but can also be triggered programmatically, or by pressing <kbd>Enter</kbd> or <kbd>Space</kbd> when an interactive HTML element is focused.
  * **Specialty events**
    * `dblclick`, [`contextmenu`](https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event), `auxclick`
  * **Scroll events**
    * [`wheel`](https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event), `mousewheel`, `scroll`
  * **Touchscreen/trackpad events**
    * `touchstart`, `touchmove`, `touchend`, `touchcancel`
  * **Drag events**
    * `dragstart`, `drag`, `dragend`, `dragenter`, `dragover`, `dragleave`, `drop`
  * **Non-standard Apple-only gesture events**
    * `gesturestart`, `gesturechange`, `gestureend`
</details>

<Scrollycoding preview="canvas2d">
  <slot>
    <>    </>
  </slot>

  <slot path="steps">
    Just like with key presses in the previous section, our `fixedUpdate()` and `draw()` code never directly listen to pointer events. Instead there is a single global event listener for each event which sets the `state` that our `fixedUpdate()` and `draw()` can read.
  </slot>

  <slot path="steps">
    The cursor position is continuous, pollable state that persists across many frames, just like `keysDown`. To set it, listen for `pointermove` and set `(x, y)` coordinates offset by the canvas' actual location on the screen. This way `(0, 0)` is the top left corner.
  </slot>

  <slot path="steps">
    To detect if the user is clicking or hovering a specific in-game item, check if the cursor x/y position is within the bounds of that item at the time of the click. (This is the same logic we used to detect collisions in chapter 5.)
  </slot>

  <slot path="steps">
    Browsers support [dozens of built-in cursors](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/cursor) <small className="opacity-75">\[MDN]</small>, including an invisible one. Set `canvas.style.cursor` to change it.
  </slot>

  <slot path="steps">
    <>
      The event that shows the browser's native right-click menu is called `contextmenu`. If your game relies on right clicks, then you probably don't want that menu popping up. For this, use `preventDefault()` on the `contextmenu` event.

      Avoid overriding the right-click behavior without reason, though, since blocking the right-click menu is invasive to the user experience.

      <Callout type="info" title="Customizing right-click behavior">
        Every `PointerEvent` also supports an `event.button` property. To specifically detect right
        clicks, listen for `pointerdown` events where `event.button === 2`.

        ```ts twoslash
        addEventListener("pointerdown", (event) => {
          if (event.button === 0) {
            // left click occurred
          }
          if (event.button === 1) {
            // middle/wheel click occurred
          }
          if (event.button === 2) {
            // right click occurred
          }
        });
        ```
      </Callout>
    </>
  </slot>
</Scrollycoding>

## Hitbox test cheatsheet [#hitbox-test-cheatsheet]

**Rectangles:**

```ts twoslash
type Point = { x: number; y: number };
type Rect = { x: number; y: number; w: number; h: number };

function isPointInRectangle(point: Point, rect: Rect) {
  return (
    point.x >= rect.x &&
    point.x <= rect.x + rect.w &&
    point.y >= rect.y &&
    point.y <= rect.y + rect.h
  );
}
```

**An odd shape defined by a canvas path:**

To account for any current transforms, such as the DPI scaling, we need to map CSS pixel coordinates into the canvas' current transformed coordinate space before testing `isPointInPath`.

```ts twoslash
type Point = { x: number; y: number };

function makeStarPath(x: number, y: number) {
  const path = new Path2D();

  path.moveTo(x, y - 36);
  path.lineTo(x + 10, y - 10);
  path.lineTo(x + 38, y - 10);
  path.lineTo(x + 16, y + 6);
  path.lineTo(x + 24, y + 34);
  path.lineTo(x, y + 18);
  path.lineTo(x - 24, y + 34);
  path.lineTo(x - 16, y + 6);
  path.lineTo(x - 38, y - 10);
  path.lineTo(x - 10, y - 10);
  path.closePath();

  return path;
}

function isPointInPathShape(ctx: CanvasRenderingContext2D, point: Point, path: Path2D) {
  const transform = ctx.getTransform();
  const { x, y } = new DOMPoint(point.x, point.y).matrixTransform(transform);
  return ctx.isPointInPath(path, x, y);
}
```

**Circles:**

The formula for this is:

<math>
  <msup>
    <mrow>
      <mo>
        (
      </mo>

      <mi>
        x
      </mi>

      <mo>
        \-
      </mo>

      <mi>
        h
      </mi>

      <mo>
        )
      </mo>
    </mrow>

    <mn>
      2
    </mn>
  </msup>

  <mo>
    \+
  </mo>

  <msup>
    <mrow>
      <mo>
        (
      </mo>

      <mi>
        y
      </mi>

      <mo>
        \-
      </mo>

      <mi>
        k
      </mi>

      <mo>
        )
      </mo>
    </mrow>

    <mn>
      2
    </mn>
  </msup>

  <mo>
    ≤
  </mo>

  <msup>
    <mi>
      r
    </mi>

    <mn>
      2
    </mn>
  </msup>
</math>

where `(x,y)` are the coordinates for the point and `(h,k)` are the coordinates for the center of the circle.

```ts twoslash
type Point = { x: number; y: number };
type Circle = { x: number; y: number; radius: number };

function isPointInCircle(point: Point, circle: Circle) {
  const { radius } = circle;
  const distX = point.x - circle.x; // (x – h)
  const distY = point.y - circle.y; // (y – k)
  return distX ** 2 + distY ** 2 <= radius ** 2;
}
```

**Arbitrary polygons:**

If you shoot a horizontal ray to the right of the point `(x, y)` and count the number of intersections it has with each edge, an odd number of intersections means that the point is inside the polygon and an even number means it is outside.

<details>
  <summary>
    <strong>A deeper look into this algorithm</strong>
  </summary>

  Let's begin with a visualization. The polygon in question contains a point to test (the blue dot). The algorithm begins by casting a horizontal ray to the right of it, also in blue, then counting the number of edges it crosses. Edges whose `y` range contains the point's `y` become "relevant" candidates (drawn in <span className="hidden dark:inline">yellow</span><span className="dark:hidden">red</span>). If the number of edges crossed by this ray is odd, then the point is inside the polygon.

  <PointInPolygonDemo />

  (You can drag the point around to test it!)

  Credit to Harald @hg42 for [this writeup](https://observablehq.com/@hg42/untitled) from which the visualization above is forked. If you'd like to know more about how the ray-casting point-in-polygon algorithm works, I recommend you take a look at that post and the related [Wikipedia entry](https://en.wikipedia.org/wiki/Point_in_polygon).
</details>

```ts twoslash
type Point = { x: number; y: number };
type Polygon = [number, number][];

function isPointInPolygon({ x, y }: Point, vertices: Polygon) {
  let isInside = false;
  let previous = vertices.at(-1)!;

  for (const current of vertices) {
    const [xi, yi] = current;
    const [xj, yj] = previous;

    const crossesY = yi > y !== yj > y;
    const xIntersect = ((xj - xi) * (y - yi)) / (yj - yi) + xi;
    const crossesRay = crossesY && x < xIntersect;

    if (crossesRay) {
      isInside = !isInside;
    }

    previous = current;
  }

  return isInside;
}
```

## Pointer lock [#pointer-lock]

[Pointer lock](https://developer.mozilla.org/en-US/docs/Web/API/Element/requestPointerLock) <small className="opacity-75">\[MDN]</small> makes the cursor disappear and allows you to move it beyond the edges of the window. It changes the purpose of the mouse so that instead of looking at its `x`/`y` position on the screen, you only read its relative movements.

Why would you want this behavior? Picture a 3D first person shooter. There's no mouse cursor moving about the page--instead the world moves around you as you rotate in place. With pointer lock, you can move your mouse infinitely in any direction rather than being forced to stay in your display's boundaries.

The pointer events we'll be using in the next demo have `movementX` and `movementY` properties that track the deltas of the cursor's `x` and `y` positions between the current event and the previous event of the same type.

<Scrollycoding preview="canvas2d">
  <slot>
    <>    </>
  </slot>

  <slot path="steps">
    Click the canvas to lock the pointer, then move your mouse to pan around. Press <kbd>Escape</kbd> when you're done to release the lock.
  </slot>
</Scrollycoding>

## Working with world-space coordinates and cameras [#working-with-world-space-coordinates-and-cameras]

In [chapter 6](/guides/handmade-web-games/cameras-and-viewports) we used transforms to place in-game items in <World>world space</World> using a scaled coordinate system. Then we placed a <Camera>camera</Camera> over it to zoom and pan about that space.

In this chapter we'll need to invert those transforms in order to figure out what in-game location is being clicked or hovered. This will involve three transforms:

1. Event coordinates to <Screen>screen space</Screen>
2. <Screen>Screen space</Screen> coordinates to <World>world space</World>
3. Undo the <Camera>camera</Camera> to recover a <World>world</World>-space point

Let's start with the first (and simplest) transform. We need to normalize the event's coordinates into our canvas <Screen>screen space</Screen>. We also used this logic in the "Following the mouse cursor" demo above.

```ts twoslash
// a pointer event gives you window x/y coordinates.
// the canvas bounding rect tells you the position of the canvas in the window.
function toScreenSpace(event: PointerEvent, bounds: DOMRect) {
  return {
    x: event.clientX - bounds.left,
    y: event.clientY - bounds.top,
  };
}
```

After that, we can take those coordinates and see where they would fall in <World>world space</World>. If you recall our world space transformation from the previous chapter, this transformation simply inverts it.

<TransformPair>
  ```ts twoslash
  function letterbox(
    container: { width: number; height: number },
    world: { width: number; height: number },
  ) {
    const scale = Math.min(container.width / world.width, container.height / world.height);
    return {
      scale,
      xOffset: (container.width - world.width * scale) / 2,
      yOffset: (container.height - world.height * scale) / 2,
    };
  }
  // ---cut---
  function inWorldSpace(
    ctx: CanvasRenderingContext2D,
    gameArea: { width: number; height: number },
    canvasRect: { width: number; height: number },
    cb: () => void,
  ) {
    const { scale, xOffset, yOffset } = letterbox(canvasRect, gameArea);
    ctx.save();
    ctx.translate(xOffset, yOffset); // [!code highlight]
    ctx.scale(scale, scale); // [!code highlight]
    cb();
    ctx.restore();
  }
  ```

  ```ts twoslash
  function letterbox(
    container: { width: number; height: number },
    world: { width: number; height: number },
  ) {
    const scale = Math.min(container.width / world.width, container.height / world.height);
    return {
      scale,
      xOffset: (container.width - world.width * scale) / 2,
      yOffset: (container.height - world.height * scale) / 2,
    };
  }
  // ---cut---
  function toWorldSpace(
    point: { x: number; y: number },
    gameArea: { width: number; height: number },
    canvasRect: { width: number; height: number },
  ) {
    const { scale, xOffset, yOffset } = letterbox(canvasRect, gameArea);
    const inverse = new DOMMatrix()
      .translate(xOffset, yOffset) // [!code highlight]
      .scale(scale, scale) // [!code highlight]
      .inverse(); // [!code highlight]
    const worldPoint = new DOMPoint(point.x, point.y).matrixTransform(inverse);
    return worldPoint;
  }
  ```
</TransformPair>

Likewise, to get an in-camera point into world space, we can invert the <Camera>camera</Camera>.

<TransformPair>
  ```ts twoslash
  function inCamera(
    ctx: CanvasRenderingContext2D,
    bounds: { width: number; height: number },
    camera: { x: number; y: number; zoom: number },
    draw: () => void,
  ) {
    ctx.save();
    ctx.translate(bounds.width / 2, bounds.height / 2); // [!code highlight]
    ctx.scale(camera.zoom, camera.zoom); // [!code highlight]
    ctx.translate(-camera.x, -camera.y); // [!code highlight]
    draw();
    ctx.restore();
  }
  ```

  ```ts twoslash
  function fromCamera(
    point: { x: number; y: number },
    gameArea: { width: number; height: number },
    camera: { x: number; y: number; zoom: number },
  ) {
    const inverse = new DOMMatrix()
      .translate(gameArea.width / 2, gameArea.height / 2) // [!code highlight]
      .scale(camera.zoom, camera.zoom) // [!code highlight]
      .translate(-camera.x, -camera.y) // [!code highlight]
      .inverse(); // [!code highlight]
    const worldPoint = new DOMPoint(point.x, point.y).matrixTransform(inverse);
    return worldPoint;
  }
  ```
</TransformPair>

Putting all three transformations together gives us the game-world position under the pointer:

```ts
const canvasRect = canvas.getBoundingClientRect();
// pointer position within the canvas
const screenPoint = toScreenSpace(event, canvasRect);
// game units, before accounting for camera pan or zoom
const cameraPoint = toWorldSpace(screenPoint, gameArea, canvasRect);
// actual game-world position under the pointer
const worldPoint = fromCamera(cameraPoint, gameArea, camera);
```

## Up next [#up-next]

Mouse and keyboard aren't the only way for players to control a web game. Modern browsers support gamepad inputs (with haptic feedback!) so you can use triggers, analog sticks, and buttons on handheld controllers for your game inputs instead. Unlike the event-based approach from this chapter, controller values are polled each game loop. We'll do a deep dive on this in [Chapter 8 →](/guides/handmade-web-games/controllers-and-haptics).

## See also [#see-also]

* [Don't Sleep on AbortController](https://kettanaito.com/blog/dont-sleep-on-abort-controller) — by Artem Zakharchenko
* [Immediate-Mode Graphical User Interfaces (2005)](https://www.youtube.com/watch?v=Z1qyvQsjK5Y) <small className="opacity-75">\[YouTube]</small> — by Casey Muratori
