Skip to content

Accuracy, and how it is checked

Positional astronomy is easy to get subtly, confidently wrong. Every formula here produces plausible-looking output when it is broken: a star chart that is mirrored still looks like a star chart, and a declination that is off by √2 still looks like a declination.

So nothing in this app is validated against itself. Both pieces of real mathematics are checked against published values from the people who define them, and those checks run in npm test.

Magnetic declination — NOAA's own test vectors

site/src/geomag.js is a degree-12 spherical harmonic synthesis of the World Magnetic Model 2025, valid through 2030.

NOAA ships WMM2025_TestValues.txt inside WMM2025COF.zip — 100 points with expected declination, inclination and field components. That file is committed to test/fixtures/ and every one of those vectors is checked:

QuantityAgreement
Declinationbetter than 0.01°
Inclinationbetter than 0.01°
X, Y, Z, H, Fbetter than 1 nT

Three bugs those vectors caught

All three were found from the shape of the error rather than by re-reading the algorithm, which is the argument for having reference vectors at all.

  1. Schmidt normalisation, √2. The quasi-normalisation carries a √2 for m ≥ 1, and it enters the sectoral chain exactly once — at P(1,1) = sin θ. Seeding it as √½·sin θ leaves every m ≥ 1 term low by √2. The signature was unmistakable: the east component alone was wrong, by exactly 1.414.

  2. B_φ sign. It is (1/sin θ) Σ m[g·sin(mλ) − h·cos(mλ)]P, and it had been negated.

  3. Geodetic vs geocentric latitude. psi is geodetic minus geocentric. With the sign reversed, X and Z carried a latitude-shaped error — zero at the equator and at the poles, largest near 45° — while Y stayed perfect. That pattern is the rotation and nothing else.

Polaris — proper motion and precession

polarisReticle() applies proper motion and IAU-1976 precession to a Hipparcos J2000 position. Sidereal time is anchored to the textbook GMST at J2000 (18h 41m 50.55s), which is tested directly.

The Bright Star Catalog we ship for the chart rounds positions to 0.1 s of RA and 1″ of declination. That is fine for drawing dots and not fine for alignment, so the alignment path uses the high-precision constant instead of the catalogue row. Two different jobs, two different sources.

The published iOptron vector, and an unexplained residual

iOptron's SkyTracker Pro manual (#3322, section 3) publishes a worked example: Boston, 2016-08-10 17:50:18 EDT, N42°30′28″ W71°08′49″ → 00hr 18.4m, radius 40.2′.

ComputedPublishedDelta
Radius39.9′40.2′0.3′
Dial position00h 13.7m00h 18.4m≈2.4°

2.4° of position angle is about 1.6′ of alignment error at Polaris' radius — acceptable for a camera lens on a tracker, not something to be smug about.

This residual is not explained. The two candidates:

  • The manual's figure is read off a screenshot in a PDF, and 2.4° is 9.5 minutes of clock. A screenshot taken a few minutes from the timestamp printed beside it would account for all of it.
  • iOptron may apply an atmospheric refraction correction we do not.

The test bounds are deliberately loose (3°) and say why in a comment. Cross-check against Stellarium before tightening them or before trusting the dial position to better than a few arcminutes.

Reference frames — the bug an audit found

Two Sun calculations, written by different methods in different modules, disagreed by 0.3676°. General precession in longitude from J2000 to the date was 0.3733°. The methods agreed to 0.006°; the frames did not.

Sidereal time is of date. The star catalogue, the planetary elements and the galactic frame are all J2000. Placing one against the other put every star, every planet and the whole Milky Way off from where the sky had them in 2026 — 22 arcminutes near the equator and the ecliptic, where the planets and the Moon live, and about 9′ near the pole, where the pole itself moves by the smaller precession constant — growing every year. The Moon, whose series is of date, sat where it should. Polaris was on the dial at its true 37.5′ from the pole and in the sky view at its J2000 44.2′.

(The first draft of the regression test expected 22′ at the pole, and failed. The pole moves by θ ≈ 20″/yr; a star on the equator by the full 50″/yr. The test was corrected, not the code — the sixth time in this project a failing test has been the test.)

Nothing on screen looked wrong. Twenty-two arcminutes is three pixels at a 65° field, and every star was wrong by the same amount in the same direction. That is exactly the kind of error that survives: consistent, small, and invisible until something in a different frame — the Moon — is drawn beside it.

The fix is one rotation. precessionMatrix() builds the IAU-1976 precession as a 3×3 once per tick, and every J2000 object goes through it where it meets sidereal time: the stars, the planets, the Milky Way, and the circumpolar chart. The Moon is flagged frame: 'date' and left alone — precessing it with the rest would move it 22′ the other way, the mirror of the bug. The matrix is pinned against the validated precessFromJ2000() to a nano-degree, and a test now holds the two Suns together in one frame.

The alignment numbers never had this problem — poleStarReticle() precessed Polaris from the start — which is why it went unnoticed for as long as it did.

