Engraving stars for unique moments This series shares my journey learning to draw skies on tile — star charts I engrave for newborns of friends and family, capturing their birth sky. I'm currently exploring selling these, but they're not publicly available for orders.
If you're interested, ask here.

This is the second article in a series converting starplot from Python to JavaScript. We build on the coordinate types from the previous article and add the time-dependent machinery that turns fixed celestial addresses into positions in your local sky.

Greenwich Mean Sidereal Time

The younger Tuareg asks: how do you know it is the third hour past sunset and not the fourth? The elder points straight up. That star, Capella, was near the horizon when we stopped. Now it is high. The sky is a clock, and the stars are its hands. But it is a clock that runs four minutes faster than the sun; a sidereal clock.

Our clocks track the Sun: one solar day is the time between two consecutive noons. But sidereal time tracks the stars instead. A sidereal day is ~23 hours 56 minutes, about four minutes shorter than a solar day. Why the difference? Because the Earth is simultaneously spinning on its axis and orbiting the Sun. After one full spin relative to the stars, it has moved ~1° along its orbit, so it needs to rotate an extra ~1° (about 4 minutes) to bring the Sun back to the same position.

Greenwich Mean Sidereal Time (GMST) tells us which Right Ascension is directly overhead at the prime meridian (Greenwich, longitude 0°) at this instant. It is the bridge between clock time and sky position.

The formula uses the Julian Date, a continuous count of days since January 1, 4713 BCE used in astronomy to avoid calendar irregularities. The standard reference epoch is J2000.0 (January 1, 2000 at 12:00 TT).

import { normalizeDeg } from './01-coordinates.org?name=angle-utils';

/**
 * Julian Date from a JS Date.
 *
 * The Julian Date is a continuous count of days since
 * January 1, 4713 BC. Astronomers use it because it avoids
 * calendar irregularities.
 */
export function toJulianDate(date: Date): number {
  return date.getTime() / 86400000 + 2440587.5;
}

/**
 * Centuries elapsed since J2000.0 epoch.
 *
 * J2000.0 = January 1, 2000 at 12:00 TT (JD 2451545.0).
 * Dividing by 36525 converts days to Julian centuries.
 */
export function julianCenturies(jd: number): number {
  return (jd - 2451545.0) / 36525;
}

/**
 * Greenwich Mean Sidereal Time in degrees.
 *
 * Uses the IAU 1982 formula (Meeus, "Astronomical Algorithms").
 * Accurate to ~0.1 second over several decades from J2000.
 *
 * The polynomial terms account for:
 * - 280.46061837: GMST at J2000.0 epoch
 * - 360.98564736629 * d: Earth's daily sidereal rotation
 * - 0.000387933 * T^2: precession
 * - T^3 / 38710000: higher-order precession correction
 */
export function gmst(date: Date): number {
  const jd = toJulianDate(date);
  const T = julianCenturies(jd);
  const d = jd - 2451545.0;

  const theta = 280.46061837
    + 360.98564736629 * d
    + 0.000387933 * T * T
    - T * T * T / 38710000;

  return normalizeDeg(theta);
}

Local Sidereal Time

Local Sidereal Time (LST) adjusts GMST for the observer's longitude. If you are 15° east of Greenwich, your local sky is "ahead" of Greenwich by 15° of rotation. LST tells you which Right Ascension is on your local meridian right now; it is the key link between clock time, location, and the sky.

The word algorithm comes from al-Khwarizmi (c. 780–850 CE), a mathematician at the House of Wisdom in Baghdad who also gave us algebra (from al-jabr). His astronomical tables included methods for computing celestial positions step by step; the original algorithms. What we are writing here is a direct descendant of that tradition.

import { normalizeDeg } from './01-coordinates.org?name=angle-utils';
import { gmst } from './02-sidereal-time.org?name=gmst';

/**
 * Local Sidereal Time in degrees.
 *
 * LST = GMST + observer's longitude.
 * This gives the Right Ascension currently crossing
 * the observer's meridian.
 */
