503 lines
21 KiB
Python
503 lines
21 KiB
Python
"""Render-Pipeline für das Dashboard.
|
||
|
||
Unterstützte Display-Designs:
|
||
classic — Original flach, weiß, kein UI-Chrome (DEFAULT)
|
||
minimal — Schwarz/Weiß, bold, clean, viel Weißraum
|
||
minimal_color — Minimal + dezenter Akzent-Strich pro Kategorie
|
||
bold — Riesen-Typography, 1px-Rahmen, vollflächig
|
||
analog — 2×2-Uhr als Ziffernblatt, Rest clean Panels
|
||
magazine — Bold Typography, farbige Akzentstreifen links
|
||
cards — iOS-Widget-Style, Emoji-Icons, warme Paneele
|
||
kiosk — Dunkel, Daten-first, kompakte Info-Zellen
|
||
wireframe — Blueprint-Style, gestrichelte Rahmen, keine Füllungen
|
||
bauhaus — Geometrische Primärfarben, blockweise
|
||
poster — Farbige Flächen, bold, fast nur Content
|
||
retro_lcd — Alte LCD-Uhren-Ästhetik, grüner Hintergrund
|
||
glossy — Moderner iOS-Style, Emoji-Icons, Panels
|
||
data_board — Kontrollraum-Dashboard, dunkel, invertierte Karten
|
||
|
||
Aufruf:
|
||
img = render_design(items, widgets, fonts, design="classic")
|
||
"""
|
||
|
||
from PIL import Image, ImageDraw, ImageFont
|
||
from palette import FG, BG, WHITE, BLACK, GREEN, BLUE, RED, YELLOW, ORANGE, OK, WARN, ALERT, INFO, ACCENT
|
||
import math
|
||
|
||
DISPLAY_W = 800
|
||
DISPLAY_H = 480
|
||
COLS, ROWS = 4, 4
|
||
CELL_W = DISPLAY_W // COLS
|
||
CELL_H = DISPLAY_H // ROWS
|
||
|
||
|
||
def render_design(items, widgets, fonts, design="classic") -> Image.Image:
|
||
fn = {
|
||
"magazine": _render_magazine,
|
||
"cards": _render_cards,
|
||
"kiosk": _render_kiosk,
|
||
"wireframe": _render_wireframe,
|
||
"bauhaus": _render_bauhaus,
|
||
"poster": _render_poster,
|
||
"retro_lcd": _render_retro_lcd,
|
||
"glossy": _render_glossy,
|
||
"data_board": _render_data_board,
|
||
"minimal": _render_minimal,
|
||
"minimal_color": _render_minimal_color,
|
||
"bold": _render_bold,
|
||
"analog": _render_analog_clock,
|
||
"classic": _render_classic,
|
||
}.get(design, _render_classic)
|
||
return fn(items, widgets, fonts)
|
||
|
||
|
||
# ============================================================================
|
||
# MAGAZINE — Bold Typography, Minimalist
|
||
# Idee: Wanduhr-Magazin / Bloomberg-Terminal-Ästhetik
|
||
# Schwarz/Weiß + eine Akzentfarbe pro Widget
|
||
# ============================================================================
|
||
|
||
def _render_magazine(items, widgets, fonts) -> Image.Image:
|
||
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), WHITE)
|
||
draw = ImageDraw.Draw(img)
|
||
|
||
for item, widget in zip(items, widgets):
|
||
x, y, w, h = _pixels(item)
|
||
if widget is None:
|
||
continue
|
||
|
||
# Jedes Widget bekommt einen vertikalen Akzent-Streifen links
|
||
accent = _widget_accent(widget.name)
|
||
draw.rectangle((x, y, x + 6, y + h - 1), fill=accent)
|
||
|
||
# Widget-Name oben links, klein
|
||
label = (widget.label or widget.name or "").upper()
|
||
draw.text((x + 14, y + 6), label, font=fonts.get("16", fonts.get("default")), fill=(160, 160, 160))
|
||
|
||
# Horizontale Linie unter Label
|
||
draw.line((x + 14, y + 26, x + w - 10, y + 26), fill=(220, 220, 220), width=1)
|
||
|
||
# Content-Bereich (ab y+32)
|
||
cy = y + 36
|
||
ch = h - 42
|
||
widget.render(draw, fonts, x + 14, cy, w - 20, ch)
|
||
|
||
return img
|
||
|
||
|
||
# ============================================================================
|
||
# CARDS — Background Panels + Emoji Icons + Vertical Accent Bars
|
||
# Idee: iOS Widgets / Google Smart Display
|
||
# Leicht grauer Hintergrund pro Karte, Emoji für Kategorie
|
||
# ============================================================================
|
||
|
||
def _render_cards(items, widgets, fonts) -> Image.Image:
|
||
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), (245, 244, 240)) # warm white
|
||
draw = ImageDraw.Draw(img)
|
||
|
||
for item, widget in zip(items, widgets):
|
||
x, y, w, h = _pixels(item)
|
||
if widget is None:
|
||
continue
|
||
|
||
# Card background
|
||
draw.rectangle((x + 2, y + 2, x + w - 3, y + h - 3), fill=WHITE)
|
||
|
||
# Top accent bar
|
||
accent = _widget_accent(widget.name)
|
||
draw.rectangle((x + 2, y + 2, x + w - 3, y + 10), fill=accent)
|
||
|
||
# Emoji icon (use emoji renderer via fallback)
|
||
icon = _widget_icon(widget.name)
|
||
_draw_emoji(draw, icon, x + 10, y + 18, 28)
|
||
|
||
# Label
|
||
label = widget.label or widget.name or ""
|
||
draw.text((x + 44, y + 18), label, font=fonts.get("16", fonts.get("default")), fill=(80, 80, 80))
|
||
|
||
# Separator line
|
||
draw.line((x + 8, y + 52, x + w - 8, y + 52), fill=(230, 230, 230), width=1)
|
||
|
||
# Content
|
||
widget.render(draw, fonts, x + 8, y + 58, w - 16, h - 64)
|
||
|
||
return img
|
||
|
||
|
||
# ============================================================================
|
||
# KIOSK — Data-Dense, Compact, Multi-Zone
|
||
# Idee: Control-Room / LoRa-Tracker / Science-Dashboard
|
||
# Viele kleine Info-Zellen, horizontale Teilung, monochrome Palette
|
||
# ============================================================================
|
||
|
||
def _render_kiosk(items, widgets, fonts) -> Image.Image:
|
||
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), BLACK)
|
||
draw = ImageDraw.Draw(img)
|
||
|
||
for item, widget in zip(items, widgets):
|
||
x, y, w, h = _pixels(item)
|
||
if widget is None:
|
||
continue
|
||
|
||
# Invertierte Box mit Akzent-Rand
|
||
draw.rectangle((x, y, x + w - 1, y + h - 1), fill=WHITE)
|
||
accent = _widget_accent(widget.name)
|
||
draw.rectangle((x, y, x + w - 1, y + 3), fill=accent)
|
||
|
||
# Label in Akzentfarbe oben
|
||
label = (widget.label or widget.name or "").upper()
|
||
draw.text((x + 8, y + 8), label, font=fonts.get("14", fonts.get("default")), fill=accent)
|
||
|
||
# Content
|
||
widget.render(draw, fonts, x + 6, y + 28, w - 12, h - 34)
|
||
|
||
return img
|
||
|
||
|
||
# ============================================================================
|
||
# Helper
|
||
# ============================================================================
|
||
|
||
def _pixels(item):
|
||
"""Item (x,y,w,h) in Pixel (800x480, 4x4 grid)"""
|
||
COLS, ROWS = 4, 4
|
||
CELL_W = DISPLAY_W // COLS
|
||
CELL_H = DISPLAY_H // ROWS
|
||
return item.x * CELL_W, item.y * CELL_H, item.w * CELL_W, item.h * CELL_H
|
||
|
||
|
||
# Alias für neue Designs
|
||
def _px(item):
|
||
return _pixels(item)
|
||
|
||
|
||
def _widget_accent(name):
|
||
accents = {
|
||
"clock": BLUE,
|
||
"weather": GREEN,
|
||
"system": ORANGE,
|
||
"hello": RED,
|
||
"spotify": (0, 200, 0), # spotify green
|
||
"strava": ORANGE,
|
||
"gmail": RED,
|
||
"minimax": (180, 100, 255), # purple
|
||
}
|
||
return accents.get(name, ORANGE)
|
||
|
||
|
||
def _widget_icon(name):
|
||
icons = {
|
||
"clock": "⏰",
|
||
"weather": "🌤️",
|
||
"system": "💻",
|
||
"hello": "👋",
|
||
"spotify": "🎵",
|
||
"strava": "🚴",
|
||
"gmail": "📧",
|
||
"minimax": "🤖",
|
||
}
|
||
return icons.get(name, "📟")
|
||
|
||
|
||
def _draw_emoji(draw, emoji, x, y, size):
|
||
"""Draw emoji by using a fallback ImageFont trick with the emoji char."""
|
||
# On systems without an emoji font, this just draws a rectangle placeholder
|
||
# The real implementation would need a font with emoji support
|
||
# For now draw a colored square as placeholder
|
||
colors = {
|
||
"⏰": (80, 160, 255),
|
||
"🌤️": (255, 200, 80),
|
||
"💻": (100, 100, 100),
|
||
"👋": (255, 160, 120),
|
||
"🎵": (0, 200, 100),
|
||
"🚴": (255, 140, 0),
|
||
"📧": (220, 80, 80),
|
||
"🤖": (180, 100, 255),
|
||
}
|
||
c = colors.get(emoji, (200, 200, 200))
|
||
draw.ellipse((x, y, x + size, y + size), fill=c)
|
||
|
||
|
||
# ============================================================================
|
||
# DESIGN 5: WIREFRAME
|
||
# Technisches Plan-Drawing. Blaue Linien, weiße Flächen.
|
||
# Blueprint-Ästhetik.
|
||
# ============================================================================
|
||
def _render_wireframe(items, widgets, fonts) -> Image.Image:
|
||
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), (240, 248, 255))
|
||
draw = ImageDraw.Draw(img)
|
||
for item, widget in zip(items, widgets):
|
||
x, y, w, h = _px(item)
|
||
if widget is None:
|
||
continue
|
||
draw.rectangle((x + 2, y + 2, x + w - 3, y + h - 3), fill=WHITE)
|
||
draw.rectangle((x + 2, y + 2, x + w - 3, y + h - 3), outline=BLUE, width=1)
|
||
draw.rectangle((x + 2, y + 2, x + w - 3, y + 10), fill=BLUE)
|
||
label = (widget.label or widget.name or "").upper()
|
||
draw.text((x + 10, y + 14), label, font=fonts.get("14", fonts.get("default")), fill=BLUE)
|
||
for row in range(item.y, item.y + item.h):
|
||
for col in range(item.x, item.x + item.w):
|
||
if (row + col) % 2 == 0:
|
||
cx = col * CELL_W + 4
|
||
cy = row * CELL_H + 4
|
||
cw = CELL_W - 6
|
||
ch = CELL_H - 6
|
||
draw.rectangle((cx, cy, cx + cw, cy + ch), fill=(240, 248, 255))
|
||
widget.render(draw, fonts, x + 6, y + 32, w - 12, h - 38)
|
||
return img
|
||
|
||
|
||
# ============================================================================
|
||
# DESIGN 6: BAUHAUS
|
||
# Geometrische Primärfarben. Jedes Widget bekommt eine der
|
||
# 7 ACeP-Farben als Hintergrund-Balken links.
|
||
# ============================================================================
|
||
def _render_bauhaus(items, widgets, fonts) -> Image.Image:
|
||
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), (245, 245, 240))
|
||
draw = ImageDraw.Draw(img)
|
||
PALETTE_7 = [RED, GREEN, BLUE, YELLOW, ORANGE, (0, 200, 200), (200, 100, 200)]
|
||
for i, (item, widget) in enumerate(zip(items, widgets)):
|
||
x, y, w, h = _px(item)
|
||
if widget is None:
|
||
continue
|
||
color = PALETTE_7[i % len(PALETTE_7)]
|
||
block_w = w // 3
|
||
draw.rectangle((x + 2, y + 2, x + block_w - 2, y + h - 3), fill=color)
|
||
draw.rectangle((x + block_w, y + 2, x + w - 3, y + h - 3), fill=WHITE)
|
||
draw.rectangle((x + 2, y + 2, x + w - 3, y + h - 3), outline=FG, width=2)
|
||
label = (widget.label or widget.name or "").upper()
|
||
draw.text((x + 6, y + 8), label, font=fonts.get("14", fonts.get("default")), fill=WHITE)
|
||
widget.render(draw, fonts, x + block_w + 4, y + 4, w - block_w - 8, h - 8)
|
||
return img
|
||
|
||
|
||
# ============================================================================
|
||
# DESIGN 7: POSTER
|
||
# Farbige Flächen, bold, fast nur Content.
|
||
# Jedes Widget in einer kräftigen Farbe.
|
||
# ============================================================================
|
||
def _render_poster(items, widgets, fonts) -> Image.Image:
|
||
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), BLACK)
|
||
draw = ImageDraw.Draw(img)
|
||
colors = [RED, GREEN, BLUE, YELLOW, ORANGE, (0, 200, 200), (200, 100, 200)]
|
||
for i, (item, widget) in enumerate(zip(items, widgets)):
|
||
x, y, w, h = _px(item)
|
||
if widget is None:
|
||
continue
|
||
c = colors[i % len(colors)]
|
||
draw.rectangle((x, y, x + w - 1, y + h - 1), fill=c)
|
||
draw.rectangle((x, y, x + w - 1, y + h - 1), outline=WHITE, width=3)
|
||
label = (widget.label or widget.name or "").upper()
|
||
draw.text((x + 8, y + 6), label[:18], font=fonts.get("12", fonts.get("default")), fill=WHITE)
|
||
widget.render(draw, fonts, x + 6, y + 24, w - 12, h - 30)
|
||
return img
|
||
|
||
|
||
# ============================================================================
|
||
# DESIGN 8: RETRO LCD
|
||
# Alte LCD-Uhren-Ästhetik. Dunkelgrüner Hintergrund,
|
||
# leuchtende grüne Ziffern, Fake-3D-Rahmen.
|
||
# ============================================================================
|
||
def _render_retro_lcd(items, widgets, fonts) -> Image.Image:
|
||
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), (0, 20, 0))
|
||
draw = ImageDraw.Draw(img)
|
||
LCD_GREEN = (0, 255, 120)
|
||
LCD_DARK = (0, 60, 30)
|
||
for item, widget in zip(items, widgets):
|
||
x, y, w, h = _px(item)
|
||
if widget is None:
|
||
continue
|
||
draw.rectangle((x + 3, y + 3, x + w - 4, y + h - 4), fill=LCD_DARK)
|
||
draw.rectangle((x + 1, y + 1, x + w - 2, y + h - 2), outline=LCD_GREEN, width=2)
|
||
label = (widget.label or widget.name or "").upper()
|
||
draw.text((x + 8, y + 6), label[:16], font=fonts.get("11", fonts.get("default")), fill=LCD_GREEN)
|
||
draw.line((x + 8, y + 22, x + w - 8, y + 22), fill=LCD_GREEN, width=1)
|
||
widget.render(draw, fonts, x + 6, y + 26, w - 12, h - 32)
|
||
return img
|
||
|
||
|
||
# ============================================================================
|
||
# DESIGN 9: GLOSSY
|
||
# Moderne iOS-Widget-Ästhetik. Weiße Karten, Glossy-Header
|
||
# in Accent-Farbe, dezente Schatten-Rahmen.
|
||
# ============================================================================
|
||
def _render_glossy(items, widgets, fonts) -> Image.Image:
|
||
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), (220, 228, 235))
|
||
draw = ImageDraw.Draw(img)
|
||
WIDGET_ACCENTS = {
|
||
"clock": (80, 160, 255),
|
||
"weather": (60, 200, 100),
|
||
"system": (255, 160, 40),
|
||
"hello": (200, 80, 80),
|
||
"spotify": (0, 180, 80),
|
||
"strava": (255, 120, 0),
|
||
"gmail": (200, 60, 60),
|
||
"minimax": (160, 80, 255),
|
||
}
|
||
for item, widget in zip(items, widgets):
|
||
x, y, w, h = _px(item)
|
||
if widget is None:
|
||
continue
|
||
accent = WIDGET_ACCENTS.get(widget.name, (100, 100, 100))
|
||
draw.rectangle((x + 2, y + 2, x + w - 2, y + h - 2), fill=WHITE)
|
||
draw.rectangle((x + 2, y + 2, x + w - 2, y + 14), fill=accent)
|
||
# Glossy highlight
|
||
for i in range(6):
|
||
alpha = int(80 - i * 12)
|
||
c = tuple(min(255, accent[j] + alpha) for j in range(3))
|
||
draw.line((x + 2, y + 2 + i, x + w - 2, y + 2 + i), fill=c, width=1)
|
||
draw.rectangle((x + 2, y + 2, x + w - 2, y + h - 2), outline=(200, 210, 220), width=1)
|
||
draw.rectangle((x + 3, y + 3, x + w - 3, y + h - 3), outline=(220, 228, 235), width=1)
|
||
draw.rectangle((x + 4, y + 4, x + w - 4, y + h - 4), outline=(180, 190, 200), width=1)
|
||
label = (widget.label or widget.name or "").title()
|
||
draw.text((x + 10, y + 18), label, font=fonts.get("14", fonts.get("default")), fill=accent)
|
||
draw.line((x + 8, y + 36, x + w - 8, y + 36), fill=(220, 220, 220), width=1)
|
||
widget.render(draw, fonts, x + 6, y + 40, w - 12, h - 46)
|
||
return img
|
||
|
||
|
||
# ============================================================================
|
||
# DESIGN 10: DATA BOARD
|
||
# Kontrollraum-Ästhetik. Winzige Zellen, viele Daten.
|
||
# Monochrome, Tick-Marks, Data-first.
|
||
# ============================================================================
|
||
def _render_data_board(items, widgets, fonts) -> Image.Image:
|
||
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), (10, 10, 15))
|
||
draw = ImageDraw.Draw(img)
|
||
for item, widget in zip(items, widgets):
|
||
x, y, w, h = _px(item)
|
||
if widget is None:
|
||
continue
|
||
draw.rectangle((x, y, x + w - 1, y + h - 1), fill=WHITE, width=2)
|
||
draw.rectangle((x, y, x + w - 1, y + 22), fill=WHITE)
|
||
draw.text((x + 6, y + 4), (widget.label or widget.name or "").upper(),
|
||
font=fonts.get("12", fonts.get("default")), fill=BLACK)
|
||
for tx in range(x + 10, x + w - 10, 20):
|
||
draw.line((tx, y + 24, tx, y + 28), fill=(180, 180, 180), width=1)
|
||
widget.render(draw, fonts, x + 4, y + 30, w - 8, h - 34)
|
||
return img
|
||
|
||
|
||
# ============================================================================
|
||
# CLASSIC — Original flaches Design
|
||
# Weißer Hintergrund, kein UI-Chrome, widget füllt Zelle aus
|
||
# Das was auf deinem Referenz-Bild zu sehen ist
|
||
# ============================================================================
|
||
def _render_classic(items, widgets, fonts) -> Image.Image:
|
||
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), WHITE)
|
||
draw = ImageDraw.Draw(img)
|
||
for item, widget in zip(items, widgets):
|
||
x, y, w, h = _px(item)
|
||
if widget is None:
|
||
continue
|
||
widget.render(draw, fonts, x + 8, y + 8, w - 16, h - 16)
|
||
return img
|
||
|
||
|
||
# ============================================================================
|
||
# MINIMAL — das Referenz-Design
|
||
# Schwarz/Weiß, bold, clean, viel Weißraum. Kein UI-Chrome.
|
||
# ============================================================================
|
||
def _render_minimal(items, widgets, fonts) -> Image.Image:
|
||
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), WHITE)
|
||
draw = ImageDraw.Draw(img)
|
||
for item, widget in zip(items, widgets):
|
||
x, y, w, h = _px(item)
|
||
if widget is None:
|
||
continue
|
||
widget.render(draw, fonts, x + 12, y + 12, w - 24, h - 24)
|
||
return img
|
||
|
||
|
||
# ============================================================================
|
||
# MINIMAL COLOR — Minimal mit dezenter Akzentfarbe pro Kategorie
|
||
# Kleiner Farbbalken oben links, sonst weiß + schwarz
|
||
# ============================================================================
|
||
def _render_minimal_color(items, widgets, fonts) -> Image.Image:
|
||
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), WHITE)
|
||
draw = ImageDraw.Draw(img)
|
||
CATEGORY_COLORS = {
|
||
"clock": (60, 80, 200),
|
||
"weather": (50, 160, 80),
|
||
"system": (200, 120, 30),
|
||
"hello": (180, 60, 60),
|
||
"spotify": (0, 160, 80),
|
||
"strava": (230, 110, 20),
|
||
"gmail": (200, 60, 60),
|
||
"minimax": (140, 80, 240),
|
||
}
|
||
for item, widget in zip(items, widgets):
|
||
x, y, w, h = _px(item)
|
||
if widget is None:
|
||
continue
|
||
color = CATEGORY_COLORS.get(widget.name, FG)
|
||
draw.rectangle((x + 12, y + 10, x + 16, y + 18), fill=color)
|
||
label = (widget.label or widget.name or "").upper()
|
||
draw.text((x + 22, y + 8), label, font=fonts.get("11", fonts.get("default")), fill=(160, 160, 160))
|
||
widget.render(draw, fonts, x + 12, y + 24, w - 24, h - 36)
|
||
return img
|
||
|
||
|
||
# ============================================================================
|
||
# BOLD — Riesen-Typography
|
||
# Zahlen und Text füllen die Zelle komplett.
|
||
# Kaum Weißraum, maximaler Informationsdichte.
|
||
# ============================================================================
|
||
def _render_bold(items, widgets, fonts) -> Image.Image:
|
||
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), WHITE)
|
||
draw = ImageDraw.Draw(img)
|
||
for item, widget in zip(items, widgets):
|
||
x, y, w, h = _px(item)
|
||
if widget is None:
|
||
continue
|
||
draw.rectangle((x + 1, y + 1, x + w - 2, y + h - 2), outline=BLACK, width=1)
|
||
widget.render(draw, fonts, x + 6, y + 6, w - 12, h - 12)
|
||
return img
|
||
|
||
|
||
# ============================================================================
|
||
# ANALOG CLOCK — Die Uhr als Mittelpunkt
|
||
# 2x2-Clock-Zellen zeigen ein analoges Ziffernblatt.
|
||
# Übrige Zellen: cleanes Panel mit Monospace.
|
||
# ============================================================================
|
||
def _render_analog_clock(items, widgets, fonts) -> Image.Image:
|
||
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), (245, 245, 240))
|
||
draw = ImageDraw.Draw(img)
|
||
import math
|
||
for item, widget in zip(items, widgets):
|
||
x, y, w, h = _px(item)
|
||
if widget is None:
|
||
continue
|
||
is_2x2 = item.w >= 2 and item.h >= 2
|
||
is_clock = widget.name == "clock" and is_2x2
|
||
if is_clock:
|
||
cx, cy = x + w // 2, y + h // 2
|
||
r = min(w, h) // 2 - 16
|
||
draw.ellipse((cx - r, cy - r, cx + r, cy + r), fill=WHITE, outline=BLACK, width=2)
|
||
for i in range(12):
|
||
angle = (i / 12) * 2 * math.pi - math.pi / 2
|
||
mx = cx + int((r - 12) * math.cos(angle))
|
||
my = cy + int((r - 12) * math.sin(angle))
|
||
draw.ellipse((mx - 3, my - 3, mx + 3, my + 3), fill=BLACK)
|
||
from datetime import datetime
|
||
now = datetime.now()
|
||
hour_angle = ((now.hour % 12) / 12) * 2 * math.pi - math.pi / 2
|
||
min_angle = (now.minute / 60) * 2 * math.pi - math.pi / 2
|
||
hx = cx + int(r * 0.5 * math.cos(hour_angle))
|
||
hy = cy + int(r * 0.5 * math.sin(hour_angle))
|
||
draw.line((cx, cy, hx, hy), fill=BLACK, width=4)
|
||
mx = cx + int(r * 0.75 * math.cos(min_angle))
|
||
my = cy + int(r * 0.75 * math.sin(min_angle))
|
||
draw.line((cx, cy, mx, my), fill=BLACK, width=2)
|
||
draw.ellipse((cx - 4, cy - 4, cx + 4, cy + 4), fill=BLACK)
|
||
draw.text((cx - 40, cy + r + 8), now.strftime("%H:%M"),
|
||
font=fonts.get("20", fonts.get("default")), fill=BLACK)
|
||
else:
|
||
draw.rectangle((x + 2, y + 2, x + w - 2, y + h - 2), fill=WHITE)
|
||
draw.ellipse((x + 8, y + 8, x + 14, y + 14), fill=(60, 100, 200))
|
||
label = (widget.label or widget.name or "").upper()[:12]
|
||
draw.text((x + 20, y + 6), label, font=fonts.get("12", fonts.get("default")), fill=(100, 100, 100))
|
||
draw.line((x + 8, y + 22, x + w - 8, y + 22), fill=(200, 200, 200), width=1)
|
||
widget.render(draw, fonts, x + 6, y + 26, w - 12, h - 34)
|
||
return img
|