angles
Version:
A function collection for working with angles
320 lines (272 loc) • 7.37 kB
JavaScript
'use strict';
var TAU = 2 * Math.PI;
var EPS = 1e-10;
// var DIRECTIONS = ["N", "E", "S", "W"];
var DIRECTIONS = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"];
// var DIRECTIONS = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"];
/**
* Mathematical modulo
*
* @param {number} x
* @param {number} m
* @returns {number}
*/
function mod(x, m) {
return (x % m + m) % m;
}
var Angles = {
'SCALE': 360,
/**
* Normalize an arbitrary angle to the interval [-180, 180)
*
* @param {number} n
* @returns {number}
*/
'normalizeHalf': function (n) {
var c = this['SCALE'];
var h = c / 2;
return mod(n + h, c) - h;
},
/**
* Normalize an arbitrary angle to the interval [0, 360)
*
* @param {number} n
* @returns {number}
*/
'normalize': function (n) {
var c = this['SCALE'];
return mod(n, c);
},
/**
* Gets the shortest direction to rotate to another angle
*
* @param {number} from
* @param {number} to
* @returns {number}
*/
'shortestDirection': function (from, to) {
var z = from - to;
// mod(-z, 360) < mod(z, 360) <=> mod(z + 180, 360) < 180 , for all z \ 180
if (from === to) {
return 0;
// if (mod(-z, 360) < mod(z, 360)) {
} else if (this['normalizeHalf'](z) < 0) {
return -1; // Left
} else {
return +1; // Right
}
},
/**
* Checks if an angle is between two other angles
*
* @param {number} n
* @param {number} a
* @param {number} b
* @returns {boolean}
*/
'between': function (n, a, b) { // Check if an angle n is between a and b
var c = this['SCALE'];
n = mod(n, c);
a = mod(a, c);
b = mod(b, c);
if (a < b)
return a <= n && n <= b;
// return 0 <= n && n <= b || a <= n && n < 360;
return a <= n || n <= b;
},
/**
* Calculates the angular difference between two angles
* @param {number} a
* @param {number} b
* @returns {number}
*/
'diff': function (a, b) {
return Math.abs(b - a) % this['SCALE'];
},
/**
* Calculate the minimal distance between two angles
*
* @param {number} a
* @param {number} b
* @returns {number}
*/
'distance': function (a, b) {
var m = this['SCALE'];
var h = m / 2;
// One-Liner:
//return Math.min(mod(a - b, m), mod(b - a, m));
var diff = this['normalizeHalf'](a - b);
if (diff > h)
diff = diff - m;
return Math.abs(diff);
},
/**
* Calculate radians from current angle
*
* @param {number} n
* @returns {number}
*/
'toRad': function (n) {
// https://en.wikipedia.org/wiki/Radian
return n / this['SCALE'] * TAU;
},
/**
* Calculate degrees from current angle
*
* @param {number} n
* @returns {number}
*/
'toDeg': function (n) {
// https://en.wikipedia.org/wiki/Degree_(angle)
return n / this['SCALE'] * 360;
},
/**
* Calculate gons from current angle
*
* @param {number} n
* @returns {number}
*/
'toGon': function (n) {
// https://en.wikipedia.org/wiki/Gradian
return n / this['SCALE'] * 400;
},
/**
* Given the sine and cosine of an angle, what is the original angle?
*
* @param {number} sin
* @param {number} cos
* @returns {number}
*/
'fromSinCos': function (sin, cos) {
var s = this['SCALE'];
var angle = (1 + Math.acos(cos) / TAU) * s;
if (sin < 0) {
angle = s - angle;
}
return mod(angle, s);
},
/**
* What is the angle of two points making a line
*
* @param {Array} p1
* @param {Array} p2
* @returns {number}
*/
'fromSlope': function (p1, p2) {
var s = this['SCALE'];
var angle = (TAU + Math.atan2(p2[1] - p1[1], p2[0] - p1[0])) % TAU;
return angle / TAU * s;
},
/**
* Returns the quadrant
*
* @param {number} x The point x-coordinate
* @param {number} y The point y-coordinate
* @param {number=} k The optional number of regions in the coordinate-system
* @param {number=} shift An optional angle to rotate the coordinate system
* @returns {number}
*/
'quadrant': function (x, y, k, shift) {
var s = this['SCALE'];
if (k === undefined)
k = 4; // How many regions? 4 = quadrant, 8 = octant, ...
if (shift === undefined)
shift = 0; // Rotate the coordinate system by shift° (positiv = counter-clockwise)
/* shift = PI / k, k = 4:
* I) 45-135
* II) 135-225
* III) 225-315
* IV) 315-360
*/
/* shift = 0, k = 4:
* I) 0-90
* II) 90-180
* III) 180-270
* IV) 270-360
*/
var phi = (Math.atan2(y, x) + TAU) / TAU;
if (Math.abs(phi * s % (s / k)) < EPS) {
return 0;
}
return 1 + mod(Math.floor(k * shift / s + k * phi), k);
},
/**
* Calculates the compass direction of the given angle
*
* @param {number} course
* @returns {string}
*/
'compass': function (course) {
// 0° = N
// 90° = E
// 180° = S
// 270° = W
var s = this['SCALE'];
var k = DIRECTIONS.length;
// floor((2ck + s) / (2s)) = round((c / s) * k)
var dir = Math.round(course / s * k);
return DIRECTIONS[mod(dir, k)];
},
/**
* Calculates the linear interpolation of two angles
*
* @param {number} a Angle one
* @param {number} b Angle two
* @param {number} p Percentage
* @param {number} dir Direction (either 1 [=CW] or -1 [=CCW])
* @returns {number}
*/
'lerp': function (a, b, p, dir) {
var s = this['SCALE'];
a = mod(a, s);
b = mod(b, s);
if (a === b)
return a;
// dir becomes an offset if we have to add a full revolution (=scale)
if (!dir)
dir = -s;
else if ((dir === 1) === (a < b))
dir *= s;
else
dir = 0;
return mod(a + p * (b - a - dir), s);
},
/**
* Calculates the average (mean) angle of an array of angles
*
* @param {Array<number>} angles Angle array
* @returns {number}
*/
'average': function (angles) {
var s = this['SCALE'];
// Basically treat each angle as a vector, add all the vecotrs up,
// and return the angle of the resultant vector.
var y = angles.map(a => Math.sin(a * TAU / s)).reduce((a, b) => a + b);
var x = angles.map(a => Math.cos(a * TAU / s)).reduce((a, b) => a + b);
// If the resultant vector is very short, this means the average angle is likely wrong or ambiguous.
// For instance, what if a users asks for the average of the angles [0, PI]?
// TODO: Warn user (or return undefined / null / NaN) when using opposite angles
// Could be as simple as:
//if (x * x + y * y < EPS * EPS) return NaN;
return Math.atan2(y, x) * s / TAU;
},
/**
* Determines if two angles are equal
*
* @param {number} angle1
* @param {number} angle2
* @returns {boolean}
*/
'equals': function (angle1, angle2) {
var m = this['SCALE'];
const normalizedAngle1 = mod(angle1, m);
const normalizedAngle2 = mod(angle2, m);
const diff1 = Math.abs(normalizedAngle1 - normalizedAngle2);
const diff2 = Math.abs(diff1 - 2 * Math.PI);
return diff1 < EPS || diff2 < EPS;
}
};
Object.defineProperty(Angles, "__esModule", { 'value': true });
Angles['default'] = Angles;
Angles['Angles'] = Angles;
module['exports'] = Angles;