Mr. Penney / TAS1O1 / HTML · CSS · JS · Canvas · Gemini

Canvas and Game Reference

This sheet covers everything the game lessons use (Lessons 19–24), in the order you learn it. Those lessons are extension lessons: they are not taught in class, and this sheet ships with the Final Project for a student who chooses to build a canvas game. The last section is not taught — Level 4 material. Variables, functions, if, loops, arrays and objects are on the JavaScript Reference Sheet.

Taught in class

Setup — Lesson 19

What Example What it does
The canvas tag <canvas id="game" width="800" height="500"></canvas> A drawing area on the page. width and height are the size of the grid in pixels: 800 across, 500 down.
Find it canvas = document.querySelector("#game"); Finds the element with id="game". The # works the same way as in CSS.
Get the drawing tools ctx = canvas.getContext("2d"); ctx is the object every drawing command is called on.

Set width and height in the tag, not in CSS. CSS only stretches the picture; the tag sets how many pixels there are to draw on.

The coordinate grid — Lesson 19

Every position on the canvas is two numbers: x (how far from the left edge) and y (how far from the top edge). (0, 0) is the top-left corner. x grows to the right. y grows downwards — a bigger y is lower on the screen.

(0, 0) ─────────────────────── (800, 0)
  │                               │
  │           x → right           │
  │           y ↓ down            │
  │                               │
(0, 500) ───────────────────── (800, 500)

An object's x and y are its top-left corner. A 40-wide object whose right edge touches the right side of the canvas has x = 800 - 40.

Drawing — Lesson 19

What Example What it does
Pick a colour ctx.fillStyle = "royalblue"; Every fill after this line uses this colour, until you change it. Any CSS colour works: "gold", "#ff0055".
A filled rectangle ctx.fillRect(100, 230, 40, 40); fillRect(x, y, width, height) — top-left corner, then size.
Text ctx.font = "20px sans-serif"; then ctx.fillText("Score: 0", 10, 25); fillText(text, x, y). For text, y is the bottom of the letters, not the top.
Wipe the canvas ctx.clearRect(0, 0, 800, 500); Clears a rectangle — here, the whole canvas. The CSS background colour shows through.

Colour first, then draw. Changing fillStyle does not change anything already on the canvas.

ctx.clearRect(0, 0, 800, 500);
ctx.fillStyle = "gold";
ctx.fillRect(600, 0, 20, 20);
ctx.fillStyle = "white";
ctx.font = "20px sans-serif";
ctx.fillText("Score: 0", 10, 25);

A game object is data — Lesson 19

const player = { x: 100, y: 230, width: 40, height: 40, speed: 10 };

ctx.fillStyle = "royalblue";
ctx.fillRect(player.x, player.y, player.width, player.height);

Everything on the canvas is an object with x, y, width and height. The draw line reads those four parts, so moving the object means changing player.x — never editing the draw line.

const — change the parts, never replace the whole object. player.x = 50; is allowed. player = { x: 50, y: 230, width: 40, height: 40, speed: 10 }; is the error Assignment to constant variable.

The game loop — Lesson 20

Function What it does
update() Changes numbers: positions, score, lives. Draws nothing.
draw() Clears the canvas and redraws everything from the current numbers. Changes no numbers.
gameLoop() Runs update(), then draw(), then asks for the next frame.
requestAnimationFrame(gameLoop) Asks the browser to run gameLoop again just before it paints the next frame — about 60 times a second.
function update() {
  enemy.x = enemy.x - enemy.speed;
  if (enemy.x < -enemy.width) {
    enemy.x = WIDTH;
  }
}

function draw() {
  ctx.clearRect(0, 0, WIDTH, HEIGHT);
  ctx.fillStyle = "crimson";
  ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height);
}

function gameLoop() {
  update();
  draw();
  requestAnimationFrame(gameLoop);
}

gameLoop();   // start it once. Started twice, everything runs at double speed.

One frame is one update() and one draw(). Moving 2 pixels per frame is about 120 pixels a second. The if in update() is wrap-around: an object that leaves one edge comes back at the other.

Keyboard — Lesson 21

document.addEventListener("keydown", function (event) {
  if (event.key.startsWith("Arrow") || event.key === " ") {
    event.preventDefault();
  }
  handleKey(event.key);
});

The listener runs once per key press and hands event.key — the name of the key — to your handleKey function. handleKey is where the ifs go. preventDefault() stops the arrow keys and the space bar from scrolling the page.

Key pressed event.key
Arrow keys "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"
Letters "a", "w", "d", "s", "r" — lowercase unless Shift is held
Space bar " " — one space inside the quotes
Enter "Enter"
function handleKey(key) {
  if (key === "ArrowLeft") {
    player.x = player.x - player.speed;
  }
  if (key === "ArrowRight") {
    player.x = player.x + player.speed;
  }
}

