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.
We can just use our project from Part 1
as the starting point for most of our animations.
<!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>
// 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();
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.
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();
};
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));
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);
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.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
While we believe that this content benefits our community, we have not yet thoroughly reviewed it. If you have any suggestions for improvements, please let us know by clicking the “report an issue“ button at the bottom of the tutorial.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Sign up for Infrastructure as a Newsletter.
Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.