@verifyhash/sky-planner
v0.1.0
Published
Offline astronomy/photography night planner engine: solar events (sunrise/sunset/solar noon, civil+nautical+astronomical twilight, golden & blue hour), moon events (rise/set/phase/illumination), and a moonless-dark stargazing score for any lat/lon/date. Z
Maintainers
Readme
sky-planner
Offline night-planning engine for photographers and stargazers. Give it a place and a date; it returns the Sun's schedule (rise, set, solar noon, all three twilights, and the golden- and blue-hour windows), the Moon's schedule (rise, set, phase name, illumination), and a single 0–100 stargazing quality score for that night based on how much of the night is both astronomically dark and free of the Moon.
Everything runs locally with no dependencies and no network. index.js +
vendor/ + lib/ are the pure computation engine (T-APP.1); index.html is a
self-contained single-page UI (T-APP.2) that drives that same engine.
The UI (index.html)
Open index.html in any browser (double-click it, or serve the folder) — it is
one self-contained file with no CDN, no external fonts/scripts/styles, and no
network at all. Enter a latitude, longitude and date (or press Use my
location, which degrades gracefully to manual entry) and the page updates live:
a noon-to-noon sky-gradient timeline with sun/moon markers, readouts for every
twilight and the golden/blue-hour windows, moon phase and rise/set, the moonless
astronomical-dark window, and a stargazing-quality meter.
The engine is inlined verbatim into index.html by build.js, wrapped in a
tiny in-page CommonJS shim — no source is duplicated by hand. If you change the
engine, re-sync the page with:
node build.jsbuild.js reads index.js and its vendor//lib/ modules and splices their
exact bytes between the ENGINE_START/ENGINE_END markers in index.html.
Who it's for
- Landscape / portrait photographers chasing the golden hour (warm, low Sun) and the blue hour (the deep-blue twilight just before/after it) — the engine gives you the exact start/end of each, morning and evening.
- Astrophotographers and stargazers who need true darkness: it reports the astronomical-twilight boundaries (Sun 18° below the horizon) and computes how many minutes of the night are actually Moon-free.
- Anyone searching golden-hour / moon-phase / "is tonight dark?" for a trip, an event, or a shoot — one function call answers all of it for any latitude/longitude/date, past or future.
What "golden hour" and "blue hour" mean here
Photography terms don't have one universal definition, so here are the exact Sun-elevation thresholds this engine uses (documented so you can trust the numbers):
| Window | Sun elevation | Notes | |--------------|--------------------|----------------------------------------------| | Golden hour | −4° to +6° | Soft, warm light; the "hour" is much shorter near the equator and much longer near the poles. | | Blue hour | −6° to −4° | The saturated-blue twilight; sits just inside civil twilight. | | Civil dusk/dawn | Sun at −6° | Enough light to see; horizon still visible. | | Nautical dusk/dawn | Sun at −12° | Horizon fades; brighter stars out. | | Astronomical dusk/dawn | Sun at −18° | Sky is fully dark — the start/end of true night. |
Quick start
const { planNight } = require('sky-planner'); // or require('./index.js')
const plan = planNight({
lat: 40.7128, // + North
lon: -74.006, // + East
date: { year: 2024, month: 3, day: 20 }, // or a JS Date
tz: -4, // hours from UTC — only affects local-day selection
});
console.log(plan.sun.sunrise); // → UTC Date instant
console.log(plan.sun.goldenHour.evening); // → { start: Date, end: Date }
console.log(plan.moon.phaseName, plan.moon.illuminationPercent + '%');
console.log('stargazing score:', plan.stargazing.score); // 0–100All time fields are UTC Date instants (or null when the event doesn't
occur — polar day/night, or a Moon that never rises/sets that day). To print a
local clock time, format with the same tz you passed in, or use the
re-exported helper:
const { localClockMinutes } = require('sky-planner');
localClockMinutes(plan.sun.sunset, -4); // → minutes-after-midnight, localResult shape
{
input: { lat, lon, tz, date: { year, month, day } },
sun: {
solarNoon, sunrise, sunset, dayLengthMinutes,
twilight: { civilDawn, civilDusk, nauticalDawn, nauticalDusk,
astronomicalDawn, astronomicalDusk },
goldenHour: { morning: { start, end }, evening: { start, end } },
blueHour: { morning: { start, end }, evening: { start, end } }
},
moon: {
moonrise, moonset, alwaysUp, alwaysDown,
phaseName, // 'New' | 'Waxing Crescent' | ... | 'Waning Crescent'
illumination, // 0..1, fraction of the disk lit
illuminationPercent // 0..100, rounded
},
stargazing: {
astronomicalNightMinutes, // length of Sun-below-−18° night
moonlessDarkMinutes, // of that, how much has the Moon down
score // 0..100
}
}The stargazing score
The score answers one question: how much genuinely dark, Moon-free sky does this night offer? It is computed as
- the astronomical night = this evening's astronomical dusk → the next morning's astronomical dawn (Sun below −18° the whole time);
- minus the minutes within that window when the Moon is above the horizon (sampled every 5 minutes from the lunar altitude);
- the resulting moonless-dark minutes mapped onto 0–100, rising linearly and plateauing at 100 once you have 6 hours of moonless darkness — beyond that, more time doesn't make the night meaningfully better.
By construction the score is bounded to [0, 100] and monotonically
non-decreasing in the length of the moonless-dark window (stargazingScore()
is exported separately so you can verify this). A new-moon night in winter
scores near 100; a full-moon night, where the Moon is up all night, scores near
0. Nights with no astronomical darkness at all (high-latitude summer) score 0.
stargazingScore(moonlessDarkMinutes) is also exported on its own if you want to
score a custom dark window.
Honest limits
- Sun times use the NOAA/Meeus solar geometry (see
vendor/solar-calc) and land within ~1–2 minutes of published almanac times at mid-latitudes. Near the poles, and for the exact moment of a horizon grazing, expect more spread. - Moon phase / illumination use a simplified Meeus series (see
vendor/moon-phase): phase angle good to ~0.5°, illuminated fraction to ~1%. Illumination is evaluated at the middle of the night, not at a fixed instant. - Moonrise / moonset are found by sampling the lunar altitude hourly and interpolating the horizon crossing — typically within ~5 minutes at mid-latitudes, worse where the Moon skims the horizon. This is a planning tool, not an occultation or eclipse predictor.
- The score ignores clouds, light pollution, air quality, and the Moon's phase during its time up (a thin crescent above the horizon still counts as "Moon up"). It measures dark-window geometry, not sky transparency.
- No atmospheric-refraction term is added to the reported instantaneous Sun altitude; the twilight/rise thresholds already fold standard refraction into their zenith angles.
How it's built (offline, self-contained)
sky-planner/
├── index.js engine — the planNight() + stargazingScore() API
├── lib/
│ └── moon-position.js Moon altitude + moonrise/moonset (original code)
├── vendor/ pinned, verbatim copies — see PROVENANCE.md
│ ├── solar-calc/ NOAA solar geometry (MIT)
│ └── moon-phase/ simplified Meeus lunar phase (MIT)
└── test/
└── engine.test.jsThe solar and lunar-phase math is vendored (copied + committed + pinned by
SHA-256) from the sibling solar-calc and moon-phase projects rather than
required across directories, so this folder is fully standalone. See
vendor/PROVENANCE.md for the exact source commits and hashes. There is no
network access, no server, and no runtime dependency anywhere in the project.
Running the tests
One command, Node's built-in runtime only (no test framework, nothing to install):
node test/engine.test.jsIt checks published sunrise/sunset for New York, London, and Sydney (±3 min), the full ordering of the twilight and golden/blue-hour events, lunar illumination against documented new/full moons, moonrise/moonset horizon geometry, and that the stargazing score is bounded, monotonic, and correctly lower on a full-moon night than a new-moon night. The process exits non-zero if any assertion fails.
License
MIT. Vendored components are MIT (see vendor/PROVENANCE.md).
