#!/usr/bin/env python3 """Wireframe dello studio UX/UI di harbour-aira (proposta). Sono SCHEMI, non anteprime rifinite: mostrano disposizione, gerarchia e comportamento dei componenti. Le anteprime curate delle schermate attuali stanno in docs/anteprima/; qui si confrontano le proposte dello studio. python3 scripts/render_wireframes.py -> docs/ux/*.png """ from __future__ import annotations import pathlib from PIL import Image, ImageDraw from render_mockups import (W, H, M, font, background, status_bar, PRIMARY, SECONDARY, ACCENT, BTN_BG, BTN_EDGE, OK) OUT = pathlib.Path("docs/ux") WARN = (232, 178, 92) # ambra: attenzione BAD = (226, 106, 106) # rosso: problema # ---------------------------------------------------------------- primitive def badge(d, x, y, n): """Numero di richiamo: si spiega nelle note dello studio.""" d.ellipse([x, y, x + 34, y + 34], fill=ACCENT) t = str(n) tw = d.textlength(t, font=font(22, bold=True)) d.text((x + 17 - tw / 2, y + 5), t, font=font(22, bold=True), fill=(10, 12, 16)) def box(d, x, y, w, h, fill=None, outline=BTN_EDGE, radius=14, width=2): d.rounded_rectangle([x, y, x + w, y + h], radius=radius, fill=fill, outline=outline, width=width) def btn(d, x, y, w, text, h=66, primary=False): box(d, x, y, w, h, fill=BTN_BG if not primary else (30, 62, 78), outline=ACCENT if primary else BTN_EDGE) f = font(24) tw = d.textlength(text, font=f) d.text((x + (w - tw) / 2, y + (h - 26) / 2), text, font=f, fill=PRIMARY) def text_line(d, x, y, text, size=24, color=SECONDARY, bold=False, width=None, align="left"): f = font(size, bold) if width is None: d.text((x, y), text, font=f, fill=color) return y + size + 8 words, lines, cur = text.split(), [], "" for wtxt in words: probe = (cur + " " + wtxt).strip() if d.textlength(probe, font=f) <= width or not cur: cur = probe else: lines.append(cur) cur = wtxt lines.append(cur) for ln in lines: tx = x if align == "left" else (x + (width - d.textlength(ln, font=f)) / 2) d.text((tx, y), ln, font=f, fill=color) y += size + 10 return y def pulley_hint(d, y, items): d.text((M, y), "▾", font=font(28, bold=True), fill=ACCENT) x = M + 40 for i, it in enumerate(items): if i: d.text((x, y + 2), "·", font=font(22), fill=BTN_EDGE) x += 22 d.text((x, y + 2), it, font=font(22), fill=SECONDARY) x += d.textlength(it, font=font(22)) + 16 return y + 40 def pushup_hint(d, y, items): d.text((M, y), "▴", font=font(28, bold=True), fill=ACCENT) x = M + 40 for i, it in enumerate(items): if i: d.text((x, y + 2), "·", font=font(22), fill=BTN_EDGE) x += 22 d.text((x, y + 2), it, font=font(22), fill=SECONDARY) x += d.textlength(it, font=font(22)) + 16 return y + 40 def cell(d, x, y, w, h, title, value, sub, frac=None, chevron=True): box(d, x, y, w, h, fill=BTN_BG) d.text((x + 24, y + 22), title, font=font(24), fill=ACCENT) d.text((x + 24, y + 58), value, font=font(54, bold=True), fill=PRIMARY) d.text((x + 24, y + 124), sub, font=font(22), fill=SECONDARY) if frac is not None: d.rounded_rectangle([x + 24, y + h - 34, x + w - 24, y + h - 24], radius=5, fill=(56, 64, 74)) d.rounded_rectangle([x + 24, y + h - 34, x + 24 + (w - 48) * frac, y + h - 24], radius=5, fill=ACCENT) if chevron: d.text((x + w - 40, y + h / 2 - 22), "›", font=font(38, bold=True), fill=SECONDARY) def slider(d, x, y, w, frac, left, right): cy = y + 18 d.text((x, cy - 14), left, font=font(22), fill=SECONDARY) rw = d.textlength(right, font=font(22)) d.text((x + w - rw, cy - 14), right, font=font(22), fill=SECONDARY) tx0, tx1 = x + 60, x + w - 60 d.rounded_rectangle([tx0, cy - 3, tx1, cy + 3], radius=3, fill=(56, 64, 74)) kx = tx0 + (tx1 - tx0) * frac d.rounded_rectangle([tx0, cy - 3, kx, cy + 3], radius=3, fill=ACCENT) d.ellipse([kx - 22, cy - 22, kx + 22, cy + 22], fill=PRIMARY) return y + 62 def switch_row(d, y, label, desc, on, x=M, w=W - 2 * M): d.text((x, y), label, font=font(26), fill=PRIMARY) d.text((x, y + 34), desc, font=font(20), fill=SECONDARY) sw, sh = 104, 56 sx, sy = x + w - sw, y + 4 d.rounded_rectangle([sx, sy, sx + sw, sy + sh], radius=sh / 2, fill=(30, 62, 78) if on else (44, 50, 58), outline=ACCENT if on else BTN_EDGE, width=2) kx = sx + sw - 28 if on else sx + 28 d.ellipse([kx - 22, sy + sh / 2 - 22, kx + 22, sy + sh / 2 + 22], fill=PRIMARY) return y + 96 def presets(d, y, values): n = len(values) gap = 10 bw = (W - 2 * M - gap * (n - 1)) / n for i, v in enumerate(values): x = M + i * (bw + gap) box(d, x, y, bw, 58, fill=BTN_BG) tw = d.textlength(v, font=font(22)) d.text((x + (bw - tw) / 2, y + 16), v, font=font(22), fill=PRIMARY) return y + 74 # ------------------------------------------------------------------ schermate def casa_screen(): img = background() d = ImageDraw.Draw(img) status_bar(d) d.text((M, 84), "Aira", font=font(44, bold=True), fill=PRIMARY) y = pulley_hint(d, 142, ["Aggiorna", "Dati", "Impianto", "Impostazioni"]) badge(d, W - M - 34, 136, 1) # stato in una riga sola d.ellipse([M, y + 10, M + 22, y + 32], fill=OK) d.text((M + 38, y + 6), "Tutto regolare · Automatico", font=font(24), fill=PRIMARY) d.text((W - M - 120, y + 8), "21:18", font=font(22), fill=SECONDARY) badge(d, W - M - 34, y + 2, 2) y += 74 # due letture grandi: sono il motivo per cui si apre l'app cw = (W - 2 * M - 20) / 2 cell(d, M, y, cw, 214, "Casa", "22,8 °C", "obiettivo 19,0 °C") cell(d, M + cw + 20, y, cw, 214, "Acqua calda", "50,7 °C", "obiettivo 55,0 °C", frac=0.78) badge(d, W - M - 34, y + 180, 3) y += 230 y = text_line(d, M, y, "Tocca un valore per regolarlo ›", 22, SECONDARY) badge(d, W - M - 34, y - 44, 4) y += 18 y = text_line(d, M, y, "Azioni rapide", 28, ACCENT, bold=True) badge(d, W - M - 34, y - 40, 5) bw = (W - 2 * M - 2 * 16) / 3 for i, t in enumerate(("Boost ACS", "Assenza", "Altri comandi")): btn(d, M + i * (bw + 16), y, bw, t, h=72) y += 92 # letture di contorno, in fondo: servono di rado y = text_line(d, M, y, "Esterna 18,0 °C · Termostato 22,8 °C · 1 zona", 22, SECONDARY) badge(d, W - M - 34, y - 44, 6) d.text((M, H - 76), "Aggiornato alle 21:18", font=font(22), fill=SECONDARY) badge(d, W - M - 34, H - 88, 7) return img def regolazioni_screen(): img = background() d = ImageDraw.Draw(img) status_bar(d) d.text((M, 84), "Regolazioni", font=font(44, bold=True), fill=PRIMARY) y = pulley_hint(d, 142, ["Aggiorna", "Dati", "Impianto", "Impostazioni"]) + 10 yh = y y = text_line(d, M, y, "Riscaldamento · casa", 28, ACCENT, bold=True) badge(d, W - M - 34, yh - 4, 1) fv = font(38, bold=True) vw = d.textlength("19,0 °C", font=fv) d.text((W - M - vw, y), "19,0 °C", font=fv, fill=PRIMARY) y += 54 y = slider(d, M, y, W - 2 * M, (19.0 - 5) / 25.0, "5 °C", "30 °C") y = presets(d, y, ["16°", "17°", "18°", "19°", "20°", "21°"]) btn(d, M, y, W - 2 * M, "Imposta 19,0 °C", h=70, primary=True) y += 104 yh = y y = text_line(d, M, y, "Acqua calda", 28, ACCENT, bold=True) badge(d, W - M - 34, yh - 4, 2) vw = d.textlength("55,0 °C", font=fv) d.text((W - M - vw, y), "55,0 °C", font=fv, fill=PRIMARY) y += 54 y = slider(d, M, y, W - 2 * M, (55.0 - 45) / 15.0, "45 °C", "60 °C") y = presets(d, y, ["45°", "47°", "50°", "55°", "60°"]) btn(d, M, y, W - 2 * M, "Imposta 55,0 °C", h=70, primary=True) y += 104 yh = y y = text_line(d, M, y, "Boost acqua calda", 28, ACCENT, bold=True) badge(d, W - M - 34, yh - 4, 3) bw = (W - 2 * M - 16) / 2 btn(d, M, y, bw, "Avvia boost", h=70) btn(d, M + bw + 16, y, bw, "Ferma boost", h=70) y += 108 text_line(d, M, y, "Il comando passa dal bridge e può richiedere qualche " "secondo: lo stato si aggiorna da solo.", 20, SECONDARY, width=W - 2 * M) badge(d, W - M - 34, y + 34, 4) return img def comandi_screen(): img = background() d = ImageDraw.Draw(img) status_bar(d) d.text((M, 84), "Comandi impianto", font=font(44, bold=True), fill=PRIMARY) y = pulley_hint(d, 142, ["Aggiorna", "Dati", "Impianto", "Impostazioni"]) + 6 yh = y y = text_line(d, M, y, "Modalità", 28, ACCENT, bold=True) badge(d, W - M - 34, yh - 4, 1) y = switch_row(d, y, "Assenza", "Abbassa la casa e spegne l'acqua calda", False) y = switch_row(d, y, "Riscaldamento forzato", "Scalda finché non lo fermi", False) y = switch_row(d, y, "Funzione riscaldamento", "Il riscaldamento può accendersi", True) y += 12 yh = y y = text_line(d, M, y, "Acqua calda", 28, ACCENT, bold=True) badge(d, W - M - 34, yh - 4, 2) y = switch_row(d, y, "Riscaldamento ACS", "Manterrà l'acqua in temperatura", True) btn(d, M, y, W - 2 * M, "Ciclo legionella", h=70) y += 96 y = text_line(d, M, y, "Rumore", 28, ACCENT, bold=True) btn(d, M, y, W - 2 * M, "Modalità notte per un'ora", h=70) y += 96 yh = y y = text_line(d, M, y, "Diagnostica", 28, ACCENT, bold=True) badge(d, W - M - 34, yh - 4, 3) btn(d, M, y, W - 2 * M, "Verifica impianto", h=70) y += 100 text_line(d, M, y, "Gli interruttori mostrano lo stato attuale: la sezione " "\"Stato attuale\" non serve più.", 20, SECONDARY, width=W - 2 * M) badge(d, W - M - 34, y + 34, 4) return img def confronto(): """Prima/dopo affiancati, con l'altezza del contenuto a colpo d'occhio.""" left = Image.open("docs/anteprima/it/dashboard.png") right = casa_screen() s = 0.52 lw, lh = int(left.width * s), int(left.height * s) rw, rh = int(right.width * s), int(right.height * s) gap, top, capt = 28, 92, 158 canvas = Image.new("RGB", (gap * 3 + lw + rw, top + max(lh, rh) + capt), (18, 22, 28)) d = ImageDraw.Draw(canvas) canvas.paste(left.resize((lw, lh)), (gap, top)) canvas.paste(right.resize((rw, rh)), (gap * 2 + lw, top)) # la home attuale scorre oltre lo schermo ov = Image.new("RGB", (lw, 74), BAD) sh = Image.blend(canvas.crop((gap, top + lh - 74, gap + lw, top + lh)), ov, 0.35) canvas.paste(sh, (gap, top + lh - 74)) d.text((gap + 16, top + lh - 52), "↓ il contenuto continua sotto", font=font(22, bold=True), fill=(20, 20, 20)) # didascalie: vanno a capo dentro la larghezza della schermata text_line(d, gap, 34, "OGGI: si scorre (≈1,7 schermate)", 26, BAD, bold=True, width=lw) text_line(d, gap * 2 + lw, 34, "PROPOSTA: sta in una schermata", 26, OK, bold=True, width=rw) text_line(d, gap, top + lh + 22, "Due slider con pulsante di conferma e tre pulsanti di " "navigazione ridondanti col menù.", 20, SECONDARY, width=lw) text_line(d, gap * 2 + lw, top + rh + 22, "Stato, due letture grandi, tre azioni rapide: le regolazioni " "hanno una pagina loro.", 20, SECONDARY, width=rw) return canvas if __name__ == "__main__": OUT.mkdir(parents=True, exist_ok=True) for name, fn in (("casa", casa_screen), ("regolazioni", regolazioni_screen), ("comandi", comandi_screen), ("confronto", confronto)): p = OUT / f"{name}.png" fn().save(p) print(f"scritto {p} ({p.stat().st_size} byte)")