Frame-by-frame animation

Draw images and sprite animations

Learn how to draw images on the HTML5 canvas with drawImage(), and turn a sprite sheet into a JavaScript sprite animation. By the end of this tutorial, you'll have a swarm of animated bees buzzing around your game, each with its own hitbox and direction.

Part of a series - In this series, you make an HTML5 game step by step. Here, the bouncing circles from the collision detection part become flying bees.

Load an image with JavaScript

Before you can draw an image on the canvas, you need the image itself. In JavaScript, you load one by creating an Image object and setting its src property to the path or URL of the image file. The browser starts loading the image as soon as you set src.

Loading takes a moment, and your code doesn't wait for it. So use the onload event to find out when the image is ready to be drawn:

let img = new Image();
img.onload = function () {
    // The image has loaded, so you can draw it now
    context.drawImage(img, 275, 30);
};
img.src = 'potion.png';

The browser calls the function as soon as potion.png has loaded. Inside that function, drawImage() draws the image on the canvas, using the canvas context. You'll learn more about both in a moment. It's a good habit to set onload before src, so the function is in place before the image starts loading.

Here's the image this tutorial uses for its first examples. It's an image from our tutorial on drawing potion bottles.

A potion bottle, the example image used in this tutorial

Why isn't the image visible?

Have you implemented every step so far, but you still don't see the image being drawn? Or the image is drawn, but only sometimes? Chances are you're drawing the image before it has fully loaded.

Loading an image takes some time. If you call drawImage() too early, there's nothing to draw yet, and the canvas stays empty. That's why the drawing happens inside onload.

Does your image come from an <img> element on the same HTML page as your canvas? Then give that element an id, like myImage in the code below, and get it with getElementById(). You can draw it the same way. Use the window.onload event to wait for the page and all its images to load completely:

window.onload = function () {
    // The page is completely loaded now
    // You can reference the image element
    let img = document.getElementById('myImage');
    context.drawImage(img, 275, 30);
};

Draw an image on the canvas

Drawing images on the canvas is pretty straightforward. You can simply use the drawImage() function on the 2D context. The tutorial on how to set up an HTML5 canvas, earlier in this series, shows you how to get that context with canvas.getContext('2d').

The function needs a reference to an image and a position. The method signature looks like this: context.drawImage(img, x, y). Your img variable holds the image, so all you need is an x and a y position. Call the function like this:

context.drawImage(img, 275, 30);

The image your img variable refers to will be drawn on the canvas. In this example, its top-left corner will be drawn at (275, 30): 275 pixels from the left edge of the canvas and 30 pixels from the top. It looks exactly the same as the original image.

Resize an image

With the drawImage() method you can easily stretch and scale images. Just add width and height to the existing arguments of the function call, like so: context.drawImage(img, x, y, width, height).

The image from before is 200x242 pixels. Here's an example that stretches it to a size of 100x200:

context.drawImage(img, 325, 50, 100, 200);

The aspect ratio of the image, the ratio between its width and height, has changed. The image looks distorted now.

Scale while keeping the aspect ratio

To resize the image but keep its aspect ratio, use its width and height properties.

In the next example, the image is drawn at half its original size:

context.drawImage(img, 205, 90, img.width / 2, img.height / 2);

Here's the result, with the original image displayed next to it.

The image is scaled, but no longer distorted. The aspect ratio is the same as that of the original image. Just keep in mind you need to scale the width and height by the same factor if you want to preserve the aspect ratio of an image.

How to fix scaling artifacts

When you scale an image up or down from its original size, you might notice some quality issues, like jagged edges or other scaling artifacts. Smoothing can help fix this problem. With smoothing on, the browser blends neighboring pixels when it scales an image, so the result looks better.

By default, image smoothing is enabled on the canvas, so you'll usually only see jagged edges if you turn it off. You can manually toggle smoothing with the imageSmoothingEnabled property on the context. You can even control the smoothing quality: set imageSmoothingQuality to 'low', 'medium' or 'high', although this option isn't supported by all browsers. Keep in mind that enabling smoothing or setting a high smoothing quality can affect performance.

