3

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;
}
sjaustirni
  • 3,056
  • 7
  • 32
  • 50
  • 2
    You need to introduce Object Oriented Programming into your code, meaning writing a `Circle` class and instantiating two of them, one red and one blue – AlpacaMax May 01 '22 at 03:35
  • Using canvas sort of 'loses' any structure so you are going to have to recreate the fact that you have some circle objects somehow. Have you considered scrapping the canvas and instead using HTML elements as the system will let you know if the user has clicked one without you needing to do a lot of calculation. – A Haworth May 01 '22 at 06:54
  • 1
    @AlpacaMax OOP is not a required pattern in JS, you can just as well use arrays and normal functions. OOP would be an alternative – Asplund May 01 '22 at 09:42
  • @Undo I agree. Sorry for the poor wording here. I meant to say OOP is usually a better approach for problems like this – AlpacaMax May 01 '22 at 21:19

2 Answers2

2

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>
Ryan White
  • 2,366
  • 1
  • 22
  • 34
  • Thank you so so much!! That's exactly what I wanted :) – jasmine shum May 02 '22 at 16:10
  • Hi, is there anyway you could demonstrate a version where its more similar to that of the code i used? The one you have above is perfect and has all the requirements I want but I find it a little hard to throughly understand because I haven't been taught some methods. Thanks! – jasmine shum May 03 '22 at 00:56
  • I think the only new thing I added that wasn't in your code is the "this" keyword, which is a bit confusing, but you can find out more [here](https://stackoverflow.com/questions/3127429/how-does-the-this-keyword-work) and [here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this). Basically, the functions you were using on the circles, like "turn" and "bounce", the circles will now call themselves, so instead of bounce(circle) you can use circle.bounce(), and within the function, instead of circle.x and circle.y, you can use this.x and this.y. – Ryan White May 03 '22 at 05:11
2

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))
}
Mohammad Esam
  • 502
  • 3
  • 10
  • Do you know if its possible to add collision detection between the circles so they don't overlap? – jasmine shum May 02 '22 at 16:59
  • I have edited the code with the collision detection, the way I resolved the collision is very basic and unreal, so it is better to make your research and use a better way to get much real result – Mohammad Esam May 03 '22 at 10:57