Building a Browser Game Part 2: I Like the Way You Move with requestAnimationFrame()

Daniel Irwin
May 7 · 8 min read

So, last time we figured out how to add some simple shapes to an HTML canvas element. But perhaps afterward we were feeling a bit underwhelmed; a salmon-colored rectangle on a white screen does not a AAA video game make. In this article, we’re going to look into how to breathe some life into that little box by adding some movement with requestAnimationFrame().

The Set-Up

Before we dive into what requestAnimationFrame() does and how we can harness its powers to render animations, we have some work to do. If you are following along with the tutorial, we will work with the directory we created last time*. First, we will remove all the lines of code in our script.js file that rendered the box and the circle. Right now all we should have is the following:

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");

*If you’re not following along, you’ll just need to add a <canvas> element in the body of your HTML and give it an id of “canvas.”

As a quick review, ctx stands for context and is how we render everything onto the HTML canvas using its built-in methods.

Let's get some variables out of the way.

const radius = 50; //The radius of the circle we'll be drawing
const xPosition = 100; //The x position of our circle
const yPosition = 85; //The y position of our cirlce

Next, let's build a function to draw our circle. I’ll be using ES6 arrow functions but this will work the same way using classic function declaration. Our goal is to draw a circle on the canvas. This function will draw the circle centered around our (x,y) position.

