Typescript Animated Sprites for HTML5 Games

☐ ☑ ⛛ If we want to animate an image, we must load multiple version of that image at once, then rotate through the version with every frame we render in the game. If you look at the following sprite sheet, you will notice that we have 35 versions of the same asteroid, but 3D rotated. I used a 3d model of an asteroid to render out each of those images, and if we flip through the images one at a time, it should look like the asteroid is rotating in 3D space.



The Animated Sprite Class

We're going to create an Animated Sprite class that will use our texture atlas and flip through a series of images. An animated sprite is going to need an x and y coordinate. We will replace the global x and y coordinate with values inside the game objects we want to render. We need a frameCount attribute so that we know how many frames will be inside our atlas. We actually need to have the atlas, because in the future we might have more than one atlas in a game. We will also need the sequence name. In our case, this will be "Asteroid" because our frames will be named "Asteroid00.png", "Asteroid01.png", "Asteroid02.png", etc. The sequenceName will become the first part of the frame name, followed by our frame number, followed by ".png" (for simplicity we will have all of our sprites be .png files). We will also need to know what our current frame is.

class cAnimatedSprite {
   public x: number = 0;
   public y: number = 0;
   public frameCount: number = 0;
   public atlas: cTextureAtlas;
   public sequenceName: string = "";

   public currentFrame = 0;

   constructor(x: number, y: number, frame_count: number, atlas: cTextureAtlas, sequence_name: string) {
      this.x = x;
      this.y = y;
      this.frameCount = frame_count;
      this.atlas = atlas;
      this.sequenceName = sequence_name;
   }
}

Adding a Frame class

One of the things about using a texture atlas that could throw off our animation is trimming. It is usually desirable to trim all of the sprites that make up your texture atlas for the purpose of saving space in your image file. The down side to this, is that the frames of your animation may not all be lined up as they should. This could cause your animation to jitter, which is needless to say, undesirable. In order to prevent this, we need to know how much of an offset there should be when rendering your image to the canvas. We are going to replace our cRectangle class with a cFrame class, that will have two additional attributes, ox and oy (standing for offset x and offset y).

class cFrame {
   x: number;
   y: number;
   w: number;
   h: number;

   ox: number; // OFFSET X
   oy: number // OFFSET Y

   constructor(x: number = 0, y: number = 0, w: number = 1, h: number = 1, ox: number = 0, oy: number = 0) {
       this.x = x;
       this.y = y;
       this.w = w;
       this.h = h;
       this.ox = ox;
       this.oy = oy;
   }

   set(x: number, y: number) {
       this.x = x;
       this.y = y;
   }

}

I'm also going to need to change the _onRead method inside my cTextureAtlas class to use the new cFrame class

protected _onRead = (data: any) => {
   var temp_frame: cFrame;

   for (var frame_name in data.frames) {
      var sprite_data: any = data.frames[frame_name];

      temp_frame = new cFrame(sprite_data.frame.x,
      sprite_data.frame.y,
      sprite_data.frame.w,
      sprite_data.frame.h,
      sprite_data.spriteSourceSize.x,
      sprite_data.spriteSourceSize.y
   );

   this.frames[frame_name] = temp_frame;
}

And then I need to make the following change to the dictionary atrribute frames:

class cTextureAtlas {
   public frames: { [index: string]: cFrame } = {};

Drawing the Animated Sprites

We are going to need to draw out animated sprite to the canvas, so we will want to add a draw() method to our cAnimatedSprite class. That draw method must increment the current frame, and use that to generate a frame string and call drawimage with the x and y coordinates of your frame as well as the width and height and the x and y offsets.

public draw = (): void => {
   this.currentFrame++;
   if (this.currentFrame >= this.frameCount) {
      this.currentFrame = 0;
   }

   ctx.save();
   ctx.translate(this.x, this.y);
   ctx.drawImage(this.atlas.image,
   this.atlas.frames[this.getFrameString()].x, this.atlas.frames[this.getFrameString()].y,
   this.atlas.frames[this.getFrameString()].w, this.atlas.frames[this.getFrameString()].h,
   this.atlas.frames[this.getFrameString()].ox, this.atlas.frames[this.getFrameString()].oy,
   this.atlas.frames[this.getFrameString()].w, this.atlas.frames[this.getFrameString()].h);
   ctx.restore();
}

I'm calling a method getFrameString() in order to get the name of the frame in the atlas. If the current frame is less than 10 I need to pad that string with a "0". I also need to add ".png" on to the end. This is making the assumption that all the images that make up our texture atlas come from .png files.

public getFrameString = (): string => {
   if (this.currentFrame > 9) {
      return this.sequenceName + this.currentFrame.toString() + ".png";
   }
   return this.sequenceName + "0" + this.currentFrame.toString() + ".png";
}

Use the arrow keys to move the asteroid

You can take a look at the full source code.

Prev -> Texture Atlas

Image to Canvas