npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

angles

v0.6.0

Published

Angle normalization, conversion, interpolation, circular mean, distance, and compass helpers for JavaScript

Readme

Angles.js

NPM Package MIT license

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.

Installation

npm install angles

ES modules:

import Angles, {
  type CompassDirection,
  type PointTuple,
  type RotationDirection,
  type ShortestDirection,
} from 'angles';

Angles.SCALE = 360;
console.log(Angles.normalize(-10)); // 350

CommonJS:

const Angles = require('angles');

Angles.SCALE = 360;
console.log(Angles.normalize(-10)); // 350

The package requires Node.js 20 or newer and includes CommonJS, ES module, browser, source-map, and TypeScript declaration files.

Browser

Browser global with jsDelivr

The minified browser build creates the global Angles class:

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/angles.min.js"></script>
<script>
  Angles.SCALE = 360;
  console.log(Angles.normalize(-725)); // 355
  console.log(Angles.compass(225));    // "SW"
</script>

Browser global with unpkg

<script src="https://unpkg.com/[email protected]/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.

Browser ES module

<script type="module">
  import Angles from 'https://cdn.jsdelivr.net/npm/[email protected]/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.

Angular scale

Angles.SCALE

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;         // gradians

SCALE 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.

Normalization and comparison

Angles.normalize(angle)

Normalizes angle to the half-open interval [0, SCALE).

Angles.SCALE = 360;
Angles.normalize(-55); // 305
Angles.normalize(415); // 55

Angles.normalizeHalf(angle)

Normalizes angle to [-SCALE / 2, SCALE / 2). The positive half-turn is represented by its negative equivalent.

Angles.normalizeHalf(190); // -170
Angles.normalizeHalf(180); // -180

Angles.distance(a, b)

Returns the minimal unsigned angular distance between a and b.

Angles.distance(358, 1); // 3

Angles.diff(a, b)

Returns the minimal unsigned angular difference. It is equivalent to distance(a, b) and retained as a concise compatibility alias.

Angles.diff(1, 359); // 2

Angles.equals(a, b)

Returns 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); // false

Angles.between(angle, start, end)

Checks 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); // false

Direction and interpolation

Angles.shortestDirection(from, to)

Returns the shortest rotation direction:

  • -1 for clockwise
  • 1 for counter-clockwise
  • 0 when 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);  // 0

Exactly opposite angles use the normalized negative half-turn and therefore return 1.

Angles.lerp(a, b, amount, direction?)

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);  // 240

amount is not clamped: values outside [0, 1] extrapolate. Equal endpoints return the normalized start angle.

Unit conversion

Conversions interpret their input in the current SCALE; they do not modify the input or change SCALE.

Angles.toRad(angle)

Converts from the current units to radians.

Angles.SCALE = 360;
Angles.toRad(90); // Math.PI / 2

Angles.toDeg(angle)

Converts from the current units to degrees.

Angles.SCALE = 2 * Math.PI;
Angles.toDeg(Math.PI); // 180

Angles.toGon(angle)

Converts from the current units to gradians, where one full turn is 400.

Angles.SCALE = 360;
Angles.toGon(90); // 100

Constructing angles

Angles.fromSinCos(sine, cosine)

Reconstructs 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.3

Passing both values as zero follows Math.atan2(0, 0) and returns zero.

Angles.fromSlope(start, end)

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]); // NaN

Regions and compass labels

Angles.quadrant(x, y, regions = 4, shift = 0)

Divides 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 boundary

The origin has no geometric direction, but follows Math.atan2(0, 0) and is reported as boundary 0.

Angles.compass(course)

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.

Circular mean

Angles.average(angles)

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]); // 90

An empty array or a directionally balanced set such as [0, 180] returns NaN, because no unique mean direction exists.

TypeScript types

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;

Development

Node.js 20 or newer is required.

npm install
npm test

npm 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 build

Copyright and licensing

Copyright (c) 2026, Robert Eisele Licensed under the MIT license.