From 23446d1501d57f6c51d98e45dbe8370d8975b18d Mon Sep 17 00:00:00 2001
From: "Craig C."
Date: Thu, 22 Oct 2026 22:33:18 +0300
Subject: [PATCH 1/6] feat(helsinki): aurora farther north, lower in the sky
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Helsinki sits about 6° of latitude south of Ísafjörður, so the same oval is
much farther away and lower. The curtains move from 550/900 km to
750/1000 km north: the lower border now sits about 4° above the skyline and
the tallest rays reach about 17°, a third of Ísafjörður's height.
---
apps/site/src/scripts/aurora.sites.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/apps/site/src/scripts/aurora.sites.ts b/apps/site/src/scripts/aurora.sites.ts
index e807317..f67e6a5 100644
--- a/apps/site/src/scripts/aurora.sites.ts
+++ b/apps/site/src/scripts/aurora.sites.ts
@@ -102,8 +102,8 @@ export const SITES: Site[] = [
dip: 74,
decl: 11,
curtains: [
- { base: 550, centre: -250, half: 1000 },
- { base: 900, centre: -400, half: 1600 },
+ { base: 750, centre: -350, half: 1300 },
+ { base: 1000, centre: -470, half: 1800 },
],
glow: 1,
limit: 4.2,
From 9282745348a1b1fbe8e406af44ee43fe0d011b39 Mon Sep 17 00:00:00 2001
From: "Craig C."
Date: Thu, 22 Oct 2026 22:34:02 +0300
Subject: [PATCH 2/6] feat(site): the sun, planets and eclipses where they
really are
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The popover now says it is always night here, but the stars, planets, moon
and sun are where they really are right now.
- Planets: Mercury to Saturn from JPL's Keplerian elements (Standish), with
Meeus ch. 41 magnitudes and Saturn's ring tilt. Drawn with the star
program, barely twinkling. Within 5′ of astronomy-engine.
- Sun: only while it is really up and in frame (summer evenings at
Helsinki, summer nights at Ísafjörður, often behind the mountains). An
ice-blue disc at true size with a soft glow, a faint, patchy 22° halo and
a pillar when low. The sky stays dark.
- Moon: the same soft glow as the sun, scaled by phase, and half as bright
while the sun is up so the sun always outshines it.
- Eclipses: the moon covers the sun (crescent, diamond ring, ice-blue
corona at totality), and Earth's shadow crosses the moon (soft penumbra,
a dim umbra in the theme's purple where life has copper, a turquoise
fringe at its edge). The moon lights the sky less while eclipsed.
- Accuracy for eclipses: terrestrial time (ΔT = 69.184 s) for the orbits,
the full Meeus lunar tables, and solar aberration plus fitted
perturbation terms. Moon within 2–7″ and the sun–moon separation within
17″ of astronomy-engine; the 2026-08-12 eclipse is total at Ísafjörður
and 79% at Helsinki, and the 2025-09-07, 2026-03-03 and 2026-08-28 lunar
eclipses match the published umbral magnitudes (Danjon's rule).
---
apps/site/src/components/Aurora.astro | 5 +-
apps/site/src/scripts/aurora.shaders.ts | 105 +++++++-
apps/site/src/scripts/aurora.sites.ts | 4 +
apps/site/src/scripts/aurora.sky.ts | 315 +++++++++++++++++++++++-
apps/site/src/scripts/aurora.worker.ts | 55 +++--
5 files changed, 450 insertions(+), 34 deletions(-)
diff --git a/apps/site/src/components/Aurora.astro b/apps/site/src/components/Aurora.astro
index 2a914bc..003c2ce 100644
--- a/apps/site/src/components/Aurora.astro
+++ b/apps/site/src/components/Aurora.astro
@@ -58,8 +58,9 @@ import {
{coords(site.lat, site.lon)}, eye height {site.eye} m.
- The stars, moon and {site.ground} are real and show this moment.
- The aurora is simulated.
+ It is always night here, but the stars, planets, moon and sun are
+ where they really are right now, above the real {site.ground}. The
+ aurora is simulated.
{site.credits.map(([what, source]) => (
diff --git a/apps/site/src/scripts/aurora.shaders.ts b/apps/site/src/scripts/aurora.shaders.ts
index 70cfccd..e8c7b55 100644
--- a/apps/site/src/scripts/aurora.shaders.ts
+++ b/apps/site/src/scripts/aurora.shaders.ts
@@ -117,6 +117,9 @@ uniform vec2 uTan;
uniform float uPxAng;
uniform vec4 uMoon; // direction, angular radius
uniform vec3 uMoonN, uSun;
+uniform vec2 uSunDisc; // angular radius, fraction not behind the moon
+uniform vec3 uShadow; // earth's shadow axis from the moon's centre, moon radii
+uniform vec2 uShadowR; // umbra and penumbra radii, moon radii
uniform float uMoonlight;
uniform float uGlow; // city light near the horizon, 0..1
uniform float uLights; // lit windows and streets, 0..1
@@ -150,9 +153,28 @@ float fetchCoarse(float x, float k) {
float profile(float h, float g) {
return smoothstep(94.0, 106.0, h) * exp(-max(h - 106.0, 0.0) / (25.0 + 45.0 * g));
}
+// Earth's shadow on the moon: how far the surface point n (moon radii from
+// its centre) is from the shadow's axis, and the sunlight left there
+// (0 in the umbra, 1 outside the penumbra).
+float shadowDist(vec3 n) {
+ vec3 q = n - uShadow;
+ return length(q - uSun * dot(q, uSun));
+}
+float sunlit(vec3 n) {
+ return smoothstep(uShadowR.x, uShadowR.y, shadowDist(n));
+}
vec3 moon(vec3 rd) {
+ if (uMoon.y < -0.02) return vec3(0.0);
float cm = dot(rd, uMoon.xyz);
- if (cm < cos(uMoon.w * 3.0) || uMoon.y < -0.02) return vec3(0.0);
+ // Daylight washes the moon out: half as bright while the sun is up, so the
+ // sun always outshines it.
+ float washed = 1.0 - 0.5 * smoothstep(-0.0145, 0.03, uSun.y);
+ // The same soft glow as the sun's, as strong as the moon is full and lit.
+ float full = (0.5 - 0.5 * dot(uSun, uMoon.xyz)) * sunlit(-uMoon.xyz);
+ float a = acos(clamp(cm, -1.0, 1.0));
+ vec3 glow = full * (mix(uIce, uCyan, 0.3) * 0.3 * exp(-a / 0.012)
+ + mix(uIce, uCyan, 0.6) * 0.035 * exp(-a / 0.08));
+ if (cm < cos(uMoon.w * 3.0)) return glow * washed;
vec3 o = (rd - uMoon.xyz * cm) / sin(uMoon.w);
float r = length(o);
float px = uPxAng / uMoon.w;
@@ -164,12 +186,76 @@ vec3 moon(vec3 rd) {
float lon = atan(dot(n, west), dot(n, -uMoon.xyz));
float alb = texture2D(uMoonTex, vec2(0.5 + lon / (2.0 * PI), 0.5 - lat / PI)).r;
float day = max(dot(n, uSun), 0.0);
- float halo = exp(-(1.0 - cm) / (uMoon.w * uMoon.w * 6.0)) * 0.06;
- float full = 0.5 - 0.5 * dot(uSun, uMoon.xyz);
- return uIce * (cover * alb * (2.2 * day + 0.03) + halo * full);
+ // Lunar eclipse: the penumbra dims softly; in the umbra only sunlight bent
+ // through the earth's air reaches the moon, a dim glow brightening toward
+ // the edge (copper in life, the theme's purple here) with a turquoise
+ // fringe at the edge, from ozone.
+ float lit = sunlit(n);
+ float d = shadowDist(n);
+ float edge = (d - uShadowR.x) / 0.12;
+ vec3 bent = uPurple * (0.07 + 0.2 * smoothstep(0.0, uShadowR.x, d))
+ + uCyan * 0.1 * exp(-edge * edge);
+ vec3 surface = alb * (uIce * (2.2 * day * lit + 0.03) + 2.2 * day * (1.0 - lit) * bent);
+ // Less glow over the disc itself, so the maria still show.
+ return (surface * cover + glow * (1.0 - 0.8 * cover)) * washed;
+}
+// The sun, only while it is really up: an ice-blue disc at true size, a faint
+// 22° ice halo and, when low, a sun pillar. The moon can eclipse it. The sky stays dark (always night here).
+// Halo colours run in the theme's order where real ones run red to blue.
+const float HALO = 0.3840; // 22° halo radius
+vec3 sun(vec3 rd) {
+ float up = smoothstep(-0.0145, 0.03, uSun.y);
+ if (up <= 0.0) return vec3(0.0);
+ float cs = dot(rd, uSun);
+ float a = acos(clamp(cs, -1.0, 1.0));
+ float px = uPxAng / uSunDisc.x;
+ float r = a / uSunDisc.x;
+ // Solar eclipse: the moon's dark disc in front. What is left of the sun
+ // still glows (the last sliver flares: the diamond ring).
+ float mr = acos(clamp(dot(rd, uMoon.xyz), -1.0, 1.0)) / uMoon.w;
+ float mpx = uPxAng / uMoon.w;
+ float behind = 1.0 - smoothstep(1.0 - mpx, 1.0 + mpx, mr);
+ float shine = sqrt(uSunDisc.y);
+ // Limb-darkened disc and a tight aureole of forward scattering.
+ float disc = (1.0 - smoothstep(1.0 - px, 1.0 + px, r))
+ * (0.55 + 0.45 * sqrt(max(0.0, 1.0 - r * r))) * (1.0 - behind);
+ vec3 ice = mix(uIce, uCyan, 0.3);
+ vec3 col = ice * (2.4 * disc + 0.35 * shine * exp(-a / 0.012))
+ + mix(uIce, uCyan, 0.6) * 0.04 * shine * exp(-a / 0.08);
+ // Totality: the corona, pearly and streaked, two or three solar radii out.
+ float total = 1.0 - smoothstep(0.0, 0.004, uSunDisc.y);
+ if (total > 0.0) {
+ vec3 side = normalize(cross(uSun, vec3(0.0, 1.0, 0.0)));
+ vec3 lift = cross(side, uSun);
+ vec2 pa = normalize(vec2(dot(rd, side), dot(rd, lift)) + 1e-6);
+ float streak = noise(pa * 3.0 + 5.0) * noise(pa * 9.0 + 11.0);
+ float h = max(r - 1.0, 0.0);
+ float corona = 0.6 * exp(-h / 0.3) + 0.5 * streak * exp(-h / 1.5);
+ col += mix(uIce, uCyan, 0.2) * corona * total * (1.0 - behind) * step(1.0, r);
+ }
+ // Thin cirrus or diamond dust: the crystals come and go across the sky,
+ // so the halos are patchy and drift slowly.
+ vec2 q = vec2(atan(rd.x, rd.z) * 5.0, rd.y * 9.0) + uT * vec2(0.004, 0.002);
+ float veil = smoothstep(0.15, 0.8, 0.65 * noise(q) + 0.35 * noise(q * 2.7 + 9.0));
+ // 22° halo: a faint ring, coloured on its inner side and broken where the
+ // crystals thin out. Soft on both sides: a sharp inner edge with a long
+ // outer fade makes the sky inside look darker (the Cornsweet effect).
+ float dh = a - HALO;
+ float ring = exp(-dh * dh / (dh < 0.0 ? 0.00008 : 0.00018));
+ vec3 tint = mix(uPurple, mix(uCyan, uIce, smoothstep(0.004, 0.03, dh)),
+ smoothstep(-0.004, 0.012, dh));
+ col += tint * ring * 0.035 * veil * veil * uSunDisc.y;
+ // A sun pillar over a low sun, from plate crystals.
+ float se = uSun.y, ce = sqrt(1.0 - se * se);
+ float dAz = atan(rd.x * uSun.z - rd.z * uSun.x, rd.x * uSun.x + rd.z * uSun.z);
+ float dAlt = asin(clamp(rd.y, -1.0, 1.0)) - asin(se);
+ float low = 1.0 - smoothstep(0.03, 0.11, se);
+ col += ice * 0.25 * low * uSunDisc.y * exp(-dAz * dAz * ce * ce / 0.000016)
+ * exp(-abs(dAlt) / 0.05);
+ return col * up;
}
// Sky seen along rd (above the horizon): background, city glow, moonlight,
-// the aurora and the moon. Before tone mapping. j is the step jitter.
+// the aurora, the moon and the sun. Before tone mapping. j is the step jitter.
vec3 skyColour(vec3 rd, float j) {
float c2 = dot(rd.xz, rd.xz);
vec3 acc = vec3(0.0);
@@ -205,7 +291,7 @@ vec3 skyColour(vec3 rd, float j) {
vec3 col = uVoid + uTeal * 0.05 * exp(-e * 8.0)
+ uGlow * (uTeal * 0.09 * exp(-e * 9.0) + uIce * 0.035 * exp(-e * 22.0))
+ uMoonlight * (uIce * 0.02 + uTeal * 0.05) * (0.5 + 0.5 * exp(-e * 3.0));
- return col + (acc * 0.035 + moon(rd)) * air;
+ return col + (acc * 0.035 + moon(rd) + sun(rd)) * air;
}
// City lights along the shore, by azimuth bin: on or off, and how bright.
float shoreLight(float az) {
@@ -287,7 +373,8 @@ void main() {
// Catalogue stars: J2000 directions turned into the sky of date. Fainter
// near the horizon (extinction) and when the moon brightens the sky. They
-// twinkle; they never move except with the sky itself.
+// twinkle; they never move except with the sky itself. The planets use the
+// same program, barely twinkling.
export const STAR_VS = `
precision highp float;
${PROJECT}
@@ -296,6 +383,8 @@ attribute vec2 aMag;
uniform mat3 uStars;
uniform vec3 uIce, uCyan;
uniform float uPx, uT, uLimit, uFade;
+// 1 for planets, which hardly twinkle.
+uniform float uSteady;
varying vec3 vCol;
void main() {
vec3 w = uStars * aDir;
@@ -304,7 +393,7 @@ void main() {
float b = pow(2.512, -m);
float a = min(2.6 * sqrt(b), 1.6) * smoothstep(uLimit + 0.3, uLimit - 0.6, m) * uFade;
float seed = fract(sin(dot(aDir.xy, vec2(91.7, 37.3)) + aDir.z * 11.1) * 4375.85);
- float amp = 0.1 + 0.3 * (1.0 - smoothstep(0.05, 0.5, alt));
+ float amp = (0.1 + 0.3 * (1.0 - smoothstep(0.05, 0.5, alt))) * (1.0 - 0.8 * uSteady);
float tw = 1.0 + amp * sin(uT * (1.1 + 2.0 * seed) + seed * 60.0)
* sin(uT * 0.37 + seed * 17.0);
vCol = mix(uIce, uCyan, 0.35 * smoothstep(0.4, -0.1, aMag.y)) * a * tw;
diff --git a/apps/site/src/scripts/aurora.sites.ts b/apps/site/src/scripts/aurora.sites.ts
index f67e6a5..de63c06 100644
--- a/apps/site/src/scripts/aurora.sites.ts
+++ b/apps/site/src/scripts/aurora.sites.ts
@@ -120,6 +120,10 @@ export const SITES: Site[] = [
export const SKY_CREDITS: [string, string][] = [
["Stars", "Yale Bright Star Catalogue, 5th revised edition (CDS V/50)."],
["Moon", "NASA's Scientific Visualization Studio."],
+ [
+ "Planets",
+ "JPL Keplerian elements for approximate positions (E. M. Standish).",
+ ],
];
const POINTS = [
diff --git a/apps/site/src/scripts/aurora.sky.ts b/apps/site/src/scripts/aurora.sky.ts
index 17015e8..c6508d7 100644
--- a/apps/site/src/scripts/aurora.sky.ts
+++ b/apps/site/src/scripts/aurora.sky.ts
@@ -2,7 +2,9 @@
// precession, and the sun and moon. Vectors use the scene's world frame:
// x east, y up, z north. Angles in radians unless named *Deg.
// Moon and sun follow Meeus, "Astronomical Algorithms" (2nd ed.), ch. 25 and
-// 47, with the larger periodic terms only (moon good to about 0.05°).
+// 47, with the full periodic tables (moon good to about 10″, which eclipses
+// need). The planets use JPL's mean Keplerian elements (good to a few arcminutes over
+// 1800–2050) and Meeus ch. 41 for their brightness.
import type { Site } from "./aurora.sites";
@@ -23,6 +25,10 @@ const RAD = Math.PI / 180;
const ARCSEC = RAD / 3600;
const julian = (ms: number) => ms / 86400000 + 2440587.5;
+// Terrestrial time runs ΔT ahead of the clock (UTC): 32.184 s plus 37 leap
+// seconds since 2017. The moon moves half an arcsecond a second, so the
+// orbit formulas need it.
+const DELTA_T = 69.184;
/** Row-major 3×3 product a·b. */
function mul(a: Mat3, b: Mat3): Mat3 {
@@ -33,7 +39,7 @@ function mul(a: Mat3, b: Mat3): Mat3 {
}) as Mat3;
}
-function apply(m: Mat3, v: Vec3): Vec3 {
+export function apply(m: Mat3, v: Vec3): Vec3 {
return [
m[0] * v[0] + m[1] * v[1] + m[2] * v[2],
m[3] * v[0] + m[4] * v[1] + m[5] * v[2],
@@ -114,6 +120,34 @@ const MOON_LR: number[][] = [
[2, -1, -2, 0, 2390, 10056],
[1, 0, 1, 0, -2348, 6322],
[2, -2, 0, 0, 2236, -9884],
+ [0, 1, 2, 0, -2120, 5751],
+ [0, 2, 0, 0, -2069, 0],
+ [2, -2, -1, 0, 2048, -4950],
+ [2, 0, 1, -2, -1773, 4130],
+ [2, 0, 0, 2, -1595, 0],
+ [4, -1, -1, 0, 1215, -3958],
+ [0, 0, 2, 2, -1110, 0],
+ [3, 0, -1, 0, -892, 3258],
+ [2, 1, 1, 0, -810, 2616],
+ [4, -1, -2, 0, 759, -1897],
+ [0, 2, -1, 0, -713, -2117],
+ [2, 2, -1, 0, -700, 2354],
+ [2, 1, -2, 0, 691, 0],
+ [2, -1, 0, -2, 596, 0],
+ [4, 0, 1, 0, 549, -1423],
+ [0, 0, 4, 0, 537, -1117],
+ [4, -1, 0, 0, 520, -1571],
+ [1, 0, -2, 0, -487, -1739],
+ [2, 1, 0, -2, -399, 0],
+ [0, 0, 2, -2, -381, -4421],
+ [1, 1, 1, 0, 351, 0],
+ [3, 0, -2, 0, -340, 0],
+ [4, 0, -3, 0, 330, 0],
+ [2, -1, 2, 0, 327, 0],
+ [0, 2, 1, 0, -323, 1165],
+ [1, 1, -1, 0, 299, 0],
+ [2, 0, 3, 0, 294, 0],
+ [2, 0, -1, -2, 0, 8752],
];
// Meeus table 47.B: D, M, M', F, latitude (1e-6°).
const MOON_B: number[][] = [
@@ -134,6 +168,49 @@ const MOON_B: number[][] = [
[2, -1, -1, 1, 2463],
[2, -1, 0, 1, 2211],
[2, -1, -1, -1, 2065],
+ [0, 1, -1, -1, -1870],
+ [4, 0, -1, -1, 1828],
+ [0, 1, 0, 1, -1794],
+ [0, 0, 0, 3, -1749],
+ [0, 1, -1, 1, -1565],
+ [1, 0, 0, 1, -1491],
+ [0, 1, 1, 1, -1475],
+ [0, 1, 1, -1, -1410],
+ [0, 1, 0, -1, -1344],
+ [1, 0, 0, -1, -1335],
+ [0, 0, 3, 1, 1107],
+ [4, 0, 0, -1, 1021],
+ [4, 0, -1, 1, 833],
+ [0, 0, 1, -3, 777],
+ [4, 0, -2, 1, 671],
+ [2, 0, 0, -3, 607],
+ [2, 0, 2, -1, 596],
+ [2, -1, 1, -1, 491],
+ [2, 0, -2, 1, -451],
+ [0, 0, 3, -1, 439],
+ [2, 0, 2, 1, 422],
+ [2, 0, -3, -1, 421],
+ [2, 1, -1, 1, -366],
+ [2, 1, 0, 1, -351],
+ [4, 0, 0, 1, 331],
+ [2, -1, 1, 1, 315],
+ [2, -2, 0, -1, 302],
+ [0, 0, 1, 3, -283],
+ [2, 1, 1, -1, -229],
+ [1, 1, 0, -1, 223],
+ [1, 1, 0, 1, 223],
+ [0, 1, -2, -1, -220],
+ [2, 1, -1, -1, -220],
+ [1, 0, 1, 1, -185],
+ [2, -1, -2, -1, 181],
+ [0, 1, 2, 1, -177],
+ [4, 0, -2, -1, 176],
+ [4, -1, -1, -1, 166],
+ [1, 0, 1, -1, -164],
+ [4, 0, 1, -1, 132],
+ [1, 0, -1, -1, -119],
+ [4, -1, 0, -1, 115],
+ [2, -2, 0, 1, 107],
];
/** Geocentric ecliptic longitude, latitude (radians) and distance (km). */
@@ -172,7 +249,8 @@ export function moonEcliptic(t: number): [number, number, number] {
return [(Lp + sl / 1e6) * RAD, (sb / 1e6) * RAD, 385000.56 + sr / 1000];
}
-/** Geocentric ecliptic longitude of the sun (radians). */
+/** Apparent geocentric ecliptic longitude of the sun (radians), less the
+ * nutation the moon also leaves out. */
function sunLongitude(t: number): number {
const L0 = 280.46646 + 36000.76983 * t;
const M = (357.52911 + 35999.05029 * t) * RAD;
@@ -180,7 +258,176 @@ function sunLongitude(t: number): number {
(1.914602 - 0.004817 * t) * Math.sin(M) +
(0.019993 - 0.000101 * t) * Math.sin(2 * M) +
0.000289 * Math.sin(3 * M);
- return (L0 + C) * RAD;
+ // Aberration: the sun appears 20.5″ behind where it is.
+ return (L0 + C - 20.4898 / 3600 + perturbation(t) / 3600) * RAD;
+}
+
+/** The pull of the moon, Venus, Mars and Jupiter on the earth (and the
+ * formula's own drift), in arcseconds of solar longitude. Fitted to VSOP87
+ * via astronomy-engine over 1990–2060: the formula above alone is out by up
+ * to 39″, this leaves 14″ over 1900–2100, which keeps eclipses right. */
+function perturbation(t: number): number {
+ const e = (100.464 + 35999.372 * t) * RAD;
+ const v = e - (181.979 + 58517.815 * t) * RAD;
+ const j = e - (34.396 + 3034.746 * t) * RAD;
+ const ma = e - (-4.553 + 19140.303 * t) * RAD;
+ const d = (297.8501921 + 445267.1114034 * t) * RAD;
+ const m = (357.5291 + 35999.0503 * t) * RAD;
+ const om = (125.04 - 1934.136 * t) * RAD;
+ return (
+ -8.51 +
+ 6.46 * Math.sin(d) -
+ 4.82 * Math.sin(v) +
+ 5.51 * Math.sin(2 * v) +
+ 0.64 * Math.sin(3 * v) -
+ 7.37 * Math.sin(j) +
+ 2.73 * Math.sin(2 * j) -
+ 2.12 * Math.sin(2 * ma) -
+ 2.1 * Math.sin(om) -
+ 0.68 * Math.sin(j - m) -
+ 2.53 * Math.cos(j - m) -
+ 3.01 * Math.sin(m) -
+ 1.19 * Math.cos(m)
+ );
+}
+
+// JPL, "Approximate Positions of the Planets" (E. M. Standish), table 1:
+// a (au), e, I, L, long. perihelion, long. node (degrees) at J2000, then
+// their rates per Julian century. J2000 ecliptic and equinox.
+const ORBITS: Record = {
+ mercury: [
+ 0.38709927, 0.20563593, 7.00497902, 252.2503235, 77.45779628, 48.33076593,
+ 0.00000037, 0.00001906, -0.00594749, 149472.67411175, 0.16047689,
+ -0.12534081,
+ ],
+ venus: [
+ 0.72333566, 0.00677672, 3.39467605, 181.9790995, 131.60246718, 76.67984255,
+ 0.0000039, -0.00004107, -0.0007889, 58517.81538729, 0.00268329, -0.27769418,
+ ],
+ earth: [
+ 1.00000261, 0.01671123, -0.00001531, 100.46457166, 102.93768193, 0,
+ 0.00000562, -0.00004392, -0.01294668, 35999.37244981, 0.32327364, 0,
+ ],
+ mars: [
+ 1.52371034, 0.0933941, 1.84969142, -4.55343205, -23.94362959, 49.55953891,
+ 0.00001847, 0.00007882, -0.00813131, 19140.30268499, 0.44441088,
+ -0.29257343,
+ ],
+ jupiter: [
+ 5.202887, 0.04838624, 1.30439695, 34.39644051, 14.72847983, 100.47390909,
+ -0.00011607, -0.00013253, -0.00183714, 3034.74612775, 0.21252668,
+ 0.20469106,
+ ],
+ saturn: [
+ 9.53667594, 0.05386179, 2.48599187, 49.95424423, 92.59887831, 113.66242448,
+ -0.0012506, -0.00050991, 0.00193609, 1222.49362201, -0.41897216,
+ -0.28867794,
+ ],
+};
+
+/** Heliocentric J2000 ecliptic position (au) from ORBITS. */
+function heliocentric(el: number[], t: number): Vec3 {
+ const [a, e, i, l, peri, node] = [0, 1, 2, 3, 4, 5].map(
+ (k) => el[k]! + el[k + 6]! * t,
+ ) as [number, number, number, number, number, number];
+ const m = ((((l - peri) % 360) + 540) % 360) - 180;
+ const w = (peri - node) * RAD;
+ const M = m * RAD;
+ let E = M + e * Math.sin(M);
+ for (let k = 0; k < 6; k++) {
+ E -= (E - e * Math.sin(E) - M) / (1 - e * Math.cos(E));
+ }
+ const xp = a * (Math.cos(E) - e);
+ const yp = a * Math.sqrt(1 - e * e) * Math.sin(E);
+ const [cw, sw] = [Math.cos(w), Math.sin(w)];
+ const [cn, sn] = [Math.cos(node * RAD), Math.sin(node * RAD)];
+ const [ci, si] = [Math.cos(i * RAD), Math.sin(i * RAD)];
+ return [
+ (cw * cn - sw * sn * ci) * xp + (-sw * cn - cw * sn * ci) * yp,
+ (cw * sn + sw * cn * ci) * xp + (-sw * sn + cw * cn * ci) * yp,
+ sw * si * xp + cw * si * yp,
+ ];
+}
+
+// Saturn's north pole (J2000 equatorial), for the tilt of its rings.
+const SATURN_POLE: Vec3 = [0.0855, 0.0732, 0.9936];
+
+/** Apparent magnitude from distances to the sun (r) and earth (d), the
+ * phase angle i (degrees) and, for Saturn, the ring tilt (Meeus 41). */
+const MAGNITUDE: Record number> = {
+ mercury: (i) => -0.42 + 0.038 * i - 0.000273 * i * i + 0.000002 * i ** 3,
+ venus: (i) => -4.4 + 0.0009 * i + 0.000239 * i * i - 0.00000065 * i ** 3,
+ mars: (i) => -1.52 + 0.016 * i,
+ jupiter: (i) => -9.4 + 0.005 * i,
+ saturn: (i, ring) =>
+ -8.88 + 0.044 * i - 2.6 * Math.abs(ring) + 1.25 * ring * ring,
+};
+// B − V colour index, as in the star catalogue.
+const COLOUR: Record = {
+ mercury: 0.93,
+ venus: 0.82,
+ mars: 1.36,
+ jupiter: 0.83,
+ saturn: 1.04,
+};
+const EPS0 = 23.43928 * RAD;
+
+/** Planet names in the order planets() lists them. */
+export const PLANETS = ["Mercury", "Venus", "Mars", "Jupiter", "Saturn"];
+
+/** The naked-eye planets as star records: J2000 equatorial direction, then
+ * magnitude and colour (see starData in the worker). */
+export function planets(t: number): Float32Array {
+ const earth = heliocentric(ORBITS.earth!, t);
+ const sunDist = Math.hypot(...earth);
+ const names = PLANETS.map((name) => name.toLowerCase());
+ const out = new Float32Array(names.length * 5);
+ names.forEach((name, k) => {
+ const p = heliocentric(ORBITS[name]!, t);
+ const g: Vec3 = [p[0] - earth[0], p[1] - earth[1], p[2] - earth[2]];
+ const r = Math.hypot(...p);
+ const d = Math.hypot(...g);
+ const i =
+ Math.acos(
+ Math.min(
+ 1,
+ Math.max(-1, (r * r + d * d - sunDist * sunDist) / (2 * r * d)),
+ ),
+ ) / RAD;
+ const [ce, se] = [Math.cos(EPS0), Math.sin(EPS0)];
+ const eq: Vec3 = [
+ g[0] / d,
+ (ce * g[1] - se * g[2]) / d,
+ (se * g[1] + ce * g[2]) / d,
+ ];
+ const ring = -(
+ eq[0] * SATURN_POLE[0] +
+ eq[1] * SATURN_POLE[1] +
+ eq[2] * SATURN_POLE[2]
+ );
+ const mag = MAGNITUDE[name]!(i, ring) + 5 * Math.log10(r * d);
+ out.set([...eq, mag, COLOUR[name]!], k * 5);
+ });
+ return out;
+}
+
+const smoothstep = (a: number, b: number, x: number) => {
+ const k = Math.min(1, Math.max(0, (x - a) / (b - a)));
+ return k * k * (3 - 2 * k);
+};
+
+/** Fraction of a disc of radius a left uncovered by a disc of radius b whose
+ * centre is d away (small angles, so flat geometry). */
+function uncovered(a: number, b: number, d: number): number {
+ if (d >= a + b) return 1;
+ if (d <= b - a) return 0;
+ if (d <= a - b) return 1 - (b * b) / (a * a);
+ const p = Math.acos((d * d + a * a - b * b) / (2 * d * a));
+ const q = Math.acos((d * d + b * b - a * a) / (2 * d * b));
+ const lens =
+ a * a * (p - Math.sin(p) * Math.cos(p)) +
+ b * b * (q - Math.sin(q) * Math.cos(q));
+ return 1 - lens / (Math.PI * a * a);
}
const normalize = (v: Vec3): Vec3 => {
@@ -197,13 +444,26 @@ export type Sky = {
/** Lunar north (ecliptic pole is within 1.5° of it). */
moonNorth: Vec3;
sun: Vec3;
- /** 0..1: how much the moon lights the sky (phase and altitude). */
+ /** Sun's angular radius, and the fraction of its disc the moon leaves
+ * uncovered (1 outside a solar eclipse). */
+ sunRadius: number;
+ sunVisible: number;
+ /** Earth's shadow at the moon, in moon radii: the shadow axis relative to
+ * the moon's centre (world frame), then the umbra and penumbra radii. */
+ shadow: Vec3;
+ umbra: number;
+ penumbra: number;
+ /** 0..1: how much the moon lights the sky (phase, altitude, eclipse). */
moonlight: number;
+ /** The planets as star records, drawn with the stars matrix. */
+ planets: Float32Array;
};
export function sky(site: Site, ms: number): Sky {
const jd = julian(ms);
- const t = (jd - 2451545) / 36525;
+ // Julian centuries of terrestrial time for the orbits; sidereal time uses
+ // the clock.
+ const t = (jd + DELTA_T / 86400 - 2451545) / 36525;
const gmst =
(280.46061837 + 360.98564736629 * (jd - 2451545) + 0.000387933 * t * t) *
RAD;
@@ -228,17 +488,52 @@ export function sky(site: Site, ms: number): Sky {
const sun = normalize(apply(hor, ecliptic(sunLongitude(t), 0, eps)));
const moonNorth = normalize(apply(hor, [0, -Math.sin(eps), Math.cos(eps)]));
+ const dot = (a: Vec3, b: Vec3) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
+ const moonRadius = Math.asin(1737.4 / range);
+
+ // Solar eclipse: how much of the sun's disc the moon leaves showing.
+ const m = (357.52911 + 35999.05029 * t) * RAD;
+ const au = 1.00014 - 0.01671 * Math.cos(m) - 0.00014 * Math.cos(2 * m);
+ const sunRadius = (959.63 / au) * ARCSEC;
+ const sunVisible = uncovered(
+ sunRadius,
+ moonRadius,
+ Math.acos(Math.min(1, dot(sun, moon))),
+ );
+
+ // Lunar eclipse: Earth's shadow cone where the moon crosses it. The
+ // atmosphere widens the earth by 1/85 (Danjon's rule, as eclipse tables use).
+ const mw = apply(hor, geo);
+ const behind = -dot(mw, sun);
+ const shadow = mw.map((c, i) => (sun[i]! * -behind - c) / 1737.4) as Vec3;
+ const sunKm = au * 149597870.7;
+ // Radii below zero: the moon is on the sun's side, out of the shadow.
+ const earth = 6367.5 * (1 + 1 / 85);
+ const umbra =
+ behind > 0 ? (earth - (behind * (696000 - 6367.5)) / sunKm) / 1737.4 : -2;
+ const penumbra =
+ behind > 0 ? (earth + (behind * (696000 + 6367.5)) / sunKm) / 1737.4 : -1;
+ // Sunlight left at the centre of the moon's face.
+ const q = moon.map((c, i) => -c - shadow[i]!) as Vec3;
+ const off = q.map((c, i) => c - sun[i]! * dot(q, sun)) as Vec3;
+ const sunlit = smoothstep(umbra, penumbra, Math.hypot(...off));
+
// Illuminated fraction from the sun–moon angle, faded by moon altitude.
- const lit =
- (1 - (sun[0] * moon[0] + sun[1] * moon[1] + sun[2] * moon[2])) / 2;
+ const lit = (1 - dot(sun, moon)) / 2;
const alt = Math.asin(moon[1]) / RAD;
const up = Math.min(1, Math.max(0, (alt + 2) / 17));
return {
stars: mul(hor, precession(t)),
moon,
- moonRadius: Math.asin(1737.4 / range),
+ moonRadius,
moonNorth,
sun,
- moonlight: lit ** 1.5 * up * up * (3 - 2 * up),
+ sunRadius,
+ sunVisible,
+ shadow,
+ umbra,
+ penumbra,
+ moonlight: lit ** 1.5 * up * up * (3 - 2 * up) * sunlit,
+ planets: planets(t),
};
}
diff --git a/apps/site/src/scripts/aurora.worker.ts b/apps/site/src/scripts/aurora.worker.ts
index c28ee98..647d0e2 100644
--- a/apps/site/src/scripts/aurora.worker.ts
+++ b/apps/site/src/scripts/aurora.worker.ts
@@ -3,8 +3,10 @@
// Passes per frame:
// 1. Footprint: each curtain's ground track and brightness into a small
// texture (see aurora.shaders.ts).
-// 2. Sky: a fullscreen march through the aurora in perspective, plus the moon.
-// 3. Stars from the Bright Star Catalogue, then the sparks, added on top.
+// 2. Sky: a fullscreen march through the aurora in perspective, plus the moon
+// and, when it is up, the sun and its ice halos.
+// 3. Stars from the Bright Star Catalogue and the planets, then the sparks,
+// added on top.
// 4. The mountain skyline, drawn last so it hides everything behind it.
// The page sends the theme token values, sizes and visibility; we answer "on"
// once a frame is up and "off" if we give up.
@@ -34,8 +36,9 @@ const MAX_EDGE = 1280;
const SPARKS = 4000;
const RAD = Math.PI / 180;
-// Wall-clock epoch milliseconds from Temporal (via gmt): the stars and moon
-// need the real time. Frame timing uses rAF stamps, not the wall clock.
+// Wall-clock epoch milliseconds from Temporal (via gmt): the stars, planets,
+// moon and sun need the real time. Frame timing uses rAF stamps, not the wall
+// clock.
let wallMs = 0;
const wall = () => (wallMs = getUnixNow() ?? wallMs);
@@ -272,6 +275,7 @@ function start(init: Extract): void {
const seeds = buffer(Float32Array.from({ length: SPARKS * 4 }, Math.random));
const ridgeBuf = gl.createBuffer();
let ridgeCount = 0;
+ const planetBuf = gl.createBuffer();
let starBuf: WebGLBuffer | null = null;
let starCount = 0;
let starsAt = 0;
@@ -370,6 +374,7 @@ function start(init: Extract): void {
let tuning = true;
let skyAt = wall();
let now: Sky = sky(site, skyAt);
+ let planetsAt = 0;
const resize = () => {
const w = width || 1;
@@ -392,6 +397,12 @@ function start(init: Extract): void {
if (wall() - skyAt > 1000) {
skyAt = wallMs;
now = sky(site, skyAt);
+ planetsAt = 0;
+ }
+ if (!planetsAt) {
+ planetsAt = skyAt;
+ gl.bindBuffer(gl.ARRAY_BUFFER, planetBuf);
+ gl.bufferData(gl.ARRAY_BUFFER, now.planets, gl.DYNAMIC_DRAW);
}
const tan = [tanY * (canvas.width / canvas.height), tanY];
const px = Math.max(1, canvas.height / 540);
@@ -430,25 +441,40 @@ function start(init: Extract): void {
gl.uniform4f(at(sp, "uMoon"), ...now.moon, now.moonRadius);
gl.uniform3fv(at(sp, "uMoonN"), now.moonNorth);
gl.uniform3fv(at(sp, "uSun"), now.sun);
+ gl.uniform2f(at(sp, "uSunDisc"), now.sunRadius, now.sunVisible);
+ gl.uniform3fv(at(sp, "uShadow"), now.shadow);
+ gl.uniform2f(at(sp, "uShadowR"), now.umbra, now.penumbra);
quad(sp);
// 3. Stars and sparks, added on top.
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE);
+ view(stars);
+ gl.uniformMatrix3fv(at(stars, "uStars"), false, columns(now.stars));
+ gl.uniform1f(at(stars, "uLimit"), site.limit - 2 * now.moonlight);
+ const points = (
+ buf: WebGLBuffer | null,
+ count: number,
+ fade: number,
+ steady: number,
+ ) => {
+ gl.uniform1f(at(stars, "uFade"), fade);
+ gl.uniform1f(at(stars, "uSteady"), steady);
+ const a = attrib(stars, "aDir", buf, 3, 20, 0);
+ const b = attrib(stars, "aMag", buf, 2, 20, 12);
+ gl.drawArrays(gl.POINTS, 0, count);
+ gl.disableVertexAttribArray(a);
+ gl.disableVertexAttribArray(b);
+ };
if (starBuf && starCount) {
- view(stars);
- gl.uniformMatrix3fv(at(stars, "uStars"), false, columns(now.stars));
- gl.uniform1f(at(stars, "uLimit"), site.limit - 2 * now.moonlight);
- gl.uniform1f(
- at(stars, "uFade"),
+ points(
+ starBuf,
+ starCount,
Math.min(1, (performance.now() - starsAt) / 1500),
+ 0,
);
- const a = attrib(stars, "aDir", starBuf, 3, 20, 0);
- const b = attrib(stars, "aMag", starBuf, 2, 20, 12);
- gl.drawArrays(gl.POINTS, 0, starCount);
- gl.disableVertexAttribArray(a);
- gl.disableVertexAttribArray(b);
}
+ points(planetBuf, now.planets.length / 5, 1, 1);
view(sparks);
const s = attrib(sparks, "aSeed", seeds, 4);
gl.drawArrays(gl.POINTS, 0, sparkCount);
@@ -542,6 +568,7 @@ function start(init: Extract): void {
place(data.site);
skyAt = wall();
now = sky(site, skyAt);
+ planetsAt = 0;
draw();
scope.postMessage("on");
} else if (data.type === "size") {
From eb9ea586ce9109f13786cf56ad620a78aa9593bf Mon Sep 17 00:00:00 2001
From: "Craig C."
Date: Thu, 22 Oct 2026 22:34:17 +0300
Subject: [PATCH 3/6] build: IAU star names for the sky's tooltips
scripts/build-stars.mjs also writes apps/site/public/star-names.json: the
IAU Catalog of Star Names (WGSN) matched to the Bright Star Catalogue by HR
number, as [record index, name, constellation]. 324 named stars, 3 KB
gzipped. stars.bin is unchanged byte for byte.
---
apps/site/public/star-names.json | 1 +
scripts/build-stars.mjs | 130 +++++++++++++++++++++++++++++++
2 files changed, 131 insertions(+)
create mode 100644 apps/site/public/star-names.json
diff --git a/apps/site/public/star-names.json b/apps/site/public/star-names.json
new file mode 100644
index 0000000..c725a2f
--- /dev/null
+++ b/apps/site/public/star-names.json
@@ -0,0 +1 @@
+[[1,"Alpheratz","Andromeda"],[2,"Caph","Cassiopeia"],[11,"Algenib","Pegasus"],[25,"Ankaa","Phoenix"],[43,"Fulu","Cassiopeia"],[50,"Schedar","Cassiopeia"],[56,"Diphda","Cetus"],[67,"Achird","Cassiopeia"],[82,"Castula","Cassiopeia"],[104,"Mirach","Andromeda"],[105,"Wurren","Phoenix"],[111,"Revati","Pisces"],[120,"Adhil","Andromeda"],[123,"Ruchbah","Cassiopeia"],[129,"Polaris","Ursa Minor"],[134,"Alpherg","Pisces"],[138,"Titawin","Andromeda"],[140,"Nembus","Andromeda"],[142,"Achernar","Eridanus"],[152,"Torcular","Pisces"],[158,"Baten Kaitos","Cetus"],[159,"Segin","Cassiopeia"],[161,"Mothallah","Triangulum"],[163,"Mesarthim","Aries"],[166,"Sheratan","Aries"],[186,"Alrescha","Pisces"],[189,"Almach","Andromeda"],[194,"Hamal","Aries"],[210,"Mira","Cetus"],[250,"Kaffaljidhma","Cetus"],[257,"Lilii Borea","Aries"],[258,"Miram","Perseus"],[261,"Bharani","Aries"],[267,"Angetenar","Eridanus"],[275,"Azha","Eridanus"],[287,"Acamar","Eridanus"],[290,"Menkar","Cetus"],[300,"Algol","Perseus"],[302,"Misam","Perseus"],[304,"Botein","Aries"],[307,"Dalim","Fornax"],[311,"Zibal","Eridanus"],[328,"Mirfak","Perseus"],[344,"Ran","Eridanus"],[354,"Atik","Perseus"],[357,"Rana","Eridanus"],[359,"Celaeno","Taurus"],[360,"Electra","Taurus"],[362,"Taygeta","Taurus"],[365,"Maia","Taurus"],[368,"Merope","Taurus"],[370,"Alcyone","Taurus"],[376,"Atlas","Taurus"],[377,"Pleione","Taurus"],[395,"Menkib","Perseus"],[397,"Zaurak","Eridanus"],[420,"Beid","Eridanus"],[433,"Keid","Eridanus"],[445,"Prima Hyadum","Taurus"],[454,"Secunda Hyadum","Taurus"],[463,"Beemim","Eridanus"],[468,"Ain","Taurus"],[470,"Chamukuy","Taurus"],[482,"Aldebaran","Taurus"],[485,"Theemin","Eridanus"],[495,"Sceptrum","Eridanus"],[515,"Tabit","Orion"],[529,"Hassaleh","Auriga"],[535,"Almaaz","Auriga"],[538,"Saclateni","Auriga"],[547,"Haedus","Auriga"],[558,"Cursa","Eridanus"],[574,"Capella","Auriga"],[576,"Rigel","Orion"],[605,"Bellatrix","Orion"],[606,"Elnath","Taurus"],[614,"Nihal","Lepus"],[622,"Mintaka","Orion"],[627,"Arneb","Lepus"],[633,"Meissa","Orion"],[638,"Hatysa","Orion"],[640,"Alnilam","Orion"],[642,"Tianguan","Taurus"],[652,"Alnitak","Orion"],[656,"Phact","Columba"],[669,"Saiph","Orion"],[683,"Wazn","Columba"],[688,"Betelgeuse","Orion"],[695,"Menkalinan","Auriga"],[699,"Mahasim","Auriga"],[723,"Elkurud","Columba"],[734,"Propus","Gemini"],[753,"Furud","Canis Major"],[754,"Tejat","Gemini"],[757,"Mirzam","Canis Major"],[761,"Canopus","Carina"],[784,"Alhena","Gemini"],[798,"Mebsuta","Gemini"],[803,"Alzirr","Gemini"],[806,"Sirius","Canis Major"],[843,"Adhara","Canis Major"],[847,"Nganurganity","Canis Major"],[850,"Mekbuda","Gemini"],[853,"Muliphein","Canis Major"],[862,"Wezen","Canis Major"],[892,"Wasat","Gemini"],[915,"Aludra","Canis Major"],[918,"Gomeisa","Canis Minor"],[936,"Castor","Gemini"],[943,"Jishui","Gemini"],[947,"Procyon","Canis Minor"],[960,"Pollux","Gemini"],[980,"Azmidi","Puppis"],[1015,"Naos","Puppis"],[1021,"Tureis","Puppis"],[1040,"Tarf","Cancer"],[1044,"Alsciaukat","Lynx"],[1051,"Avior","Carina"],[1056,"Muscida","Ursa Major"],[1074,"Minchir","Hydra"],[1089,"Asellus Borealis","Cancer"],[1096,"Asellus Australis","Cancer"],[1102,"Ashlesha","Hydra"],[1104,"Alsephina","Vela"],[1127,"Talitha","Ursa Major"],[1129,"Acubens","Cancer"],[1137,"Alkaphrah","Ursa Major"],[1151,"Nahn","Cancer"],[1153,"Suhail","Vela"],[1173,"Miaplacidus","Carina"],[1179,"Aspidiske","Carina"],[1190,"Markeb","Vela"],[1191,"Intercrus","Ursa Major"],[1192,"Alphard","Hydra"],[1204,"Alterf","Leo"],[1231,"Ukdah","Hydra"],[1233,"Subra","Leo"],[1251,"Zhang","Hydra"],[1252,"Rasalas","Leo"],[1257,"Felis","Hydra"],[1271,"Regulus","Leo"],[1280,"Adhafera","Leo"],[1281,"Tania Borealis","Ursa Major"],[1288,"Algieba","Leo"],[1292,"Tania Australis","Ursa Major"],[1358,"Praecipua","Leo Minor"],[1367,"Chalawan","Ursa Major"],[1369,"Alkes","Crater"],[1374,"Merak","Ursa Major"],[1377,"Dubhe","Ursa Major"],[1393,"Zosma","Leo"],[1394,"Chertan","Leo"],[1400,"Alula Australis","Ursa Major"],[1401,"Alula Borealis","Ursa Major"],[1421,"Giausar","Draco"],[1451,"Taiyangshou","Ursa Major"],[1462,"Denebola","Leo"],[1465,"Zavijava","Virgo"],[1469,"Phecda","Ursa Major"],[1489,"Alchiba","Corvus"],[1496,"Imai","Crux"],[1497,"Megrez","Ursa Major"],[1498,"Gienah","Corvus"],[1507,"Zaniah","Virgo"],[1513,"Ginan","Crux"],[1522,"Acrux","Crux"],[1533,"Algorab","Corvus"],[1535,"Gacrux","Crux"],[1543,"Chara","Canes Venatici"],[1544,"Kraz","Corvus"],[1559,"Porrima","Virgo"],[1567,"La Superba","Canes Venatici"],[1571,"Mimosa","Crux"],[1572,"Tianyi","Draco"],[1584,"Alioth","Ursa Major"],[1587,"Minelauva","Virgo"],[1589,"Cor Caroli","Canes Venatici"],[1590,"Taiyi","Draco"],[1596,"Vindemiatrix","Virgo"],[1605,"Diadem","Coma Berenices"],[1633,"Mizar","Ursa Major"],[1635,"Spica","Virgo"],[1637,"Alcor","Ursa Major"],[1648,"Heze","Virgo"],[1668,"Alkaid","Ursa Major"],[1681,"Muphrid","Boötes"],[1691,"Hadar","Centaurus"],[1694,"Menkent","Centaurus"],[1696,"Thuban","Draco"],[1705,"Kang","Virgo"],[1713,"Syrma","Virgo"],[1715,"Arcturus","Boötes"],[1718,"Xuange","Boötes"],[1721,"Khambalia","Virgo"],[1739,"Elgafar","Virgo"],[1746,"Seginus","Boötes"],[1751,"Rigil Kentaurus","Centaurus"],[1752,"Toliman","Centaurus"],[1771,"Izar","Boötes"],[1778,"Zubenelgenubi","Libra"],[1790,"Kochab","Ursa Minor"],[1801,"Nekkar","Boötes"],[1802,"Brachium","Libra"],[1828,"Zubeneschamali","Libra"],[1846,"Alkalurops","Boötes"],[1847,"Pherkad","Ursa Minor"],[1852,"Edasich","Draco"],[1854,"Nusakan","Corona Borealis"],[1866,"Zubenelhakrabi","Libra"],[1869,"Alphecca","Corona Borealis"],[1890,"Unukalhai","Serpens"],[1894,"Gudja","Serpens"],[1917,"Iklil","Scorpius"],[1924,"Fang","Scorpius"],[1927,"Dschubba","Scorpius"],[1943,"Acrab","Scorpius"],[1950,"Marsic","Hercules"],[1959,"Jabbah","Scorpius"],[1968,"Yed Prior","Ophiuchus"],[1974,"Yed Posterior","Ophiuchus"],[1979,"Alniyat","Scorpius"],[1997,"Cujam","Hercules"],[2004,"Athebyne","Draco"],[2005,"Antares","Scorpius"],[2013,"Kornephoros","Hercules"],[2014,"Marfik","Ophiuchus"],[2022,"Paikauhale","Scorpius"],[2032,"Atria","Triangulum Australe"],[2040,"Larawag","Scorpius"],[2043,"Xamidimura","Scorpius"],[2045,"Pipirima","Scorpius"],[2072,"Sabik","Ophiuchus"],[2076,"Aldhibah","Draco"],[2078,"Guniibuu","Ophiuchus"],[2079,"Rasalgethi","Hercules"],[2081,"Sarin","Hercules"],[2113,"Lesath","Scorpius"],[2117,"Maasym","Hercules"],[2118,"Shaula","Scorpius"],[2119,"Rastaban","Draco"],[2123,"Sargas","Scorpius"],[2126,"Rasalhague","Ophiuchus"],[2136,"Cervantes","Ara"],[2140,"Cebalrai","Ophiuchus"],[2146,"Fuyue","Scorpius"],[2148,"Dziban","Draco"],[2157,"Grumium","Draco"],[2164,"Eltanin","Draco"],[2181,"Alnasl","Sagittarius"],[2192,"Yildun","Ursa Minor"],[2198,"Polis","Sagittarius"],[2211,"Kaus Media","Sagittarius"],[2221,"Kaus Australis","Sagittarius"],[2232,"Kaus Borealis","Sagittarius"],[2245,"Fafnir","Draco"],[2260,"Vega","Lyra"],[2286,"Sheliak","Lyra"],[2289,"Ainalrami","Sagittarius"],[2293,"Nunki","Sagittarius"],[2303,"Alya","Serpens"],[2315,"Sulafat","Lyra"],[2321,"Ascella","Sagittarius"],[2333,"Polaris Australis","Octans"],[2335,"Okab","Aquila"],[2340,"Meridiana","Corona Australis"],[2344,"Albaldah","Sagittarius"],[2351,"Aladfar","Lyra"],[2355,"Altais","Draco"],[2365,"Arkab Prior","Sagittarius"],[2368,"Arkab Posterior","Sagittarius"],[2369,"Rukbat","Sagittarius"],[2381,"Anser","Vulpecula"],[2383,"Albireo","Cygnus"],[2398,"Alsafi","Draco"],[2403,"Sham","Sagitta"],[2415,"Tarazed","Aquila"],[2416,"Fawaris","Cygnus"],[2423,"Altair","Aquila"],[2435,"Libertas","Aquila"],[2436,"Terebellum","Sagittarius"],[2437,"Alshain","Aquila"],[2489,"Algedi","Capricornus"],[2494,"Alshat","Capricornus"],[2495,"Dabih","Capricornus"],[2496,"Peacock","Pavo"],[2498,"Sadr","Cygnus"],[2509,"Aldulfin","Delphinus"],[2519,"Rotanev","Delphinus"],[2530,"Sualocin","Delphinus"],[2533,"Deneb","Cygnus"],[2544,"Aljanah","Cygnus"],[2545,"Albali","Aquarius"],[2569,"Musica","Delphinus"],[2595,"Kitalpha","Equuleus"],[2603,"Alderamin","Cepheus"],[2622,"Sadalsuud","Aquarius"],[2623,"Alfirk","Cepheus"],[2628,"Bunda","Aquarius"],[2632,"Nashira","Capricornus"],[2640,"Azelfafage","Cygnus"],[2643,"Enif","Pegasus"],[2651,"Deneb Algedi","Capricornus"],[2659,"Aldhanab","Grus"],[2672,"Sadalmelik","Aquarius"],[2674,"Kurhah","Cepheus"],[2677,"Alnair","Grus"],[2685,"Biham","Pegasus"],[2705,"Ancha","Aquarius"],[2712,"Sadachbia","Aquarius"],[2741,"Situla","Aquarius"],[2750,"Homam","Pegasus"],[2751,"Tiaki","Grus"],[2756,"Matar","Pegasus"],[2765,"Sadalbari","Pegasus"],[2774,"Skat","Aquarius"],[2778,"Fomalhaut","Piscis Austrinus"],[2779,"Helvetios","Pegasus"],[2788,"Fumalsamakah","Pisces"],[2790,"Scheat","Pegasus"],[2793,"Markab","Pegasus"],[2824,"Salm","Pegasus"],[2830,"Alkarab","Pegasus"],[2836,"Veritate","Andromeda"],[2848,"Errai","Cepheus"]]
\ No newline at end of file
diff --git a/scripts/build-stars.mjs b/scripts/build-stars.mjs
index 5dc3794..279e9b3 100644
--- a/scripts/build-stars.mjs
+++ b/scripts/build-stars.mjs
@@ -6,13 +6,22 @@
// Run: node scripts/build-stars.mjs
// Record, 6 bytes little-endian: ra uint16 (0..65536 = 0..360°),
// dec int16 (±32767 = ±90°), vmag uint8 ((V + 2) × 20), b-v int8 (× 50).
+//
+// Also builds apps/site/public/star-names.json for the sky's tooltips: the
+// IAU Catalog of Star Names (WGSN), matched by HR number, as
+// [record index, name, constellation] for each named star kept above.
import { writeFileSync } from "node:fs";
import { gunzipSync } from "node:zlib";
const SOURCE = "http://tdc-www.harvard.edu/catalogs/bsc5.dat.gz";
const LIMIT = 5.5;
+const NAMES = "https://www.pas.rochester.edu/~emamajek/WGSN/IAU-CSN.txt";
const OUT = new URL("../apps/site/public/stars.bin", import.meta.url);
+const NAMES_OUT = new URL(
+ "../apps/site/public/star-names.json",
+ import.meta.url,
+);
const res = await fetch(SOURCE);
if (!res.ok) throw new Error(`catalogue: HTTP ${res.status}`);
@@ -24,6 +33,8 @@ const text = gunzipSync(Buffer.from(await res.arrayBuffer())).toString(
const field = (line, from, to) => line.slice(from - 1, to).trim();
const stars = [];
+/** Record index by HR (Bright Star) number. */
+const byHr = new Map();
for (const line of text.split("\n")) {
const raH = field(line, 76, 77);
const vmag = field(line, 103, 107);
@@ -43,6 +54,7 @@ for (const line of text.split("\n")) {
Number(field(line, 87, 88)) / 60 +
Number(field(line, 89, 90)) / 3600);
const bv = Number(field(line, 110, 114) || "0.6");
+ byHr.set(Number(field(line, 1, 4)), stars.length);
stars.push({ ra, dec, v, bv });
}
@@ -56,3 +68,121 @@ stars.forEach(({ ra, dec, v, bv }, i) => {
});
writeFileSync(OUT, buf);
console.log(`${stars.length} stars, ${buf.length} bytes`);
+
+// The 88 constellations by IAU abbreviation.
+const CONSTELLATIONS = {
+ And: "Andromeda",
+ Ant: "Antlia",
+ Aps: "Apus",
+ Aqr: "Aquarius",
+ Aql: "Aquila",
+ Ara: "Ara",
+ Ari: "Aries",
+ Aur: "Auriga",
+ Boo: "Boötes",
+ Cae: "Caelum",
+ Cam: "Camelopardalis",
+ Cnc: "Cancer",
+ CVn: "Canes Venatici",
+ CMa: "Canis Major",
+ CMi: "Canis Minor",
+ Cap: "Capricornus",
+ Car: "Carina",
+ Cas: "Cassiopeia",
+ Cen: "Centaurus",
+ Cep: "Cepheus",
+ Cet: "Cetus",
+ Cha: "Chamaeleon",
+ Cir: "Circinus",
+ Col: "Columba",
+ Com: "Coma Berenices",
+ CrA: "Corona Australis",
+ CrB: "Corona Borealis",
+ Crv: "Corvus",
+ Crt: "Crater",
+ Cru: "Crux",
+ Cyg: "Cygnus",
+ Del: "Delphinus",
+ Dor: "Dorado",
+ Dra: "Draco",
+ Equ: "Equuleus",
+ Eri: "Eridanus",
+ For: "Fornax",
+ Gem: "Gemini",
+ Gru: "Grus",
+ Her: "Hercules",
+ Hor: "Horologium",
+ Hya: "Hydra",
+ Hyi: "Hydrus",
+ Ind: "Indus",
+ Lac: "Lacerta",
+ Leo: "Leo",
+ LMi: "Leo Minor",
+ Lep: "Lepus",
+ Lib: "Libra",
+ Lup: "Lupus",
+ Lyn: "Lynx",
+ Lyr: "Lyra",
+ Men: "Mensa",
+ Mic: "Microscopium",
+ Mon: "Monoceros",
+ Mus: "Musca",
+ Nor: "Norma",
+ Oct: "Octans",
+ Oph: "Ophiuchus",
+ Ori: "Orion",
+ Pav: "Pavo",
+ Peg: "Pegasus",
+ Per: "Perseus",
+ Phe: "Phoenix",
+ Pic: "Pictor",
+ Psc: "Pisces",
+ PsA: "Piscis Austrinus",
+ Pup: "Puppis",
+ Pyx: "Pyxis",
+ Ret: "Reticulum",
+ Sge: "Sagitta",
+ Sgr: "Sagittarius",
+ Sco: "Scorpius",
+ Scl: "Sculptor",
+ Sct: "Scutum",
+ Ser: "Serpens",
+ Sex: "Sextans",
+ Tau: "Taurus",
+ Tel: "Telescopium",
+ Tri: "Triangulum",
+ TrA: "Triangulum Australe",
+ Tuc: "Tucana",
+ UMa: "Ursa Major",
+ UMi: "Ursa Minor",
+ Vel: "Vela",
+ Vir: "Virgo",
+ Vol: "Volans",
+ Vul: "Vulpecula",
+};
+
+// Fixed-width columns, located from the header row.
+const namesRes = await fetch(NAMES);
+if (!namesRes.ok) throw new Error(`star names: HTTP ${namesRes.status}`);
+const lines = (await namesRes.text()).split("\n");
+const header = lines.find((l) => l.startsWith("#Name/ASCII")) ?? "";
+const col = (label) => header.indexOf(label);
+const [nameAt, designAt, conAt] = [
+ col("Name/Diacritics"),
+ col("Designation"),
+ col("Con"),
+];
+const names = [];
+for (const line of lines) {
+ if (line.startsWith("#") || line.startsWith("$") || !line.trim()) continue;
+ const hr = /^HR (\d+)/.exec(line.slice(designAt, conAt).trim());
+ const index = hr && byHr.get(Number(hr[1]));
+ if (index === undefined || index === null) continue;
+ const name = line.slice(nameAt, designAt).trim();
+ const con = line.slice(conAt, conAt + 3).trim();
+ names.push([index, name, CONSTELLATIONS[con] ?? con]);
+}
+names.sort((a, b) => a[0] - b[0]);
+const json = JSON.stringify(names);
+writeFileSync(NAMES_OUT, json);
+console.log(`${names.length} star names, ${json.length} bytes`);
From 0ec09de9476364c164653da2a6cd2586751ea2d7 Mon Sep 17 00:00:00 2001
From: "Craig C."
Date: Thu, 22 Oct 2026 22:34:25 +0300
Subject: [PATCH 4/6] feat(site): tooltips name the sun, moon, planets and
stars
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Hover (or tap) near the sun, the moon, a planet or one of the 324 IAU-named
stars over bare sky and a small label names it, with the constellation for
stars: "Capella · Auriga".
The page sends the pointer position to the worker at most once a frame and
the worker answers with the nearest object that is actually drawn: up,
clear of the skyline, bright enough for the site and the moonlight, and in
frame. The main thread only places the label. Star names load after the
stars, and the popover credits the IAU Working Group on Star Names.
---
apps/site/src/components/Aurora.astro | 92 ++++++++++++++++++++++--
apps/site/src/scripts/aurora.sites.ts | 1 +
apps/site/src/scripts/aurora.worker.ts | 96 ++++++++++++++++++++++++--
3 files changed, 178 insertions(+), 11 deletions(-)
diff --git a/apps/site/src/components/Aurora.astro b/apps/site/src/components/Aurora.astro
index 003c2ce..d9492fa 100644
--- a/apps/site/src/components/Aurora.astro
+++ b/apps/site/src/components/Aurora.astro
@@ -32,6 +32,9 @@ import {
+
+
{
@@ -86,7 +89,11 @@ import {