Animate sprites using JavaScript to create games in CodeHS
A sprite is a 2-dimensional image used in video games or animation. Nearly all of your favorite characters from 2D games are represented with sprites.
Here’s a sprite of Karel the Dog in a classic, 2-d pixel art style:
Sprites are generally stored in a “spritesheet”, a single image which contains all of the poses of a sprite:
By cycling through a number of different sprites from the spritesheet, you can create the appearance of movement, like this walk cycle built from three frames:
In this tutorial we’ll create a technique for breaking a spritesheet into individual frames, then loop through them to create an animation of Karel walking both left and right.
Here’s our spritesheet for all the poses Karel will have:
The first row contains the frames for a walk cycle facing right, and the second row contains the frames for a walk cycle facing left.
In reality, this image is only 44x30 pixels, but we’ll enlarge it for use in our examples.
In order to break the spritesheet down, we’ll put it in an HTML5 canvas as an image, then use JavaScript to divide the image into each individual frame.
In this program, we first created a <canvas>
element in HTML, sized to the same dimensions as our very small spritesheet, 44px by 30px. In order to get it to display well, we scaled the canvas using CSS, and applied special rules to make sure the pixels would resize crisply.
In main.js
, we select the <canvas>
element from index.html
, create a new Image
from our spritesheet, then draw that image onto the canvas.
Note:
Going forward, we’re going to keep our style.css
and index.html
file the same and only focus on the main.js
.
Now we need to break this image into individual frames.
Spritesheets need to be consistent in their layout so that it’s possible to divide them evenly. Each sprite is the same width and height, in our case 14 pixels tall and 13 pixels wide. Spritesheets generally will have a border (marked in blue) and spacing (marked in green):
In order to extract a sprites, we’ll need to consider the border and spacing.
For example, the first sprite’s upper-left corner is positioned at (1, 1), then the second sprite’s upper-left corner is positioned at (16, 1).
In general, the x-coordinate of this upper-left corner will be equal to borderWidth + i * (1 + spacingWidth + spriteWidth)
, where i
is the column in which the sprite is located. In our case, borderWidth
and spacingWidth
are both 1, and spriteWidth
is 13. Similarly, the y-coordinate will be equal to borderWidth + j * (1 + spacingWidth + spriteHeight)
, where j
is the row in which the sprite is located.
Let’s write some quick JavaScript for generating the x and y coordinates of a sprite.
Using that function, we can figure out where to start when we pull a sprite from our spritesheet.
Now, we’ll need to actually extract the images from our spritesheet!
In the first program I used the context.drawImage
function, which draws a sprite onto the canvas:
The function takes a lot of arguments, so let’s go over them:
context.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
- sx
is the horizontal offset into the source image
- sy
is the vertical offset into the source image
- sWidth
is the width of the source image
- sHeight
is the height of the source image
- dx
is the horizontal offset into the destination canvas
- dy
is the vertical offset into the destination canvas
- dWidth
is the width of the destination canvas
- dHeight
is the height of the destination canvas
We’re going to be particularly concerned with sx
and sy
, the offset into the source image. In our case, the source image is our spritesheet, and by changing this offset, we can extract different frames from the spritesheet.
Using the same code from our first example, let’s see if we can extract the first frame of the second row, Karel facing left.
We did it!
Look on line 32 of main.js
—we’re calling context.drawImage
using values for sx
, sy
, sWidth
, sHeight
, dWidth
and dHeight
.
To get the sx
and sy
values we’re after, we use the spritePositionToImagePosition
function we wrote. Then, sWidth
and dWidth
are both SPRITE_WIDTH
, and sHeight
and dHeight
are both SPRITE_HEIGHT
.
By cycling through the different tiles of our spritesheet, we can animate Karel!
Let’s start by simply going through each tile in order, switching to the next one every .5 seconds (500 ms) using setInterval
.
It’s hideous!
Our animation is working, but we’re not clearing the canvas between every frame, so each Karel is stacking on top of the last one, creating some kind of monster. It might be a cool sprite, but that’s not what we’re going for.
Fortunately there’s an easy fix for this—the context.clearRect()
function. Let’s call it immediately before we draw a new frame to make sure there aren’t any Karels already on the canvas.
Spritesheets aren’t usually made to be looped through in order. You need to pick out which sprites you want then sequence them into a pattern which makes sense.
Here’s a good walk cycle for Karel, which reuses the second frame to create a smooth, continuous loop. I’ve annotated the row and column of each frame.
Because it’s kind of tedious to always refer to the row and column of a frame, let’s extract the frames into variables. Each variable will store an x and y position in the spritesheet corresponding to the frame it represents. Then, we can think of the frames this way:
So far, everything I’ve shown has been written in an HTML program, with an HTML file, a CSS file, and a JavaScript file. In order to use what we’ve learned in a JavaScript Graphics program, we’ll need to make some modifications.
First, we need to make sure everything can run in a single JavaScript file, so we can use it in a JavaScript Graphics program, which just has one file. To do this, we’ll need to create a canvas manually using JavaScript. We can use document.createElement('canvas')
to do this, then modify the canvas we created to have the right properties.
We set the width and height of the canvas to be large enough to store the individual sprite frame once we draw it on the canvas. We set the display to be ‘none’, which will let us use this canvas without it ever showing up.
Rather than setting the image-rendering
CSS property of the canvas
, we can set the imageRendering
property of the 2d context, which will give us crisp edges when we scale up the context, which we do on the next line.
Next, we’ll need to use WebImage
, the CodeHS class for managing a picture in a Graphics program. We can manually set the data
attribute of a WebImage
with the image data we extract from the canvas. We’ll need to set displayFromData = true;
on the WebImage
in order to get it to use the data we set for drawing the image.
Here’s the code for extracting a single frame from the spritesheet and storing it in a WebImage
. We’ll start by extracting the sprite at row 0, column 0:
There’s a lot happening, so let’s break it down.
First, we create an Image
from the spritesheet URL, like we’ve done before. Once it’s loaded, we can start to extract the data from it, but we’ll need to wait for that to happen with the onload
function.
Next, we create the WebImage
where we’ll store the data for this sprite. We copy the data from the spritesheet image onto the hidden canvas’ context, offset by this sprites position in the spritesheet. We can then extract that cropped sprite from the context using context.getImageData()
and store the result in the WebImage
. Because the context is scaled, when we draw the image it will be enlarged. When extracting the cropped sprite, then, we’ll need to account for the scale of the image.
I’ve put this all together in a class, Sprite
, which will take care of both this initial set up, but additionally adds some functions like addAnimation
and animate
which are used to loop through the frames. Sprite
also implements draw
, move
and setPosition
, so we can interact with the sprite just like we interact with other objects in Graphics programs.
You can copy the implementation of the Sprite class from my Sandbox at https://codehs.com/sandbox/andy/Sprite. By passing different arguments when you initialize your sprite, you can use different spritesheets:
Then, you can define an animation by which frames it includes and how long each should take:
Calling sprite.animate('walkright')
will start the animation.
Share what sprites you make with us on Twitter @CodeHSSandbox and @AndyCodeHS, or ask me any questions or feedback you have about the Sprite class.