Creating Sprite Animations on the Canvas

by kirupa   |   18 March 2016

  Have questions? Discuss this HTML5 / Canvas tutorial with others on the forums.

For billions of years, sprite sheets have been used to simplify how you can define 2d visuals for video games and (more recently) web sites. Sprites can be used for displaying just a single visual, but they can also be made up of many visuals that you sequentially play back to create an animation. You see sprite sheets all the time, and you probably never even notice.

Take a look at the following sprite sheet:

This sprite sheet is taken from Twitter's implementation of the heart icon. When you click on the heart icon to favorite a tweet, an animation plays. It looks something like this:

The animation you see when you click on the heart icon is made up of the same individual frames you saw in the sprite sheet earlier. Yes, seriously!

In this tutorial, we are going to learn all about how to create an animation from a sprite sheet. The twist is that we will learn how to do that entirely inside the canvas. Also, we won't be re-creating the Twitter heart/favorite example. Instead, I have something equally exciting...and a whole lot less copyright infringing for us to work on instead.

Onwards!

OMG! A Canvas Book Written by Kirupa?!!

To kick your canvas skills up a few notches, everything you see here and more (with all its casual clarity!) is available in both paperback and digital editions.

BUY ON AMAZON

If you are looking for how to create a sprite animation using CSS (instead of using the canvas), check out this tutorial instead.

The Sprite Sheet

Before you can animate sprites from a sprite sheet, you first need a sprite sheet. Don't start panicking just yet. If you don't have a sprite sheet, you can just use one that I have already created here: https://www.kirupa.com/images/sprites_final.png (This is the same sprite sheet I will be using in our explanation and code, so I encourage you to use it if this is your first foray into creating animations from sprites.)

Now, if you are brave enough to want to create your own sprite sheet, then there are a variety of tools out there that can help you out with this. My favorite is Flash Professional's Generate Sprite Sheet functionality:

Flash's Generate Sprite Sheet funciontality

If you Google around, you'll find many other solutions that people rave about. Covering how to create a sprite sheet goes beyond the scope of this tutorial, but whatever tool you use, just make your sprite sheet meets the following two criteria:

  1. Each sprite in your sprite sheet is evenly sized.
  2. The sprites you wish to animate are arranged on a single row. Some tools like to break up the sprites into a single column or a combination of rows and columns! We don't want that.

If all of this boggles your brain, just use the sprite sheet from the URL I provided earlier. You can always experiment with your own sprite sheet once you've learned all about how to use and manipulate them.

How All of This Works

Before we start looking at the implementation, let's first take a few steps back and learn more about how a series of sprites in a sprite sheet can end up creating an animation. Our sprite sheet looks as follows:

Displaying the full sheet would take up too much space, but there are a few more circles beyond what you see here.

For simplicity, I am going to replace our sprite sheet with just solid colored circles to explain what is going on. The secret magic sauce to a sprite animation is to display just a single sprite at a time:

It doesn't matter how big or small your sprite sheet is. All users will ever see is just that one single sprite. To display the next sprite, we show the contents of our next sprite:

We keep going through our sprite sheet displaying each individual sprite. All of this is very sudden. Users will never see the transition from one sprite to another. All they will see is the end result of a sequence of images replacing each other. What you get is an animation in the most traditional sense. Really quickly replacing one picture with another is how hand-drawn animations and film strips basically work. What we are going to be doing isn't going to look a whole lot different than that!

It's Coding Time!

Now that we've seen an English version of how a sprite animation works, it's time to convert all of that into JavaScript. Our code is going to follow these four basic steps:

  1. Load the sprite sheet. This is pretty straightforward.
  2. Use drawImage to display just the first sprite from our sprite sheet. If you recall, the drawImage method allows you to optionally specify the exact co-ordinates and dimension of the image you want to display instead of displaying the whole thing.
  3. Shift the drawImage co-ordinates to display the next sprite...and the next sprite...and so on.
  4. Put all of the drawImage logic inside a requestAnimationFrame loop to create our animation.

