Phase 3 · Canvas
HU EN
Phase 3 · Lesson 1

Canvas basics — the first line

The Canvas is an HTML element that we draw onto using JavaScript. All our games draw the track, characters and enemies onto this "drawing board" — frame by frame.

⏱ 60 min
📁 Canvas 2D
🎯 fillRect · strokeRect · clearRect

1Creating the canvas and getting the context

The canvas itself is just an empty rectangle in HTML. Drawing is only possible through the 2D context (ctx) — this is the "pencil" we work with.

canvas_basic.htmlHTML + JS
<!-- HTML: the canvas element -->
<canvas id="gc" width="540" height="580"></canvas>

// JavaScript: getting the drawing tool
const CV  = document.getElementById('gc');
const ctx = CV.getContext('2d');

// Store width and height in variables
const W = CV.width;   // 540
const H = CV.height;  // 580

The first drawings — rectangles

rectangles.jsJavaScript
// Filled rectangle: fillRect(x, y, width, height)
ctx.fillStyle = '#4aaa22';     // set colour
ctx.fillRect(0, 0, W, H);    // fill entire canvas green

// Outlined rectangle: strokeRect
ctx.strokeStyle = '#fff';
ctx.lineWidth   = 2;
ctx.strokeRect(10, 10, 100, 60);

// Clears an area (makes it transparent)
ctx.clearRect(0, 0, W, H);   // called at the start of every frame
The coordinate system

The top-left corner of the canvas is the (0, 0) point. X increases to the right, Y increases downward — this is the opposite of mathematical coordinates! So (0, 580) is the bottom-left corner.

3 examples from our games

🏰 Castle Siege

Drawing the background and road with fillRect:

castle_siege.html
// Dark background for the whole canvas
ctx.fillStyle = '#1a1208';
ctx.fillRect(0, UIH, W, GH);

// The path — grey rectangle
ctx.fillStyle = '#3a3028';
ctx.fillRect(PATH_X, UIH, PATH_W, GH);

// Castle base area
ctx.fillStyle = '#2a1808';
ctx.fillRect(0, UIH, 80, GH);
🐸 Froggy Rush

Drawing lane rows (road, river, grass):

froggy_rush.html
// One fillRect per row
for (let r = 0; r < ROWS; r++) {
  const y = rowY(r);
  ctx.fillStyle = ROW_COLORS[r];
  ctx.fillRect(0, y, W, ROW_H);
}

// Side grass area
ctx.fillStyle = '#2d7a10';
ctx.fillRect(0, 0, TRACK_LEFT, H);
🐢 Turtle Race

Drawing tree trunks at the track edges:

turtle_race.html
// Tree trunk — brown line
ctx.strokeStyle = '#8B6040';
ctx.lineWidth   = 3;
ctx.beginPath();
ctx.moveTo(0, 10);
ctx.lineTo(0, -18);
ctx.stroke();

// Side green strips (track edge grass)
ctx.fillStyle = '#2a6a10';
ctx.fillRect(0, 0, TL, H);
ctx.fillRect(TR, 0, W-TR, H);
✏️ Lesson 1 Task

Create an HTML file with a canvas and draw on it:

  • Green background for the whole canvas (fillRect)
  • Grey road running across the centre (fillRect, horizontal)
  • White border around it (strokeRect)
  • Clear the top third of the road (clearRect) — what do you see?

🧠 Where is the origin (0,0) of the canvas coordinate system?

Bottom-left corner
Centre point
Top-left corner
Top-right corner
Phase 3 · Lesson 2

Circles, Arcs, Lines

The turtle's shell, eggs, rocks, seagulls — they're all built from circles and arcs. With the Path system we can draw any shape.

⏱ 75 min
🎯 arc · beginPath · ellipse
🎮 Egg · Rock · Seagull

1The Path system

To draw circles and complex shapes, we first describe a path, then fill or outline it.

path_basics.jsJavaScript
// Circle: arc(x, y, radius, startAngle, endAngle)
ctx.beginPath();                    // always start with this
ctx.arc(100, 100, 30, 0, Math.PI * 2); // full circle
ctx.fillStyle = '#3ddc84';
ctx.fill();                          // fills it
ctx.stroke();                        // draws outline

// Ellipse: ellipse(x, y, rx, ry, angle, start, end)
ctx.beginPath();
ctx.ellipse(200, 100, 40, 25, 0, 0, Math.PI*2);
ctx.fill();

