diff --git a/nodejs-tools/nodejs-apps/vighnesh153-astro-svelte/src/components/projects/graphics/brick-breaker-game/ProjectRoot.svelte b/nodejs-tools/nodejs-apps/vighnesh153-astro-svelte/src/components/projects/graphics/brick-breaker-game/ProjectRoot.svelte
new file mode 100644
index 00000000..ec57868f
--- /dev/null
+++ b/nodejs-tools/nodejs-apps/vighnesh153-astro-svelte/src/components/projects/graphics/brick-breaker-game/ProjectRoot.svelte
@@ -0,0 +1,46 @@
+
+
+
diff --git a/nodejs-tools/nodejs-apps/vighnesh153-astro-svelte/src/components/projects/graphics/pong-game/ProjectRoot.svelte b/nodejs-tools/nodejs-apps/vighnesh153-astro-svelte/src/components/projects/graphics/pong-game/ProjectRoot.svelte
index b52d6deb..e0bf55b2 100644
--- a/nodejs-tools/nodejs-apps/vighnesh153-astro-svelte/src/components/projects/graphics/pong-game/ProjectRoot.svelte
+++ b/nodejs-tools/nodejs-apps/vighnesh153-astro-svelte/src/components/projects/graphics/pong-game/ProjectRoot.svelte
@@ -9,7 +9,7 @@
let game: PongGame;
function handleMouseMove(e: MouseEvent) {
- game.handleMouseMove(e);
+ game.handleMouseMove(e, document.documentElement.scrollTop);
}
function newGame() {
diff --git a/nodejs-tools/nodejs-apps/vighnesh153-astro-svelte/src/pages/projects/graphics/brick-breaker-game.astro b/nodejs-tools/nodejs-apps/vighnesh153-astro-svelte/src/pages/projects/graphics/brick-breaker-game.astro
new file mode 100644
index 00000000..416ced71
--- /dev/null
+++ b/nodejs-tools/nodejs-apps/vighnesh153-astro-svelte/src/pages/projects/graphics/brick-breaker-game.astro
@@ -0,0 +1,11 @@
+---
+import { graphicsProjectsMap } from '@vighnesh153/graphics-programming';
+import GraphicsProjectLayout from '@/layouts/GraphicsProjectLayout.astro';
+import ProjectRoot from '@/components/projects/graphics/brick-breaker-game/ProjectRoot.svelte';
+
+const project = graphicsProjectsMap.brickBreakerGame;
+---
+
+
+
+
diff --git a/nodejs-tools/nodejs-lib/graphics-programming/src/brick-breaker-game/Ball.ts b/nodejs-tools/nodejs-lib/graphics-programming/src/brick-breaker-game/Ball.ts
new file mode 100644
index 00000000..fbe1ca45
--- /dev/null
+++ b/nodejs-tools/nodejs-lib/graphics-programming/src/brick-breaker-game/Ball.ts
@@ -0,0 +1,61 @@
+import { CanvasWrapper } from '@/canvas-wrapper';
+
+interface BallOptions {
+ readonly size?: number;
+ readonly color?: string;
+ readonly coordinate: Coordinate;
+ readonly velocity?: Velocity;
+}
+
+interface Coordinate {
+ x: number;
+ y: number;
+}
+
+interface Velocity {
+ dx: number;
+ dy: number;
+}
+
+export class Ball {
+ readonly size: number;
+ readonly color: string;
+ readonly coordinate: Coordinate;
+ readonly velocity: Velocity;
+
+ constructor(
+ private readonly canvasWrapper: CanvasWrapper,
+ options: BallOptions
+ ) {
+ this.coordinate = options.coordinate;
+ this.size = options.size ?? 5;
+ this.color = options.color ?? 'black';
+ this.velocity = options.velocity ?? {
+ dx: 5,
+ dy: -5,
+ };
+ }
+
+ draw(): void {
+ const {
+ canvasWrapper,
+ coordinate: { x, y },
+ color,
+ size,
+ } = this;
+ canvasWrapper.drawFilledRect(x, y, size, size, color);
+ }
+
+ update(): void {
+ this.coordinate.x += this.velocity.dx;
+ this.coordinate.y += this.velocity.dy;
+ }
+
+ flipVelocityX(): void {
+ this.velocity.dx *= -1;
+ }
+
+ flipVelocityY(): void {
+ this.velocity.dy *= -1;
+ }
+}
diff --git a/nodejs-tools/nodejs-lib/graphics-programming/src/brick-breaker-game/Brick.ts b/nodejs-tools/nodejs-lib/graphics-programming/src/brick-breaker-game/Brick.ts
new file mode 100644
index 00000000..40a64481
--- /dev/null
+++ b/nodejs-tools/nodejs-lib/graphics-programming/src/brick-breaker-game/Brick.ts
@@ -0,0 +1,57 @@
+import { CanvasWrapper } from '@/canvas-wrapper';
+import { not } from '@vighnesh153/utils';
+
+interface BrickOptions {
+ readonly row: number;
+ readonly column: number;
+ readonly width: number;
+
+ readonly height?: number;
+ readonly color?: string;
+ readonly endPadding?: number;
+
+ readonly visible?: boolean;
+}
+
+export class Brick {
+ readonly row: number;
+ readonly column: number;
+
+ readonly width: number;
+ readonly height: number;
+ readonly color: string;
+ readonly endPadding: number;
+
+ private visible: boolean;
+
+ get isVisible(): boolean {
+ return this.visible;
+ }
+
+ constructor(
+ private readonly canvasWrapper: CanvasWrapper,
+ options: BrickOptions
+ ) {
+ this.row = options.row;
+ this.column = options.column;
+
+ this.width = options.width;
+ this.height = options.height ?? 20;
+ this.color = options.color ?? 'blue';
+ this.endPadding = options.endPadding ?? 1;
+ this.visible = options.visible ?? true;
+ }
+
+ draw(): void {
+ if (not(this.isVisible)) {
+ return;
+ }
+
+ const { canvasWrapper, row, column, width, height, color, endPadding } = this;
+ canvasWrapper.drawFilledRect(column * width, row * height, width - endPadding, height - endPadding, color);
+ }
+
+ destroy(): void {
+ this.visible = false;
+ }
+}
diff --git a/nodejs-tools/nodejs-lib/graphics-programming/src/brick-breaker-game/Game.ts b/nodejs-tools/nodejs-lib/graphics-programming/src/brick-breaker-game/Game.ts
new file mode 100644
index 00000000..7f17ca9e
--- /dev/null
+++ b/nodejs-tools/nodejs-lib/graphics-programming/src/brick-breaker-game/Game.ts
@@ -0,0 +1,250 @@
+import { CanvasWrapper } from '@/canvas-wrapper';
+import { getCanvasBgColor } from '@/getCanvasBgColor';
+import { Paddle } from './Paddle';
+import { Ball } from './Ball';
+import { ScoreTracker } from './ScoreTracker';
+import { Brick } from './Brick';
+import { createBricksGrid } from './createBricksGrid';
+import { not } from '@vighnesh153/utils';
+
+interface BrickBreakerGameOptions {
+ readonly bgColor?: string;
+ readonly scoreColor?: string;
+ readonly scoreFontSize?: number;
+ readonly rows?: number;
+ readonly placeholderBrickRows?: number;
+ readonly columns?: number;
+
+ onGameOver(): void;
+}
+
+export class BrickBreakerGame {
+ readonly canvasWrapper: CanvasWrapper;
+ readonly bgColor: string;
+ readonly scoreColor: string;
+ readonly scoreFontSize: number;
+ readonly rows: number;
+ readonly placeholderBrickRows: number;
+ readonly columns: number;
+ private readonly onGameOver: () => void;
+
+ private paddle!: Paddle;
+ private ball!: Ball;
+
+ private readonly scoreTracker = new ScoreTracker();
+ private bricksGrid!: readonly (readonly Brick[])[];
+
+ private isRunning = false;
+
+ private previousBrickRow: number | null = null;
+ private previousBrickCol: number | null = null;
+
+ constructor(canvasWrapper: CanvasWrapper, options: BrickBreakerGameOptions) {
+ this.canvasWrapper = canvasWrapper;
+
+ this.bgColor = options.bgColor ?? getCanvasBgColor(canvasWrapper);
+ this.scoreColor = options.scoreColor ?? 'black';
+ this.scoreFontSize = options.scoreFontSize ?? 15;
+ this.rows = options.rows ?? 15;
+ this.placeholderBrickRows = options.placeholderBrickRows ?? 3;
+ this.columns = options.columns ?? 20;
+ this.onGameOver = options.onGameOver;
+
+ this.reset();
+ }
+
+ private reset(): void {
+ const { canvasWrapper } = this;
+
+ // reset score
+ this.scoreTracker.reset();
+
+ // reset ball
+ this.ball = new Ball(canvasWrapper, {
+ coordinate: {
+ x: canvasWrapper.width / 2,
+ y: (canvasWrapper.height * 5) / 6,
+ },
+ });
+
+ // reset paddle
+ this.paddle = new Paddle(canvasWrapper, {
+ y: (canvasWrapper.height * 6) / 7,
+ });
+
+ // create bricks grid
+ this.bricksGrid = createBricksGrid(
+ this.rows,
+ this.columns,
+ (row, column) =>
+ new Brick(canvasWrapper, {
+ row,
+ column,
+ width: canvasWrapper.width / 20,
+ visible: row >= this.placeholderBrickRows && row < this.rows - this.placeholderBrickRows,
+ })
+ );
+ }
+
+ *start() {
+ this.isRunning = true;
+ while (this.isRunning) {
+ this.draw();
+ this.update();
+ yield;
+ }
+ }
+
+ stop() {
+ this.isRunning = false;
+ }
+
+ private draw(): void {
+ this.clear();
+ this.paddle.draw();
+ this.ball.draw();
+ this.writeScore();
+ this.drawBricks();
+ }
+
+ private update(): void {
+ this.ball.update();
+ this.handleCollisions();
+ this.handleGameOver();
+ }
+
+ handleMouseMove(e: MouseEvent, scrollLeft: number) {
+ const { rect } = this.canvasWrapper;
+ const mouseX = e.clientX - rect.left - scrollLeft;
+
+ // we subtract width/2 to align the mouse position with pad's center
+ const paddleX = mouseX - this.paddle.width / 2;
+
+ this.paddle.updateX(paddleX);
+ }
+
+ clear() {
+ const rect = this.canvasWrapper.getBoundingClientRect();
+ const canvasWidth = rect.width;
+ const canvasHeight = rect.height;
+ this.canvasWrapper.drawFilledRect(0, 0, canvasWidth, canvasHeight, this.bgColor);
+ }
+
+ private writeScore(): void {
+ const cw = this.canvasWrapper;
+ const color = this.scoreColor;
+ const fontSize = this.scoreFontSize;
+
+ const scoreText = `Your score: ${this.scoreTracker.getScore()}`;
+
+ cw.writeText(scoreText, 20, 20, color, fontSize);
+ }
+
+ private drawBricks(): void {
+ for (const bricksRow of this.bricksGrid) {
+ for (const brick of bricksRow) {
+ brick.draw();
+ }
+ }
+ }
+
+ private handleGameOver(): void {
+ const maxScore = this.rows * this.columns;
+ if (this.scoreTracker.getScore() === maxScore) {
+ this.onGameOver();
+ }
+ }
+
+ private handleCollisions(): void {
+ this.handleCollisionWithLeftWall();
+ this.handleCollisionWithRightWall();
+ this.handleCollisionWithTopWall();
+ this.handleCollisionWithBottomWall();
+ this.handleCollisionWithPaddle();
+ this.handleBrickCollision();
+ }
+
+ private handleCollisionWithLeftWall(): void {
+ if (this.ball.coordinate.x <= 0) {
+ this.ball.flipVelocityX();
+ }
+ }
+
+ private handleCollisionWithRightWall(): void {
+ if (this.ball.coordinate.x + this.ball.size >= this.canvasWrapper.width) {
+ this.ball.flipVelocityX();
+ }
+ }
+
+ private handleCollisionWithTopWall(): void {
+ if (this.ball.coordinate.y <= 0) {
+ this.ball.flipVelocityY();
+ }
+ }
+
+ private handleCollisionWithBottomWall(): void {
+ if (this.ball.coordinate.y + this.ball.size < this.canvasWrapper.height) {
+ return;
+ }
+
+ this.reset();
+ }
+
+ private handleCollisionWithPaddle(): void {
+ const { ball, paddle } = this;
+
+ const ballLeft = ball.coordinate.x;
+ const ballRight = ballLeft + ball.size;
+ const ballTop = ball.coordinate.y;
+ const ballBottom = ballTop + ball.size;
+
+ const paddleLeft = paddle.x;
+ const paddleRight = paddleLeft + paddle.width;
+ const paddleTop = paddle.y;
+ const paddleBottom = paddleTop + paddle.height;
+
+ if (ballRight < paddleLeft) return;
+ if (ballLeft > paddleRight) return;
+ if (ballBottom < paddleTop) return;
+ if (ballTop > paddleBottom) return;
+
+ // collided with paddle
+ ball.flipVelocityY();
+ }
+
+ private handleBrickCollision(): void {
+ const { previousBrickRow, previousBrickCol } = this;
+ const [[randomBrick]] = this.bricksGrid;
+ const currentBrickRow = Math.floor(this.ball.coordinate.y / randomBrick.height);
+ const currentBrickCol = Math.floor(this.ball.coordinate.x / randomBrick.width);
+ const currentBrick = this.bricksGrid[currentBrickRow]?.[currentBrickCol] ?? null;
+
+ if (
+ previousBrickCol === null ||
+ previousBrickRow === null ||
+ currentBrick === null ||
+ not(currentBrick.isVisible)
+ ) {
+ this.previousBrickRow = currentBrickRow;
+ this.previousBrickCol = currentBrickCol;
+ return;
+ }
+
+ if (currentBrickRow !== previousBrickRow && currentBrickCol !== previousBrickCol) {
+ // ball is travelling diagonally
+ this.ball.flipVelocityX();
+ this.ball.flipVelocityY();
+ } else if (currentBrickRow !== previousBrickRow) {
+ // ball is travelling more vertically
+ this.ball.flipVelocityY();
+ } else if (currentBrickCol !== previousBrickCol) {
+ // ball is travelling more horzontally
+ this.ball.flipVelocityX();
+ }
+
+ currentBrick.destroy();
+ this.scoreTracker.increment();
+ this.previousBrickRow = currentBrickRow;
+ this.previousBrickCol = currentBrickCol;
+ }
+}
diff --git a/nodejs-tools/nodejs-lib/graphics-programming/src/brick-breaker-game/Paddle.ts b/nodejs-tools/nodejs-lib/graphics-programming/src/brick-breaker-game/Paddle.ts
new file mode 100644
index 00000000..c2e258f9
--- /dev/null
+++ b/nodejs-tools/nodejs-lib/graphics-programming/src/brick-breaker-game/Paddle.ts
@@ -0,0 +1,47 @@
+import { CanvasWrapper } from '@/canvas-wrapper';
+
+interface PaddleOptions {
+ readonly width?: number;
+ readonly height?: number;
+ readonly color?: string;
+
+ readonly initialX?: number;
+ readonly y: number;
+}
+
+export class Paddle {
+ readonly width: number;
+ readonly height: number;
+ readonly color: string;
+ readonly y: number;
+
+ x: number;
+
+ constructor(
+ private readonly canvasWrapper: CanvasWrapper,
+ options: PaddleOptions
+ ) {
+ this.width = options.width ?? canvasWrapper.width / 5;
+ this.height = options.height ?? 3;
+ this.color = options.color ?? 'black';
+
+ this.x = options.initialX ?? (canvasWrapper.width - this.width) / 2;
+ this.y = options.y;
+ }
+
+ draw(): void {
+ const { width, height, color, x, y, canvasWrapper } = this;
+
+ canvasWrapper.drawFilledRect(x, y, width, height, color);
+ }
+
+ updateX(x: number): void {
+ this.x = x;
+ this.coerceX();
+ }
+
+ private coerceX(): void {
+ this.x = Math.max(0, this.x);
+ this.x = Math.min(this.canvasWrapper.width - this.width, this.x);
+ }
+}
diff --git a/nodejs-tools/nodejs-lib/graphics-programming/src/brick-breaker-game/ScoreTracker.ts b/nodejs-tools/nodejs-lib/graphics-programming/src/brick-breaker-game/ScoreTracker.ts
new file mode 100644
index 00000000..69aeb67e
--- /dev/null
+++ b/nodejs-tools/nodejs-lib/graphics-programming/src/brick-breaker-game/ScoreTracker.ts
@@ -0,0 +1,15 @@
+export class ScoreTracker {
+ private score = 0;
+
+ getScore(): number {
+ return this.score;
+ }
+
+ increment(): void {
+ this.score++;
+ }
+
+ reset(): void {
+ this.score = 0;
+ }
+}
diff --git a/nodejs-tools/nodejs-lib/graphics-programming/src/brick-breaker-game/createBricksGrid.ts b/nodejs-tools/nodejs-lib/graphics-programming/src/brick-breaker-game/createBricksGrid.ts
new file mode 100644
index 00000000..c8a7f6c4
--- /dev/null
+++ b/nodejs-tools/nodejs-lib/graphics-programming/src/brick-breaker-game/createBricksGrid.ts
@@ -0,0 +1,17 @@
+import { repeat } from '@vighnesh153/utils';
+import { Brick } from './Brick';
+
+type BrickCreator = (row: number, column: number) => Brick;
+
+export function createBricksGrid(rows: number, columns: number, brickCreator: BrickCreator): Brick[][] {
+ const grid = new Array(rows);
+ repeat(rows, (rowCount) => {
+ const row = rowCount - 1;
+ grid[row] = new Array(columns);
+ repeat(columns, (colCount) => {
+ const col = colCount - 1;
+ grid[row][col] = brickCreator(row, col);
+ });
+ });
+ return grid;
+}
diff --git a/nodejs-tools/nodejs-lib/graphics-programming/src/brick-breaker-game/index.ts b/nodejs-tools/nodejs-lib/graphics-programming/src/brick-breaker-game/index.ts
new file mode 100644
index 00000000..e171380c
--- /dev/null
+++ b/nodejs-tools/nodejs-lib/graphics-programming/src/brick-breaker-game/index.ts
@@ -0,0 +1 @@
+export { BrickBreakerGame } from './Game';
diff --git a/nodejs-tools/nodejs-lib/graphics-programming/src/collection.ts b/nodejs-tools/nodejs-lib/graphics-programming/src/collection.ts
index 70cc6d77..70733c35 100644
--- a/nodejs-tools/nodejs-lib/graphics-programming/src/collection.ts
+++ b/nodejs-tools/nodejs-lib/graphics-programming/src/collection.ts
@@ -13,7 +13,7 @@ const barnsleysFern: GraphicsProject = {
};
const brickBreakerGame: GraphicsProject = {
- imageLink: 'https://i.imgur.com/C0N3twn.png',
+ imageLink: 'https://i.imgur.com/yauRTBv.png',
title: 'Brick Breaker Game',
id: 'brick-breaker-game',
description: 'Brick breaker is a arcade game emulation, which destroys the bricks by shooting a ball at them',
diff --git a/nodejs-tools/nodejs-lib/graphics-programming/src/index.ts b/nodejs-tools/nodejs-lib/graphics-programming/src/index.ts
index b90afa7d..95886b76 100644
--- a/nodejs-tools/nodejs-lib/graphics-programming/src/index.ts
+++ b/nodejs-tools/nodejs-lib/graphics-programming/src/index.ts
@@ -1,5 +1,6 @@
export * from './barnsleys-fern';
export * from './bonding-particles';
+export * from './brick-breaker-game';
export * from './flappy-block-game';
export * from './grid-path-finder';
export * from './pong-game';
diff --git a/nodejs-tools/nodejs-lib/graphics-programming/src/pong-game/Arena.ts b/nodejs-tools/nodejs-lib/graphics-programming/src/pong-game/Arena.ts
index 444049fa..11998a17 100644
--- a/nodejs-tools/nodejs-lib/graphics-programming/src/pong-game/Arena.ts
+++ b/nodejs-tools/nodejs-lib/graphics-programming/src/pong-game/Arena.ts
@@ -150,11 +150,11 @@ export class Arena {
}
}
- handleMouseMove(e: MouseEvent) {
+ handleMouseMove(e: MouseEvent, scrollTop: number) {
const { rect } = this.#canvasWrapper;
- const mouseY = e.clientY - rect.top - document.documentElement.scrollTop;
+ const mouseY = e.clientY - rect.top - scrollTop;
- // we subtract y to align the mouse position with pad's center
+ // we subtract height/2 to align the mouse position with pad's center
const playerPadY = mouseY - this.#playerPad.height / 2;
this.#playerPad.setPosition(playerPadY);
diff --git a/nodejs-tools/nodejs-lib/graphics-programming/src/pong-game/Game.ts b/nodejs-tools/nodejs-lib/graphics-programming/src/pong-game/Game.ts
index dd0372e2..b1725485 100644
--- a/nodejs-tools/nodejs-lib/graphics-programming/src/pong-game/Game.ts
+++ b/nodejs-tools/nodejs-lib/graphics-programming/src/pong-game/Game.ts
@@ -64,7 +64,7 @@ export class PongGame {
this.#canvasWrapper.drawFilledRect(0, 0, canvasWidth, canvasHeight, this.#bgColor);
}
- handleMouseMove(e: MouseEvent) {
- this.#arena.handleMouseMove(e);
+ handleMouseMove(e: MouseEvent, scrollTop: number) {
+ this.#arena.handleMouseMove(e, scrollTop);
}
}