TECH DETAILS

Wine Bottle Pyramid - Day 11

Physics Simulation

Table of Contents

01 / Tech Stack

A physics-based throwing game built with pure JavaScript and Canvas - no physics libraries required.

// Core technologies const canvas = document.getElementById('gameCanvas'); const ctx = canvas.getContext('2d'); // Physics constants - tune these for game feel const GRAVITY = 0.35; // Downward acceleration per frame const FRICTION = 0.98; // Air resistance (1 = none) const BOUNCE = 0.6; // Energy retained on bounce (1 = perfect)
Why Custom Physics?

For a simple projectile game, custom physics gives you precise control over game feel without the overhead of libraries like Matter.js or Box2D.

02 / Physics Engine Basics

Every physics object needs position, velocity, and we apply forces each frame. This is the foundation of any physics simulation.

The Physics Object Structure

const ball = { x: 80, // Position X y: GROUND_Y - 20, // Position Y radius: 18, // Size for collision vx: 0, // Velocity X (pixels/frame) vy: 0, // Velocity Y (pixels/frame) launched: false // State flag }; const bottle = { x: 0, y: 0, width: 22, height: 60, vx: 0, vy: 0, rotation: 0, // Current angle (radians) rotationSpeed: 0, // Angular velocity fallen: false, // Hit by ball? grounded: false // Stopped moving? };

The Update Loop

function updatePhysics() { // Apply gravity (acceleration) ball.vy += GRAVITY; // Apply velocity to position ball.x += ball.vx; ball.y += ball.vy; // Apply air friction ball.vx *= FRICTION; // Ground collision if (ball.y + ball.radius > GROUND_Y) { ball.y = GROUND_Y - ball.radius; // Prevent sinking ball.vy *= -BOUNCE; // Reverse & reduce velocity ball.vx *= 0.9; // Ground friction } }
Apply Gravity -> Update Position -> Apply Friction -> Handle Collisions

03 / Collision Detection & Response

Detecting when the ball hits bottles, and making bottles knock each other over - this creates the satisfying chain reaction effect.

Circle-to-Rectangle Collision

function checkCollisions() { bottles.forEach(bottle => { if (bottle.grounded) return; // Find bottle center const bottleCenterX = bottle.x + bottle.width / 2; const bottleCenterY = bottle.y + bottle.height / 2; // Distance from ball to bottle center const dx = ball.x - bottleCenterX; const dy = ball.y - bottleCenterY; const distance = Math.sqrt(dx * dx + dy * dy); // Collision threshold (simplified as circle-to-circle) if (distance < ball.radius + bottle.width / 2) { // Mark as hit if (!bottle.fallen) { bottle.fallen = true; score += 100; } // Calculate collision angle const angle = Math.atan2(dy, dx); // Transfer momentum to bottle const force = Math.sqrt(ball.vx**2 + ball.vy**2) * 0.5; bottle.vx += Math.cos(angle + Math.PI) * force; bottle.vy += Math.sin(angle + Math.PI) * force * 0.5; bottle.rotationSpeed = (Math.random() - 0.5) * 0.3; // Ball loses momentum ball.vx *= 0.7; ball.vy *= 0.7; } }); }

Bottle Chain Reactions

When bottles collide with each other, they transfer momentum - creating satisfying domino effects.

// Inside the bottles loop, check bottle-to-bottle collisions bottles.forEach(other => { if (other === bottle || other.grounded) return; const odx = bottleCenterX - (other.x + other.width / 2); const ody = bottleCenterY - (other.y + other.height / 2); const odist = Math.sqrt(odx * odx + ody * ody); // If bottles are close and one is falling if (odist < bottle.width && bottle.fallen) { if (!other.fallen) { other.fallen = true; score += 100; } // Transfer partial momentum other.vx += bottle.vx * 0.3; other.vy += bottle.vy * 0.3; other.rotationSpeed = (Math.random() - 0.5) * 0.2; } });
Simplification Trade-off

We use circle-to-circle collision instead of true rectangle collision. It's less accurate but much simpler and works well for this game style.

04 / Trajectory Prediction

Showing the ball's predicted path before throwing helps players aim - it simulates the physics forward in time.

function drawAimLine() { if (!isDragging || ball.launched) return; // Calculate initial velocity from drag const dx = dragStart.x - dragEnd.x; const dy = dragStart.y - dragEnd.y; const power = Math.min(Math.sqrt(dx*dx + dy*dy), 150); const angle = Math.atan2(dy, dx); // Simulate trajectory let previewX = ball.x; let previewY = ball.y; let previewVX = Math.cos(angle) * power * 0.15; let previewVY = Math.sin(angle) * power * 0.15; ctx.setLineDash([5, 5]); ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)'; ctx.beginPath(); ctx.moveTo(ball.x, ball.y); // Simulate 30 frames ahead for (let i = 0; i < 30; i++) { previewX += previewVX; previewY += previewVY; previewVY += GRAVITY; // Apply same gravity ctx.lineTo(previewX, previewY); if (previewY > GROUND_Y) break; // Stop at ground } ctx.stroke(); ctx.setLineDash([]); // Reset dash }
Performance Note

Keep the prediction loop short (30 iterations here). Each prediction recalculates every frame during drag, so it needs to be fast.

05 / Drag-to-Throw Mechanics

The slingshot-style aiming: drag back to set power and angle, release to throw. Works identically on mouse and touch.

Unified Event Handling

// Convert any event (mouse or touch) to canvas coordinates function getEventPos(e) { const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; if (e.touches) { return { x: (e.touches[0].clientX - rect.left) * scaleX, y: (e.touches[0].clientY - rect.top) * scaleY }; } return { x: (e.clientX - rect.left) * scaleX, y: (e.clientY - rect.top) * scaleY }; }

Launch Calculation

canvas.addEventListener('mouseup', (e) => { if (!isDragging || ball.launched) return; // Vector from end to start (pull back direction) const dx = dragStart.x - dragEnd.x; const dy = dragStart.y - dragEnd.y; // Power clamped to max 150 const power = Math.min(Math.sqrt(dx*dx + dy*dy), 150); // Angle in radians const angle = Math.atan2(dy, dx); // Minimum power threshold prevents accidental taps if (power > 10) { // Convert polar (angle, power) to cartesian (vx, vy) ball.vx = Math.cos(angle) * power * 0.15; ball.vy = Math.sin(angle) * power * 0.15; ball.launched = true; } isDragging = false; });
Power Indicator

The color-changing power bar (rgba(255, 255-power, 0)) gives instant visual feedback - green for weak throws, red for maximum power.

06 / Bottle Rendering & Rotation

Drawing bottles with rotation requires Canvas transforms. Save state, translate, rotate, draw, restore.

function drawBottle(bottle) { ctx.save(); // Save current transform state // Move origin to bottle center ctx.translate( bottle.x + bottle.width / 2, bottle.y + bottle.height / 2 ); // Apply rotation around center ctx.rotate(bottle.rotation); // Draw bottle body (now centered at origin) const gradient = ctx.createLinearGradient( -bottle.width/2, 0, bottle.width/2, 0 ); gradient.addColorStop(0, bottle.color); gradient.addColorStop(0.5, lightenColor(bottle.color, 50)); gradient.addColorStop(1, bottle.color); ctx.fillStyle = gradient; ctx.beginPath(); ctx.roundRect( -bottle.width/2, -bottle.height/2 + 15, bottle.width, bottle.height - 15, 3 ); ctx.fill(); // Neck, cork, label... (all relative to origin) ctx.restore(); // Restore original transform }

Glass Shine Effect

// Subtle highlight that makes the bottle look like glass ctx.fillStyle = 'rgba(255,255,255,0.2)'; ctx.beginPath(); ctx.ellipse( -bottle.width/4, // Left side 0, // Center vertically 3, // Width bottle.height/3, // Height - tall oval 0, // No rotation 0, Math.PI * 2 ); ctx.fill();
Transform Order Matters

Always translate before rotate. This ensures rotation happens around the bottle's center, not the canvas origin.