e-os-v4 / scripts /os4_draw.py
KillerBoss's picture
Upload folder using huggingface_hub
c978bcc verified
Raw History Blame Contribute Delete
29.8 kB
# E-OS v4 — dual-theme scene renderer.
# draw_scene(st, theme) returns a PIL RGB image at FULL resolution (384x832).
# theme="hint" -> rough flat layout (the generator's conditioning input)
# theme="styled" -> rich ground-truth frame (gradients, shadows, glows, anims)
# Both themes share EXACT geometry so the model can learn "upscale + restyle".
import math
import os
import colorsys
import numpy as np
from PIL import Image, ImageDraw, ImageFilter, ImageFont
import os4_spec as S
FDIR = "/usr/share/fonts/truetype/dejavu"
_fc = {}
def _font(sz, bold=False):
k = (sz, bold)
if k not in _fc:
n = "DejaVuSans-Bold.ttf" if bold else "DejaVuSans.ttf"
_fc[k] = ImageFont.truetype(os.path.join(FDIR, n), sz)
return _fc[k]
def _lerp(a, b, t): return a + (b - a) * t
def _lerpc(c1, c2, t): return tuple(int(round(_lerp(c1[i], c2[i], t))) for i in range(3))
def accent_rgb(hue, l=0.60, s=0.68):
r, g, b = colorsys.hls_to_rgb((hue % 360) / 360.0, l, s)
return (int(r * 255), int(g * 255), int(b * 255))
def warmth(phase):
g1 = math.exp(-((phase - 0.79) / 0.075) ** 2) # evening
g2 = 0.45 * math.exp(-((phase - 0.29) / 0.07) ** 2) # morning
return min(1.0, g1 + g2)
def day_light(phase):
return max(0.0, min(1.0, 0.5 * math.cos((phase - 0.5) * 2 * math.pi) + 0.5))
# ---------------------------------------------------------------- scenes ---
def fireflies(tsec, n=9):
out = []
for i in range(n):
sp = 0.010 + 0.022 * ((i * 37) % 7) / 7.0
x = ((i * 0.618034 + tsec * sp) % 1.25 - 0.125) * S.FULL_W
y = 120 + ((i * 0.37 + 0.13 * math.sin(tsec * 0.7 + i)) % 1.0) * (S.FULL_H - 260)
out.append((x, y, 0.55 + 0.45 * math.sin(tsec * 1.7 + i * 2.1)))
return out
def music_bars(st):
lvl = st["music_level"]; t = st["tsec"]
bars = []
for i in range(24):
if st["music_playing"]:
v = abs(math.sin(t * 2.6 + i * 0.83)) * 0.72 + 0.28 * abs(math.sin(t * 7.1 + i * 2.3))
else:
v = 0.18 + 0.10 * ((i * 13) % 5) / 4.0
bars.append(max(0.06, min(1.0, v * (0.35 + 0.65 * lvl))))
return bars
def weather_state(st):
cond = st["weather_cond"]; t = st["tsec"]
if cond == 0: # sunny: rotating rays
return [("sun", 292, 296, t * 0.9)]
if cond == 1: # clouds drifting
return [("cloud", (t * 9 + i * 160) % (S.FULL_W + 120) - 60, 268 + (i % 2) * 62, 0.8 + 0.25 * i)
for i in range(3)]
if cond == 2: # rain
drops = [(28 + (i * 23) % 340, 216 + (t * 300 + i * 47) % 200) for i in range(16)]
return [("cloud", 120, 236, 1.0), ("cloud", 268, 248, 0.9), ("rain", drops, 0)]
return [("stars", t, 0)] # night clear
# ---------------------------------------------------------------- helpers --
def _rr(d, box, rad, fill=None, outline=None, width=1):
d.rounded_rectangle(box, radius=rad, fill=fill, outline=outline, width=width)
def _txt(d, xy, s, sz, fill, bold=False, anchor="la"):
d.text(xy, s, font=_font(sz, bold), fill=fill, anchor=anchor)
def _arrow_left(d, cx, cy, sz, fill):
d.polygon([(cx + sz * 0.5, cy - sz), (cx + sz * 0.5, cy + sz), (cx - sz * 0.7, cy)], fill=fill)
d.line([(cx - sz * 0.7, cy), (cx + sz * 0.5, cy)], fill=fill, width=max(2, sz // 3))
def _home_glyph(d, cx, cy, sz, fill):
d.polygon([(cx, cy - sz), (cx - sz, cy), (cx + sz, cy)], fill=fill)
d.rectangle([cx - sz * 0.72, cy, cx + sz * 0.72, cy + sz * 0.85], fill=fill)
def _apps_glyph(d, cx, cy, sz, fill):
g = sz * 0.62
for dx in (-1, 1):
for dy in (-1, 1):
d.ellipse([cx + dx * g - sz * 0.32, cy + dy * g - sz * 0.32,
cx + dx * g + sz * 0.32, cy + dy * g + sz * 0.32], fill=fill)
# ------------------------------------------------------------- one screen --
def _screen_base(d, st, th, acc, dim, txtc):
if th == "hint":
d.rectangle([0, 0, S.FULL_W, S.FULL_H], fill=S.H_BG)
else:
_gradient_bg(d, st, acc)
def _gradient_bg(d, st, acc):
ph = st["phase"]; dl = day_light(ph); wm = warmth(ph)
top = _lerpc((8, 10, 18), (26, 32, 48), dl * 0.8)
top = _lerpc(top, (46, 30, 26), wm * 0.35)
bot = _lerpc(top, (2, 3, 8), 0.75)
h = np.linspace(0, 1, S.FULL_H, dtype=np.float32)[:, None]
a = np.array(top, np.float32)[None, :]
b = np.array(bot, np.float32)[None, :]
col = (a * (1 - h) + b * h).astype(np.uint8) # (H,3)
img = np.broadcast_to(col[:, None, :], (S.FULL_H, S.FULL_W, 3)).copy()
return img
def _draw_screen(idx, d, st, th, C, layers):
"""C = color bundle for current theme."""
acc = C["acc"]; dim = C["dim"]; txtc = C["txt"]; card = C["card"]
scr = S.SCREENS[idx]
if scr == "LOCK": _lock(d, st, th, C)
elif scr == "HOME": _home(d, st, th, C)
elif scr == "DRAWER": _drawer(d, st, th, C)
elif scr == "CALC": _calc(d, st, th, C)
elif scr == "WEATHER":_weather(d, st, th, C)
elif scr == "MUSIC": _music(d, st, th, C)
elif scr == "SETTINGS": _settings(d, st, th, C)
else: _about(d, st, th, C)
# ------------------------------------------------------------------ LOCK ---
def _lock(d, st, th, C):
acc = C["acc"]; dim = C["dim"]; txtc = C["txt"]
# aurora bands
for i in range(3):
ph = st["tsec"] * (0.05 + i * 0.02) + i * 2.1
x = int((0.5 + 0.42 * math.sin(ph)) * S.FULL_W)
col = C["aurora"][i] if th != "hint" else (44, 50, 62)
if th == "hint":
d.polygon([(x - 90, 0), (x + 40, 0), (x - 60, S.FULL_H), (x - 190, S.FULL_H)], fill=col)
else:
C["aurora_pts"].append((x, i))
hh, mm = st["clock"][0], st["clock"][1]
_txt(d, (S.FULL_W // 2, 128), "%02d:%02d" % (hh, mm), 56, txtc, bold=True, anchor="mm")
_txt(d, (S.FULL_W // 2, 186), st["date"], 13, dim, anchor="mm")
card = C["card"]
_rr(d, (32, 300, 352, 380), 14, fill=card)
_txt(d, (48, 314), "Benachrichtigung", 11, C["acc"] if th != "hint" else C["acc"], bold=True)
_txt(d, (48, 334), "E-OS läuft · NPU-Rendering aktiv", 12, txtc)
_txt(d, (48, 354), "Tippe für Effekte · wische nach oben", 10, dim)
# weather line
_txt(d, (S.FULL_W // 2, 404), "%s · %s %d°" % (st["city"], S.CONDS[st["weather_cond"]], st["temp"]),
13, dim, anchor="mm")
# unlock pill
_rr(d, (92, 700, 292, 746), 23, fill=C["card2"], outline=C["stroke"], width=1)
_txt(d, (S.FULL_W // 2 + 8, 723), "Nach oben wischen", 12, txtc, bold=True, anchor="mm")
d.polygon([(116, 730), (124, 730), (120, 722)], fill=C["acc"])
d.line([(120, 730), (120, 737)], fill=C["acc"], width=2)
# ------------------------------------------------------------------ HOME ---
def _home(d, st, th, C):
acc = C["acc"]; dim = C["dim"]; txtc = C["txt"]; card = C["card"]
ph = st["phase"]
g = "Guten Morgen" if 5 <= ph * 24 < 11 else ("Guten Tag" if ph * 24 < 18
else ("Guten Abend" if ph * 24 < 23 else "Gute Nacht"))
_txt(d, (16, 34), g, 13, dim)
# clock card
cx0, cy0, cx1, cy1 = S.HOME_CARDS["clock"]
_rr(d, (cx0, cy0, cx1, cy1), 16, fill=card)
hh, mm, ss = st["clock"]
_txt(d, (cx0 + 18, cy0 + 26), "%02d:%02d" % (hh, mm), 42, txtc, bold=True)
_txt(d, (cx0 + 18, cy0 + 96), st["date"], 12, dim)
pw = (ss / 59.0) * (cx1 - cx0 - 36)
_rr(d, (cx0 + 18, cy0 + 118, cx0 + 18 + pw, cy0 + 126), 4, fill=acc)
# weather mini card
wx0, wy0, wx1, wy1 = S.HOME_CARDS["weather"]
_rr(d, (wx0, wy0, wx1, wy1), 16, fill=card)
_txt(d, (wx0 + 16, wy0 + 14), st["city"], 11, dim)
_txt(d, (wx0 + 16, wy0 + 36), "%d°" % st["temp"], 30, txtc, bold=True)
_txt(d, (wx0 + 16, wy0 + 92), S.CONDS[st["weather_cond"]], 11, acc)
# music mini card
mx0, my0, mx1, my1 = S.HOME_CARDS["music"]
_rr(d, (mx0, my0, mx1, my1), 16, fill=card)
_txt(d, (mx0 + 14, my0 + 14), "Musik", 11, dim)
for i in range(4):
bh = 8 + (st["music_level"] * 40 * abs(math.sin(st["tsec"] * 2.5 + i))) if st["music_playing"] else 10 + i * 4
_rr(d, (mx0 + 14 + i * 20, my1 - 24 - bh, mx0 + 14 + i * 20 + 12, my1 - 20), 4, fill=acc)
_txt(d, (mx0 + 14, my0 + 62), S.MUSIC_TRACKS[st["track"]][0], 10, txtc)
# tip card
tx0, ty0, tx1, ty1 = S.HOME_CARDS["tip"]
_rr(d, (tx0, ty0, tx1, ty1), 14, fill=C["card2"])
_txt(d, (tx0 + 16, ty0 + 14), "Tipp: Tippe überall — alles wird vom NPU generiert", 11, dim)
# dock
for i, x in enumerate(S.DOCK_XS):
_rr(d, (x - 30, S.DOCK_Y0, x + 30, S.DOCK_Y0 + 60), 18, fill=card)
C["acc_fills"].append((x - 18, S.DOCK_Y0 + 12, x + 18, S.DOCK_Y0 + 48))
lbl = ["Rechner", "Wetter", "Musik", "Einst."][i]
_txt(d, (x, S.DOCK_Y0 + 72), lbl, 10, dim, anchor="ma")
# apps pill
ax0, ay0, ax1, ay1 = S.APPS_PILL
_rr(d, (ax0, ay0, ax1, ay1), 14, fill=C["card2"], outline=C["stroke"], width=1)
_txt(d, (S.FULL_W // 2, (ay0 + ay1) // 2), "Alle Apps", 14, txtc, bold=True, anchor="mm")
# fireflies
for (fx, fy, fv) in fireflies(st["tsec"]):
if th == "hint":
d.ellipse([fx - 2, fy - 2, fx + 2, fy + 2], fill=(96, 102, 116))
else:
C["fire_pts"].append((fx, fy, fv))
# ---------------------------------------------------------------- DRAWER ---
def _drawer(d, st, th, C):
acc = C["acc"]; dim = C["dim"]; txtc = C["txt"]; card = C["card"]
sx0, sy0, sx1, sy1 = S.DRAWER_SEARCH
_rr(d, (sx0, sy0, sx1, sy1), 12, fill=C["card2"], outline=C["stroke"], width=1)
d.ellipse([sx0 + 14, sy0 + 12, sx0 + 30, sy0 + 28], outline=dim, width=2)
d.line([(sx0 + 27, sy0 + 25), (sx0 + 34, sy0 + 32)], fill=dim, width=2)
_txt(d, (sx0 + 44, sy0 + 13), "Suchen…", 12, dim)
for i, name in enumerate(S.DRAWER_APPS):
r, c = divmod(i, 3)
x0 = 24 + c * S.DRAWER_CELL_W
y0 = S.DRAWER_Y0 + r * S.DRAWER_CELL_H
icx, icy = x0 + 32, y0 + 40
sel = (i == st.get("drawer_sel", -1))
fillc = acc if sel else card
_rr(d, (icx - 28, icy - 28, icx + 28, icy + 28), 14, fill=fillc)
if sel:
C["acc_fills"].append((icx - 20, icy - 20, icx + 20, icy + 20))
_txt(d, (x0 + 32, y0 + 84), name, 10, txtc if sel else dim, anchor="ma")
# ------------------------------------------------------------------ CALC ---
def _calc(d, st, th, C):
acc = C["acc"]; dim = C["dim"]; txtc = C["txt"]; card = C["card"]
dx0, dy0, dx1, dy1 = S.CALC_DISP
_rr(d, (dx0, dy0, dx1, dy1), 16, fill=C["card2"], outline=C["stroke"], width=1)
disp = st["calc_disp"] if st["calc_disp"] else "0"
_txt(d, (dx1 - 16, (dy0 + dy1) // 2), disp, 26, txtc, bold=True, anchor="rm")
gx0, gy0, gx1, gy1 = S.CALC_GRID
bw = (gx1 - gx0 - 3 * 10) / 4.0
bh = (gy1 - gy0 - 4 * 10) / 5.0
for r in range(5):
for c in range(4):
k = S.CALC_KEYS[r][c]
bx0 = gx0 + c * (bw + 10); by0 = gy0 + r * (bh + 10)
pressed = (st["calc_pressed"] == r * 4 + c)
is_op = k in "÷×−+"
is_eq = k == "="
fill = acc if is_eq else (C["card2"] if pressed else card)
_rr(d, (bx0, by0, bx0 + bw, by0 + bh), 12, fill=fill,
outline=(acc if (pressed and not is_eq) else None), width=2)
if pressed and not is_eq:
C["acc_fills"].append((bx0 + 6, by0 + 6, bx0 + bw - 6, by0 + bh - 6))
col = (10, 12, 16) if is_eq else (acc if is_op else txtc)
_txt(d, (bx0 + bw / 2, by0 + bh / 2 - 1), k, 17, col, bold=True, anchor="mm")
# --------------------------------------------------------------- WEATHER ---
def _weather(d, st, th, C):
acc = C["acc"]; dim = C["dim"]; txtc = C["txt"]; card = C["card"]
_txt(d, (16, 36), "Wetter · " + st["city"], 14, txtc, bold=True)
_txt(d, (16, 64), "%d°" % st["temp"], 54, txtc, bold=True)
_txt(d, (150, 96), S.CONDS[st["weather_cond"]], 15, dim)
_txt(d, (150, 122), "Gefühlt %d° · H:%d° T:%d°" % (st["temp"] + 1, st["temp"] + 3, st["temp"] - 6), 11, dim)
# animated zone
ws = weather_state(st)
ax0, ay0, ax1, ay1 = S.WEATHER_ANIM
for it in ws:
if it[0] == "sun":
_, sx, sy, ang = it
if th != "hint":
C["acc_fills"].append((sx - 30, sy - 30, sx + 30, sy + 30))
d.ellipse([sx - 26, sy - 26, sx + 26, sy + 26], fill=C["sun"])
for k in range(8):
a = ang + k * math.pi / 4
x2 = sx + 40 * math.cos(a); y2 = sy + 40 * math.sin(a)
d.line([(sx + 30 * math.cos(a), sy + 30 * math.sin(a)), (x2, y2)],
fill=C["sun"], width=4)
elif it[0] == "cloud":
_, cx, cy, sc = it
col = (208, 214, 226) if th != "hint" else S.H_STROKE
for (ox, oy, r) in [(-22, 4, 16), (0, -6, 20), (22, 4, 15), (0, 10, 18)]:
d.ellipse([cx + ox * sc - r, cy + oy * sc - r, cx + ox * sc + r, cy + oy * sc + r], fill=col)
elif it[0] == "rain":
_, drops, _ = it
col = (150, 190, 255) if th != "hint" else S.H_STROKE
for (dxp, dyp) in drops:
d.line([(dxp, dyp), (dxp - 4, dyp + 12)], fill=col, width=2)
else:
_, t, _ = it
for i in range(14):
sx = (i * 137) % 350 + 17
sy = 218 + (i * 53) % 180
tw = 0.5 + 0.5 * math.sin(t * 2.2 + i * 1.7)
r = 1 + 2 * tw
d.ellipse([sx - r, sy - r, sx + r, sy + r],
fill=(240, 240, 255) if th != "hint" else (120, 126, 140))
mx, my = 296, 288
d.ellipse([mx - 24, my - 24, mx + 24, my + 24], fill=(226, 230, 240))
d.ellipse([mx + 6, my - 18, mx + 34, my + 10], fill=C["card"] if th != "hint" else S.H_BG)
# hourly
hx0, hy0, hx1, hy1 = S.WEATHER_HOURLY
cw = (hx1 - hx0 - 4 * 8) / 5.0
for i in range(5):
x0 = hx0 + i * (cw + 8)
_rr(d, (x0, hy0, x0 + cw, hy1), 12, fill=card)
hr = (st["clock"][0] + i + 1) % 24
tp = st["temp"] + ((i * 7) % 5) - 2
_txt(d, (x0 + cw / 2, hy0 + 12), "%02dh" % hr, 10, dim, anchor="ma")
d.ellipse([x0 + cw / 2 - 7, hy0 + 34, x0 + cw / 2 + 7, hy0 + 48],
fill=(C["sun"] if i % 2 == 0 else (120, 130, 150)))
_txt(d, (x0 + cw / 2, hy0 + 58), "%d°" % tp, 12, txtc, bold=True, anchor="ma")
# details
dx0, dy0, dx1, dy1 = S.WEATHER_DETAIL
_rr(d, (dx0, dy0, dx1, dy1), 14, fill=card)
rows = [("Wind", "%d km/h" % st["wind"]), ("Luftfeuchte", "%d %%" % st["hum"]),
("Regenwahrsch.", "%d %%" % st["rainp"])]
for i, (k, v) in enumerate(rows):
y = dy0 + 16 + i * 40
_txt(d, (dx0 + 18, y), k, 12, dim)
_txt(d, (dx1 - 18, y), v, 13, txtc, bold=True, anchor="ra")
if i < 2:
d.line([(dx0 + 16, y + 28), (dx1 - 16, y + 28)], fill=C["stroke"], width=1)
# ----------------------------------------------------------------- MUSIC ---
def _music(d, st, th, C):
acc = C["acc"]; dim = C["dim"]; txtc = C["txt"]; card = C["card"]
ax0, ay0, ax1, ay1 = S.MUSIC_ART
_rr(d, (ax0, ay0, ax1, ay1), 20, fill=card)
if th != "hint":
C["acc_fills"].append((ax0 + 8, ay0 + 8, ax1 - 8, ay1 - 8))
d.ellipse([(ax0 + ax1) / 2 - 44, (ay0 + ay1) / 2 - 44,
(ax0 + ax1) / 2 + 44, (ay0 + ay1) / 2 + 44],
fill=C["card2"], outline=acc, width=3)
d.ellipse([(ax0 + ax1) / 2 - 8, (ay0 + ay1) / 2 - 8,
(ax0 + ax1) / 2 + 8, (ay0 + ay1) / 2 + 8], fill=acc)
tr, ar = S.MUSIC_TRACKS[st["track"]]
_txt(d, (S.FULL_W // 2, S.MUSIC_TITLE_Y), tr, 17, txtc, bold=True, anchor="ma")
_txt(d, (S.FULL_W // 2, S.MUSIC_ARTIST_Y), ar, 12, dim, anchor="ma")
px0, py0, px1, py1 = S.MUSIC_PROG
_rr(d, (px0, py0, px1, py1), 4, fill=C["stroke"])
pw = int((px1 - px0) * st["music_prog"])
_rr(d, (px0, py0, px0 + pw, py1), 4, fill=acc)
d.ellipse([px0 + pw - 7, py0 - 4, px0 + pw + 7, py1 + 4], fill=txtc)
secs = st["music_prog"] * 231
_txt(d, (px0, py1 + 8), "%d:%02d" % (secs // 60, secs % 60), 10, dim)
_txt(d, (px1, py1 + 8), "3:51", 10, dim, anchor="ra")
# spectrum
sx0, sy0, sx1, sy1 = S.MUSIC_SPEC
bars = music_bars(st)
bw = (sx1 - sx0 - 23 * 5) / 24.0
for i, v in enumerate(bars):
bh = v * (sy1 - sy0)
x0 = sx0 + i * (bw + 5)
_rr(d, (x0, sy1 - bh, x0 + bw, sy1), 3, fill=acc)
if th != "hint" and v > 0.55:
C["acc_fills"].append((x0, sy1 - bh, x0 + bw, sy1))
# controls
for i, x in enumerate(S.MUSIC_CTRL_XS):
cy = (S.MUSIC_CTRL_Y0 + S.MUSIC_CTRL_Y1) / 2
if i == 1:
d.ellipse([x - 34, cy - 34, x + 34, cy + 34], fill=acc)
if st["music_playing"]:
d.rectangle([x - 10, cy - 14, x - 3, cy + 14], fill=(10, 12, 16))
d.rectangle([x + 3, cy - 14, x + 10, cy + 14], fill=(10, 12, 16))
else:
d.polygon([(x - 9, cy - 15), (x - 9, cy + 15), (x + 14, cy)], fill=(10, 12, 16))
else:
d.ellipse([x - 26, cy - 26, x + 26, cy + 26], outline=dim, width=2)
sgn = -1 if i == 0 else 1
d.polygon([(x + sgn * 14, cy - 10), (x + sgn * 14, cy + 10), (x + sgn * 2, cy)],
fill=txtc)
d.rectangle([x + sgn * 16 - 2, cy - 10, x + sgn * 16 + 2, cy + 10], fill=txtc)
# -------------------------------------------------------------- SETTINGS ---
def _settings(d, st, th, C):
acc = C["acc"]; dim = C["dim"]; txtc = C["txt"]; card = C["card"]
_txt(d, (16, 34), "Einstellungen", 18, txtc, bold=True)
_txt(d, (16, 66), "Akzentfarbe", 11, dim)
for i in range(12):
x0 = 16 + i * 29
col = accent_rgb(i * 30) if th != "hint" else S.H_ACC
sel = (st["sel_hue"] == i)
_rr(d, (x0, S.SETTINGS_HUE_Y[0], x0 + 26, S.SETTINGS_HUE_Y[1]), 8, fill=col,
outline=(txtc if sel else None), width=2)
if sel and th != "hint":
C["acc_fills"].append((x0 + 4, S.SETTINGS_HUE_Y[0] + 4, x0 + 22, S.SETTINGS_HUE_Y[1] - 4))
y = S.SETTINGS_ROW_Y0
for i, (name, ti) in enumerate(S.SETTINGS_ROWS):
_rr(d, (16, y, 368, y + S.SETTINGS_ROW_H), 12, fill=card)
_txt(d, (32, y + S.SETTINGS_ROW_H / 2 - 7), name, 13, txtc)
on = st["toggles"][ti]
tx0, tx1 = S.SETTINGS_TOGGLE
_rr(d, (tx0, y + 12, tx1, y + S.SETTINGS_ROW_H - 12), 13, fill=acc if on else C["stroke"])
kx = tx1 - 11 if on else tx0 + 11
d.ellipse([kx - 9, y + S.SETTINGS_ROW_H / 2 - 9, kx + 9, y + S.SETTINGS_ROW_H / 2 + 9],
fill=(250, 250, 252))
y += S.SETTINGS_ROW_H + S.SETTINGS_ROW_GAP
# npu status
_rr(d, (16, S.SETTINGS_INFO_Y0, 368, S.SETTINGS_INFO_Y0 + S.SETTINGS_CARD_H), 14, fill=C["card2"])
_txt(d, (32, S.SETTINGS_INFO_Y0 + 12), "Renderer: %s" % st["npu_label"], 12, txtc, bold=True)
_txt(d, (32, S.SETTINGS_INFO_Y0 + 34), "FrameGenerator · %.2fM Parameter · %d Hz Ziel" %
(st.get("params_m", 0.94), st.get("fps", 60)), 10, dim)
# diagnose
dy0 = S.SETTINGS_INFO_Y0 + S.SETTINGS_CARD_H + 12
_rr(d, (16, dy0, 368, dy0 + S.SETTINGS_CARD_H), 14, fill=C["card2"])
_txt(d, (32, dy0 + 12), "Diagnose", 12, txtc, bold=True)
_txt(d, (32, dy0 + 34), st.get("diag", "Keine Abstürze · alles normal"), 10, dim)
# ----------------------------------------------------------------- ABOUT ---
def _about(d, st, th, C):
acc = C["acc"]; dim = C["dim"]; txtc = C["txt"]
cx, cy = S.FULL_W // 2, 140
if th != "hint":
C["acc_fills"].append((cx - 46, cy - 46, cx + 46, cy + 46))
d.ellipse([cx - 44, cy - 44, cx + 44, cy + 44], fill=C["card"], outline=acc, width=3)
_txt(d, (cx, cy), "E", 30, acc, bold=True, anchor="mm")
_txt(d, (cx, 220), "E-OS", 30, txtc, bold=True, anchor="ma")
_txt(d, (cx, 262), "v0.4 · Vollgenerierung auf NPU", 12, dim, anchor="ma")
rows = [("Paket", "com.killerboss.eos"), ("Modell", "FrameGenerator 0.94M"),
("Auflösung", "384 × 832 · Ziel 60 Hz"), ("Renderer", st["npu_label"])]
y = 310
for k, v in rows:
_txt(d, (28, y), k, 12, dim)
_txt(d, (356, y), v, 12, txtc, bold=True, anchor="ra")
y += 42
# ----------------------------------------------------------- frame chrome --
def _status(d, st, th, C):
txtc = C["txt"]; dim = C["dim"]
hh, mm, _ = st["clock"]
_txt(d, (12, 6), "%02d:%02d" % (hh, mm), 11, txtc, bold=True)
_txt(d, (340, 6), "%d%%" % st["battery"], 10, dim, anchor="ra")
_rr(d, (352, 8, 372, 16), 2, outline=dim, width=1)
_rr(d, (354, 10, 354 + int(16 * st["battery"] / 100.0), 14), 1, fill=C["acc"])
def _nav(d, st, th, C):
txtc = C["txt"]; dim = C["dim"]
if th == "hint":
d.rectangle([0, S.NAV_Y0, S.FULL_W, S.FULL_H], fill=S.H_CARD)
else:
d.rectangle([0, S.NAV_Y0, S.FULL_W, S.FULL_H], fill=C["navbg"])
d.line([(0, S.NAV_Y0), (S.FULL_W, S.NAV_Y0)], fill=C["stroke"], width=1)
for (bx0, by0, bx1, by1), pressed in [(S.NAV_BACK, st["nav_pressed"] == 0),
(S.NAV_HOME, st["nav_pressed"] == 1),
(S.NAV_APPS, st["nav_pressed"] == 2)]:
cx, cy = (bx0 + bx1) / 2, (by0 + by1) / 2
if pressed:
_rr(d, (bx0, by0, bx1, by1), 10, fill=C["card2"])
if th != "hint":
C["acc_fills"].append((bx0 + 6, by0 + 6, bx1 - 6, by1 - 6))
if (bx0, by0) == S.NAV_BACK[:2]:
_arrow_left(d, cx + 2, cy, 7, C["acc"])
elif (bx0, by0) == S.NAV_HOME[:2]:
_home_glyph(d, cx, cy - 1, 9, txtc)
else:
_apps_glyph(d, cx, cy, 8, txtc)
def _toast(d, st, th, C):
if not st.get("toast"):
return
on_lock = (S.SCREENS[st["screen"]] == "LOCK")
x0, y0, x1, y1 = (72, 620, 312, 660) if on_lock else S.TOAST_RECT
_rr(d, (x0, y0, x1, y1), 18, fill=C["card2"], outline=C["stroke"], width=1)
_txt(d, ((x0 + x1) / 2, (y0 + y1) / 2), st["toast"], 11, C["txt"], bold=True, anchor="mm")
def _ripples(d, st, th, C):
for (rx, ry, rr_, s) in st["ripples"]:
if s <= 0.01:
continue
rad = rr_ * S.RMAX
col = C["acc"] if th != "hint" else S.H_ACC
a = int(200 * s)
if th == "hint":
d.ellipse([rx - rad, ry - rad, rx + rad, ry + rad], outline=(90, 96, 110), width=3)
else:
C["glow_rings"].append((rx, ry, rad, a))
d.ellipse([rx - rad, ry - rad, rx + rad, ry + rad], outline=col + (a,), width=S.RIPPLE_W)
d.ellipse([rx - rad * 0.55, ry - rad * 0.55, rx + rad * 0.55, ry + rad * 0.55],
outline=col + (int(110 * s),), width=2)
# ------------------------------------------------------------------ main ---
def draw_scene(st, theme="styled"):
"""st = scene dict. Returns PIL RGB Image 384x832."""
S2 = 2 if theme == "styled" else 1
W, H = S.FULL_W * S2, S.FULL_H * S2
hue = st["hue"]
acc = accent_rgb(hue)
ph = st["phase"]
wm = warmth(ph)
if theme == "hint":
C = dict(acc=S.H_ACC, dim=S.H_DIM, txt=S.H_TXT, card=S.H_CARD, card2=(38, 43, 54),
stroke=S.H_STROKE, navbg=S.H_CARD, sun=(150, 150, 150), aurora=[None] * 3,
acc_fills=[], fire_pts=[], glow_rings=[], aurora_pts=[])
img = Image.new("RGB", (W, H), S.H_BG)
d = ImageDraw.Draw(img, "RGBA")
else:
base = _gradient_bg(d=None, st=st, acc=acc)
img = Image.fromarray(base, "RGB").convert("RGBA")
ov = Image.new("RGBA", (W, H), (0, 0, 0, 0))
d = ImageDraw.Draw(ov, "RGBA")
C = dict(acc=acc, dim=(148, 156, 170), txt=(236, 240, 246),
card=_lerpc(accent_rgb(hue, 0.16, 0.30), (30, 34, 44), 0.55) + (238,),
card2=_lerpc(accent_rgb(hue, 0.22, 0.32), (40, 45, 56), 0.5) + (245,),
stroke=(70, 78, 92), navbg=(14, 16, 22, 216),
sun=accent_rgb(48, 0.62, 0.9), aurora=[None] * 3,
acc_fills=[], fire_pts=[], glow_rings=[], aurora_pts=[])
for i in range(3):
C["aurora"][i] = _lerpc(accent_rgb(hue + i * 40, 0.5, 0.7), (255, 180, 120), wm * 0.6)
C["ov"] = ov
# screens (with slide transition)
tr = st["trans"]
if tr > 0.001:
fs, ts = st["from_screen"], st["screen"]
dx_out = int(-tr * S.FULL_W * S2)
dx_in = int((1 - tr) * S.FULL_W * S2)
tmp_out = Image.new("RGBA", (W, H), (0, 0, 0, 0))
d1 = ImageDraw.Draw(tmp_out, "RGBA")
_screen_base(d1, st, theme, acc, C["dim"], C["txt"])
_draw_screen(fs, d1, st, theme, C, None)
tmp_in = Image.new("RGBA", (W, H), (0, 0, 0, 0))
d2 = ImageDraw.Draw(tmp_in, "RGBA")
_screen_base(d2, st, theme, acc, C["dim"], C["txt"])
_draw_screen(ts, d2, st, theme, C, None)
_status(d2, st, theme, C); _nav(d2, st, theme, C)
if theme == "hint":
img.paste(tmp_out.split()[3], (dx_out, 0), tmp_out.split()[3])
img.paste(tmp_in.convert("RGB"), (dx_in, 0), tmp_in.split()[3].point(lambda a: a))
img.paste(tmp_in.convert("RGB"), (dx_in, 0), tmp_in.split()[3])
else:
img.alpha_composite(tmp_out, (dx_out, 0))
img.alpha_composite(tmp_in, (dx_in, 0))
d = d2
if theme == "styled":
C["ov"] = tmp_in
else:
_screen_base(d, st, theme, acc, C["dim"], C["txt"])
_draw_screen(st["screen"], d, st, theme, C, None)
_status(d, st, theme, C); _nav(d, st, theme, C)
_ripples(d, st, theme, C)
_toast(d, st, theme, C)
if theme == "styled":
ov = C["ov"]
# 1) soft shadows for cards: draw dark blobs blurred
sh = Image.new("RGBA", (W, H), (0, 0, 0, 0))
ds = ImageDraw.Draw(sh)
for box in _card_boxes(st):
ds.rounded_rectangle([box[0] * S2, (box[1] + S.SHADOW_DY) * S2,
box[2] * S2, (box[3] + S.SHADOW_DY) * S2],
radius=14 * S2, fill=(0, 0, 0, int(255 * S.SHADOW_A)))
sh = sh.filter(ImageFilter.GaussianBlur(S.SHADOW_BLUR * S2))
img.alpha_composite(sh)
# 2) aurora blur (lock)
if S.SCREENS[st["screen"]] == "LOCK" or (st["trans"] > 0.001 and
(S.SCREENS[st["from_screen"]] == "LOCK" or S.SCREENS[st["screen"]] == "LOCK")):
au = Image.new("RGBA", (W, H), (0, 0, 0, 0))
da = ImageDraw.Draw(au)
for (x, i) in C["aurora_pts"]:
col = tuple(C["aurora"][i]) + (66,)
da.polygon([((x - 130) * S2, 0), ((x + 60) * S2, 0),
((x - 80) * S2, H), ((x - 270) * S2, H)], fill=col)
au = au.filter(ImageFilter.GaussianBlur(30 * S2))
img.alpha_composite(au)
# 3) accent glows (batched)
gl = Image.new("RGBA", (W, H), (0, 0, 0, 0))
dg = ImageDraw.Draw(gl)
ga = int(120 + 60 * st["music_level"]) if st["music_playing"] else 130
for box in C["acc_fills"]:
dg.rounded_rectangle([box[0] * S2, box[1] * S2, box[2] * S2, box[3] * S2],
radius=10 * S2, fill=C["acc"] + (ga,))
for (rx, ry, rad, a) in C["glow_rings"]:
dg.ellipse([(rx - rad) * S2, (ry - rad) * S2, (rx + rad) * S2, (ry + rad) * S2],
outline=C["acc"] + (a,), width=S.RIPPLE_W * S2)
for (fx, fy, fv) in C["fire_pts"]:
r = (5 + 3 * fv) * S2
dg.ellipse([(fx - r) * 1, (fy - r) * 1, (fx + r) * 1, (fy + r) * 1],
fill=(255, 200, 110, int(160 * fv)))
gl = gl.filter(ImageFilter.GaussianBlur(S.GLOW_BLUR * S2))
img.alpha_composite(gl)
# 4) firefly cores
for (fx, fy, fv) in C["fire_pts"]:
r = 2.2 * S2
d.ellipse([fx * S2 - r, fy * S2 - r, fx * S2 + r, fy * S2 + r],
fill=(255, 224, 150, int(200 * fv + 55)))
img.alpha_composite(ov)
img = img.convert("RGB").resize((S.FULL_W, S.FULL_H), Image.LANCZOS)
else:
img = img.convert("RGB").resize((S.HINT_W, S.HINT_H), Image.BOX)
return img
def _card_boxes(st):
scr = S.SCREENS[st["screen"]]
boxes = []
if scr == "LOCK":
boxes += [(32, 300, 352, 380), (92, 700, 292, 746)]
elif scr == "HOME":
boxes += [S.HOME_CARDS["clock"], S.HOME_CARDS["weather"], S.HOME_CARDS["music"],
S.HOME_CARDS["tip"], S.APPS_PILL]
for x in S.DOCK_XS:
boxes.append((x - 30, S.DOCK_Y0, x + 30, S.DOCK_Y0 + 60))
elif scr == "DRAWER":
boxes.append(S.DRAWER_SEARCH)
for i in range(12):
r, c = divmod(i, 3)
boxes.append((24 + c * S.DRAWER_CELL_W + 4, S.DRAWER_Y0 + r * S.DRAWER_CELL_H + 12,
24 + c * S.DRAWER_CELL_W + 60, S.DRAWER_Y0 + r * S.DRAWER_CELL_H + 68))
elif scr == "CALC":
boxes.append(S.CALC_DISP)
elif scr == "WEATHER":
boxes.append(S.WEATHER_DETAIL)
hx0, hy0, hx1, hy1 = S.WEATHER_HOURLY
cw = (hx1 - hx0 - 4 * 8) / 5.0
for i in range(5):
boxes.append((hx0 + i * (cw + 8), hy0, hx0 + (i + 1) * cw + i * 8, hy1))
elif scr == "MUSIC":
boxes.append(S.MUSIC_ART)
elif scr == "SETTINGS":
y = S.SETTINGS_ROW_Y0
for _ in S.SETTINGS_ROWS:
boxes.append((16, y, 368, y + S.SETTINGS_ROW_H)); y += S.SETTINGS_ROW_H + S.SETTINGS_ROW_GAP
boxes += [(16, S.SETTINGS_INFO_Y0, 368, S.SETTINGS_INFO_Y0 + S.SETTINGS_CARD_H)]
return boxes