export function localSiderealTime(date: Date, lonDeg: number): number {
  return normalizeDeg(gmst(date) + lonDeg);
}

Visualizing Sidereal Time

Drag the sliders to change the time and longitude. The stars (outer ring) are fixed on the celestial sphere. The Earth (inner disc) rotates underneath. The green line is the observer's meridian; the LST readout shows which Right Ascension is overhead.

Hour Angle

The elder extends his arm toward a bright star in the south, fingers spread wide. He measures three hand-widths from the meridian, the imaginary line running from north through the zenith to south. The star has already passed its highest point. It is descending westward. That angular distance from the meridian is the hour angle.

The hour angle (HA) measures how far an object is from the observer's meridian (the imaginary north-to-south line passing through the zenith). When HA = 0, the object is crossing the meridian at its highest point in the sky; this is called transit. Positive HA means it has already transited and is moving westward; negative HA means it is still approaching from the east.

The formula is simply HA = LST - RA. Since LST tells us which RA is overhead now, subtracting the star's RA gives the angular distance from the meridian.

import { normalizeDeg } from './01-coordinates.org?name=angle-utils';
import { localSiderealTime } from './02-sidereal-time.org?name=lst';

/**
 * Hour angle in degrees.
 *
 * HA = LST - RA. When HA = 0, the object is on the meridian
 * (highest point in the sky). Positive HA means the object
 * is west of the meridian (past transit), negative means east
 * (not yet transited).
 */
export function hourAngle(date: Date, lonDeg: number, raDeg: number): number {
  const lst = localSiderealTime(date, lonDeg);
  return normalizeDeg(lst - raDeg);
}

Equatorial to Horizontal

Now the elder does something remarkable. He points at a star the younger one has never navigated by, and without instruments, without tables, he says: that star is three fists above the horizon, in the direction of Agadez. He has done, in his head, refined by decades of practice, the conversion from where a star is on the celestial sphere to where it appears from where they sit.

This is the core conversion: given a star's RA/Dec and an observer's position/time, where does it appear in the sky?

The math involves the astronomical triangle: a triangle on the celestial sphere formed by three points: the north celestial pole (P), the observer's zenith (Z), and the star (S). The three sides of this triangle correspond to known quantities (the star's declination, the observer's latitude, and the hour angle we just computed). Solving for the remaining angles gives us altitude and azimuth. This is spherical trigonometry, the geometry of triangles drawn on the surface of a sphere rather than on a flat plane.

Al-Battani (c. 858–929 CE), working in Raqqa, was the first to systematically use sines instead of Ptolemy's chords for these calculations; a fundamental advance that made the formulas both simpler and more accurate. His Kitab al-Zij was cited 23 times by Copernicus. Nasir al-Din al-Tusi (1201–1274) wrote the first standalone treatise on spherical trigonometry, Shakl al-Qatta, which formalized exactly the triangle we solve below. The math was partly driven by a practical need: determining prayer times and the qibla (direction to Mecca) from any location requires precisely this pole-zenith-star conversion.

import type { EquatorialCoord, HorizontalCoord, Observer } from './01-coordinates.org?name=types';
import { degToRad, radToDeg, normalizeDeg } from './01-coordinates.org?name=angle-utils';
import { hourAngle } from './02-sidereal-time.org?name=hour-angle';

/**
 * Convert equatorial (RA/Dec) to horizontal (Alt/Az) coordinates.
 *
 * The formulas come from spherical trigonometry applied to the
 * "astronomical triangle" formed by the celestial pole, zenith,
 * and the object:
 *
 *   sin(alt) = sin(dec)*sin(lat) + cos(dec)*cos(lat)*cos(ha)
 *
 *   sin(az)  = -cos(dec)*sin(ha) / cos(alt)
 *   cos(az)  = (sin(dec) - sin(lat)*sin(alt)) / (cos(lat)*cos(alt))
 *
 * We use atan2 for the azimuth to get the correct quadrant.
 */