Two traps that produce correct-looking output

Both are locked down by tests, because neither is visible by inspection.

The dial is 30° per hour, not 15°

The iOptron AccuAlign reticle is a 12-hour dial spanning a full circle. One dial hour is therefore 30°, while one hour of hour-angle is 15°. Mapping hour-angle hours straight onto dial hours is a silent factor-of-two error that still lands Polaris somewhere plausible on the reticle.

This is also why iOptron tell users of Takahashi's 24-hour reticle to halve their reading: both dials span the same circle with different numbering.

test('dial hours are 30 degrees each, not 15') advances time by two sidereal hours and asserts the dial moves exactly one hour.

The chart is drawn facing north, so west is on the left

Up is the zenith; down is the horizon. A star at hour angle 0 is at upper culmination — directly above the pole — so it must land at the top. As its hour angle grows it moves west, which is to your left when you are facing north rather than reading a map.

The first version of this was 180° out. It was not caught by looking at it — a rotated star chart is still a perfectly convincing star chart. It was caught by checking against the real sky: on a September evening at 42°N, Cassiopeia is high in the northeast and the Big Dipper low in the northwest, and the chart showed precisely the reverse.

test('chart orientation: up is the zenith, west is on the left') now asserts all four quadrants, so both a rotation and a mirroring fail.

The southern hemisphere

The south is not the north with a sign flipped, and three things differ.

There is no southern Polaris. Sigma Octantis — Polaris Australis — is magnitude 5.47, roughly 25x fainter than Polaris at 2.02, and it sits about 63' from the pole rather than 38'. It is below naked-eye visibility except under dark skies and is hard work even in a polar scope. The app says so rather than offering it as an equivalent, and points at the Southern Cross instead.

That faintness caused a bug worth recording: the sky chart's magnitude limit was 5.2, so shipping it unchanged southward would have filtered the south pole star out of its own chart. The southern limit is 5.6; the northern one stays at 5.2 because it is cleaner without the extra stars.

The dial runs the other way. Looking north the sky turns anticlockwise about the pole; looking south it turns clockwise. The polar scope inverts both identically, so the inversion cancels and the dial simply counts in the opposite direction. This is not externally validated — iOptron publish no southern worked example the way they do for Polaris, so it rests on that reasoning. The test pins the claim actually being made: that the two dials travel in opposite directions by the same amount. It deliberately does not assert that the two readings mirror each other at a given instant, because they cannot — Polaris and Sigma Octantis have completely unrelated right ascensions (about 2.9h and 21.1h).

The chart's handedness flips. Facing north, east is on your right; turn around to face south and east and west swap. Both charts still put upper culmination at the top, so only the x axis changes sign.

The check that makes the southern chart trustworthy

The published rule is that the Southern Cross's long axis, Gacrux through Acrux, extended about 4.5 times its own length, lands on the south celestial pole. Nothing in the code knows that number — it is a fact about the sky, not a constant in the projection.

Measured off the projection, the axis is 0.12 units and Acrux sits 0.537 from the pole: a ratio of 4.5. If the projection were wrong in scale, orientation or handedness, that would not come out, so it is pinned as a test. Sigma Octantis independently lands 1.05 deg from the chart's centre, against its true 1.04 deg.

The app also draws the Pointers' perpendicular bisector — a line at right angles from the midpoint of Alpha and Beta Centauri. Where it crosses the Cross's axis is the pole. Two independent lines meeting on the same empty patch of sky is far more convincing than one line running into nothing, which is all the Cross gives you alone. The Pointers also distinguish the real Cross from the False Cross, which has no such pair beside it.

What altitude is worth

Almost nothing, and the app says so rather than implying otherwise.

Measured across five sites, moving from sea level to 3000 m shifts magnetic declination by under 0.01°. A good polar alignment is about 0.1°. So altitude is an order of magnitude below the threshold that matters, and leaving it at sea level costs nothing.

The lookup exists because nobody should have to go and find their own elevation and type it in — not because it makes the answer better.

Solar position

Low-precision Astronomical Almanac, good to about 0.01° through 2050 — far better than is needed to find north from a shadow.

Solar noon is found by iterating the Sun's hour angle to zero, which reuses the sidereal-time code the rest of the suite already covers rather than introducing a second path. Tested at four sites including the southern hemisphere and the equator; the Sun lands on the meridian to within 0.2° at each.

Which way the noon shadow falls is read off the computed azimuth, not assumed from hemisphere: inside the tropics the Sun passes north of the zenith for part of the year and the shadow flips with it.

The Moon

The Moon matters here more than it looks: it is the brightest thing that will ruin a deep-sky exposure, so is it up, and how full is it decides whether tonight is worth setting up for at all.

Computed, not fetched. Unlike the ISS, the Moon's motion is known centuries ahead, so this needs no network and works with the radio off — which is the rest of the app's promise.

