Joshua Shoemaker bf2572e3e9 Init
2016-11-22 22:48:39 -06:00

1474 lines
47 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class Bump {
constructor(renderingEngine = PIXI) {
if (renderingEngine === undefined) throw new Error("Please assign a rendering engine in the constructor before using bump.js");
//Find out which rendering engine is being used (the default is Pixi)
this.renderer = "";
//If the `renderingEngine` is Pixi, set up Pixi object aliases
if (renderingEngine.ParticleContainer && renderingEngine.Sprite) {
this.renderer = "pixi";
}
}
//`addCollisionProperties` adds extra properties to sprites to help
//simplify the collision code. It won't add these properties if they
//already exist on the sprite. After these properties have been
//added, this methods adds a Boolean property to the sprite called `_bumpPropertiesAdded`
//and sets it to `true` to flag that the sprite has these
//new properties
addCollisionProperties(sprite) {
//Add properties to Pixi sprites
if (this.renderer === "pixi") {
//gx
if (sprite.gx === undefined) {
Object.defineProperty(sprite, "gx", {
get(){return sprite.getGlobalPosition().x},
enumerable: true, configurable: true
});
}
//gy
if (sprite.gy === undefined) {
Object.defineProperty(sprite, "gy", {
get(){return sprite.getGlobalPosition().y},
enumerable: true, configurable: true
});
}
//centerX
if (sprite.centerX === undefined) {
Object.defineProperty(sprite, "centerX", {
get(){return sprite.x + sprite.width / 2},
enumerable: true, configurable: true
});
}
//centerY
if (sprite.centerY === undefined) {
Object.defineProperty(sprite, "centerY", {
get(){return sprite.y + sprite.height / 2},
enumerable: true, configurable: true
});
}
//halfWidth
if (sprite.halfWidth === undefined) {
Object.defineProperty(sprite, "halfWidth", {
get(){return sprite.width / 2},
enumerable: true, configurable: true
});
}
//halfHeight
if (sprite.halfHeight === undefined) {
Object.defineProperty(sprite, "halfHeight", {
get(){return sprite.height / 2},
enumerable: true, configurable: true
});
}
//xAnchorOffset
if (sprite.xAnchorOffset === undefined) {
Object.defineProperty(sprite, "xAnchorOffset", {
get(){
if (sprite.anchor !== undefined) {
return sprite.height * sprite.anchor.x;
} else {
return 0;
}
},
enumerable: true, configurable: true
});
}
//yAnchorOffset
if (sprite.yAnchorOffset === undefined) {
Object.defineProperty(sprite, "yAnchorOffset", {
get(){
if (sprite.anchor !== undefined) {
return sprite.width * sprite.anchor.y;
} else {
return 0;
}
},
enumerable: true, configurable: true
});
}
if (sprite.circular && sprite.radius === undefined) {
Object.defineProperty(sprite, "radius", {
get(){return sprite.width / 2},
enumerable: true, configurable: true
});
}
//Earlier code - not needed now.
/*
Object.defineProperties(sprite, {
"gx": {
get(){return sprite.getGlobalPosition().x},
enumerable: true, configurable: true
},
"gy": {
get(){return sprite.getGlobalPosition().y},
enumerable: true, configurable: true
},
"centerX": {
get(){return sprite.x + sprite.width / 2},
enumerable: true, configurable: true
},
"centerY": {
get(){return sprite.y + sprite.height / 2},
enumerable: true, configurable: true
},
"halfWidth": {
get(){return sprite.width / 2},
enumerable: true, configurable: true
},
"halfHeight": {
get(){return sprite.height / 2},
enumerable: true, configurable: true
},
"xAnchorOffset": {
get(){
if (sprite.anchor !== undefined) {
return sprite.height * sprite.anchor.x;
} else {
return 0;
}
},
enumerable: true, configurable: true
},
"yAnchorOffset": {
get(){
if (sprite.anchor !== undefined) {
return sprite.width * sprite.anchor.y;
} else {
return 0;
}
},
enumerable: true, configurable: true
}
});
*/
}
//Add a Boolean `_bumpPropertiesAdded` property to the sprite to flag it
//as having these new properties
sprite._bumpPropertiesAdded = true;
}
/*
hitTestPoint
------------
Use it to find out if a point is touching a circlular or rectangular sprite.
Parameters:
a. An object with `x` and `y` properties.
b. A sprite object with `x`, `y`, `centerX` and `centerY` properties.
If the sprite has a `radius` property, the function will interpret
the shape as a circle.
*/
hitTestPoint(point, sprite) {
//Add collision properties
if (!sprite._bumpPropertiesAdded) this.addCollisionProperties(sprite);
let shape, left, right, top, bottom, vx, vy, magnitude, hit;
//Find out if the sprite is rectangular or circular depending
//on whether it has a `radius` property
if (sprite.radius) {
shape = "circle";
} else {
shape = "rectangle";
}
//Rectangle
if (shape === "rectangle") {
//Get the position of the sprite's edges
left = sprite.x - sprite.xAnchorOffset;
right = sprite.x + sprite.width - sprite.xAnchorOffset;
top = sprite.y - sprite.yAnchorOffset;
bottom = sprite.y + sprite.height - sprite.yAnchorOffset;
//Find out if the point is intersecting the rectangle
hit = point.x > left && point.x < right && point.y > top && point.y < bottom;
}
//Circle
if (shape === "circle") {
//Find the distance between the point and the
//center of the circle
let vx = point.x - sprite.x - (sprite.width / 2) + sprite.xAnchorOffset,
vy = point.y - sprite.y - (sprite.height / 2) + sprite.yAnchorOffset,
magnitude = Math.sqrt(vx * vx + vy * vy);
//The point is intersecting the circle if the magnitude
//(distance) is less than the circle's radius
hit = magnitude < sprite.radius;
}
//`hit` will be either `true` or `false`
return hit;
}
/*
hitTestCircle
-------------
Use it to find out if two circular sprites are touching.
Parameters:
a. A sprite object with `centerX`, `centerY` and `radius` properties.
b. A sprite object with `centerX`, `centerY` and `radius`.
*/
hitTestCircle(c1, c2, global = false) {
//Add collision properties
if (!c1._bumpPropertiesAdded) this.addCollisionProperties(c1);
if (!c2._bumpPropertiesAdded) this.addCollisionProperties(c2);
let vx, vy, magnitude, combinedRadii, hit;
//Calculate the vector between the circles center points
if (global) {
//Use global coordinates
vx = (c2.gx + (c2.width / 2) - c2.xAnchorOffset) - (c1.gx + (c1.width / 2) - c1.xAnchorOffset);
vy = (c2.gy + (c2.width / 2) - c2.yAnchorOffset) - (c1.gy + (c1.width / 2) - c1.yAnchorOffset);
} else {
//Use local coordinates
vx = (c2.x + (c2.width / 2) - c2.xAnchorOffset) - (c1.x + (c1.width / 2) - c1.xAnchorOffset);
vy = (c2.y + (c2.width / 2) - c2.yAnchorOffset) - (c1.y + (c1.width / 2) - c1.yAnchorOffset);
}
//Find the distance between the circles by calculating
//the vector's magnitude (how long the vector is)
magnitude = Math.sqrt(vx * vx + vy * vy);
//Add together the circles' total radii
combinedRadii = c1.radius + c2.radius;
//Set `hit` to `true` if the distance between the circles is
//less than their `combinedRadii`
hit = magnitude < combinedRadii;
//`hit` will be either `true` or `false`
return hit;
}
/*
circleCollision
---------------
Use it to prevent a moving circular sprite from overlapping and optionally
bouncing off a non-moving circular sprite.
Parameters:
a. A sprite object with `x`, `y` `centerX`, `centerY` and `radius` properties.
b. A sprite object with `x`, `y` `centerX`, `centerY` and `radius` properties.
c. Optional: true or false to indicate whether or not the first sprite
should bounce off the second sprite.
The sprites can contain an optional mass property that should be greater than 1.
*/
circleCollision(c1, c2, bounce = false, global = false) {
//Add collision properties
if (!c1._bumpPropertiesAdded) this.addCollisionProperties(c1);
if (!c2._bumpPropertiesAdded) this.addCollisionProperties(c2);
let magnitude, combinedRadii, overlap,
vx, vy, dx, dy, s = {},
hit = false;
//Calculate the vector between the circles center points
if (global) {
//Use global coordinates
vx = (c2.gx + (c2.width / 2) - c2.xAnchorOffset) - (c1.gx + (c1.width / 2) - c1.xAnchorOffset);
vy = (c2.gy + (c2.width / 2) - c2.yAnchorOffset) - (c1.gy + (c1.width / 2) - c1.yAnchorOffset);
} else {
//Use local coordinates
vx = (c2.x + (c2.width / 2) - c2.xAnchorOffset) - (c1.x + (c1.width / 2) - c1.xAnchorOffset);
vy = (c2.y + (c2.width / 2) - c2.yAnchorOffset) - (c1.y + (c1.width / 2) - c1.yAnchorOffset);
}
//Find the distance between the circles by calculating
//the vector's magnitude (how long the vector is)
magnitude = Math.sqrt(vx * vx + vy * vy);
//Add together the circles' combined half-widths
combinedRadii = c1.radius + c2.radius;
//Figure out if there's a collision
if (magnitude < combinedRadii) {
//Yes, a collision is happening
hit = true;
//Find the amount of overlap between the circles
overlap = combinedRadii - magnitude;
//Add some "quantum padding". This adds a tiny amount of space
//between the circles to reduce their surface tension and make
//them more slippery. "0.3" is a good place to start but you might
//need to modify this slightly depending on the exact behaviour
//you want. Too little and the balls will feel sticky, too much
//and they could start to jitter if they're jammed together
let quantumPadding = 0.3;
overlap += quantumPadding;
//Normalize the vector
//These numbers tell us the direction of the collision
dx = vx / magnitude;
dy = vy / magnitude;
//Move circle 1 out of the collision by multiplying
//the overlap with the normalized vector and subtract it from
//circle 1's position
c1.x -= overlap * dx;
c1.y -= overlap * dy;
//Bounce
if (bounce) {
//Create a collision vector object, `s` to represent the bounce "surface".
//Find the bounce surface's x and y properties
//(This represents the normal of the distance vector between the circles)
s.x = vy;
s.y = -vx;
//Bounce c1 off the surface
this.bounceOffSurface(c1, s);
}
}
return hit;
}
/*
movingCircleCollision
---------------------
Use it to make two moving circles bounce off each other.
Parameters:
a. A sprite object with `x`, `y` `centerX`, `centerY` and `radius` properties.
b. A sprite object with `x`, `y` `centerX`, `centerY` and `radius` properties.
The sprites can contain an optional mass property that should be greater than 1.
*/
movingCircleCollision(c1, c2, global = false) {
//Add collision properties
if (!c1._bumpPropertiesAdded) this.addCollisionProperties(c1);
if (!c2._bumpPropertiesAdded) this.addCollisionProperties(c2);
let combinedRadii, overlap, xSide, ySide,
//`s` refers to the distance vector between the circles
s = {},
p1A = {},
p1B = {},
p2A = {},
p2B = {},
hit = false;
//Apply mass, if the circles have mass properties
c1.mass = c1.mass || 1;
c2.mass = c2.mass || 1;
//Calculate the vector between the circles center points
if (global) {
//Use global coordinates
s.vx = (c2.gx + c2.radius - c2.xAnchorOffset) - (c1.gx + c1.radius - c1.xAnchorOffset);
s.vy = (c2.gy + c2.radius - c2.yAnchorOffset) - (c1.gy + c1.radius - c1.yAnchorOffset);
} else {
//Use local coordinates
s.vx = (c2.x + c2.radius - c2.xAnchorOffset) - (c1.x + c1.radius - c1.xAnchorOffset);
s.vy = (c2.y + c2.radius - c2.yAnchorOffset) - (c1.y + c1.radius - c1.yAnchorOffset);
}
//Find the distance between the circles by calculating
//the vector's magnitude (how long the vector is)
s.magnitude = Math.sqrt(s.vx * s.vx + s.vy * s.vy);
//Add together the circles' combined half-widths
combinedRadii = c1.radius + c2.radius;
//Figure out if there's a collision
if (s.magnitude < combinedRadii) {
//Yes, a collision is happening
hit = true;
//Find the amount of overlap between the circles
overlap = combinedRadii - s.magnitude;
//Add some "quantum padding" to the overlap
overlap += 0.3;
//Normalize the vector.
//These numbers tell us the direction of the collision
s.dx = s.vx / s.magnitude;
s.dy = s.vy / s.magnitude;
//Find the collision vector.
//Divide it in half to share between the circles, and make it absolute
s.vxHalf = Math.abs(s.dx * overlap / 2);
s.vyHalf = Math.abs(s.dy * overlap / 2);
//Find the side that the collision is occurring on
(c1.x > c2.x) ? xSide = 1 : xSide = -1;
(c1.y > c2.y) ? ySide = 1 : ySide = -1;
//Move c1 out of the collision by multiplying
//the overlap with the normalized vector and adding it to
//the circles' positions
c1.x = c1.x + (s.vxHalf * xSide);
c1.y = c1.y + (s.vyHalf * ySide);
//Move c2 out of the collision
c2.x = c2.x + (s.vxHalf * -xSide);
c2.y = c2.y + (s.vyHalf * -ySide);
//1. Calculate the collision surface's properties
//Find the surface vector's left normal
s.lx = s.vy;
s.ly = -s.vx;
//2. Bounce c1 off the surface (s)
//Find the dot product between c1 and the surface
let dp1 = c1.vx * s.dx + c1.vy * s.dy;
//Project c1's velocity onto the collision surface
p1A.x = dp1 * s.dx;
p1A.y = dp1 * s.dy;
//Find the dot product of c1 and the surface's left normal (s.lx and s.ly)
let dp2 = c1.vx * (s.lx / s.magnitude) + c1.vy * (s.ly / s.magnitude);
//Project the c1's velocity onto the surface's left normal
p1B.x = dp2 * (s.lx / s.magnitude);
p1B.y = dp2 * (s.ly / s.magnitude);
//3. Bounce c2 off the surface (s)
//Find the dot product between c2 and the surface
let dp3 = c2.vx * s.dx + c2.vy * s.dy;
//Project c2's velocity onto the collision surface
p2A.x = dp3 * s.dx;
p2A.y = dp3 * s.dy;
//Find the dot product of c2 and the surface's left normal (s.lx and s.ly)
let dp4 = c2.vx * (s.lx / s.magnitude) + c2.vy * (s.ly / s.magnitude);
//Project c2's velocity onto the surface's left normal
p2B.x = dp4 * (s.lx / s.magnitude);
p2B.y = dp4 * (s.ly / s.magnitude);
//4. Calculate the bounce vectors
//Bounce c1
//using p1B and p2A
c1.bounce = {};
c1.bounce.x = p1B.x + p2A.x;
c1.bounce.y = p1B.y + p2A.y;
//Bounce c2
//using p1A and p2B
c2.bounce = {};
c2.bounce.x = p1A.x + p2B.x;
c2.bounce.y = p1A.y + p2B.y;
//Add the bounce vector to the circles' velocity
//and add mass if the circle has a mass property
c1.vx = c1.bounce.x / c1.mass;
c1.vy = c1.bounce.y / c1.mass;
c2.vx = c2.bounce.x / c2.mass;
c2.vy = c2.bounce.y / c2.mass;
}
return hit;
}
/*
multipleCircleCollision
-----------------------
Checks all the circles in an array for a collision against
all the other circles in an array, using `movingCircleCollision` (above)
*/
multipleCircleCollision(arrayOfCircles, global = false) {
for (let i = 0; i < arrayOfCircles.length; i++) {
//The first circle to use in the collision check
var c1 = arrayOfCircles[i];
for (let j = i + 1; j < arrayOfCircles.length; j++) {
//The second circle to use in the collision check
let c2 = arrayOfCircles[j];
//Check for a collision and bounce the circles apart if
//they collide. Use an optional `mass` property on the sprite
//to affect the bounciness of each marble
this.movingCircleCollision(c1, c2, global);
}
}
}
/*
rectangleCollision
------------------
Use it to prevent two rectangular sprites from overlapping.
Optionally, make the first rectangle bounce off the second rectangle.
Parameters:
a. A sprite object with `x`, `y` `centerX`, `centerY`, `halfWidth` and `halfHeight` properties.
b. A sprite object with `x`, `y` `centerX`, `centerY`, `halfWidth` and `halfHeight` properties.
c. Optional: true or false to indicate whether or not the first sprite
should bounce off the second sprite.
*/
rectangleCollision(
r1, r2, bounce = false, global = true
) {
//Add collision properties
if (!r1._bumpPropertiesAdded) this.addCollisionProperties(r1);
if (!r2._bumpPropertiesAdded) this.addCollisionProperties(r2);
let collision, combinedHalfWidths, combinedHalfHeights,
overlapX, overlapY, vx, vy;
//Calculate the distance vector
if (global) {
vx = (r1.gx + r1.halfWidth - r1.xAnchorOffset) - (r2.gx + r2.halfWidth - r2.xAnchorOffset);
vy = (r1.gy + r1.halfHeight - r1.yAnchorOffset) - (r2.gy + r2.halfHeight - r2.yAnchorOffset);
} else {
//vx = r1.centerX - r2.centerX;
//vy = r1.centerY - r2.centerY;
vx = (r1.x + r1.halfWidth - r1.xAnchorOffset) - (r2.x + r2.halfWidth - r2.xAnchorOffset);
vy = (r1.y + r1.halfHeight - r1.yAnchorOffset) - (r2.y + r2.halfHeight - r2.yAnchorOffset);
}
//Figure out the combined half-widths and half-heights
combinedHalfWidths = r1.halfWidth + r2.halfWidth;
combinedHalfHeights = r1.halfHeight + r2.halfHeight;
//Check whether vx is less than the combined half widths
if (Math.abs(vx) < combinedHalfWidths) {
//A collision might be occurring!
//Check whether vy is less than the combined half heights
if (Math.abs(vy) < combinedHalfHeights) {
//A collision has occurred! This is good!
//Find out the size of the overlap on both the X and Y axes
overlapX = combinedHalfWidths - Math.abs(vx);
overlapY = combinedHalfHeights - Math.abs(vy);
//The collision has occurred on the axis with the
//*smallest* amount of overlap. Let's figure out which
//axis that is
if (overlapX >= overlapY) {
//The collision is happening on the X axis
//But on which side? vy can tell us
if (vy > 0) {
collision = "top";
//Move the rectangle out of the collision
r1.y = r1.y + overlapY;
} else {
collision = "bottom";
//Move the rectangle out of the collision
r1.y = r1.y - overlapY;
}
//Bounce
if (bounce) {
r1.vy *= -1;
/*Alternative
//Find the bounce surface's vx and vy properties
var s = {};
s.vx = r2.x - r2.x + r2.width;
s.vy = 0;
//Bounce r1 off the surface
//this.bounceOffSurface(r1, s);
*/
}
} else {
//The collision is happening on the Y axis
//But on which side? vx can tell us
if (vx > 0) {
collision = "left";
//Move the rectangle out of the collision
r1.x = r1.x + overlapX;
} else {
collision = "right";
//Move the rectangle out of the collision
r1.x = r1.x - overlapX;
}
//Bounce
if (bounce) {
r1.vx *= -1;
/*Alternative
//Find the bounce surface's vx and vy properties
var s = {};
s.vx = 0;
s.vy = r2.y - r2.y + r2.height;
//Bounce r1 off the surface
this.bounceOffSurface(r1, s);
*/
}
}
} else {
//No collision
}
} else {
//No collision
}
//Return the collision string. it will be either "top", "right",
//"bottom", or "left" depending on which side of r1 is touching r2.
return collision;
}
/*
hitTestRectangle
----------------
Use it to find out if two rectangular sprites are touching.
Parameters:
a. A sprite object with `centerX`, `centerY`, `halfWidth` and `halfHeight` properties.
b. A sprite object with `centerX`, `centerY`, `halfWidth` and `halfHeight` properties.
*/
hitTestRectangle(r1, r2, global = false) {
//Add collision properties
if (!r1._bumpPropertiesAdded) this.addCollisionProperties(r1);
if (!r2._bumpPropertiesAdded) this.addCollisionProperties(r2);
let hit, combinedHalfWidths, combinedHalfHeights, vx, vy;
//A variable to determine whether there's a collision
hit = false;
//Calculate the distance vector
if (global) {
vx = (r1.gx + r1.halfWidth - r1.xAnchorOffset) - (r2.gx + r2.halfWidth - r2.xAnchorOffset);
vy = (r1.gy + r1.halfHeight - r1.yAnchorOffset) - (r2.gy + r2.halfHeight - r2.yAnchorOffset);
} else {
vx = (r1.x + r1.halfWidth - r1.xAnchorOffset) - (r2.x + r2.halfWidth - r2.xAnchorOffset);
vy = (r1.y + r1.halfHeight - r1.yAnchorOffset) - (r2.y + r2.halfHeight - r2.yAnchorOffset);
}
//Figure out the combined half-widths and half-heights
combinedHalfWidths = r1.halfWidth + r2.halfWidth;
combinedHalfHeights = r1.halfHeight + r2.halfHeight;
//Check for a collision on the x axis
if (Math.abs(vx) < combinedHalfWidths) {
//A collision might be occuring. Check for a collision on the y axis
if (Math.abs(vy) < combinedHalfHeights) {
//There's definitely a collision happening
hit = true;
} else {
//There's no collision on the y axis
hit = false;
}
} else {
//There's no collision on the x axis
hit = false;
}
//`hit` will be either `true` or `false`
return hit;
}
/*
hitTestCircleRectangle
----------------
Use it to find out if a circular shape is touching a rectangular shape
Parameters:
a. A sprite object with `centerX`, `centerY`, `halfWidth` and `halfHeight` properties.
b. A sprite object with `centerX`, `centerY`, `halfWidth` and `halfHeight` properties.
*/
hitTestCircleRectangle(c1, r1, global = false) {
//Add collision properties
if (!r1._bumpPropertiesAdded) this.addCollisionProperties(r1);
if (!c1._bumpPropertiesAdded) this.addCollisionProperties(c1);
let region, collision, c1x, c1y, r1x, r1y;
//Use either global or local coordinates
if (global) {
c1x = c1.gx;
c1y = c1.gy
r1x = r1.gx;
r1y = r1.gy;
} else {
c1x = c1.x;
c1y = c1.y;
r1x = r1.x;
r1y = r1.y;
}
//Is the circle above the rectangle's top edge?
if (c1y - c1.yAnchorOffset < r1y - r1.halfHeight - r1.yAnchorOffset) {
//If it is, we need to check whether it's in the
//top left, top center or top right
if (c1x - c1.xAnchorOffset < r1x - 1 - r1.halfWidth - r1.xAnchorOffset) {
region = "topLeft";
} else if (c1x - c1.xAnchorOffset > r1x + 1 + r1.halfWidth - r1.xAnchorOffset) {
region = "topRight";
} else {
region = "topMiddle";
}
}
//The circle isn't above the top edge, so it might be
//below the bottom edge
else if (c1y - c1.yAnchorOffset > r1y + r1.halfHeight - r1.yAnchorOffset) {
//If it is, we need to check whether it's in the bottom left,
//bottom center, or bottom right
if (c1x - c1.xAnchorOffset < r1x - 1 - r1.halfWidth - r1.xAnchorOffset) {
region = "bottomLeft";
} else if (c1x - c1.xAnchorOffset > r1x + 1 + r1.halfWidth - r1.xAnchorOffset) {
region = "bottomRight";
} else {
region = "bottomMiddle";
}
}
//The circle isn't above the top edge or below the bottom edge,
//so it must be on the left or right side
else {
if (c1x - c1.xAnchorOffset < r1x - r1.halfWidth - r1.xAnchorOffset) {
region = "leftMiddle";
} else {
region = "rightMiddle";
}
}
//Is this the circle touching the flat sides
//of the rectangle?
if (region === "topMiddle" || region === "bottomMiddle" || region === "leftMiddle" || region === "rightMiddle") {
//Yes, it is, so do a standard rectangle vs. rectangle collision test
collision = this.hitTestRectangle(c1, r1, global);
}
//The circle is touching one of the corners, so do a
//circle vs. point collision test
else {
let point = {};
switch (region) {
case "topLeft":
point.x = r1x - r1.xAnchorOffset;
point.y = r1y - r1.yAnchorOffset;
break;
case "topRight":
point.x = r1x + r1.width - r1.xAnchorOffset;
point.y = r1y - r1.yAnchorOffset;
break;
case "bottomLeft":
point.x = r1x - r1.xAnchorOffset;
point.y = r1y + r1.height - r1.yAnchorOffset;
break;
case "bottomRight":
point.x = r1x + r1.width - r1.xAnchorOffset;
point.y = r1y + r1.height - r1.yAnchorOffset;
}
//Check for a collision between the circle and the point
collision = this.hitTestCirclePoint(c1, point, global);
}
//Return the result of the collision.
//The return value will be `undefined` if there's no collision
if (collision) {
return region;
} else {
return collision;
}
}
/*
hitTestCirclePoint
------------------
Use it to find out if a circular shape is touching a point
Parameters:
a. A sprite object with `centerX`, `centerY`, and `radius` properties.
b. A point object with `x` and `y` properties.
*/
hitTestCirclePoint(c1, point, global = false) {
//Add collision properties
if (!c1._bumpPropertiesAdded) this.addCollisionProperties(c1);
//A point is just a circle with a diameter of
//1 pixel, so we can cheat. All we need to do is an ordinary circle vs. circle
//Collision test. Just supply the point with the properties
//it needs
point.diameter = 1;
point.width = point.diameter;
point.radius = 0.5;
point.centerX = point.x;
point.centerY = point.y;
point.gx = point.x;
point.gy = point.y;
point.xAnchorOffset = 0;
point.yAnchorOffset = 0;
point._bumpPropertiesAdded = true;
return this.hitTestCircle(c1, point, global);
}
/*
circleRectangleCollision
------------------------
Use it to bounce a circular shape off a rectangular shape
Parameters:
a. A sprite object with `centerX`, `centerY`, `halfWidth` and `halfHeight` properties.
b. A sprite object with `centerX`, `centerY`, `halfWidth` and `halfHeight` properties.
*/
circleRectangleCollision(
c1, r1, bounce = false, global = false
) {
//Add collision properties
if (!r1._bumpPropertiesAdded) this.addCollisionProperties(r1);
if (!c1._bumpPropertiesAdded) this.addCollisionProperties(c1);
let region, collision, c1x, c1y, r1x, r1y;
//Use either the global or local coordinates
if (global) {
c1x = c1.gx;
c1y = c1.gy;
r1x = r1.gx;
r1y = r1.gy;
} else {
c1x = c1.x;
c1y = c1.y;
r1x = r1.x;
r1y = r1.y;
}
//Is the circle above the rectangle's top edge?
if (c1y - c1.yAnchorOffset < r1y - r1.halfHeight - r1.yAnchorOffset) {
//If it is, we need to check whether it's in the
//top left, top center or top right
if (c1x - c1.xAnchorOffset < r1x - 1 - r1.halfWidth - r1.xAnchorOffset) {
region = "topLeft";
} else if (c1x - c1.xAnchorOffset > r1x + 1 + r1.halfWidth - r1.xAnchorOffset) {
region = "topRight";
} else {
region = "topMiddle";
}
}
//The circle isn't above the top edge, so it might be
//below the bottom edge
else if (c1y - c1.yAnchorOffset > r1y + r1.halfHeight - r1.yAnchorOffset) {
//If it is, we need to check whether it's in the bottom left,
//bottom center, or bottom right
if (c1x - c1.xAnchorOffset < r1x - 1 - r1.halfWidth - r1.xAnchorOffset) {
region = "bottomLeft";
} else if (c1x - c1.xAnchorOffset > r1x + 1 + r1.halfWidth - r1.xAnchorOffset) {
region = "bottomRight";
} else {
region = "bottomMiddle";
}
}
//The circle isn't above the top edge or below the bottom edge,
//so it must be on the left or right side
else {
if (c1x - c1.xAnchorOffset < r1x - r1.halfWidth - r1.xAnchorOffset) {
region = "leftMiddle";
} else {
region = "rightMiddle";
}
}
//Is this the circle touching the flat sides
//of the rectangle?
if (region === "topMiddle" || region === "bottomMiddle" || region === "leftMiddle" || region === "rightMiddle") {
//Yes, it is, so do a standard rectangle vs. rectangle collision test
collision = this.rectangleCollision(c1, r1, bounce, global);
}
//The circle is touching one of the corners, so do a
//circle vs. point collision test
else {
let point = {};
switch (region) {
case "topLeft":
point.x = r1x - r1.xAnchorOffset;
point.y = r1y - r1.yAnchorOffset;
break;
case "topRight":
point.x = r1x + r1.width - r1.xAnchorOffset;
point.y = r1y - r1.yAnchorOffset;
break;
case "bottomLeft":
point.x = r1x - r1.xAnchorOffset;
point.y = r1y + r1.height - r1.yAnchorOffset;
break;
case "bottomRight":
point.x = r1x + r1.width - r1.xAnchorOffset;
point.y = r1y + r1.height - r1.yAnchorOffset;
}
//Check for a collision between the circle and the point
collision = this.circlePointCollision(c1, point, bounce, global);
}
if (collision) {
return region;
} else {
return collision;
}
}
/*
circlePointCollision
--------------------
Use it to boucnce a circle off a point.
Parameters:
a. A sprite object with `centerX`, `centerY`, and `radius` properties.
b. A point object with `x` and `y` properties.
*/
circlePointCollision(c1, point, bounce = false, global = false) {
//Add collision properties
if (!c1._bumpPropertiesAdded) this.addCollisionProperties(c1);
//A point is just a circle with a diameter of
//1 pixel, so we can cheat. All we need to do is an ordinary circle vs. circle
//Collision test. Just supply the point with the properties
//it needs
point.diameter = 1;
point.width = point.diameter;
point.radius = 0.5;
point.centerX = point.x;
point.centerY = point.y;
point.gx = point.x;
point.gy = point.y;
point.xAnchorOffset = 0;
point.yAnchorOffset = 0;
point._bumpPropertiesAdded = true;
return this.circleCollision(c1, point, bounce, global);
}
/*
bounceOffSurface
----------------
Use this to bounce an object off another object.
Parameters:
a. An object with `v.x` and `v.y` properties. This represents the object that is colliding
with a surface.
b. An object with `x` and `y` properties. This represents the surface that the object
is colliding into.
The first object can optionally have a mass property that's greater than 1. The mass will
be used to dampen the bounce effect.
*/
bounceOffSurface(o, s) {
//Add collision properties
if (!o._bumpPropertiesAdded) this.addCollisionProperties(o);
let dp1, dp2,
p1 = {},
p2 = {},
bounce = {},
mass = o.mass || 1;
//1. Calculate the collision surface's properties
//Find the surface vector's left normal
s.lx = s.y;
s.ly = -s.x;
//Find its magnitude
s.magnitude = Math.sqrt(s.x * s.x + s.y * s.y);
//Find its normalized values
s.dx = s.x / s.magnitude;
s.dy = s.y / s.magnitude;
//2. Bounce the object (o) off the surface (s)
//Find the dot product between the object and the surface
dp1 = o.vx * s.dx + o.vy * s.dy;
//Project the object's velocity onto the collision surface
p1.vx = dp1 * s.dx;
p1.vy = dp1 * s.dy;
//Find the dot product of the object and the surface's left normal (s.lx and s.ly)
dp2 = o.vx * (s.lx / s.magnitude) + o.vy * (s.ly / s.magnitude);
//Project the object's velocity onto the surface's left normal
p2.vx = dp2 * (s.lx / s.magnitude);
p2.vy = dp2 * (s.ly / s.magnitude);
//Reverse the projection on the surface's left normal
p2.vx *= -1;
p2.vy *= -1;
//Add up the projections to create a new bounce vector
bounce.x = p1.vx + p2.vx;
bounce.y = p1.vy + p2.vy;
//Assign the bounce vector to the object's velocity
//with optional mass to dampen the effect
o.vx = bounce.x / mass;
o.vy = bounce.y / mass;
}
/*
contain
-------
`contain` can be used to contain a sprite with `x` and
`y` properties inside a rectangular area.
The `contain` function takes four arguments: a sprite with `x` and `y`
properties, an object literal with `x`, `y`, `width` and `height` properties. The
third argument is a Boolean (true/false) value that determines if the sprite
should bounce when it hits the edge of the container. The fourth argument
is an extra user-defined callback function that you can call when the
sprite hits the container
```js
contain(anySprite, {x: 0, y: 0, width: 512, height: 512}, true, callbackFunction);
```
The code above will contain the sprite's position inside the 512 by
512 pixel area defined by the object. If the sprite hits the edges of
the container, it will bounce. The `callBackFunction` will run if
there's a collision.
An additional feature of the `contain` method is that if the sprite
has a `mass` property, it will be used to dampen the sprite's bounce
in a natural looking way.
If the sprite bumps into any of the containing object's boundaries,
the `contain` function will return a value that tells you which side
the sprite bumped into: “left”, “top”, “right” or “bottom”. Here's how
you could keep the sprite contained and also find out which boundary
it hit:
```js
//Contain the sprite and find the collision value
let collision = contain(anySprite, {x: 0, y: 0, width: 512, height: 512});
//If there's a collision, display the boundary that the collision happened on
if(collision) {
if collision.has("left") console.log("The sprite hit the left");
if collision.has("top") console.log("The sprite hit the top");
if collision.has("right") console.log("The sprite hit the right");
if collision.has("bottom") console.log("The sprite hit the bottom");
}
```
If the sprite doesn't hit a boundary, the value of
`collision` will be `undefined`.
*/
/*
contain(sprite, container, bounce = false, extra = undefined) {
//Helper methods that compensate for any possible shift the the
//sprites' anchor points
let nudgeAnchor = (o, value, axis) => {
if (o.anchor !== undefined) {
if (o.anchor[axis] !== 0) {
return value * ((1 - o.anchor[axis]) - o.anchor[axis]);
} else {
return value;
}
} else {
return value;
}
};
let compensateForAnchor = (o, value, axis) => {
if (o.anchor !== undefined) {
if (o.anchor[axis] !== 0) {
return value * o.anchor[axis];
} else {
return 0;
}
} else {
return 0;
}
};
let compensateForAnchors = (a, b, property1, property2) => {
return compensateForAnchor(a, a[property1], property2) + compensateForAnchor(b, b[property1], property2)
};
//Create a set called `collision` to keep track of the
//boundaries with which the sprite is colliding
let collision = new Set();
//Left
if (sprite.x - compensateForAnchor(sprite, sprite.width, "x") < container.x - sprite.parent.gx - compensateForAnchor(container, container.width, "x")) {
//Bounce the sprite if `bounce` is true
if (bounce) sprite.vx *= -1;
//If the sprite has `mass`, let the mass
//affect the sprite's velocity
if(sprite.mass) sprite.vx /= sprite.mass;
//Keep the sprite inside the container
sprite.x = container.x - sprite.parent.gx + compensateForAnchor(sprite, sprite.width, "x") - compensateForAnchor(container, container.width, "x");
//Add "left" to the collision set
collision.add("left");
}
//Top
if (sprite.y - compensateForAnchor(sprite, sprite.height, "y") < container.y - sprite.parent.gy - compensateForAnchor(container, container.height, "y")) {
if (bounce) sprite.vy *= -1;
if(sprite.mass) sprite.vy /= sprite.mass;
sprite.y = container.x - sprite.parent.gy + compensateForAnchor(sprite, sprite.height, "y") - compensateForAnchor(container, container.height, "y");
collision.add("top");
}
//Right
if (sprite.x - compensateForAnchor(sprite, sprite.width, "x") + sprite.width > container.width - compensateForAnchor(container, container.width, "x")) {
if (bounce) sprite.vx *= -1;
if(sprite.mass) sprite.vx /= sprite.mass;
sprite.x = container.width - sprite.width + compensateForAnchor(sprite, sprite.width, "x") - compensateForAnchor(container, container.width, "x");
collision.add("right");
}
//Bottom
if (sprite.y - compensateForAnchor(sprite, sprite.height, "y") + sprite.height > container.height - compensateForAnchor(container, container.height, "y")) {
if (bounce) sprite.vy *= -1;
if(sprite.mass) sprite.vy /= sprite.mass;
sprite.y = container.height - sprite.height + compensateForAnchor(sprite, sprite.height, "y") - compensateForAnchor(container, container.height, "y");
collision.add("bottom");
}
//If there were no collisions, set `collision` to `undefined`
if (collision.size === 0) collision = undefined;
//The `extra` function runs if there was a collision
//and `extra` has been defined
if (collision && extra) extra(collision);
//Return the `collision` value
return collision;
}
*/
contain(sprite, container, bounce = false, extra = undefined) {
//Add collision properties
if (!sprite._bumpPropertiesAdded) this.addCollisionProperties(sprite);
//Give the container x and y anchor offset values, if it doesn't
//have any
if (container.xAnchorOffset === undefined) container.xAnchorOffset = 0;
if (container.yAnchorOffset === undefined) container.yAnchorOffset = 0;
if (sprite.parent.gx === undefined) sprite.parent.gx = 0;
if (sprite.parent.gy === undefined) sprite.parent.gy = 0;
//Create a Set called `collision` to keep track of the
//boundaries with which the sprite is colliding
let collision = new Set();
//Left
if (sprite.x - sprite.xAnchorOffset < container.x - sprite.parent.gx - container.xAnchorOffset) {
//Bounce the sprite if `bounce` is true
if (bounce) sprite.vx *= -1;
//If the sprite has `mass`, let the mass
//affect the sprite's velocity
if (sprite.mass) sprite.vx /= sprite.mass;
//Reposition the sprite inside the container
sprite.x = container.x - sprite.parent.gx - container.xAnchorOffset + sprite.xAnchorOffset;
//Make a record of the side which the container hit
collision.add("left");
}
//Top
if (sprite.y - sprite.yAnchorOffset < container.y - sprite.parent.gy - container.yAnchorOffset) {
if (bounce) sprite.vy *= -1;
if (sprite.mass) sprite.vy /= sprite.mass;
sprite.y = container.y - sprite.parent.gy - container.yAnchorOffset + sprite.yAnchorOffset;;
collision.add("top");
}
//Right
if (sprite.x - sprite.xAnchorOffset + sprite.width > container.width - container.xAnchorOffset) {
if (bounce) sprite.vx *= -1;
if (sprite.mass) sprite.vx /= sprite.mass;
sprite.x = container.width - sprite.width - container.xAnchorOffset + sprite.xAnchorOffset;
collision.add("right");
}
//Bottom
if (sprite.y - sprite.yAnchorOffset + sprite.height > container.height - container.yAnchorOffset) {
if (bounce) sprite.vy *= -1;
if (sprite.mass) sprite.vy /= sprite.mass;
sprite.y = container.height - sprite.height - container.yAnchorOffset + sprite.yAnchorOffset;
collision.add("bottom");
}
//If there were no collisions, set `collision` to `undefined`
if (collision.size === 0) collision = undefined;
//The `extra` function runs if there was a collision
//and `extra` has been defined
if (collision && extra) extra(collision);
//Return the `collision` value
return collision;
}
//`outsideBounds` checks whether a sprite is outide the boundary of
//another object. It returns an object called `collision`. `collision` will be `undefined` if there's no
//collision. But if there is a collision, `collision` will be
//returned as a Set containg strings that tell you which boundary
//side was crossed: "left", "right", "top" or "bottom"
outsideBounds(s, bounds, extra) {
let x = bounds.x,
y = bounds.y,
width = bounds.width,
height = bounds.height;
//The `collision` object is used to store which
//side of the containing rectangle the sprite hits
let collision = new Set();
//Left
if (s.x < x - s.width) {
collision.add("left");
}
//Top
if (s.y < y - s.height) {
collision.add("top");
}
//Right
if (s.x > width + s.width) {
collision.add("right");
}
//Bottom
if (s.y > height + s.height) {
collision.add("bottom");
}
//If there were no collisions, set `collision` to `undefined`
if (collision.size === 0) collision = undefined;
//The `extra` function runs if there was a collision
//and `extra` has been defined
if (collision && extra) extra(collision);
//Return the `collision` object
return collision;
}
/*
_getCenter
----------
A utility that finds the center point of the sprite. If it's anchor point is the
sprite's top left corner, then the center is calculated from that point.
If the anchor point has been shifted, then the anchor x/y point is used as the sprite's center
*/
_getCenter(o, dimension, axis) {
if (o.anchor !== undefined) {
if (o.anchor[axis] !== 0) {
return 0;
} else {
//console.log(o.anchor[axis])
return dimension / 2;
}
} else {
return dimension;
}
}
/*
hit
---
A convenient universal collision function to test for collisions
between rectangles, circles, and points.
*/
hit(a, b, react = false, bounce = false, global, extra = undefined) {
//Local references to bump's collision methods
let hitTestPoint = this.hitTestPoint.bind(this),
hitTestRectangle = this.hitTestRectangle.bind(this),
hitTestCircle = this.hitTestCircle.bind(this),
movingCircleCollision = this.movingCircleCollision.bind(this),
circleCollision = this.circleCollision.bind(this),
hitTestCircleRectangle = this.hitTestCircleRectangle.bind(this),
rectangleCollision = this.rectangleCollision.bind(this),
circleRectangleCollision = this.circleRectangleCollision.bind(this);
let collision,
aIsASprite = a.parent !== undefined,
bIsASprite = b.parent !== undefined;
//Check to make sure one of the arguments isn't an array
if (aIsASprite && b instanceof Array || bIsASprite && a instanceof Array) {
//If it is, check for a collision between a sprite and an array
spriteVsArray();
} else {
//If one of the arguments isn't an array, find out what type of
//collision check to run
collision = findCollisionType(a, b);
if (collision && extra) extra(collision);
}
//Return the result of the collision.
//It will be `undefined` if there's no collision and `true` if
//there is a collision. `rectangleCollision` sets `collsision` to
//"top", "bottom", "left" or "right" depeneding on which side the
//collision is occuring on
return collision;
function findCollisionType(a, b) {
//Are `a` and `b` both sprites?
//(We have to check again if this function was called from
//`spriteVsArray`)
let aIsASprite = a.parent !== undefined;
let bIsASprite = b.parent !== undefined;
if (aIsASprite && bIsASprite) {
//Yes, but what kind of sprites?
if (a.diameter && b.diameter) {
//They're circles
return circleVsCircle(a, b);
} else if (a.diameter && !b.diameter) {
//The first one is a circle and the second is a rectangle
return circleVsRectangle(a, b);
} else {
//They're rectangles
return rectangleVsRectangle(a, b);
}
}
//They're not both sprites, so what are they?
//Is `a` not a sprite and does it have x and y properties?
else if (bIsASprite && !(a.x === undefined) && !(a.y === undefined)) {
//Yes, so this is a point vs. sprite collision test
return hitTestPoint(a, b);
} else {
//The user is trying to test some incompatible objects
throw new Error(`I'm sorry, ${a} and ${b} cannot be use together in a collision test.'`);
}
}
function spriteVsArray() {
//If `a` happens to be the array, flip it around so that it becomes `b`
if (a instanceof Array) {
let [a, b] = [b, a];
}
//Loop through the array in reverse
for (let i = b.length - 1; i >= 0; i--) {
let sprite = b[i];
collision = findCollisionType(a, sprite);
if (collision && extra) extra(collision, sprite);
}
}
function circleVsCircle(a, b) {
//If the circles shouldn't react to the collision,
//just test to see if they're touching
if (!react) {
return hitTestCircle(a, b);
}
//Yes, the circles should react to the collision
else {
//Are they both moving?
if (a.vx + a.vy !== 0 && b.vx + b.vy !== 0) {
//Yes, they are both moving
//(moving circle collisions always bounce apart so there's
//no need for the third, `bounce`, argument)
return movingCircleCollision(a, b, global);
} else {
//No, they're not both moving
return circleCollision(a, b, bounce, global);
}
}
}
function rectangleVsRectangle(a, b) {
//If the rectangles shouldn't react to the collision, just
//test to see if they're touching
if (!react) {
return hitTestRectangle(a, b, global);
} else {
return rectangleCollision(a, b, bounce, global);
}
}
function circleVsRectangle(a, b) {
//If the rectangles shouldn't react to the collision, just
//test to see if they're touching
if (!react) {
return hitTestCircleRectangle(a, b, global);
} else {
return circleRectangleCollision(a, b, bounce, global);
}
}
}
}