<html lang="en-us">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <title>Bouncing balls</title>
    <link rel="stylesheet" href="style.css">
  </head>

  <body>
    <h1>bouncing balls</h1>
    <p>Ball count: </p>
    <canvas></canvas>

    <script src="main5.js"></script>
  </body>
</html>
html, body {
  margin: 0;
}

html {
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
  height: 100%;
}

body {
  overflow: hidden;
  height: inherit;
}

h1 {
  font-size: 2rem;
  letter-spacing: -1px;
  position: absolute;
  margin: 0;
  top: -4px;
  right: 5px;

  color: transparent;
  text-shadow: 0 0 4px white;
}

/* 5. Implementing the score counter */
p {
  position: absolute;
  margin: 0;
  top: 35px;
  right: 5px;
  color: #aaa;
}
// set up canvas

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');

const width = canvas.width = window.innerWidth;
const height = canvas.height = window.innerHeight;

// function to generate random number

function random(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

// function to generate random RGB color value

function randomRGB() {
  return `rgb(${random(0, 255)},${random(0, 255)},${random(0, 255)})`;
}

// 5. Ball count (1/3)

const para = document.querySelector('p');
let count = 0;

// 1. Create a Shape class

class Shape {

   constructor(x, y, velX, velY) {
      this.x = x;
      this.y = y;
      this.velX = velX;
      this.velY = velY;
   }
}

class Ball extends Shape {

   constructor(x, y, velX, velY, color, size) {
      super(x, y, velX, velY);
      this.color = color;
      this.size = size;
      this.exists = true; // NOTE: couldn't figure this out by myself
   }

   draw() {
      ctx.beginPath();
      ctx.fillStyle = this.color;
      ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI);
      ctx.fill();
   }

   update() {
      if ((this.x + this.size) >= width) {
         this.velX = -(this.velX);
      }

      if ((this.x - this.size) <= 0) {
         this.velX = -(this.velX);
      }

      if ((this.y + this.size) >= height) {
         this.velY = -(this.velY);
      }

      if ((this.y - this.size) <= 0) {
         this.velY = -(this.velY);
      }

      this.x += this.velX;
      this.y += this.velY;
   }

   collisionDetect() {
      for (const ball of balls) {
         if (!(this === ball) && ball.exists) {
            const dx = this.x - ball.x;
            const dy = this.y - ball.y;
            const distance = Math.sqrt(dx * dx + dy * dy);

            if (distance < this.size + ball.size) {
              ball.color = this.color = randomRGB();
            }
         }
      }
   }

}

// 2. Defining EvilCircle

class EvilCircle extends Shape {

   constructor(x, y) {
      super(x, y, 20, 20);
      this.color = "white"; // NOTE: I originally forgot to add quote marks
      this.size = 10;
      window.addEventListener('keydown', (e) => {
         switch(e.key) {
           case 'a':
             this.x -= this.velX;
             break;
           case 'd':
             this.x += this.velX;
             break;
           case 'w':
             this.y -= this.velY;
             break;
           case 's':
             this.y += this.velY;
             break;
         }
       });       
   }
   
   // 3. Defining methods for EvilCircle

   draw() {
      ctx.beginPath();
      ctx.lineWidth = 3;
      ctx.strokeStyle = this.color;
      ctx.arc(this.x, this.y, this.size, 0, 2 * Math.PI);
      ctx.stroke();
   }

   checkBounds() {
      if ((this.x + this.size) >= width) {
         this.x -= this.size;
      }

      if ((this.x - this.size) <= 0) {
         this.x += this.size;
      }

      if ((this.y + this.size) >= height) {
         this.y -= this.size;
      }

      if ((this.y - this.size) <= 0) {
         this.y += this.size;
      }
   }

   collisionDetect() {
      for (const ball of balls) {
         if (ball.exists) {  // NOTE: I wasn't sure but was correct
            const dx = this.x - ball.x;
            const dy = this.y - ball.y;
            const distance = Math.sqrt(dx * dx + dy * dy);

            if (distance < this.size + ball.size) {
              ball.exists = false; 
              /* QUESTION 1: I originally wrote "ball.exists === false" but then the count kept declining. 
              Why should this be "=" instead of "==="?
              
              Answer 1:
              We don’t want to compare the values, we want to assign false to ball.exists. 
              The reason is that when the condition (distance < this.size + ball.size) is met, 
              we have a collision and want to remove the ball. 
              Therefor we set its exists property to false.
              */
              
              // 5. Ball count (2/3)
              count--;
              para.textContent = `Ball count: ${count}`;
              /* QUESTION 2: Is there any way I don't have to re-write "Ball count: " 
              but somehow utilise the text in <p></p> in HTML?
              
              Answer 2:
              If you absolutely want to, you could have a span inside the para and only change its content:
              
              HTML:
                <p>Ball count: <span></span></p>
              
              JS:
                const span = document.querySelector('span');
                ...
                span.textContent = count;
              */
              
            }
         }
      }
   }
}

const balls = [];

while (balls.length < 25) {
   const size = random(10,20);
   const ball = new Ball(
      // ball position always drawn at least one ball width
      // away from the edge of the canvas, to avoid drawing errors
      random(0 + size, width - size),
      random(0 + size, height - size),
      random(-7,7),
      random(-7,7),
      randomRGB(),
      size
   );

  balls.push(ball);
  // 5. Ball count (3/3)
  count++;
  para.textContent = `Ball count: ${count}`;
}

// 4. Bringing the evil circle into the program
const evilCircle = new EvilCircle(
   random(0, width), random(0, height)
   /* QUESTION 3: I originally set the x and y of evil circle as below,
   but that way, nothing was displayed on screen. Why does this not work?
   random(0 + size, width - size),
   random(0 + size, height - size) 
   
   Answer 3:
   When you encounter such errors I recommend looking at the console. 
   There we see: “ReferenceError: size is not defined”. 
   There reason is that the size variable is defined inside the EvilCircle class 
   and is unknown to the global code that creates the evil circle.
   */
);

function loop() {
   ctx.fillStyle = 'rgba(0, 0, 0, 0.25)';
   ctx.fillRect(0, 0,  width, height);

   for (const ball of balls) {
      if(ball.exists) {
         ball.draw();
         ball.update();
         ball.collisionDetect();
      } 
   }

   evilCircle.draw();
   evilCircle.checkBounds();
   evilCircle.collisionDetect();

   requestAnimationFrame(loop);
}

loop();

/* NOTE: my original attempt of 5. Ball count (didn't work)
const para = document.querySelector('p');
let count = balls.length; <- This was incorrect!
for (ball of balls) {
   if (ball.exist) { <- This doesn't mean when a ball is pushed
      count++;
   } else if (!ball.exist) {
      count--;
   }
}
para.textContent += `${count}`; <- This showed all numbers from 0 to 25
*/
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.