Handling mouse input events for a html canvas game

- 5 mins
Hello1
ViewSource code

Intro

In this tutorial we are going to cover handling mouse input events which will include the following concepts and a practical example to tie them all together.

  • Mouse event listeners
  • Mouse event properties
  • Screen vs Offset vs Page vs Client positions
  • Canvas position translation
  • Canvas mouse event example

Mouse event listeners

Below are the most common mouse event listeners.

  • mouseenter: This event is triggered when the mouse cursor position is moved inside the target element.
  • mouseleave: This event is triggered when the mouse cursor position is moved outside the target element.
  • mousemove: This event is triggered when the mouse cursor position is changed when inside the target element.
  • mousedown: This event is triggered when any mouse button is pressed.
  • mouseup: This event is triggered when any mouse button is released.

Mouse event properties

When a mouseevent occurs properties are attached via a MouseEvent type that help describe the nature of the event. We will go over most of the common events that will be used for html canvas game design.

The mouse position properties below can seem similar as we'll observe in the example but they serve distinct use cases which are described below.

screenX and screenY property

The screenX and screenY properties return the mouse cursors position relative to the devices screen.

clientX and clientY

The clientX and clientY properties return the mouse cursors position relative to the application's viewport, this does not include any scrolling overflow.

x and y properties are alias for the clientX and clientY property.

pageX and pageY

The pageX and pageY properties return the mouse cursors position relative to the current html document. This includes the distance the page has been scrolled horizontally or vertically.

offsetX and offsetY

The offsetX and offsetY properties return the mouse cursors position relative to the target element the event was added to.

movementX and movementY

The movementX and movementY properties return the relative position of the mouse cursor between the current and previous mousemove events firing.

button

The button property returns a number which relates to a single button.

mdn docs button

  • 0: Main button pressed, usually the left button or the un-initialised state
  • 1: Auxiliary button pressed, usually the wheel button or the middle button (if present)
  • 2: Secondary button pressed, usually the right button

buttons

The buttons property returns a number which helps determine when multiple buttons have been pressed or released during a particular mouse event.

mdn docs buttons

Each button is assigned a weighted value that doubles in magnitude as below.

  • 0: No button
  • 1: Primary button (usually the left button)
  • 2: Secondary button (usually the right button)
  • 4: Auxiliary button (usually the mouse wheel button or middle button)

The buttons value equals the sum of all of the weighted button values that are contributing to the current mouse event.

The buttons assigned weighted values double so that we can determine given a mouse event which particular buttons contributed to it. For example if buttons is equal to 5 we know that the only combination that adds to 5 is when the primary and auxiliary button are contributing to the event.

Mouse screen diagram

You can use this handy diagram as a guide to tell the different mouse coordinates apart.

Image describing html canvas mouse event offset, page etc.

Mouse event canvas example

This tutorial will use the typescript-html5-game-loop-starter project as a base. If you need help setting that up or a refresher on the different files head over to typescript game loop project setup otherwise if you're familar with the code and zip file can be found at typescript-html5-game-loop-starter, unzip, change to the project's root directory and open the code in your favourite IDE.

In this example we will go over all of the above mentioned mouse event related properties in a detailed example.

Set gameState

Update GameStateType.ts in the types directory as below.

type GameState = {
  mouseInside: boolean,
  mouseDown: boolean,
  button: number,
  buttons: number,
  movementX: number,
  movementY: number,
  position: {
    screenX: number,
    screenY: number,
    clientX: number,
    clientY: number,
    pageX: number,
    pageY: number,
    offsetX: number,
    offsetY: number,
    canvasX: number,
    canvasY: number
  } 
}

The GameState type will capture all of the properties mentioned above (except canvasX and canvasY which are discussed below) so that we can interact with and display them on the canvas.

The position related properties have been grouped into their own field.

The mouseInside property is a boolean that will represent whether the mouse cursor is inside the canvas.

The mouseDown property is a boolean that will represent whether a mouse button is currently pressed.

Next inside GameRuntime.ts we will update the gameState static constant in the GameRuntime class as below.

static gameState: GameState = {
  buttons: 0,
  button: 0,
  mouseInside: false,
  mouseDown: false,
  movementX: 0,
  movementY: 0,
  position: {
    canvasX: 0,
    canvasY: 0,
    clientX: 0,
    clientY: 0,
    screenX: 0,
    screenY: 0,
    pageX: 0,
    pageY: 0,
    offsetX: 0,
    offsetY: 0
  }
}