export function equatorialToHorizontal(
  coord: EquatorialCoord,
  observer: Observer,
): HorizontalCoord {
  const ha = degToRad(hourAngle(observer.datetime, observer.lon, coord.ra));
  const dec = degToRad(coord.dec);
  const lat = degToRad(observer.lat);

  const sinAlt = Math.sin(dec) * Math.sin(lat)
    + Math.cos(dec) * Math.cos(lat) * Math.cos(ha);
  const alt = Math.asin(sinAlt);

  const cosAlt = Math.cos(alt);
  const sinAz = -Math.cos(dec) * Math.sin(ha) / cosAlt;
  const cosAz = (Math.sin(dec) - Math.sin(lat) * sinAlt) / (Math.cos(lat) * cosAlt);
  const az = Math.atan2(sinAz, cosAz);

  return {
    alt: radToDeg(alt),
    az: normalizeDeg(radToDeg(az)),
  };
}

Interactive Sky Dome

Click any star to select it. Drag the time slider to watch the sky rotate: stars rising in the east, transiting south, setting in the west. The dome shows the view from Rome, looking straight up, with our equatorialToHorizontal function computing every position.

The astrolabe, originally described in rough form by Hipparchus and later by Theon of Alexandria (~335–405 CE), was transformed into a precision instrument by Islamic astronomers, particularly Nastulus (9th c.) and al-Biruni (973–1048). It is essentially a mechanical version of this conversion: a stereographic projection of the celestial sphere onto a flat disc, letting navigators read off altitude and azimuth by rotating overlays. Mariam al-Astrulabi, a 10th-century woman in Aleppo, was among the celebrated makers of these instruments. Our equatorialToHorizontal function does in microseconds what her brass discs did by hand.

Horizontal to Equatorial

The inverse of the conversion above. Given where something appears in the observer's sky (altitude and azimuth), what are its equatorial coordinates (RA/Dec)? This is useful for mapping screen coordinates back to the celestial sphere, for example to identify what you clicked on. Try it in the dome above: click on empty sky to place a custom star, and the info panel shows the computed RA/Dec.

import type { EquatorialCoord, HorizontalCoord, Observer } from './01-coordinates.org?name=types';
import { degToRad, radToDeg, normalizeDeg } from './01-coordinates.org?name=angle-utils';
import { localSiderealTime } from './02-sidereal-time.org?name=lst';

/**
 * Convert horizontal (Alt/Az) to equatorial (RA/Dec) coordinates.
 *
 * The inverse of equatorialToHorizontal:
 *
 *   sin(dec) = sin(alt)*sin(lat) + cos(alt)*cos(lat)*cos(az)
 *
 *   HA = atan2(-cos(alt)*sin(az),
 *              sin(alt)*cos(lat) - cos(alt)*sin(lat)*cos(az))
 *
 *   RA = LST - HA
 */
export function horizontalToEquatorial(
  coord: HorizontalCoord,
  observer: Observer,
): EquatorialCoord {
  const alt = degToRad(coord.alt);
  const az = degToRad(coord.az);
  const lat = degToRad(observer.lat);

  const sinDec = Math.sin(alt) * Math.sin(lat)
    + Math.cos(alt) * Math.cos(lat) * Math.cos(az);
  const dec = Math.asin(sinDec);

  const sinHA = -Math.cos(alt) * Math.sin(az) / Math.cos(dec);
  const cosHA = (Math.sin(alt) * Math.cos(lat)
    - Math.cos(alt) * Math.sin(lat) * Math.cos(az)) / Math.cos(dec);
  const ha = Math.atan2(sinHA, cosHA);

  const lst = localSiderealTime(observer.datetime, observer.lon);
  const ra = normalizeDeg(lst - radToDeg(ha));

  return { ra, dec: radToDeg(dec) };
}

Angular Separation

