Week 11: Canvas Games & APIs with fetch()

To Do This Week

Explore Canvas game techniques and APIs with fetch(). Add to your running list of combination ideas for the Two Front-End Module Projects: #1 Due Nov 9th, #2 Due Nov 16th. Each project combines at least two front-end modules:

Use AI freely to research and test ideas. If you use AI to build logic steps and syntax, use the JS Mentor Metaprompt, and provide a link to your chat in the script, commented out after logic steps.


Monday: Canvas Games

A game is an animation loop plus state (score, lives, level, game over) plus input (keys, mouse, touch). Each frame the loop clears the canvas, reads input, updates every object, checks for collisions, and redraws.

HTML5 Games Introduction

Here is a simple template for Falling Balls with keyboard inputs.


Review: Classes

A class is a blueprint. Write it once, then make as many objects from it as you need: one player, fifty obstacles, a hundred falling stars. Games are full of things that share the same properties and behavior, which is why classes show up in almost every game. The examples here come from Falling Balls.

class FallingObject {
  constructor(x, y, radius, color, speed) {   // runs when a new ball is made
    this.x = x;                                // "this" = the ball being made
    this.y = y;
    this.radius = radius;
    this.color = color;
    this.speed = speed;
  }
 
  draw() {                                     // a method: how the ball looks
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
    ctx.fillStyle = this.color;
    ctx.fill();
  }
 
  update() {                                   // a method: what the ball does each frame
    this.y += this.speed;                      // move down
    this.draw();                               // then draw in the new spot
  }
}

One class, many objects. The demo keeps every ball in an array and adds a new one whenever there are fewer than ten:

let fallingObjects = [];                       // many objects in an array
 
function spawnFallingObject() {
  const radius = 20;
  const x = Math.random() * (canvas.width - radius * 2) + radius;  // random x, fully on the canvas
  const speed = 2;
  fallingObjects.push(new FallingObject(x, 0, radius, 'red', speed));
}

The Player class follows the same pattern, but there is only one, so it is stored in a single variable:

// Player(x, y, width, height, color)
const player = new Player(canvas.width / 2 - 50, canvas.height - 60, 100, 50, 'blue');

Arrow Functions (=>)

An arrow function is a shorter way to write a function. If you see =>, it means “this is a function.” Falling Balls uses both styles: an arrow function in the game loop and regular functions for the keyboard listeners.

// Arrow function (from the game loop)
fallingObjects.forEach(object => {
  object.update();
});
 
// Same thing as a regular function
fallingObjects.forEach(function(object) {
  object.update();
});

forEach hands the function each item in the array, one at a time (object).

Removing Objects from an Array

When a ball is caught or falls off the canvas, the demo marks it with a new property, remove. After the loop, filter() keeps only the balls that are not marked:

fallingObjects.forEach(object => {
  object.update();
  if (/* caught */) {
    score++;
    object.remove = true;     // mark it, don't delete it yet
  }
  if (object.y - object.radius > canvas.height) {
    object.remove = true;     // fell off the bottom
  }
});

fallingObjects = fallingObjects.filter(object => !object.remove);  // keep the rest

Why not delete it inside the loop with splice()? Removing an item shifts every later item down one place, so forEach skips the ball right after the one you removed. Marking during the loop and filtering after avoids that. This is also why the array uses let: filter() makes a new array and stores it back in the same variable.


Breakdown: Dodging Obstacles

Explore the W3Schools HTML5 Games Demo to see how JavaScript builds a game on the Canvas element.

Open Dodging Obstacles, refactored for ES6 classes. Play it, then view the source. The script has seven parts, in this order, plus the controls.

1. Game Variables

let player;
let obstacles = [];
let score = 0;

The game’s state. player is empty until the game starts. obstacles is an array because there will be many pipes. score goes up every frame.

2. The Player Class

class Player {
  constructor(x, y, width, height, color) {
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
    this.color = color;
    this.gravity = 0.05;      // the pull: how much fall speed is added each frame
    this.gravitySpeed = 0;    // the current fall speed, which keeps building
  }

Two gravity properties work together. gravity is the force. gravitySpeed is the result: it grows a little every frame, so the square falls faster and faster, like a real object. This is acceleration.