const drawCircle = () => {
ctx.fillStyle = "salmon";
ctx.arc(xPosition, yPosition, radius, 0, 2 * Math.PI);
drawCircle(); //We'll remove this line after testing
//and put it somewhere else later
A Salmon-colored circle on an HTML canvas element
A Salmon-colored circle on an HTML canvas element

This function will draw our lovely salmon circle wherever we want given x and y coordinates. Go ahead and try moving around and resizing by playing with the numbers in our position and radius variables.

The Animation Loop

Every computer renders all of the images on the screen by constantly updating the data and rendering, updating and rendering, over and over again. The metric that we use to talk about how often that cycle repeats is called the Frames Per Second or FPS. Typical cartoons usually render at 12 FPS, film and tv at 24 FPS and video games often aim for 60+FPS so they can render as much detail as possible. For our purpose it doesn’t really matter how many FPS we have, we’ll just want to have a function that gets called each frame to render on our canvas and update our position.

My first instinct when trying to solve the animation loop problem was to use the setTimeout() function. It's a good function. It takes in a callback function and a delay in milliseconds and will call the function after the specified delay. But what happens if our draw function takes longer than the delay? It would get called twice and our canvas would be trying to draw our little circle in two positions at once. Instead, we’ll build a recursive function using requestAnimationFrame()

MDN puts it best (as they usually do):

The window.requestAnimationFrame() method tells the browser that you wish to perform an animation and requests that the browser calls a specified function to update an animation before the next repaint.

This means that we can sidestep the issue of trying to render different frames at the same time. Let's get to the code. Just to see that it is working, we’ll throw in an incrementer and console log it.

let i = 0; //Just for testing, get rid of it when we move onconst loop = () => {    console.log(i++) //Just for testing    // Here we will update and draw 
// whatever we need to during each frame

And that's really the most basic version of it. In your console, you should be seeing an increasing number each time the function is called. You can go pretty far with adding additional controls, framerate options, and ways of dealing with lag, but for now, we now have a working animation loop!

Moving Around

We’re so close to having that beautiful ball bouncing all over the place. All that really needs to be done is to build our updatePosition() function. This function will run each frame and will change our position variable to reflect movement. We’ll start simple to get the idea and then get a little fancier as we go. In order to figure out where we’re going, we’ll need a few additional variables to handle speed and direction. We’ll split the direction up into x and y values. Don’t worry too much about those, for now, we’ll be doing more with them in a minute. For now, we’ll initialize both by setting them equal to the speed.

let speed = 3;
let xDirection = speed;
let yDirection = speed;
const updatePosition = () => {
xPosition += xDirection;

Each time we call this function, the x position will be updated (1 x speed) pixels to the right. We’re just changing the xPostion right now so we can see how everything works. Let's plug in our updatePosition() and drawCircle() functions into loop().

const loop = () => {

Ok, that's...interesting. What is going on here? First off, our circle is going right forever, so it quickly leaves our drawing area. Maybe we’ll see it again after it wraps all the way around the earth and comes back. In the meantime, why does it look like the end of a popsicle stick?

So far, we have only told the canvas to re-draw our circle and nothing else. Every pixel that was drawn on during previous frames will keep the color it has unless we draw over it again. In short, we’re going to have to re-draw the entire canvas for each frame, not just our circle. For this, we’ll use ctx.clearRect(). This is a method that will clear the canvas area within it. It uses the same arguments as fillRect() which we used in part 1. In our loop() function, we’ll set the entire canvas to be cleared before we draw anything else.

const loop = () => {    ctx.clearRect(0, 0, canvas.width, canvas.height);
// We can get the width and height properties
// of our canvas element and use them as the
// width and height in clearRect().

And there we have it! Our popsicle stick is gone, and our orb is rapidly fleeing to the right. The last step is to change our updatePosition() function a little bit.

Fancier Movement

For this exercise, let's make our updatePosition() work a little differently. We are going to want to do 2 different things:

1: Move the circle in the given directions.

2: If the edge of the circle comes in contact with the edge of the canvas, reverse directions.

Remember, our xPosition and yPosition are at the center of our circle so if we want to know when the edge collides with the canvas, we’ll have to take the radius into account. Let's take a look at how we can do that:

const updatePosition = () => {
if (xPosition + xDirection > canvas.width - radius ||
xPosition + xDirection < 0 + radius) {
xDirection *= -1;
if (yPosition + yDirection > canvas.height - radius ||
yPosition + yDirection < 0 + radius) {
yDirection *= -1;
xPosition += xDirection;
yPosition += yDirection;

This code first checks if the next position will be beyond the edge of the canvas (taking account for the radius of the circle). If so, it reverses the direction. Either way, it then adds the directions to the current positions.

Badda-bing badda-boom we have a bouncing baby ball. And if we go back in and change the radius everything will still work! We could even set a speed variable and dynamically change the speed as we go if we wanted.

Time to Math

We’re looking pretty good at this point. We have a circle that will bounce around forever, we can pick the color, size, and speed very easily and nothing will break. Let's add a bit of randomness to the equation. Right now, our circle starts out by shooting off at a 45-degree angle and bounces around at that same relative angle the whole time. Remember in High School when a math teacher desperately tried to get you to feel like learning trigonometry was useful? Well, jokes on you, we’re using it now. Let’s use some old-school geometry and vector math to start our circle moving at a random angle. Don’t worry, it’s isn’t as involved as it sounds.

First, we’ll declare a direction variable and generate a random number between 0 and 360 degrees. Math.random() is a built-in function that will return a float between 0 and 1. We’ll just multiply that by 360 to get our random starting angle.

let direction = Math.random() * 360;

Next, we’ll make a function that will take our speed and direction and convert it to x and y vectors. Vectors are used to represent speed (or velocity, or acceleration, or force, etc) in a certain direction. In the image, our vector would be represented by C. The length of C is equal to our speed and θ (theta) represents the angle of our direction. Now we solve for A and B using SOH CAH TOA. We’ll wrap it all in a function just for fun. Again we’ll use a method of the built-in Math object, Math.cos() and Math.Sin()

const calculateVectors = () => {
xDirection = speed * Math.cos(direction);
yDirection = speed * Math.sin(direction);

This function will set our xDirection and yDirection based on our target speed and direction. And that's about it! Now we just need to call it. Go ahead head and pop our calculateVectors() function right before we first call our loop() function.

Play around! From here the possibilities become nearly endless. You can use these same ideas to draw complex shapes and move them however you want. How could you bring two balls in there and have them bounce off of each other? How could you build in gravity? Can you build a slider to dynamically control speed and direction? Trow in some Pong Paddles and you’ve nearly re-made gaming history.

Want to see some other examples using the methods we learned today? Check out some generative art doodles I made while learning how to animate using this technique. Here is one, here is another.

That's the basics of building an animation loop. In the next installment, we’ll be saying goodbye to our salmon circle and start to build up our game properly.

Geek Culture

Proud to geek out. Follow to join our +500K monthly readers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store