// Line: moveTo + lineTo
ctx.beginPath();
ctx.moveTo(0, 50);   // lift pen and move here
ctx.lineTo(200, 50); // draw line to here
ctx.stroke();

// Closed shape: closePath
ctx.beginPath();
ctx.moveTo(100, 0);
ctx.lineTo(200, 100);
ctx.lineTo(0, 100);
ctx.closePath();    // connects back to start → triangle
ctx.fill();

3 examples from our games

🏰 Castle Siege

Drawing projectile and enemy circles:

castle_siege.html
// Enemy: filled circle + outline
ctx.beginPath();
ctx.arc(e.x, e.y, e.r, 0, Math.PI*2);
ctx.fillStyle = e.col;
ctx.fill();
ctx.strokeStyle = 'rgba(0,0,0,.3)';
ctx.lineWidth = 2;
ctx.stroke();

// Hero projectile: small glowing circle
ctx.beginPath();
ctx.arc(p.x, p.y, 4, 0, Math.PI*2);
ctx.fillStyle = '#00d4ff';
ctx.fill();
🐸 Froggy Rush

Drawing an egg as an ellipse:

froggy_rush.html
// Egg = elongated ellipse
const g = ctx.createRadialGradient(
  x-2, y-3, 1, x, y, 9
);
g.addColorStop(0, '#fffef5');
g.addColorStop(1, '#e8d080');
ctx.fillStyle = g;
ctx.beginPath();
ctx.ellipse(x, y, 7, 9, 0, 0, Math.PI*2);
ctx.fill();
🐢 Turtle Race

Rock's irregular shape using moveTo + lineTo:

turtle_race.html
// Rock = polygon (not a circle — it's jagged)
ctx.beginPath();
ctx.moveTo(-9, 6);
ctx.lineTo(-12, -2);
ctx.lineTo(-6, -10);
ctx.lineTo(5, -12);
ctx.lineTo(13, -4);
ctx.lineTo(11, 6);
ctx.closePath();     // close shape
ctx.fill();
beginPath() — why is it needed?

Without it, the previous path stays in memory. Draw one circle, then another — the old one also gets re-filled, causing a double outline. Always start a new shape with beginPath()!

✏️ Lesson 2 Task

Draw simple shapes on the canvas:

  • Draw a full circle (arc, 0 to Math.PI*2)
  • Draw a semicircle (arc, 0 to Math.PI)
  • Draw an egg (ellipse, rx≠ry)
  • Draw a triangle (moveTo + lineTo + lineTo + closePath + fill)
  • Bonus: using the Froggy Rush egg code as reference, draw an egg with a radial gradient

🧠 What happens if we don't call beginPath() between two arc() calls?

An error is thrown
The previous and new shapes are joined together — both get re-filled
Nothing is visible
Only the new shape is drawn
Phase 3 · Lesson 3

Colours, Transparency, Gradients

The depth of the turtle's shell, the castle's shadows, the shiny surface of an egg — all done with gradients. This is what gives our games their visual depth.

⏱ 60 min
🎯 rgba · globalAlpha · createLinearGradient
🎮 Shell · Shadow · Egg

1Ways to specify colour

colours.jsJavaScript
// Colour as a string (CSS colours)
ctx.fillStyle = 'red';
ctx.fillStyle = '#4aaa22';       // hex
ctx.fillStyle = '#4aaa22aa';     // hex + alpha

// rgba — red, green, blue + transparency (0=transparent, 1=solid)
ctx.fillStyle = 'rgba(255, 100, 0, 0.6)';  // 60% orange

// Linear gradient
const g = ctx.createLinearGradient(x1, y1, x2, y2);
g.addColorStop(0,   '#ff0000'); // red at start
g.addColorStop(0.5, '#ffff00'); // yellow at middle
g.addColorStop(1,   '#00ff00'); // green at end
ctx.fillStyle = g;

// Radial gradient (inside out)
const rg = ctx.createRadialGradient(cx, cy, r1, cx, cy, r2);
rg.addColorStop(0, '#ffffff'); // white at centre
rg.addColorStop(1, '#1a5a10'); // dark at edge
ctx.fillStyle = rg;

// Global transparency — affects everything
ctx.globalAlpha = 0.5;  // 50% transparent for all drawing
ctx.fillRect(0, 0, W, H);
ctx.globalAlpha = 1;    // reset it!

