#!/usr/bin/env python3 """Anteprima di layout delle schermate di harbour-aira. NON sono screenshot: sono disegni generati con Pillow che riproducono disposizione, gerarchia e misure delle tre schermate (dashboard, impostazioni, copertina) usando i dati reali dell'impianto. Servono a validare il layout prima di compilare: l'app vera usa i componenti Silica, non questi disegni. python3 scripts/render_mockups.py -> docs/anteprima/*.png """ from __future__ import annotations import pathlib from PIL import Image, ImageDraw, ImageFont W, H = 720, 1280 M = 72 # margine laterale (= Theme.horizontalPageMargin circa) OUT = pathlib.Path("docs/anteprima") # Palette Silica, ambience scura BG_TOP = (10, 12, 16) BG_BOT = (22, 28, 36) PRIMARY = (240, 242, 245) SECONDARY = (139, 147, 156) ACCENT = (79, 195, 232) TRACK = (56, 64, 74) BTN_BG = (32, 39, 48) BTN_EDGE = (98, 110, 124) OK = (122, 214, 150) FONTS = "/usr/share/fonts/truetype/dejavu/" def font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont: name = "DejaVuSans-Bold.ttf" if bold else "DejaVuSans.ttf" return ImageFont.truetype(FONTS + name, size) def background() -> Image.Image: img = Image.new("RGB", (W, H), BG_TOP) d = ImageDraw.Draw(img) for y in range(H): t = y / H d.line([(0, y), (W, y)], fill=( int(BG_TOP[0] + (BG_BOT[0] - BG_TOP[0]) * t), int(BG_TOP[1] + (BG_BOT[1] - BG_TOP[1]) * t), int(BG_TOP[2] + (BG_BOT[2] - BG_TOP[2]) * t), )) return img def status_bar(d: ImageDraw.ImageDraw) -> None: d.text((M, 26), "21:20", font=font(22), fill=SECONDARY) # indicazioni di sistema (segno grafico, non testo reale) x = W - M - 96 d.rounded_rectangle([x, 30, x + 44, 48], radius=5, outline=SECONDARY, width=2) d.rounded_rectangle([x + 4, 34, x + 30, 44], radius=2, fill=SECONDARY) d.line([(x + 22, 16), (x + 30, 16)], fill=SECONDARY, width=2) d.line([(x + 8, 14), (x + 20, 14)], fill=SECONDARY, width=2) d.line([(x + 38, 16), (x + 48, 16)], fill=SECONDARY, width=2) for i in range(4): d.line([(x + 62 + i * 9, 42 - i * 5), (x + 66 + i * 9, 42 - i * 5)], fill=SECONDARY, width=3) def page_header(d: ImageDraw.ImageDraw, title: str) -> None: d.text((M, 84), title, font=font(44, bold=True), fill=PRIMARY) def section_header(d: ImageDraw.ImageDraw, y: int, title: str) -> int: d.text((M, y), title, font=font(30), fill=ACCENT) return y + 52 def label_pair(d: ImageDraw.ImageDraw, y: int, left: str, right: str, right_color=PRIMARY) -> int: d.text((M, y), left, font=font(26), fill=SECONDARY) rw = d.textlength(right, font=font(26)) d.text((W - M - rw, y), right, font=font(26), fill=right_color) return y + 44 def progress(d: ImageDraw.ImageDraw, y: int, frac: float) -> None: d.rounded_rectangle([M, y, W - M, y + 3], radius=2, fill=TRACK) end = M + int((W - 2 * M) * frac) d.rounded_rectangle([M, y, end, y + 3], radius=2, fill=ACCENT) def slider(d: ImageDraw.ImageDraw, y: int, frac: float, value_text: str, label: str = "Obiettivo ACS") -> int: d.text((M, y), label, font=font(24), fill=SECONDARY) vw = d.textlength(value_text, font=font(24)) d.text((W - M - vw, y), value_text, font=font(24), fill=PRIMARY) ty = y + 58 d.rounded_rectangle([M, ty, W - M, ty + 3], radius=2, fill=TRACK) end = M + int((W - 2 * M) * frac) d.rounded_rectangle([M, ty, end, ty + 3], radius=2, fill=ACCENT) d.ellipse([end - 9, ty - 7, end + 9, ty + 11], fill=PRIMARY) return ty + 44 def button(d: ImageDraw.ImageDraw, y: int, text: str, primary: bool = False) -> int: h = 74 d.rounded_rectangle([M, y, W - M, y + h], radius=h // 2, fill=BTN_BG, outline=ACCENT if primary else BTN_EDGE, width=2) tw = d.textlength(text, font=font(28, bold=primary)) d.text(((W - tw) / 2, y + (h - 34) / 2), text, font=font(28, bold=primary), fill=ACCENT if primary else PRIMARY) return y + h + 16 # --------------------------------------------------------------------------- # def main_screen() -> Image.Image: img = background() d = ImageDraw.Draw(img) status_bar(d) page_header(d, "Aira") y = 156 # ---- casa: il dato principale, in cima ------------------------------- # c = "Temperatura in casa" d.text(((W - d.textlength(c, font=font(24))) / 2, y), c, font=font(24), fill=SECONDARY) y += 40 big = "23,0 °C" d.text(((W - d.textlength(big, font=font(92, bold=True))) / 2, y), big, font=font(92, bold=True), fill=PRIMARY) y += 116 progress(d, y, (23.0 - 5) / (30 - 5)) y += 22 t = "obiettivo 19,0 °C · Riscaldamento" d.text(((W - d.textlength(t, font=font(24))) / 2, y), t, font=font(24), fill=SECONDARY) y += 46 y = slider(d, y, (19.0 - 5) / (30 - 5), "19,0 °C", "Nuovo obiettivo casa") y = button(d, y, "Imposta casa a 19,0 °C", primary=True) # ---- acqua calda sanitaria ------------------------------------------- # y = section_header(d, y + 24, "Acqua calda sanitaria") acs = "51,0 °C" d.text(((W - d.textlength(acs, font=font(56, bold=True))) / 2, y), acs, font=font(56, bold=True), fill=PRIMARY) y += 76 progress(d, y, (51.0 - 20) / (65 - 20)) y += 22 y = label_pair(d, y, "Obiettivo acqua calda", "55,0 °C") y = slider(d, y + 10, (55.0 - 45) / (60 - 45), "55 °C") y = button(d, y, "Imposta obiettivo ACS", primary=True) y = button(d, y, "Avvia boost ACS") y = button(d, y, "Ferma boost ACS") # ---- stato impianto (scorre sotto) ----------------------------------- # y = section_header(d, y + 24, "Impianto") y = label_pair(d, y, "Esterna", "18,0 °C") y = label_pair(d, y, "Interna (pompa)", "22,5 °C") d.text(((W - d.textlength("Trascina verso il basso per aggiornare", font=font(22))) / 2, y + 8), "Trascina verso il basso per aggiornare", font=font(22), fill=SECONDARY) return img def settings_screen() -> Image.Image: img = background() d = ImageDraw.Draw(img) status_bar(d) page_header(d, "Impostazioni") y = 172 for lab, val in (("Indirizzo del bridge", "192.168.3.21:8790"), ("Token del bridge", "••••••••••••••••••••••••••••")): d.text((M, y), lab, font=font(24), fill=SECONDARY) y += 36 d.text((M, y), val, font=font(30), fill=PRIMARY) y += 52 d.line([(M, y), (W - M, y)], fill=BTN_EDGE, width=2) y += 44 d.multiline_text((M, y), "Indirizzo e token restano nella configurazione\n" "locale dell'app, in sola lettura per gli altri.", font=font(22), fill=SECONDARY, spacing=10) y += 104 y = button(d, y, "Prova connessione", primary=True) d.text((M, y), "Connessione riuscita (bridge 0.1.0)", font=font(24), fill=OK) y += 58 y = button(d, y, "Salva") return img def cover_screen() -> Image.Image: img = Image.new("RGB", (W, H), (16, 20, 26)) d = ImageDraw.Draw(img) # griglia di app sullo sfondo, per collocare la copertina for row in range(5): for col in range(4): cx = 108 + col * 168 cy = 150 + row * 180 d.rounded_rectangle([cx - 52, cy - 52, cx + 52, cy + 52], radius=22, fill=(30, 36, 45)) # copertina card = [96, 430, W - 96, 430 + 470] d.rounded_rectangle(card, radius=34, fill=(14, 18, 24), outline=ACCENT, width=2) d.text((card[0] + 34, card[1] + 30), "Aira", font=font(40, bold=True), fill=PRIMARY) d.line([(card[0] + 34, card[1] + 100), (card[2] - 34, card[1] + 100)], fill=BTN_EDGE, width=2) y = card[1] + 132 big = "23,0 °C" d.text(((W - d.textlength(big, font=font(76, bold=True))) / 2, y), big, font=font(76, bold=True), fill=PRIMARY) y += 108 for lab, val in (("obiettivo", "19,0 °C"), ("ACS", "51,0 °C"), ("esterna", "18,0 °C")): d.text((card[0] + 34, y), lab, font=font(24), fill=SECONDARY) vw = d.textlength(val, font=font(24)) d.text((card[2] - 34 - vw, y), val, font=font(24), fill=PRIMARY) y += 52 d.text((card[0] + 34, card[3] - 60), "temperatura in casa", font=font(22), fill=SECONDARY) return img if __name__ == "__main__": OUT.mkdir(parents=True, exist_ok=True) for name, fn in (("dashboard", main_screen), ("impostazioni", settings_screen), ("copertina", cover_screen)): path = OUT / f"{name}.png" fn().save(path) print(f"scritto {path} ({path.stat().st_size} byte)")