Clicking Circle Game Javascript

Viewed 161

I'm trying to make a simple interactive game where there are circles of different colours moving in the canvas and when the user clicks on the blue circles, it logs the number of clicks on the screen. When clicking circles with any other colour, the animation stops.

I'm very new to javascript but this is what I have for now. I've made a function with random coloured circles and a function with blue circles moving but I'm totally stuck on how to stop the animation when clicking on the function with a random coloured circles and logging the amount of clicks on the blue circles. If someone could help me move forward with it in any way (doesn't have to be the full thing), that would be awesome, thanks.

JS

var canvas;
var ctx;
var w = 1000;
var h = 600;
var colours = ["red", "blue"];
var allCircles = []; 


for(var i=0; i<1; i++){
    setTimeout(function(){console.log(i)},1000);
    }
    
document.querySelector("#myCanvas").onclick = click;
createData(2);
createDataTwo(20);
setUpCanvas();
animationLoop();

function animationLoop(){
    clear();
    for(var i = 0; i<allCircles.length; i++){
    circle(allCircles[i]);
    forward(allCircles[i], 5)  
    turn(allCircles[i], randn(30));
    collisionTestArray(allCircles[i],allCircles)
    bounce(allCircles[i]);
}

    requestAnimationFrame(animationLoop);
}


function collisionTestArray(o, a){
    for(var i=0; i<a.length; i++){
        if(o !=a[i]){
            collision(o,a[i]);
        }
        
    }
}




function collision(o1,o2){
    if(o1 && o2){
    var differencex = Math.abs(o1.x-o2.x);
    var differencey = Math.abs(o1.y-o2.y);
    var hdif = Math.sqrt(differencex*differencex+differencey*differencey);
    if(hdif<o1.r+o2.r){
        if(differencex < differencey){
            turn(o1, 180-2*o1.angle);
            turn(o2, 180-2*o2.angle);
        }else{
            turn(o1, 360-2*o1.angle);
            turn(o2, 360-2*o2.angle);
        }
        turn(o1, 180);
        turn(o2, 180);
        console.log("collision");
    };
    }
}


function click(event){
  clear()
}




function bounce (o){
    if(o.x > w || o.x < 0){
        turn(o, 180-2*o.angle);
    };
    if(o.y > h || o.y < 0){
        turn(o, 360-2*o.angle);
    }
}
function clear(){
    ctx.clearRect(0,0,w,h);
}
function stop (){
    o1.changex = 0;
    o1.changey = 0;
    o2.changex = 0;
    o2.changey = 0;
}

function circle (o){
    var x = o.x; 
    var y = o.y;
    var a = o.angle;
    var d = o.d;
    ctx.beginPath();
    ctx.arc(o.x,o.y,o.r,0,2*Math.PI);
    ctx.fillStyle = "hsla("+o.c+",100%,50%, "+o.a+")";
    ctx.fill();


    o.x = x;
    o.y = y;
    o.angle = a;
    o.d = d;
}


function  createData(num){
    for(var i=0; i<num; i++){
        allCircles.push({

                    "x": rand(w),
                    "changex": rand(10),
                    "y":rand(h),
                     "changex": rand(10),
                     "w": randn(w),
                    "h": randn(h),
                    "d": 1, 
                     "a": 1,
                     "angle": 0,
                     "changle":15,
                     "c":216,
                     "r": 50
         }
        )
    }
}

function  createDataTwo(num){
    for(var i=0; i<num; i++){
        allCircles.push({

                    "x": rand(w),
                    "changex": rand(10),
                    "y":rand(h),
                     "changex": rand(10),
                     "w": randn(w),
                    "h": randn(h),
                    "d": 1, 
                     "a": 1,
                     "angle": 0,
                     "changle":15,
                     "c":rand(90),
                     "r": 50
         }
        )
    }
}

function turn(o,angle){
    if(angle != undefined){ 
        o.changle=angle; 
    };
        o.angle+=o.changle;
    }


function forward(o,d){ 
    var changeX;
    var changeY;
    var oneDegree = Math.PI/180; 
    if(d != undefined){
        o.d = d;
    };
        changeX= o.d*Math.cos(o.angle*oneDegree);
        changeY = o.d*Math.sin(o.angle*oneDegree);
        o.x+=changeX; 
        o.y+=changeY;
}


function randn(r){
    var result = Math.random()*r - r/2
    return result
}

function randi(r) {
    var result = Math.floor(Math.random()*r); 
    return result
}


function rand(r){
    return Math.random()*r
}




function setUpCanvas(){
    canvas = document.querySelector("#myCanvas");
    ctx = canvas.getContext("2d");
    canvas.width = w;
    canvas.height = h;
    canvas.style.border = "5px solid orange"
    ctx.fillStyle = "blue";
    ctx.fillRect(0, 0, w, h);
}

