const vec = require("../geometry/vec.js");
/**
* @namespace
*/
let Common = {
clamp: function(x, min, max) { // clamps x so that min <= x <= max
return Math.max(min, Math.min(x, max));
},
angleDiff: function(angle1, angle2) { // returns the signed difference between 2 angles
function mod(a, b) {
return a - Math.floor(a / b) * b;
}
return mod(angle1 - angle2 + Math.PI, Math.PI * 2) - Math.PI;
},
modDiff: function(x, y, m = 1) { // returns the signed difference between 2 values with any modulo, ie 11 oclock is 2 hours from 1 oclock with m = 12
function mod(a, b) {
return a - Math.floor(a / b) * b;
}
return mod(x - y + m/2, m) - m/2;
},
/**
* Pairs 2 positive integers, returning a unique number for each possible pairing using [elegant pairing](http://szudzik.com/ElegantPairing.pdf)
* @param {number} x - 1st number, must be positive integer
* @param {number} y - 2nd number, must be positive integer
* @return {number} Unique number from those
*/
pair: function(x, y) {
if (x > y)
return x*x + x + y;
return y*y + x;
},
/**
* Takes a paired number and returns the x/y values that created that number
* @param {number} n Paired number
* @return {vec} Pair of x/y that created that pair
*/
unpair: function(n) {
let z = Math.floor(Math.sqrt(n));
let l = n - z * z;
return l < z ? new vec(l, z) : new vec(z, l - z);
},
/**
* Pairs 2 positive integers, returning a unique number for each possible pairing using [elegant pairing](http://szudzik.com/ElegantPairing.pdf)
* Returns the same value if x/y are switched
* @param {number} x - 1st number, must be positive integer
* @param {number} y - 2nd number, must be positive integer
* @return {number} Unique number given inputs
*/
pairCommon: function(x, y) { // Elegant pairing function, but gives the same result if x/y are switched
if (x > y)
return x*x + x + y;
return y*y + y + x;
},
/**
* Calculates the center of mass of a convex body. Uses algorithm from [bell0bytes.eu/centroid-convex](https://bell0bytes.eu/centroid-convex/)
* @param {Array} vertices - Convex body vertices
*/
getCenterOfMass(vertices) {
let centroid = new vec(0, 0);
let det = 0;
let tempDet = 0;
let numVertices = vertices.length;
for (let i = 0; i < vertices.length; i++) {
let curVert = vertices[i];
let nextVert = vertices[(i + 1) % numVertices];
tempDet = curVert.x * nextVert.y - nextVert.x * curVert.y;
det += tempDet;
centroid.add2(new vec((curVert.x + nextVert.x) * tempDet, (curVert.y + nextVert.y) * tempDet));
}
centroid.div2(3 * det);
return centroid;
},
/**
* Parses a color into its base hex code and alpha. Supports hex, hex with alpha, rgb, and rgba
* @param {string} originalColor - Color to be parsed
* @return {Array} Array of [hex code, alpha] of parsed color
*/
parseColor: function(originalColor) {
if (originalColor === "transparent") {
return ["#000000", 0];
}
let color;
let alpha = 1;
if (originalColor[0] === "#") { // is a hex code
if (originalColor.length === 9) { // with alpha
color = originalColor.slice(0, 7);
alpha = parseInt(originalColor.slice(7), 16) / 256; // convert to decimel
}
else if (originalColor.length === 7) { // no alpha
color = originalColor;
}
else if (originalColor.length === 4) { // shorthand
let r = originalColor[1];
let g = originalColor[2];
let b = originalColor[3];
color = "#" + r+r + g+g + b+b;
}
else if (originalColor.length === 3) { // very shorthand (nonstandard)
let value = originalColor[1] + originalColor[2];
color = "#" + value + value + value;
}
}
else if (originalColor.slice(0, 4) === "rgb(") { // rgb
color = originalColor.slice(originalColor.indexOf("(") + 1, originalColor.indexOf(")")).split(",");
color = "#" + color.map(value => parseInt(value).toString(16).padStart(2, "0")).join("");
alpha = 1;
}
else if (originalColor.slice(0, 5) === "rgba(") { // rgba
color = originalColor.slice(originalColor.indexOf("(") + 1, originalColor.indexOf(")")).split(",");
alpha = parseInt(color.pop()) / 255;
color = "#" + color.map(value => parseInt(value).toString(16).padStart(2, "0")).join("");
}
if (!color) color = "#000000"; // color format not detected, default to black
return [color, alpha];
},
/**
* Deep copies `objB` onto `objA` in place.
* @param {Object} objA - First object
* @param {Object} objB - 2nd object, copied onto `objA`
* @param {number} maxDepth - Maximum depth it can copy. If set to 1 it is a shallow copy only
*/
merge: function(objA, objB, maxDepth = Infinity, hash = new WeakSet()) {
hash.add(objB);
Object.keys(objB).forEach(option => {
let value = objB[option];
let isElement = value instanceof Element || value instanceof Document || value === window;
if (Array.isArray(value) && maxDepth > 1) {
objA[option] = [ ...value ]; // todo: deep clone values within an array
}
else if (typeof value === "object" && value !== null && !isElement && maxDepth > 1) {
if (hash.has(value)) { // Cyclic reference
objA[option] = value;
return;
}
if (typeof objA[option] !== "object") {
objA[option] = {};
}
Common.merge(objA[option], value, maxDepth - 1, hash);
}
else {
objA[option] = value;
}
});
},
/**
* Finds if a variable is a class in disguise
* @param {*} obj - Variable to check
* @return {boolean} If the variable is a class
*/
isClass: function(obj) {
const isCtorClass = obj.constructor
&& obj.constructor.toString().substring(0, 5) === 'class'
if(obj.prototype === undefined) {
return isCtorClass;
}
const isPrototypeCtorClass = obj.prototype.constructor
&& obj.prototype.constructor.toString
&& obj.prototype.constructor.toString().substring(0, 5) === 'class'
return isCtorClass || isPrototypeCtorClass;
},
/**
* Checks if line `a1`->`a2` is intersecting line `b1`->`b2`, and at what point
* @param {vec} a1 - Start of line 1
* @param {vec} a2 - End of line 1
* @param {vec} b1 - Start of line 2
* @param {vec} b2 - End of line 2
* @return {vec|object} Point of intersection, or null if they don't intersect
*/
lineIntersects: function(a1, a2, b1, b2) { // tells you if lines a1->a2 and b1->b2 are intersecting, and at what point
if (a1.x === a2.x || a1.y === a2.y) {
a1 = new vec(a1);
}
if (b1.x === b2.x || b1.y === b2.y) {
b1 = new vec(b1);
}
if (a1.x === a2.x)
a1.x += 0.00001;
if (b1.x === b2.x)
b1.x += 0.00001;
if (a1.y === a2.y)
a1.y += 0.00001;
if (b1.y === b2.y)
b1.y += 0.00001;
let d = (a1.x - a2.x) * (b1.y - b2.y) - (a1.y - a2.y) * (b1.x - b2.x);
if (d === 0) return null;
let nx = (a1.x * a2.y - a1.y * a2.x) * (b1.x - b2.x) - (a1.x - a2.x) * (b1.x * b2.y - b1.y * b2.x);
let ny = (a1.x * a2.y - a1.y * a2.x) * (b1.y - b2.y) - (a1.y - a2.y) * (b1.x * b2.y - b1.y * b2.x);
let pt = new vec(nx / d, ny / d);
let withinX = pt.x > Math.min(a1.x, a2.x) && pt.x < Math.max(a1.x, a2.x) && pt.x > Math.min(b1.x, b2.x) && pt.x < Math.max(b1.x, b2.x);
let withinY = pt.y > Math.min(a1.y, a2.y) && pt.y < Math.max(a1.y, a2.y) && pt.y > Math.min(b1.y, b2.y) && pt.y < Math.max(b1.y, b2.y);
if (withinX && withinY) {
return pt;
}
else {
return null;
}
},
/**
* Tests if line `a1`->`a2` is intersecting `body`
* @param {vec} a1 - Start of line
* @param {vec} a2 - End of line
* @param {RigidBody} body - Body to test
* @return {boolean} If the line is intersecting the body
*/
lineIntersectsBody: function(a1, a2, body) { // tells you if line a1->a2 is intersecting with body, returns true/false
if (body.children.length > 0) {
for (let child of body.children) {
if (Common.lineIntersectsBody(a1, a2, child)) {
return true;
}
}
return false;
}
let ray = a2.sub(a1);
let rayNormalized = ray.normalize();
let rayAxes = [ rayNormalized, rayNormalized.normal() ];
let rayVertices = [ a1, a2 ];
function SAT(verticesA, verticesB, axes) {
for (let axis of axes) {
let boundsA = { min: Infinity, max: -Infinity };
let boundsB = { min: Infinity, max: -Infinity };
for (let vertice of verticesA) {
let projected = vertice.dot(axis);
if (projected < boundsA.min) {
boundsA.minVertice
}
boundsA.min = Math.min(boundsA.min, projected);
boundsA.max = Math.max(boundsA.max, projected);
}
for (let vertice of verticesB) {
let projected = vertice.dot(axis);
boundsB.min = Math.min(boundsB.min, projected);
boundsB.max = Math.max(boundsB.max, projected);
}
if (boundsA.min > boundsB.max || boundsA.max < boundsB.min) { // they are NOT colliding on this axis
return false;
}
}
return true;
}
// SAT using ray axes and body axes
return SAT(rayVertices, body.vertices, rayAxes) && SAT(rayVertices, body.vertices, body.axes);
},
/**
* Finds the static bodies around the ray from `start` to `end`. Useful for getting bodies when calling `Common.raycast` or `Common.raycastSimple`
* @param {vec} start - Start of ray
* @param {vec} end - End of ray
* @param {World} World - World to get bodies from
*/
getRayNearbyStaticBodies(start, end, World) {
let grid = World.staticGrid;
let size = grid.gridSize;
let bounds = { min: start.min(end).div2(size).floor2(), max: start.max(end).div2(size).floor2() };
let bodies = new Set();
for (let x = bounds.min.x; x <= bounds.max.x; x++) {
for (let y = bounds.min.y; y <= bounds.max.y; y++) {
let n = grid.pair(new vec(x, y));
let node = grid.grid[n];
if (node) {
for (let body of node) {
if (!bodies.has(body)) {
bodies.add(body);
}
}
}
}
}
},
getRayNearbyDynamicBodies(start, end, World) {
let grid = World.dynamicGrid;
let size = grid.gridSize;
let bounds = { min: start.min(end).div2(size).floor2(), max: start.max(end).div2(size).floor2() };
let bodies = new Set();
for (let x = bounds.min.x; x <= bounds.max.x; x++) {
for (let y = bounds.min.y; y <= bounds.max.y; y++) {
let n = grid.pair(new vec(x, y));
let node = grid.grid[n];
if (node) {
for (let body of node) {
if (!bodies.has(body)) {
bodies.add(body);
}
}
}
}
}
},
/**
*
* @param {vec} start - Start of ray
* @param {vec} end - End of ray
* @param {Array} [bodies] - Array of bodies to test
* @return {Object} { collision: boolean, distance: Number, point: vec, body: RigidBody, verticeIndex: Number }
*/
raycast: function(start, end, bodies = []) {
let lineIntersects = Common.lineIntersects;
let minDist = Infinity;
let minPt = null;
let minBody = null;
let minVert = -1;
for (let i = 0; i < bodies.length; i++) {
let body = bodies[i];
let { vertices } = body;
let len = vertices.length;
for (let i = 0; i < len; i++) {
let cur = vertices[i];
let next = vertices[(i + 1) % len];
let intersection = lineIntersects(start, end, cur, next);
if (intersection) {
let dist = intersection.sub(start).length;
if (dist < minDist) {
minDist = dist;
minPt = intersection;
minBody = body;
minVert = i;
}
}
}
}
return {
collision: minPt !== null,
distance: minDist,
point: minPt,
body: minBody,
verticeIndex: minVert,
};
},
raycastSimple: function(start, end, bodies) { // raycast that only tells you if there is a collision; faster than full raycast; returns true/false
let lineIntersectsBody = Common.lineIntersectsBody;
for (let body of bodies) {
let intersection = lineIntersectsBody(start, end, body);
if (intersection) {
return true;
}
}
return false;
},
boundCollision: function(boundsA, boundsB) { // checks if 2 bounds { min: vec, max: vec } are intersecting, returns true/false
return (boundsA.max.x >= boundsB.min.x && boundsA.min.x <= boundsB.max.x &&
boundsA.max.y >= boundsB.min.y && boundsA.min.y <= boundsB.max.y);
},
pointInBounds: function(point, bounds) { // checks if a point { x: x, y: y } is within bounds { min: vec, max: vec }, returns true/false
return (point.x >= bounds.min.x && point.x <= bounds.max.x &&
point.y >= bounds.min.y && point.y <= bounds.max.y);
},
/**
* Deletes first instance of `value` from `array`
* @param {Array} array Array item is deleted from
* @param {*} value Value deleted from array
*/
arrayDelete(array, value) {
let index = array.indexOf(value);
if (index !== -1) {
array.splice(index, 1);
}
}
}
module.exports = Common;