We are going to add some code that does all four of these steps next! Make sure you have our usual HTML document setup with a canvas element whose id value is myCanvas. This is the same type of document we've been starting off from forever, but for your reference, the content look as follows:

<!DOCTYPE html>
<html>

<head>
  <title>Canvas Follow Mouse</title>
  <style>
    canvas {
      border: #333 10px solid;
    }

    body {
      padding: 50px;
    }
  </style>
</head>

<body>
  <canvas id="myCanvas" width="550px" height="350px"></canvas>

  <script>


  </script>

</body>

</html>

Inside the script block, add the following code:

var canvas = document.querySelector("#myCanvas");
var context = canvas.getContext("2d");

var myImage = new Image();
myImage.src = "https://www.kirupa.com/stuff/sprites_blue.png";
myImage.addEventListener("load", loadImage, false);

function loadImage(e) {
  animate();
}

var shift = 0;
var frameWidth = 300;
var frameHeight = 300;
var totalFrames = 24;
var currentFrame = 0;

function animate() {
  context.clearRect(120, 25, 300, 300);

  //draw each frame + place them in the middle
  context.drawImage(myImage, shift, 0, frameWidth, frameHeight,
                    120, 25, frameWidth, frameHeight);

  shift += frameWidth + 1;

  /*
    Start at the beginning once you've reached the
    end of your sprite!
  */
  if (currentFrame == totalFrames) {
    shift = 0;
    currentFrame = 0;
  }

  currentFrame++;

  requestAnimationFrame(animate);
}

Once you've added your code, make sure everything works by previewing your document in your browser. If everything worked properly, you will see a blue circle (with a sweet circular design inside it) happily rotating. This is all a result of us animating the contents of the sprite sheet that you saw earlier. In the next section, we'll take apart this code and learn how everything works!

Loading the Image

Before we can even think about animating our sprite sheet, we first need to load the sprite sheet image into our canvas. That is handled by the following chunk of code:

var myImage = new Image();
myImage.src = "https://www.kirupa.com/stuff/sprites_blue.png";
myImage.addEventListener("load", loadImage, false);

function loadImage(e) {
  animate();
}	  

All of this should be familiar to you if you've already seen the Drawing Images on the Canvas tutorial. We create a new Image object called myImage. We set the src property to the image we want to load, and then we listen for the load event to ensure we don't do anything until the image has fully made its way across the internet to your browser. Once our image has been loaded, we call the animate function via the loadImage event handler. The fun is about to start now!

It's Animation Time!

Before we get to the animate function, we have a few variables that we should look at first:

var shift = 0;
var frameWidth = 300;
var frameHeight = 300;
var totalFrames = 24;
var currentFrame = 0;

The variable names kinda hint at what they do, but for now, just know that they exist and pay attention to the default values assigned to them. We'll see all of these variables used really soon.

Now, we get to the animate function. This function is responsible for quickly cycling through each sprite in the sprite sheet to create the animation. A bulk of this responsibility lies in the following line:

context.drawImage(myImage, shift, 0, frameWidth, frameHeight,
                  120, 25, frameWidth, frameHeight);

This line handles which part of the sprite sheet to display, and it handles where to display it. Looking at this in greater detail, each sprite in our sprite sheet looks similar to this:

Each sprite is a square that is 300 pixels on each side. There are a bunch of sprites just like this arranged side-by-side, and what we are doing is taking just the first image and displaying it on the screen. To see how, let's look at our drawImage code with all of the variables replaced with their actual numerical values:

context.drawImage(myImage, 0, 0, 300, 300,
                  120, 25, 300, 300);

From our sprite sheet, we grab a 300px by 300px square starting at the (0, 0) mark. That is our first sprite and handled by the first line in our drawImage call. We place that grabbed sprite at its original size of 300px by 300px on our canvas at the (120, 25) mark. The end result looks a little bit like this:

Because of how drawImage works, we don't see any part of the rest of the sprite sheet. We only see the 300 by 300 pixel square we cut out from the sprite sheet and placed in our canvas.

