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

@amplib/cosmos

v0.1.0

Published

Calculations about the relationship between the earth, moon, sun, planets, and more.

Readme

@amplib/cosmos

The state of the sky for a given time and place, normalised for driving sound.

import { generate } from "@amplib/cosmos";

const cosmos = generate({ latitude: 40.7128, longitude: -74.006 });

cosmos.moon.phase.unitRange; // 0.56 — position in the synodic cycle
cosmos.moon.phase.sin; // continuous across the new-moon wrap
cosmos.sun.daylightProgress.unitRange; // 0.24 — sunrise to sunset
cosmos.tides.potential.unitRange; // 0.72 — two peaks per lunar day
cosmos.seed.code; // "005FJZ96" — feed to @amplib/procedural-generation

Live demo

Design

It is a pure function. Given the same latitude, longitude, and UTC timestamp, generate() returns the same result on every machine, in every timezone, in any JavaScript runtime. Nothing in the library reads the host clock, the host timezone, or the host locale. This matters because the sibling package exists for "shared procedural experiences between disconnected devices" — two phones have to agree without talking to each other, and that is only possible if the input fully determines the output.

npm test checks this by running an 840-sample sweep under nine timezones, including half-hour and 45-minute offsets, and comparing hashes.

Positions come from an ephemeris. astronomy-engine (MIT, no dependencies) is accurate to roughly an arcminute for the Sun, Moon, and planets over 1700–2200. It is reached through exactly one file, src/ephemeris.ts, so the backend can be swapped without touching anything else.

Interpretation is ours. The sidereal-time and rotation-angle derivations, the tidal model, the normalisation contract, the timescale grouping, and the seed derivation all live in this package. That is where the value is — an ephemeris says where Jupiter is, but nothing about how a number should behave when it feeds an oscillator.

The value contract

Every numeric leaf carries its real-world value alongside pre-normalised forms:

{
  value: 0.5077,      // real value, never clamped
  unit: "degrees",
  min: 0.4885,        // the domain used to normalise
  max: 0.5683,
  unitRange: 0.24,    // always in [0, 1]
  bipolarRange: -0.52 // always in [-1, 1], always 2 * unitRange - 1
}

Three guarantees hold everywhere, and the test suite sweeps eight locations across six years asserting them:

  1. unitRange is in [0, 1].
  2. bipolarRange is in [-1, 1] and equals 2 * unitRange - 1.
  3. min/max state the domain, so you can re-derive or re-scale.

There is a fourth property the tests check that is a design goal rather than a hard guarantee: no value is pinned to a sliver of its range. A field normalised against the wrong domain is technically in contract and useless in practice. Each planet's distance, brightness, and apparent size are therefore normalised against that planet's own extremes rather than a solar-system-wide scale — otherwise Venus's near-circular orbit would occupy 0.03% of the range and read as a constant.

Cyclic values

Anything that wraps — an angle, a phase, a time of day — is a CyclicValue, which adds sin, cos, and period:

cosmos.moon.phase.unitRange; // jumps 1 → 0 at new moon
cosmos.moon.phase.sin; // continuous through it
cosmos.moon.phase.cos;

Use unitRange when you want the hard reset — triggering an event, indexing a table. Use sin/cos for anything continuous, because feeding the wrap point into a filter cutoff produces an audible click.

Events

Rise and set times are EventValue:

{ timestamp: 1785535912153, iso: "2026-07-31T09:51:52.153Z", secondsUntil: -12420 }

null is a real answer, not an error: inside the polar circles the Sun stays up or down for weeks, and the Moon skips a rise roughly once a month because its day is 24h50m. Branch on it rather than substituting zero. sun.dayLength reports 24 or 0 in those cases so you always have a usable number.

Timescales

The result tree is organised by body, but the useful question when mapping the sky onto music is how fast something moves. cosmos.timescales regroups the same value objects — by reference, nothing is copied — into four bands:

| Band | Period | Suits | | ------------ | ------------- | -------------------------------------- | | rotational | hours | rhythm, filter sweeps, stereo movement | | lunar | days to weeks | phrase length, register, density | | annual | months | key centre, mode, timbre | | epochal | years | long-form structure, tuning drift |

for (const signal of cosmos.timescales.rotational.signals) {
  signal.path; // "sun.hourAngle"
  signal.periodSeconds; // 86400
  signal.cyclic; // true — carries sin/cos
  signal.value.unitRange;
}

Tides

cosmos.tides is the equilibrium tide-generating potential — the real second-degree term, (3cos²θ − 1) / 2 scaled by GM/d³, summed over the Moon and the Sun.

Because it goes as cos², the Moon overhead and the Moon underfoot both raise a bulge: two high tides a lunar day, not one. And because the solar term adds to the lunar one only when they share an axis, spring and neap tides fall out without being modelled — tides.range traces the synodic month on its own. Musically that is the useful part: a semidiurnal pulse whose depth breathes over four weeks.

These are equilibrium tides on a hypothetical ocean over a rigid Earth. Real coastal tides are dominated by basin resonance and can lag this by hours. It is an honest astronomical driving force, not a tide table.

Seeds

cosmos.seed is derived from quantised inputs, not from the computed sky:

generate({
  latitude,
  longitude,
  timestamp,
  seedResolution: { seconds: 3600, degrees: 0.25 },
});

Two devices in the same position cell and time bucket agree with no coordination. The quantisation absorbs GPS jitter and clock drift.

Deriving the seed from the cosmic state instead is tempting and wrong. ECMAScript specifies Math.sin, Math.cos, and Math.pow as implementation-approximated — V8, JavaScriptCore, and SpiderMonkey each return results differing in the last bits. Every value here passes through dozens of those calls. Two engines would usually land in the same quantised bucket and agree, but near a boundary they would not, and the failure would be rare, silent, and unreproducible. Latitude, longitude, and a timestamp are exact doubles, and the hash is FNV-1a over Math.imul, which is exact everywhere.

The state still shapes the music. It just does not shape the seed.

Descriptions

generate() allocates no strings, which is what makes calling it every frame reasonable. Text is opt-in:

import { describe, describeLines } from "@amplib/cosmos";

describe(cosmos)["moon.phase"]; // "0.5616 (0.562)"
console.log(describeLines(cosmos)); // aligned and sorted, for a <pre> or a log

Performance

A full warm evaluation is about 0.24 ms; sun and moon only (skipPlanets: true) is about 0.07 ms. Both fit comfortably inside a 16 ms frame.

Rise/set and moon-quarter searches are iterative and cost roughly a millisecond each, so they are memoised per local solar day and per observer position rounded to 0.01°. A running clock hits the same cache entry all day. Continuous quantities are recomputed every call.

Accuracy

Verified against external references in test/run.ts — defining constants, published rise/set times, and eclipse instants, which are unambiguous syzygies:

| Check | Result | | ---------------------------------- | -------------------------------- | | GMST at J2000.0 | matches 18h41m50.548s to 1e-6 h | | Moon phase at six eclipse instants | within 0.5° of elongation | | Sunrise/sunset, three cities | within 2 minutes of published | | Equation of time extremes | within 1 minute | | Inner-planet elongation limits | never exceeded over 1500 samples | | Planet magnitudes | inside published ranges |

npm test              # accuracy suite + timezone determinism
npm run test:accuracy # accuracy only