Frank Mitchell

Animate your game sprites without jQuery

When you’re first starting out with HTML5 game development, it’s tempting to reuse existing knowledge. Especially if you’ve got a web development background. Need to animate a sprite? Spritely is a jQuery plugin that does just that.

Grab a sprite sheet that’s set up with animation frames from left to right. This jewel from Dan Cook’s collection of Tyrian graphics works nicely.

A six frame sprite sheet of an orange jewel with a moving hilight applied to it.

Create a HTML <div> element for the jewel and a style it up with a little bit of CSS.

<div id="jewel"/>

#jewel {
  background: url(jewel.png) 0px 0px no-repeat;
  width: 24px;
  height: 24px;
}

Sprinkle in some JavaScript to animate it.

$('#jewel')
  .sprite({fps: 4, no_of_frames: 6})
  .active()

And you’re good, right?

Kind of. At four frames per second, the jewel loops through its animation in 1.5 seconds. But what if you want something more compilicated? Say a three second animation loop where half the time the jewel is unlit. The “easy” solution would be to change the image. Just add four more unlit frames before the first one.

If you go down that road, you’ll wind up with two problems. One, it’ll take forever to tweak animations, since you’ll be doing the work in an image editor. And two, you’ll blow up your game size. Bytes matter. Smaller games make for faster downloads. Faster downloads make for happier users.

Getting consistent FPS

jQuery is a lovely library, but it’s seriously overkill for simple sprite animation. Instead, we’ll use the window.requestAnimationFrame function to roll our own animation loop.

function render (now) {
  requestAnimationFrame(render)
}
requestAnimationFrame(render)

The now value that’s passed into our render function is a high resolution time stamp. It tells us when our render function will be called. If we want a consistent FPS, we’ll need to capture it in a timer class.

function Timer () {
  this.last = null
  this.elapsed = 0
}

Timer.prototype = {
  tick: function (now) {
    this.last = this.last || now
    this.elapsed = now - this.last
  }
}

We want to animate our jewel at four frames per second. That means we draw one frame every 250 milliseconds. We’ll use a global timer to track elapsed time, and if it’s been more than 250 milliseconds, we’ll draw a frame.

var timer = new Timer()

function render (now) {
  requestAnimationFrame(render)
  timer.tick(now)
  if (timer.elapsed >= 250) {
    timer.last = now
    // draw jewel
  }
}

Before we get into the drawing code, let’s take a minute to reason about our timing ocde and make sure it’s doing what we want. Timing is the heart of animation, so it’s important we get this right.

Let’s pretend time starts at zero and our render function gets called every 16 milliseconds. That means the first time through the now value will be 16. The second time through it will be 32. Then 48 and so on. How many loops do we have to go through before our jewel gets drawn?

15.625 = 250 / 16

So we have to go through 16 loops before our jewel’s first frame is drawn. After 16 loops, our now value will be 256. When does the second frame get drawn?

15.25 = 500 - 256 / 16

Again, 16 calls to our render function later, we get another frame. How about the third frame?

14.875 = 750 - 512 / 16

Oops. The third frame gets drawn 15 calls after the second frame. In this case, when we’re only animating at four frames per second, being off by a few calls might not matter. But once we try to animate at speeds of 30 or 60 FPS, being off by a few calls starts to add up.

To fix this, we can set the last variable in our timer to the expected time, instead of the actual time. If we expect our first frame to draw happen at 250 milliseconds, but it doesn’t happen until 256 milliseconds, we need to remove those extra 6 milliseconds from the recorded time.

function render (now) {
  requestAnimationFrame(render)
  timer.tick(now)
  if (timer.elapsed >= 250) {
    timer.last = now - (timer.elapsed % 250)
    // draw jewel
  }
}

Now the timeing of our render function is more accurate, since errors per frame won’t accumulate over time. As a side effect, this also handles cases where the time between calls to our render function isn’t consistent.

Animating without jQuery

Even though we’re not using jQuery and the Spritely plugin, we’d like to keep some of its flexibility. Like being able to set different frame rates for different sprites. Instead of keeping a global timer in our render function, lets put a custom timer in each sprite.

function Sprite (options) {
  this.interval = 1000 / options.fps
  this.timer = new Timer()
}

