Download scripts/os4_draw.py from KillerBoss/e-os-v4: direct link, hf CLI and curl.
- Browser
- Download file 29.8 kB
-
https://huggingface.co/KillerBoss/e-os-v4/resolve/main/scripts/os4_draw.py
- Command line
-
hf download hf://KillerBoss/e-os-v4/scripts/os4_draw.py
-
curl -L -o os4_draw.py https://huggingface.co/KillerBoss/e-os-v4/resolve/main/scripts/os4_draw.py
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 | |