onlyforbopi
9/24/2018 - 10:28 AM

JS.Interact.Animate.Game.BounceBall.v2

JS.Interact.Animate.Game.BounceBall.v2

canvas {
  border: 1px solid black;
}
// useful to have them as global variables
var canvas, ctx, w, h;
var mousePos;
var numBalls = 10;

// an empty array!
var balls = [];

var player = {
  x: 10,
  y: 10,
  width: 20,
  height: 20,
  color: "red"
};

window.onload = function init() {
  // called AFTER the page has been loaded
  canvas = document.querySelector("#myCanvas");

  // often useful
  w = canvas.width;
  h = canvas.height;

  // important, we will draw with this object
  ctx = canvas.getContext("2d");

  // create 10 balls
  balls = createBalls(numBalls);

  // add a mousemove event listener to the canvas
  canvas.addEventListener("mousemove", mouseMoved);

  // ready to go !
  mainLoop();
};

function mouseMoved(evt) {
  mousePos = getMousePos(canvas, evt);
}

function getMousePos(canvas, evt) {
  // necessary work in the canvas coordinate system
  var rect = canvas.getBoundingClientRect();
  return {
    x: evt.clientX - rect.left,
    y: evt.clientY - rect.top
  };
}

function movePlayerWithMouse() {
  if (mousePos !== undefined) {
    player.x = mousePos.x;
    player.y = mousePos.y;
  }
}

function mainLoop() {
  // 1 - clear the canvas
  ctx.clearRect(0, 0, w, h);

  // draw the ball and the player
  drawFilledRectangle(player);
  drawAllBalls(balls);
  drawNumberOfBallsAlive(balls);

  // animate the ball that is bouncing all over the walls
  moveAllBalls(balls);

  movePlayerWithMouse();

  // ask for a new animation frame
  requestAnimationFrame(mainLoop);
}

// Collisions between rectangle and circle
function circRectsOverlap(x0, y0, w0, h0, cx, cy, r) {
  var testX = cx;
  var testY = cy;
  if (testX < x0) testX = x0;
  if (testX > x0 + w0) testX = x0 + w0;
  if (testY < y0) testY = y0;
  if (testY > y0 + h0) testY = y0 + h0;
  return (cx - testX) * (cx - testX) + (cy - testY) * (cy - testY) < r * r;
}

function createBalls(n) {
  // empty array
  var ballArray = [];

  // create n balls
  for (var i = 0; i < n; i++) {
    var b = {
      x: w * Math.random(),
      y: h * Math.random(),
      radius: 5 + 30 * Math.random(), // between 5 and 35
      speedX: -5 + 10 * Math.random(), // between -5 and + 5
      speedY: -5 + 10 * Math.random(), // between -5 and + 5
      color: getARandomColor(),
    };
    
    b.mass = Math.PI * b.radius * b.radius;
    
    // add ball b to the array
    ballArray.push(b);
  }
  // returns the array full of randomly created balls
  return ballArray;
}

function getARandomColor() {
  var colors = ["red", "blue", "cyan", "purple", "pink", "green", "yellow"];
  // a value between 0 and color.length-1
  // Math.round = rounded value
  // Math.random() a value between 0 and 1
  var colorIndex = Math.round((colors.length - 1) * Math.random());
  var c = colors[colorIndex];

  // return the random color
  return c;
}

function drawNumberOfBallsAlive(balls) {
  ctx.save();
  ctx.font = "30px Arial";

  if (balls.length === 0) {
    ctx.fillText("YOU WIN!", 20, 30);
  } else {
    ctx.fillText(balls.length, 20, 30);
  }
  ctx.restore();
}

function drawAllBalls(ballArray) {
  ballArray.forEach(function(b) {
    drawFilledCircle(b);
  });
}

function moveAllBalls(ballArray) {
  // iterate on all balls in array
  ballArray.forEach(function(b) {
    // b is the current ball in the array
    b.x += b.speedX;
    b.y += b.speedY;
  });

  ballArray.forEach(function(b, index) {
    testCollisionWithOtherBalls(b, index); // do this first
    testCollisionBallWithWalls(b);
    testCollisionWithPlayer(b, index);
  });
}

function testCollisionWithPlayer(b, index) {
  if (
    circRectsOverlap(
      player.x,
      player.y,
      player.width,
      player.height,
      b.x,
      b.y,
      b.radius
    )
  ) {
    // we remove the element located at index
    // from the balls array
    // splice: first parameter = starting index
    //         second parameter = number of elements to remove
    balls.splice(index, 1);
  }
}