If keys do nothing, click the game once. Key presses go to whatever was clicked last, and after Run that is not always the game.

Random positions — Lesson 21

What Example What it does
A random x that keeps the object on the canvas Math.floor(Math.random() * (WIDTH - player.width)) A whole number from 0 up to WIDTH - width, so the object's right edge never leaves the canvas.
A random y Math.floor(Math.random() * (HEIGHT - enemy.height)) The same, downwards.
coin.x = Math.floor(Math.random() * (WIDTH - coin.width));
coin.y = -coin.height;   // just above the top edge, so it falls into view

Collision — the provided helper — Lesson 21

touching(a, b) answers one question: do these two rectangles overlap? It gives back true or false. Both objects need x, y, width and height.

// ===== PROVIDED HELPER — use it, do not change it =====
// touching(a, b) answers one question: do these two rectangles overlap?
// Both a and b need x, y, width and height. You use it; you never work out the maths.
function touching(a, b) {
  return a.x < b.x + b.width &&
         a.x + a.width > b.x &&
         a.y < b.y + b.height &&
         a.y + a.height > b.y;
}
// ===== END OF PROVIDED HELPER =====

The helper is in your starter file. You call it; you do not write it.

if (touching(player, coin)) {
  game.score = game.score + 1;
  coin.y = -coin.height;                                      // move it away …
  coin.x = Math.floor(Math.random() * (WIDTH - coin.width));  // … or it scores again next frame
}

Collision checks go inside update(), and the consequence must move the thing away. The loop runs 60 times a second, so two objects left overlapping touch 60 times a second.

Game state and rules — Lesson 21

What Example What it does
The state object const game = { score: 0, lives: 3, over: false, message: "" }; Everything the game remembers that is not a position.
The rules checkRules() Reads the state and decides whether the game has ended. Called at the end of update().
A new game newGame() Puts every number back to the start — score, lives, over, message, and every position.
Restart if (game.over && key === "r") { newGame(); } Goes in handleKey.
function checkRules() {
  if (game.score >= 10) {
    game.over = true;
    game.message = "You win! Press r to play again.";
  }
  if (game.lives <= 0) {
    game.over = true;
    game.message = "Game over. Press r to play again.";
  }
}

function newGame() {
  game.score = 0;
  game.lives = 3;
  game.over = false;
  game.message = "";
  player.x = 100;
  player.y = 230;
}

update() starts with if (game.over) { return; }, so nothing moves after the game ends. draw() still runs, so game.message can be shown. handleKey checks game.over first, so the only key that works after the end is r.

All of it together

The three files of a finished Lesson 21 game. script.js is the exact shape of your starter file from Lesson 20 on — the same names, in the same order. Save all three in one folder and it runs.

<!-- index.html -->
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>My Game</title>
    <link rel="stylesheet" href="style.css">
  </head>
  <body>
    <h1>My Game</h1>
    <canvas id="game" width="800" height="500"></canvas>
    <p id="instructions">Arrow keys to move. Collect coins, avoid the enemy.</p>
    <script src="script.js"></script>
  </body>
</html>
// script.js

// ⚙️ Leave this line alone — it lets the auto-checker run your file.
if (typeof prompt === "undefined") { globalThis.prompt = function () { return ""; }; }

// ===== PROVIDED HELPER — use it, do not change it =====
// touching(a, b) answers one question: do these two rectangles overlap?
// Both a and b need x, y, width and height. You use it; you never work out the maths.
function touching(a, b) {
  return a.x < b.x + b.width &&
         a.x + a.width > b.x &&
         a.y < b.y + b.height &&
         a.y + a.height > b.y;
}
// ===== END OF PROVIDED HELPER =====

// ===== THE WORLD — data only. Change the parts, never replace the whole object. =====
const WIDTH = 800;    // the canvas is 800 wide …
const HEIGHT = 500;   // … and 500 tall. (0, 0) is the top-left corner.

const player = { x: 100, y: 230, width: 40, height: 40, speed: 10 };
const coin   = { x: 600, y: 0,   width: 20, height: 20, speed: 3 };
const enemy  = { x: 700, y: 300, width: 40, height: 40, speed: 2 };
const game   = { score: 0, lives: 3, over: false, message: "" };

// Put the world back to the start of a new game.
function newGame() {
  game.score = 0;
  game.lives = 3;
  game.over = false;
  game.message = "";
  player.x = 100;
  player.y = 230;
  coin.x = 600;
  coin.y = 0;
  enemy.x = 700;
  enemy.y = 300;
}