Here's a quick example. The image is drawn at three times its size. Turn smoothing on and off, and look closely at the edges:

Live demo The code updates as you play around
imageSmoothingEnabled
context.imageSmoothingEnabled = true;
context.imageSmoothingQuality = 'high';
context.drawImage(img, 10, 30, img.width * 3, img.height * 3);

When imageSmoothingEnabled is false, each pixel of the image becomes a small square, and the edges look blocky. When it's true, the edges look smooth, but a little soft.

Smoothing makes most images look better. Pixel art is the exception, because when you scale it up, every pixel should stay a crisp square. So for pixel art, turn smoothing off and round x and y to whole pixels with Math.round(). The tutorial on setting up a canvas shows how to scale up pixel art without blurring it.

Draw only part of an image

Sometimes you only want to draw a small part of the original image. Leaving out part of the source image is called clipping, and the drawImage() function can do it. All you need to do is add a few extra arguments. Don't confuse this kind of clipping with the clip() method of the context, which limits the area you can draw in.

First you define the source rectangle of the image, then you define the destination rectangle. The method signature looks like this:

context.drawImage(
    img,
    sx, sy, sWidth, sHeight,
    dx, dy, dWidth, dHeight
);

Here's what the eight numbers do:

  • sx, sy - The top-left corner of the part you cut out, measured from the top-left corner of the image.
  • sWidth, sHeight - The size of that part.
  • dx, dy - Where the top-left corner of the part goes on the canvas.
  • dWidth, dHeight - How big you draw it. If it's smaller or larger than the source rectangle, the part is scaled.

Drag the red rectangle over the image in the example below, and pick a size for the destination. The code shows the numbers for each position:

Live demo The code updates as you play around
Size
context.drawImage(
    img,
    50, 0, 100, 50,
    80, 70, 200, 100
);

If the destination rectangle is twice the size of the source rectangle, as with Double in the demo, the image is clipped and scaled at the same time.

What is a sprite sheet?

You can use the technique of clipping to display images from sprite sheets. A sprite sheet is a collection of images, all merged together into one source image. Sprite sheets are most commonly used in games to store animations or a set of assets in a single image file.

Here's the sprite sheet you'll use in the rest of this tutorial. It's a bee, seen from above:

A sprite sheet with eight frames of a bee seen from above, in two rows

Every small image on the sheet is a frame. The sheet has two rows, and each row is a different animation:

  • The first row - The bee flies. The wings flap in four frames, and this animation loops.
  • The second row - The bee bumps into something, gets squashed and feels dizzy for a moment. This animation plays once.

All frames are the same size: 128x128 pixels. That's twice the size you'll draw the bees at later in this tutorial. The extra pixels keep the bees sharp on high-resolution screens.

You load the sprite sheet like any other image. In the examples below, it's in a variable called sprite. To draw a single frame, you need to know its column and row. Multiply the column by the width of a frame and the row by its height, and you have the top-left corner of the source rectangle:

// The sprite sheet has 4 columns and 2 rows
const numColumns = 4;
const numRows = 2;

// Define the size of a frame
let frameWidth = sprite.width / numColumns;
let frameHeight = sprite.height / numRows;

// Columns and rows start at 0
let column = 2;
let row = 0;

context.drawImage(
    sprite,
    column * frameWidth, row * frameHeight, frameWidth, frameHeight,
    10, 30, frameWidth, frameHeight
);

This draws the frame at its full size of 128x128. Click or tap a frame on the sprite sheet to draw it:

Live demo The code updates as you play around
// Columns and rows start at 0
let column = 2;
let row = 0;

Where to find sprites

You can find royalty-free game assets online, including sprite sheets. Pick one and use what you've learned here to create your own animation. You can even take it a step further and make your own sprite sheet.

Real games often pack all their images into a few big sprite sheets. Our own game Pixi Pop has four of them. Here are some of its sprites:

Sprites of different sizes from the game Pixi Pop, such as potions, orbs, flowers and icons, packed into one sheet

The sprites come in different sizes, so a game has to keep track of the position and size of each one. A sprite sheet like this is also called a texture atlas. Want to see these sprites in action? Play Pixi Pop in your browser.

