Angles.js is a small, dependency-free TypeScript library for normalized angles, wrapped ranges, shortest-path interpolation, circular means, unit conversion, compass labels, and directions derived from points.
Angles are expressed in a configurable number of units per full turn. The
default is 360, but 2 * Math.PI supports radians and 400 supports gradians.
All operations are exposed as static methods on the Angles class.
npm install anglesES modules:
import Angles, {
type CompassDirection,
type PointTuple,
type RotationDirection,
type ShortestDirection,
} from 'angles';
Angles.SCALE = 360;
console.log(Angles.normalize(-10)); // 350CommonJS:
const Angles = require('angles');
Angles.SCALE = 360;
console.log(Angles.normalize(-10)); // 350The package requires Node.js 20 or newer and includes CommonJS, ES module, browser, source-map, and TypeScript declaration files.
The minified browser build creates the global Angles class:
<script src="https://cdn.jsdelivr.net/npm/angles@0.6.0/dist/angles.min.js"></script>
<script>
Angles.SCALE = 360;
console.log(Angles.normalize(-725)); // 355
console.log(Angles.compass(225)); // "SW"
</script><script src="https://unpkg.com/angles@0.6.0/dist/angles.min.js"></script>
<script>
const midpoint = Angles.lerp(350, 10, 0.5, -1);
console.log(midpoint); // 0
</script>Pinning the version in CDN URLs prevents an update from changing behavior without a corresponding application deployment.
<script type="module">
import Angles from 'https://cdn.jsdelivr.net/npm/angles@0.6.0/dist/angles.min.mjs';
Angles.SCALE = 2 * Math.PI;
console.log(Angles.normalize(-Math.PI / 2)); // 3π / 2
</script>Bundlers can use the dedicated browser subpath:
import Angles from 'angles/browser';The global build also supports AMD loaders.
The number of units in one full turn. It defaults to 360 and must be a finite
positive number.
Angles.SCALE = 360; // degrees
Angles.SCALE = 2 * Math.PI; // radians
Angles.SCALE = 400; // gradiansSCALE is shared mutable state. Do not switch it between unit systems while
independent operations are running concurrently. Convert values explicitly
when an application uses multiple units at the same time.
Assigning zero, a negative value, NaN, or infinity throws a RangeError.
Normalizes angle to the half-open interval [0, SCALE).
Angles.SCALE = 360;
Angles.normalize(-55); // 305
Angles.normalize(415); // 55Normalizes angle to [-SCALE / 2, SCALE / 2). The positive half-turn is
represented by its negative equivalent.
Angles.normalizeHalf(190); // -170
Angles.normalizeHalf(180); // -180Returns the minimal unsigned angular distance between a and b.
Angles.distance(358, 1); // 3Returns the minimal unsigned angular difference. It is equivalent to
distance(a, b) and retained as a concise compatibility alias.
Angles.diff(1, 359); // 2Returns whether two angles differ by less than the internal angular tolerance, including across the wrap boundary.
Angles.equals(0, 360); // true
Angles.equals(90, 270); // falseChecks whether angle lies in the inclusive clockwise interval from start to
end. Inputs need not be normalized, and wrapped intervals are supported.
Angles.between(5, 350, 10); // true
Angles.between(355, 350, 10); // true
Angles.between(180, 350, 10); // falseReturns the shortest rotation direction:
-1for clockwise1for counter-clockwise0when both angles are equal within tolerance
For systems where angles increase counter-clockwise, the visual interpretation of these signs is naturally reversed.
Angles.shortestDirection(350, 10); // -1
Angles.shortestDirection(10, 350); // 1
Angles.shortestDirection(10, 10); // 0Exactly opposite angles use the normalized negative half-turn and therefore
return 1.
Interpolates from a to b. Without direction, the normalized shortest path
is used. Pass -1 to force clockwise movement or 1 to force
counter-clockwise movement.
Angles.lerp(350, 10, 0.5); // 0
Angles.lerp(300, 60, 0.25, -1); // 330
Angles.lerp(300, 60, 0.25, 1); // 240amount is not clamped: values outside [0, 1] extrapolate. Equal endpoints
return the normalized start angle.
Conversions interpret their input in the current SCALE; they do not modify
the input or change SCALE.
Converts from the current units to radians.
Angles.SCALE = 360;
Angles.toRad(90); // Math.PI / 2Converts from the current units to degrees.
Angles.SCALE = 2 * Math.PI;
Angles.toDeg(Math.PI); // 180Converts from the current units to gradians, where one full turn is 400.
Angles.SCALE = 360;
Angles.toGon(90); // 100Reconstructs an angle from its sine and cosine using Math.atan2() and returns
it normalized to [0, SCALE).
Angles.SCALE = 2 * Math.PI;
Angles.fromSinCos(Math.sin(-0.3), Math.cos(-0.3)); // 2π - 0.3Passing both values as zero follows Math.atan2(0, 0) and returns zero.
Returns the direction from start to end. Points are readonly tuples in
[x, y] order. Equal points have no direction and return NaN.
Angles.SCALE = 360;
Angles.fromSlope([0, 0], [0, 1]); // 90
Angles.fromSlope([2, 3], [2, 3]); // NaNDivides the full turn into equal regions and returns a one-based region index.
Points exactly on a region boundary return 0. Set regions to 4 for
quadrants or 8 for octants. shift rotates the region boundaries in current
angle units and may be positive or negative.
Angles.SCALE = 360;
Angles.quadrant(1, 1); // 1
Angles.quadrant(-1, 1); // 2
Angles.quadrant(1, 0); // 0, on a boundary
Angles.quadrant(1, 0, 4, -45); // 4
Angles.quadrant(1, 1, 8); // 0, on an octant boundaryThe origin has no geometric direction, but follows Math.atan2(0, 0) and is
reported as boundary 0.
Returns the nearest eight-point compass label. Compass courses use north at zero and increase through east, south, and west.
Angles.SCALE = 360;
Angles.compass(0); // "N"
Angles.compass(45); // "NE"
Angles.compass(225); // "SW"
Angles.compass(-45); // "NW"The result is one of "N", "NE", "E", "SE", "S", "SW", "W",
or "NW". Non-finite input returns undefined.
Returns the circular mean of an array or readonly array of angles. Unlike an ordinary arithmetic mean, values on either side of the wrap boundary remain close to each other.
Angles.average([350, 10]); // 0
Angles.average([80, 90, 100]); // 90An empty array or a directionally balanced set such as [0, 180] returns
NaN, because no unique mean direction exists.
The package exports the following types:
type CompassDirection = 'N' | 'NE' | 'E' | 'SE' | 'S' | 'SW' | 'W' | 'NW';
type RotationDirection = -1 | 1;
type ShortestDirection = RotationDirection | 0;
type PointTuple = readonly [x: number, y: number];The declaration files are generated from the TypeScript source for both ESM and CommonJS consumers.
With CommonJS TypeScript syntax, the same types are available on the merged class namespace:
import Angles = require('angles');
const point: Angles.PointTuple = [0, 1];
const direction: Angles.RotationDirection = -1;Node.js 20 or newer is required.
npm install
npm testnpm test performs strict type checking, builds every distribution, validates
ESM and CommonJS type consumers, and runs the runtime and browser-format tests.
Build without running the tests:
npm run buildCopyright (c) 2026, Robert Eisele Licensed under the MIT license.