TECH DETAILS

Yeti Penguin Launcher - Day 22

Intermediate Level

Table of Contents

01 / Projectile Physics

The penguin follows realistic projectile motion with gravity, air resistance, ground friction, and bounce damping. These constants define how the penguin flies and stops.

Physics Constants

const GRAVITY = 0.15; // Downward acceleration per frame const AIR_RESISTANCE = 0.998; // Velocity multiplier (slight slowdown) const GROUND_FRICTION = 0.96; // When rolling on ground const BOUNCE_DAMPING = 0.5; // Energy lost on bounce (50%)

Physics Update Loop

Every frame, we apply forces and update the penguin's position.

function update() { // Apply gravity (increases downward velocity) penguin.vy += GRAVITY; // Apply air resistance (reduces both velocities slightly) penguin.vx *= AIR_RESISTANCE; penguin.vy *= AIR_RESISTANCE; // Update position based on velocity penguin.x += penguin.vx; penguin.y += penguin.vy; // Rotation for visual tumbling effect penguin.rotation += penguin.rotationSpeed; penguin.rotationSpeed *= 0.99; // Slow rotation over time }

Bounce Physics

When the penguin hits the ground, we reverse and dampen the vertical velocity.

// Ground collision check if (penguin.y >= GROUND_Y - penguin.height / 2) { penguin.y = GROUND_Y - penguin.height / 2; if (Math.abs(penguin.vy) > 1) { // Bounce: reverse Y velocity, reduce by damping penguin.vy = -penguin.vy * BOUNCE_DAMPING; penguin.vx *= 0.9; // Lose some horizontal speed too bounceCount++; // Random spin on bounce penguin.rotationSpeed = (Math.random() - 0.5) * 0.2; } else { // Too slow to bounce - start rolling penguin.vy = 0; penguin.isOnGround = true; penguin.vx *= GROUND_FRICTION; penguin.rotation = 0; // Upright when rolling } }
Physics Tuning

Adjust GRAVITY for arc height, AIR_RESISTANCE for max distance, and BOUNCE_DAMPING for bounce behavior. Small changes make big differences!

02 / Charge & Launch Mechanics

The charge bar creates a satisfying power-up mechanic. Longer hold = more power, up to a 2-second maximum.

Charge System

let chargeStart = 0; let chargePower = 0; function handleStart(e) { if (gameState === 'ready') { gameState = 'charging'; chargeStart = Date.now(); // Record start time } } function updateCharge() { // Calculate power: 0-1 over 2 seconds const chargeTime = Math.min(Date.now() - chargeStart, 2000); chargePower = chargeTime / 2000; }

Launch Calculation

On release, we convert the charge power into initial velocity using trigonometry.

function handleEnd(e) { if (gameState === 'charging') { // Base power + bonus from charge const launchPower = 15 + chargePower * 20; // 15-35 range // Launch angle: higher charge = slightly lower angle // Base angle: 45 degrees up (-PI/4) const launchAngle = -Math.PI / 4 - (chargePower * Math.PI / 8); // Convert polar (angle, power) to Cartesian (vx, vy) penguin.vx = Math.cos(launchAngle) * launchPower; penguin.vy = Math.sin(launchAngle) * launchPower; // Add some rotation for visual flair penguin.rotationSpeed = 0.1 + Math.random() * 0.1; gameState = 'flying'; } }

Flap Boost Mechanic

Players can tap during flight to get extra boosts, adding skill to the game.

const FLAP_BOOST = -3; // Upward velocity boost const MAX_FLAPS = 3; // Limited flaps per launch // In handleStart, when already flying: if (gameState === 'flying' && penguin.flapsLeft > 0 && !penguin.isOnGround) { penguin.vy += FLAP_BOOST; // Boost upward penguin.vx *= 1.05; // Small speed boost penguin.flapsLeft--; }
    Launch Vector Breakdown:

         vy (negative = up)
          ^
          |  /
          | / launchPower
          |/
          +-------> vx (positive = right)
              launchAngle

    vx = cos(angle) * power
    vy = sin(angle) * power
                

Trigonometry converts angle and power to velocity components

03 / Collision Detection & Response

Different obstacles have different collision responses - ramps boost you up, seals give big bounces, and ice blocks slow you down.

AABB Collision Check

function checkCollision(penguin, obstacle) { // Convert penguin center to top-left corner const px = penguin.x - penguin.width / 2; const py = penguin.y - penguin.height / 2; // Axis-Aligned Bounding Box overlap test return px < obstacle.x + obstacle.width && px + penguin.width > obstacle.x && py < obstacle.y + obstacle.height && py + penguin.height > obstacle.y; }

Obstacle Type Responses

function handleObstacleCollision(obs) { if (obs.type === 'ramp') { // Ramp: converts horizontal momentum to vertical if (penguin.vy > 0) { // Only if falling penguin.vy = -Math.abs(penguin.vx) * 0.6; penguin.vx *= 1.1; // Speed boost penguin.rotationSpeed = 0.15; bounceCount++; } } else if (obs.type === 'seal') { // Seal: big bouncy surface penguin.vy = -12; // Strong upward boost penguin.vx *= 1.15; // Speed boost penguin.rotationSpeed = 0.2; bounceCount++; } else if (obs.type === 'snowpile') { // Snow pile: small bounce if (penguin.vy > 0) { penguin.vy = -5; bounceCount++; } } else if (obs.type === 'iceblock') { // Ice block: wall - slows you down penguin.vx *= 0.5; // Lose half speed penguin.vy = -3; // Small bounce up } }
Game Feel

The ramp's velocity conversion (horizontal to vertical) creates satisfying "launch" moments. The seal's large bounce encourages players to aim for it.

04 / Camera Scrolling System

The camera follows the penguin but stays slightly behind, creating a smooth parallax effect and showing where you're going.

Camera Follow Logic

let cameraX = 0; // World X position of camera's left edge function updateCamera() { // Keep penguin at 30% from left of screen const targetX = canvas.width * 0.3; if (penguin.x > cameraX + targetX) { cameraX = penguin.x - targetX; } }

World-to-Screen Transformation

All world positions must be converted to screen positions for drawing.

// Drawing the penguin const screenX = penguin.x - cameraX; drawPenguin(screenX, penguin.y); // Drawing obstacles (only visible ones) for (const obs of obstacles) { const obsScreenX = obs.x - cameraX; // Culling: skip objects outside view if (obsScreenX > -100 && obsScreenX < canvas.width + 100) { drawObstacle(obs, obsScreenX); } }

Parallax Background

// Mountains scroll slower (parallax factor 0.1) const mountainX = (i * 200 - cameraX * 0.1) % (canvas.width + 200) - 100; // Clouds scroll at half speed (parallax factor 0.5) const cloudScreenX = cloud.x - cameraX * 0.5;

05 / Procedural Obstacle Generation

Obstacles are generated procedurally at game start with random types and positions, creating variety in each playthrough.

Generation Algorithm

function generateObstacles() { obstacles = []; // Generate from 400 to 50000 units away for (let x = 400; x < 50000; x += Math.random() * 300 + 150) { const type = Math.random(); if (type < 0.3) { // 30% chance: Ice ramp (boosts up) obstacles.push({ type: 'ramp', x: x, y: GROUND_Y, width: 60, height: 25 }); } else if (type < 0.5) { // 20% chance: Snow pile (small bounce) obstacles.push({ type: 'snowpile', ... }); } else if (type < 0.7) { // 20% chance: Ice block (wall) obstacles.push({ type: 'iceblock', ... }); } else { // 30% chance: Bouncy seal obstacles.push({ type: 'seal', ... }); } } }

Obstacle Distribution

// Spacing: random 150-450 units apart x += Math.random() * 300 + 150; // This creates variable density: // - Min gap: 150 units (dense) // - Max gap: 450 units (sparse) // - Average: 300 units between obstacles
Balancing Tips

Seals and ramps are "good" obstacles (30% + 30% = 60%), while ice blocks are "bad" (20%). Snow piles are neutral. This ratio ensures satisfying gameplay.

06 / Character & Environment Rendering

Characters are drawn using basic Canvas primitives - ellipses, arcs, and rectangles - creating a cute, stylized look without external assets.

Drawing the Penguin

function drawPenguin(x, y) { ctx.save(); ctx.translate(x, y); ctx.rotate(penguin.rotation); // Tumbling effect // Body (black oval) ctx.fillStyle = '#1a1a2e'; ctx.beginPath(); ctx.ellipse(0, 0, 12, 15, 0, 0, Math.PI * 2); ctx.fill(); // Belly (white oval) ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.ellipse(0, 2, 8, 10, 0, 0, Math.PI * 2); ctx.fill(); // Head, eyes, beak, wings, feet... ctx.restore(); }

Animated Wings During Flight

if (gameState === 'flying' && !penguin.isOnGround) { // Calculate flap angle using sine wave const flapAngle = Math.sin(Date.now() / 50) * 0.5; // Left wing ctx.save(); ctx.rotate(flapAngle); ctx.fillRect(-18, -5, 8, 15); ctx.restore(); // Right wing (opposite angle) ctx.save(); ctx.rotate(-flapAngle); ctx.fillRect(10, -5, 8, 15); ctx.restore(); }

Drawing the Yeti with Club

// Yeti's arm holding club rotates during charge ctx.save(); ctx.translate(x + 30, y - 10); // Shoulder position ctx.rotate(yeti.clubAngle + Math.PI / 4); // Arm ctx.fillStyle = '#f5f5f5'; // White fur ctx.fillRect(-8, 0, 16, 50); // Club handle ctx.fillStyle = '#8b4513'; // Brown wood ctx.fillRect(-10, 40, 20, 60); // Club head ctx.fillStyle = '#5d3a1a'; ctx.beginPath(); ctx.arc(0, 95, 18, 0, Math.PI * 2); ctx.fill(); ctx.restore();
Canvas Tips

Always use save() and restore() around transformations. This prevents rotation and translation from affecting other drawings.