collider2d
Version:
A 2D collision checker for modern JavaScript games.
1,703 lines (1,413 loc) • 48.6 kB
JavaScript
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
var Vector = /*#__PURE__*/function () {
/**
* The x coordinate of this vector.
*
* @private
*
* @property {number}
*/
/**
* The y coordinate of this vector.
*
* @private
*
* @property {number}
*/
/**
* @param {number} [x=0] The x coordinate of this vector.
* @param {number} [y=0] The y coordinate of this vector.
*/
function Vector() {
var x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
var y = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
_classCallCheck(this, Vector);
_defineProperty(this, "_x", 0);
_defineProperty(this, "_y", 0);
this._x = x;
this._y = y;
}
/**
* Returns the x value of this vector.
*
* @returns {number}
*/
_createClass(Vector, [{
key: "x",
get: function get() {
return this._x;
}
/**
* Returns the y value of this vector.
*
* @returns {number}
*/
,
set:
/**
* Sets a new x value for this vector.
*
* @param {number} x The new x value for this vector.
*/
function set(x) {
this._x = x;
}
/**
* Sets a new y value for this vector.
*
* @param {number} y The new y value for this vector.
*/
}, {
key: "y",
get: function get() {
return this._y;
},
set: function set(y) {
this._y = y;
}
/**
* Copy the values of another Vector into this one.
*
* @param {Vector} other The other Vector.
*
* @returns {Vector} Returns this for chaining.
*/
}, {
key: "copy",
value: function copy(other) {
this._x = other.x;
this._y = other.y;
return this;
}
/**
* Create a new Vector with the same coordinates as the one.
*
* @returns {Vector} The new cloned Vector.
*/
}, {
key: "clone",
value: function clone() {
return new Vector(this.x, this.y);
}
/**
* Change this Vector to be perpendicular to what it was before.
*
* Effectively this rotates it 90 degrees in a clockwise direction.
*
* @returns {Vector} Returns this for chaining.
*/
}, {
key: "perp",
value: function perp() {
var x = this.x;
this._x = this.y;
this._y = -x;
return this;
}
/**
* Rotate this Vector (counter-clockwise) by the specified angle (in radians).
*
* @param {number} angle The angle to rotate (in radians).
*
* @returns {Vector} Returns this for chaining.
*/
}, {
key: "rotate",
value: function rotate(angle) {
var x = this.x;
var y = this.y;
this._x = x * Math.cos(angle) - y * Math.sin(angle);
this._y = x * Math.sin(angle) + y * Math.cos(angle);
return this;
}
/**
* Reverse this Vector.
*
* @returns {Vector} Returns this for chaining.
*/
}, {
key: "reverse",
value: function reverse() {
this._x = -this.x;
this._y = -this.y;
return this;
}
/**
* Normalize this vector (make it have a length of `1`).
*
* @returns {Vector} Returns this for chaining.
*/
}, {
key: "normalize",
value: function normalize() {
var d = this.len();
if (d > 0) {
this._x = this.x / d;
this._y = this.y / d;
}
return this;
}
/**
* Add another Vector to this one.
*
* @param {Vector} other The other Vector.
*
* @returns {Vector} Returns this for chaining.
*/
}, {
key: "add",
value: function add(other) {
this._x += other.x;
this._y += other.y;
return this;
}
/**
* Subtract another Vector from this one.
*
* @param {Vector} other The other Vector.
*
* @returns {Vector} Returns this for chaining.
*/
}, {
key: "sub",
value: function sub(other) {
this._x -= other.x;
this._y -= other.y;
return this;
}
/**
* Scale this Vector.
*
* An independent scaling factor can be provided for each axis, or a single scaling factor will scale both `x` and `y`.
*
* @param {number} x The scaling factor in the x direction.
* @param {number} [y] The scaling factor in the y direction.
*
* @returns {Vector} Returns this for chaining.
*/
}, {
key: "scale",
value: function scale(x, y) {
this._x *= x;
this._y *= typeof y != 'undefined' ? y : x;
return this;
}
/**
* Project this Vector onto another Vector.
*
* @param {Vector} other The Vector to project onto.
*
* @returns {Vector} Returns this for chaining.
*/
}, {
key: "project",
value: function project(other) {
var amt = this.dot(other) / other.len2();
this._x = amt * other.x;
this._y = amt * other.y;
return this;
}
/**
* Project this Vector onto a Vector of unit length.
*
* This is slightly more efficient than `project` when dealing with unit vectors.
*
* @param {Vector} other The unit vector to project onto.
*
* @returns {Vector} Returns this for chaining.
*/
}, {
key: "projectN",
value: function projectN(other) {
var amt = this.dot(other);
this._x = amt * other.x;
this._y = amt * other.y;
return this;
}
/**
* Reflect this Vector on an arbitrary axis.
*
* @param {Vector} axis The Vector representing the axis.
*
* @returns {Vector} Returns this for chaining.
*/
}, {
key: "reflect",
value: function reflect(axis) {
var x = this.x;
var y = this.y;
this.project(axis).scale(2);
this._x -= x;
this._y -= y;
return this;
}
/**
* Reflect this Vector on an arbitrary axis.
*
* This is slightly more efficient than `reflect` when dealing with an axis that is a unit vector.
*
* @param {Vector} axis The Vector representing the axis.
*
* @returns {Vector} Returns this for chaining.
*/
}, {
key: "reflectN",
value: function reflectN(axis) {
var x = this.x;
var y = this.y;
this.projectN(axis).scale(2);
this._x -= x;
this._y -= y;
return this;
}
/**
* Get the dot product of this Vector and another.
*
* @param {Vector} other The Vector to dot this one against.
*
* @returns {number} Returns the dot product of this vector.
*/
}, {
key: "dot",
value: function dot(other) {
return this.x * other.x + this.y * other.y;
}
/**
* Get the squared length of this Vector.
*
* @returns {number} Returns the squared length of this vector.
*/
}, {
key: "len2",
value: function len2() {
return this.dot(this);
}
/**
* Get the length of this Vector.
*
* @returns {number} Returns the length of this vector.
*/
}, {
key: "len",
value: function len() {
return Math.sqrt(this.len2());
}
}]);
return Vector;
}();
/**
* Represents a *convex* polygon with any number of points (specified in counter-clockwise order).
*
* Note: Do _not_ manually change the `points`, `angle`, or `offset` properties. Use the provided setters.
* Otherwise the calculated properties will not be updated correctly.
*
* The `pos` property can be changed directly.
*/
var Polygon = /*#__PURE__*/function () {
/**
* A vector representing the origin of this polygon (all other points are relative to this one).
*
* @private
*
* @property {Vector}
*/
/**
* An array of vectors representing the points in the polygon, in counter-clockwise order.
*
* @private
*
* @property {Array<Vector>}
*/
/**
* An Array of the points of this polygon as numbers instead of Vectors.
*
* @private
*
* @property {Array<number>}
*/
/**
* The angle of this polygon.
*
* @private
*
* @property {number}
*/
/**
* The offset of this polygon.
*
* @private
*
* @property {Vector}
*/
/**
* The calculated points of this polygon.
*
* @private
*
* @property {Array<Vector>}
*/
/**
* The edges of this polygon.
*
* @private
*
* @property {Array<Vector>}
*/
/**
* The normals of this polygon.
*
* @private
*
* @property {Array<Vector>}
*/
/**
* Create a new polygon, passing in a position vector, and an array of points (represented by vectors
* relative to the position vector). If no position is passed in, the position of the polygon will be `(0,0)`.
*
* @param {Vector} [position=Vector] A vector representing the origin of the polygon (all other points are relative to this one)
* @param {Array<Vector>} [points=[]] An array of vectors representing the points in the polygon, in counter-clockwise order.
*/
function Polygon() {
var position = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : new Vector();
var points = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
_classCallCheck(this, Polygon);
_defineProperty(this, "_position", new Vector());
_defineProperty(this, "_points", []);
_defineProperty(this, "_pointsGeneric", []);
_defineProperty(this, "_angle", 0);
_defineProperty(this, "_offset", new Vector());
_defineProperty(this, "_calcPoints", []);
_defineProperty(this, "_edges", []);
_defineProperty(this, "_normals", []);
this._position = position;
this.setPoints(points);
}
/**
* Returns the position of this polygon.
*
* @returns {Vector}
*/
_createClass(Polygon, [{
key: "position",
get: function get() {
return this._position;
}
/**
* **Note:** Not sure if this will be kept or not but for now it's disabled.
*
* Sets a new position for this polygon and recalculates the points.
*
* @param {Vector} position A Vector representing the new position of this polygon.
*/
// set position(position: Vector) {
// const diffX: number = -(this._position.x - position.x);
// const diffY: number = -(this._position.y - position.y);
// const diffPoint: Vector = new Vector(diffX, diffY);
// const points: Array<Vector> = [];
// this._points.map((point: Vector) => {
// const tempX: number = point.x;
// const tempY: number = point.y;
// const tempPoint: Vector = new Vector(tempX, tempY);
// const calculatedPoint: Vector = tempPoint.add(diffPoint);
// points.push(calculatedPoint);
// });
// this.setPoints(points, true);
// }
/**
* Returns the points of this polygon.
*
* @returns {Array<Vector>}
*/
}, {
key: "points",
get: function get() {
return this._points;
}
/**
* Returns the points of this polygon as numbers instead of Vectors.
*
* @returns {Array<number>}
*/
}, {
key: "pointsGeneric",
get: function get() {
return this._pointsGeneric;
}
/**
* Returns the calculated points of this polygon.
*
* @returns {Array<Vector>}
*/
}, {
key: "calcPoints",
get: function get() {
return this._calcPoints;
}
/**
* Returns the offset of this polygon.
*
* @returns {Vector}
*/
}, {
key: "offset",
get: function get() {
return this._offset;
}
/**
* Returns the angle of this polygon.
*
* @returns {number}
*/
}, {
key: "angle",
get: function get() {
return this._angle;
}
/**
* Returns the edges of this polygon.
*
* @returns {Array<Vector>}
*/
}, {
key: "edges",
get: function get() {
return this._edges;
}
/**
* Returns the normals of this polygon.
*
* @returns {Array<Vector>}
*/
}, {
key: "normals",
get: function get() {
return this._normals;
}
/**
* Set the points of the polygon. Any consecutive duplicate points will be combined.
*
* Note: The points are counter-clockwise *with respect to the coordinate system*. If you directly draw the points on a screen
* that has the origin at the top-left corner it will _appear_ visually that the points are being specified clockwise. This is
* just because of the inversion of the Y-axis when being displayed.
*
* @param {Array<Vector>} points An array of vectors representing the points in the polygon, in counter-clockwise order.
* *
* @returns {Polygon} Returns this for chaining.
*/
}, {
key: "setPoints",
value: function setPoints(points) {
// Only re-allocate if this is a new polygon or the number of points has changed.
var lengthChanged = !this.points || this.points.length !== points.length;
if (lengthChanged) {
var i;
var calcPoints = this._calcPoints = [];
var edges = this._edges = [];
var normals = this._normals = []; // Allocate the vector arrays for the calculated properties
for (i = 0; i < points.length; i++) {
// Remove consecutive duplicate points
var p1 = points[i];
var p2 = i < points.length - 1 ? points[i + 1] : points[0]; // Push the points to the generic points Array.
this._pointsGeneric.push(points[i].x, points[i].y);
if (p1 !== p2 && p1.x === p2.x && p1.y === p2.y) {
points.splice(i, 1);
i -= 1;
continue;
}
calcPoints.push(new Vector());
edges.push(new Vector());
normals.push(new Vector());
}
}
this._points = points;
this._recalc();
return this;
}
/**
* Set the current rotation angle of the polygon.
*
* @param {number} angle The current rotation angle (in radians).
*
* @returns {Polygon} Returns this for chaining.
*/
}, {
key: "setAngle",
value: function setAngle(angle) {
this._angle = angle;
this._recalc();
return this;
}
/**
* Set the current offset to apply to the `points` before applying the `angle` rotation.
*
* @param {Vector} offset The new offset Vector.
*
* @returns {Polygon} Returns this for chaining.
*/
}, {
key: "setOffset",
value: function setOffset(offset) {
this._offset = offset;
this._recalc();
return this;
}
/**
* Rotates this Polygon counter-clockwise around the origin of *its local coordinate system* (i.e. `position`).
*
* Note: This changes the **original** points (so any `angle` will be applied on top of this rotation).
*
* @param {number} angle The angle to rotate (in radians).
*
* @returns {Polygon} Returns this for chaining.
*/
}, {
key: "rotate",
value: function rotate(angle) {
var points = this.points;
var len = points.length;
for (var i = 0; i < len; i++) {
points[i].rotate(angle);
}
this._recalc();
return this;
}
/**
* Translates the points of this polygon by a specified amount relative to the origin of *its own coordinate system* (i.e. `position`).
*
* Note: This changes the **original** points (so any `offset` will be applied on top of this translation)
*
* @param {number} x The horizontal amount to translate.
* @param {number} y The vertical amount to translate.
*
* @returns {Polygon} Returns this for chaining.
*/
}, {
key: "translate",
value: function translate(x, y) {
var points = this.points;
var len = points.length;
for (var i = 0; i < len; i++) {
points[i].x += x;
points[i].y += y;
}
this._recalc();
return this;
}
/**
* Computes the calculated collision Polygon.
*
* This applies the `angle` and `offset` to the original points then recalculates the edges and normals of the collision Polygon.
*
* @private
*
* @returns {Polygon} Returns this for chaining.
*/
}, {
key: "_recalc",
value: function _recalc() {
// Calculated points - this is what is used for underlying collisions and takes into account
// the angle/offset set on the polygon.
var calcPoints = this.calcPoints; // The edges here are the direction of the `n`th edge of the polygon, relative to
// the `n`th point. If you want to draw a given edge from the edge value, you must
// first translate to the position of the starting point.
var edges = this._edges; // The normals here are the direction of the normal for the `n`th edge of the polygon, relative
// to the position of the `n`th point. If you want to draw an edge normal, you must first
// translate to the position of the starting point.
var normals = this._normals; // Copy the original points array and apply the offset/angle
var points = this.points;
var offset = this.offset;
var angle = this.angle;
var len = points.length;
var i;
for (i = 0; i < len; i++) {
var calcPoint = calcPoints[i].copy(points[i]);
calcPoint.x += offset.x;
calcPoint.y += offset.y;
if (angle !== 0) calcPoint.rotate(angle);
} // Calculate the edges/normals
for (i = 0; i < len; i++) {
var p1 = calcPoints[i];
var p2 = i < len - 1 ? calcPoints[i + 1] : calcPoints[0];
var e = edges[i].copy(p2).sub(p1);
normals[i].copy(e).perp().normalize();
}
return this;
}
/**
* Compute the axis-aligned bounding box.
*
* Any current state (translations/rotations) will be applied before constructing the AABB.
*
* Note: Returns a _new_ `Polygon` each time you call this.
*
* @returns {Polygon} Returns this for chaining.
*/
}, {
key: "getAABB",
value: function getAABB() {
var points = this.calcPoints;
var len = points.length;
var xMin = points[0].x;
var yMin = points[0].y;
var xMax = points[0].x;
var yMax = points[0].y;
for (var i = 1; i < len; i++) {
var point = points[i];
if (point["x"] < xMin) xMin = point["x"];else if (point["x"] > xMax) xMax = point["x"];
if (point["y"] < yMin) yMin = point["y"];else if (point["y"] > yMax) yMax = point["y"];
}
return new Polygon(this._position.clone().add(new Vector(xMin, yMin)), [new Vector(), new Vector(xMax - xMin, 0), new Vector(xMax - xMin, yMax - yMin), new Vector(0, yMax - yMin)]);
}
/**
* Compute the centroid (geometric center) of the Polygon.
*
* Any current state (translations/rotations) will be applied before computing the centroid.
*
* See https://en.wikipedia.org/wiki/Centroid#Centroid_of_a_polygon
*
* Note: Returns a _new_ `Vector` each time you call this.
*
* @returns {Vector} Returns a Vector that contains the coordinates of the centroid.
*/
}, {
key: "getCentroid",
value: function getCentroid() {
var points = this.calcPoints;
var len = points.length;
var cx = 0;
var cy = 0;
var ar = 0;
for (var i = 0; i < len; i++) {
var p1 = points[i];
var p2 = i === len - 1 ? points[0] : points[i + 1]; // Loop around if last point
var a = p1["x"] * p2["y"] - p2["x"] * p1["y"];
cx += (p1["x"] + p2["x"]) * a;
cy += (p1["y"] + p2["y"]) * a;
ar += a;
}
ar = ar * 3; // we want 1 / 6 the area and we currently have 2*area
cx = cx / ar;
cy = cy / ar;
return new Vector(cx, cy);
}
}]);
return Polygon;
}();
/**
* A box represents an axis-aligned box with a width and height.
*/
var Box = /*#__PURE__*/function () {
/**
* The position of this box as a Vector.
*
* @private
*
* @property {Vector}
*/
/**
* The width of this box.
*
* @private
*
* @property {number}
*/
/**
* The height of this box.
*
* @private
*
* @property {number}
*/
/**
* Creates a new Box, with the specified position, width, and height.
*
* If no position is given, the position will be `(0, 0)`. If no width or height are given, they will be set to `0`.
*
* @param {Vector} [position=new Vector()] The position of this box as a Vector.
* @param {number} [width=0] The width of this box.
* @param {number} [height=0] The height of this box.
*/
function Box() {
var position = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : new Vector();
var width = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
var height = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
_classCallCheck(this, Box);
_defineProperty(this, "_position", new Vector());
_defineProperty(this, "_width", 0);
_defineProperty(this, "_height", 0);
this._position = position;
this._width = width;
this._height = height;
}
/**
* Returns a Polygon whose edges are the same as this Box.
*
* @returns {Polygon} A new Polygon that represents this Box.
*/
_createClass(Box, [{
key: "toPolygon",
value: function toPolygon() {
return new Polygon(new Vector(this._position.x, this._position.y), [new Vector(), new Vector(this._width, 0), new Vector(this._width, this._height), new Vector(0, this._height)]);
}
}]);
return Box;
}();
/**
* Represents a circle with a position and a radius.
*
* Creates a new Circle, optionally passing in a position and/or radius. If no position is given, the Circle will be at `(0,0)`.
*
* If no radius is provided the circle will have a radius of `0`.
*/
var Circle = /*#__PURE__*/function () {
/**
* A Vector representing the center point of this circle.
*
* @private
*
* @property {Vector}
*/
/**
* The radius of this circle.
*
* @private
*
* @property {number}
*/
/**
* A Vector representing the offset of this circle.
*
* @private
*
* @property {Vector}
*/
/**
* @param {Vector} position A Vector representing the center of this Circle.
* @param {number} radius The radius of this Circle.
*/
function Circle() {
var position = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : new Vector();
var radius = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
_classCallCheck(this, Circle);
_defineProperty(this, "_position", new Vector());
_defineProperty(this, "_radius", 0);
_defineProperty(this, "_offset", new Vector());
this._position = position;
this._radius = radius;
}
/**
* Returns the position of this circle.
*
* @returns {Vector}
*/
_createClass(Circle, [{
key: "position",
get: function get() {
return this._position;
}
/**
* Returns the radius of this circle.
*
* @returns {number}
*/
}, {
key: "radius",
get: function get() {
return this._radius;
}
/**
* Returns the offset of this circle.
*
* @returns {Vector}
*/
}, {
key: "offset",
get: function get() {
return this._offset;
}
/**
* Set a new offset for this circle.
*
* @param {Vector} offset The new offset for this circle.
*/
,
set: function set(offset) {
this._offset = offset;
}
/**
* Translate the center of the cirlc.e
*
* @param {Vector} position A Vector representing the new center of this circle.
*/
}, {
key: "translate",
value: function translate(x, y) {
this._position.x += x;
this._position.y += y;
}
/**
* Compute the axis-aligned bounding box (AABB) of this Circle.
*
* Note: Returns a new `Polygon` each time this is called.
*
* @returns {Polygon} Returns the AABB of this circle.
*/
}, {
key: "getAABB",
value: function getAABB() {
var corner = this._position.clone().add(this._offset).sub(new Vector(this._radius, this._radius));
return new Box(corner, this._radius * 2, this._radius * 2).toPolygon();
}
/**
* Set the current offset to apply to the radius.
*
* @param {Vector} offset The new offset Vector.
*
* @returns {Circle} Returns this for chaining.
*/
}, {
key: "setOffset",
value: function setOffset(offset) {
this._offset = offset;
return this;
}
}]);
return Circle;
}();
/**
* An object representing the result of an intersection containing:
* - The two objects participating in the intersection
* - The vector representing the minimum change necessary to extract the first object from the second one (as well as a unit vector in that direction and the magnitude of the overlap)
* - Whether the first object is entirely inside the second, and vice versa.
*/
var CollisionDetails = /*#__PURE__*/function () {
/**
* The first collision object.
*
* @property {Circle|Polygon}
*/
/**
* The second collision object.
*
* @property {Circle|Polygon}
*/
/**
* A unit vector representing the direction and magnitude of the overlap.
*
* @property {Vector}
*/
/**
* A vector representing the minimum change necessary to extract the first object from the second one.
*
* @property {Vector}
*/
/**
* The amount that is overlapping.
*
* @property {number}
*/
/**
* Returns true if the first collision object is completely in the second collision object.
*
* @property {boolean}
*/
/**
* Returns true if the second collision object is completely in the first collision object.
*
* @property {boolean}
*/
function CollisionDetails() {
_classCallCheck(this, CollisionDetails);
_defineProperty(this, "a", void 0);
_defineProperty(this, "b", void 0);
_defineProperty(this, "overlapN", new Vector());
_defineProperty(this, "overlapV", new Vector());
_defineProperty(this, "overlap", Number.MAX_VALUE);
_defineProperty(this, "aInB", true);
_defineProperty(this, "bInA", true);
this.clear();
}
/**
* Set some values of the response back to their defaults.
*
* Call this between tests if you are going to reuse a single CollisionDetails object for multiple intersection tests (recommended as it will avoid allcating extra memory)
*
* @returns {CollisionDetails} Returns this for chaining.
*/
_createClass(CollisionDetails, [{
key: "clear",
value: function clear() {
this.aInB = true;
this.bInA = true;
this.overlap = Number.MAX_VALUE;
return this;
}
}]);
return CollisionDetails;
}();
var Collider2D = /*#__PURE__*/function () {
/**
* A pool of `Vector objects that are used in calculations to avoid allocating memory.
*
* @private
*
* @property {Array<Vector>}
*/
/**
* A pool of arrays of numbers used in calculations to avoid allocating memory.
*
* @private
*
* @property {Array<Array<number>>}
*/
/**
* Temporary collision details object used for hit detection.
*
* @private
*
* @property {CollisionDetails}
*/
/**
* Tiny "point" Polygon used for Polygon hit detection.
*
* @private
*
* @property {Polygon}
*/
/**
* Constant used for left voronoi region.
*
* @private
*
* @property {number}
*/
/**
* Constant used for middle voronoi region.
*
* @private
*
* @property {number}
*/
/**
* Constant used for right voronoi region.
*
* @private
*
* @property {number}
*/
function Collider2D() {
_classCallCheck(this, Collider2D);
_defineProperty(this, "_T_VECTORS", []);
_defineProperty(this, "_T_ARRAYS", []);
_defineProperty(this, "_T_COLLISION_DETAILS", new CollisionDetails());
_defineProperty(this, "_TEST_POINT", new Box(new Vector(), 0.000001, 0.000001).toPolygon());
_defineProperty(this, "_LEFT_VORONOI_REGION", -1);
_defineProperty(this, "_MIDDLE_VORONOI_REGION", 0);
_defineProperty(this, "_RIGHT_VORONOI_REGION", 1);
// Populate T_VECTORS
for (var i = 0; i < 10; i++) {
this._T_VECTORS.push(new Vector());
} // Populate T_ARRAYS
for (var _i = 0; _i < 5; _i++) {
this._T_ARRAYS.push([]);
}
}
/**
* Check if a point is inside a circle.
*
* @param {Vector} point The point to test.
* @param {Circle} circle The circle to test.
*
* @returns {boolean} Returns true if the point is inside the circle or false otherwise.
*/
_createClass(Collider2D, [{
key: "pointInCircle",
value: function pointInCircle(point, circle) {
var differenceV = this._T_VECTORS.pop().copy(point).sub(circle.position).sub(circle.offset);
var radiusSq = circle.radius * circle.radius;
var distanceSq = differenceV.len2();
this._T_VECTORS.push(differenceV); // If the distance between is smaller than the radius then the point is inside the circle.
return distanceSq <= radiusSq;
}
/**
* Check if a point is inside a convex polygon.
*
* @param {Vector} point The point to test.
* @param {Polygon} polygon The polygon to test.
*
* @returns {boolean} Returns true if the point is inside the polygon or false otherwise.
*/
}, {
key: "pointInPolygon",
value: function pointInPolygon(point, polygon) {
this._TEST_POINT.position.copy(point);
this._T_COLLISION_DETAILS.clear();
var result = this.testPolygonPolygon(this._TEST_POINT, polygon, true);
if (result) result = this._T_COLLISION_DETAILS.aInB;
return result;
}
/**
* Check if two circles collide.
*
* @param {Circle} a The first circle.
* @param {Circle} b The second circle.
* @param {boolean} [details=false] If set to true and there is a collision, an object highlighting details about the collision will be returned instead of just returning true.
*
* @returns {boolean} Returns true if the circles intersect or false otherwise.
*/
}, {
key: "testCircleCircle",
value: function testCircleCircle(a, b) {
var details = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
// Check if the distance between the centers of the two circles is greater than their combined radius.
var differenceV = this._T_VECTORS.pop().copy(b.position).add(b.offset).sub(a.position).sub(a.offset);
var totalRadius = a.radius + b.radius;
var totalRadiusSq = totalRadius * totalRadius;
var distanceSq = differenceV.len2(); // If the distance is bigger than the combined radius, they don't intersect.
if (distanceSq > totalRadiusSq) {
this._T_VECTORS.push(differenceV);
return false;
}
if (details) {
this._T_COLLISION_DETAILS.clear();
var dist = Math.sqrt(distanceSq);
this._T_COLLISION_DETAILS.a = a;
this._T_COLLISION_DETAILS.b = b;
this._T_COLLISION_DETAILS.overlap = totalRadius - dist;
this._T_COLLISION_DETAILS.overlapN.copy(differenceV.normalize());
this._T_COLLISION_DETAILS.overlapV.copy(differenceV).scale(this._T_COLLISION_DETAILS.overlap);
this._T_COLLISION_DETAILS.aInB = a.radius <= b.radius && dist <= b.radius - a.radius;
this._T_COLLISION_DETAILS.bInA = b.radius <= a.radius && dist <= a.radius - b.radius;
return this._T_COLLISION_DETAILS;
}
this._T_VECTORS.push(differenceV);
return true;
}
/**
* Checks whether polygons collide.
*
* @param {Polygon} a The first polygon.
* @param {Polygon} b The second polygon.
* @param {boolean} [details=false] If set to true and there is a collision, an object highlighting details about the collision will be returned instead of just returning true.
*
* @returns {boolean} Returns true if they intersect or false otherwise.
*/
}, {
key: "testPolygonPolygon",
value: function testPolygonPolygon(a, b) {
var details = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
this._T_COLLISION_DETAILS.clear();
var aPoints = a.calcPoints;
var aLen = aPoints.length;
var bPoints = b.calcPoints;
var bLen = bPoints.length; // If any of the edge normals of A is a separating axis, no intersection.
for (var i = 0; i < aLen; i++) {
if (this._isSeparatingAxis(a.position, b.position, aPoints, bPoints, a.normals[i], this._T_COLLISION_DETAILS)) {
return false;
}
} // If any of the edge normals of B is a separating axis, no intersection.
for (var _i2 = 0; _i2 < bLen; _i2++) {
if (this._isSeparatingAxis(a.position, b.position, aPoints, bPoints, b.normals[_i2], this._T_COLLISION_DETAILS)) {
return false;
}
} // Since none of the edge normals of A or B are a separating axis, there is an intersection
// and we've already calculated the smallest overlap (in isSeparatingAxis).
// Calculate the final overlap vector.
if (details) {
this._T_COLLISION_DETAILS.a = a;
this._T_COLLISION_DETAILS.b = b;
this._T_COLLISION_DETAILS.overlapV.copy(this._T_COLLISION_DETAILS.overlapN).scale(this._T_COLLISION_DETAILS.overlap);
return this._T_COLLISION_DETAILS;
}
return true;
}
/**
* Check if a polygon and a circle collide.
*
* @param {Polygon} polygon The polygon.
* @param {Circle} circle The circle.
* @param {boolean} [details=false] If set to true and there is a collision, an object highlighting details about the collision will be returned instead of just returning true.
*
* @returns {boolean} Returns true if they intersect or false otherwise.
*/
}, {
key: "testPolygonCircle",
value: function testPolygonCircle(polygon, circle) {
var details = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
this._T_COLLISION_DETAILS.clear(); // Get the position of the circle relative to the polygon.
var circlePos = this._T_VECTORS.pop().copy(circle.position).add(circle.offset).sub(polygon.position);
var radius = circle.radius;
var radius2 = radius * radius;
var points = polygon.calcPoints;
var len = points.length;
var edge = this._T_VECTORS.pop();
var point = this._T_VECTORS.pop(); // For each edge in the polygon:
for (var i = 0; i < len; i++) {
var next = i === len - 1 ? 0 : i + 1;
var prev = i === 0 ? len - 1 : i - 1;
var overlap = 0;
var overlapN = null; // Get the edge.
edge.copy(polygon.edges[i]); // Calculate the center of the circle relative to the starting point of the edge.
point.copy(circlePos).sub(points[i]); // If the distance between the center of the circle and the point is bigger than the radius, the polygon is definitely not fully in the circle.
if (details && point.len2() > radius2) this._T_COLLISION_DETAILS.aInB = false; // Calculate which Voronoi region the center of the circle is in.
var region = this._voronoiRegion(edge, point); // If it's the left region:
if (region === this._LEFT_VORONOI_REGION) {
// We need to make sure we're in the RIGHT_VORONOI_REGION of the previous edge.
edge.copy(polygon.edges[prev]); // Calculate the center of the circle relative the starting point of the previous edge
var point2 = this._T_VECTORS.pop().copy(circlePos).sub(points[prev]);
region = this._voronoiRegion(edge, point2);
if (region === this._RIGHT_VORONOI_REGION) {
// It's in the region we want. Check if the circle intersects the point.
var dist = point.len();
if (dist > radius) {
// No intersection
this._T_VECTORS.push(circlePos);
this._T_VECTORS.push(edge);
this._T_VECTORS.push(point);
this._T_VECTORS.push(point2);
return false;
} else if (details) {
// It intersects, calculate the overlap.
this._T_COLLISION_DETAILS.bInA = false;
overlapN = point.normalize();
overlap = radius - dist;
}
}
this._T_VECTORS.push(point2); // If it's the right region:
} else if (region === this._RIGHT_VORONOI_REGION) {
// We need to make sure we're in the left region on the next edge
edge.copy(polygon.edges[next]); // Calculate the center of the circle relative to the starting point of the next edge.
point.copy(circlePos).sub(points[next]);
region = this._voronoiRegion(edge, point);
if (region === this._LEFT_VORONOI_REGION) {
// It's in the region we want. Check if the circle intersects the point.
var _dist = point.len();
if (_dist > radius) {
// No intersection
this._T_VECTORS.push(circlePos);
this._T_VECTORS.push(edge);
this._T_VECTORS.push(point);
return false;
} else if (details) {
// It intersects, calculate the overlap.
this._T_COLLISION_DETAILS.bInA = false;
overlapN = point.normalize();
overlap = radius - _dist;
}
} // Otherwise, it's the middle region:
} else {
// Need to check if the circle is intersecting the edge, change the edge into its "edge normal".
var normal = edge.perp().normalize(); // Find the perpendicular distance between the center of the circle and the edge.
var _dist2 = point.dot(normal);
var distAbs = Math.abs(_dist2); // If the circle is on the outside of the edge, there is no intersection.
if (_dist2 > 0 && distAbs > radius) {
// No intersection
this._T_VECTORS.push(circlePos);
this._T_VECTORS.push(normal);
this._T_VECTORS.push(point);
return false;
} else if (details) {
// It intersects, calculate the overlap.
overlapN = normal;
overlap = radius - _dist2; // If the center of the circle is on the outside of the edge, or part of the circle is on the outside, the circle is not fully inside the polygon.
if (_dist2 >= 0 || overlap < 2 * radius) this._T_COLLISION_DETAILS.bInA = false;
}
} // If this is the smallest overlap we've seen, keep it.
// (overlapN may be null if the circle was in the wrong Voronoi region).
if (overlapN && details && Math.abs(overlap) < Math.abs(this._T_COLLISION_DETAILS.overlap)) {
this._T_COLLISION_DETAILS.overlap = overlap;
this._T_COLLISION_DETAILS.overlapN.copy(overlapN);
}
} // Calculate the final overlap vector - based on the smallest overlap.
if (details) {
this._T_COLLISION_DETAILS.a = polygon;
this._T_COLLISION_DETAILS.b = circle;
this._T_COLLISION_DETAILS.overlapV.copy(this._T_COLLISION_DETAILS.overlapN).scale(this._T_COLLISION_DETAILS.overlap);
}
this._T_VECTORS.push(circlePos);
this._T_VECTORS.push(edge);
this._T_VECTORS.push(point);
if (details) return this._T_COLLISION_DETAILS;
return true;
}
/**
* Check if a circle and a polygon collide.
*
* **NOTE:** This is slightly less efficient than polygonCircle as it just runs polygonCircle and reverses everything
* at the end.
*
* @param {Circle} circle The circle.
* @param {Polygon} polygon The polygon.
* @param {boolean} [details=false] If set to true and there is a collision, an object highlighting details about the collision will be returned instead of just returning true.
*
* @returns {boolean} Returns true if they intersect or false otherwise.
*/
}, {
key: "testCirclePolygon",
value: function testCirclePolygon(circle, polygon) {
var details = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
// Test the polygon against the circle.
var result = this.testPolygonCircle(polygon, circle, details);
if (result && details) {
var collisionDetails = result; // Swap A and B in the collision details.
var a = collisionDetails.a;
var aInB = collisionDetails.aInB;
collisionDetails.overlapN.reverse();
collisionDetails.overlapV.reverse();
collisionDetails.a = collisionDetails.b;
collisionDetails.b = a;
collisionDetails.aInB = collisionDetails.bInA;
collisionDetails.bInA = aInB;
}
return result;
}
/**
* Check whether two convex polygons are separated by the specified axis (must be a unit vector).
*
* @private
*
* @param {Vector} aPos The position of the first polygon.
* @param {Vector} bPos The position of the second polygon.
* @param {Array<Vector>} aPoints The points in the first polygon.
* @param {Array<Vector>} bPoints The points in the second polygon.
* @param {Vector} axis The axis (unit sized) to test against. The points of both polygons will be projected onto this axis.
* @param {CollisionDetails} collisionDetails A CollisionDetails object (optional) which will be populated if the axis is not a separating axis.
*
* @return {boolean} true if it is a separating axis, false otherwise. If false, and a CollisionDetails is passed in, information about how much overlap and the direction of the overlap will be populated.
*/
}, {
key: "_isSeparatingAxis",
value: function _isSeparatingAxis(aPos, bPos, aPoints, bPoints, axis, collisionDetails) {
var rangeA = this._T_ARRAYS.pop();
var rangeB = this._T_ARRAYS.pop(); // The magnitude of the offset between the two polygons
var offsetV = this._T_VECTORS.pop().copy(bPos).sub(aPos);
var projectedOffset = offsetV.dot(axis); // Project the polygons onto the axis.
this._flattenPointsOn(aPoints, axis, rangeA);
this._flattenPointsOn(bPoints, axis, rangeB); // Move B's range to its position relative to A.
rangeB[0] += projectedOffset;
rangeB[1] += projectedOffset; // Check if there is a gap. If there is, this is a separating axis and we can stop
if (rangeA[0] > rangeB[1] || rangeB[0] > rangeA[1]) {
this._T_VECTORS.push(offsetV);
this._T_ARRAYS.push(rangeA);
this._T_ARRAYS.push(rangeB);
return true;
} // This is not a separating axis. If we're calculating collision details, calculate the overlap.
if (collisionDetails) {
var overlap = 0; // A starts further left than B
if (rangeA[0] < rangeB[0]) {
collisionDetails.aInB = false; // A ends before B does. We have to pull A out of B
if (rangeA[1] < rangeB[1]) {
overlap = rangeA[1] - rangeB[0];
collisionDetails.bInA = false; // B is fully inside A. Pick the shortest way out.
} else {
var option1 = rangeA[1] - rangeB[0];
var option2 = rangeB[1] - rangeA[0];
overlap = option1 < option2 ? option1 : -option2;
} // B starts further left than A
} else {
collisionDetails.bInA = false; // B ends before A ends. We have to push A out of B
if (rangeA[1] > rangeB[1]) {
overlap = rangeA[0] - rangeB[1];
collisionDetails.aInB = false; // A is fully inside B. Pick the shortest way out.
} else {
var _option = rangeA[1] - rangeB[0];
var _option2 = rangeB[1] - rangeA[0];
overlap = _option < _option2 ? _option : -_option2;
}
} // If this is the smallest amount of overlap we've seen so far, set it as the minimum overlap.
var absOverlap = Math.abs(overlap);
if (absOverlap < collisionDetails.overlap) {
collisionDetails.overlap = absOverlap;
collisionDetails.overlapN.copy(axis);
if (overlap < 0) collisionDetails.overlapN.reverse();
}
}
this._T_VECTORS.push(offsetV);
this._T_ARRAYS.push(rangeA);
this._T_ARRAYS.push(rangeB);
return false;
}
/**
* Flattens the specified array of points onto a unit vector axis resulting in a one dimensionsl
* range of the minimum and maximum value on that axis.
*
* @private
*
* @param {Array<Vector>} points The points to flatten.
* @param {Vector} normal The unit vector axis to flatten on.
* @param {Array<number>} result An array. After calling this function, result[0] will be the minimum value, result[1] will be the maximum value.
*/
}, {
key: "_flattenPointsOn",
value: function _flattenPointsOn(points, normal, result) {
var min = Number.MAX_VALUE;
var max = -Number.MAX_VALUE;
var len = points.length;
for (var i = 0; i < len; i++) {
// The magnitude of the projection of the point onto the normal.
var dot = points[i].dot(normal);
if (dot < min) min = dot;
if (dot > max) max = dot;
}
result[0] = min;
result[1] = max;
}
/**
* Calculates which Voronoi region a point is on a line segment.
*
* It is assumed that both the line and the point are relative to `(0,0)`
*
* | (0) |
* (-1) [S]--------------[E] (1)
* | (0) |
*
* @param {Vector} line The line segment.
* @param {Vector} point The point.
* @return {number} LEFT_VORONOI_REGION (-1) if it is the left region,
* MIDDLE_VORONOI_REGION (0) if it is the middle region,
* RIGHT_VORONOI_REGION (1) if it is the right region.
*/
}, {
key: "_voronoiRegion",
value: function _voronoiRegion(line, point) {
var len2 = line.len2();
var dp = point.dot(line); // If the point is beyond the start of the line, it is in the left voronoi region.
if (dp < 0) return this._LEFT_VORONOI_REGION; // If the point is beyond the end of the line, it is in the right voronoi region.
else if (dp > len2) return this._RIGHT_VORONOI_REGION; // Otherwise, it's in the middle one.
else return this._MIDDLE_VORONOI_REGION;
}
}]);
return Collider2D;
}();
export { Box, Circle, Collider2D as Collider2d, Polygon, Vector };