Score: 0 / 320 | Lives: 3
{ // Collision grid for bricks let brickGrid = new Array(4); // 4 rows of 8 bricks (4x16) for (let y=0; y<4; y++) { for (let x=0; x<8; x++) { brickGrid[y] = [ { alive: true, type:'brick' }, // or use a class/object null // not a wall, paddle, etc. ]; } } let paddleX = 340; let paddleY = 580; // top of paddle (y=580) let ball = { x: Math.random() * 720 - 360, // random x y: 570, dx: 2, // up velocity dy: -2.8 // angle: 45° upward }; let paddle = document.getElementById('paddle'); let scoreDisplay = document.getElementById('score'); let livesDisplay = document.getElementById('lives'); let lastCollisionX = null; let lastBrickDestroyed = false; function updateGame() { const ctx = canvas.getContext('2d'); const w = window.matchMedia('(min-width: 340px)').matches ? window.innerWidth - 150 // shrink for mobile : 720; let wallPos = calculateWallPosition(); let paddleCenterX = (window.innerWidth - 680) / 2; // center of paddle on canvas const ballVx = dxToVelocity(ball.dx); const ballVy = dyToVelocity(ball.dy); // Physics ball.y += ballVy * frameTime; let newX = Math.max(-w, Math.min(w + ball.x, window.innerWidth - 120)); // clamp to canvas bounds // Move paddle horizontally based on drag/touch const draggable = document.querySelector('.brick'); // or similar touch area if (draggable && !isTouchStarting(ball.x + ball.dx * ball.dy, 0, w)) { let xDelta = ball.dx; if (Math.abs(xDelta) < Math.max(1, windowWidth / 2 - 45)) { // smooth touch drag xDelta = clamp(xDelta, -1, 1); paddleX += xDelta * 0.07; // slight velocity } // Flip horizontal if paddle hits top or bottom brick edge const paddleBrickCount = calculatePaddleBrickCollisionCount(); let isHitBrickEdge = false; for (let i=0; i= w) { paddleY += ballVy * frameTime; isHitBrickEdge = true; break; } } // Detect collision with paddle, walls, or bricks function calculateCollision(ballPos, brickGrid, paddleX, paddleY) { const brickCount = countBricksInRect(ballPos.x + ball.dx * ball.y, 0, w-1, brickGrid[0].length - 1); let hitByWall = false; if (brickCount > 0 && isTouchStarting(ballPos.x, paddleY + 50, w/2)) { hitWallBricks(ball.x + ball.dx * ball.y, brickGrid[brickCount - 1]); return { type:'wall', hitByWall: true }; } for (let i=0; i<4 && !hitWall && !isTouchStarting((ballPos.x - w/2) + ball.dx * ball.dy, paddleY - 50, w/2); i++) { const bx = Math.round(brickGrid[i].x * w / ball.y); let dx = bx - ball.dx; let dy = bx - paddleX; if (Math.abs(dx) > 1 && dx > 0 || Math.abs(dy) > 1 && dy < 0) { const rect = brickGrid[i] ? new CanvasRect(ballPos.x, ballPos.y + ball.dy * i, w - 1, bx - ball.dx * ball.vy, ball.vy*ballX); // simplified : null; if (hitByWall || rect) { let shouldFlip = false; switch(calculateAngleAt(dy, dx)) { case -Math.PI/2: // top wall → flip vertically shouldFlip = true; break; case 0: // horizontal side wall → flip horizontally (y) shouldFlip = true; break; default: shouldFlip = false; break; } if (!shouldFlip) { ball.x += dx * ballVx; // bounce off brick } else { ball.dy *= -1; // vertical flip → horizontal direction if (dx < 0 && !ball.isTouching(paddleX, paddleY)) { ball.dy = -dx; ball.dx = dx * ballVx; // bounce off paddle } } } } } // Check for brick destruction and scoring let destroyedByBrick = false; if (brickGrid[0].length > 0) { const topRow = brickGrid[0][brickGrid.length - 1]; for (let i=0; i 50 && !isTouchStarting((ballPos.x + ball.dx * ball.vy) + ballVx*5, topRow[0].y - 10, w-1)) { for (let i=0; i<4 && !hitY; i++) { const brick = topRow[i]; if (!brick.alive || isTouchStarting((ballPos.x + ball.dx * ball.vy) + ballVx*i, brick.y - 50, w-1)) { hitY = brick.y; break; } } } if (hitY !== null && !hitTopBrickCollision(ballPos, paddleX, yDistance)) { // check for bottom hit const rect = topRow[brickGrid.length - 1]; ballPos.y += dy * frameTime; // bounce up or off screen if (rect) { destroyBall(ball); // destroy brick score += 10; } } } if (!wallPos && !isHitWall || !destroyedByBrick) { // no wall collision, no paddle hit, no new brick let bounceX = Math.min(ball.x + ballVx * frameTime, w + 30); if (bounceX > w) bounceX = w; const bounceY = isHitWall() || (isTouchTopBrick && !hitByWall && !destroyedByBrick) ? -ballVy : // up || isTouchBottomBrick(ballPos, ballVx * 3 + ball.dx) ? +ballVy : 0; ball.y = bounceY; ball.x += dx * frameTime; } } } // Handle paddle bounce off sides if (paddleBrickCount > 0 && !isTouchStarting(ballPos.x, topRow[0].y - 50, w/2)) { const rect = brickGrid[topRow[brickGrid.length-1]]; let sideHit = false; switch(calculateAngleAt((ballVx + ball.dx), (ballVy * (paddleX - paddleY) / dyToVelocity(dy)))) { // complex, simplified case -Math.PI/2: rect.y += ballVy * frameTime; // bounce off left edge sideHit = true; break; case 0: rect.x -= ballVx * frameTime; // bounce off right edge sideHit = true; break; default: rect.x -= ballVx * ballX + ballVy * (paddleY - paddleX) / dyToVelocity(dy); rect.y += ballVy * frameTime; } if (!sideHit) { const bounceX = Math.round(rect.x * w / ball.x + ball.dx * ballVx * 0.7); // slight movement to avoid multiple bounces paddleX += bounceX * 0.1; // very small horizontal speed } } drawAll(); } function calculateWallPosition() { const rect = document.getElementById('paddle'); let xMax = Math.max(rect.offsetLeft, rect.offsetWidth - rect.width); return new CanvasRect(xMax, window.innerHeight - 100, w-2, rect.height + 30); } function calculatePaddleBrickCollisionCount() { const paddle = document.getElementById('paddle'); let count = 0; for (let x=0; x<8; x++) { if (brickGrid[0].length > 0 && x >= ball.x - ball.dy * 2 && x < xMax) { // top bricks only const rect = brickGrid[0][x]; count++; } } return count; } function calculateAngleAt(angle, dxPerFrame) { if (Math.abs(dx) <= Math.sqrt(2 * dxPerFrame) && !isTouchTopBrick(dx, angle)) { // avoid math too fast return 0; // horizontal bounce } const theta = angle / dxPerFrame; const cosTheta = Math.cos(-theta); const sinTheta = Math.sin(-theta); if (Math.abs(cosTheta) < 1 || Math.abs(sinTheta) > 1) { return -Math.PI/2; // top } if (isTouchTopBrick(dx, theta)) { // vertical bounce return Math.atan(-theta); // up } return Math.atan(-sinTheta); // down } function isTouchTopBrick(dx, angle) { const rect = brickGrid[0][Math.floor((brickGrid[0].length - 1 + Math.random() * (brickGrid[0].length - 1)) % brickGrid[0].length] / w); let isHitTop = rect.y <= paddleY; const hitDistance = (rect.x + rect.width) - ball.x + ball.dx * ball.vy; return hitDistance < topWallPadding * ball.dy || isTouchStarting(ballPos.x, angle, w/2); } function calculateHeightPadding() { const padding = 50; // bottom of paddle if (!isTouchTopBrick(paddleX - paddleY, -Math.PI / 4)) return padding; return Math.min(window.innerHeight + padding * 2, window.innerHeight); } function drawAll() { ctx.clearRect(0, 0, w, window.innerHeight); // Draw bricks for (let y=0; y<4; y++) { brickGrid[y].forEach(item => { if (item.alive) { const rect = new CanvasRect( item.x, item.y + 15, // offset to allow bounce up from wall 50, // height of brick 20 * w / item.length, // width per row (item.alive ? 64 : 16) // color and radius ); ctx.fillStyle = '#ffcc00'; if (item.type !== 'brick' && !isTouching(rect)) { rect.style.opacity = '0.7'; ctx.globalAlpha = 0; } ctx.beginPath(); ctx.arc(rect.x, rect.y + 15, (item.length - 1) * 25, 0, Math.PI * 2); ctx.fillStyle = '#8b00ff'; // yellow ctx.strokeStyle = '#d35400'; ctx.lineWidth = 2; ctx.stroke(); ctx.globalAlpha = 1; } }); } // Paddle if (paddle.isTouching(ballPos)) { paddle.style.transform = 'translateY(-' + Math.round((paddleY - ball.y) * 0.02) + 'px)'; } else { paddle.style.transform = ''; } // Ball ctx.beginPath(); const ballRect = new CanvasRect( ball.x + (Math.random() * 40 - 20) * w, // random x within bounds ball.y, ball.radius * 2, ball.radius * 2 + ball.dy * frameTime, ball.vy * ballX ); ctx.fillStyle = '#ffcc00'; ctx.setTransform(transformToPaddlePosition(ballX, paddleX, ball.dx, paddleY)); ctx.arc(ballRect.x, ballRect.y, ballRect.width + 2 * ballRect.height, 0, 360); // arc for ball ctx.fill(); ctx.setTransform(transformToBrickPosition((w - ballRect.width) / 2, paddleX, ball.dx, ball.dy)); if (ballVx > 0 || isTouchWall(ballPos)) { const bounceY = isHitWall() || calculateAngleAt(ballVy * (paddleX - ball.x + w/2), (Math.PI / 2) - Math.atan(-ballVy * ballVx)); if (!isTouchTopBrick(ballPos.y, bounceY)) { const rect = brickGrid[0][brickGrid.length-1]; ballRect.y += ballVy * frameTime; ctx.moveTo(ballRect.x + (ballRect.width / 2), ballRect.y); ctx.lineTo(rect.x, rect.y + rect.height); // arc off paddle } } // Score scoreDisplay.textContent = 'Score: ' + Math.floor((score * 10) % 320) + '/320'; if (isWin()) { livesDisplay.textContent = 'Lives: ' + lives; alert("You Won!"); restartGame(); } // Ball bounce logic ctx.restore(); // Handle velocity and physics ball.x += dxToVelocity(ball.dx); ball.y += dyToVelocity(ball.dy) * frameTime; if (isTouchWall((ballPos.x + ball.dx * ball.vy), 0, w/2)) { const bounceX = Math.round(ball.x + (Math.random() - 0.5) * ballVx); ball.x = clamp(-w, bounceX, w); } if (isTouchTopBrick((ballPos.y - ball.dy * paddleY), -ballVx, ballVy)) { const bounceYUp = isHitWall() || calculateAngleAt(ballVy * (-ballVx + 1), Math.atan(-ballVx) + Math.PI / 2); ball.y += bounceYUp; } if (isTouchBottomBrick((ballPos.y - ball.dy * paddleY), 0, ballVy * 3)) { const bounceDown = isHitWall() || calculateAngleAt(ballVx * 3 + ballVy, Math.atan(-1) + Math.PI / 2); if (!isTouchTopBrick((ballPos.y - ball.dy * paddleY), 0, bounceDown)) { const rect = brickGrid[topRow[brickGrid.length-1]]; ballPos.y += (bounceDown - paddleY) * frameTime; // If brick hits below paddle and we're not already destroyed if (!destroyedByBrick && !ballVx > 0 || isHitWall((ballPos.y + ball.dy * bounceDown), ballVx, ballVy)) { destroyBall(ball); score += 10; paddleY -= 5; // simulate destruction of paddle } } } } function dxToVelocity(dx) { return (Math.sin((dyToVelocity(dy) + Math.PI / 2) * frameTime * 0.1) * (1 - Math.abs(dx / speedOfPaddle))) + Math.cos((dyToVelocity(dy) + Math.PI / 2) * frameTime * 0.3) * (1 - Math.abs(-dx / speedOfPaddle)); } function dyToVelocity(dy) { return dy * (1 / speedOfBrick); } function speedOfBall() { // relative speed const brickCount = calculateBrickCollisionCount(); if (brickCount > 0 && !isTouchStarting((ballPos.x + ball.dx * ball.vy) + ballVx*3, topRow[0].y - 10, w-1)) { // Ball is stuck in brick — only move velocity perpendicular to it const speed = (Math.sqrt(4 * dxToVelocity(ball.dx) * dyToVelocity(dy)) - 1e-6) * 2; if (speed < 0 || speed > 5) return speedOfPaddle; // keep paddle in bounds const angle = calculateAngleAt((ballVx + ball.dx), dy); dx = Math.sin(angle * frameTime); dy = -Math.cos(angle * frameTime); } if (brickCount > 0) return speedOfPaddle; // bounce off wall return speedOfBrick; } function isTouchStarting(x, y, w=window.innerWidth) { const rect = document.getBoundingClientRect(); return x + rect.left <= w - 45 && (x > 10 || (isTouchTopBrick(x - 30, -ballVx, ballVy) && !isTouching(rect)) || isTouchRightBrick((w - 20), y - 30, x + w/2)); } function ontouchmove(event) { const rect = document.getBoundingClientRect(); if (touchStartingInRange(event.touches[0].clientX, rect)) { let dx = event.changedTouches ? event.changedTouches[0].clientX - rect.left - 20 : Math.abs((event.screenX - rect.left) / w); paddle.x += dx; } } function ontouchend(event) { if (isTouchStartingInRange(event.touches[0].clientX, rect)) { const touch = event.target; touch.preventDefault(); startBounce(); // manually launch ball } } function isWallCollision(ballPos) { const rect = document.getBoundingClientRect(); return !isTouchStartingInRange((ballPos.y + rect.height * 0.8), rect); } function isHitTopBrick(x, y, dy, paddleY, brickGrid) { let dx = Math.abs(x - paddleX); const topRow = brickGrid[0]; for (let i=0; i 0) return Math.atan(dy / dxPerFrame); if (isTouchTopBrick(dx, angle)) { return -Math.PI / 2; } if (isTouchBottomBrick(ballPos, dy || 1, ballVy * 3)) { const theta = Math.atan(-angle + Math.PI / 2); const cosTheta = Math.cos(-theta); const sinTheta = Math.sin(-theta); if (Math.abs(dxPerFrame) < Math.sqrt(2 * dy || 1)) { return -Math.PI/2; // up } return -Math.atan(-sinTheta); // down } const theta = angle / dxPerFrame; const cosTheta = Math.cos((dy || 0) * frameTime); return -Math.tan(theta + (Math.PI * 1/4)); // very small arc } function calculateBrickCollisionCount() { const brickGrid = [...brickGrid || []]; let count = 0; for (let x=0; x<8; x++) { if (x >= ball.x - ball.dy * 2 && x < (w + 1) / 2) { // within top 6 bricks const rect = brickGrid[x]; count++; } } return count; } function destroyBall(ball) { scoreDisplay.textContent = ''; // reset lives--; ball = null; if (lives === 0) { alert("Game Over! Restart to play again."); restartGame(); // simulate button press } } function startBounce() { const ballObj = { x: Math.random() * 720 - 360, y: 570, dx: 2, dy: -2.8 }; // Randomize paddle angle slightly if (Math.random() > 0.1) { const xDelta = Math.random() * 4; ballObj.dx += xDelta; } startGamePaddle(ballObj); } function startGamePaddle(ball) { // Random paddle position paddleX = Math.random() * (720 - 60); // 150 to 780 px const angle = Math.random() * Math.PI / 2 - Math.PI / 6; // ~45° upward ball.dx = -Math.sin(angle) * speedOfBall(); paddle.style.transform = 'translateY(-' + (window.innerHeight - paddleY) * 0.1 + 'px)'; } function calculateVelocityFromDirection() { const angle = dyToVelocity(ball.dy); const cosTheta = Math.cos(angle); const sinTheta = Math.sin(angle); return { dx: (speedOfBall() * cosTheta - gravity * sinTheta) * 3, dy: (speedOfBall() * sinTheta + gravity * cosTheta) * 2 }; } function gravity(dt) { const ballVx = dxToVelocity(ball.dx); const vy = dyToVelocity(ball.dy); let accelX = -40; // max if (vy > 0) accelX = 15; if (vy < 0) accelX *= -1; return accelX * dt; } function updateTime() { requestAnimationFrame(updateGame); const deltaT = performance.now() - lastDeltaTime; lastDeltaTime = performance.now(); dt += deltaT / 60; // ~60 FPS if (dt >= 1) dt = 0; ballVx -= gravity(ball.dxToVelocity(ball.dy)); ballVy -= gravity(ball.dy); } }