FarmHeart

Animation · Sprites · Game Art · 9 min read · October 7, 2026

Sprite Animation Techniques for Browser Games

Animation brings a game world to life. A character that slides across the screen feels like a placeholder; a character that runs, jumps, and idles with fluid animation feels like a character. Browser games have excellent animation tools built into the platform — canvas, WebGL, and requestAnimationFrame give you everything you need.

Sprite Sheet Basics

A sprite sheet packs multiple animation frames into a single image. Instead of loading 12 separate PNG files for a walk cycle, you load one sheet and draw a different region of it each frame. This reduces HTTP requests (critical for load time) and keeps all frames in a single GPU texture (critical for render performance).

class SpriteSheet {
  constructor(image, frameWidth, frameHeight, columns) {
    this.image = image;
    this.fw = frameWidth;
    this.fh = frameHeight;
    this.cols = columns;
  }

  draw(ctx, frameIndex, x, y) {
    const sx = (frameIndex % this.cols) * this.fw;
    const sy = Math.floor(frameIndex / this.cols) * this.fh;
    ctx.drawImage(this.image, sx, sy, this.fw, this.fh, x, y, this.fw, this.fh);
  }
}

Frame Timing

Do not advance animation frames every render frame. At 60 FPS, a 6-frame walk cycle would complete in 100ms — impossibly fast. Instead, use a frame timer that advances at a configurable speed:

class Animation {
  constructor(frames, frameDuration) {
    this.frames = frames;       // array of frame indices
    this.duration = frameDuration; // ms per frame
    this.elapsed = 0;
    this.index = 0;
    this.loop = true;
  }

  update(dt) {
    this.elapsed += dt;
    while (this.elapsed >= this.duration) {
      this.elapsed -= this.duration;
      this.index++;
      if (this.index >= this.frames.length) {
        this.index = this.loop ? 0 : this.frames.length - 1;
      }
    }
  }

  currentFrame() {
    return this.frames[this.index];
  }
}

A frame duration of 100-150ms per frame works well for walk cycles. Faster actions (sword swings, explosions) use 50-80ms. Idle animations use 200-300ms for a relaxed feel.

Animation State Machines

Characters have multiple animations: idle, walk, run, jump, fall, attack, hurt, die. A state machine manages which animation plays and handles transitions between them:

class AnimationController {
  constructor() {
    this.animations = {};
    this.current = null;
  }

  add(name, animation) {
    this.animations[name] = animation;
  }

  play(name) {
    if (this.current === name) return;
    this.current = name;
    this.animations[name].index = 0;
    this.animations[name].elapsed = 0;
  }

  update(dt) {
    if (this.current) {
      this.animations[this.current].update(dt);
    }
  }

  frame() {
    return this.animations[this.current]?.currentFrame() ?? 0;
  }
}

Transition logic belongs in the game update, not the animation system. The game decides "player is moving and grounded, play walk" or "player velocity.y > 0, play fall." The animation system just plays what it is told.

Directional Sprites

Top-down and isometric games need sprites facing multiple directions. The common approach: a sprite sheet with rows for each direction (down, left, right, up) and columns for each frame in the animation.

For 2D side-scrollers, flip the sprite horizontally for the opposite direction instead of drawing separate left-facing frames. Canvas supports this with negative scale:

function drawFlipped(ctx, image, sx, sy, sw, sh, dx, dy) {
  ctx.save();
  ctx.translate(dx + sw, dy);
  ctx.scale(-1, 1);
  ctx.drawImage(image, sx, sy, sw, sh, 0, 0, sw, sh);
  ctx.restore();
}

Tweening and Easing

Not everything is sprite-based. UI elements, camera transitions, damage numbers, and particle effects use tweened animations — smoothly interpolating a value from start to end over a duration.

function easeOutQuad(t) { return t * (2 - t); }
function easeInOutCubic(t) {
  return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}

class Tween {
  constructor(from, to, duration, easing = t => t) {
    this.from = from;
    this.to = to;
    this.duration = duration;
    this.easing = easing;
    this.elapsed = 0;
  }

  update(dt) {
    this.elapsed = Math.min(this.elapsed + dt, this.duration);
    const t = this.easing(this.elapsed / this.duration);
    return this.from + (this.to - this.from) * t;
  }

  done() { return this.elapsed >= this.duration; }
}

Performance Tips

Asset Pipeline

For indie browser games, asset tools that work well:

Related Articles

Ready to Play?

Jump into FarmHeart — free, no download, runs in your browser.

Play FarmHeart