It is the truncated lunar theory: the largest periodic terms out of the hundreds in the full series, including evection and variation, which are the two corrections that dominate what is left and without which the error triples. Good to roughly a quarter of a degree — about half the Moon's own width. Ample for "look south-east, forty degrees up"; useless for predicting an occultation, and the app does not claim otherwise.

The illuminated fraction is far better than the position, because it depends on the Sun–Moon angle rather than on either body's exact place.

How it is checked

The reference lunation — the new Moon of 2000 January 6 at 18:14 UT, the epoch the lunation tables are quoted from — plus invariants that need no quoted date at all:

  • successive new Moons found by search are 29.53 days apart
  • the phase repeats on the synodic month and explicitly not on the sidereal one; the position does the opposite
  • ecliptic latitude peaks near 5.2°, the orbit's inclination
  • distance stays between perigee and apogee
  • declination never exceeds about 28.7°, obliquity plus inclination

Four dates in the first version of that test were written from memory and labelled "published". One was wrong by a fortnight and the test caught it. They are gone: a reference value that cannot be vouched for is worse than no reference value, because it looks like corroboration.

The drawn disc uses an elliptical terminator, not a straight edge — you are looking at a sphere, so the boundary between lit and unlit is a circle seen at an angle, and a straight one is a shape nobody has ever seen in the sky.

Moonrise and moonset

The card gives the times the Moon arrives and leaves for the day being shown — which is the planned night, not today, whenever a date is set, because every other number on the page already is.

The altitude the Moon rises at is neither zero nor the Sun's, and that is the part that is easy to get wrong. A star rises when its centre is at −0.5667°, where refraction lifts it over the horizon a little early. The Sun rises at −0.8333°, adding its own half-degree radius. The Moon goes the other way: it is close enough that parallax dominates, and seen from the surface it sits about 0.95° lower than the geocentric position the app computes — more than refraction and semidiameter together lift it. Meeus gives h₀ = 0.7275·π − 0.5667° ≈ +0.125° (π is the Moon's horizontal parallax), so the geocentric centre is slightly above the horizon at the moment the real upper limb touches it. Borrowing the Sun's −0.8333°, or using zero, puts every time out by minutes in the same direction. The crossing is found by search over the day and bisected to the second; the times are printed in the order they happen (within one calendar day the Moon usually sets before it rises, having come up the previous afternoon), and each carries ± a few minutes — the position is good to about a quarter of a degree, and near the horizon the Moon's slow motion turns that into a larger error in time than in place.

Either time can be genuinely absent — the Moon rises about fifty minutes later each day, so roughly once a month a calendar day has no moonrise at all — and inside the polar circles it can stay up or down for days. The app says which, rather than inventing a time. Every instant is an absolute UTC moment and only the display is local, so one computation reads correctly in any time zone.

The planets

Computed, not fetched — like the Moon and unlike the ISS.

The method. JPL's approximate Keplerian elements: each orbit as six numbers plus six rates of change per century. Propagate the elements to the date, solve Kepler's equation for the position in the orbit, rotate into the ecliptic, subtract the Earth's own position, and what is left is a geocentric direction.

Accuracy, stated honestly. The element set is published as good from 1800 to 2050, and over that span it is worth a few arcminutes for the inner planets and better than an arcminute for the outer ones — a small fraction of the Moon's width, and far finer than anyone can point a phone. It is not good enough to predict an occultation or a transit time, and it does not pretend to be: there is no perturbation theory here at all, only ellipses.

Deliberately left out: light-time correction (up to about 30 minutes for Saturn, which moves it by well under an arcminute), aberration, and nutation. All are smaller than the element error they would be correcting inside.

How it is checked. The strongest available test is an event anyone can look up: the great conjunction of 2020 December 21, the closest Jupiter–Saturn pairing since 1623, reported everywhere at the time as about six arcminutes — a fifth of the Moon's width. The test asserts a separation under 0.2° on the day, and a separation over 0.5° a week earlier, so it cannot be passed by a projection that happens to pile everything into one spot. It pins both planets at once and a wrong answer is not close.

The rest are invariants that need no quoted figure at all, which is the point — nothing in the code knows any of these numbers:

  • Every planet stays between its own perihelion and aphelion. The strongest test of the Kepler solver there is.
  • Each goes round in its own sidereal period — Mercury 88 days through Saturn 10,759 — and the longitude comes back to where it started. "No change" is zero, not half a turn: the same trap the Moon's sidereal-month test fell into.
  • Mercury never gets more than about 28° from the Sun, Venus about 47°. These limits are what make them morning and evening objects, they fall straight out of the geometry, and getting them right validates the geocentric subtraction.
  • A superior planet reaches opposition and is nearest there. Both halves have to hold together.
  • Venus outshines every other planet, and Neptune is never naked-eye.
  • Mars's phase angle peaks near 47°, Jupiter's near 12° — the Earth is too close to the Sun for an outer planet to show much of a phase.

One test failed for an honest reason worth recording: Neptune's aphelion came out 0.12 au short. The sampling window was 98 years and Neptune's orbit is 165, so it never reached aphelion inside it. The test was wrong, not the ephemeris. The window is now 1880–2050 — the span the elements are published for, and just longer than one Neptune year — with a 62-day step that shares no factor with any period here.

