Back

HTML5 Canvas: Sprite Animation and Game Physics

10 min read

HTML5 Canvas: Sprite Animation and Game Physics

Now that we understand the Game Loop, inputs, and canvas transformations, it's time to put it all together. Let's look at how to build a complete Player Class using real production code as an example. We'll implement gravity for jumping and cycle through an array of images for Sprite Animation.

1. Preparing the Image Assets

Instead of drawing a single square canvas shape, real games load sequential image items (frames). We load them carefully into memory and group them into Javascript objects based on the player's "State" (like Idle, Jump, MoveForward).

// Helper to generate the real DOM Image object
function loadImg(src) {
  const img = new Image();
  img.src = src;
  return img;
}

// Group downloaded image arrays by player animation state
function loadImagePlayer(country) {
  const animations = { Idle: [], Jump: [], MoveForward: [] /* ... */ };
  
  // Example: Loading 18 consecutive frames of Idle animation
  for (let i = 0; i <= 17; i++) {
    // String(i).padStart(2, "0") converts number 1 to exactly "01"
    animations.Idle.push(loadImg(`./Sprites/Characters/${country}/Idle/Idle_0${String(i).padStart(2, "0")}.png`));
  }
  return animations;
}

2. The Player Class Structure

Using a JavaScript class is the best standard to handle complex game entities. Our player needs properties mapping its physical location (x, y), movement momentum variables (dx, dy), animation status (frame, state), and world physics (speed, gravity).

class Player {
  constructor(country, position, ctx) {
    this.ctx = ctx;
    this.position = position; // Is player on "left" or "right" side?
    this.image = loadImagePlayer(country); // Preload all sprite images
    
    // Position and Physical Body Rules
    this.size = 150;
    this.x = position === "left" ? 300 : 600;
    this.y = 0; 
    this.dy = 0; // Delta Y: Vertical momentum velocity
    this.speed = 5;
    this.gravity = 0.2; // Constant downward pulling force

    // Animation & State Control tracking
    this.state = "Idle"; // Default state
    this.frame = 0; // Current animation slide tracker
    this.lastAnimate = Date.now(); // Built-in timer to control frame pacing
  }
}

3. Game Physics: Gravity and Jumping

In our update() method, we handle the classic Jump mechanic. Gravity constantly adds weight pushing the player down (this.dy += this.gravity), unless they touch the Ground/Floor level. Once on the ground, the downward falling stops (dy = 0). But if they hit the jump button, we apply an massive burst of negative upward velocity (this.dy = -8) to launch them!

  update() {
    this.prevState = this.state; // Track past state to detect animation switching
    const groundLevel = canvas.height - this.size - 90;

    // Is the player Y level touching or below Ground floor?
    const onGround = this.y >= groundLevel;

    if (onGround) {
      this.y = groundLevel; // Snap firmly to floor (Anti sink)
      this.dy = 0; // Stop gravity fall speed

      // JUMP trigger from keyboard tracking Map
      if (this.keys["arrowup"]) {
        this.dy = -8; // Instant upward pushing force applied!
      }
    } else {
      this.dy += this.gravity; // Gravity pulling downward continuously mid-air
    }

    this.y += this.dy; // Apply vertical momentum sum to the actual world Y pos!

    // STATE MACHINE : Intelligently determine what happens based on conditions
    if (this.y < groundLevel) this.state = "Jump";
    else if (this.keys["space"]) this.state = "Kick";
    else if (this.keys["arrowright"]) this.state = "MoveForward";
    else this.state = "Idle";

    // Auto-reset frame to 0 if animation state changed (e.g., Idle -> Jump)
    if (this.prevState !== this.state) this.frame = 0;
  }

4. Animating the Sprite & Horizontal Flipping Tricks

Finally, the animate() function controls which image slide from our array is painted. We use a built-in timer limiter logic (Date.now()) to slow down the animation pacing. Otherwise, the frames would change 60 times a second and skip too fast!

And lastly, Remember our Transformation (Origin Flip/Scale) lesson? If the player is on the "Right" side of the football field team, we want them facing left! So we shift our origin point using ctx.translate and mirror their rendering orientation with ctx.scale(-1, 1)!

  animate() {
    const activeImageArray = this.image[this.state];
    const now = Date.now();
    
    // Only increment frame every 50ms (Animation Pacing)
    if (now - this.lastAnimate > 50) {
      this.frame++;
      // Loop back to start if we exceed the frames length
      if (this.frame >= activeImageArray.length) this.frame = 0; 
      
      this.lastAnimate = now;
    }

    // Flipping trick specifically for Player 2 (Right side)
    if (this.position === "right") {
      this.ctx.save();
      // Move context pivot exactly at character width before scale mirroring!
      this.ctx.translate(this.x + this.size, this.y);
      this.ctx.scale(-1, 1); // Flip Horizontally!

      this.ctx.drawImage(
        this.image[this.state][this.frame], // Print the exact sliding image index based on state
        0, 0, this.size, this.size
      );
      this.ctx.restore(); // Undo flip to secure next canvas painting!
    } else {
      // Normal straightforward draw for Player 1 Left side
      this.ctx.drawImage(
        this.image[this.state][this.frame],
        this.x, this.y, this.size, this.size
      );
    }
  }

If you can conquer this system, this code pattern acts as the most rock-solid foundational blueprint for virtually every 2D platformer and fighting game code!