Animations with the Canvas API - Part 2: Basic Collisions

Joshua Hall

In Part 1 of this series we went over the basics of rendering reusable objects to the canvas, using our GUI for more intuitive controls, and creating the illusion of basic movement with our animation loop. In this part we’ll get comfortable with creating collision effects with a simple ball that changes colors as it hits the borders of our canvas.

Boilerplate

We can just use our project from Part 1 as the starting point for most of our animations.

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <meta http-equiv="X-UA-Compatible" content="ie=edge"/>
    <title>HTML Canvas</title>
  </head>
  <body>
      
    <canvas></canvas>

  </body>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.6/dat.gui.min.js"></script>
  <script src="./canvas.js"></script>
</html>

canvas.js

// Get canvas element
const canvas = document.querySelector('canvas');
const c = canvas.getContext('2d');

// Make canvas fullscreen
canvas.width = innerWidth;
canvas.height = innerHeight;
addEventListener('resize', () => {
  canvas.width = innerWidth;
  canvas.height = innerHeight;
});

// Control Panel
const gui = new dat.GUI();

const controls = {
  dx: 0,
  dy: 0,
};

gui.add(controls, 'dx', 0, 10);
gui.add(controls, 'dy', 0, 10);

// New Object
class Ball {
  constructor(x, y, radius, color) {
    this.x = x;
    this.y = y;
    this.radius = radius;
    this.color = color;
  }
}

Ball.prototype.draw = function () {
  c.beginPath();
  c.fillStyle = this.color;
  c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false);
  c.fill();
  c.closePath();
};

Ball.prototype.update = function () {
  this.x += controls.dx;
  this.y += -controls.dy;
  this.draw();
};

const ball = new Ball(innerWidth / 2, innerHeight / 2, 50, 'red');

// Render new instances
const init = () => ball.draw();

// Handle changes
const animate = () => {
  requestAnimationFrame(animate);

  c.clearRect(0, 0, canvas.width, canvas.height);

  ball.update();
};

init();
animate();

Bouncer

You can preview the our end result here.

To change our behavior on a collision, we just need to add a condition to our update method that will change the ball’s behavior whenever it hits a border, in this case, reversing its direction. Keep in mind that the browser is looking at the center of our object for its position, so we always want to include the radius into the calculation.

canvas.js

Ball.prototype.update = function() {
  if (this.y + this.radius > canvas.height || this.y - this.radius < 0) {
    controls.dy = -controls.dy;
  }
  this.y -= controls.dy;

  if (this.x + this.radius > canvas.width || this.x - this.radius < 0) {
    controls.dx = -controls.dx;
  }
  this.x += controls.dx;

  this.draw();
};

Colors

Now let’s add some more interesting behavior when we hit something, like changing the ball’s color. First we need an array of colors to choose from and a function to randomly select one for us. Whenever we hit a border we can re-assign our color value to a new random one.

I recommend checking out Kuler to make your own colors palettes.

// Returns a color between 0 and the length of our color array
const randomColor = colors => colors[Math.floor(Math.random() * colors.length)];

const colors = [
  '#e53935',
  '#d81b60',
  '#8e24aa',
  '#5e35b1',
  '#3949ab',
  '#1e88e5',
  '#039be5',
  '#00acc1',
  '#00897b',
  '#43a047',
  '#ffeb3b',
  '#ef6c00'
];

// Re-assign color on contact
Ball.prototype.update = function () {
  if (this.y + this.radius > canvas.height || this.y - this.radius < 0) {
    this.color = randomColor(colors);
    controls.dy = -controls.dy;
  };
  this.y -= controls.dy;

  if (this.x + this.radius > canvas.width || this.x - this.radius < 0) {
    this.color = randomColor(colors);
    controls.dx = -controls.dx;
  };
  this.x += controls.dx;

  this.draw();
};

// Make it start with a random color
const newBall = new Ball(innerWidth / 2, innerHeight / 2, 50, randomColor(colors));

Tail

Now that we have the basic functionality in place we can make it a bit more visually interesting by adding a colored tail that trails behind it. We can do this by removing clearRect and filling our whole canvas with a dark RGBA value. This creates a ‘residue’ effect from anything that moves through it and we can control the residue’s intensity with the background’s opacity.

const animate = () => {
  requestAnimationFrame(animate);

  c.fillStyle = `rgba(33, 33, 33, ${-controls.tail / 10})`; // Lower opacity creates a longer tail
  c.fillRect(0, 0, canvas.width, canvas.height);

  newBall.update();
};

// We also need to update our controls with some default values
const controls = {
  dx: 5,
  dy: 5,
  tail: -5
};

gui.add(controls, 'dx', 0, 10);
gui.add(controls, 'dy', 0, 10);
gui.add(controls, 'tail', -10, 0);

Conclusion

Just like that, we now have a very basic collision system with some special effects. In the upcoming Part 3 of this series we’ll be using the concepts covered here to create this dynamic rain animation.

If you had any problems following along, a working example is available on Codepen. Feel free to fork it and share what you made.

  Tweet It

🕵 Search Results

🔎 Searching...

Sponsored by #native_company# — Learn More
#native_title# #native_desc#
#native_cta#