Sprite.prototype = {
  render: function (now) {
    this.timer.tick(now)
    if (this.timer.elapsed >= this.interval) {
      var then = this.timer.elapsed % this.interval
      this.timer.last = now - then
      // draw sprite
    }
  }
}

Now we can create a new instance of the Sprite class as our jewel and call its render function in the global render function.

var jewel = new Sprite({
  fps: 4
})

function render (now) {
  requestAnimationFrame(render)
  jewel.render(now)
}

I’m pretty sure it was Dave Shea who popularized the idea of doing image rollovers by changing an element’s background position on hover. We can use the same idea to animate our sprite. Get the HTML element that corresponds to our jewel and adjust its backgroundPositionX property.

Our Sprite class needs to track a few more things to make this happen. We’ll keep track of the HTML element for our sprite, the index of the frame we’re in, the pixel width of each frame, and the total number of frames.

function Sprite (element, options) {
  this.interval = 1000 / options.fps
  this.timer = new Timer()
  this.element = element
  this.width = options.width
  this.index = 0
  this.frames = options.frames
}

Assuming our jewel image is in a HTML element with an id attribute of “jewel”, we can set up our sprite like this

var element = document.getElementById('jewel')
var jewel = new Sprite(element, {
  fps: 4,
  width: 24,
  frames: 6
})

Now we can tweak the render function in the Sprite class to increment the frame index, calculate a new background position based on the frame index and image width, and set the background position on the element.

render: function (now) {
  this.timer.tick(now)
  if (this.timer.elapsed >= this.interval) {
    var then = this.timer.elapsed % this.interval
    this.timer.last = now - then

    this.index += 1
    this.index %= this.frames

    var offset = this.index * this.width
    var x = '-' + offset + 'px'
    this.element.style.backgroundPositionX = x
  }
}

Since the image we’re using has frames that go left to right, we use a negative offset for the background position so the frames move in the correct direction. Taking the modulus of the index by the total number of frames after incrementing it means our animation will loop. Once we hit the last frame we’ll go back to the start.

Livening things up a bit

We’ve ditched jQuery but we haven’t improved on Spritely much. Our API looks very similar and we can still only do a single step animation that matches up with our original image. How do we do something more complicated? Like animate our jewel so it looks like it’s breathing.

A normal human breathing cycle is a 1.5 second inhilation, a 1.5 second exhilation, and a 2 second rest period. Five seconds total. To get our jewel to animate like that, we can play frames 1, 2, and 3 as the inhale, frames 3, 4, and 5 as the exhale, and frame 0 as the rest period. What if we made the “frames” option in the Sprite class an array of frame indices instead of the total number of frames? Than we could construct a breathing jewel.

var element = document.getElementById('jewel')
var jewel = new Sprite(element, {
  fps: 2,
  width: 24,
  frames: [1, 2, 3, 3, 4, 5, 0, 0, 0, 0]
})

The two FPS value was calculated by divinding the three inhalation frames by the one and half seconds we want the inhalation to take.

2 = 3 / 1.5

Given an animation speed of 2 FPS, we need four unlit frames to account for the two seconds of rest. Hence the four zeros at the end of the frames array. The only other bit of code that needs to change is the render function in the Sprite class. It needs to index into the frames array for the offset.

render: function (now) {
  this.timer.tick(now)
  if (this.timer.elapsed >= this.interval) {
    var then = this.timer.elapsed % this.interval
    this.timer.last = now - then

    this.index += 1
    this.index %= this.frames.length

    var offset = this.frames[this.index] * this.width
    var x = '-' + offset + 'px'
    this.element.style.backgroundPositionX = x
  }
}

Now we’ve got a jewel that breathes.

Writing your own code

It’s totally tempting to say, “I can rewrite Spritely in 37 lines of code. Less code is always better. I’ll just use my version.” Sometimes that’s the right decision. Like in a game jam where one of the rules is that all code has to be new. Sometimes it’s the wrong decision. Like when you’re prototyping animations and you need something that just works. You have to pick.

I tend to do code rewrites when I want to really learn how something works. By taking an idea like sprite animation and writing my own handful of functions to implement it, I gain a better understanding of the potential issues that might crop up while I’m using a library I didn’t write. So when I have to dig into someone else’s code to fix something, I’ve already got a mental map of how it might work.

Plus it expands my toolbox for making video games.