To display the next sprite, we tell our drawImage method to grab the next sprite from our sprite sheet. Our shift variable is responsible for telling drawImage where to start looking for the next sprite, so what we need to do is simply adjust the value stored by the shift variable:

To move to our next frame, we increase our shift variable by 301. The number of pixels you need to shift to get to the next frame depends entirely on your sprite sheet. In our case, our sprite sheet has a 1 pixel gap between each sprite. That is why we increment our shift variable really awkwardly by both the frameWidth and a 1 value:

shift += frameWidth + 1; 

Ignoring the rest of the code for a second, you can now see how our drawImage function works to create the animation. For the second frame, looking at our drawImage call with all of the variables replaced with their stored values, you'll see something that looks like the following:

context.drawImage(myImage, 301, 0, 300, 300,
                  120, 25, 300, 300);

This ensures that our next sprite is taken from the (301, 0) position at the same 300 by 300 pixel size. This taken sprite is then placed at the (120, 25) mark at the original 300 by 300 pixel size. To our users, this will look like a direct replacement of the earlier sprite with a new one that is slightly more rotated.

With every requestAnimationFrame call to the animate function, we shift over to the next frame in our sprite sheet. We do this shifting by increasing the value of the shift variable by 301 each frame. That's it! This automatically ensures drawImage is looking at the right part of our sprite sheet and displays the correct sprite. This is all done very rapidly, so what you end up seeing is each frame played back to create a smooth animation.

Ok, we are almost done here. The last thing we are going to look at is some of the code we skipped:

if (currentFrame == totalFrames) {
  shift = 0;
  currentFrame = 0;
}

The code we skipped is kinda important. We need a way to know when we have reached the end of our sprite sheet so that we can restart our animation from the beginning. The currentFrame variable acts as a counter, and the totalFrames variable specifies the number of frames in our sprite sheet. The way you can figure out how many frames you have is by simply counting the number of sprites you have. Some image tools may provide you with that information. If your particular sprite sheet doesn't come with that information, you'll have to manually count...like an animal :P

Anyway, we determine the end by constantly checking when the value of currentFrame is the same as totalFrames. When both of those variables are equal, it means that we've reached the end of our sprite sheet and it's time to reset everything:

if (currentFrame == totalFrames) {
  shift = 0;
  currentFrame = 0;
}	  

By setting the value of shift to 0, is we ensure our next drawImage call looks at the first frame in our sprite sheet. Setting currentFrame to 0 simply resets our counter.

If we are not at the last frame where currentFrame is equal to totalFrames, then we should go right on and increment the value of currentFrame:

currentFrame++;

And...that's exactly what we do! This ensures we keep an accurate tally of where in the sprite sheet we are, and that helps us pull the plug and reset everything back to the beginning when we've reached the end of our sprite sheet.

Conclusion

For displaying 2d visuals, sprite sheets are wildly popular. They are most commonly used in games, but as you saw here, you can also use sprite sheets to define normal (and boring) things. Now that you've seen how to implement an animation using a sprite sheet, it all seems pretty easy, right? All you do is just load your sprite sheet and use drawImage to display a frame of your sprite sheet at a time. The fun starts to happen when you have no control over the sprite sheets you have to work with. In those cases, your drawImage shifting logic might be more involved than just incrementing one variable by a fixed amount. Anyway, no need to worry about that now. When you get to that point, you'll know what to do. If not, just comment here or post on the forums, and I or somebody else will try to help you out :P

If you have a question about this or any other topic, the easiest thing is to drop by our forums where a bunch of the friendliest people you'll ever run into will be happy to help you out!

THE KIRUPA NEWSLETTER

Get cool tips, tricks, selfies, and more...personally hand-delivered to your inbox!

( View past issues for an idea of what you've been missing out on all this time! )

WHAT DO YOU THINK?

blog comments powered by Disqus

NEWSLETTER

No spam. No fluff. Just awesome content sent straight to your inbox!

Awesome and high-performance web hosting!
BACK TO TOP
new books - yay!!!