Magnitudes use the standard phase-angle polynomials. Saturn's is the weak one: its rings contribute up to about 0.9 magnitudes depending on how open they are to us, and that tilt is not modelled, so Saturn can read up to a magnitude too faint. It decides how big a dot gets drawn and nothing else.

Above the horizon is not the same as findable. Anything within about 12° of the Sun is in the glare whatever its magnitude says, and the summary line calls that out rather than offering it as a target.

The Moon in the sky view

Two things had to be right for the Moon to be drawn where it is rather than merely near it.

The bright limb faces the Sun. A crescent does not sit with its horns pointing wherever you like — the lit edge always faces the Sun, and drawing it otherwise produces a picture that is wrong in a way people notice instantly without being able to say why. Low in the west after sunset the crescent leans one way; the same phase in the east before dawn leans the other. The position angle of the bright limb is computed from the Sun's and the Moon's equatorial positions.

Which way is "north" on screen is measured, not reasoned about. A position angle is meaningless without knowing where north and east run in the picture, and that changes with every turn of the view. Rather than derive it — working out which way east runs in a projection of the sky seen from inside, not from outside like a map, is a handedness argument that is very easy to get backwards — two neighbours a quarter-degree away are projected alongside the Moon itself and the directions read off. A mirrored crescent is exactly the kind of error that looks fine until someone who knows the sky glances at it.

Its size is exaggerated and its position is not. The real Moon is about half a degree across, which at a 65° field is six pixels — too small to read a phase from at all. It is drawn at a floor of about a twentieth of the view height, the way a chart exaggerates a symbol it needs you to recognise. The caption says so.

The Milky Way — a photograph, not a formula

Until 2026-09-22 the band was two terms of arithmetic: a cosine toward the galactic centre times a Gaussian across the plane, sampled on a 6°×3° grid and drawn as 780 soft blobs. A smooth, symmetric glow. It could not show the Great Rift — the dark dust lane down the middle that is the most recognisable thing about the naked-eye Milky Way — because a symmetric Gaussian cannot have a dark middle. It had no lopsided bulge, no Cygnus or Scutum clouds, and no Magellanic Clouds at all, in a hemisphere the app otherwise supports. And because the blobs were sized to the canvas rather than to the sky, zooming in never resolved more; at the tightest field it fell apart into separate spots.

It is now a photograph: ESO/S. Brunier's all-sky panorama, 6000×3000 with the galactic plane horizontal, reduced to a 4096×2048 greyscale strip of the whole sky — 59 KB, against 352 KB of star catalogue — and projected onto the view through the same basis and focal length as every star.

What had to be true for that to be honest

Its stars had to come out. It is a photograph, so it carried tens of thousands of point stars, and the app draws its own from the Bright Star Catalog at computed positions. Two sets, slightly offset, would undermine the one thing the sky view promises. The build script removes point sources by difference — blur for the diffuse component, replace only what stands far above it — after a first attempt with a morphological opening printed its own square structuring element across the band as a cellular texture. A handful of one-degree halos survive, being the very brightest stars and some globular clusters; they sit at the true positions of stars the app draws anyway, so they read as halo rather than as duplicates.

Its handedness had to be measured. Getting galactic longitude backwards mirrors the entire sky and looks completely plausible. So it was checked against two objects whose galactic coordinates are known exactly, on the 6000-pixel source:

objectl, bunder l leftwardunder l rightward
LMC280.5°, −32.9°38.211.1
SMC302.8°, −44.3°18.710.0

against a background of 7.7. Both land on a bright cloud only one way round — the astronomical convention — and the galactic centre independently falls at x = 2978 of 3000. That check is written into scripts/build-milkyway.py next to the code it justifies.

It had to use the same astronomy as the stars. The galactic axes are found by pushing (l=0, b=0), (l=90, b=0) and the north galactic pole through galacticToEquatorial() and equatorialToVector() — the exact pipeline the stars go through, precession included — so the band cannot sit a fraction of a degree off the stars because two copies of the rotation disagreed. A test round-trips seven directions through both and requires them to agree to a millionth of a degree.

Two renderers, one picture

A WebGL fragment shader runs the inverse projection for every pixel on the GPU — full resolution, full frame rate, on the ~97% of browsers that have it. The shader is a transcription of screenToVector(), which is the inverse projection the tap and the drag already use.

Where WebGL is missing, the same arithmetic runs in JavaScript per pixel at reduced resolution and is smoothed back up by the browser. Measured on a desktop at 12.7 ms for a quarter-size pass; a phone is three to five times slower, so the divisor adapts to what the device can actually manage. It suits the subject: the band is a diffuse glow with no edges, so a quarter-resolution render upscaled is not a visible compromise. ?nogl=1 forces that path, so it can be looked at on a device that would never otherwise use it.