The above code sets an initial GameState with some default properties. Add the below code to the draw method to draw onto the screen the respective hardcoded mouse properties.

context.fillText(JSON.stringify({screenX: gameState.position.screenX, screenY: gameState.position.screenY} , null, 0), 0, 40)
context.fillText(JSON.stringify({offsetX: gameState.position.offsetX, offsetY: gameState.position.offsetY} , null, 0), 0, 60)
context.fillText(JSON.stringify({clientX: gameState.position.clientX, clientY: gameState.position.clientY} , null, 0), 0, 80)
context.fillText(JSON.stringify({pageX: gameState.position.pageX, pageY: gameState.position.pageY} , null, 0), 0, 100)
context.fillText(JSON.stringify({canvasX: gameState.position.canvasX, canvasY: gameState.position.canvasY} , null, 0), 0, 120)
context.fillText(JSON.stringify({mouseDown: gameState.mouseDown, mouseInside: gameState.mouseInside} , null, 0), 0, 140)
context.fillText(JSON.stringify({movementX: gameState.movementX, movementY: gameState.movementY} , null, 0), 0, 160)
context.fillText(JSON.stringify({button: gameState.button} , null, 0), 0, 180)
context.fillText(JSON.stringify({buttons: gameState.buttons} , null, 0), 0, 200)

Run the app and you should see the default hard-coded properties on the gameState displayed on the canvas.

Triggering Mouse Events

Add the below code to the GameRuntime class.

static setPosition = (event: MouseEvent) => {

  GameRuntime.gameState.position.clientX = event.clientX
  GameRuntime.gameState.position.clientY = event.clientY
  GameRuntime.gameState.position.pageX = event.pageX
  GameRuntime.gameState.position.pageY = event.pageY
  GameRuntime.gameState.position.offsetX = event.offsetX
  GameRuntime.gameState.position.offsetY = event.offsetY
  GameRuntime.gameState.position.screenX = event.screenX
  GameRuntime.gameState.position.screenY = event.screenY
  GameRuntime.gameState.movementX = event.movementX
  GameRuntime.gameState.movementY = event.movementY
}

static setButton = (event: MouseEvent) => {
  GameRuntime.gameState.button = event.button
  GameRuntime.gameState.buttons = event.buttons
}

static mouseEnter = (event: Event) => {
  GameRuntime.gameState.mouseInside = true
  GameRuntime.setPosition(event as MouseEvent)
}

static mouseLeave = (event: Event) => {
  GameRuntime.gameState.mouseInside = false
  GameRuntime.setPosition(event as MouseEvent)
}

static mouseMove = (event: Event) => {
  GameRuntime.setPosition(event as MouseEvent)
}

static mouseUp = (event: Event) => {
  GameRuntime.gameState.mouseDown = false
  GameRuntime.setButton(event as MouseEvent)
}

static mouseDown = (event: Event) => {
  GameRuntime.gameState.mouseDown = true
  GameRuntime.setButton(event as MouseEvent)
}

The code above defines methods we will use to handle the various mouse events.

The setPosition method takes a MouseEvent and sets all of the position based properties on the gameState relating to that event.

The setButton method groups updating button related properties for a MouseEvent.

The other methods update relevant properties in the gameState directly or call setPosition and setButton to do so.

Next we will connect our methods above as handlers to the relevant mouse events.

Add the following mouse event listeners to GameRuntime's initialise method as below.

canvas.addEventListener('mouseenter', GameRuntime.mouseEnter)
canvas.addEventListener('mouseleave', GameRuntime.mouseLeave)
canvas.addEventListener('mousemove', GameRuntime.mouseMove)
canvas.addEventListener('mousedown', GameRuntime.mouseDown)
canvas.addEventListener('mouseup', GameRuntime.mouseUp)

This will add the relevant event listeners required to update the gameState.

Refresh the app in the browser and move your mouse cursor onto the canvas. You should now see the relevant mouse event properties update.

