skymaths
v0.1.1
Published
Accurate positional astronomy with no dependencies: Sun, Moon, planets, precession, meteor showers, and a stereographic sky projection with real atmospheric extinction.
Maintainers
Readme
skymaths
Accurate positional astronomy in JavaScript, with no dependencies and no platform.
Where the Sun, Moon and planets are; where the stars are once precession is applied; how to get any of it onto a screen; and how much the atmosphere dims it on the way. Runs in Node, in a browser, and in GJS — it imports nothing, so there is nothing to be incompatible with.
npm install skymathsimport {bodies, basis, vec, projectPoints, scale} from 'skymaths';
const now = Math.floor(Date.now() / 1000);
const {moon, planets} = bodies(now);
console.log(`Moon is ${(moon.illum * 100).toFixed(0)}% lit`);
// Where does Jupiter land on a 1920-wide view, 150° across, looking south?
const R = basis(now, {latitude: 51.48, longitude: 0, skyAzimuth: 180, skyAltitude: 42, skyFov: 150});
const jupiter = planets.find(p => p.name === 'Jupiter');
const pts = [vec(jupiter.ra, jupiter.dec)];
projectPoints(pts, R.ofDate, 960, 540, scale(1920, 150), true, true);
console.log(pts[0].behind ? 'below/behind you' : `${pts[0].x | 0}, ${pts[0].y | 0}`);What it gives you
ephemeris — Sun and Moon (phase, illuminated fraction, angular diameter, elongation),
the seven planets (RA/Dec, magnitude with the phase-angle term, apparent size, illuminated
fraction, Saturn's ring opening, Jupiter's Galilean moons), precession matrices, obliquity, and
a meteor-shower table whose peaks are stored in solar longitude — so the Perseids arrive in
August because the Earth gets there, not because a date is hard-coded.
projection — a stereographic sky projection with three coordinate frames, plus
Kasten–Young airmass, real extinction and scintillation.
The three frames, which is the load-bearing idea
basis() returns three row-sets, and using the wrong one is the bug this design exists to
prevent:
| Row-set | For | Because |
|---|---|---|
| j2000 | star catalogues | precession is baked into the rows, so it is never applied per star |
| ofDate | Sun, Moon, planets | the ephemeris already returns these of date |
| enu | horizon, compass, anything at an azimuth | it must stand still while the sky turns past it |
Mixing the first two puts the Moon about a third of its own width off — small enough to look fine and be wrong, which is the worst kind of bug.
Accuracy, honestly
| | Error | Validated against | |---|---|---| | Sun | ~0.01′ | Meeus example 25.a — 0.03″ | | Moon | ~10″ | Meeus example 47.a — 0.00″ | | Planets | ~1″ | Meeus example 33.a (Venus) — 0.48″ | | Nutation | ~0.5″ | Meeus example 22.a — 0.075″ | | Aberration | ~0.13″ | two independent forms agree | | Precession | exact to IAU 1976 | — |
Every figure is checked against Meeus's own published worked examples rather than against another part of this library — agreeing with the author of the method is a much stronger claim than two of your own components agreeing with each other.
This was a naked-eye library until it was used for sight reduction, where one arcminute of angle is one nautical mile of position and the distinctions above stop being academic. Three things were wrong, and none of them was visible at 2 arcmin/px:
- The planets carried the whole of precession since J2000. The JPL elements are referred to the J2000 ecliptic while the Sun and Moon come out already of date, so the planets were handed an of-date obliquity as though they were of-date themselves. 50″/year — 22 arcminutes by 2026, growing forever. It was never an orbital-elements problem.
- The Moon was on an abridged term list, 0.1° — a fifth of its own disc.
- ΔT was absent entirely.
julianDate()gives UT; every formula here wants TT. They differ by ~69 seconds, which is 38″ of lunar motion.
Two Julian Dates, and they are not interchangeable
bodies() returns both, named, because the caller must choose deliberately:
const b = bodies(unixSeconds);
b.jd // UT — the rotation of the Earth: sidereal time, hour angles, azimuth
b.jdTT // TT — the ephemeris. Every position was computed at this.
b.deltaT // seconds between themUsing one for both is a silent systematic error that drifts with the season.
Still limited
Planets rest on JPL approximate elements (valid 1800–2050), not VSOP87 — accurate now because
the frame, light time and aberration are handled, but that is the ceiling. Star proper motion is
not applied: properMotion() exists, a catalogue carrying the columns does not, and over 26
years the fastest navigational stars have moved ~1.6′. Aberration is opt-in in the projection
({aberration: true}) — 20.5″ is 0.34 nautical miles to a navigator and 0.17 pixels to a sky
chart, so paying for it every frame would be the same unmeasured-negligibility mistake reversed.
Where it is deliberately exact: precession, sidereal time (15.0411°/hour — the sidereal day, not the solar one), the phase-angle magnitude terms, and airmass.
Tests
npm test40 checks, and every one is an invariant rather than a number copied from an almanac: the June solstice declination must equal the obliquity; a full moon must be opposite the Sun; Venus's greatest elongation must fall between 45° and 47°; Perseid ZHR must peak at its tabulated value and fall away either side; Polaris must sit at the observer's latitude. They need no network and no reference data, so they cannot rot.
Provenance
Extracted from TerraFirma, a GNOME Shell extension that draws the real sky over the desktop wallpaper, where this code ran for months before being lifted out. It contains no catalogue data — only maths. If you need a star catalogue, d3-celestial is a good source and is what TerraFirma bakes its own from.
MIT licensed.
