Basic Animation Using HTML Canvas and Javascript

Rachael Ghorbani
The Startup
Published in
8 min readSep 26, 2020

Javascript can do a lot of cool things. The introduction of the canvas element in HTML5 expanded what we can do with Javascript even further. So what is a canvas element? It’s an element that acts as a container for javascript drawn graphics. Although we won’t demonstrate this here, you can take your animations a step further by interacting with them using event listeners and handlers. We’ll instead make a simple animation with some different size circles bouncing around the screen.

First let’s create and set up our canvas element in our index.html file:

index.html
....

<body>
<canvas></canvas>
<script src="canvas.js"></script>
</body>
....

The <html> tag of a document doesn’t quite take up the entire screen. Since we’re planning on making our animation full page, and we have access to the window element which does fill the entire screen, we’ll use javascript to access it’s innerHeight and innerWidth properties and set our canvas height and width to those values. This will ensure our canvas takes up the entire height and width of the browser window. Because the default margin value of the <body> tag isn’t 0, we’ll need to update that as well:

canvas.jsconst canvas = document.querySelector('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const body = document.querySelector('body');
body.style.margin = 0;

Next we’ll create a c variable in our javascript file that will call the .getContext() method on our canvas and pass in 2d as an argument. This returns a drawing context on the canvas and essentially turns c into a super object that has access to all of the drawing properties and functions we need to draw 2d elements:

canvas.jsconst c = canvas.getContext('2d')

A few key methods that we’ll be utilizing on c are .beginPath(), .strokeStyle(), and .stroke(). .beginPath() does just what it says, it begins a new path from which we can draw. The .strokeStyle() method is used to alter the color, gradient, or pattern of our objects, and the .stroke() method is what actually draws our object.

Let’s go ahead and create a circle. We can do this by first calling c.beginPath() to indicate that we wish to begin a new path, and then calling c.arc() . This method accepts arguments for the x coordinate for the center of the circle, the y coordinate for the center of the circle, the radius, the starting angle in radians, and the end angle in radians. It also takes an optional argument for counterclockwise. For the time being we’ll hard code values for x, y, and radius. Lastly we can give our circle a color and finally get it on the page by calling c.strokesStyle() and c.stroke():

canvas.jsc.beginPath();
c.arc(200, 200, 30, 0, Math.PI * 2, false );
c.strokeStyle = "pink";
c.stroke();

Now that we have our circle, let’s get it to actually do something. We’ll need a function, let’s call it animate, that calls requestAnimationFrame(). requestAnimationFrame() takes a function as an argument and was introduced as an alternative to the traditional way of creating animations which included recursively calling .setTimeout(). We’re going to pass in our newly created animate function as the argument for requestAnimateFrame(), which will essentially create an infinite loop. This is what we want since we’ll be altering the x and y coordinates of our circle slightly with each page repaint. This will give the illusion of our circle moving across the page:

canvas.jsconst animate = () => {
requestAnimationFrame(animate)
}
animate()

Let’s now place the code we previously wrote to create a circle within animate. Right now this code is just repainting the circle in place since we haven’t actually updated the x and y coordinates:

canvas.jsconst animate = () => {
requestAnimationFrame(animate);
c.beginPath();
c.arc(200, 200, 30, 0, Math.PI * 2, false );
c.strokeStyle = "pink";
c.stroke();
}
animate()

We want to change the x and y values of our circle on each refresh as this is what will give us the illusion of movement. First we’ll create variables for our circles x and y coordinates outside of our function. We’ll then increment them on every refresh by setting x and y to x += 1 and y += 1. We’ll also need to clear the canvas every time the page refreshes so that the images aren’t just drawn on top of each other. We do this by calling c.clearRect() which takes an x-axis coordinate of the canvas’s starting point, a y-axis coordinate of the canvas’s starting point, width of the canvas and its height as arguments. We want to clear the entire canvas, so we’ll set those values to 0, 0, innerWidth, innerHeight:

canvas.jslet x = 200;
let y = 200;
const animate = () => {requestAnimationFrame(animate);c.clearRect(0, 0, innerWidth, innerHeight)
c.beginPath();
c.arc(x, y, 30, 0, Math.PI * 2, false );
c.strokeStyle = "pink";
c.stroke();
x += 1
y += 1
}
animate()

Now the circle moves! But off the screen. We’re going to fix that, but first let’s think about what x += 1 and y += 1 are doing. They’re incrementing the x and y position of our circle by one pixel on every repaint. We can think of this as our velocity, so as it’s currently set up, our velocity is 1. Let’s make this a bit more dynamic and set variables dx and dy outside of our function that will represent the velocity of x and y. Now inside of our function we can increment x by dx and y by dy instead. We need some conditionals now to keep our circles x and y coordinates from exceeding the height and width of our canvas element. We can do this by changing the sign of our velocity values right before we exceed the innerHeight or innerWidth values of our canvas:

canvas.jslet x = 200;
let dx = 5;
let y = 200;
let dy = 5;
let radius = 30
const animate = () => {
requestAnimationFrame(animate);
c.clearRect(0, 0, innerWidth, innerHeight);
c.beginPath();
c.arc(x, y, radius, 0, Math.PI * 2, false );
c.strokeStyle = "pink";
c.stroke();
if(x + radius > innerWidth || x - radius < 0){
dx = -dx
}
if(y + radius > innerHeight || y - radius < 0){
dy = -dy
}
x += dx;
y += dy;
}
animate()

Next we want to randomize the velocity of our circle and position so that it doesn’t have the same path each time. We can start by randomizing x and y, which will give the circle a different starting point whenever we reload the page. We want to also randomize velocities so they can be positive or negative to change up the direction in which the circle appears to be moving upon a refresh:

canvas.jslet x = Math.random() * innerWidth;
let dx = (Math.random() - 1) * 10;
let y = Math.random() * innerHeight;
let dy = (Math.random() - 1) * 10;
let radius = 30;
const animate = () => {
requestAnimationFrame(animate);
c.clearRect(0, 0, innerWidth, innerHeight);
c.beginPath();
c.arc(x, y, radius, 0, Math.PI * 2, false );
c.strokeStyle = "pink";
c.stroke();
if(x + radius > innerWidth || x - radius < 0){
dx = -dx
}
if(y + radius > innerHeight || y - radius < 0){
dy = -dy
}
x += dx;
y += dy;
}
animate()

This is great, we have a circle and it moves. But wouldn’t it be so much better if we had a hundred circles that moved? To do this we’ll need to create a bunch of circle objects. The fastest and most efficient way to do this will be to create a circle class from which we can instantiate new instances of circle objects. This way they will all have access to the same methods through prototypal inheritance. Each will need its own x, y, dx, dy, and radius values, so we’ll pass those in to the constructor function as arguments. We can then move the code we initially used to draw our circle into a function we’ll create in our class called draw(). We’ll need a second function update() that will update the velocity, so we can create that within our class as well. We can again take the previously written code updating our circles velocity and place it in our new update function. We’ll have to call draw() inside update() to actually get the circle on the screen and then call update() in animate()

canvas.jsclass Circle {
constructor(x, y, dx, dy, radius){
this.x = x;
this.y = y;
this.dx = dx;
this.dy = dy;
this.radius = radius
}
draw() {
c.beginPath();
c.arc(this.x, this.y, this.radius, 0, Math.PI*2, false);
c.strokeStyle = "pink";
c.stroke();
}
update() {
if(this.x + this.radius > innerWidth || this.x - this.radius < 0){
this.dx = -this.dx
}
if(this.y + this.radius > innerHeight || this.y - this.radius < 0){
this.dy = -this.dy
}
this.x += this.dx;
this.y += this.dy;
this.draw();
}
}
let x = Math.random() * innerWidth;
let dx = (Math.random() - 0.5) * 10;
let y = Math.random() * innerHeight;
let dy = (Math.random() - 0.5) * 10;
let radius = Math.random() * 50;
const circle = new Circle(x, y, dx, dy, radius)const animate = () => {
requestAnimationFrame(animate);
c.clearRect(0, 0, innerWidth, innerHeight)
circle.update()
}
animate()

So far we still only have one circle. We can solve that by using a simple for loop to create as many circle objects as we want, in this case 100, and push those onto an array. We’ll move our variable declarations for x, y, dx, dy, and radius in this loop as well:

canvas.js....
const circleArray = []
for(let i = 0; i < 100; i++){let radius = Math.random() * 50
let x = Math.random() * (innerWidth - radius * 2) + radius;
let dx = (Math.random() - 1.5) * 5
let y = Math.random() * (innerHeight - radius * 2) + radius;
let dy = (Math.random() - 1.5) * 5
circleArray.push(new Circle(x, y, dx, dy, radius))
}
....

Now we have all of our circle instances, but how do we get them to show? Well we have them all in an array, so let’s create another for loop and put it inside of animate() where we call update on all of the circles we just created. If you recall, we have draw(), and update(). draw() creates a new circle with the values we pass in. update() then adjusts the x and y position of the circle using the velocity values we passed in. It then calls draw() to actually get the circle on the page. By putting the for loop that iterates through all of our circles in our animate page, we are both drawing and updating the position of every circle we created every time the page repaints:

cavans.jsclass Circle {
constructor(x, y, dx, dy, radius){
this.x = x;
this.y = y;
this.dx = dx;
this.dy = dy;
this.radius = radius;
this.color = Math.floor(Math.random()*16777215).
toString(16);
}draw() {
c.beginPath();
c.arc(this.x, this.y, this.radius, 0, Math.PI * 2, false );
c.fillStyle = `#${this.color}`;
c.fill();
c.strokeStyle = `#${this.color}`;
c.stroke();
}
update() {
if(this.x + this.radius > innerWidth || this.x - this.radius < 0){
this.dx = -this.dx
}
if(this.y + this.radius > innerHeight || this.y - this.radius < 0){
this.dy = -this.dy
}
this.x += this.dx;
this.y += this.dy;
this.draw()
}
}const circleArray = [];for(let i = 0; i < 100; i++){
let radius = Math.random() * 50
let x = Math.random() * (innerWidth - radius * 2) + radius;
let dx = (Math.random() - 1.5) * 5
let y = Math.random() * (innerHeight - radius * 2) + radius;
let dy = (Math.random() - 1.5) * 5
circleArray.push(new Circle(x, y, dx, dy, radius))
}
const animate = () => {
requestAnimationFrame(animate);
c.clearRect(0, 0, innerWidth, innerHeight)
for(let i = 0; i < circleArray.length; i++){
circleArray[i].update()
}
}
animate()

Finally all our circles all move in different directions and at different speeds! This was just the tip of the iceberg in terms of what you can achieve with the HTML canvas element and javascript.

--

--