Admin UI: Linear.app dark theme — near-black canvas, indigo-violet accent, Inter font Display: Minimal, Minimal Color, Bold, Analog Clock, Magazine, Cards, Kiosk, Classic
204 lines
7.2 KiB
Python
204 lines
7.2 KiB
Python
"""Minimal Display-Designs für ACeP 7-Farben.
|
|
|
|
Inspiration von loiccoyle/tinyticker + inkflow-eink:
|
|
- Weißer Hintergrund, schwarze Geometrie
|
|
- Bold Typography, große Zahlen
|
|
- SVG-style rendering (saubere Linien, einheitliche Strichstärke)
|
|
- Dezente Akzentfarbe pro Widget-Kategorie
|
|
"""
|
|
|
|
from PIL import Image, ImageDraw
|
|
from palette import FG, BG, WHITE, BLACK, GREEN, BLUE, RED, YELLOW, ORANGE, OK, WARN, ALERT, INFO, ACCENT
|
|
|
|
DISPLAY_W = 800
|
|
DISPLAY_H = 480
|
|
COLS, ROWS = 4, 4
|
|
CELL_W = DISPLAY_W // COLS
|
|
CELL_H = DISPLAY_H // ROWS
|
|
|
|
# Einheitliche Strichstärke
|
|
STROKE = 2 # px für alle Linien
|
|
SMALL_STROKE = 1
|
|
|
|
|
|
def render_design(items, widgets, fonts, design="minimal") -> Image.Image:
|
|
fn = {
|
|
"minimal": _render_minimal,
|
|
"minimal_color": _render_minimal_color,
|
|
"bold": _render_bold,
|
|
"analog": _render_analog_clock,
|
|
}.get(design, _render_minimal)
|
|
return fn(items, widgets, fonts)
|
|
|
|
|
|
# ============================================================================
|
|
# MINIMAL — das Referenz-Design
|
|
# Schwarz/Weiß, bold, clean, viel Weißraum
|
|
# ============================================================================
|
|
def _render_minimal(items, widgets, fonts) -> Image.Image:
|
|
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), WHITE)
|
|
draw = ImageDraw.Draw(img)
|
|
|
|
# Keine sichtbaren Grid-Linien — jedes Widget steht für sich
|
|
for item, widget in zip(items, widgets):
|
|
x, y, w, h = _px(item)
|
|
if widget is None:
|
|
continue
|
|
|
|
# Kein Rahmen, kein Hintergrund — reiner Content auf Weiß
|
|
# Content füllt die ganze Zelle
|
|
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), # blau
|
|
"weather": (50, 160, 80), # grün
|
|
"system": (200, 120, 30), # orange
|
|
"hello": (180, 60, 60), # rot
|
|
"spotify": (0, 160, 80), # spotify-grün
|
|
"strava": (230, 110, 20), # strava-orange
|
|
"gmail": (200, 60, 60), # gmail-rot
|
|
"minimax": (140, 80, 240), # lila
|
|
}
|
|
|
|
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)
|
|
|
|
# Winziger Akzent-Strich oben (8px hoch, 3px breit)
|
|
draw.rectangle((x + 12, y + 10, x + 16, y + 18), fill=color)
|
|
|
|
# Widget-Name winzig darüber
|
|
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))
|
|
|
|
# Content
|
|
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
|
|
|
|
# Weißer Hintergrund, schwarzer Rahmen (1px)
|
|
draw.rectangle((x + 1, y + 1, x + w - 2, y + h - 2), outline=BLACK, width=1)
|
|
|
|
# Content — groß
|
|
widget.render(draw, fonts, x + 6, y + 6, w - 12, h - 12)
|
|
|
|
return img
|
|
|
|
|
|
# ============================================================================
|
|
# ANALOG CLOCK — Die Uhr als Mittelpunkt
|
|
# Mittlere Zellen (2x2) zeigen die Uhr als analoges Ziffernblatt.
|
|
# Kleine Zellen (1x1) zeigen minimalistische Icons.
|
|
# Übrige Zellen: Daten-Widget mit Monospace-Text.
|
|
# ============================================================================
|
|
def _render_analog_clock(items, widgets, fonts) -> Image.Image:
|
|
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), (245, 245, 240)) # warm white
|
|
draw = ImageDraw.Draw(img)
|
|
|
|
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:
|
|
# 2x2 Clock → Analoges Ziffernblatt
|
|
_draw_analog_clock(draw, x, y, w, h, fonts)
|
|
else:
|
|
# Andere Widgets: cleanes Panel
|
|
draw.rectangle((x + 2, y + 2, x + w - 2, y + h - 2), fill=WHITE)
|
|
# Kleiner farbiger Punkt oben links
|
|
color = (60, 100, 200)
|
|
draw.ellipse((x + 8, y + 8, x + 14, y + 14), fill=color)
|
|
# Label
|
|
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))
|
|
# Separator
|
|
draw.line((x + 8, y + 22, x + w - 8, y + 22), fill=(200, 200, 200), width=1)
|
|
# Content
|
|
widget.render(draw, fonts, x + 6, y + 26, w - 12, h - 34)
|
|
|
|
return img
|
|
|
|
|
|
def _draw_analog_clock(draw, x, y, w, h, fonts):
|
|
"""Zeichne ein analoges Ziffernblatt."""
|
|
import math
|
|
cx, cy = x + w // 2, y + h // 2
|
|
r = min(w, h) // 2 - 16
|
|
|
|
# Ziffernblatt-Kreis
|
|
draw.ellipse((cx - r, cy - r, cx + r, cy + r), fill=WHITE, outline=BLACK, width=STROKE)
|
|
|
|
# Stunden-Markierungen (kleine punkte)
|
|
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)
|
|
|
|
# Zeiger
|
|
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
|
|
|
|
# Stundenzeiger
|
|
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)
|
|
|
|
# Minutenzeiger
|
|
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)
|
|
|
|
# Mittelpunkt
|
|
draw.ellipse((cx - 4, cy - 4, cx + 4, cy + 4), fill=BLACK)
|
|
|
|
# Digitale Zeit darunter
|
|
draw.text((cx - 40, cy + r + 8),
|
|
now.strftime("%H:%M"),
|
|
font=fonts.get("20", fonts.get("default")), fill=BLACK)
|
|
|
|
|
|
# ============================================================================
|
|
# Helper
|
|
# ============================================================================
|
|
def _px(item):
|
|
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
|