collider2d
Version:
A 2D collision checker for modern JavaScript games.
433 lines (356 loc) • 12.6 kB
text/typescript
'use strict'
import Vector from './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.
*/
export default class Polygon {
/**
* A vector representing the origin of this polygon (all other points are relative to this one).
*
* @private
*
* @property {Vector}
*/
private _position: Vector = new Vector();
/**
* An array of vectors representing the points in the polygon, in counter-clockwise order.
*
* @private
*
* @property {Array<Vector>}
*/
private _points: Array<Vector> = [];
/**
* An Array of the points of this polygon as numbers instead of Vectors.
*
* @private
*
* @property {Array<number>}
*/
private _pointsGeneric: Array<number> = []
/**
* The angle of this polygon.
*
* @private
*
* @property {number}
*/
private _angle: number = 0;
/**
* The offset of this polygon.
*
* @private
*
* @property {Vector}
*/
private _offset: Vector = new Vector();
/**
* The calculated points of this polygon.
*
* @private
*
* @property {Array<Vector>}
*/
private _calcPoints: Array<Vector> = [];
/**
* The edges of this polygon.
*
* @private
*
* @property {Array<Vector>}
*/
private _edges: Array<Vector> = [];
/**
* The normals of this polygon.
*
* @private
*
* @property {Array<Vector>}
*/
private _normals: 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.
*/
constructor(position: Vector = new Vector(), points: Array<Vector> = []) {
this._position = position;
this.setPoints(points);
}
/**
* Returns the position of this polygon.
*
* @returns {Vector}
*/
get position(): Vector { 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>}
*/
get points(): Array<Vector> { return this._points; }
/**
* Returns the points of this polygon as numbers instead of Vectors.
*
* @returns {Array<number>}
*/
get pointsGeneric(): Array<number> { return this._pointsGeneric; }
/**
* Returns the calculated points of this polygon.
*
* @returns {Array<Vector>}
*/
get calcPoints(): Array<Vector> { return this._calcPoints; }
/**
* Returns the offset of this polygon.
*
* @returns {Vector}
*/
get offset(): Vector { return this._offset; }
/**
* Returns the angle of this polygon.
*
* @returns {number}
*/
get angle(): number { return this._angle; }
/**
* Returns the edges of this polygon.
*
* @returns {Array<Vector>}
*/
get edges(): Array<Vector> { return this._edges; }
/**
* Returns the normals of this polygon.
*
* @returns {Array<Vector>}
*/
get normals(): Array<Vector> { 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.
*/
setPoints(points: Array<Vector>): Polygon {
// Only re-allocate if this is a new polygon or the number of points has changed.
const lengthChanged: boolean = !this.points || this.points.length !== points.length;
if (lengthChanged) {
let i: number;
const calcPoints: Array<Vector> = this._calcPoints = [];
const edges: Array<Vector> = this._edges = [];
const normals: Array<Vector> = this._normals = [];
// Allocate the vector arrays for the calculated properties
for (i = 0; i < points.length; i++) {
// Remove consecutive duplicate points
const p1: Vector = points[i];
const p2: Vector = 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.
*/
setAngle(angle: number): Polygon {
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.
*/
setOffset(offset: Vector): Polygon {
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.
*/
rotate(angle: number): Polygon {
const points: Array<Vector> = this.points;
const len: number = points.length;
for (let 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.
*/
translate(x: number, y: number): Polygon {
const points: Array<Vector> = this.points;
const len: number = points.length;
for (let i: number = 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.
*/
private _recalc(): Polygon {
// Calculated points - this is what is used for underlying collisions and takes into account
// the angle/offset set on the polygon.
const calcPoints: Array<Vector> = 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.
const edges: Array<Vector> = 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.
const normals: Array<Vector> = this._normals;
// Copy the original points array and apply the offset/angle
const points: Array<Vector> = this.points;
const offset: Vector = this.offset;
const angle: number = this.angle;
const len: number = points.length;
let i: number;
for (i = 0; i < len; i++) {
const calcPoint: Vector = 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++) {
const p1: Vector = calcPoints[i];
const p2: Vector = i < len - 1 ? calcPoints[i + 1] : calcPoints[0];
const e: Vector = 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.
*/
getAABB(): Polygon {
const points: Array<Vector> = this.calcPoints;
const len: number = points.length;
let xMin: number = points[0].x;
let yMin: number = points[0].y;
let xMax: number = points[0].x;
let yMax: number = points[0].y;
for (let i: number = 1; i < len; i++) {
const point: Vector = 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.
*/
getCentroid(): Vector {
const points: Array<Vector> = this.calcPoints;
const len: number = points.length;
let cx: number = 0;
let cy: number = 0;
let ar: number = 0;
for (var i: number = 0; i < len; i++) {
const p1: Vector = points[i];
const p2: Vector = i === len - 1 ? points[0] : points[i + 1]; // Loop around if last point
const a: number = 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);
}
}