JavaScript/Exercises/Collisions

Speed

When objects move around, they do this at a certain speed. Therefore you should add two properties to the object's `class` representing its speed in x-direction and in y-direction. Positive values represent the direction to the right resp. bottom, and negative to the left resp. top. Furthermore, the class needs functions to modify the speed.

Collision - 1

Objects can collide with other objects or with the border of the canvas. Algorithms to detect such collisions depend on the object's type: concerning the x-direction for rectangles, the left side is given by the starting point, and for circles, the left side must be computed from the central point and the radius. A collision with the canvas' border is a collision 'from inner to outer', a collision between objects is always 'between outer and outer'.

Nevertheless, it's possible to develop generic algorithms that solve many of the 'collision' problems. Every single 2-dimensional object and every group of such objects can be surrounded by their minimum bounding box (MBB), which is a rectangle by definition. Hence the collision of objects can be solved by an algorithm that detects the collision of their MBBs, at least in the first approximation. It is not absolutely exact in all cases, but for our examples, it should be sufficient.

Constant speed

We create a ball (circle) and let him move within the canvas.

• As usual, the function `playTheGame` contains the 'logic' of the game. It is straightforward: move the ball according to it's 'speed'. The 'speed' is the number of pixels by which it should advance.
• A function `detectBorderCollision` checks whether the border of the canvas is touched by the current step. If that is the case, the speed is reversed to the opposite direction.
• `detectBorderCollision` checks whether the border of a rectangle is touched by a rectangle that moves within its inner space. This is different from the case where two rectangles collide like two cars.
• The 'outer' rectangle is the canvas itself. We use its properties as the four first arguments of the function call.
• The ball inside the canvas is not a rectangle; it's a circle. We 'compute' the MBB of the circle and use the properties of the MBB as the four last arguments of the function call. (For this algorithm, the MBB delivers not only an approximation of the problem, it's an exact solution.)
Click to see solution
```<!DOCTYPE html>
<html>
<title>SPEED 1</title>
<script>
"use strict";

// ---------------------------------------------------------------------
// class 'Circle' (should be implemented in a separate file 'circle.js')
// ---------------------------------------------------------------------
class Circle {
constructor(ctx, x = 10, y = 10, radius = 10, color = 'blue') {
this.ctx = ctx;
// position of the center
this.x = x;
this.y = y;

// movement
this.speedX = 0;
this.speedY = 0;

this.color = color;
}

// render the circle
render() {
this.ctx.beginPath(); // restart colors and lines
this.ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, false);
this.ctx.fillStyle = this.color;
this.ctx.fill();
}

// set the speed (= step size)
setSpeed(x, y) {
this.speedX = x;
this.speedY = y;
}
setSpeedX(x) {
this.speedX = x;
}
setSpeedY(y) {
this.speedY = y;
}

// change the position according to the speed
move() {
this.x += this.speedX;
this.y += this.speedY;
}
}   // end of class 'circle'

// ----------------------------------------------------
// variables which are known in the complete file
// ----------------------------------------------------
let ball;           // an instance of class 'circle'
let stop = false;   // indication
let requestId;      // ID of animation frame

// ----------------------------------------------------
// functions
// ----------------------------------------------------

// initialize all objects, variables, .. of the game
function start() {
// provide canvas and context
const canvas = document.getElementById("canvas");
const context = canvas.getContext("2d");

// create a circle at a certain position
ball = new Circle(context, 400, 100, 40, 'lime');
ball.setSpeedX(2);  // 45° towards upper right corner
ball.setSpeedY(-2); // 45° towards upper right corner

document.getElementById("stop").disabled = false;
document.getElementById("reset").disabled = true;

// start the game
stop = false;
playTheGame(canvas, context);
}

// the game's logic
function playTheGame(canvas, context) {

// move the ball according to its speed
ball.move();

// if we detect a collision with a border, the speed
// keeps constant but the direction reverses
const [crashL, crashR, crashT, crashB] =
detectBorderCollision(
0, 0, canvas.width, canvas.height,
);
if (crashL || crashR) {ball.speedX = -ball.speedX};
if (crashT || crashB) {ball.speedY = -ball.speedY};

renderAll(canvas, context);
}

// rendering consists off:
//   - clear the complete screen
//   - re-paint the complete screen
//   - call the game's logic again via requestAnimationFrame()
function renderAll(canvas, context) {

// remove every old drawing from the canvas (before re-rendering)
context.clearRect(0, 0, canvas.width, canvas.height);

// draw the sceen
ball.render();

if (stop) {
// if the old animation is still running, it must be canceled
cancelAnimationFrame(requestId);
// no call to 'requestAnimationFrame'. The loop terminates.
} else {
// re-start the game's logic, which lastly leads to
// a rendering of the canvas
requestId = window.requestAnimationFrame(() => playTheGame(canvas, context));
}
}

// terminate the rendering by setting a boolean flag
function stopEvent() {
stop = true;
document.getElementById("stop").disabled = true;
document.getElementById("reset").disabled = false;
}

// -------------------------------------------------------
// helper function (can be in a separate file: 'tools.js')
// -------------------------------------------------------

function detectBorderCollision(boarderX, boarderY, boarderWidth, boarderHeight,
rectX,    rectY,    rectWidth,    rectHeight)
{

// the rectangle touches the (outer) boarder, if x <= borderX, ...
let collisionLeft   = false;
let collisionRight  = false;
let collisionTop    = false;
let collisionBottom = false;

if (rectX              <= boarderX                ) {collisionLeft  = true}
if (rectX + rectWidth  >= boarderX + boarderWidth ) {collisionRight = true}
if (rectY              <= boarderY                ) {collisionTop   = true}
if (rectY + rectHeight >= boarderY + boarderHeight) {collisionBottom= true}

return [collisionLeft, collisionRight, collisionTop, collisionBottom];
}
</script>

<h1 style="text-align: center">Moving ball</h1>

<canvas id="canvas" width="700" height="300"
style="margin-top:1em; background-color:yellow" >
</canvas>

<p></p>
<button id="reset" onClick="start()" >Reset</button>
<button id="stop"  onClick="stopEvent()" >Stop</button>

</body>
</html>
```

