-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmoon_print.py
More file actions
106 lines (86 loc) · 3.67 KB
/
Copy pathmoon_print.py
File metadata and controls
106 lines (86 loc) · 3.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#!/usr/bin/env python3
"""Print tonight's moon phase on the S01 thermal printer.
Computes the moon's age from a known new-moon epoch and the synodic month, then
draws the disc with the correct illuminated portion (dark side filled, lit side
left white). No network needed -- it's pure astronomy.
Examples:
python moon_print.py # tonight
python moon_print.py --date 2026-12-25
python moon_print.py --no-print
"""
from __future__ import annotations
import argparse
from datetime import datetime, timezone
from math import cos, pi, sqrt
from pathlib import Path
from PIL import Image, ImageDraw
from print_common import ROOT, Card
SYNODIC = 29.530588853 # mean length of a lunar month (days)
NEW_MOON_JD = 2451550.1 # known new moon: 2000-01-06 18:14 UT
PHASES = [
(0.020, "New Moon"), (0.235, "Waxing Crescent"), (0.265, "First Quarter"),
(0.480, "Waxing Gibbous"), (0.520, "Full Moon"), (0.735, "Waning Gibbous"),
(0.765, "Last Quarter"), (0.980, "Waning Crescent"), (1.001, "New Moon"),
]
def julian_day(dt: datetime) -> float:
y, m = dt.year, dt.month
a = (14 - m) // 12
y += 4800 - a
m += 12 * a - 3
jdn = dt.day + (153 * m + 2) // 5 + 365 * y + y // 4 - y // 100 + y // 400 - 32045
return jdn + (dt.hour - 12) / 24 + dt.minute / 1440 + dt.second / 86400
def phase_name(p: float) -> str:
for upper, name in PHASES:
if p < upper:
return name
return "New Moon"
def draw_moon(p: float, size: int) -> Image.Image:
img = Image.new("L", (size, size), 255)
d = ImageDraw.Draw(img)
cx = cy = size / 2
R = size / 2 - 3
ct = cos(2 * pi * p) # terminator scale: 1 (new) -> -1 (full)
for dy in range(-int(R), int(R) + 1):
xe = sqrt(max(0.0, R * R - dy * dy))
xt = xe * ct
lit = (xt, xe) if p < 0.5 else (-xe, -xt)
y = cy + dy
# fill the whole disc row black, then carve the lit span back to white
d.line((cx - xe, y, cx + xe, y), fill=0)
if lit[1] > lit[0]:
d.line((cx + lit[0], y, cx + lit[1], y), fill=255)
d.ellipse((cx - R, cy - R, cx + R, cy + R), outline=0, width=2)
return img
def main() -> int:
parser = argparse.ArgumentParser(description="Print tonight's moon phase on the S01 thermal printer.")
parser.add_argument("--date", default=None, help="Date as YYYY-MM-DD (default: now).")
parser.add_argument("--out", type=Path, default=ROOT / "moon.png")
parser.add_argument("--darkness", type=int, choices=range(1, 6), default=3)
parser.add_argument("--bottom-feed", type=int, default=24)
parser.add_argument("--no-print", action="store_true")
args = parser.parse_args()
if args.date:
try:
dt = datetime.strptime(args.date, "%Y-%m-%d").replace(hour=21, tzinfo=timezone.utc)
except ValueError:
raise SystemExit(f"Bad --date {args.date!r}; expected YYYY-MM-DD")
else:
dt = datetime.now(timezone.utc)
age = (julian_day(dt) - NEW_MOON_JD) % SYNODIC
p = age / SYNODIC
illum = (1 - cos(2 * pi * p)) / 2
name = phase_name(p)
print(f"{dt:%Y-%m-%d}: {name}, {illum * 100:.0f}% lit, age {age:.1f} d")
card = Card()
card.title("MOON PHASE")
card.heading(f"{dt:%B %d, %Y}", size=16)
card.gap(4).image(draw_moon(p, card.inner_w - 40), border=False)
card.gap(4).divider()
card.kv("Phase", name)
card.kv("Illumination", f"{illum * 100:.0f}%")
card.kv("Moon age", f"{age:.1f} days")
card.kv("Lunation", f"{p * 100:.0f}%")
card.footer("lunar synodic month")
return card.finish(args.out, args.bottom_feed, args.darkness, do_print=not args.no_print)
if __name__ == "__main__":
raise SystemExit(main())