console.log("assi4")

HTML

<html>
    <head>
        <link rel="stylesheet" type="text/css" href="../modules.css">
    </head>
    <body>
        <div id="container">
            <h1>Click the Blue Circles Only</h1>
            <canvas id = "myCanvas"></canvas>
        </div>
        <script src="assi5.js"></script>
    </body>
</html>

CSS

#container {
    margin: auto;
    width: 75%;
    text-align: center;
}
2 Answers

You can use cancelAnimationFrame to stop the animation when a non-blue circle is clicked

You need to pass it a reference to the frame ID returned from requestAnimationFrame for it to work.

In order to tell if a circle was clicked, you need to check the coordinates of each circle against the coordinates of the click.

I have an example below if you had your blue circles in an array "blue", and other circles in array "other", the ID returned by requestAnimationFrame as "frame".

The check function returns the number of blue circles hit (the points scored) and if any other circles were hit, it stops the animation.

getCoords returns the coordinates of the click on the canvas from the click event.

canvas.addEventListener('click', event=>{
    points += check(getCoords(event), blue, other, frame);
    document.getElementById('points').textContent = points;
})

function check({x, y}, blue, other, frame) {
    other.filter(circle=>circle.isWithin(x, y))
        .length && cancelAnimationFrame(frame); // This is where animation stops
    return blue.filter(circle=>circle.isWithin(x, y)).length;
}

function getCoords(event) {
    const canvas = event.target;
    const rect = canvas.getBoundingClientRect()
    const x = event.clientX - rect.left;
    const y = event.clientY - rect.top;
    return { x, y };
}

I have an example that works below where I changed the circles to the result of a function rather than an inline object, and moved the functions you use on them into their own class. You don't have to do this, but I find it a lot easier to understand.

function main() {
  const canvas = document.getElementById('canvas');
  const context = canvas.getContext("2d");
  const clear = () => context.clearRect(0, 0, canvas.width, canvas.height);
  const blue = new Array(2).fill().map(() => new Circle(context, 216));
  const other = new Array(10).fill().map(() => new Circle(context));
  let circles = [...blue, ...other];
  let frame = 0;
  let points = 0;

  // Move the circle a bit and check if it needs to bounce
  function update(circle) {
    circle.forward(1)
    circle.turn(30, true)
    circle.collisionTestArray(circles)
    circle.bounce();
  }

  // Main game loop, clear canvas, update circle positions, draw circles
  function loop() {
    clear();
    circles.filter(circle => circle.free).forEach(update);
    circles.forEach(circle => circle.draw());
    frame = requestAnimationFrame(loop);
  }
  loop();

  canvas.addEventListener('click', event => {
    points += check(getCoords(event), blue, other, frame, circles);
    document.getElementById('points').textContent = points;
  })
}

function check({ x, y }, blue, other, frame) {
  other.filter(circle => circle.isWithin(x, y))
    // .map(circle=>circle.toggle())
    .length && cancelAnimationFrame(frame); // This is where animation stops
  return blue.filter(circle => circle.isWithin(x, y)).length;
}

function getCoords(event) {
  const canvas = event.target;
  const rect = canvas.getBoundingClientRect()
  const x = event.clientX - rect.left;
  const y = event.clientY - rect.top;
  return { x, y };
}

main();

function Circle(context, c) {
  const randn = r => rand(r) - r / 2;
  const randi = r => Math.floor(randi(r));
  const rand = r => Math.random() * r;

  // These are for easily stopping and starting a circle;
  this.free = true;
  this.stop = () => this.free = false;
  this.release = () => this.free = true;
  this.toggle = () => this.free = !this.free;

  const {
    width,
    height
  } = context.canvas;

  // These are the same properties you were using in your code
  this.x = rand(width);
  this.changex = rand(10);
  this.y = rand(height);
  this.changey = rand(10);
  this.w = randn(width);
  this.h = randn(height);
  this.d = 1;
  this.a = 1;
  this.angle = 0;
  this.changle = 15;
  this.c = c || rand(90); // This is the only difference between blue and other circles
  this.r = 50;

  // These next functions you had in your code, I just moved them into the circle definition
  this.draw = () => {
    const { x, y, r, c } = this;
    context.beginPath();
    context.arc(x, y, r, 0, 2 * Math.PI);
    context.fillStyle = "hsla(" + c + ",100%,50%, 1)";
    context.fill();
  }
  this.bounce = () => {
    const { x, y, angle } = this;
    if (x > width || x < 0) {
      this.turn(180 - 2 * angle);
    }
    if (y > height || y < 0) {
      this.turn(360 - 2 * angle);
    }
  }
  this.turn = (angle, random = false) => {
    this.changle = random ? randn(angle) : angle;
    this.angle += this.changle;
  }
  this.forward = d => {
    this.d = d;
    this.x += this.d * Math.cos(this.angle * Math.PI / 180);
    this.y += this.d * Math.sin(this.angle * Math.PI / 180);
  }
  this.collisionTestArray = a => a
    .filter(circle => circle != this)
    .forEach(circle => this.collision(circle));
  this.collision = circle => {
    var differencex = Math.abs(this.x - circle.x);
    var differencey = Math.abs(this.y - circle.y);
    var hdif = Math.sqrt(differencex ** 2 + differencey ** 2);
    if (hdif < this.r + circle.r) {
      if (differencex < differencey) {
        this.turn(180 - 2 * this.angle);
        circle.turn(180 - 2 * circle.angle);
      } else {
        this.turn(360 - 2 * this.angle);
        circle.turn(360 - 2 * circle.angle);
      }
      this.turn(180);
      circle.turn(180);
    }
  }

  // These 2 functions I added to check if the circle was clicked
  this.distanceFrom = (x, y) => Math.sqrt((this.x - x) ** 2 + (this.y - y) ** 2);
  this.isWithin = (x, y) => this.r > this.distanceFrom(x, y);
}
#canvas {
  border: 5px solid orange;
}

