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);
}
}