Changing speed

The example is identical to the above one with the additional feature of changing the speed of the ball. It adds two HTML elements `input type="range"` as a slider. The sliders indicate the intended speed in x- and y- directions.

Click to see solution
```<!DOCTYPE html>
<html>
<title>SPEED 2</title>
<script>
"use strict";

// ---------------------------------------------------------------------
// class 'Circle' (should be implemented in a separate file 'circle.js')
// ---------------------------------------------------------------------
class Circle {
constructor(ctx, x = 10, y = 10, radius = 10, color = 'blue') {
this.ctx = ctx;
// position of the center
this.x = x;
this.y = y;

// movement
this.speedX = 0;
this.speedY = 0;

this.color = color;
}

// render the circle
render() {
this.ctx.beginPath(); // restart colors and lines
this.ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, false);
this.ctx.fillStyle = this.color;
this.ctx.fill();
}

// set the speed (= step size)
setSpeed(x, y) {
this.speedX = x;
this.speedY = y;
}
setSpeedX(x) {
this.speedX = x;
}
setSpeedY(y) {
this.speedY = y;
}

// change the position according to the speed
move() {
this.x += this.speedX;
this.y += this.speedY;
}
}   // end of class 'circle'

// ----------------------------------------------------
// variables which are known in the complete file
// ----------------------------------------------------
let ball;           // an instance of class 'circle'
let stop = false;   // indication
let requestId;      // ID of animation frame

// ----------------------------------------------------
// functions
// ----------------------------------------------------

// initialize all objects, variables, .. of the game
function start() {
// provide canvas and context
const canvas = document.getElementById("canvas");
const context = canvas.getContext("2d");

// create a circle at a certain position
ball = new Circle(context, 400, 100, 40, 'lime');
ball.setSpeedX(2);  // 45° towards upper right corner
ball.setSpeedY(-2); // 45° towards upper right corner

document.getElementById("sliderSpeedX").value = 2;
document.getElementById("sliderSpeedY").value = 2;

document.getElementById("stop").disabled = false;
document.getElementById("reset").disabled = true;

// start the game
stop = false;
playTheGame(canvas, context);
}

// the game's logic
function playTheGame(canvas, context) {

// move the ball according to its speed
ball.move();

// if we detect a collision with a border, the speed
// keeps constant but the direction reverses
const [crashL, crashR, crashT, crashB] =
detectBorderCollision(
0, 0, canvas.width, canvas.height,
);
if (crashL || crashR) {ball.speedX = -ball.speedX};
if (crashT || crashB) {ball.speedY = -ball.speedY};

renderAll(canvas, context);
}

// rendering consists off:
//   - clear the complete screen
//   - re-paint the complete screen
//   - call the game's logic again via requestAnimationFrame()
function renderAll(canvas, context) {

// remove every old drawing from the canvas (before re-rendering)
context.clearRect(0, 0, canvas.width, canvas.height);

// draw the sceen
ball.render();

if (stop) {
// if the old animation is still running, it must be canceled
cancelAnimationFrame(requestId);
// no call to 'requestAnimationFrame'. The loop terminates.
} else {
// re-start the game's logic, which lastly leads to
// a rendering of the canvas
requestId = window.requestAnimationFrame(() => playTheGame(canvas, context));
}
}

// terminate the rendering by setting a boolean flag
function stopEvent() {
stop = true;
document.getElementById("stop").disabled = true;
document.getElementById("reset").disabled = false;
}

function speedEventX(event) {
// read the slider's value and change speed
const value = event.srcElement.value;
ball.setSpeedX(parseFloat(value));
}
function speedEventY(event) {
// read the slider's value and change speed
const value = event.srcElement.value;
ball.setSpeedY(parseFloat(value));
}

// -------------------------------------------------------
// helper function (can be in a separate file: 'tools.js')
// -------------------------------------------------------

function detectBorderCollision(boarderX, boarderY, boarderWidth, boarderHeight,
rectX,    rectY,    rectWidth,    rectHeight)
{

// the rectangle touches the (outer) boarder, if x <= borderX, ...
let collisionLeft   = false;
let collisionRight  = false;
let collisionTop    = false;
let collisionBottom = false;

if (rectX              <= boarderX                ) {collisionLeft  = true}
if (rectX + rectWidth  >= boarderX + boarderWidth ) {collisionRight = true}
if (rectY              <= boarderY                ) {collisionTop   = true}
if (rectY + rectHeight >= boarderY + boarderHeight) {collisionBottom= true}

return [collisionLeft, collisionRight, collisionTop, collisionBottom];
}
</script>

<h1 style="text-align: center">Moving ball</h1>

<canvas id="canvas" width="700" height="300"
style="margin-top:1em; background-color:yellow" >
</canvas>

<p></p>
<button id="reset" onClick="start()" >Reset</button>
<button id="stop"  onClick="stopEvent()" >Stop</button>

<!-- sliders to indicate speed  -->
<div>
<input type="range" id="sliderSpeedX" name="sliderSpeedX" min="1" max="10"
step=".1" onchange="speedEventX(event)">
<label for="sliderSpeedX">Speed X</label>
</div>
<div>
<input type="range" id="sliderSpeedY" name="sliderSpeedY" min="1" max="10"
step=".1" onchange="speedEventY(event)">
<label for="sliderSpeedY">Speed Y</label>
</div>

</body>
</html>
```

