New screens/weather_icons.py: vector icons drawn with PIL primitives using the panel's actual 6-color palette (black/white/yellow/red/blue/green, from epd4in0e's own getbuffer() quantization table) -- sun with rays, line-art clouds, blue rain/snow, yellow lightning bolt, fog bands. Replaces the old monochrome unicode-glyph approach entirely (also sidesteps font coverage gaps -- that's what caused the earlier tofu-box bug). Forecast row given significantly more vertical space per request. Caught two real bugs while building this: - Current-conditions block height was guessed independently of the space allocated to it, so it overflowed into the forecast row for real content. Now derives the space it needs from actual measured text/icon size instead of assuming both fit. - `scale * 0.045 if has_forecast else 0.05` -- conditional-expression precedence bug, silently evaluated to a bare 0.05 (not scale * 0.05) in the else branch, crashing font loading with size=0 when forecast is empty. Only surfaced by testing that specific case. - Sun icon's rays extended past the nominal icon_size box used for layout math, throckwing off vertical centering when no forecast is shown.
122 lines
4.9 KiB
Python
122 lines
4.9 KiB
Python
"""Hand-drawn weather icons using the panel's actual 6-color palette (from
|
|
epd4in0e's getbuffer() quantization palette: black, white, yellow, red, blue,
|
|
green) instead of relying on font glyph coverage for weather emoji, which
|
|
turned out to be incomplete (DejaVu Sans has no partly-cloudy glyph) and is
|
|
monochrome anyway.
|
|
"""
|
|
|
|
import math
|
|
|
|
BLACK = (0, 0, 0)
|
|
WHITE = (255, 255, 255)
|
|
YELLOW = (255, 255, 0)
|
|
BLUE = (0, 0, 255)
|
|
|
|
# WMO weather code -> (description, icon kind). Not exhaustive, covers the
|
|
# common cases; unmapped codes fall back to "clear".
|
|
WMO_CODES = {
|
|
0: ("Klarer Himmel", "clear"),
|
|
1: ("Überwiegend klar", "clear"),
|
|
2: ("Teilweise bewölkt", "partly_cloudy"),
|
|
3: ("Bedeckt", "cloudy"),
|
|
45: ("Nebel", "fog"),
|
|
48: ("Nebel", "fog"),
|
|
51: ("Leichter Nieselregen", "rain"),
|
|
53: ("Nieselregen", "rain"),
|
|
55: ("Starker Nieselregen", "rain"),
|
|
61: ("Leichter Regen", "rain"),
|
|
63: ("Regen", "rain"),
|
|
65: ("Starker Regen", "rain"),
|
|
71: ("Leichter Schneefall", "snow"),
|
|
73: ("Schneefall", "snow"),
|
|
75: ("Starker Schneefall", "snow"),
|
|
80: ("Regenschauer", "rain"),
|
|
81: ("Regenschauer", "rain"),
|
|
82: ("Heftige Regenschauer", "rain"),
|
|
85: ("Schneeschauer", "snow"),
|
|
86: ("Schneeschauer", "snow"),
|
|
95: ("Gewitter", "thunder"),
|
|
96: ("Gewitter mit Hagel", "thunder"),
|
|
99: ("Gewitter mit Hagel", "thunder"),
|
|
}
|
|
|
|
|
|
def _cloud_shape(draw, cx, cy, w, h, color):
|
|
draw.rounded_rectangle((cx - w / 2, cy - h * 0.1, cx + w / 2, cy + h * 0.55), radius=h * 0.28, fill=color)
|
|
draw.ellipse((cx - w * 0.42, cy - h * 0.48, cx - w * 0.02, cy + h * 0.12), fill=color)
|
|
draw.ellipse((cx - w * 0.18, cy - h * 0.62, cx + w * 0.22, cy + h * 0.02), fill=color)
|
|
draw.ellipse((cx + w * 0.05, cy - h * 0.42, cx + w * 0.45, cy + h * 0.18), fill=color)
|
|
|
|
|
|
def draw_cloud(draw, cx, cy, size, outline=BLACK, fill=WHITE):
|
|
"""Simple line-art cloud: draw the silhouette in `outline`, then a
|
|
shrunk-down copy in `fill` on top -- fakes an outlined shape without
|
|
needing real path-offsetting."""
|
|
w, h = size, size * 0.7
|
|
border = max(2, size * 0.07)
|
|
_cloud_shape(draw, cx, cy, w, h, outline)
|
|
_cloud_shape(draw, cx, cy, w - border * 2.4, h - border * 2.4, fill)
|
|
|
|
|
|
def draw_sun(draw, cx, cy, r, color=YELLOW, outline=BLACK):
|
|
ray_w = max(2, int(r * 0.16))
|
|
for angle_deg in range(0, 360, 45):
|
|
rad = math.radians(angle_deg)
|
|
# Kept within roughly r*1.4 so the whole icon (rays included) stays
|
|
# inside the nominal size x size box callers lay out around it.
|
|
inner, outer = r * 1.15, r * 1.4
|
|
draw.line(
|
|
(cx + math.cos(rad) * inner, cy + math.sin(rad) * inner,
|
|
cx + math.cos(rad) * outer, cy + math.sin(rad) * outer),
|
|
fill=color, width=ray_w,
|
|
)
|
|
draw.ellipse((cx - r, cy - r, cx + r, cy + r), fill=color, outline=outline, width=max(2, int(r * 0.1)))
|
|
|
|
|
|
def _draw_snowflake(draw, cx, cy, r, color):
|
|
for angle_deg in range(0, 180, 60):
|
|
rad = math.radians(angle_deg)
|
|
draw.line(
|
|
(cx - math.cos(rad) * r, cy - math.sin(rad) * r,
|
|
cx + math.cos(rad) * r, cy + math.sin(rad) * r),
|
|
fill=color, width=max(2, int(r * 0.35)),
|
|
)
|
|
|
|
|
|
def draw_weather_icon(draw, kind: str, cx: float, cy: float, size: float):
|
|
"""Draws into a roughly `size` x `size` box centered on (cx, cy)."""
|
|
if kind == "clear":
|
|
draw_sun(draw, cx, cy, size * 0.34)
|
|
elif kind == "partly_cloudy":
|
|
draw_sun(draw, cx - size * 0.16, cy - size * 0.18, size * 0.22)
|
|
draw_cloud(draw, cx + size * 0.08, cy + size * 0.14, size * 0.75)
|
|
elif kind == "cloudy":
|
|
draw_cloud(draw, cx, cy, size * 0.9)
|
|
elif kind == "fog":
|
|
for i, dy in enumerate((-0.25, -0.05, 0.15, 0.35)):
|
|
band_w = size * (0.9 - i * 0.12)
|
|
yy = cy + size * dy
|
|
draw.line((cx - band_w / 2, yy, cx + band_w / 2, yy), fill=BLACK, width=max(2, int(size * 0.05)))
|
|
elif kind == "rain":
|
|
draw_cloud(draw, cx, cy - size * 0.12, size * 0.85)
|
|
for dx in (-0.28, -0.02, 0.24):
|
|
x0, y0 = cx + size * dx, cy + size * 0.3
|
|
draw.line((x0, y0, x0 - size * 0.08, y0 + size * 0.24), fill=BLUE, width=max(2, int(size * 0.05)))
|
|
elif kind == "snow":
|
|
draw_cloud(draw, cx, cy - size * 0.12, size * 0.85)
|
|
for dx in (-0.28, -0.02, 0.24):
|
|
_draw_snowflake(draw, cx + size * dx, cy + size * 0.38, size * 0.08, BLUE)
|
|
elif kind == "thunder":
|
|
draw_cloud(draw, cx, cy - size * 0.16, size * 0.85)
|
|
bolt = [
|
|
(cx + size * 0.05, cy + size * 0.16),
|
|
(cx - size * 0.08, cy + size * 0.42),
|
|
(cx + size * 0.02, cy + size * 0.42),
|
|
(cx - size * 0.1, cy + size * 0.7),
|
|
(cx + size * 0.18, cy + size * 0.36),
|
|
(cx + size * 0.06, cy + size * 0.36),
|
|
]
|
|
draw.polygon(bolt, fill=YELLOW, outline=BLACK)
|
|
else:
|
|
draw_sun(draw, cx, cy, size * 0.34)
|