/**
* @fileoverview Mathematical helper utilities.
*/
// -------------------------------------- CONSTANTS
/**
* Golden ratio.
* @constant {number}
*/
const PHI = (Math.sqrt(5) + 1) / 2;
/**
* Euler's constant.
* @constant {number}
*/
const E = Math.E;
// -------------------------------------- HASHING
/**
* Simple deterministic hash from an integer to a float in [0,1].
* @param {number} i
* @returns {number}
*/
function simpleIntToFloatHash(i) {
return fract(sin(i * 1097238.23492523 * 23479.23429237));
}
/**
* Hash a string to a float in [0,1].
* @param {string} inputString
* @returns {number}
*/
function stringToFloatHash(inputString) {
let hash = 0,
chr;
if (inputString.length === 0) return hash;
for (let i = 0; i < inputString.length; i++) {
chr = inputString.charCodeAt(i);
hash = (hash << 5) - hash + chr;
hash |= 0; // Convert to 32bit integer
}
return abs(hash / 2147483647);
}
/**
* Combine three integers into a single hash value.
* @param {number} a
* @param {number} b
* @param {number} c
* @returns {number}
*/
function hashThreeIntegers(a, b, c) {
const prime = 31; // A prime number to avoid common patterns
let hash = 17; // Initial value, can be any prime number
hash = hash * prime + a;
hash = hash * prime + b;
hash = hash * prime + c;
return hash;
}
// -------------------------------------- MAPPING
/**
* Returns the sign of a number as -1 or 1.
* @param {number} x
* @returns {number}
*/
function sign(x) {
return x >= 0 ? 1 : -1;
}
/**
* Normalised cosine mapping in range [0,1].
* @param {number} x
* @returns {number}
*/
function nmc(x) {
return -cos(x) * 0.5 + 0.5;
}
/**
* Sigmoid curve.
* @param {number} x
* @returns {number}
*/
function sigmoid(x) {
return 1 / (1 + exp(-x));
}
/**
* Hyperbolic tangent implemented via sigmoid.
* @param {number} x
* @returns {number}
*/
function tanh(x) {
return sigmoid(2 * x) * 2 - 1;
}
/**
* Standard gaussian function e^(-x^2).
* @param {number} x
* @returns {number}
*/
function gaussian(x) {
return exp(-pow(x, 2));
}
/**
* Absolute value gaussian for a sharper peak.
* @param {number} x
* @returns {number}
*/
function gaussianSharp(x) {
return exp(-abs(x));
}
/**
* Mix between gaussian and gaussianSharp.
* @param {number} x
* @returns {number}
*/
function gaussianAngular(x) {
return lerp(gaussian(x), gaussianSharp(x), 0.5);
}
/**
* Highly peaked gaussian used for noise wobbles.
* @param {number} x
* @returns {number}
*/
function gaussianWobble(x) {
return lerp(gaussian(x), gaussianSharp(x), 3);
}
/**
* Convert a parameter t in [0,1] into an integer range [0,n).
* @param {number} t
* @param {number} n
* @returns {number}
*/
function paramToIntSteps(t, n) {
return floor(t * n * (1 - 1e-5));
}
/**
* Wrap any angle to the range [0,TAU).
* @param {number} angle
* @returns {number}
*/
function simplifyAngle(angle) {
return ((angle % TAU) + TAU) % TAU;
}
/**
* Calculate signed difference between two angles.
* @param {number} angle
* @param {number} anchorAngle
* @returns {number}
*/
function signedAngleDiff(angle, anchorAngle) {
return PI - simplifyAngle(angle + PI - anchorAngle);
}
/**
* Constrain an angle around an anchor by a maximum deviation.
* @param {number} angle
* @param {number} anchorAngle
* @param {number} constraint
* @returns {number}
*/
function constrainAngle(angle, anchorAngle, constraint) {
// constrain the angle to be within a certain range of the anchorAngle
if (abs(signedAngleDiff(angle, anchorAngle)) <= constraint) {
return simplifyAngle(angle);
}
if (signedAngleDiff(angle, anchorAngle) > constraint) {
return simplifyAngle(anchorAngle - constraint);
}
// <= constraint
return simplifyAngle(anchorAngle + constraint);
}
// -------------------------------------- RANDOMNESS
/**
* Generate a 2D vector with normally distributed components using the
* Box–Muller method.
* @param {Vec2D} [mu=new Vec2D()] Mean vector
* @param {number} [sigma=1] Standard deviation
* @returns {Vec2D}
*/
function randomGaussianBoxMueller2(mu = new Vec2D(), sigma = 1) {
// outputs normally distributed 2d vector
// x and y are individually normally distributed
let u1 = random(1);
let u2 = random(1);
let r = sqrt(-2 * log(u1));
let th = TAU * u2;
return new Vec2D(r * sigma, th).toCartesian().add(mu);
}
// -------------------------------------- L.ALGEBRA & GEOMETRY
/**
* Linear interpolation between two vectors.
* @param {Vec2D|Vec3D} va
* @param {Vec2D|Vec3D} vb
* @param {number} t
* @returns {Vec2D|Vec3D}
*/
function mix(va, vb, t) {
return vb.sub(va).scale(t).add(va);
}
Vec2D.prototype.mix = function (v, t) {
return mix(this, v, t);
};
Vec3D.prototype.mix = function (v, t) {
return mix(this, v, t);
};
/**
* Midpoint between two vectors.
* @param {Vec2D|Vec3D} va
* @param {Vec2D|Vec3D} vb
* @returns {Vec2D|Vec3D}
*/
function midPoint(va, vb) {
return mix(va, vb, 0.5);
}
Vec2D.prototype.mix = function (v) {
return midPoint(this, v);
};
Vec3D.prototype.mix = function (v) {
return midPoint(this, v);
};
/**
* Check whether a vector lies within rectangular bounds.
* @param {Vec2D} v
* @param {number} x
* @param {number} y
* @param {number} w
* @param {number} h
* @param {number} [offs=0] Optional margin
* @returns {boolean}
*/
function isInBounds(v, x, y, w, h, offs = 0) {
return (
v.x >= x - offs &&
v.x < width + offs &&
v.y >= y - offs &&
v.y < height + offs
);
}
/**
* Check whether a vector lies within the main canvas.
* @param {Vec2D} v
* @param {number} [offs=0]
* @returns {boolean}
*/
function isInCanvas(v, offs = 0) {
return isInBounds(v, 0, 0, width, height, offs);
}
/**
* Test if the mouse is inside a rectangle.
* @param {number} x
* @param {number} y
* @param {number} w
* @param {number} h
* @param {number} [offs=0]
* @returns {boolean}
*/
function isMouseInside(x, y, w, h, offs = 0) {
return isInBounds(new Vec2D(mouseX, mouseY), x, y, w, h, offs);
}
/**
* Check whether a vector lies within the offscreen graphics buffer.
* @param {Vec2D} v
* @param {number} [offs=0]
* @returns {boolean}
*/
function inPg(v, offs = 0) {
return isInBounds(v, 0, 0, pg.width, pg.height, offs);
}
/**
* Helper sign used in {@link isPointInTriangle}.
*/
function triangleSign(p1, p2, p3) {
// see isPointInTriangle
return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y);
}
/**
* Check if a point lies inside the triangle defined by v1,v2,v3.
* @param {Vec2D} pt
* @param {Vec2D} v1
* @param {Vec2D} v2
* @param {Vec2D} v3
* @returns {boolean}
*/
function isPointInTriangle(pt, v1, v2, v3) {
const d1 = triangleSign(pt, v1, v2);
const d2 = triangleSign(pt, v2, v3);
const d3 = triangleSign(pt, v3, v1);
const has_neg = d1 < 0 || d2 < 0 || d3 < 0;
const has_pos = d1 > 0 || d2 > 0 || d3 > 0;
return !(has_neg && has_pos);
}
/**
* Signed angle from vector v to vector w.
* @param {Vec2D} v
* @param {Vec2D} w
* @returns {number}
*/
function signedAngleBetween(v, w) {
// angle measured from v
return atan2(v.x * w.y - v.y * w.x, v.x * w.x + v.y * w.y);
}
// -------------------------------------- STATISTICS
/**
* Convert an array of numbers to rounded percentages that sum to 100.
* @param {number[]} list
* @returns {number[]}
*/
function numsToRoundedPercentages(list) {
let listSum = list.reduce((acc, x) => acc + x, 0);
if (listSum <= 0) return;
let normalizedList = [...list];
let percentages = [];
let index = 0;
for (let num of normalizedList) {
let percent = (num / listSum) * 100;
let rounded = round(percent);
let error = percent - rounded;
percentages.push([index, rounded, error]);
index++;
}
while (true) {
let sum = percentages
.map(item => item[1])
.reduce((acc, x) => acc + x, 0);
if (sum == 100 || sum <= 0) break;
percentages.sort((a, b) => abs(b[2]) - abs(a[2]));
percentages[0][1] += sign(percentages[0][2]);
percentages[0][2] = 0;
}
percentages.sort((a, b) => a[0] - b[0]); // sort index
return percentages.map(item => item[1]);
}