Collision - 2

The example is identical to the above one with the additional feature of detecting an obstacle (rectangle). If the ball collides with the obstacle, the game stops.

The collision is detected by the function `detectRectangleCollision`. It compares two rectangles. We use the MBB of the circle as the second parameter, which leads to a slight inaccuracy.

Click to see solution
```<!DOCTYPE html>
<html>
<title>Collision 2</title>
<script>
"use strict";

// ---------------------------------------------------------------------
// class 'Circle' (should be implemented in a separate file 'circle.js')
// ---------------------------------------------------------------------
class Circle {
constructor(ctx, x = 10, y = 10, radius = 10, color = 'blue') {
this.ctx = ctx;
// position of the center
this.x = x;
this.y = y;

// movement
this.speedX = 0;
this.speedY = 0;

this.color = color;
}

// render the circle
render() {
this.ctx.beginPath(); // restart colors and lines
this.ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, false);
this.ctx.fillStyle = this.color;
this.ctx.fill();
}

// set the speed (= step size)
setSpeed(x, y) {
this.speedX = x;
this.speedY = y;
}
setSpeedX(x) {
this.speedX = x;
}
setSpeedY(y) {
this.speedY = y;
}

// change the position according to the speed
move() {
this.x += this.speedX;
this.y += this.speedY;
}
}   // end of class 'circle'

// ----------------------------------------------------
// variables which are known in the complete file
// ----------------------------------------------------
let ball;           // an instance of class 'circle'
let stop = false;   // indication
let requestId;      // ID of animation frame

// ----------------------------------------------------
// functions
// ----------------------------------------------------

// initialize all objects, variables, .. of the game
function start() {
// provide canvas and context
const canvas = document.getElementById("canvas");
const context = canvas.getContext("2d");

// create a circle at a certain position
ball = new Circle(context, 400, 100, 40, 'lime');
ball.setSpeedX(2);  // 45° towards upper right corner
ball.setSpeedY(-2); // 45° towards upper right corner

document.getElementById("sliderSpeedX").value = 2;
document.getElementById("sliderSpeedY").value = 2;

document.getElementById("stop").disabled = false;
document.getElementById("reset").disabled = true;

// start the game
stop = false;
playTheGame(canvas, context);
}

// the game's logic
function playTheGame(canvas, context) {

// move the ball according to its speed
ball.move();

// if we detect a collision with a border, the speed
// keeps constant but the direction reverses
const [crashL, crashR, crashT, crashB] =
detectBorderCollision(
// the MBB of the canvas
0, 0, canvas.width, canvas.height,
// the MBB of the circle
);
if (crashL || crashR) {ball.speedX = -ball.speedX};
if (crashT || crashB) {ball.speedY = -ball.speedY};

// if we detect a collision with the 'obstacle' the game stops
const collision = detectRectangleCollision(
// the MBB of the obstacle
330, 130, 30, 30,
// the MBB of the circle
if (collision) {
stopEvent();
}

renderAll(canvas, context);
}

// rendering consists off:
//   - clear the complete screen
//   - re-paint the complete screen
//   - call the game's logic again via requestAnimationFrame()
function renderAll(canvas, context) {

// remove every old drawing from the canvas (before re-rendering)
context.clearRect(0, 0, canvas.width, canvas.height);

// draw the scene: 'obstacle' plus ball
context.fillStyle = "red";
context.fillRect(330, 130, 30, 30);
ball.render();

if (stop) {
// if the old animation is still running, it must be canceled
cancelAnimationFrame(requestId);
// no call to 'requestAnimationFrame'. The loop terminates.
} else {
// re-start the game's logic, which lastly leads to
// a rendering of the canvas
requestId = window.requestAnimationFrame(() => playTheGame(canvas, context));
}
}

// terminate the rendering by setting a boolean flag
function stopEvent() {
stop = true;
document.getElementById("stop").disabled = true;
document.getElementById("reset").disabled = false;
}

function speedEventX(event) {
// read the slider's value and change speed
const value = event.srcElement.value;
ball.setSpeedX(parseFloat(value));
}
function speedEventY(event) {
// read the slider's value and change speed
const value = event.srcElement.value;
ball.setSpeedY(parseFloat(value));
}

// ----------------------------------------------------------
// helper function (should be in a separate file: 'tools.js')
// ----------------------------------------------------------

function detectBorderCollision(boarderX, boarderY, boarderWidth, boarderHeight,
rectX,    rectY,    rectWidth,    rectHeight)
{

// the rectangle touches the (outer) boarder, if x <= borderX, ...
let collisionLeft   = false;
let collisionRight  = false;
let collisionTop    = false;
let collisionBottom = false;

if (rectX              <= boarderX                ) {collisionLeft  = true}
if (rectX + rectWidth  >= boarderX + boarderWidth ) {collisionRight = true}
if (rectY              <= boarderY                ) {collisionTop   = true}
if (rectY + rectHeight >= boarderY + boarderHeight) {collisionBottom= true}

return [collisionLeft, collisionRight, collisionTop, collisionBottom];
}

// ---
function detectRectangleCollision(x1, y1, width1, height1,
x2, y2, width2, height2) {

// The algorithm takes its decision by detecting areas
// WITHOUT ANY overlapping

// No overlapping if one rectangle is COMPLETELY on the
// left side of the other
if (x1 > x2 + width2 || x2 > x1 + width1) {
return false;
}
// No overlapping if one rectangle is COMPLETELY
// above the other
if (y1 > y2 + height2 || y2 > y1 + height1) {
return false;
}

// all other cases
return true;
}
</script>

<h1 style="text-align: center">Moving ball</h1>

<canvas id="canvas" width="700" height="300"
style="margin-top:1em; background-color:yellow" >
</canvas>

<p></p>
<button id="reset" onClick="start()" >Reset</button>
<button id="stop"  onClick="stopEvent()" >Stop</button>

<!-- sliders to indicate speed  -->
<div>
<input type="range" id="sliderSpeedX" name="sliderSpeedX" min="1" max="10"
step=".1" onchange="speedEventX(event)">
<label for="sliderSpeedX">Speed X</label>
</div>
<div>
<input type="range" id="sliderSpeedY" name="sliderSpeedY" min="1" max="10"
step=".1" onchange="speedEventY(event)">
<label for="sliderSpeedY">Speed Y</label>
</div>

</body>
</html>
```