// One key press changes the world. key is event.key: "ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "r" …
function handleKey(key) {
  if (game.over) {
    if (key === "r") {
      newGame();
    }
    return;
  }
  if (key === "ArrowLeft") {
    player.x = player.x - player.speed;
  }
  if (key === "ArrowRight") {
    player.x = player.x + player.speed;
  }
  if (key === "ArrowUp") {
    player.y = player.y - player.speed;
  }
  if (key === "ArrowDown") {
    player.y = player.y + player.speed;
  }
}

// One frame of the world moving. Numbers only — nothing here draws.
function update() {
  if (game.over) {
    return;
  }

  // keep the player on the canvas
  if (player.x < 0) {
    player.x = 0;
  }
  if (player.x > WIDTH - player.width) {
    player.x = WIDTH - player.width;
  }
  if (player.y < 0) {
    player.y = 0;
  }
  if (player.y > HEIGHT - player.height) {
    player.y = HEIGHT - player.height;
  }

  // the coin falls; when it leaves the bottom it comes back at the top at a random x
  coin.y = coin.y + coin.speed;
  if (coin.y > HEIGHT) {
    coin.y = -coin.height;
    coin.x = Math.floor(Math.random() * (WIDTH - coin.width));
  }

  // the enemy moves left; when it leaves the left edge it comes back on the right
  enemy.x = enemy.x - enemy.speed;
  if (enemy.x < -enemy.width) {
    enemy.x = WIDTH;
    enemy.y = Math.floor(Math.random() * (HEIGHT - enemy.height));
  }

  // collisions: condition → consequence
  if (touching(player, coin)) {
    game.score = game.score + 1;
    coin.y = -coin.height;
    coin.x = Math.floor(Math.random() * (WIDTH - coin.width));
  }
  if (touching(player, enemy)) {
    game.lives = game.lives - 1;
    enemy.x = WIDTH;            // move it away, or it would hit again next frame
  }

  checkRules();
}

// The rules of the game: what ends it.
function checkRules() {
  if (game.score >= 10) {
    game.over = true;
    game.message = "You win! Press r to play again.";
  }
  if (game.lives <= 0) {
    game.over = true;
    game.message = "Game over. Press r to play again.";
  }
}

// ===== DRAWING — the only code that touches ctx =====
let canvas;
let ctx;

function draw() {
  ctx.clearRect(0, 0, WIDTH, HEIGHT);

  ctx.fillStyle = "gold";
  ctx.fillRect(coin.x, coin.y, coin.width, coin.height);

  ctx.fillStyle = "crimson";
  ctx.fillRect(enemy.x, enemy.y, enemy.width, enemy.height);

  ctx.fillStyle = "royalblue";
  ctx.fillRect(player.x, player.y, player.width, player.height);

  ctx.fillStyle = "white";
  ctx.font = "20px sans-serif";
  ctx.fillText("Score: " + game.score, 10, 25);
  ctx.fillText("Lives: " + game.lives, 10, 50);

  if (game.over) {
    ctx.font = "36px sans-serif";
    ctx.fillText(game.message, 200, 250);
  }
}

// update → draw → update → draw …  requestAnimationFrame asks the browser to run gameLoop again
// before it paints the next frame (about 60 times a second).
function gameLoop() {
  update();
  draw();
  requestAnimationFrame(gameLoop);
}

// ===== THE BROWSER BLOCK — runs only on the page =====
if (typeof document !== "undefined") {
  canvas = document.querySelector("#game");
  ctx = canvas.getContext("2d");

  document.addEventListener("keydown", function (event) {
    if (event.key.startsWith("Arrow") || event.key === " ") {
      event.preventDefault();
    }
    handleKey(event.key);
  });

  newGame();
  canvas.tabIndex = 0;   // so the game can take keyboard focus …
  canvas.focus();        // … and has it as soon as the page loads
  gameLoop();
}
/* style.css */
body {
  margin: 0;
  padding: 16px;
  font-family: system-ui, sans-serif;
  background: #0f172a;
  color: #e2e8f0;
  text-align: center;
}

canvas {
  background: #1e1e2e;   /* the board colour. clearRect leaves this showing, not white */
  display: block;
  margin: 0 auto;
  border: 2px solid #ccc;
}

Read script.js top to bottom. It begins with the ⚙️ line, then the provided helper, then the world data, then newGame, handleKey, update, checkRules, draw and gameLoop, and last the browser block. Everything above draw() runs in the checker with no page at all. draw() and the browser block are the only code that needs one.

Not taught in class — Level 4 material

Level 4 tasks expect you to find what you need here and work out how to use it.

Circles — arc()

Code What it does
ctx.beginPath(); Starts a new shape. Without it, every circle joins on to the one before.
ctx.arc(x, y, radius, 0, Math.PI * 2); A circle: centre (x, y), then the radius. 0 to Math.PI * 2 means the whole way round.
ctx.fill(); Fills the shape with the current fillStyle.
ctx.fillStyle = "orange";
ctx.beginPath();
ctx.arc(ball.x + ball.width / 2, ball.y + ball.height / 2, ball.width / 2, 0, Math.PI * 2);
ctx.fill();