3 examples from our games

🏰 Castle Siege

HP bar gradient — green to red:

castle_siege.html
// HP colour based on current value
const pct = G.castle.hp / G.castle.maxHp;
let hpCol;
if (pct > 0.6)      hpCol = '#3a8a3a';
else if (pct > 0.3) hpCol = '#9a8a20';
else               hpCol = '#8a2020';

// Bar background
ctx.fillStyle = 'rgba(0,0,0,.5)';
ctx.fillRect(bx, by, bw, bh);
// Filled portion
ctx.fillStyle = hpCol;
ctx.fillRect(bx, by, bw*pct, bh);
🐢 Turtle Race

Turtle shell with radial gradient:

turtle_race.html
// Radial gradient: light inside, dark outside
const g2 = ctx.createRadialGradient(
  -3, -4, 2,  // inner circle: offset (light effect)
   0,  1, 14  // outer circle
);
g2.addColorStop(0, '#3a9a28'); // light green inside
g2.addColorStop(1, '#1a5a10'); // dark green outside
ctx.fillStyle = g2;
ctx.ellipse(0, 1, 14, 11, 0, 0, Math.PI*2);
ctx.fill();
🐸 Froggy Rush

Boost pad pulsing animation with globalAlpha:

froggy_rush.html
// oscillates 0–1 based on frame
const pulse = (Math.sin(frame * .1) + 1) * .5;

// globalAlpha: pulsing transparency
ctx.globalAlpha = 0.75 + pulse * .25;
ctx.fillStyle = `rgba(255,180,0,${.35+pulse*.3})`;
ctx.beginPath();
ctx.arc(0, 0, 13, 0, Math.PI*2);
ctx.fill();
ctx.globalAlpha = 1;  // ALWAYS reset!
Warning — always reset globalAlpha

If you set globalAlpha and forget to reset it to 1, everything drawn after it will also be transparent! Always add: ctx.globalAlpha = 1; at the end.

✏️ Lesson 3 Task

Experiment with colours and gradients:

  • Draw 3 overlapping circles with different rgba transparencies (0.2, 0.5, 0.9)
  • Create a linear gradient from left to right (blue → green) and fill a rectangle with it
  • Create a radial gradient and draw a "shiny ball" with it
  • Draw an HP bar: background (grey), filled portion (green), then reduce it to 50%

🧠 What does rgba(255, 0, 0, 0.5) mean?

50% white colour
50% transparent red
Fully transparent red
An RGB code where the 4th number is blue intensity
Phase 3 · Lesson 4

The Game Loop — requestAnimationFrame

The game loop is the heart of every game. It runs 60 times per second, updates the game state and redraws the picture. Without it the game would be just a snapshot.

⏱ 75 min
🎯 requestAnimationFrame · update · draw
🎮 Every game's loop

1The loop structure

game_loop.js — the basic structureJavaScript
// The game state
let x = 100;   // circle's horizontal position
let vx = 2;    // speed (pixels/frame)

// 1. UPDATE — refreshes positions, physics
function update() {
  x += vx;                    // moves
  if (x > W || x < 0) vx *= -1; // bounces off wall
}

// 2. DRAW — renders the current state
function draw() {
  ctx.clearRect(0, 0, W, H); // clear
  ctx.beginPath();
  ctx.arc(x, 100, 20, 0, Math.PI*2);
  ctx.fillStyle = '#3ddc84';
  ctx.fill();
}

// 3. LOOP — calls itself again and again
function loop() {
  update();
  draw();
  requestAnimationFrame(loop); // ~60x/second
}

loop(); // starts it
Why requestAnimationFrame and not setInterval?

setInterval runs at fixed intervals even when the tab is in the background — wasting battery. requestAnimationFrame only runs when the browser is actively drawing — it pauses automatically in the background and synchronises with the monitor's refresh rate (usually 60fps).

3 examples from our games

🏰 Castle Siege

Castle Siege's complete loop:

castle_siege.html
function loop() {
  update(); // moves hero, enemies
  draw();   // renders everything
  requestAnimationFrame(loop);
}

function update() {
  // Only when actively playing
  if (!G || G.phase !== 'play') return;
  G.frame++;
  moveHero(G.hero);
  moveEnemies(G.enemies);
  moveProjectiles(G.hProj);
  checkCollisions();
}
🐸 Froggy Rush