Create a sprite animation

To animate a sprite, you draw its frames one after another, in quick succession. Draw them fast enough, and your eyes see movement.

But when should you move on to the next frame? Your game loop, which you built earlier in this series, runs at the refresh rate of the screen. That's 60 times per second on most screens, but some screens refresh 144 times per second or more. If you pick a new frame every time the loop runs, the bee flaps its wings more than twice as fast on a 144 Hz screen as on a 60 Hz screen.

To fix this, give the animation its own speed, in frames per second, and keep track of the time that has passed. Here, frames means the frames in the sprite sheet, not how often your game loop runs. You already know how much time has passed: the JavaScript game loop calculates secondsPassed and passes it to your update() function. Add up the time in update(), and pick a new frame when it's due:

// The animation plays at 10 frames per second
const fps = 10;
const numFrames = 4;

let currentFrame = 0;
let frameTimer = 0;

function update(secondsPassed) {
    // Add up the time that has passed
    frameTimer += secondsPassed;

    while (frameTimer >= 1 / fps) {
        frameTimer -= 1 / fps;

        // Pick the next frame, and start over after the last one
        currentFrame = (currentFrame + 1) % numFrames;
    }
}

The frameTimer adds up the time that has passed. Every time it reaches 1 / fps, a tenth of a second here, the animation moves on to the next frame. The while loop catches up when more than one frame is due, for example when the game loop slows down for a moment. The % operator gives the remainder of a division, so after the last frame, currentFrame goes back to 0.

In draw(), the other function your game loop calls, you draw the current frame from the first row:

function draw() {
    context.clearRect(0, 0, canvas.width, canvas.height);
    context.drawImage(
        sprite,
        currentFrame * frameWidth, 0, frameWidth, frameHeight,
        10, 30, frameWidth, frameHeight
    );
}

Pick a frame rate and watch the wings:

fps

At 2 frames per second, you see every single frame. At 10, the wings start to flap, and at 20, the bee really buzzes. A walking character often uses 8 to 12 frames per second. Small, quick movements like wings can go faster.

Play an animation once

The flying animation loops forever. But the bump animation should play once, and then the bee goes back to flying. So for every animation, keep track of its row, its speed and whether it loops:

// One animation for every row of the sprite sheet
const animations = {
    fly: {row: 0, fps: 20, loop: true},
    bump: {row: 1, fps: 10, loop: false}
};

let animation = animations.fly;

function play(name) {
    // Start the new animation at its first frame
    animation = animations[name];
    currentFrame = 0;
    frameTimer = 0;
}

Then update() checks what to do after the last frame:

function update(secondsPassed) {
    frameTimer += secondsPassed;

    while (frameTimer >= 1 / animation.fps) {
        frameTimer -= 1 / animation.fps;
        currentFrame++;

        if (currentFrame == numFrames) {
            if (animation.loop) {
                // Start over
                currentFrame = 0;
            } else {
                // Done, so go back to flying
                play('fly');
            }
        }
    }
}

Until now, the top of the source rectangle in draw() was always 0, the top of the first row. Now it depends on the animation: animation.row * frameHeight. Press the Bump button in the demo below to play the bump animation:

The bee flies, gets squashed and dizzy for a moment, and then flies on. Any animation in your game can work like this, from a character that jumps to an explosion that plays once and disappears.

Load your images before the game starts

A game usually has more than one image. You don't want the game to start while some of them are still loading. So wait until every image has loaded, and start your game loop after that.

A small function that returns a Promise makes that easy. A Promise is a placeholder for a result that isn't ready yet, like an image that's still loading. The function below creates a Promise for one image and calls resolve() with that image as soon as it has loaded:

function loadImage(src) {
    return new Promise(function (resolve, reject) {
        let img = new Image();
        img.onload = function () {
            resolve(img);
        };
        img.onerror = reject;
        img.src = src;
    });
}

Promise.all() waits for several of these Promises at once. The images can finish loading in any order, but you get them back in the same order you listed them in:

Promise.all([
    loadImage('bee-sprite-sheet.png'),
    loadImage('potion.png')
]).then(function (images) {
    // All images have loaded, so start the game
    sprite = images[0];
    img = images[1];
    window.requestAnimationFrame(gameLoop);
});