Neither is the last resort. If the image never loads, or the GL context is lost — which phones do when a tab is backgrounded — the layer says so and the old procedural blobs are drawn instead. The band is never simply absent.

The credit is on the picture

ESO's licence is CC BY 4.0, which permits redistribution and commercial use with a credit that "cannot be hidden or separated from the image". In full screen the image is the screen, so the credit lives on the legend plate that is drawn over the map in both modes, with the longer form and the link under the windowed view. It appears exactly when the photograph is what is on screen — not while the Milky Way is hidden, and not while the procedural band is standing in for an image that has not arrived.

The planets are little worlds, and two things about them are not true

A planet used to be a dot sized from its magnitude: correct, and indistinguishable from the ten thousand other dots except by its caption. The thing that makes Jupiter recognisable through any telescope is its belts, and Mars its dark maria, and none of that survives being a circle. Each planet is now drawn as a textured sphere, sampled from an equirectangular map onto a disc with limb darkening, so it reads as a ball rather than a sticker.

The position is exact, as everywhere else here. Two things are not, and both are the compromise the Moon already makes and already declares:

  • Size. A planet is arcseconds across. At any field this app draws, that is a fraction of a pixel, so the disc is drawn larger than life — a symbol exaggerated to be recognisable.
  • Which face. The texture is the planet's whole map with an arbitrary central meridian. Showing the hemisphere actually turned toward Earth needs each planet's rotation model, which this app does not carry. Jupiter's belts are in the right places and its Great Red Spot is not.

The phase is real. Mercury and Venus show crescents, from the same phase angle the magnitude calculation already uses, with the lit side measured against the projection through the same function the Moon uses — one implementation, because a mirrored crescent is wrong in a way people notice instantly without being able to say why.

Saturn's rings are computed, not drawn in

A ringless Saturn is a cream ball nobody would name, and the texture maps carry the globe alone. The rings are an ellipse squashed by how far the ring plane is opened toward Earth — and that genuinely changes, from edge-on to about 27°, over roughly fifteen years. A fixed oval would be wrong for most of Saturn's orbit and absurd at the crossings, so the angle is computed each tick from the ring plane's pole (RA 40.589°, Dec 83.537°, J2000):

sin(B) = −(pole · toSaturn)

It validates against an event nothing in the code knows about. The rings passed edge-on on 2025 March 23, published years in advance; the formula returns 0.01° there, 26.5° near the 2017 maximum, and −26.9° in 2032 with the opposite face showing. That is the ring system's version of the Southern Cross rule — a number the projection cannot have been fitted to.

The far half of the ring is drawn behind the globe and the near half in front, which is the only thing that makes a ring read as encircling rather than as a hoop lying on a circle.

What it costs

Seven planets at 128×64 each, as one WebP atlas: 9 KB. The app works with the radio off, so everything is precached before it is needed — one request and one cache entry rather than seven. Pluto is deliberately textureless: Solar System Scope publishes no map, the New Horizons one covers a single hemisphere, and at magnitude 14 it is never visible to the eye anyway. It keeps its dot, and so does anything else whose texture cannot be drawn.

The maps are Solar System Scope's, built from NASA imagery and licensed CC BY 4.0 — the same licence the Milky Way panorama ships under — so the credit sits beside ESO's on the legend plate and appears on the same terms: only while the textures are what is on screen.

The constellation figures, ghosted behind the stars

The app draws the constellation lines, which are a join-the-dots: correct, and silent about why a line of stars is called a bear. James Hedberg drew all 88 figures and placed them where they belong; the app ships twenty-five of them, painted behind the stars — additive, so the art only ever lightens the sky rather than covering it — with the stars drawn after, so they stay the subject. Off in one press, in Visual Settings or with the Art button in full screen, and remembered.

Which twenty-five, and why not all 88

Eighty-four shipped first and read as "a bit crowded": at a wide field a dozen figures lie over the stars a person is trying to recognise, and most of them are figures nobody recognises. So the app ships the ones people know — the twelve of the zodiac, and thirteen more that a beginner's chart names first: the three of the Polaris star-hop (Ursa Major, Ursa Minor, Cassiopeia) above all, since finding Polaris is what the app is for; Orion, Canis Major and Boötes; the Summer Triangle's Cygnus, Lyra and Aquila; the autumn group of Perseus, Andromeda and Pegasus; and Crux, the figure the southern half of the world knows best. The two lists sit at the top of the build script, and a figure joins or leaves by editing them.

Twenty-five is not a round number by accident. Night Mode tints a copy of the atlas on a canvas the same size, and 4096 × 4096 is the largest canvas every phone will make; at 768 px a tile, that is five by five.

Ursa Major, which never shipped

The first build matched each row of OpenSpace's placement file to its image by the abbreviation column — and that column spells Ursa Major "Uma" while its image is "UMa.png". The match failed silently, the build reported 84 figures and no error, and the one figure missing was the Big Dipper: the pointer to Polaris, on the app's own front page. Rows are now matched by the image name each one carries, which is also the spelling the star catalogue uses, and a test holds the three star-hop figures in the shipped set by name.