Frame counter and phase-based update:

froggy_rush.html
function update() {
  if (!G) return;
  G.frame++;  // 0, 1, 2, 3... increases forever

  if (G.phase === 'play')    updatePlay();
  if (G.phase === 'dead')    updateDead();
  if (G.phase === 'levelup') updateLevel();
}

function loop() {
  update(); draw();
  requestAnimationFrame(loop);
}
loop();
🐢 Turtle Race

Using the frame number for sine animation:

turtle_race.html
function loop() {
  update(); draw();
  requestAnimationFrame(loop);
}

function draw() {
  // G.frame keeps increasing
  // sin(frame * 0.2) → wave animation
  const walk = Math.sin(G.frame * .2) * 2;
  // turtle legs move based on walk
  drawTurtle(G.player.x, PLAYER_Y,
             G.player, G.frame);
}
✏️ Lesson 4 Task — First animation

Build your first animation:

  • A circle moves left-right and bounces off the walls
  • Add vertical movement too (up-down bounce)
  • The circle leaves a faint trail (don't clearRect fully)
  • Bonus: pause/resume with the spacebar

🧠 Why is requestAnimationFrame better than setInterval for games?

Because it's faster — always runs at 120fps
It synchronises with the monitor refresh rate, and pauses when the tab is in the background
Because it doesn't need to be called manually
It's only for animation; setInterval is correct for games
Phase 3 · Lesson 5

Coordinate System and Transformations

The turtle tilts in the direction it's moving, the tree is built from rotations, shadows appear offset. All of this is achieved with save/restore and translate/rotate.

⏱ 75 min
🎯 save · restore · translate · rotate · scale
🎮 Turtle tilt · Tree drawing

1save() and restore() — saving the context

transformation.jsJavaScript
// Without save/restore: one element's transform
// affects everything that comes after!

// CORRECT approach:
ctx.save();              // saves: colour, transform, etc.

  ctx.translate(100, 200);  // shift the origin
  ctx.rotate(0.5);          // 0.5 radians = ~28.6 degrees
  ctx.scale(1.5, 1.5);      // 1.5x zoom

  // All drawing is relative to NEW origin:
  ctx.fillStyle = '#3ddc84';
  ctx.fillRect(-25, -25, 50, 50); // around origin

ctx.restore();           // restores the original state
// Everything is back to normal from here

3 examples from our games

🐢 Turtle Race

Turtle tilt based on direction (vx):

turtle_race.html
function drawTurtle(x, y, t) {
  ctx.save();
  ctx.translate(x, y);  // origin to turtle centre

  // Tilt based on vx: moving right → tilts right
  const tilt = Math.max(-0.28,
    Math.min(0.28, t.vx * .075)
  );
  ctx.rotate(tilt);

  // Drawing around (0,0) → from turtle's centre
  drawShell();
  drawHead();
  drawLegs();

  ctx.restore();
}
🐸 Froggy Rush

Drawing the seagull with translate to its own origin:

froggy_rush.html
function drawGull(x, y, frame) {
  const flap = Math.sin(frame * .2) * 7;

  ctx.save();
  ctx.translate(x, y);  // to seagull's centre

  // Body, wings, head around (0,0)
  ctx.fillStyle = '#fff';
  ctx.beginPath();
  ctx.ellipse(0, 0, 9, 5, 0, 0, Math.PI*2);
  ctx.fill();
  // Wings move based on flap
  ctx.ellipse(-12, flap, 10, 3, 0, 0, Math.PI*2);
  ctx.fill();

  ctx.restore();
}
🐢 Turtle Race

Drawing trees — always around their own origin:

turtle_race.html
function drawTree(x, y) {
  ctx.save();
  ctx.translate(x, y);  // origin to tree base

  // Trunk — upward (negative Y)
  ctx.strokeStyle = '#8B6040';
  ctx.lineWidth = 2.5;
  ctx.beginPath();
  ctx.moveTo(0, 10);
  ctx.lineTo(0, -18);
  ctx.stroke();

  // Foliage — at the top of the trunk
  ctx.fillStyle = '#2a8010';
  ctx.beginPath();
  ctx.arc(0, -22, 12, 0, Math.PI*2);
  ctx.fill();

  ctx.restore();
}
Rule: every character drawing goes between save/restore

Every complex drawing function (drawTurtle, drawGull, drawTree) must be wrapped in ctx.save() and ctx.restore(), and positioned with ctx.translate(x, y). This way the drawing function always works around (0,0) and doesn't need to know where the character is on the canvas.

✏️ Lesson 5 Task

Practise transformations:

  • Draw a square and rotate it 45 degrees (Math.PI/4 radians)
  • Write a drawCar(x, y) function with save/translate/restore, call it 3 times at different positions
  • Animation: a rotating arm — save → translate to pivot position → rotate(frame*0.05) → lineTo → restore

🧠 Why do we draw around (0,0) in drawTurtle, when the turtle might be at (170, 400)?

Because the canvas origin is always (0,0)
Because ctx.translate(170, 400) moves the origin — after that, (0,0) is the turtle's centre, so rotate also happens at the right point
Because coordinates are relative to the previous element
Not true — the turtle is drawn with fillRect(170,400,...)
Phase 3 · Lesson 6

Drawing Text on the Canvas

The score, "GAME OVER", the floating "+10" messages, the level number — all are put on screen with ctx.fillText().

⏱ 45 min
🎯 fillText · font · textAlign · measureText
🎮 HUD · Score · Floating text

1Text basics

text_canvas.jsJavaScript
// Font: "weight size family"
ctx.font = 'bold 20px Fredoka One, sans-serif';

// Horizontal alignment: 'left', 'center', 'right'
ctx.textAlign = 'center';

// Vertical alignment: 'top', 'middle', 'bottom'
ctx.textBaseline = 'middle';

// Drawing: fillText(text, x, y)
ctx.fillStyle = '#ffe44d';
ctx.fillText('Score: 1500', W/2, 30);

// Outlined text (used rarely)
ctx.strokeStyle = '#000';
ctx.strokeText('GAME OVER', W/2, H/2);

// Text width (e.g. for centering calculations)
const w = ctx.measureText('Hello').width;

3 examples from our games

🏰 Castle Siege

Floating text (+gold, -HP) animation:

castle_siege.html
// floats array: {x, y, txt, col, life, vy}
G.floats.forEach(f => {
  // Transparency fades as it disappears
  ctx.globalAlpha = f.life / 95;
  ctx.fillStyle = f.col;
  ctx.font = 'bold 13px Nunito,sans-serif';
  ctx.textAlign = 'center';
  ctx.fillText(f.txt, f.x, f.y);

  f.y  += f.vy;   // floats upward
  f.life--;        // fades out
});
ctx.globalAlpha = 1;
🐸 Froggy Rush

Game Over / win overlay text:

froggy_rush.html
// Dark overlay background
ctx.fillStyle = 'rgba(0,0,0,.6)';
ctx.fillRect(0, 0, W, H);

// Large title in the centre
ctx.fillStyle = '#ffe44d';
ctx.font = 'bold 38px Fredoka One';
ctx.textAlign = 'center';
ctx.fillText('🏆 YOU WIN!', W/2, H/2-18);

// Smaller subtitle below
ctx.fillStyle = '#fff';
ctx.font = '16px Nunito';
ctx.fillText('Score: ' + G.score, W/2, H/2+16);
🐢 Turtle Race

Turtle name and egg count label:

turtle_race.html
ctx.save();
ctx.translate(x, y);

// Label above the turtle
ctx.fillStyle = 'rgba(0,80,0,.8)';
ctx.font = 'bold 9px Nunito';
ctx.textAlign = 'center';
ctx.fillText('🐢 You', 0, -28);

// Egg count above if any
if (t.eggs > 0) {
  ctx.fillStyle = 'rgba(255,200,0,.9)';
  ctx.fillText('🥚×' + t.eggs, 0, -39);
}
ctx.restore();
✏️ Lesson 6 Task

Text on the canvas:

  • Write a centred "GAME" label at the top of the canvas (textAlign: center)
  • Score text in the bottom-right corner (textAlign: right, textBaseline: bottom)
  • Animation: text floats upward and fades out (globalAlpha decreasing)
  • Bonus: Game Over overlay — dark background + large text + smaller subtitle

🧠 Which textAlign value means the X coordinate marks the centre of the text?

left
right
center
middle
Phase 3 · Lesson 7

Collision Detection

When the frog reaches an egg, it disappears. When an enemy reaches the castle, HP is lost. Both are collision detection — with two methods: circle and rectangle.

⏱ 75 min
🎯 Math.hypot · AABB · overlap
🎮 Egg collecting · Tower range

1For circles: Math.hypot distance

collision.jsJavaScript
// Two circles collide when the distance between them
// is less than the sum of their radii
function circleHit(ax, ay, ar, bx, by, br) {
  const dist = Math.hypot(bx-ax, by-ay);
  return dist < ar + br;
}

// Usage:
if (circleHit(frog.x, frog.y, 13,
              egg.x,  egg.y,  9)) {
  egg.collected = true;
  score += 10;
}

// Rectangle collision (AABB - Axis-Aligned Bounding Box)
function rectHit(ax, ay, aw, ah, bx, by, bw, bh) {
  return ax < bx+bw && ax+aw > bx &&
         ay < by+bh && ay+ah > by;
}

3 examples from our games

🏰 Castle Siege

Tower range check — is an enemy in range?

castle_siege.html
// Tower projectiles only hit enemies
// WITHIN RANGE
G.enemies.forEach(enemy => {
  const d = Math.hypot(
    enemy.x - tower.x,
    enemy.y - tower.y
  );
  if (d < tower.range) {
    // Hit!
    enemy.hp -= tower.dmg;
  }
});
🐸 Froggy Rush

Egg and seagull collision with frog circle:

froggy_rush.html
G.objects.forEach(o => {
  if (o.done) return;
  const sy = o.y + G.scrollY;
  // Math.hypot: distance between frog and object
  const d = Math.hypot(
    p.x - o.x,
    (PLAYER_Y - p.jumpH) - sy
  );
  if (o.type === 'egg' && d < o.r + 13) {
    o.done = true;
    p.eggs++;
  }
});
🐢 Turtle Race

Rock collision — no collision while jumping:

turtle_race.html
// Rock collision: only when NOT jumping
if (o.type === 'rock' &&
    !p.jumping &&
    !p.stunned) {
  const pd = Math.hypot(
    p.x - o.x,
    (PLAYER_Y - p.jumpH) - sy
  );
  if (pd < o.r + 11) {
    loseLife(p);  // collision!
  }
}
Why Math.hypot and not our own formula?

Math.hypot(dx, dy) is the same as Math.sqrt(dx*dx + dy*dy) — but shorter and you don't have to write sqrt. Pythagoras' theorem: a² + b² = c², where c is the distance between two points.

✏️ Lesson 7 Task

Practise collision detection:

  • Draw 2 circles, and print a message when they collide (circleHit function)
  • Moving circle + stationary target: indicate collision visually (colour change)
  • Write a rectHit function and test it with 2 overlapping rectangles
  • Bonus: collecting game — a controllable circle collects eggs (circle collision)

🧠 When do two circles collide?

When their centres are at the same point
When their X coordinates are equal
When the distance between them is less than the sum of their radii
When their areas overlap
Phase 3 · Lesson 8

Scroll and Camera Follow

In Turtle Race the track is longer than the screen — the camera scrolls. In Froggy Rush the track scrolls "upward". Both are based on the same principle.

⏱ 75 min
🎯 scrollY · world-space · screen-space
🎮 Turtle Race · Froggy Rush

1World-space vs Screen-space

scroll_system.js — the basic principleJavaScript
// scrollY: how much we've scrolled (increases as we move)
let scrollY = 0;

// Object has a WORLD position (fixed)
const egg = { x: 200, y: -800 }; // a point on the track

// SCREEN position = world + scroll (where it appears on screen)
const screenY = egg.y + scrollY;

// Object is on screen if screenY is in [0, H]
if (screenY > -30 && screenY < H+30) {
  ctx.fillRect(egg.x, screenY, 14, 18); // draw it
}

// In update: scrollY increases → objects move upward
scrollY += scrollSpd; // e.g. 2.2 px/frame

// The player stays in place (PLAYER_Y is a fixed position)
// This creates the feeling of moving forward!

3 examples from our games

🐢 Turtle Race

Objects are fixed in world-space, only scrollY moves:

turtle_race.html
// Spawn: world Y = PLAYER_Y - scrollAtPlayer
// At that moment the player is standing on it
const y = PLAYER_Y - S_start;

// In draw: screen_y = o.y + scrollY
G.objects.forEach(o => {
  const sy = o.y + G.scrollY;
  if (sy < -40 || sy > H+40) return;
  drawEgg(o.x, sy);
});

// In update: only scrollY increases, objects DON'T move
G.scrollY += scrollSpd;
🐸 Froggy Rush

Lane row Y position based on scroll:

froggy_rush.html
// Row (0–12) Y position
// Fixed: a row's position on screen doesn't change
function rowY(row) {
  return H - ROW_H * (row + 1);
}
// Frog "moves up" = objects
"scroll down" (scrollY increases)
const sy = o.y + G.scrollY; // screen position
// Collision must also account for scrollY
const d = Math.hypot(p.x-o.x, PLAYER_Y-sy);
💡 Custom example

Simple side-scrolling camera follow:

camera.js
// Camera = player's X minus screen centre
let camX = 0;

function update() {
  player.x += player.vx;
  // Camera smoothly follows the player
  camX += (player.x - W/2 - camX) * 0.1;
}

function draw() {
  ctx.save();
  // Camera offset: everything shifts left by camX
  ctx.translate(-camX, 0);

  // Level (in camera coordinate space)
  ground.forEach(g => ctx.fillRect(g.x, g.y, g.w, 20));
  drawPlayer(player.x, player.y);

  ctx.restore();
}
✏️ Lesson 8 Task

Implement a scroll system:

  • Create 20 objects (eggs) at random world-space Y positions (between -100 and -2000)
  • In the game loop, increase scrollY each frame (e.g. 2 px/frame)
  • Draw objects using screenY = obj.y + scrollY
  • If screenY > H+30, remove from the array (filter)
  • Bonus: collision detection with the player (Math.hypot)

🧠 What is the screen_y = obj.y + scrollY formula?

The object removes itself from scrollY
We add the camera offset to the object's world position to find where it appears on screen
scrollY is the object's own speed
This is how we calculate collisions
Phase 3 · Lesson 9

Sprite Animation — without image files

Our games have no image files — the turtle's legs, the frog's jump, the seagull's wingbeat are all animated using Math.sin() and a frame counter.

⏱ 75 min
🎯 Math.sin · frame · jumpH · walkBob
🎮 Turtle legs · Frog jump · Seagull wing

1sin()-based animation

sin_animation.js — how it worksJavaScript
// frame increases constantly: 0, 1, 2, 3, 4...
// Math.sin(frame * speed) → oscillates between -1 and +1
// * amplitude → sets how large the movement is

// Slow wave:
const slow = Math.sin(frame * 0.05) * 10; // ±10px slowly

// Medium stepping:
const walk = Math.sin(frame * 0.2) * 3;  // ±3px medium

// Fast wingbeat:
const flap = Math.sin(frame * 0.25) * 8; // ±8px fast

// Pulsing size (0 to 1, not -1 to 1):
const pulse = (Math.sin(frame * 0.1) + 1) * .5;

Jump parabola with sin()

jump.js — parabola arcJavaScript
// jumpT: jump time counter (counts from 0 to N)
let jumpT = 0;
const JUMP_FRAMES = 22; // lands after 22 frames

if (jumping) {
  jumpT++;
  // sin(0) = 0, sin(π/2) = 1, sin(π) = 0
  // → parabola: goes up and comes back down
  jumpH = Math.sin(jumpT / JUMP_FRAMES * Math.PI) * 38;
  // 38px is the peak height

  if (jumpT >= JUMP_FRAMES) {
    jumping = false;
    jumpH   = 0;
  }
}

// When drawing: subtract jumpH from Y (moves upward)
ctx.translate(p.x, PLAYER_Y - p.jumpH);

3 examples from our games

🐢 Turtle Race

Turtle legs stepping in opposite phase:

turtle_race.html
// walk: oscillates between -2 and +2
const walk = Math.sin(frame * .2) * 2;

// Rear-left, rear-right — step in OPPOSITE phase
ctx.ellipse(-10, 12 + walk,  7, 4, .3, 0, Math.PI*2);
ctx.fill();
ctx.ellipse( 10, 12 - walk,  7, 4, -.3, 0, Math.PI*2);
ctx.fill();
// Front legs also in opposite phase:
ctx.ellipse(-10, -8 - walk, 6, 4, .2, 0, Math.PI*2);
ctx.ellipse( 10, -8 + walk, 6, 4, -.2, 0, Math.PI*2);
🐸 Froggy Rush

Frog jump parabola arc with sin():

froggy_rush.html
// jumpT runs from 0 to 22 (22 frames)
if (p.jumping) {
  p.jumpT++;
  p.jumpH = Math.sin(
    p.jumpT / 22 * Math.PI
  ) * 38;  // peak height 38px

  if (p.jumpT >= 22) {
    p.jumping = false;
    p.jumpH   = 0;
  }
}
// When drawing:
ctx.translate(p.x, PLAYER_Y - p.jumpH);
🏰 Castle Siege

Hero "breathing" animation when idle:

castle_siege.html
// Slow up-down movement = "breathing"
const bob = Math.sin(G.frame * 0.04) * 1.5;

ctx.save();
// Hero moves slightly up and down
ctx.translate(h.x, h.y + bob);

drawHeroBody();
drawHeroHead();
drawHeroWeapon();
ctx.restore();
✏️ Lesson 9 Task

Animate everything with sin():

  • Draw a character (simple stick figure) and animate its arm with sin()
  • Implement the parabola jump (jumpT, jumpH, sin(jumpT/N * PI))
  • A circle "pulses" — its size oscillates between 20 and 30 pixels with sin()
  • Bonus: two characters stepping in opposite phase (one +walk, other -walk)

🧠 Why is +1 added then multiplied by 0.5 in pulsing: (sin(frame)+1)*.5?

To make the animation faster
Because sin() is between -1 and +1, but we need 0 to 1 — adding 1 shifts it to 0–2, then *.5 brings it to 0–1
To prevent negative transparency
Only needed on canvas, not in other contexts
Phase 3 · Lesson 10

🏆 Phase Project — Moving Character

The summary of Phase 3: a fully controllable, animated character on the canvas — game loop, scroll, collision, sin() animation all at once.

⏱ 3–4 hours
🎯 Independent work
📁 Single HTML file

1The project task

Build a simple canvas-based mini game where a character moves and collects items. Everything you learned in this phase is included.

Minimum requirements

✓ Canvas + 2D context, clearRect every frame
✓ Game loop: requestAnimationFrame, update + draw separated
✓ Controllable character: keyboard / touch, save/translate/restore
✓ At least 1 animation: sin()-based stepping or jumping
✓ At least 5 collectible items: Math.random spawning
✓ Collision detection: Math.hypot
✓ Score text on canvas: ctx.fillText

mini_game.html — suggested skeletonJavaScript
// === STATE ===
const CV = ..., ctx = CV.getContext('2d');
let player = { x: 200, y: 300, vx:0, vy:0 };
let items   = []; // collectible items
let score   = 0;
let frame   = 0;
const K     = {};  // held keys

// === UPDATE ===
function update() {
  frame++;
  // Movement based on keys
  if (K['ArrowLeft'])  player.vx -= 0.5;
  if (K['ArrowRight']) player.vx += 0.5;
  player.vx *= 0.85; // friction
  player.x  += player.vx;
  // Collision: collecting items?
  items = items.filter(item => {
    if (Math.hypot(player.x-item.x, player.y-item.y) < 25) {
      score++; return false; // disappears
    }
    return true;
  });
}

// === DRAW ===
function draw() {
  ctx.clearRect(0, 0, W, H);
  items.forEach(item => { drawItem(item.x, item.y); });
  drawPlayer(player.x, player.y, frame);
  // Score
  ctx.fillStyle = '#fff';
  ctx.font = 'bold 18px sans-serif';
  ctx.textAlign = 'left';
  ctx.fillText('Score: ' + score, 10, 30);
}

// === LOOP ===
function loop() { update(); draw(); requestAnimationFrame(loop); }
loop();
✏️ Project Checklist
  • Canvas created, ctx obtained, W and H in variables
  • requestAnimationFrame loop: update() + draw() separated
  • clearRect at the start of every draw() (no leftover traces)
  • Character controllable by keyboard (keydown/keyup, K object)
  • Character drawn with save/translate/restore
  • At least one sin()-based animation (stepping, wave)
  • Collectible items spawned at Math.random positions
  • Collision detection with Math.hypot
  • Score displayed with ctx.fillText on canvas
  • Works on mobile too (D-pad buttons in HTML)
Bonus challenges

⭐ Scroll system — the level is longer than the screen
⭐⭐ Jump implementation (jumpT, jumpH, sin() parabola)
⭐⭐ Enemy that chases the player (AI: move toward player)
⭐⭐⭐ Life system + Game Over screen