Files
epaper-dashboard/renderer2.py
T
ki 49de61bb26 Linear design system: new admin UI + display design switcher (8 designs)
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
2026-08-26 15:08:17 +04:00

273 lines
9.9 KiB
Python

"""Neue Display-Designs für ACeP 7-Farben e-Paper.
Jedes Design definiert:
- Hintergrund / Rahmen
- Widget-Rahmen ( Frames)
- content_fn: wie das Widget gerendert wird
"""
from PIL import Image, ImageDraw
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
# ============================================================================
# DESIGN 5: WIREFRAME
# Technisches Plan-Drawing. Blaue Linien, weiße Flächen,
# schmale weiße Zellenrahmen. Blueprint-Ästhetik.
# ============================================================================
def render_wireframe(items, widgets, fonts) -> Image.Image:
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), (240, 248, 255)) # aliceblue
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 pro Zelle
draw.rectangle((x + 2, y + 2, x + w - 3, y + h - 3), fill=WHITE)
# Dünner Rahmen (blau)
draw.rectangle((x + 2, y + 2, x + w - 3, y + h - 3), outline=BLUE, width=1)
# Obere Akzent-Linie (breit)
draw.rectangle((x + 2, y + 2, x + w - 3, y + 10), fill=BLUE)
# Widget-Name oben links
label = (widget.label or widget.name or "").upper()
draw.text((x + 10, y + 14), label, font=fonts.get("14", fonts.get("default")), fill=BLUE)
# Grid-Checkerboard im Hintergrund (subtle)
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))
# Content
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. Reine Formen.
# ============================================================================
def render_bauhaus(items, widgets, fonts) -> Image.Image:
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), (245, 245, 240)) # warm gray
draw = ImageDraw.Draw(img)
PALETTE_7 = [RED, GREEN, BLUE, YELLOW, ORANGE, (0, 200, 200), (200, 100, 200)]
item_colors = {}
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)]
item_colors[item.id] = color
# Großer farbiger Block links (30% width)
block_w = w // 3
draw.rectangle((x + 2, y + 2, x + block_w - 2, y + h - 3), fill=color)
# Rest weiß
draw.rectangle((x + block_w, y + 2, x + w - 3, y + h - 3), fill=WHITE)
# Rahmen
draw.rectangle((x + 2, y + 2, x + w - 3, y + h - 3), outline=FG, width=2)
# Widget-Name in der farbigen Fläche
label = (widget.label or widget.name or "").upper()
draw.text((x + 6, y + 8), label, font=fonts.get("14", fonts.get("default")), fill=WHITE)
# Content (rechte Seite)
widget.render(draw, fonts, x + block_w + 4, y + 4, w - block_w - 8, h - 8)
return img
# ============================================================================
# DESIGN 7: POSTER
# Überdimensionierte Typography. Jedes Widget großflächig.
# Widget-Hintergrund = monochrome Farbe aus Palette.
# Fast nur Text, kein UI-Chrome.
# ============================================================================
def render_poster(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 = _px(item)
if widget is None:
continue
# Zufällige kräftige Farbe pro widget (basierend auf name hash)
c = _hash_color(widget.name)
draw.rectangle((x, y, x + w - 1, y + h - 1), fill=c)
# Weißer Rahmen drum
draw.rectangle((x, y, x + w - 1, y + h - 1), outline=WHITE, width=3)
# Label oben Winzig
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)
# Content — das Widget bekommt 90% der Fläche
# Wir übergeben ein invertiertes draw-Objekt (fg=WHITE, bg=c)
widget.render(draw, fonts, x + 6, y + 24, w - 12, h - 30)
return img
# ============================================================================
# DESIGN 8: RETRO LCD
# Alte LCD/Uhren-Ästhetik. Schwarzer Hintergrund, Ziffern in
# satten UnOld Green (LCD-STYLE). Rahmen mit abgerundeten Ecken,
# die wie eingeritzte Rillen aussehen.
# ============================================================================
def render_retro_lcd(items, widgets, fonts) -> Image.Image:
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), (0, 20, 0)) # dunkles LCD-grün
draw = ImageDraw.Draw(img)
LCD_GREEN = (0, 255, 120)
LCD_DARK = (0, 60, 30)
LCD_BG = (0, 30, 15)
for item, widget in zip(items, widgets):
x, y, w, h = _px(item)
if widget is None:
continue
# LCD-Zelle: dunkler als Hintergrund
draw.rectangle((x + 3, y + 3, x + w - 4, y + h - 4), fill=LCD_DARK)
# Rahmen mit Fake-3D (oben/links hell, unten/rechts dunkel)
draw.rectangle((x + 1, y + 1, x + w - 2, y + h - 2), outline=LCD_GREEN, width=2)
# Label in kleiner Schrift oben
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)
# Separator-Linie
draw.line((x + 8, y + 22, x + w - 8, y + 22), fill=LCD_GREEN, width=1)
# Content
widget.render(draw, fonts, x + 6, y + 26, w - 12, h - 32)
return img
# ============================================================================
# DESIGN 9: GLOSSY
# Moderner Glossy-Style. Weiße Karten mit leichtem Farbverlauf oben,
# ikonischer Rand, dezente Schatten durch mehrfache Ränder.
# ============================================================================
def render_glossy(items, widgets, fonts) -> Image.Image:
img = Image.new("RGB", (DISPLAY_W, DISPLAY_H), (220, 228, 235)) # cool gray
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))
# Karte weiß
draw.rectangle((x + 2, y + 2, x + w - 2, y + h - 2), fill=WHITE)
# Glossy-Effekt: heller Balken oben
draw.rectangle((x + 2, y + 2, x + w - 2, y + 14), fill=accent)
# Weißer Gradient-Overlay oben
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)
# Rahmeneffekt: 3-lagig für Schatten
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 in Accent-Farbe
label = (widget.label or widget.name or "").title()
draw.text((x + 10, y + 18), label, font=fonts.get("14", fonts.get("default")), fill=accent)
# Separator
draw.line((x + 8, y + 36, x + w - 8, y + 36), fill=(220, 220, 220), width=1)
# Content
widget.render(draw, fonts, x + 6, y + 40, w - 12, h - 46)
return img
# ============================================================================
# DESIGN 10: DATA BOARD
# Kontrollraum-Ästhetik. Winzige Zellen, viele Daten.
# Horizontale Balken, Tick-Marks, monochrome Palette.
#===========================================================================
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
# Weißer Rand
draw.rectangle((x, y, x + w - 1, y + h - 1), fill=WHITE, width=2)
# Header-Balken (invertiert)
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)
# Kleine Tick-Marks oben (dekorativ)
for tx in range(x + 10, x + w - 10, 20):
draw.line((tx, y + 24, tx, y + 28), fill=(180, 180, 180), width=1)
# Content
widget.render(draw, fonts, x + 4, y + 30, w - 8, h - 34)
return img
# ============================================================================
# Helper
# ============================================================================
def _px(item):
return item.x * CELL_W, item.y * CELL_H, item.w * CELL_W, item.h * CELL_H
def _hash_color(name: str):
"""Consistent color from name using simple hash."""
colors = [RED, GREEN, BLUE, YELLOW, ORANGE, (0, 200, 200), (200, 0, 200)]
h = sum(ord(c) for c in name)
return colors[h % len(colors)]