The geometry happens once, in the build script

OpenSpace hangs each figure as a flat plane in space: a galactic direction, a GLM Euler rotation and a size. Reproducing that at runtime would mean carrying a galactic-to-equatorial conversion, GLM's exact quaternion convention and a plane's corner maths into a canvas, where none of it could be tested.

So the build script does it once and ships the answer: four corner vectors per figure in equatorial J2000, which the app blends, normalises and projects like any other points on the sky. The runtime keeps no constellation geometry at all. Three conventions had to be right, and all three were read rather than guessed — the CSV's coordinates are galactic (confirmed by converting and landing on each constellation's known position), StaticRotation is glm::mat3_cast(glm::quat(euler)), and the plane spans ±Size about its centre.

The corners are stored in J2000 and rotated into the horizontal frame on the slow tick, beside the stars; the drawing layer itself converts no frames. It did not start that way: the layer first projected the corners straight from J2000 against a horizontal basis, and the figures drifted with the time of night and the latitude, away from the very stars they were drawn around — "that orion image isn't even close to the reference." A test now holds the corners to the same journey every star makes.

The corners are not unit vectors, on purpose

OpenSpace's planes do not face the viewer square on. Measured across the set, each is tilted between 2° and 28° from its own line of sight, and a tilted plane seen from the origin is foreshortened — its far edge smaller than its near one. That keystone is part of where Hedberg put every line.

The app finds a point of a drawing by blending its four corners and putting the result back on the sphere. Blending the corners as unit vectors throws the tilt away: near and far corners are made the same length, the blend lands between them in the wrong place, and only the corners themselves stay right. Measured before it was fixed, Orion's interior was off by up to 1.1°, Virgo's by 3.5° and Pegasus's by 6.1°, with every corner exactly in place — which is the signature of the residual reported as "closer but still off a bit" once the frame fix had put the corners where they belong.

So each corner ships at its true distance from the eye, on the plane. The blend of four corners of a flat parallelogram is a point of that same plane exactly, normalising afterwards gives its direction, and the slow tick keeps each corner's length when it rotates the figures into the horizontal frame. Checked against the source geometry at 81 points per figure, the blend now lands within 0.00004° of the true plane — the rounding of the data file. A test holds every figure to a parallelogram whose corners differ in length, so the tilt cannot quietly be normalised away again.

And the placement is checked against this app's own stars

A figure upside down, or half a sky away, is worse than no figure. So every quad is tested against the star catalogue the app already ships: the constellation's brightest stars must fall inside it, and the build stops if any figure fails. All 25 pass. A second script measures how far each of a constellation's stars sits from the nearest inked line — 1.5° on average across the set, Ursa Minor at 0.15° — which is the regression check that a crop, a resolution change or an encoding change moved nothing.

That measurement also settled a question. A refit was tried — three small rotations and a scale per figure, accepted only if the star-to-ink distance improved — and over 67 figures it moved the mean from 1.78° to 1.76°: nothing. The shipped placements were already at that objective's optimum; what remains is in the drawings, because Hedberg drew figures, not star charts, and a line need not pass through every star.

Why a figure is not drawn as one quad

A figure spans thirty or forty degrees, and a gnomonic projection bends a shape that size: its edges are curves and its diagonals do not stay straight. Drawn into a single four-cornered shape, Orion visibly shears. So each figure is subdivided into a grid and every cell drawn separately — the same reason the ground is drawn as cells rather than as one hemisphere.

Canvas can only map a texture onto a triangle, never a quadrilateral, so each cell is two triangles, each grown a pixel about its own centre. Without that overlap a hairline seam shows along every shared edge, and a grid of hairlines across a ghost is far more visible than the ghost.

Resolution: crop to the ink first

Every source image is 2048 px square, but the drawing occupies only the middle of it — across the set the ink spans between a fifth and nine tenths of the tile edge. So a 256 px tile gave Orion about 130 px of actual drawing, which is where "very blurry" came from, and scaling the tile up would have spent most of the new pixels on nothing.

Each figure is now cropped to a square around its ink first, with a small margin, and that is resampled to a 768 px tile. The crop is a sub-rectangle of the same plane, so its corners are the plane's own points at the crop's edges and nothing moves on the sky. Against the old tiles that is three times the pixels per degree before the crop, and the crop adds between a tenth and four times more, figure by figure: Orion went from 4 px per degree of sky to 23, Lyra from 8 to 103.

The source is pure white with the drawing in its alpha channel, and WebP stores an alpha plane losslessly — measured at this size, the same atlas costs three times as much stored as alpha as it does stored as brightness on black. Because the art is drawn additively, black adds nothing and brightness on black is transparency, so the atlas ships as one grey channel: 25 tiles in 3840 × 3840 at 534 KB, chosen against 312 KB at 512 px, 423 KB at 640 px and 784 KB at 1024 px.

How bright is bright enough

