Animating Sprite Sheets With JavaScript
Tags: javascript, html, canvas
Published: 2018-07-01 19:33:27

---
title: Animating Sprite Sheets With JavaScript
published: true
description: Learn how to animate a character on a sprite sheet using HTML5's canvas and JavaScript.
tags: javascript, html5, canvas
series: Sprite Sheet Animations with JavaScript
---

Let's look at animating a sprite sheet, on an HTML5 canvas, using JavaScript.

### A Little Setup

First, let's create the canvas element.

```html
<canvas width="300" height="200"></canvas>
```

Add a border (so we can see our usable area).

```css
canvas {
  border: 1px solid black;
}
```

And load the sprite sheet ([https://opengameart.org/content/green-cap-character-16x18](https://opengameart.org/content/green-cap-character-16x18)). While we're at it, let's get access to the canvas and its 2D context.

```js
let img = new Image();
img.src = 'https://opengameart.org/sites/default/files/Green-Cap-Character-16x18.png';
img.onload = function() {
  init();
};

let canvas = document.querySelector('canvas');
let ctx = canvas.getContext('2d');

function init() {
  // future animation code goes here
}
```

The `init` function is called after the image is loaded, via `img.onload`. This is to ensure the image is loaded before we try working with it. All of animation code will go in the `init` function. For the sake of this tutorial, this will work. If we were dealing with multiple images, we'd probably want to use Promises to wait for all of them to load before doing anything with them.

### The Spritesheet

Now that we're set up, let's take a look at the image.

![Character sprite sheet](https://opengameart.org/sites/default/files/Green-Cap-Character-16x18.png)

Each row represents and animation cycle. The first (top) row is the character walking in a downward direction, the second row is walking up, the third row is walking left, and the fourth (bottom) row is walking right. Technically, the left column is a standing (no animation) while the middle and right columns are animation frames. I think we can use all three for a smoother walking animation, though. 😊

### Context's `drawImage` Method

Before we get to animating our image, let's look at the `drawImage` context method, as that's what we'll use for automatically slicing up the sprite sheet and applying it to our canvas.

[MDN docs - drawImage](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage)

Whoa, there are a lot of parameters in that method! Especially the third form, which is the one we'll be using. Don't worry, it's not as bad as it seems. There's a logical grouping to it.


```js
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
```

The `image` argument is the source image. The next four (`sx`, `sy`, `sWidth`, and `sHeight`) relate to the source image - the sprite sheet. The last four (`dx`, `dy`, `dWidth`, and `dHeight`) relate to the destination - the canvas.

The "x" and "y" parameters (`sx`, `sy`, `dx`, `dy`) relate to the sprite sheet (source) and canvas (destination) starting positions, respectively. It's essentially a grid, where the top left starts at (0, 0) and moves positively to the right and down. In other words, (50, 30) is 50 pixels to the right and 30 pixels down.

The "Width" and "Height" parameters (`sWidth`, `sHeight`, `dWidth`, and `dHeight`) refer to the width and height of the sprite sheet and canvas, starting at their respective "x" and "y" positions. Let's break it down to one section, say the source image. If the source parameters (`sx`, `sy`, `sWidth`, `sHeight`) are (10, 15, 20, 30), the the starting position (in grid coordinates) would be (10, 15) and stretch to (30, 45). Then ending coordinates are calculated as (`sx` + `sWidth`, `sy` + `sHeight`).

### Drawing The First Frame

Now that we've gone over the `drawImage` method, let's actually see it in action.

Our sprite sheet's character frame size is conveniently labeled in the file name (`16x18`), so that gives us our width and height attributes. The first frame will start at (0, 0) and end at (16, 18). Let's draw that to the canvas. We'll start with drawing this frame starting at (0, 0) on the canvas and keep the proportions.

```js
function init() {
  ctx.drawImage(img, 0, 0, 16, 18, 0, 0, 16, 18);
}
```

{% codepen https://codepen.io/martyhimmel/pen/yEGZpd %}

And we have our first frame! It's a little small though. Let's scale it up a bit to make it easier to see.

Change the above to this:

```js
const scale = 2;
function init() {
  ctx.drawImage(img, 0, 0, 16, 18, 0, 0, 16 * scale, 18 * scale);
}
```

You should see the image drawn on the canvas has doubled in size both horizontally and vertically. By changing the `dWidth` and `dHeight` values, we can scale the original image to be smaller or larger on the canvas. Be careful when doing this though, as you're dealing with pixels, it can start blurring pretty quickly. Try changing the `scale` value and see how the output is changed.

### Next Frames

To draw a second frame, the only thing we need to do is change some values for the source set. Specifically, `sx` and `sy`. The width and height of each frame are the same, so we'll never have to change those values. In fact, let's pull those values out, create a couple scaled values, and draw our next two frames to the right of our current frame.

```js
const scale = 2;
const width = 16;
const height = 18;
const scaledWidth = scale * width;
const scaledHeight = scale * height;

function init() {
  ctx.drawImage(img, 0, 0, width, height, 0, 0, scaledWidth, scaledHeight);
  ctx.drawImage(img, width, 0, width, height, scaledWidth, 0, scaledWidth, scaledHeight);
  ctx.drawImage(img, width * 2, 0, width, height, scaledWidth * 2, 0, scaledWidth, scaledHeight);
}
```

And this is what it looks like now:

{% codepen https://codepen.io/martyhimmel/pen/yEGZRB %}

Now we have the entire top row of the sprite sheet, but in three separate frames. If you look at the `ctx.drawImage` calls, there are only 4 values that change now -  `sx`, `sy`, `dx`, and `dy`.

Let's simplify it a bit. While we're at it, let's start using frame numbers from the sprite sheet instead of dealing with pixels.

Replace all the `ctx.drawImage` calls with this:

```js
function drawFrame(frameX, frameY, canvasX, canvasY) {
  ctx.drawImage(img,
                frameX * width, frameY * height, width, height,
                canvasX, canvasY, scaledWidth, scaledHeight);
}

function init() {
  drawFrame(0, 0, 0, 0);
  drawFrame(1, 0, scaledWidth, 0);
  drawFrame(0, 0, scaledWidth * 2, 0);
  drawFrame(2, 0, scaledWidth * 3, 0);
}
```

Our `drawFrame` function handles the sprite sheet math, so we only need to pass in frame numbers (starting at 0, like an array, so the "x" frames are 0, 1, and 2).

The canvas "x" and "y" values still take pixel values so we have better control over positioning the character. Moving the `scaledWidth` multiplier inside the function (i.e. `scaledWidth * canvasX`) would mean everything moves/changes an entire scaled character width at a time. That wouldn't work with a walking animation if, say, the character moves 4 or 5 pixels each frame. So we leave that as it is.

There's also an extra line in that list of `drawFrame` calls. This is to show what our animation cycle will look like, rather than just drawing the top three frames of the sprite sheet. Instead of the animation cycle repeating "left step, right step", it will repeat "stand, left, stand, right" - it's a slightly better animation cycle. Either is fine though - a number of games in the 80s used two step animations.

This is where we're currently at:

{% codepen https://codepen.io/martyhimmel/pen/RJEvvy %}

### Let's Animate This Character!

Now we're ready to animate our character! Let's take a look at `requestAnimationFrame` in the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame).

This is what we'll use to create our loop. We could also use `setInterval`, but `requestAnimationFrame` has some nice optimizations in place already, like running at 60 frames per second (or as close as it can) and stopping the animation loop when the browser/tab loses focus.

Essentially, the `requestAnimationFrame` is a recursive function - to create our animation loop, we'll call `requestAnimationFrame` again from the function we're passing as the argument. Something like this:

```js
window.requestAnimationFrame(step);

function step() {
  // do something
  window.requestAnimationFrame(step);
}
```

The lone call before the `walk` function starts the loop, then it's continuously called within.

Before we get to using it, there's one other context method we need to know and use - `clearRect` ([MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/clearRect)). When drawing to the canvas, if we keep calling `drawFrame` on the same position, it'll keep drawing on top of what's already there. For simplicity, we'll clear the entire canvas between each draw, rather than just the area we draw to.

So, our draw loop will look something like clear, draw the first frame, clear, draw the second frame, and so on.

In other words:

```js
ctx.clearRect(0, 0, canvas.width, canvas.height);
drawFrame(0, 0, 0, 0);
// repeat for each frame
```

Okay, let's animate this character! Let's create an array for the cycle loop (0, 1, 0, 2) and something to keep track of where we are in that cycle. Then we'll create our `step` function, which will act as the main animation loop.

The step function clears the canvas, draws the frame, advances (or resets) our position in the cycle loop, then calls itself via `requestAnimationFrame`.

```js
const cycleLoop = [0, 1, 0, 2];
let currentLoopIndex = 0;

function step() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawFrame(cycleLoop[currentLoopIndex], 0, 0, 0);
  currentLoopIndex++;
  if (currentLoopIndex >= cycleLoop.length) {
    currentLoopIndex = 0;
  }
  window.requestAnimationFrame(step);
}
```

And to get the animation started, let's update the `init` function.

```js
function init() {
  window.requestAnimationFrame(step);
}
```

{% codepen https://codepen.io/martyhimmel/pen/mKvXxB %}

That character is going places fast! 😂

### Slow Down There!

Looks like our character is a little out of control. If the browser allows it, the character will be drawn 60 frames per second, or as close as possible. Let's put a limit on that so it's stepping every 15 frames. We'll need to keep track of which frame we're on. Then, in the `step` function, we'll advance the counter every call, but only draw after 15 frames pass. Once 15 frames pass, reset the counter, and draw the frame.

```js
const cycleLoop = [0, 1, 0, 2];
let currentLoopIndex = 0;
let frameCount = 0;

function step() {
  frameCount++;
  if (frameCount < 15) {
    window.requestAnimationFrame(step);
    return;
  }
  frameCount = 0;
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawFrame(cycleLoop[currentLoopIndex], 0, 0, 0);
  currentLoopIndex++;
  if (currentLoopIndex >= cycleLoop.length) {
    currentLoopIndex = 0;
  }
  window.requestAnimationFrame(step);
}
```

{% codepen https://codepen.io/martyhimmel/pen/bKzvqP %}

Much better!

### The Other Directions

So far, we've only handled the down direction. How about we modify the animation a bit so the character does a complete 4-step cycle in each direction?

Remember, the "down" frames are in row 0 in our code (first row of the sprite sheet), up is row 1, left is row 2, and right is row 3 (bottom row of the sprite sheet). The cycle remains 0, 1, 0, 2 for each row. Since we're already handling the cycle changes, the only thing we need to change is the row number, which is the second parameter of the `drawFrame` function.

We'll add a variable to keep track of our current direction. To keep it simple, we'll go in the sprite sheet's order (down, up, left, right) so it's sequential (0, 1, 2, 3, repeat).

When the cycle resets, we'll move to the next direction. And once we've gone through every direction, we'll start over. So, our updated `step` function and associated variables look like this:

```js
const cycleLoop = [0, 1, 0, 2];
let currentLoopIndex = 0;
let frameCount = 0;
let currentDirection = 0;

function step() {
  frameCount++;
  if (frameCount < 15) {
    window.requestAnimationFrame(step);
    return;
  }
  frameCount = 0;
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawFrame(cycleLoop[currentLoopIndex], currentDirection, 0, 0);
  currentLoopIndex++;
  if (currentLoopIndex >= cycleLoop.length) {
    currentLoopIndex = 0;
    currentDirection++; // Next row/direction in the sprite sheet
  }
  // Reset to the "down" direction once we've run through them all
  if (currentDirection >= 4) {
    currentDirection = 0;
  }
  window.requestAnimationFrame(step);
}
```

{% codepen https://codepen.io/martyhimmel/pen/VdgXXB %}

And there we have it! Our character is walking in all four directions, animated all from a single image.