Add MoonWatch app, widget extension, shared module, and .gitignore
Adds the real MoonWatch implementation on top of the initial Xcode template: new SwiftUI views (MoonDiskView, StarFieldView), the MoonWatchWidget extension, and a Shared module containing the moon phase calculator, shadow shape, and shared asset catalog. Also adds a Swift/Xcode/macOS .gitignore and untracks per-user xcuserdata files. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
102
Shared/MoonPhaseCalculator.swift
Normal file
102
Shared/MoonPhaseCalculator.swift
Normal file
@@ -0,0 +1,102 @@
|
||||
//
|
||||
// MoonPhaseCalculator.swift
|
||||
// Shared between MoonWatch and MoonWatchWidget.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum MoonPhaseCalculator: Sendable {
|
||||
/// Mean synodic month length in days (Meeus).
|
||||
public static let synodicMonth = 29.530588853
|
||||
|
||||
/// Julian date at Unix epoch (1970-01-01 00:00 UTC).
|
||||
private static let julianDayUnixEpoch = 24_405_87.5
|
||||
|
||||
/// Known new moon reference: 2000-01-06 18:14 UTC, JD 2451550.09765.
|
||||
private static let julianDayKnownNewMoon = 2_451_550.09765
|
||||
|
||||
public static func julianDay(for date: Date) -> Double {
|
||||
date.timeIntervalSince1970 / 86_400.0 + julianDayUnixEpoch
|
||||
}
|
||||
|
||||
/// Days since the reference new moon, wrapped to `[0, synodicMonth)`.
|
||||
public static func moonAgeDays(for date: Date) -> Double {
|
||||
let jd = julianDay(for: date)
|
||||
var age = jd - julianDayKnownNewMoon
|
||||
age = age.truncatingRemainder(dividingBy: synodicMonth)
|
||||
if age < 0 { age += synodicMonth }
|
||||
return age
|
||||
}
|
||||
|
||||
/// Normalized phase in `[0, 1)`; 0 is new moon.
|
||||
public static func normalizedPhase(for date: Date) -> Double {
|
||||
moonAgeDays(for: date) / synodicMonth
|
||||
}
|
||||
|
||||
/// Illuminated fraction of the apparent disk (simple geometric model).
|
||||
public static func illuminatedFraction(for date: Date) -> Double {
|
||||
let t = normalizedPhase(for: date)
|
||||
let angle = 2 * Double.pi * t
|
||||
return (1 - cos(angle)) / 2
|
||||
}
|
||||
|
||||
public static func namedPhase(normalizedPhase t: Double) -> MoonPhaseName {
|
||||
let x = t.truncatingRemainder(dividingBy: 1)
|
||||
let u = x < 0 ? x + 1 : x
|
||||
// Cardinal phases (New, First Quarter, Full, Last Quarter) are astronomical
|
||||
// moments, not equal eighths of the cycle. Treat them as narrow windows of
|
||||
// 1/16 of the cycle (~1.85 days) centered at 0, 0.25, 0.5, 0.75; the four
|
||||
// intermediate phases fill the 3/16 gaps between them.
|
||||
let step = u * 32
|
||||
switch step {
|
||||
case ..<1, 31...: return .newMoon
|
||||
case 1..<7: return .waxingCrescent
|
||||
case 7..<9: return .firstQuarter
|
||||
case 9..<15: return .waxingGibbous
|
||||
case 15..<17: return .fullMoon
|
||||
case 17..<23: return .waningGibbous
|
||||
case 23..<25: return .thirdQuarter
|
||||
default: return .waningCrescent
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the next phase transition: which phase comes next and how many
|
||||
/// days until the boundary is crossed.
|
||||
public static func nextPhaseTransition(normalizedPhase t: Double) -> (phase: MoonPhaseName, daysAway: Double) {
|
||||
let raw = t.truncatingRemainder(dividingBy: 1)
|
||||
let u = raw < 0 ? raw + 1 : raw
|
||||
let step = u * 32
|
||||
|
||||
// Each entry is (boundary in 32nds, the phase that begins at that boundary)
|
||||
let transitions: [(Double, MoonPhaseName)] = [
|
||||
(1, .waxingCrescent),
|
||||
(7, .firstQuarter),
|
||||
(9, .waxingGibbous),
|
||||
(15, .fullMoon),
|
||||
(17, .waningGibbous),
|
||||
(23, .thirdQuarter),
|
||||
(25, .waningCrescent),
|
||||
(31, .newMoon),
|
||||
]
|
||||
for (boundary, phase) in transitions where step < boundary {
|
||||
return (phase, (boundary / 32.0 - u) * synodicMonth)
|
||||
}
|
||||
// Late new-moon window (step ≥ 31): next is waxing crescent at 1/32 next cycle
|
||||
return (.waxingCrescent, (1.0 + 1.0 / 32.0 - u) * synodicMonth)
|
||||
}
|
||||
|
||||
public static func snapshot(for date: Date) -> MoonPhaseSnapshot {
|
||||
let age = moonAgeDays(for: date)
|
||||
let norm = age / synodicMonth
|
||||
let (nextPhase, daysToNext) = nextPhaseTransition(normalizedPhase: norm)
|
||||
return MoonPhaseSnapshot(
|
||||
date: date,
|
||||
normalizedPhase: norm,
|
||||
moonAgeDays: age,
|
||||
namedPhase: namedPhase(normalizedPhase: norm),
|
||||
illuminatedFraction: illuminatedFraction(for: date),
|
||||
nextPhaseName: nextPhase,
|
||||
daysToNextPhase: daysToNext
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user