arc takes the centre, not the top-left corner. Keep the object's x, y, width and height as they are — touching() still needs them — and add half the width to find the centre.

A bouncing object — dx and dy

Code What it does
const ball = { x: 400, y: 250, width: 20, height: 20, dx: 3, dy: 2 }; dx is how far x changes each frame; dy the same for y. Negative means left or up.
ball.x = ball.x + ball.dx; Moves the ball. Goes in update().
ball.dx = -ball.dx; Flips the direction. Do it when the ball reaches a side.
function update() {
  ball.x = ball.x + ball.dx;
  ball.y = ball.y + ball.dy;
  if (ball.x < 0 || ball.x > WIDTH - ball.width) {
    ball.dx = -ball.dx;
  }
  if (ball.y < 0 || ball.y > HEIGHT - ball.height) {
    ball.dy = -ball.dy;
  }
}

|| means "or": either edge counts. Bouncing is the opposite of wrap-around — the object turns round instead of coming back on the other side.

Pictures instead of rectangles — drawImage

Code What it does
playerImage = new Image(); then playerImage.src = "images/ship.png"; Loads a picture file from your project folder.
ctx.drawImage(playerImage, player.x, player.y, player.width, player.height); Draws it in place of fillRect, stretched to the object's size. Goes inside draw().
let playerImage;   // beside let ctx;

// inside the browser block — Image only exists on a page
playerImage = new Image();
playerImage.src = "images/ship.png";

The picture loads in the background, so the first frame or two may be blank where it should be. That is normal: draw() runs 60 times a second and the picture appears as soon as it has loaded.

A timer — game.time

const game = { score: 0, lives: 3, over: false, message: "", time: 0 };

// in update():
game.time = game.time + 1;   // one more frame

// in draw():
ctx.fillText("Time: " + Math.ceil(game.time / 60), 10, 75);   // 60 frames ≈ 1 second

game.time counts frames. There are about 60 in a second, so Math.ceil(game.time / 60) is the time in whole seconds, rounded up. For a countdown, start it at 60 * 30 (30 seconds) and subtract 1 per frame; checkRules() ends the game when it reaches 0. Reset it in newGame().

Projectiles — a bullets array

Code What it does
const bullets = []; Starts empty. Every bullet is an object with x, y, width, height and speed.
bullets.push({ x: player.x, y: player.y, width: 6, height: 12, speed: 8 }); In handleKey, when key === " ". A new bullet starts where the player is.
bullets.splice(i, 1); Removes the bullet at position i — one that left the canvas or hit something.
bullets.length = 0; Empties the array without replacing it. Goes in newGame().
// in update(): move every bullet, then drop the ones that left the canvas
for (let i = bullets.length - 1; i >= 0; i = i - 1) {
  bullets[i].y = bullets[i].y - bullets[i].speed;
  if (bullets[i].y < -bullets[i].height) {
    bullets.splice(i, 1);
  }
}

The loop counts down. Removing an item shifts every item after it one place left, so a loop counting up would skip the next one. draw() needs its own for loop that draws each bullet with fillRect.

Levels — rising difficulty

const game = { score: 0, lives: 3, over: false, message: "", level: 1 };

// in checkRules():
if (game.score >= game.level * 5) {
  game.level = game.level + 1;
}

// in update():
enemy.x = enemy.x - enemy.speed * game.level;   // faster every level

Multiplying a speed by game.level makes everything faster as the level rises. newGame() sets game.level = 1;.

Many enemies — spawnEnemies(n)

const enemies = [];

function spawnEnemies(n) {
  enemies.length = 0;
  for (let i = 0; i < n; i = i + 1) {
    enemies.push({ x: WIDTH + i * 150, y: Math.floor(Math.random() * (HEIGHT - 40)), width: 40, height: 40, speed: 2 });
  }
}

Call spawnEnemies(3) from newGame(). update() and draw() each get a for loop over enemies, and the collision check becomes if (touching(player, enemies[i])) inside the update() loop.

Kinds of item

const items = [
  { x: 600, y: 0, width: 20, height: 20, speed: 3, kind: "coin" },
  { x: 300, y: 0, width: 20, height: 20, speed: 5, kind: "bomb" },
];

// in update(), inside the loop over items:
if (touching(player, items[i])) {
  if (items[i].kind === "coin") {
    game.score = game.score + 1;
  }
  if (items[i].kind === "bomb") {
    game.lives = game.lives - 1;
  }
  items[i].y = -items[i].height;
}

One array, one loop and one collision check are enough, because kind decides what happens. draw() can choose the colour by kind the same way.