When all images have loaded, the function you passed to then() gets an array with the image each loadImage() passed to resolve(). So images[0] is the sprite sheet, and images[1] is the potion.

If an image can't be loaded, for example because of a typo in its name, the Promise is rejected. Then the game doesn't start, and your browser's console tells you which image is missing.

Turn the circles into bees

Drawing a single image is fun, but a game usually has lots of them. In the previous tutorial, you added collision detection and physics to your game. If you apply what you've learned about sprites to that game example, you can create something a bit more game-like.

In the old example, simple circles are drawn to the canvas. Let's replace them with bees. Bees don't fall, so leave out the line in update() that adds gravity. Then create a Bee class that extends GameObject, just like the Circle class did for the circles. GameObject is the class from the previous tutorial that holds the position, velocity and mass of an object. To turn the circles into bees, you'll need to take the following steps:

  • Load the sprite sheet - Do this only once, for all bees.
  • Update the animation - In update(), move on to the next frame when it's time.
  • Draw the current frame - In draw(), cut the current frame out of the sprite sheet and put it on the canvas.
  • React to collisions - When a bee hits something, play the bump animation once.

Your Bee class will look something like this:

class Bee extends GameObject {
    // One sprite sheet for all bees, with 4 columns and 2 rows
    static sprite;
    static numColumns = 4;
    static numRows = 2;
    static frameWidth = 128;
    static frameHeight = 128;

    // One animation for every row of the sprite sheet
    static animations = {
        fly: {row: 0, fps: 20, loop: true},
        bump: {row: 1, fps: 10, loop: false}
    };

    constructor(context, x, y, vx, vy) {
        // Every bee has a mass of 1
        super(context, x, y, vx, vy, 1);

        // Keep flying at the same speed after a bump
        this.restitution = 1;

        // The hitbox is the round body of the bee
        this.radius = 15;

        // Start at a random frame, so the bees don't flap in sync
        this.animation = Bee.animations.fly;
        this.currentFrame =
            Math.floor(Math.random() * Bee.numColumns);
        this.frameTimer = 0;
    }

    play(name) {
        this.animation = Bee.animations[name];
        this.currentFrame = 0;
        this.frameTimer = 0;
    }

    bump() {
        // Don't start over while the bee is still dizzy
        if (this.animation != Bee.animations.bump) {
            this.play('bump');
        }
    }

    update(secondsPassed) {
        // Move at the current velocity
        this.x += this.vx * secondsPassed;
        this.y += this.vy * secondsPassed;

        // Pick the next frame when it's due
        this.frameTimer += secondsPassed;
        while (this.frameTimer >= 1 / this.animation.fps) {
            this.frameTimer -= 1 / this.animation.fps;
            this.currentFrame++;

            // After the last column, the row is done
            if (this.currentFrame == Bee.numColumns) {
                if (this.animation.loop) {
                    this.currentFrame = 0;
                } else {
                    this.play('fly');
                }
            }
        }
    }

    draw() {
        // Draw the frame at half its size
        this.context.drawImage(
            Bee.sprite,
            this.currentFrame * Bee.frameWidth,
            this.animation.row * Bee.frameHeight,
            Bee.frameWidth, Bee.frameHeight,
            this.x - 28, this.y - 32, 64, 64
        );
    }
}

All bees share one sprite sheet, so it's a static property of the class. You load it once, before the game starts, with the loadImage() function from earlier, and store it in Bee.sprite. Then, in createWorld(), the function that fills your game world, create bees instead of circles with new Bee(context, x, y, vx, vy).

Each bee starts its animation at a random frame, so they don't all flap at the same time. Want more randomness in your game, like random positions or rare loot? The tutorial on random numbers in JavaScript shows you how.

To play the bump animation, call bump() on both bees in detectCollisions(), the function that checks every pair of objects for a collision. Do it right after you change their velocity:

// Change the velocity of both objects
// ...

// Both bees feel the bump
obj1.bump();
obj2.bump();

