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.
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.
<!-- 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
// 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 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
Drawing the background and road with fillRect:
// 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);
Drawing lane rows (road, river, grass):
// 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);
Drawing tree trunks at the track edges:
// 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);
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?
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.
1The Path system
To draw circles and complex shapes, we first describe a path, then fill or outline it.
// 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
Drawing projectile and enemy circles:
// 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();
Drawing an egg as an ellipse:
// 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();
Rock's irregular shape using moveTo + lineTo:
// 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();
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()!
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?
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.
1Ways to specify colour
// 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
HP bar gradient — green to red:
// 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 shell with radial gradient:
// 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();
Boost pad pulsing animation with globalAlpha:
// 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!
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.
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?
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.
1The loop structure
// 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
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's complete loop:
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(); }
Frame counter and phase-based update:
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();
Using the frame number for sine animation:
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); }
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?
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.
1save() and restore() — saving the context
// 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 tilt based on direction (vx):
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(); }
Drawing the seagull with translate to its own origin:
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(); }
Drawing trees — always around their own origin:
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(); }
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.
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)?
Drawing Text on the Canvas
The score, "GAME OVER", the floating "+10" messages, the level number — all are put on screen with ctx.fillText().
1Text basics
// 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
Floating text (+gold, -HP) animation:
// 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;
Game Over / win overlay text:
// 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 name and egg count label:
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();
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?
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.
1For circles: Math.hypot distance
// 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
Tower range check — is an enemy in range?
// 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; } });
Egg and seagull collision with frog circle:
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++; } });
Rock collision — no collision while jumping:
// 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! } }
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.
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?
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.
1World-space vs Screen-space
// 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
Objects are fixed in world-space, only scrollY moves:
// 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;
Lane row Y position based on scroll:
// 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);
Simple side-scrolling camera follow:
// 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(); }
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?
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.
1sin()-based animation
// 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()
// 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 legs stepping in opposite phase:
// 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);
Frog jump parabola arc with sin():
// 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);
Hero "breathing" animation when idle:
// 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();
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?
🏆 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.
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.
✓ 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
// === 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();
- 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)
⭐ 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