<html>
<head>
<title>Breakout</title>
<style>
canvas {
display: block;
margin: auto;
}
</style>
</head>
<body>
<canvas width="500" height="500"></canvas>
<script src="breakout.js"></script>
</body>
</html>
// Repository: https://codeberg.org/dandeto/breakout
// Tutorial: https://www.scipress.io/post/lAnW065v4Sat48ljAgqa/html5-canvas-breakout
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const canvasBoundingRect = canvas.getBoundingClientRect();
const width = Number(canvas.width);
const height = Number(canvas.height);
const colors = [
{ light: "#f00", dark: "#600", color: "#c00" },
{ light: "#ff0", dark: "#660", color: "#cc0" },
{ light: "#0f0", dark: "#060", color: "#0c0" },
{ light: "#0ff", dark: "#066", color: "#0cc" },
{ light: "#00f", dark: "#006", color: "#00c" },
{ light: "#f0f", dark: "#606", color: "#c0c" },
];
const sfx = {
shoot: "https://codeberg.org/dandeto/breakout/raw/branch/master/sfx/shoot.ogg",
paddle_bounce: "https://codeberg.org/dandeto/breakout/raw/branch/master/sfx/paddle-bounce.ogg",
wall_bounce: "https://codeberg.org/dandeto/breakout/raw/branch/master/sfx/wall-bounce.ogg",
brick_break: "https://codeberg.org/dandeto/breakout/raw/branch/master/sfx/brick-break.ogg",
lose: "https://codeberg.org/dandeto/breakout/raw/branch/master/sfx/you-lose.ogg",
win: "https://codeberg.org/dandeto/breakout/raw/branch/master/sfx/you-win.ogg",
play: function(url) {
let audio = new Audio(url);
audio.play();
},
preload: function() {
new Audio(this.shoot);
new Audio(this.paddle_bounce);
new Audio(this.wall_bounce);
new Audio(this.brick_break);
new Audio(this.lose);
new Audio(this.win);
}
};
class Player {
constructor() {
this.w = 75;
this.h = 10;
this.x = width / 2 - this.w / 2;
this.y = height - 25;
this.speed = 400;
this.lives = 3;
}
}
class Ball {
constructor(x, y) {
this.r = 5;
this.x = x;
this.y = y - this.r;
this.velocity = { x: 0, y: 0 };
this.speed = 200;
this.scaleSpeed = 7;
this.active = false;
}
}
class Brick {
static w = 50;
static h = 20;
static borderWidth = 5;
constructor(x, y) {
this.x = x;
this.y = y;
}
}
let player;
let ball;
let bricks;
let totalBricks;
let brokenBricks;
let score;
let gameOver;
let initialized = false;
// Event Listeners
let mouse = {
x: width / 2,
y: 0,
click: false,
};
canvas.addEventListener("mousemove", e => {
mouse.x = e.clientX - canvasBoundingRect.x;
mouse.y = e.clientY - canvasBoundingRect.y;
});
canvas.addEventListener("mousedown", e => {
if (e.which == 1) mouse.click = true;
});
canvas.addEventListener("mouseup", e => {
if (e.which == 1) mouse.click = false;
});
// Lifecycle methods
function setup() {
player = new Player();
ball = new Ball(getCenter(player.x, player.w), player.y);
let bricksAcross = width / Brick.w;
let bricksDown = colors.length;
let offset = {
x: 0,
y: 50
}
bricks = createBricks(bricksDown, bricksAcross, offset);
totalBricks = bricks.length * bricks[0].length;
score = 0;
brokenBricks = 0;
gameOver = false;
mouse.click = false;
if (!initialized)
requestAnimationFrame(initAnimation);
}
function update(dt) {
// Check if the game is over or not
if (gameOver) {
if (mouse.click) {
setup();
}
return;
}
let playerCenter = getCenter(player.x, player.w);
// Move player to the right
if (mouse.x > playerCenter) {
player.x += player.speed * dt;
if (getCenter(player.x, player.w) > mouse.x) {
player.x = mouse.x - player.w / 2;
}
}
// Move player to the left
else if (mouse.x < playerCenter) {
player.x -= player.speed * dt;
if (getCenter(player.x, player.w) < mouse.x) {
player.x = mouse.x - player.w / 2;
}
}
// Bound the player
if (player.x < 0)
player.x = 0;
else if (player.x + player.w > width)
player.x = width - player.w;
// Activate the ball
if (mouse.click && !ball.active) {
ball.active = true;
ball.velocity.y = -ball.speed;
sfx.play(sfx.shoot);
}
// Update the ball's position
playerCenter = getCenter(player.x, player.w);
if (ball.active) {
ball.x += ball.velocity.x * dt;
ball.y += ball.velocity.y * dt;
} else {
ball.x = playerCenter;
}
// Bound the ball left
if (ball.x - ball.r < 0) {
ball.x = ball.r;
ball.velocity.x *= -1;
sfx.play(sfx.wall_bounce);
}
// Bound the ball left
else if (ball.x + ball.r > width)
{
ball.x = width - ball.r;
ball.velocity.x *= -1;
sfx.play(sfx.wall_bounce);
}
// Bound the ball top
if (ball.y - ball.r < 0) {
ball.y = ball.r;
ball.velocity.y *= -1;
sfx.play(sfx.wall_bounce);
}
// Falls below player
else if (ball.y - ball.r > height) {
--player.lives;
if (player.lives == 0) {
gameOver = true;
sfx.play(sfx.lose);
}
ball = new Ball(playerCenter, player.y);
}
// Ball hits paddle
if (ball.x + ball.r > player.x &&
ball.x - ball.r < player.x + player.w &&
ball.y + ball.r > player.y &&
ball.y + ball.r < player.y + player.h)
{
ball.velocity.y *= -1;
ball.y = player.y - ball.r; //snap to top
ball.velocity.x = (ball.x - playerCenter) * ball.scaleSpeed;
sfx.play(sfx.paddle_bounce);
}
// Ball hits brick
loop:
for (let i = 0; i < bricks.length; ++i) {
for (let j = 0; j < bricks[i].length; ++j) {
let brick = bricks[i][j];
if (!brick) continue;
// If the following "if" statements fail, the circle is inside the rectangle
// Default vertex to ball x and y, so that the collision will return true
let vertex = {
x: ball.x,
y: ball.y
};
// Determine which side of the brick is closest
let right = ball.x > brick.x + Brick.w;
let left = ball.x < brick.x;
let bottom = ball.y > brick.y + Brick.h;
let top = ball.y < brick.y;
// Choose the vertex which is closest to the vertex
if (right) vertex.x = brick.x + Brick.w;
else if (left) vertex.x = brick.x;
if (bottom) vertex.y = brick.y + Brick.h;
else if (top) vertex.y = brick.y;
// Collision if distance from center of circle is less than the radius of the circle
const collision = (ball.x - vertex.x) * (ball.x - vertex.x) + (ball.y - vertex.y) * (ball.y - vertex.y) < ball.r * ball.r;
if (collision) {
bricks[i][j] = 0;
score += 100;
++brokenBricks;
// If all bricks have been cleared, the player won!
if (brokenBricks == totalBricks) {
gameOver = true;
sfx.play(sfx.win);
return;
}
sfx.play(sfx.brick_break);
let neighbors = {}; // determine if there are bricks on any sides
if (i-1 >= 0 && bricks[i-1][j] !== 0) neighbors.top = true;
if (i+1 < bricks.length && bricks[i+1][j] !== 0) neighbors.bottom = true;
if (j-1 >= 0 && bricks[i][j-1] !== 0) neighbors.left = true;
if (j+1 < bricks[i].length && bricks[i][j+1] !== 0) neighbors.right = true;
// Set the velocity if it hits a corner based on the velocity and if there is a neighbor
// Otherwise we get the bizzare behavior of bouncing off a corner of the block regardless
// whether another block is next to it or not.
if (right && bottom) { // -, -
if (ball.velocity.x < 0 && !neighbors.right) ball.velocity.x *= -1; //+
if (ball.velocity.y < 0 && !neighbors.bottom) ball.velocity.y *= -1; //+
} else if (right && top) { // -, +
if (ball.velocity.x < 0 && !neighbors.right) ball.velocity.x *= -1; //+
if (ball.velocity.y > 0 && !neighbors.top) ball.velocity.y *= -1; //-
} else if (left && bottom) { // +, -
if (ball.velocity.x > 0 && !neighbors.left) ball.velocity.x *= -1; //-
if (ball.velocity.y < 0 && !neighbors.bottom) ball.velocity.y *= -1; //+
} else if (left && top) { // +, +
if (ball.velocity.x > 0 && !neighbors.left) ball.velocity.x *= -1; //-
if (ball.velocity.y > 0 && !neighbors.top) ball.velocity.y *= -1; //-
}
// Exclusively a single edge touching
else {
if (right || left) {
ball.velocity.x *= -1;
}
else if (top || bottom) {
ball.velocity.y *= -1;
}
}
// stop checking collisions once one is found.
break loop;
}
}
}
}
function render() {
// background
ctx.fillStyle = "black";
ctx.fillRect(0, 0, width, height);
// paddle
ctx.fillStyle = "white";
ctx.fillRect(player.x, player.y, player.w, player.h);
// ball
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, Math.PI * 2);
ctx.fill();
// bricks
for (let i = 0; i < bricks.length; ++i) {
for (let brick of bricks[i]) {
if (!brick) continue;
ctx.fillStyle = colors[i].color;
ctx.fillRect(brick.x, brick.y, Brick.w, Brick.h);
// Top left polygon
ctx.fillStyle = colors[i].light;
ctx.beginPath();
ctx.moveTo(brick.x, brick.y + Brick.h);
ctx.lineTo(brick.x + Brick.borderWidth, brick.y + Brick.h - Brick.borderWidth);
ctx.lineTo(brick.x + Brick.borderWidth, brick.y);
ctx.lineTo(brick.x + Brick.borderWidth, brick.y + Brick.borderWidth);
ctx.lineTo(brick.x + Brick.w, brick.y + Brick.borderWidth);
ctx.lineTo(brick.x + Brick.w, brick.y);
ctx.lineTo(brick.x, brick.y);
ctx.closePath();
ctx.fill();
// Bottom right polygon
ctx.fillStyle = colors[i].dark;
ctx.beginPath();
ctx.moveTo(brick.x, brick.y + Brick.h);
ctx.lineTo(brick.x + Brick.borderWidth, brick.y + Brick.h - Brick.borderWidth);
ctx.lineTo(brick.x + Brick.w - Brick.borderWidth, brick.y + Brick.h - Brick.borderWidth);
ctx.lineTo(brick.x + Brick.w - Brick.borderWidth, brick.y + Brick.borderWidth);
ctx.lineTo(brick.x + Brick.w, brick.y);
ctx.lineTo(brick.x + Brick.w, brick.y + Brick.h);
ctx.closePath();
ctx.fill();
}
}
// text
ctx.font = "20px sans-serif";
ctx.fillStyle = "yellow";
ctx.textAlign = "left";
ctx.fillText(`Lives: ${player.lives}`, 10, 20);
ctx.textAlign = "right";
ctx.fillText(`Score: ${score}`, width - 10, 20);
if (gameOver) {
ctx.font = "50px sans-serif";
ctx.fillStyle = "red";
ctx.textAlign = "center";
let text = brokenBricks == totalBricks ? "You Win!" : "Game Over!";
ctx.fillText(text, width / 2, height / 2);
ctx.font = "25px sans-serif";
ctx.fillText("Click to Restart", width / 2, height / 2 + 50);
}
}
const frameRate = 1 / 60;
const maxDelta = frameRate * 3;
let lastTimeStamp = 0;
let dt = 0;
function main(timeStamp) {
requestAnimationFrame(main);
// timeElapsed is the time between the last and current frame.
// The timestamps are in units of milliseconds. Divide by 1000 to get into units of seconds.
let timeElapsed = (timeStamp - lastTimeStamp) / 1000;
// Bound delta-time (dt) to the max allowed delta
if (timeElapsed < maxDelta)
dt += timeElapsed;
else
dt += maxDelta;
lastTimeStamp = timeStamp;
// Update the simulation as many times as needed
while (dt > frameRate) {
update(frameRate);
dt -= frameRate;
}
// Render after update
render();
}
function initAnimation(timeStamp) {
lastTimeStamp = timeStamp;
initialized = false;
requestAnimationFrame(main);
};
sfx.preload();
setup();
// Utility functions
function createBricks(bricksDown, bricksAcross, offset) {
let matrix = [];
for (var y = 0; y < bricksDown; ++y) {
matrix[y] = [];
for (let x = 0; x < bricksAcross; ++x) {
let xPosition = x * Brick.w + offset.x;
let yPosition = y * Brick.h + offset.y;
matrix[y].push(new Brick(xPosition, yPosition));
}
}
return matrix;
}
function getCenter(x, w) {
return x + w / 2;
}
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.