function testCollisionBallWithWalls(b) {
  // COLLISION WITH VERTICAL WALLS ?
  if ((b.x + b.radius) >= w) {
    // the ball hit the right wall
    // change horizontal direction
    b.speedX = -b.speedX;

    // put the ball at the collision point
    b.x = w - b.radius;
  } else if ((b.x - b.radius) <= 0) {
    // the ball hit the left wall
    // change horizontal direction
    b.speedX = -b.speedX;

    // put the ball at the collision point
    b.x = b.radius;
  }

  // COLLISIONS WTH HORIZONTAL WALLS ?
  // Not in the else as the ball can touch both
  // vertical and horizontal walls in corners
  if ((b.y + b.radius) >= h) {
    // the ball hit the right wall
    // change horizontal direction
    b.speedY = -b.speedY;

    // put the ball at the collision point
    b.y = h - b.radius;
  } else if ((b.y - b.radius) <= 0) {
    // the ball hit the left wall
    // change horizontal direction
    b.speedY = -b.speedY;

    // put the ball at the collision point
    b.Y = b.radius;
  }
}

function circCircOverlap(one, two) {
  let totalR = two.radius + one.radius;
  let deltaX = one.x - two.x;
  let deltaY = one.y - two.y;
  return deltaX * deltaX + deltaY * deltaY < totalR * totalR;
}

function testCollisionWithOtherBalls(one, index) {
  balls.forEach(function(two, offset) {
    // skip self (test for upper !== lower based on array index)
    if (offset != index) {
      if (circCircOverlap(one, two)) {
        // see http://williamecraver.wixsite.com/elastic-equations
        let totalM = one.mass + two.mass;
        let deltaM = one.mass - two.mass; // = deltaMassOne = -deltaMassTwo
        let totalD = one.radius + two.radius;
        let deltaX = two.x - one.x;
        let deltaY = two.y - one.y;
        
        let psi      = Math.asin(deltaY/totalD);
        let thetaOne = Math.atan2(one.speedY, one.speedX);
        let thetaTwo = Math.atan2(two.speedY, two.speedX);
        let angleOne = (thetaOne - psi);
        let angleTwo = -(thetaTwo - psi);
        let speedOne = Math.sqrt(one.speedX*one.speedX + one.speedY*one.speedY);
        let speedTwo = Math.sqrt(two.speedX*two.speedX + two.speedY*two.speedY);
        
        let bigTermOne = (speedOne*Math.cos(angleOne)*( deltaM)+2*two.mass*speedTwo*Math.cos(angleTwo))/totalM;
        let bigTermTwo = (speedTwo*Math.cos(angleTwo)*(-deltaM)+2*one.mass*speedOne*Math.cos(angleOne))/totalM;

        let newOneVelX = bigTermOne*Math.cos(psi)-speedOne*Math.sin(angleOne)*Math.sin(psi);
        let newOneVelY = bigTermOne*Math.sin(psi)+speedOne*Math.sin(angleOne)*Math.cos(psi);
        let newTwoVelX = bigTermOne*Math.cos(psi)-speedTwo*Math.sin(angleTwo)*Math.sin(psi);
        let newTwoVelY = bigTermOne*Math.sin(psi)+speedTwo*Math.sin(angleTwo)*Math.cos(psi);        
        
        let metric = Math.sqrt(deltaX*deltaX+deltaY*deltaY);
        let factOne = (one.radius > metric && metric > 0.00001) ? one.radius / metric : 1;

        one.x += factOne*newOneVelX - one.speedX;
        one.y += factOne*newOneVelY - one.speedY;
        one.speedX = newOneVelX;
        one.speedY = newOneVelY;
        
        let factTwo = (two.radius > metric && metric > 0.00001) ? two.radius / metric : 1;
        two.x += factTwo*newTwoVelX - two.speedX;
        two.y += factTwo*newTwoVelY - two.speedY;
        two.speedX = newTwoVelX;
        two.speedY = newTwoVelY;          
      }
    }
  });
}

function drawFilledRectangle(r) {
  // GOOD practice: save the context, use 2D trasnformations
  ctx.save();

  // translate the coordinate system, draw relative to it
  ctx.translate(r.x, r.y);

  ctx.fillStyle = r.color;
  // (0, 0) is the top left corner of the monster.
  ctx.fillRect(0, 0, r.width, r.height);

  // GOOD practice: restore the context
  ctx.restore();
}

function drawFilledCircle(c) {
  // GOOD practice: save the context, use 2D trasnformations
  ctx.save();

  // translate the coordinate system, draw relative to it
  ctx.translate(c.x, c.y);

  ctx.fillStyle = c.color;
  // (0, 0) is the top left corner of the monster.
  ctx.beginPath();
  ctx.arc(0, 0, c.radius, 0, 2 * Math.PI);
  ctx.fill();

  // GOOD practice: restore the context
  ctx.restore();
}

JS.Interact.Animate.Game.BounceBall.v2

A Pen by Pan Doul on CodePen.

License.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Draw a monster in a canvas</title>
</head>
<body>
  <canvas id="myCanvas"  width="400" height="400"></canvas>
</body>
</html>