The younger one asks: how far is it from that star to this one? The elder holds up his hand at arm's length. A closed fist is about 10 degrees. Three fingers is 5. The width of the little finger is 1. He measures the gap between Betelgeuse and Rigel. About three fists, 27 degrees or so. It is the oldest rangefinder in the world.

How far apart are two objects on the sky? This is called angular separation, and it uses the same geometry as great-circle distance on Earth: the shortest path along the surface of a sphere.

We use the Vincenty formula, a numerically stable form of the spherical law of cosines published by Thaddeus Vincenty in 1975. The underlying spherical trigonometry was formalized centuries earlier by Abu al-Wafa al-Buzjani (940–998 CE), who established the spherical law of sines, and matured into a complete discipline by al-Tusi in his 1265 treatise Shakl al-Qatta.

import type { EquatorialCoord } from './01-coordinates.org?name=types';
import { degToRad, radToDeg } from './01-coordinates.org?name=angle-utils';

/**
 * Angular separation between two equatorial coordinates in degrees.
 *
 * Uses the Vincenty formula (numerically stable version of the
 * spherical law of cosines):
 *
 *   Numerator:  sqrt( (cos(d2)*sin(dRA))^2 + (cos(d1)*sin(d2) - sin(d1)*cos(d2)*cos(dRA))^2 )
 *   Denominator: sin(d1)*sin(d2) + cos(d1)*cos(d2)*cos(dRA)
 *   separation = atan2(numerator, denominator)
 */
export function angularSeparation(a: EquatorialCoord, b: EquatorialCoord): number {
  const ra1 = degToRad(a.ra);
  const dec1 = degToRad(a.dec);
  const ra2 = degToRad(b.ra);
  const dec2 = degToRad(b.dec);
  const dra = ra2 - ra1;

  const num = Math.sqrt(
    (Math.cos(dec2) * Math.sin(dra)) ** 2
    + (Math.cos(dec1) * Math.sin(dec2) - Math.sin(dec1) * Math.cos(dec2) * Math.cos(dra)) ** 2,
  );
  const den = Math.sin(dec1) * Math.sin(dec2)
    + Math.cos(dec1) * Math.cos(dec2) * Math.cos(dra);

  return radToDeg(Math.atan2(num, den));
}

Measuring the Sky

Click two stars to see the great-circle arc between them and the angular separation computed with our Vincenty formula.

Putting It All Together

Hours have passed. The fire is cold. The younger Tuareg lies on his back, watching the sky wheel overhead. Polaris hangs nearly still while everything else arcs around it. Stars he saw rise in the east are now high in the south. New ones are appearing on the eastern horizon. The sky is not a painting; it is a machine, and he is starting to understand its gears.

An animated night sky over Rome. The time advances automatically. Watch stars rise in the east and set in the west. Every frame runs equatorialToHorizontal for all visible stars using the functions we built above.

Watch Polaris barely move near the center (it is near the north celestial pole) while stars in the south trace wide arcs across the sky. Stars near the horizon fade as atmospheric extinction kicks in. All of this from just the coordinate math we built in these two articles.

Before dawn, the elder stands. The stars are fading. He has watched the younger one follow Capella's path across the sky, measure the gap between Betelgeuse and Rigel with his fist, and find north from the star that does not move.

Now you understand the gears, he says. In the next lesson, we flatten the sphere.

Read More

Have you seen the previous article? Or go to Constellations for the overview.


Do you like this article? I am building it with org-press, a project that allows you to create notebooks with live code examples. Check it out.

If this resonates.

I make these as gifts for births, anniversaries, moments worth keeping. I am considering opening this practice more widely, carefully and in small numbers.

Each constellation is a 25×25 cm slate plaque, laser-engraved and painted by hand in gold, boxed in carved larch. Around six hours of focused work go into each piece. Indicative price, €150 to €200.

Because each piece takes real time, I ask for a short motivation. I prefer to make them for people who genuinely care about the moment they are marking. Any profit beyond materials and time will go to a children's association, with full transparency.

If enough thoughtful interest gathers, I may open a limited number of commissions.