Live sky over Springfield, Missouri
The Living Ozarks
Hover to preview. Click to pin a card. Scrub through the night. Stars, planets, the Sun, the Moon, the river, and the hills all drawn from live data above Springfield.
Tonight above Springfield
computed from the time currently on the slider above. Drag it to preview any moment through tomorrow morning.
No naked-eye planets above the horizon. Scrub the slider toward dusk to bring them back.
Positions from reduced Meeus ephemeris for Springfield, MO.
- Eta Aquariidspeak 05/05dust from Halley's Comet, best in the pre-dawn hourZHR ~50 at peak
Hercules Glades Wilderness
Bortle 3Table Rock Lake (south shore)
Bortle 4Buffalo National River (upper)
Bortle 2Bennett Spring State Park
Bortle 4Roaring River State Park
Bortle 3Live inputs
observer: Springfield, MO · 37.2090° N, 93.2923° W · America/Chicago
Theory of operation
Solar position
Given a UTC timestamp, the canvas computes a Julian day JD = unix_ms / 86400000 + 2440587.5, then the number of days since the J2000.0 epoch: d = JD − 2451545.0. From dwe derive the Sun’s mean anomaly, mean longitude, ecliptic longitude, mean obliquity of the ecliptic, right ascension, and declination, all of them polynomial approximations published by the USNO and NOAA with arc-minute accuracy over the 21st century.
The horizontal coordinates (altitude, azimuth) come from a rotation through the local hour angle, computed from Greenwich Mean Sidereal Time at the given UTC and the observer’s longitude:
sin(alt) = sin(φ) sin(δ) + cos(φ) cos(δ) cos(H)
cos(Az) = (sin(δ) − sin(alt) sin(φ)) / (cos(alt) cos(φ))where φ is the observer latitude, δ the solar declination, and Hthe hour angle. Good to about 0.5°, more than enough to place a 4.5-pixel sun on a 40-pixel mark.
Sunrise, sunset, solar noon
The site uses the NOAA spreadsheet formulation, which is a separable-day-of-year polynomial model of the equation of time and solar declination. For a given date, solar noon in minutes after UTC midnight is:
solarNoonMin = 720 − 4 × longitude − eqtimeSunrise and sunset are solarNoon ∓ 4 × H₀, with H₀ the horizon hour angle after correction for atmospheric refraction and the finite solar disc (-0.833°). The date passed to this routine is the visitor’s current Springfield-localcalendar date, not UTC. Otherwise visitors hitting the site in the evening Central time (early next day UTC) would get tomorrow’s sunrise.
Lunar phase
Every night sky above the hills places the Moon at its actual current phase. The computation is a simple synodic-period model keyed to a known new-moon reference epoch (2000-01-06 18:14 UTC):
phase = ((JD − refJD) / 29.530588853) mod 1
illumination = (1 − cos(2π × phase)) / 2The shadow disc inside the canvas is offset by (1 − 2 × illumination) × 5px, flipping direction across the full-moon midpoint to render waxing vs. waning. Drift between this simple model and the true Moon is under a day over a century. Plenty for a 4-pixel disc.
Weather
Every hour, the server asks Open-Meteo what Springfield looks like right now. Open-Meteo returns a WMO 4677 weather code which this site collapses into five palette buckets:
0, 1 → clear
2, 3 → clouds (partly cloudy, overcast)
45, 48 → mist (fog, rime)
51–67, 80–82 → rain (drizzle, rain, showers)
71–77, 85–86 → snow
95–99 → rain (thunderstorms collapse here)Responses are cached in Upstash Redis for an hour, which trims the outgoing request rate to ~24/day regardless of traffic. Open-Meteo is free, requires no API key, and is fully open-source, picked deliberately so the whole pipeline stays inspectable end to end.
Tonight's stargazing score
Springfield is a Bortle class 5 sky: suburban, with a visible Milky Way on the right night but plenty of scattered urban glow. Whether tonight is one of those right nights comes down to four knobs, and the score is just their product:
moonScore = 1 − 0.85 × moonIlluminated // full moon floors 0.15
cloudScore = 1 − meanCloudCoverPct / 100
bortleScore = 1 − (bortleClass − 1) / 8 // Bortle 5 → 0.5
twilightScore = min(1, astronomicalTwilightHours / 5)
score = round(10 × moonScore × cloudScore × bortleScore × twilightScore, 1)The cloud term is a 12-hour mean of Open-Meteo's cloud_cover hourly field, averaged across the window from civil dusk (sun altitude -6°) to astronomical dawn the next morning (sun altitude -18°). The astronomical-dawn math is the same NOAA spreadsheet formulation used for sunrise and sunset, just with the zenith-angle parameter bumped from 90.833° to 108°.
Because all four terms live in [0, 1], the composite does too; a one-decimal 0 to 10 scale comes from a final rounding step. Above the canvas, the score is the number on the stat card. Inside the canvas, it becomes skyClarity: the count of rendered stars and their alpha ceiling both scale with it, so a murky-moonlit sky on the site means a murky-moonlit sky in the disc.
Climate anomaly (today vs. the 30-year normal)
Sky color shifts subtly rose when Springfield is running warmer than usual for the date, and subtly cool when it’s running under. Both sides come from one number: the difference between today’s forecast high and the climatological mean high for this calendar date over the WMO-standard 1991 to 2020 window.
# 85 years of daily high/low from Open-Meteo Archive, once.
GET https://archive-api.open-meteo.com/v1/archive
?latitude=37.209&longitude=-93.2923
&start_date=1940-01-01&end_date=2024-12-31
&daily=temperature_2m_max,temperature_2m_min
&temperature_unit=fahrenheit&timezone=America%2FChicago
# Group by MM-DD:
# CLIMATE_NORMALS[mmdd] = mean high/low over 1991-2020
# CLIMATE_RECORDS[mmdd] = max-high + min-low with source year, 1940-present
# Commit both to src/lib/climate-normals.ts.At request time, the /api/climate-anomalyroute only fetches today’s forecast high and low (one Open-Meteo call). It subtracts from the static normal, compares to the static record, and caches for 24 hours keyed by Springfield-local MM-DD. By midnight central, the key rolls and a fresh normal gets loaded without any upstream change.
The canvas tint is a clamp: magnitude = min(1, |anomalyF| / 15). Warm anomalies mix skyTop and skyBottom toward a warm rose (#E8A87C), cool anomalies toward a cool slate-blue (#7FA8C9), maxing out at 25% mix when the anomaly reaches 15°F. It’s deliberately too subtle to notice on a calm day, and unmistakable on a freak one.
Ozark hydrology (James River)
The blue line slipping through the hills is the James River at Galena, Missouri (USGS gauge 07052500, about 40 miles downstream of Springfield). Two calls go out of a Next.js route handler on each cache miss, in parallel:
# discharge (code 00060) and gauge height (code 00065), latest
GET https://waterservices.usgs.gov/nwis/iv/?sites=07052500
&format=json¶meterCd=00060,00065&siteStatus=active
# daily-mean discharge, 7 days back, for the trend percent
GET https://waterservices.usgs.gov/nwis/dv/?sites=07052500
&format=json¶meterCd=00060&period=P7DEach discharge reading lands in cfs (cubic feet per second). To say whether today is high or low for the season, the card compares the live value against a static day-of-year median pulled from 30 years of daily values (1994 through 2023), grouped by MM-DD and committed to src/lib/river-normals.ts. That file is regenerated by a one-off script, not re-fetched at runtime, so the route stays fast and the USGS service stays unstressed.
In the disc itself, flow is normalized to the [0, 1] range with a simple clamp against 0.2 × median (typical low) and 3 × median (near bankfull). Opacity and stroke weight scale with that number, and an outer glow turns on above 0.55 to read as flood. Below the Ozark ridges, the water gets wider and brighter when the river is running.
Rendering
~500 lines of TypeScript. Zero runtime dependencies added: pure HTML5 Canvas 2D, no WebGL, no animation library, no particle engine. Draw loop throttles at ~40fps (25ms minimum frame time). Per-session randomness is a mulberry32 PRNG seeded from sessionStorage, so every visit draws a slightly different set of stars and data markers.
The render loop pauses via IntersectionObserver when the logo leaves the viewport, and the browser pauses requestAnimationFrame itself when the tab is hidden. Total payload delta vs. the static SVG it replaced: ~4 KB gzipped.
Accessibility: the source-of-truth SVG lockup stays in the DOM with role="img" and an aria-label. The canvas is aria-hidden and carries no semantic weight. Both the OS-level prefers-reduced-motion: reducemedia query and the site’s own “Reduce motion” toggle unmount the canvas entirely and reveal the static SVG.
Prior art & references
None of this math is new. The references below are the shoulders this page is standing on. Worth a click if any of the above caught your interest.
- suncalc· Vladimir Agafonkin
The JavaScript library for sun/moon math. ~4k stars, battle-tested, tiny. Does everything this page does (and more). If you're building anything production-grade that needs solar or lunar positions, start here.
- Astronomy Engine· Don Cross
High-accuracy TypeScript port of VSOP87 / NOVAS. Arc-second-level precision if you need it. Overkill for a 40-pixel canvas, but the reference when you need real ephemeris data.
- NOAA Solar Calculator· NOAA GML
The original government spreadsheet this site's sunrise/sunset math comes from. NOAA publishes the coefficients and the derivation; this is canonical.
- Open-Meteo· Patrick Zippenfenig · Bremen
Free, open-source weather API. No key, no rate-limit drama, sensible WMO codes. The weather tint you see in the logo is pulling from them right now.
- USGS Water Services· U.S. Geological Survey
Public REST + JSON interface to every USGS stream gauge in the country. Zero auth, zero rate limits worth mentioning, and enough history to do proper day-of-year statistics. The James River flow card, the normalized-flow bar, and the river line drifting through the hills all come from this feed.
- Bortle Dark-Sky Scale· John E. Bortle / Sky & Telescope (2001)
The nine-class system amateur astronomers use to rate a site's sky-darkness. Class 1 is a desert with no light dome; Class 9 is inner-city. Springfield sits at class 5: suburban, Milky Way on a good night, Pleiades easy, M31 direct. This page treats that class as a static input and combines it with live moon + cloud to get tonight's number.
- Open-Meteo Archive API· Patrick Zippenfenig
Daily weather for any point on Earth going back to 1940, served as JSON with no key. The 30-year climate normals and the all-time date records that drive the anomaly card were computed from this API exactly once and committed to the repo, so runtime traffic stays at one tiny call per day for today's observation.
- mulberry32· bryc
The per-session PRNG. 15 lines, period 2^32, passes PractRand up to 2^26 bytes. Perfect for ‘deterministic from a seed, different-looking for each seed’ use cases.
- Next.js 16· Vercel
The framework running all of this. App Router, React 19, edge-adjacent route handlers, Turbopack turned off for deliberate reasons.
$ head -n 1 /src/components/LivingOzarksCanvas.tsx
"use client";
If you want something like this on your own site, let’s talk.