Initial commit — Seth Calendar & Decimal Time clock site
Pages: /, /simple, /decimal, /seth, /calendar, /astro, /convert, /timegov Features: Seth Calendar (10×36 + holidays), decimal time, moon phases, astronomy (sun/moon), bidirectional time converter, Seth date display, leap day split cell in calendar grid.
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
// Astronomical calculations for SethPC Astronomy page
|
||||
// Moon phase: Jean Meeus "Astronomical Algorithms" simplified
|
||||
// Sun rise/set: NOAA solar calculator algorithm
|
||||
// Solstice/equinox: Meeus Table 27.a
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const DEG = Math.PI / 180;
|
||||
const RAD = 180 / Math.PI;
|
||||
|
||||
function frac(x) { return x - Math.floor(x); }
|
||||
function mod360(x) { return ((x % 360) + 360) % 360; }
|
||||
|
||||
// Julian Day Number from calendar date (noon UT)
|
||||
function julianDay(year, month, day) {
|
||||
if (month <= 2) { year--; month += 12; }
|
||||
const A = Math.floor(year / 100);
|
||||
const B = 2 - A + Math.floor(A / 4);
|
||||
return Math.floor(365.25 * (year + 4716)) + Math.floor(30.6001 * (month + 1)) + day + B - 1524.5;
|
||||
}
|
||||
|
||||
function jdFromDate(date) {
|
||||
return julianDay(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate()) + date.getUTCHours() / 24 + date.getUTCMinutes() / 1440;
|
||||
}
|
||||
|
||||
function dateFromJD(jd) {
|
||||
const z = Math.floor(jd + 0.5);
|
||||
const f = jd + 0.5 - z;
|
||||
let A = z;
|
||||
if (z >= 2299161) {
|
||||
const alpha = Math.floor((z - 1867216.25) / 36524.25);
|
||||
A = z + 1 + alpha - Math.floor(alpha / 4);
|
||||
}
|
||||
const B = A + 1524;
|
||||
const C = Math.floor((B - 122.1) / 365.25);
|
||||
const D = Math.floor(365.25 * C);
|
||||
const E = Math.floor((B - D) / 30.6001);
|
||||
const day = B - D - Math.floor(30.6001 * E) + f;
|
||||
const month = E < 14 ? E - 1 : E - 13;
|
||||
const year = month > 2 ? C - 4716 : C - 4715;
|
||||
return new Date(Date.UTC(year, month - 1, Math.floor(day)));
|
||||
}
|
||||
|
||||
// ── Moon phase ────────────────────────────────────────────────────────────────
|
||||
|
||||
// Returns moon age in days (0 = new moon, ~14.77 = full moon, ~29.53 = back to new)
|
||||
// and illumination fraction (0–1)
|
||||
function moonPhase(date) {
|
||||
// Reference new moon: Jan 6, 2000 18:14 UTC (JD 2451549.756)
|
||||
const KNOWN_NEW_MOON_JD = 2451549.756;
|
||||
const SYNODIC_MONTH = 29.53058867;
|
||||
|
||||
const jd = jdFromDate(date) + 0.5; // use noon of that day
|
||||
const daysSinceNew = jd - KNOWN_NEW_MOON_JD;
|
||||
const cycles = daysSinceNew / SYNODIC_MONTH;
|
||||
const age = frac(cycles) * SYNODIC_MONTH; // 0..29.53
|
||||
|
||||
// Illumination: cos curve, 0 at new, 1 at full
|
||||
const illumination = (1 - Math.cos(2 * Math.PI * age / SYNODIC_MONTH)) / 2;
|
||||
|
||||
return { age, illumination, cycles };
|
||||
}
|
||||
|
||||
function moonPhaseName(age) {
|
||||
const s = age / 29.53058867;
|
||||
if (s < 0.025 || s >= 0.975) return "New Moon";
|
||||
if (s < 0.25) return "Waxing Crescent";
|
||||
if (s < 0.275) return "First Quarter";
|
||||
if (s < 0.475) return "Waxing Gibbous";
|
||||
if (s < 0.525) return "Full Moon";
|
||||
if (s < 0.725) return "Waning Gibbous";
|
||||
if (s < 0.75) return "Last Quarter";
|
||||
return "Waning Crescent";
|
||||
}
|
||||
|
||||
function moonPhaseGlyph(age) {
|
||||
const s = age / 29.53058867;
|
||||
if (s < 0.025 || s >= 0.975) return "🌑";
|
||||
if (s < 0.25) return "🌒";
|
||||
if (s < 0.275) return "🌓";
|
||||
if (s < 0.475) return "🌔";
|
||||
if (s < 0.525) return "🌕";
|
||||
if (s < 0.725) return "🌖";
|
||||
if (s < 0.75) return "🌗";
|
||||
return "🌘";
|
||||
}
|
||||
|
||||
// Find JD of next phase after given JD: phase 0=new,1=first,2=full,3=last
|
||||
function nextMoonPhaseJD(jd, phase) {
|
||||
const SYNODIC_MONTH = 29.53058867;
|
||||
const KNOWN_NEW_MOON_JD = 2451549.756;
|
||||
const daysSince = jd - KNOWN_NEW_MOON_JD;
|
||||
const cycles = daysSince / SYNODIC_MONTH;
|
||||
const phaseFrac = phase / 4;
|
||||
let n = Math.floor(cycles - phaseFrac) + phaseFrac;
|
||||
let targetJD = KNOWN_NEW_MOON_JD + n * SYNODIC_MONTH;
|
||||
if (targetJD <= jd) targetJD += SYNODIC_MONTH;
|
||||
return targetJD;
|
||||
}
|
||||
|
||||
// ── Sun rise/set (NOAA algorithm) ────────────────────────────────────────────
|
||||
|
||||
function sunriseSunset(date, lat, lon) {
|
||||
const jd = julianDay(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate());
|
||||
const n = jd - 2451545.0 + 0.0008;
|
||||
const Js = n - lon / 360;
|
||||
const M = mod360(357.5291 + 0.98560028 * Js);
|
||||
const C = 1.9148 * Math.sin(M * DEG) + 0.0200 * Math.sin(2 * M * DEG) + 0.0003 * Math.sin(3 * M * DEG);
|
||||
const lam = mod360(M + C + 180 + 102.9372);
|
||||
const Jt = 2451545.0 + Js + 0.0053 * Math.sin(M * DEG) - 0.0069 * Math.sin(2 * lam * DEG);
|
||||
const sinDec = Math.sin(lam * DEG) * Math.sin(23.4397 * DEG);
|
||||
const cosHa = (Math.sin(-0.833 * DEG) - Math.sin(lat * DEG) * sinDec) / (Math.cos(lat * DEG) * Math.cos(Math.asin(sinDec)));
|
||||
|
||||
if (cosHa < -1) return { rise: null, set: null, noon: jdToTime(Jt), alwaysUp: true };
|
||||
if (cosHa > 1) return { rise: null, set: null, noon: jdToTime(Jt), alwaysDown: true };
|
||||
|
||||
const Ha = Math.acos(cosHa) * RAD / 360;
|
||||
const Jrise = Jt - Ha;
|
||||
const Jset = Jt + Ha;
|
||||
|
||||
return {
|
||||
rise: jdToTime(Jrise),
|
||||
set: jdToTime(Jset),
|
||||
noon: jdToTime(Jt),
|
||||
riseJD: Jrise,
|
||||
setJD: Jset,
|
||||
noonJD: Jt,
|
||||
};
|
||||
}
|
||||
|
||||
// Convert fractional JD to UTC HH:MM string
|
||||
function jdToTime(jd) {
|
||||
const totalMin = Math.round(((jd + 0.5) % 1) * 1440);
|
||||
const h = Math.floor(totalMin / 60) % 24;
|
||||
const m = totalMin % 60;
|
||||
return `${String(h).padStart(2,"0")}:${String(m).padStart(2,"0")} UTC`;
|
||||
}
|
||||
|
||||
function dayLengthStr(riseJD, setJD) {
|
||||
const mins = Math.round((setJD - riseJD) * 1440);
|
||||
const h = Math.floor(mins / 60);
|
||||
const m = mins % 60;
|
||||
return `${h}h ${String(m).padStart(2,"0")}m`;
|
||||
}
|
||||
|
||||
// ── Solstice / Equinox (Meeus Table 27.a + correction) ───────────────────────
|
||||
// Returns approximate JD for each event in a given year
|
||||
// season: 0=March equinox, 1=June solstice, 2=September equinox, 3=December solstice
|
||||
|
||||
function seasonJD(year, season) {
|
||||
const Y = (year - 2000) / 1000;
|
||||
const JDE0s = [
|
||||
// March equinox
|
||||
2451623.80984 + 365242.37404*Y + 0.05169*Y*Y - 0.00411*Y*Y*Y - 0.00057*Y*Y*Y*Y,
|
||||
// June solstice
|
||||
2451716.56767 + 365241.62603*Y + 0.00325*Y*Y + 0.00888*Y*Y*Y - 0.00030*Y*Y*Y*Y,
|
||||
// September equinox
|
||||
2451810.05917 + 365242.01767*Y - 0.11575*Y*Y + 0.00337*Y*Y*Y + 0.00078*Y*Y*Y*Y,
|
||||
// December solstice
|
||||
2451900.05952 + 365242.74049*Y - 0.06223*Y*Y - 0.00823*Y*Y*Y + 0.00032*Y*Y*Y*Y,
|
||||
];
|
||||
return JDE0s[season];
|
||||
}
|
||||
|
||||
function seasonEvents(year) {
|
||||
const names = ["March Equinox", "June Solstice", "September Equinox", "December Solstice"];
|
||||
const emojis = ["🌱", "☀️", "🍂", "❄️"];
|
||||
return names.map((name, i) => {
|
||||
const jd = seasonJD(year, i);
|
||||
const d = dateFromJD(jd);
|
||||
return { name, emoji: emojis[i], date: d, month: d.getUTCMonth() + 1, day: d.getUTCDate() };
|
||||
});
|
||||
}
|
||||
|
||||
// ── Moonrise / Moonset (simplified — horizon crossing estimate) ───────────────
|
||||
// Uses a simple approximation: moon moves ~13.18°/day, rises ~50min later each day.
|
||||
// Good enough for a display page; full computation needs perturbation terms.
|
||||
function moonriseApprox(date, lat, lon) {
|
||||
// Approximate: moon rises ~50 min later each day than previous
|
||||
// Anchor: use sun rise/set as rough guide, offset by moon's daily retardation
|
||||
// For a simple display we just note this is approximate
|
||||
return null; // placeholder — show "~" note instead
|
||||
}
|
||||
|
||||
// ── DOM ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
// Parse ?date=YYYY-MM-DD from URL, default to today
|
||||
function parseDate() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const ds = params.get("date");
|
||||
if (ds) {
|
||||
const [y, m, d] = ds.split("-").map(Number);
|
||||
if (y && m && d) return new Date(Date.UTC(y, m - 1, d));
|
||||
}
|
||||
const now = new Date();
|
||||
return new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()));
|
||||
}
|
||||
|
||||
function dateToParam(date) {
|
||||
const y = date.getUTCFullYear();
|
||||
const m = String(date.getUTCMonth() + 1).padStart(2, "0");
|
||||
const d = String(date.getUTCDate()).padStart(2, "0");
|
||||
return `${y}-${m}-${d}`;
|
||||
}
|
||||
|
||||
function stepDate(date, days) {
|
||||
const d = new Date(date);
|
||||
d.setUTCDate(d.getUTCDate() + days);
|
||||
return d;
|
||||
}
|
||||
|
||||
function formatDate(date) {
|
||||
return date.toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric", timeZone: "UTC" });
|
||||
}
|
||||
|
||||
// Default lat/lon: roughly central US (no geolocation, just reasonable default)
|
||||
// Will use user's timezone offset to pick a reasonable longitude
|
||||
function guessLon() {
|
||||
const offsetHours = -new Date().getTimezoneOffset() / 60;
|
||||
return offsetHours * 15; // rough: UTC offset * 15° per hour
|
||||
}
|
||||
|
||||
const LAT = 40.0; // roughly mid-US latitude
|
||||
const LON = guessLon();
|
||||
|
||||
let currentDate = parseDate();
|
||||
|
||||
function render() {
|
||||
const date = currentDate;
|
||||
|
||||
// Update URL without reload
|
||||
const newUrl = `${window.location.pathname}?date=${dateToParam(date)}`;
|
||||
history.replaceState(null, "", newUrl);
|
||||
|
||||
document.getElementById("dateTitle").textContent = formatDate(date);
|
||||
|
||||
// Moon
|
||||
const { age, illumination } = moonPhase(date);
|
||||
document.getElementById("moonGlyph").textContent = moonPhaseGlyph(age);
|
||||
document.getElementById("moonName").textContent = moonPhaseName(age);
|
||||
document.getElementById("moonPct").textContent =
|
||||
`${Math.round(illumination * 100)}% illuminated · ${age.toFixed(1)} days old`;
|
||||
document.getElementById("moonAge").textContent = `${age.toFixed(1)} days`;
|
||||
|
||||
// Next new & full
|
||||
const jd = jdFromDate(date);
|
||||
const nextNewJD = nextMoonPhaseJD(jd, 0);
|
||||
const nextFullJD = nextMoonPhaseJD(jd, 2);
|
||||
document.getElementById("nextNew").textContent = dateFromJD(nextNewJD).toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
|
||||
document.getElementById("nextFull").textContent = dateFromJD(nextFullJD).toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
|
||||
|
||||
// Sun
|
||||
const sun = sunriseSunset(date, LAT, LON);
|
||||
document.getElementById("sunrise").textContent = sun.rise || (sun.alwaysUp ? "Midnight sun" : "Below horizon");
|
||||
document.getElementById("sunset").textContent = sun.set || (sun.alwaysUp ? "Midnight sun" : "Below horizon");
|
||||
document.getElementById("solarNoon").textContent = sun.noon;
|
||||
document.getElementById("dayLen").textContent = sun.rise && sun.set ? dayLengthStr(sun.riseJD, sun.setJD) : "—";
|
||||
|
||||
|
||||
|
||||
// Year events
|
||||
const year = date.getUTCFullYear();
|
||||
const events = seasonEvents(year);
|
||||
const SYNODIC = 29.53058867;
|
||||
const KNOWN_NEW_MOON_JD = 2451549.756;
|
||||
|
||||
// Add major moon phases for the year
|
||||
const moonEvents = [];
|
||||
let testJD = julianDay(year, 1, 1);
|
||||
const endJD = julianDay(year, 12, 31);
|
||||
for (let phase = 0; phase < 4; phase++) {
|
||||
let pJD = nextMoonPhaseJD(testJD - SYNODIC, phase);
|
||||
while (pJD <= endJD) {
|
||||
const d = dateFromJD(pJD);
|
||||
if (d.getUTCFullYear() === year) {
|
||||
const phaseNames = ["🌑 New Moon", "🌓 First Quarter", "🌕 Full Moon", "🌗 Last Quarter"];
|
||||
moonEvents.push({ name: phaseNames[phase], date: d, jd: pJD });
|
||||
}
|
||||
pJD += SYNODIC;
|
||||
}
|
||||
}
|
||||
moonEvents.sort((a, b) => a.jd - b.jd);
|
||||
|
||||
const allEvents = [
|
||||
...events.map(e => ({ name: `${e.emoji} ${e.name}`, date: e.date, jd: jdFromDate(e.date) })),
|
||||
...moonEvents,
|
||||
].sort((a, b) => a.jd - b.jd);
|
||||
|
||||
const ul = document.getElementById("yearEvents");
|
||||
ul.innerHTML = "";
|
||||
allEvents.forEach(ev => {
|
||||
const li = document.createElement("li");
|
||||
const ds = ev.date.toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" });
|
||||
const isToday = dateToParam(ev.date) === dateToParam(date);
|
||||
li.innerHTML = `<span class="event-name"${isToday ? ' style="color:var(--accent)"' : ""}>${ev.name}</span><span>${ds}</span>`;
|
||||
ul.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("prevDay").addEventListener("click", () => {
|
||||
currentDate = stepDate(currentDate, -1);
|
||||
render();
|
||||
});
|
||||
document.getElementById("nextDay").addEventListener("click", () => {
|
||||
currentDate = stepDate(currentDate, +1);
|
||||
render();
|
||||
});
|
||||
|
||||
render();
|
||||
Reference in New Issue
Block a user