Test your understanding of the various mouse event properties by validating that the canvas outputs behave as you would expect.

  • mouseInside should reflect whether the cursor is inside the canvas or not.
  • Try moving the cursor fast and to the left then stop. Does the movementX and movementY vector represent the change in position?
  • Generally the client and page properties will appear the same. Shrink the window so that there is a vertical or horizontal scroll overflow and move the page down or accross to confirm they are different.
  • Tap a mouse button and check that the mouseDown, button and buttons properties update as expected.
  • Move the cursor to the left edge of the canvas and check the offset property.

Tracking the cursor on the canvas

We are going to add some interaction using the offset property to make a square follow our cursor around.

You may have noticed the offset property can be negative. First lets add the clamp method below to fix our offset to a given range. The clamp method takes a value as well as a min and max and will return the value if it's between the min and max and otherwise it will return the min or max value depending which threshold it has crossed.

static clamp = (num: number, min: number, max: number) => Math.min(Math.max(num, min), max);

Overwrite the offsetX and offsetY setter lines in the setPosition method to use the clamp method as below.

    GameRuntime.gameState.position.offsetX = GameRuntime.clamp(event.offsetX, 0, GameRuntime.canvas.getBoundingClientRect().width)
    GameRuntime.gameState.position.offsetY = GameRuntime.clamp(event.offsetY, 0, GameRuntime.canvas.getBoundingClientRect().height)

Add the following lines to the draw method.

context.fillStyle = gameState.mouseDown ? 'red' : 'black'
context.fillRect(gameState.position.offsetX, gameState.position.offsetY, 50, 50)

Here we are setting the fillStyle colour to red when a mouse button is being pressed and black when no buttons are pressed.

We are also drawing a rectangle to the screen based on the offset gameState position, remember this is the mouse cursor position relative to the target element, in this case the canvas.

Refresh the app in the browser and move the cursor around inside, is a black square chasing the cursor? Press a button, does the square change colour?

You may notice that when you move the cursor to the right edge of the canvas the square doesn't follow all the way, why is that?

The offset property hints at the problem here. When the cursor is at the right edge of the canvas the offset is equal to the size of the html element not the canvas width! If these values are different the offset won't be a perfect measurement for where to draw the square on our canvas.

In our case the canvas width is 1024 and the canvas html element varies depending on the browser window size. We will look at a way to calculate an accurate cursor position with relation to the canvas next.

Canvas size vs html element size

Add the following code to the draw method.

context.fillText(JSON.stringify({width: canvas.width, height: canvas.height} , null, 0), 0, 220)
context.fillText(JSON.stringify({clientWidth: canvas.getBoundingClientRect().width, clientHeight: canvas.getBoundingClientRect().height} , null, 0), 0, 240)

The getBoundingClientRect() method on the canvas returns an object with various values relating to the canvas, in particular we care about the width and height property as this represents the canvas html element width and height.

Run the app and verify that the width and clientWidth values are different.

Add the following method to GameRuntime class.

static getMouseCanvas = () => {
  var rec = GameRuntime.canvas.getBoundingClientRect();
  const x = ((GameRuntime.gameState.position.offsetX) * GameRuntime.canvas.width) / (rec.width) 
  const y = ((GameRuntime.gameState.position.offsetY) * GameRuntime.canvas.height) / (rec.height)
  return { x: x, y: y }; 
}

The above method determines the canvasX and canvasY coordinates of the mouse cursor.

We can use the offset and the getBoundingClientRect property to determine the ratio of where our cursor is in the x and y direction.

We take a ratio of the offsetX / GameRuntime.canvas.getBoundingClientRect().width to find a percentage of where our cursor is within the canvas element. Then we can take this percentage and multiple it by the canvas.width to determine the canvasX. A similar method is used to determine canvasY.

Add the following code to the end of our setPosition method.

const canvasPosition = GameRuntime.getMouseCanvas()

GameRuntime.gameState.position.canvasX = canvasPosition.x
GameRuntime.gameState.position.canvasY = canvasPosition.y

This will use the calculated getMouseCanvas() position and set the gameState canvasX and canvasY properties to it.

Run the app and verify that the square follows your cursor to the very right edge of the canvas.

Note: Adding padding or a border style to the canvas via html style attributes will change the dimensions of the getBoundingClientRect() method so it's best to use a div container around the html canvas to style it.

Summary

Being able to identify mouse events and accurately represent their position on a canvas is important for any game that uses a mouse as an input.