The figures first shipped at an opacity of 0.16 and read as "very dim"; 0.30 was picked from three rendered side by side; a planetarium screenshot then made it 0.60, where the figures are confident white line-art you read at a glance rather than a hint you look for. Then the tiles went from 256 px to 768 and the same number read dimmer — a fine line puts less light in the eye than a blurred one spread over more pixels — so it is now 0.85, close to the source drawings' own white on black.

Night Mode uses two thirds of that, and not as a matter of taste. Dark adaptation is spent by the total light reaching the eye, not by hue, so a red field that is merely bright still costs twenty minutes to get back. The atlas is re-tinted red once, on first use, by a multiply with pure red on a canvas the size of the atlas — an additive white ghost would put grey on a screen that exists to stay red.

The artwork is James Hedberg's (CUNY-CCNY), redistributed by OpenSpace under CC BY 4.0 — the licence is carried in the asset's own metadata, not merely described on a page about it — so the credit joins ESO's and Solar System Scope's on the legend plate, and appears only while the figures are on screen.

The ground hides what the ground hides

Until 2026-09-22 everything below the horizon was drawn straight through the ground — stars, the Milky Way, the planets' paths — with a comb of short ticks to say which way was down. The one thing you cannot see from the ground is through it, so a view that shows stars under the horizon is a view lying about what you can see.

The hemisphere below the horizon is now a wireframe globe, ten degrees a cell, drawn as its lines of altitude and azimuth and nothing else — see-through, so it reads as the ground you are standing on without hiding the set planet you were looking for. It is paint order: the ground is drawn after every sky layer and before the target ring, the off-screen pointer and the ISS marker, so the markers stay on top of it.

A set planet is drawn there, on its dashed path, and its caption says it has set. It was not, at first: the planet gate written for the solid ground outlived it, and the ring sat on Jupiter's path with nothing inside — "planets should be visible even if set" (fixed 2026-09-22). The Moon and the Sun keep their gates, which have refraction behind them.

Hide the ground in Visual Settings, and the ◒ button in the full-screen zoom column, switch it off. That gives the old open view back, for anyone who wants to see where a set planet is sitting under the ground.

A gnomonic projection sends a point ninety degrees from the centre to infinity, and a ground cell with one corner there would paint a wedge across the whole picture. Cells near the rim are skipped, and a test looks straight down at a 170° field — the whole rim in view — and holds every corner within bounds.

The Sun, and the constellations as targets

The Sun is a body now, like the Moon: placed of date from the same solar series the Moon's phase already uses, for the time being shown — a planned night gets its Sun too — and flagged as of date so it is not precessed a second time, the Moon's flag for the Moon's reason. It is drawn as a disc while any of it is above the horizon: half a degree of disc plus half a degree of refraction, so it stays in view with its centre almost a degree down, which is sunset as people see it. The view is used in daylight, for finding the pole before dark, and that is when the disc matters.

It is targetable always. Press Sun and the ring is on it; below the horizon the caption says it has set and the arrow points down at it, exactly as the Moon does. Press again to let go.

The constellations get the Planets button's behaviour. One press walks the shipped figures in the order they ship — the Big Dipper first, Carina last — with the ring on the middle of each and the figure's own name as the caption; after the last, nothing, then round again. The middle is the mean of the figure's stars' directions, each star counted once however many lines meet at it, from the same vectors the lines are drawn from — so the ring lands inside what is on screen, and a test holds every shipped figure to a real centre.

Pressing a target again lets go

A pressed target button used to do nothing on a second press — a switch with no off. Polaris, Moon, ISS and Sun now let go on the second press; the ring comes off and the view stays where it is. The Planets and Constellations buttons keep walking their lists, and letting go is the last stop on the walk. The active button turns green — text and border, not a fill, which reads better over the sky — and the row that used to name the target under the buttons is gone: it repeated what the lit button already says, and looked exactly like one.

The arrows: press to nudge, hold to move

An arrow used to cut fifteen degrees per press. A press now travels those fifteen degrees with an ease in and out; a hold starts the sky moving, ramps up to sixty degrees a second over six hundred milliseconds, and when the finger lifts it coasts out through the same momentum a released drag has — so it neither lurches nor stops dead. The keyboard arrows hold too: key down starts, key up releases. A click with no pointer behind it — a screen reader, or Enter on the focused button — is one press. Under prefers-reduced-motion the coast is skipped and the nudge arrives at once, as every other movement in the view does.

The drag: solved, not searched for

Two things were wrong with dragging the sky until 2026-09-22, and they were reported together as "it flips and flops" and "I'm hitting a wall going multiple different directions".

The view was held as two angles, azimuth and altitude, and a drag was found by iteration: rotate the grabbed star onto the finger, collapse the result to the two angles, rebuild the view from them, measure the miss, try again — six times, keeping the best answer. The basis builder could not represent anything past 89.9°, so a step whose answer lay beyond that was refused whole rather than taken partway, and the view stopped a full drag-length short of the limit: a wall at 71° up, with the limit at 89°. Smaller drags crept higher, which is how the cause was confirmed. Near the nadir the same iteration answered "do not move" or jumped: 4.5° of view for a 0.4° finger step at −85°.