Do the same for a bee that hits the edge of the canvas. That happens in detectEdgeCollisions(), the function that makes objects bounce off the edges. Here's what you'll get:

Colliding bees that get dizzy? That looks a lot more like an actual game. But look at which way the bees are facing. They all face right, no matter which way they fly. You'll fix that in a moment, but first, take a closer look at where the bees are drawn.

Fit the hitbox to the bee

As you may have noticed in draw(), the top-left corner of the image isn't at the bee's position (x, y). The image is shifted to the left and up. Why is that?

The circle used for collision detection is the hitbox of the bee. As explained in the previous tutorial, many games use a simple shape as a hitbox for a more complex one. Here, the circle covers the round body of the bee.

The body isn't in the middle of the image, though. The head sticks out at the front, so the body sits a bit further back. If you center the image on (x, y), the circle slides forward, partly onto the head. That's why draw() shifts the image so that the middle of the body lands on (x, y). Switch between Centered and On the body in the demo below:

Live demo The code updates as you play around
Hitbox
// The middle of the body lands on (x, y)
this.x - 28, this.y - 32, 64, 64

In draw(), you subtract 28 from x and 32 from y. Those are the horizontal and vertical distances from the top-left corner of the image to (x, y). The middle of the bee's body is at (56, 64) in the 128x128 frame. You draw the frame at half its size, so that becomes (28, 32). The arrows in the demo above show these distances.

The wings, the antennae and the stinger stick out of the circle, and that's on purpose. Choose Big circle in the demo above to see why. The wings change size from frame to frame, so a circle around them is too big most of the time. With a hitbox like that, the bees would bounce off each other before they even touch.

If a part does matter in your game, give it a hitbox of its own. Say a bee has to reach a flower with its head. Choose Two circles in the demo above, and a second, smaller circle covers the head.

If each bee has two circles, two bees collide when either circle of one bee touches either circle of the other. That's four checks instead of one. See the section about hitboxes in the previous tutorial for more examples.

You'll rotate the bees in the next section. Each bee rotates around the middle of its body, so the circle on the body stays where it is. A circle on the head would have to rotate with the bee. To keep things simple, the bees in this tutorial stick to a single circle.

You can always draw the hitbox on screen to check that it fits. Add these lines at the end of draw():

this.context.beginPath();
this.context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);
this.context.stroke();

Rotate an image on the canvas

With sprites, the bees look a lot better than the circles from the previous tutorial, but they still look a bit unnatural when they fly. It would look more natural if each bee faced the way it's flying.

You can achieve this effect by rotating images on the canvas. To start rotating, you'll first need to use the velocity of the object to calculate the angle of rotation. The tutorial on collision detection explains how. You can do this in your update() function to keep the angle of each object up to date automatically. Here's an example:

update(secondsPassed) {
    // Move at the current velocity
    this.x += this.vx * secondsPassed;
    this.y += this.vy * secondsPassed;

    // Calculate the angle, in radians
    this.angle = Math.atan2(this.vy, this.vx);

    // ...
}

Math.atan2() turns the velocity into an angle, in radians. An angle of 0 points to the right, and larger angles turn further clockwise. A full turn is 2 * Math.PI.

Now draw the bee image at that angle. You don't rotate the image itself, though. Instead, you rotate the context, which rotates the coordinate system you draw in, and then you draw the image as usual. Replace your draw() function with this one:

draw() {
    // Save the context, move it to the bee and rotate it
    this.context.save();
    this.context.translate(this.x, this.y);
    this.context.rotate(this.angle);

    // Draw the image with the middle of the body at (0, 0)
    this.context.drawImage(
        Bee.sprite,
        this.currentFrame * Bee.frameWidth,
        this.animation.row * Bee.frameHeight,
        Bee.frameWidth, Bee.frameHeight,
        -28, -32, 64, 64
    );

    // Put the context back the way it was
    this.context.restore();
}

Let's look at this in more detail. You can change the way things are drawn to the canvas with a transformation matrix. It tells the canvas how to move, rotate or scale everything you draw. The rotate() function adds a rotation to the matrix. But it always rotates around the origin, the point (0, 0), which is in the top-left corner of the canvas by default. On its own, it would swing the bee around that corner and put it in the wrong place.

