Chasing a dot on a HTML5 canvas

A forum question about how to detect a collision of two balls for a school project (in Python) lead me to write my first HTML5 <canvas> element code.

Move the mouse over the square below and try to catch the blue dot with the red one.

Sorry, this needs a browser with <canvas> support.

HTML

The HTML for this is quite simple. Just the <canvas> element with an id to get the element in JavaScript, the width and height as attributes, and some content that shows in browsers without HTML5/<canvas> support.

<canvas id="dot-chase" width="400" height="400">
  Sorry, this needs a browser with &lt;canvas&gt; support.
</canvas>

JavaScript

The programmatic part starts with the definition of the radius of the balls and the function randomInt() to get random integer numbers from the given interval \([a,b)\).

'use strict';

var BALL_RADIUS = 10;

var randomInt = function (a, b) {
  return Math.floor(Math.random() * (b - a) + a);
};

A Ball

Ball instances have a center coordinate (x,y), a radius, and a color that can be any CSS color value, like names ('green') or an rgb()/rgba() notation ('rgb(0, 1, 0)').

var Ball = function(x, y, radius, color) {
  this.x = x;
  this.y = y;
  this.radius = radius;
  this.color = color;
};

Drawing a Ball

That’s quite long and looks a bit complicated, mostly because there is no simple call to a circle() function and properties like color and shadow offset, blur, and color have to be set on the context object.

Ball.prototype.draw = function(context) {
  context.save();
  try {
    context.fillStyle = this.color;
    context.shadowOffsetX = 3;
    context.shadowOffsetY = 3;
    context.shadowBlur = 2;
    context.shadowColor = 'rgba(0, 0, 0, 0.5)';
    context.beginPath();
    context.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
    context.fill();
  } finally {
    context.restore();
  }
};

Distance between two Balls

Taking the good old Pythagorean theorem…

\[a^2 + b^2 = c^2\]

…we derive a function to calculate the distance (d) between the centers of two balls with the cartesian coordinates (x₁,y₁) and (x₂,y₂):

\[d = \sqrt{(x_1 - x_2)^2 + (y_1 - y_2)^2}\]

Looks a bit less elegant with all the Math function calls compared to the mathematical formular layout:

Ball.prototype.distanceTo = function(other) {
  return Math.sqrt(
    Math.pow(this.x - other.x, 2) + Math.pow(this.y - other.y, 2)
  );
};

Do they collide?

The balls collide iff the distance d calculated by the function in the previous section is smaller then the sum of the radii of both balls:

\[d < r_1 + r_2\]

Or in code:

Ball.prototype.collidesWith = function(other) {
  return this.distanceTo(other) < this.radius + other.radius;
};

The Field

Given an HTML5 <canvas> and the radius of the balls, Field instances contain everything needed to handle user input and keep the ”game” going. That’s the canvas to draw on, the width and heigt of the playing field, the context used to draw on it, and the two balls.

The player’s ball starts off canvas and will be placed where the mouse pointer is by the handler for mouse moves: Field.onMouseMove().

var Field = function (canvas, ballRadius) {
  this.canvas = canvas;
  this.ballRadius = ballRadius;
  this.width = this.canvas.getAttribute('width');
  this.height = this.canvas.getAttribute('height');
  this.context = this.canvas.getContext('2d');
  this.randomBall = null;
  this.randomlyPlaceBall();
  // Place outside visible area.
  this.playerBall = new Ball(
    -this.ballRadius, -this.ballRadius, this.ballRadius, 'red'
  );
  this.canvas.addEventListener('mousemove', this.onMouseMove.bind(this));
};

Randomly place the other ball

Place the other ball on a random position within the field by placing the center at least the radius of the ball away from the border of the canvas.

Field.prototype.randomlyPlaceBall = function() {
  this.randomBall = new Ball(
    randomInt(this.ballRadius, this.width - this.ballRadius),
    randomInt(this.ballRadius, this.height - this.ballRadius),
    this.ballRadius,
    'blue'
  );
};

Clear the field

Field.prototype.clear = function() {
  this.context.clearRect(0, 0, this.width, this.height);
};

Update the field

Clears the field and then redraws both balls at their current position.

Field.prototype.update = function() {
  this.clear();
  this.randomBall.draw(this.context);
  this.playerBall.draw(this.context);
};

On each mouse move

Moving the mouse over the canvas sets the player’s ball center to the mouse pointer position relative to the upper left corner of the canvas.

If the player’s ball collides with the other ball, the other ball gets placed at a new random position.

At the end of the handler function the display is updated.

Field.prototype.onMouseMove = function(event) {
  var canvasRect = this.canvas.getBoundingClientRect();
  this.playerBall.x = event.clientX - canvasRect.left;
  this.playerBall.y = event.clientY - canvasRect.top;
  if (this.playerBall.collidesWith(this.randomBall)) {
    this.randomlyPlaceBall();
  }
  this.update();
};

Connecting HTML and JavaScript

Get the canvas from the document after the page is loaded, instanciate a Field object and call its update() method to draw the initial state of the game.

window.onload = function () {
  var field = new Field(document.getElementById('dot-chase'), BALL_RADIUS);
  field.update();
};