  update() {
    this.gravitySpeed += this.gravity;   // speed builds up
    this.y += this.gravitySpeed;         // position changes by the speed

    if (this.y + this.height > gameArea.canvas.height) {  // hit the floor
      this.y = gameArea.canvas.height - this.height;
      this.gravitySpeed = 0;
    }
    if (this.y < 0) {                                     // hit the ceiling
      this.y = 0;
      this.gravitySpeed = 0;
    }
  }

The two if statements keep the player on the canvas. When it hits the floor or ceiling, it is placed at the edge and its speed resets to zero.

  collidesWith(obj) {
    return !(
      this.x > obj.x + obj.width ||    // player is fully to the right
      this.x + this.width < obj.x ||   // OR fully to the left
      this.y > obj.y + obj.height ||   // OR fully below
      this.y + this.height < obj.y     // OR fully above
    );
  }
}

This is the easier way to check a collision. It is simpler to describe how two boxes are apart than how they overlap, so the method lists the four ways they can be apart, joined with || (OR). If any one of them is true, the boxes are not touching. The ! (NOT) in front flips the answer: if they are not apart, they must be touching, so collidesWith() returns true.

Falling Balls uses the same method to catch a ball, treating the ball as a square box from x - radius to x + radius. The other way to write it is to check the overlap directly: four conditions joined with && (AND), where all four must be true. The hits() function further down this page works that way. Both methods give the same answer.

3. The Obstacle Class

class Obstacle {
  constructor(x, y, width, height, color) {
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
    this.color = color;
  }

  update() {
    this.x -= 2;   // move left 2 pixels each frame
  }

  draw(ctx) {
    ctx.fillStyle = this.color;
    ctx.fillRect(this.x, this.y, this.width, this.height);
  }
}

Simpler than the player. A pipe only moves left. The player never actually moves forward; the world moves past it.

4. The Game Area Object

const gameArea = {
  canvas: document.getElementById('gameCanvas'),
  context: null,
  frame: 0,
  start() {
    this.context = this.canvas.getContext('2d');
    this.frame = 0;
    this.interval = setInterval(updateGame, 20);
  },
  clear() {
    this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
  },
  stop() {
    clearInterval(this.interval);
  }
};

This is a single object, not a class, because there is only one game area. It holds the canvas, the drawing context, and a frame counter, and it starts and stops the loop.

setInterval(updateGame, 20) calls the game loop every 20 milliseconds (about 50 frames per second). The other way to run a loop is requestAnimationFrame(), shown in the game loop example below. It syncs with the screen and pauses when the tab is hidden.

5. Starting the Game

function startGame() {
  player = new Player(20, 120, 30, 30, "red");
  gameArea.start();
}

The class becomes an object here: a red 30 × 30 square at x = 20, y = 120.

6. Spawning Obstacles

function spawnObstacles() {
  if (gameArea.frame % 120 === 0) {          // every 120 frames
    let canvasH = gameArea.canvas.height;
    let gap = 100;
    let pipeHeight = Math.random() * (canvasH - gap);

    obstacles.push(new Obstacle(gameArea.canvas.width, 0, 20, pipeHeight, "green"));  // top pipe
    obstacles.push(new Obstacle(gameArea.canvas.width, pipeHeight + gap, 20,
                                canvasH - pipeHeight - gap, "green"));              // bottom pipe
  }
}

7. The Game Loop

function updateGame() {
  gameArea.frame++;
  score++;

  obstacles.forEach(function(obs) {           // check collisions
    if (player.collidesWith(obs)) {
      gameArea.stop();
      alert("Game Over! Score: " + score);
    }
  });

  gameArea.clear();                            // clear
  spawnObstacles();                            // add new pipes

  obstacles.forEach(function(o) {              // update and draw pipes
    o.update();
    o.draw(gameArea.context);
  });

  obstacles = obstacles.filter(o => o.x + o.width > 0);  // remove pipes off the left edge

  player.update();                             // update and draw player
  player.draw(gameArea.context);

  gameArea.context.font = "20px Arial";        // draw score
  gameArea.context.fillText(`Score: ${score}`, 360, 30);
}

