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();
}
<!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>