It looked like gimbal lock from the outside. It was not — a level view has exactly two degrees of freedom and a screen point is exactly two constraints, so the question "which level view puts this star under the finger" has a closed-form answer, and it is now solved rather than searched for. Where there is no answer — a star at 60° cannot be shown 45° right of centre in any level view, and the limits stop the view a degree short of the poles — the nearest view is given, so the sky leans on its limit instead of stopping short of it.

The orientation itself is held as a quaternion, because that is the natural home for the rest of it: travelling to a target is a slerp, which takes the short way round without anyone reasoning about the 360° seam; the altitude limit is a rotation about the view's own right axis, which slides along the limit rather than snapping to it; and the horizon is level by construction rather than nudged back toward level — a sky view has a horizon, and roll is not a freedom it has. Angles are derived from the rotation for the two jobs that genuinely need them, the readout and the limits, and nothing else.

A trackball — composing "rotate the grabbed star onto the finger" as a free rotation, then taking the roll back out — was tried first and rejected with numbers: it is exact and singularity-free and it fights its own levelling, so a grab-and-put-back round trip missed by degrees and dragging a planet up and down near the nadir walked the azimuth by thirty. The tests keep those numbers: a round trip lands within a third of an arcsecond at every altitude tried, five full-screen drags from 30° reach 89° and stay there, and sixty steps of an off-centre drag never move the view by more than the finger moved.

The −30° hand floor is gone too. Hand steering used to stop thirty degrees under the horizon, where a target could go to −89°. The fence had to be computed from where the view already was, and a limit derived from the position it limits ratchets — which is what froze the controls under the Sun at night. It was also no longer buying anything: the ground is a see-through wireframe, and the app's own targets live below it. One limit now, −89° to 89°, the same for the arrows, a tap, a drag and a target.

The ISS, and the one thing that needs the network

Everything else in this app works with the radio off. Stars are fixed for centuries and the magnetic model is good for five years, so both ship inside it. The ISS cannot be bundled — it is somewhere different every second, and its orbit is nudged by atmospheric drag and by reboosts, so its position has to be asked for. That is stated in the interface rather than discovered in a field.

Look angles are computed on the WGS-84 ellipsoid: both positions to Earth-centred coordinates, the difference rotated into the observer's east/north/up frame. Tested against the ellipsoid's own defining numbers (equatorial radius 6378.137 km, polar 6356.752), against overhead reading exactly 90°, and against the four cardinal directions.

A negative altitude is reported, never clamped. Most of the time the station is on the other side of the planet, and an app that draws it anyway is inventing a sighting someone then goes outside to look for. It is only marked on the sky when it is genuinely above the horizon.

The same goes for sunlight. The station shines only by reflected sunlight, so one that is overhead but inside the Earth's shadow is invisible — a completely different answer to "can I see it" than one that is overhead and lit. The marker is hollow when eclipsed and filled when sunlit, and the sentence says which.

The orbit it is on

The station can be drawn not just as a dot but as the light-blue dashed loop of its orbit — and, like the paths of the planets and the Moon, this is geometry, not new physics. The tracker will return positions for any ten timestamps in a single request, so one call buys ten points spread across a full 93-minute orbit, each propagated by the people who do that properly.

Ten points nineteen minutes apart, joined with straight lines, would be a polygon with 36° corners — a broken path, not an orbit. But those ten points all lie on one plane through the centre of the Earth, which is what an orbit is, so the app fits that plane and draws the whole circle from it. The samples are rotated out of the Earth-fixed frame into an inertial one first: in Earth-fixed coordinates they do not lie on a plane, because the Earth turns about 23° under the station during a single orbit — which is exactly why its ground track marches west across a map. Verified against the live tracker: the sampled latitudes reach 49° against the station's 51.6° orbital inclination.

The planets' and the Moon's paths are simpler — a run of computed positions, three months either side of now for a planet (long enough for the retrograde loops that named them wanderers) and a fortnight for the Moon — but they share one rule: every point is placed against the current sidereal time, so a track is the path a body takes through the stars, not a spiral of the Earth's own rotation. Each is dashed and named repeatedly along its length, because they all crowd the ecliptic and a single label at one end leaves you tracing a line with a finger.

What it does not do

Pass prediction. "It rises in the north-west at 21:04 for four minutes" is the genuinely useful thing for photography, and it needs SGP4 propagation from a fresh orbital element set — an orbital-mechanics library's worth of code, and elements that go stale within days. Showing where it is right now is a fraction of that work, and the app is honest about being the smaller thing.

Still open

Whether Android's deviceorientationabsolute reports magnetic or true north varies between devices. The app applies declination itself, which is correct for most Androids and would double-correct on one that had already done it. This needs checking against a known bearing on real hardware.

Nothing else depends on it: the reticle, the star chart, and both compass-free routes to true north are unaffected either way.

Documentation hub for Evomedia.net LLC products.