Every 20 ms: count the frame, check for a hit, clear, spawn, move and draw everything, remove pipes that have left the screen, show the score. The score is just the number of frames survived.

The Controls

function accelerate(force) {
  player.gravity = force;
}

accelBtn.addEventListener("mousedown", function() { accelerate(-0.2); });
accelBtn.addEventListener("mouseup",   function() { accelerate(0.05); });

The button doesn’t move the player directly. It changes the player’s gravity property. Hold it down and gravity becomes negative, so the square accelerates upward. Let go and normal gravity returns. That is why the movement feels floaty: you are changing a force, not a position.


Adding Images and Sound Effects

To add images, load the image into an Image() object, then draw it with drawImage() in place of a shape.

const playerImg = new Image();
playerImg.src = 'player.png';

// inside the player's draw() method
ctx.drawImage(playerImg, this.x, this.y, this.width, this.height);

To add sound effects, load the audio file into an Audio() object and call play() when something happens in the game.

const shootSound = new Audio('shoot.wav');

// when the player shoots
shootSound.currentTime = 0; // rewind so it can retrigger quickly
shootSound.play();

The game with images and sound | Download ZIP


Writing Logic Steps First: Pong

Many games can use canvas and many of these can be generated with AI: Pong | Asteroids — classic games refactored to ES6 classes. This is a good way to learn the fundamentals of game development so that you can build your own game

Before coding a game, describe it in plain language. Here is Pong:

  1. Set up the canvas: Get the canvas element and its drawing context so we can draw paddles and the ball.
  2. Create a Paddle class: Each paddle has a position (x, y), size (width, height), and speed. It responds to player input (W/S, arrow keys) and draws itself each frame.
  3. Create a Ball class: The ball has a position, size, and speed in both X and Y directions. It moves, bounces, detects paddle collisions, and resets after a score.
  4. Track game state: Keep score, listen for key presses, and use booleans to store which keys are held down.
  5. Game loop (updateGame): Clear the canvas, update positions, check for collisions, draw everything, and use requestAnimationFrame() to repeat.

Asteroids Logic Steps

  1. Set up the canvas: Same as Pong.
  2. Create a Ship class: Position, direction, and thrust; moves with physics, rotates, shoots, and wraps around the screen.
  3. Create a Bullet class: Starts at the ship’s position, moves in its direction, and disappears after a while.
  4. Create an Asteroid class: Random positions and velocities; moves, wraps around, and detects bullet hits.
  5. Create a Game class: Manages the ship, asteroids, and input; runs the update/draw loop.

Previous Student Canvas Games

Knight Runner: How a Larger Game Is Organized

Larger games split each class into its own file. Look at how Knight Runner is organized:

Its assets: UI elements from OwlishMedia, background layers from Edermunizz, and sprites from Dumbmanex, Craftpix, OpenGameArt, and Freepik.

HTML5 Game Elements

Game Resources

Game Assets + AI Tools

Check the license for every asset you use and credit it in your project.


Wednesday: APIs with fetch()

An API (Application Programming Interface) lets your page request data from another service: weather, art collections, space images, poems. The data usually comes back as JSON, which JavaScript reads as an object. Live data can then drive what your page shows, how an animation moves, or how a game behaves.

Server Requests

The Fetch API is a built-in browser feature: a set of methods that make network requests like GET (grab data), POST (send data), and PUT / DELETE (update or remove data). For most front-end projects you only need GET.


New Syntax: fetch()

This is the code from the weather app template (two input fields). Open it and follow along.

