docs: anteprima di layout delle tre schermate + script di rendering
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,215 @@
|
||||
#!/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) -> int:
|
||||
d.text((M, y), "Obiettivo ACS", 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
|
||||
c = "Acqua calda"
|
||||
d.text(((W - d.textlength(c, font=font(26))) / 2, y), c, font=font(26), fill=SECONDARY)
|
||||
y += 40
|
||||
big = "51,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, (51.0 - 20) / (65 - 20))
|
||||
y += 22
|
||||
t = "obiettivo 55,0 °C"
|
||||
d.text(((W - d.textlength(t, font=font(24))) / 2, y), t, font=font(24), fill=SECONDARY)
|
||||
|
||||
y = section_header(d, y + 44, "Impianto")
|
||||
y = label_pair(d, y, "Esterna", "18,0 °C")
|
||||
y = label_pair(d, y, "Interna", "22,5 °C")
|
||||
y = label_pair(d, y, "Modalità", "Auto")
|
||||
y = label_pair(d, y, "Zone", "1")
|
||||
y = label_pair(d, y, "Errori", "0")
|
||||
y = label_pair(d, y, "Aggiornato", "19:07")
|
||||
|
||||
y = section_header(d, y + 24, "Comandi")
|
||||
y = slider(d, y, (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")
|
||||
|
||||
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 = "51,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", "55,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), "acqua calda sanitaria", 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)")
|
||||
Reference in New Issue
Block a user