From 195520d66f4a718a399ba53ffebbf7c7df882a8e Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Wed, 20 Aug 2025 19:49:24 -0600 Subject: [PATCH] Lunar phases --- calendar.js | 11 +++++++++-- cells.css | 6 ++++++ date-utils.js | 30 ++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/calendar.js b/calendar.js index 4f7a18f..3089bfa 100644 --- a/calendar.js +++ b/calendar.js @@ -13,6 +13,7 @@ import { getLocalizedWeekdayNames, getLocalizedMonthName, formatDateRange + ,lunarPhaseSymbol } from './date-utils.js' class InfiniteCalendar { @@ -330,8 +331,14 @@ class InfiniteCalendar { labelYear = cur.getFullYear() } - const day = document.createElement('h1') - day.textContent = String(cur.getDate()) + const day = document.createElement('h1') + day.textContent = String(cur.getDate()) + + // lunar phase symbol (only for main phases) + const moon = document.createElement('span') + moon.className = 'lunar-phase' + moon.textContent = lunarPhaseSymbol(cur) || '' + if (moon.textContent) cell.appendChild(moon) const date = toLocalString(cur) cell.dataset.date = date diff --git a/cells.css b/cells.css index c40279a..d7b64ae 100644 --- a/cells.css +++ b/cells.css @@ -22,6 +22,12 @@ transition: background-color .15s ease; font-size: 1em; } +.cell .lunar-phase { + position: absolute; + z-index: 1; + top: .25em; + left: 50%; +} .cell.today h1 { border-radius: 2em; background: var(--today); diff --git a/date-utils.js b/date-utils.js index 076306e..446a045 100644 --- a/date-utils.js +++ b/date-utils.js @@ -121,6 +121,35 @@ function formatDateRange(startDate, endDate) { return `${startISO}/${endISO}` } +/** + * Compute lunar phase symbol for the four main phases on a given date. + * Returns one of: 🌑 (new), 🌓 (first quarter), 🌕 (full), 🌗 (last quarter), or '' otherwise. + * Uses an approximate algorithm with a fixed epoch. + */ +function lunarPhaseSymbol(date) { + // Reference new moon: 2000-01-06 18:14 UTC (J2000 era), often used in approximations + const ref = Date.UTC(2000, 0, 6, 18, 14, 0) + const synodic = 29.530588853 // days + // Use UTC noon of given date to reduce timezone edge effects + const dUTC = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 12, 0, 0) + const daysSince = (dUTC - ref) / DAY_MS + const phase = ((daysSince / synodic) % 1 + 1) % 1 + const phases = [ + { t: 0.0, s: '🌑' }, // New Moon + { t: 0.25, s: '🌓' }, // First Quarter + { t: 0.5, s: '🌕' }, // Full Moon + { t: 0.75, s: '🌗' } // Last Quarter + ] + // threshold in days from exact phase to still count for this date + const thresholdDays = 0.5 // ±12 hours + for (const p of phases) { + let delta = Math.abs(phase - p.t) + if (delta > 0.5) delta = 1 - delta + if (delta * synodic <= thresholdDays) return p.s + } + return '' +} + // Export all functions and constants export { monthAbbr, @@ -136,4 +165,5 @@ export { getLocalizedWeekdayNames, getLocalizedMonthName, formatDateRange + ,lunarPhaseSymbol }