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:
- HTML5 Audio or Video
- Canvas drawing or animation (or SVG)
- p5.js
- An API using
fetch() - Canvas game techniques
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.
classnames the blueprint. By convention class names are capitalized:Player,FallingObject.constructor()runs once, when a new object is made. It sets the starting values.thismeans “this particular object.”this.yis this ball’s y, not every ball’s y.- Properties are the object’s data: position, size, color, speed.
- Methods are functions that belong to the object: how it looks (
draw()) and what it does each frame (update()). newbuilds an object from the class:new FallingObject(x, 0, radius, 'red', speed). The arguments go to the constructor, in order.
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
}
}
%(modulo) gives the remainder after dividing.frame % 120 === 0is true on frames 120, 240, 360… so a new pair of pipes appears every 120 frames. A smaller number means more pipes.- The top pipe gets a random height. The bottom pipe starts 100 pixels below it and fills the rest of the canvas. That leaves a gap of 100 to fly through, at a random height each time.
- Both pipes start at
gameArea.canvas.width, just off the right edge.
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();
- Images load asynchronously. If a sprite doesn’t appear on the first frame, start the game after it loads:
playerImg.onload = () => startGame(); - Browsers block sound until the user interacts, so a Start button or title screen is a good place to begin music.
- Keep assets in an
images/andsounds/folder and use relative paths.
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:
- Set up the canvas: Get the canvas element and its drawing context so we can draw paddles and the ball.
- Create a
Paddleclass: 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. - Create a
Ballclass: 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. - Track game state: Keep score, listen for key presses, and use booleans to store which keys are held down.
- Game loop (
updateGame): Clear the canvas, update positions, check for collisions, draw everything, and userequestAnimationFrame()to repeat.
Asteroids Logic Steps
- Set up the canvas: Same as Pong.
- Create a
Shipclass: Position, direction, and thrust; moves with physics, rotates, shoots, and wraps around the screen. - Create a
Bulletclass: Starts at the ship’s position, moves in its direction, and disappears after a while. - Create an
Asteroidclass: Random positions and velocities; moves, wraps around, and detects bullet hits. - Create a
Gameclass: Manages the ship, asteroids, and input; runs the update/draw loop.
Previous Student Canvas Games
- Escape the Room
- Welcome to Greenhedge
- Game: Inventory
- Knight Runner
- Undead Outpost
- Cowardly Blocks
- Welcome to Kiwi’s Garden!
- Dueling Tanks
Knight Runner: How a Larger Game Is Organized
Larger games split each class into its own file. Look at how Knight Runner is organized:
- main.js — sets up the game and runs the loop
- player.js and playerStates.js — the player and its states (running, jumping, rolling)
- input.js — keyboard handling
- background.js — layered scrolling background
- enemies.js — enemy classes
- UI.js — score and on-screen text
Its assets: UI elements from OwlishMedia, background layers from Edermunizz, and sprites from Dumbmanex, Craftpix, OpenGameArt, and Freepik.
HTML5 Game Elements
Game Resources
- Franks Laboratory — Canvas game tutorials
- html5-games-cheatsheet.pdf
Game Assets + AI Tools
-
Sprites, Backgrounds, Tiles
- Ludo – AI sprite and spritesheet generator
- OpenGameArt – free sprites, textures, music, and sound effects (Creative Commons)
- itch.io Game Assets – free pixel art packs, characters, and UI kits
- CraftPix – free game kits, sprites, backgrounds, GUI elements
- GameArt2D – sprite sheets, tilesets, and backgrounds
-
Sound Effects & Music
- Freesound – user-uploaded sound effects (Creative Commons)
- itch.io Audio Packs – free SFX and music for games
-
AI Image, Sound, and Voice Tools
- ChatGPT Image, Midjourney, Leonardo AI – characters, objects, backgrounds, concept art
- ElevenLabs, PlayHT – voices and narration
- Suno – music tracks and loops
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
- Old way: Ajax — asynchronous JavaScript and XML, used for passing data (GET or POST) without refreshing the page. XML — extensible markup language, once used for storing and exchanging data.
- New way (with ES6): the JavaScript Fetch API along with JSON — JavaScript Object Notation, a format for storing and exchanging data.
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';
}
});
fetch(url)sends the request. The answer doesn't come back right away, so fetch returns a Promise: a placeholder for data that will arrive later. The rest of the page keeps working while it waits..then()runs when the Promise is ready. The first.then()gets the rawresponse. The second gets whatever the first one returned, here the parsed data.response.okistrueif the request worked. If not (a misspelled city, a bad key),throwskips straight to.catch()..catch()handles any error in the chain, including no network.- Template literals (backticks with
${ }) build the request URL from the input values.
JSON Methods
- response.json() — A fetch shortcut that reads the response and parses it into a JS object in one step.
- JSON.parse() — Converts a JSON string into a JS object. Used when you already have a JSON string in hand.
- JSON.stringify() — Converts a JS object into a JSON string. Used when sending data to a server or saving to
localStorage.
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
- Go to the sign-up page for the free version.
- Your key will be emailed to you. It can take a little while to activate.
- 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.
- Get the template working with your own API key. Test a few cities.
- Pick two or three data points to drive the animation.
- Add a canvas and an animation loop. You can start from a rain or snow animation demo, or a p5.js sketch.
- Connect each data point to something visual. Ideas:
data.wind.speed→ how fast or sideways particles movedata.clouds.all→ how many clouds, or how gray the sky isdata.main.temp→ color palette, warm to cooldata.weather[0].main→ which animation runs: rain, snow, sun, fogdata.dtcompared to sunrise and sunset → day or night sky
- 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
- OpenWeather API: Current Weather Data
- JSON Weather Data — an example response
- Weather Conditions — a list of all weather descriptions
- JSON Prettify — make a raw response readable
- Student example: Tiffanyy Shin
- Student example: Von Miller
Other APIs to Explore
- Open-Meteo — weather and forecasts, no key needed
- Art Institute of Chicago API — artworks and images, no key needed
- NASA APIs — Astronomy Picture of the Day and more (free key)
- PoetryDB — poems by author and title, no key needed
- USGS Earthquake Feed — live earthquakes worldwide, no key needed
- Where the ISS at? — real-time space station location, no key needed
- Datamuse — rhymes and related words, no key needed
- PokéAPI — Pokémon stats, types, and images, no key needed
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.
- Request 1: OpenWeather returns the current weather. The page reads
data.weather[0].main(Rain, Clouds, Clear, Snow…). - A lookup object turns that weather type into a search word:
Rain→'rain',Clear→'sunlight'. This is where the creative decisions are. Change the words and you change the mood. - Request 2: the Art Institute of Chicago API searches its collection for that word. The page picks one artwork at random and builds the image URL from its
image_id. - Chaining: returning a
fetch()from inside a.then()makes the next.then()wait for the second reply. One.catch()at the end handles errors from both requests.
.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.