#container {
  margin: auto;
  width: 75%;
  text-align: center;
}
<div id="container">
  <h1>Click the Blue Circles Only</h1>
  <canvas id="canvas" width="1000" height="600"></canvas>
  <p>
    Points: <span id="points">0</span>
  </p>
</div>

using OOP is better in this situation and will save you a lot of time I have written the OOP version of your game, I wrote it in harry so you may find some bugs but it is good as a starting point

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

let h = canvas.height = 600
let w = canvas.width = 800
const numberOfCircles = 20
let circles = []

// running gameover
let gameStatus = "running"
let score = 0
canvas.addEventListener("click", (e) => {
    if(gameStatus === "gameOver") {
       document.location.reload()
        return;
    }

    const mouse = {x: e.offsetX, y: e.offsetY}
    for(let circle of circles) {
        if(distance(mouse, circle) <= circle.radius) {
            if(circle.color == "blue") {
                gameStatus = "running"
                score += 1
            } else {
                gameStatus = "gameOver"
            }
        }
    }
}) 

class Circle {
    constructor(x, y, color, angle) {
        this.x = x
        this.y = y 
        this.color = color
        this.radius = 15
        this.angle = angle
        this.speed = 3
    }

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

    move(circles) {
        this.check(circles)
        this.x += Math.cos(this.angle) * this.speed
        this.y += Math.sin(this.angle) * this.speed
    }

    check(circles) {
        if(this.x + this.radius > w || this.x - this.radius < 0) this.angle += Math.PI 
        if(this.y + this.radius > h || this.y - this.radius < 0) this.angle += Math.PI

    for(let circle of circles) {
            if(circle === this) continue
                
            if(distance(this, circle) <= this.radius + circle.radius) {
                // invert angles or any other effect
                // there are much better soultion for resolving colliusions
                circle.angle += Math.PI / 2
                this.angle += Math.PI / 2
            }
        }
}

    }
}

setUp()
gameLoop()


function gameLoop() {
    ctx.clearRect(0,0,w,h)
    if(gameStatus === "gameOver") {
        ctx.font = "30px Comic"
        ctx.fillText("Game Over", w/2 - 150, h/2 - 100)
        ctx.fillText("you have scored : " + score, w/2 - 150, h/2)
        return;
    }
    ctx.font = "30px Comic"
    ctx.fillText("score : " + score, 20, 30)
    for (let i = 0; i < circles.length; i++) {
        const cirlce = circles[i]
        cirlce.draw(ctx)
        cirlce.move(circles)
    }
    requestAnimationFrame(gameLoop)
}



function random(to, from = 0) {
    return Math.floor(Math.random() * (to - from) + from)
}

function setUp() {
    gameStatus = "running"
    score = 0
    circles = []
    for (var i = 0; i < numberOfCircles; i++) {
        const randomAngle = random(360) * Math.PI / 180
        circles.push(new Circle(random(w, 20), random(h, 20), randomColor(),  randomAngle))
    }
}

function randomColor() {
    const factor = random(10)
    if(factor < 3) return "blue"
    return `rgb(${random(255)}, ${random(255)}, ${random(100)})`
}
function distance(obj1, obj2) {
    const xDiff = obj1.x - obj2.x
    const yDiff = obj1.y - obj2.y

    return Math.sqrt(Math.pow(xDiff, 2) + Math.pow(yDiff, 2))
}
Related