document.querySelector('#submitWeather').addEventListener('click', function() {
  let city = document.querySelector('#city').value;
  let state = document.querySelector('#state').value;

  if (city !== '' && state !== '') {
    const url = `https://api.openweathermap.org/data/2.5/weather?q=${city},${state},us&appid=${apiKey}&units=imperial`;

    fetch(url)                                   // 1. send the request
      .then(function(response) {                 // 2. when the reply arrives...
        if (response.ok) {
          return response.json();                // 3. turn the JSON text into a JS object
        } else {
          throw new Error('Error fetching data'); // e.g. 404 = city not found
        }
      })
      .then(function(data) {                     // 4. use the data
        console.log(data);
        let weatherDescription = data.weather[0].description;
        let weatherIcon = data.weather[0].icon;
        document.querySelector('#show').innerHTML =
          `${weatherDescription}<br><img src="https://openweathermap.org/img/w/${weatherIcon}.png">`;

        if (weatherDescription.includes('clouds') || weatherDescription.includes('rain')) {
          document.body.style.backgroundColor = 'grey';
        } else if (weatherDescription.includes('sun') || weatherDescription.includes('clear')) {
          document.body.style.backgroundColor = 'lightblue';
        }
      })
      .catch(function(error) {                   // 5. if anything went wrong
        document.querySelector('#error').textContent = error.message;
      });

  } else {
    document.querySelector('#error').textContent = 'Field cannot be empty';
  }
});

JSON Methods

Reading Nested JSON

Use dot notation to reach into the object, and square brackets for arrays. Open the JSON Weather Data and trace each path:

data.name                      // "Vancouver"
data.main.temp                 // 54.3
data.main.humidity             // 81
data.wind.speed                // 6.9
data.clouds.all                // 75   (cloud cover %)
data.weather[0].main           // "Rain"
data.weather[0].description    // "light rain"
data.sys.sunrise / data.sys.sunset / data.dt   // times, for day vs. night

Use includes() to search a description for a word:

if (data.weather[0].description.includes('clouds')) {
  // Do something
}

Demo: Weather App

Use the OpenWeather API to get current weather by city with fetch().

Open and look at the source code of this example: Weather app template (two input fields)

Getting an API Key

  1. Go to the sign-up page for the free version.
  2. Your key will be emailed to you. It can take a little while to activate.
  3. Replace the placeholder key in the template with your own.

Type the state as its two-letter code (WA, not Washington). The template adds us to the request for you, since OpenWeather only accepts a state for US cities.

Frontend Module Project: Weather + Canvas Animation

Turn live weather into motion. Start from a weather app template and add a canvas that animates according to the data.

  1. Get the template working with your own API key. Test a few cities.
  2. Pick two or three data points to drive the animation.
  3. Add a canvas and an animation loop. You can start from a rain or snow animation demo, or a p5.js sketch.
  4. Connect each data point to something visual. Ideas:
    • data.wind.speed → how fast or sideways particles move
    • data.clouds.all → how many clouds, or how gray the sky is
    • data.main.temp → color palette, warm to cool
    • data.weather[0].main → which animation runs: rain, snow, sun, fog
    • data.dt compared to sunrise and sunset → day or night sky
  5. Add a loading message and an error message for a city that isn’t found.
// Example: wind speed (0–30 mph) controls drift, temperature controls color
let drift = (data.wind.speed / 30) * 5;           // 0 to 5 pixels per frame
let warm  = Math.min(Math.max((data.main.temp - 20) / 80, 0), 1); // 0 = cold, 1 = hot
ctx.fillStyle = `rgb(${warm * 255}, 120, ${(1 - warm) * 255})`;

Think about mood, not just accuracy. A weather app can be calm, eerie, playful, or poetic. Other directions: a game that gets harder in bad weather, a soundscape that shifts with conditions, or another API from the list above.

What would I combine this with? Add one sentence to your list.

Weather App Resources

Other APIs to Explore

Demo: API Mashup (Weather + Art)

A mashup combines data from two or more APIs so that each request does something the other can't do alone. The answer from the first API becomes part of the question for the second: today's weather picks the search word, and an art collection answers with an image.

Open Weather + Art. Search a city, then view the source.

.then(function(data) {
  let term = searchTerms[data.weather[0].main] || 'sky';    // weather → search word
  return fetch(`https://api.artic.edu/api/v1/artworks/search?q=${term}`);  // ask the second API
})
.then(function(response) {
  return response.json();                                   // the art reply
})

Other pairings to try: poems from PoetryDB chosen by the weather, NASA images matched to the ISS's current position, or Datamuse rhymes for a word pulled from a weather description. Ask yourself what the first API's answer can mean to the second.