That's why you call translate() first. It moves the origin to (x, y), the position of the bee. Then rotate() turns the coordinate system around that point. You draw the image the same way as before, but relative to (0, 0) instead of (x, y). The middle of the body lands on the new origin, so that's the point the bee spins around.

Rotating the bee takes five steps: save the context, move its origin to the bee, rotate it, draw the image and restore the context. Step through them in the demo below. The code shows the lines that have run so far, and the see-through bee shows where drawImage() would put the bee at each step.

Live demo The code updates as you play around
Step
// Save the context so you can restore it later
this.context.save();

The sprite sheet shows the bee facing right. That's the direction of an angle of 0, so the angle from Math.atan2() works without any extra rotation. If you use your own sprite sheet and its sprites face a different direction, add a correction to the angle. For sprites that face up, that's a quarter turn clockwise: this.angle + Math.PI / 2.

When you're done drawing, the context needs to be restored. Otherwise, it stays rotated, and everything you draw after that will be rotated too. That's where save() and restore() come in. save() stores the current state of the context, and restore() brings it back.

You might also see setTransform(1, 0, 0, 1, 0, 0) used to reset the context. That works too, but it also throws away any scale you've set yourself, like the one that keeps your canvas sharp on high-resolution screens. restore() puts back exactly what you had.

This is what you get when you run the code. Click or tap the canvas to add more bees:

The moving objects look a lot more natural now! Each bee faces the way it's flying, flaps its wings and gets dizzy when it bumps into another bee or the edge of the canvas. Under the hood, the bees still move and collide just like the circles from the previous tutorial. You only changed the way they look.

Common questions about images and sprites

Why does my sprite look blurry?

When you draw an image larger than its original size, the browser smooths it, and it looks blurry. Use a sprite sheet with enough pixels, like the bee sheet, where every frame is twice the size the bees are drawn at. For pixel art, set imageSmoothingEnabled to false, and round the position to whole pixels with Math.round(). If you don't round it, some pixels of the sprite come out narrower or wider than others, and its edges look soft.

How do I rotate an image around its center?

Follow the same steps as for the bee, but draw the image at (-width / 2, -height / 2), using the width and height it has on the canvas. The center of the image then lands on the origin, and that's the point it rotates around. The bees rotate around the middle of their bodies instead, because that's where their hitbox is.

How do I flip a sprite horizontally?

Scale the context by -1 along the x-axis, using the same steps as for rotating. Call save(), call translate() to move the origin to the sprite's position, call scale(-1, 1), draw the image at (-width / 2, -height / 2) and call restore(). A character that walks left can then use the same frames as one that walks right.

Should I use one sprite sheet or separate images?

One sprite sheet is usually better. The browser loads one file instead of many, and your game only has to wait for one image. Separate images are fine for a small game or a quick prototype.

How many frames does a sprite animation need?

Fewer than you might think. The bee only needs four frames to flap its wings. A walk cycle often has six to eight frames. If the movement looks choppy, add a few more frames to make it smoother.

Can I make a sprite animation with CSS?

Yes. For a sprite on a web page, CSS can step through the frames of a sprite sheet with steps(). Our animation tool for easing functions shows how. For a game on the canvas, you draw the frames yourself, like in this tutorial.

Why do I see a thin line from the next frame?

That's called bleeding. When the browser smooths a scaled image, it mixes in a few pixels from just outside the source rectangle. Leave a few empty pixels along the edges of each frame, like the bee sheet does, and the line disappears.

The complete code

Here's the complete code for the bees. It picks up where the previous tutorial left off, and it loads the sprite sheet from spicyyoghurt.com. Paste it into an HTML file and open it in your browser:

What's next?

You've learned how to draw images, clip them and turn a sprite sheet into an animation. Your bees flap their wings, bump into each other and turn to face the way they fly. With these basic principles and some creativity, you can come up with interesting prototypes that start to look more and more like an actual game.

If you have any questions, feel free to ask them in the comment section. You can

In the next part of this series, you'll take control of one of the bees and add keyboard, mouse and touch controls to your game.