Dashboard Studio 1.6.0: app icon, clock weather, icon-only tab rail

- App store icon (icon.png) and favicon, drawn by tools/make_icon.py
- Clock can show a weather entity with today's high and low from
  weather.get_forecasts
- Side menu gets an icon-only rail style (now the default): full height,
  centered icons, active marker, names as hover/focus tooltips; phones
  keep a labelled bottom tab bar

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 22:31:00 +01:00
parent 9e7e7beaa0
commit 4187c8f38d
12 changed files with 177 additions and 15 deletions
+74
View File
@@ -0,0 +1,74 @@
"""Render the Dashboard Studio app icon: a dashboard with one lamp-lit tile."""
import sys
from PIL import Image, ImageDraw, ImageFilter
S = 1024 # draw large, downscale for anti-aliasing
def vgradient(w, h, top, bottom):
g = Image.new("RGBA", (w, h))
px = g.load()
for y in range(h):
t = y / (h - 1)
c = tuple(int(top[i] + (bottom[i] - top[i]) * t) for i in range(3)) + (255,)
for x in range(w):
px[x, y] = c
return g
def rounded_mask(size, box, r):
m = Image.new("L", size, 0)
ImageDraw.Draw(m).rounded_rectangle(box, r, fill=255)
return m
img = Image.new("RGBA", (S, S), (0, 0, 0, 0))
# background squircle
bg = vgradient(S, S, (34, 44, 78), (14, 17, 30))
img.paste(bg, (0, 0), rounded_mask((S, S), (0, 0, S - 1, S - 1), 230))
pad, gap = 150, 56
left = (pad, pad, 492, S - pad) # tall lit tile
rt = (492 + gap, pad, S - pad, 492 - gap // 2) # graph tile
rb = (492 + gap, 492 + gap // 2, S - pad, S - pad) # switch tile
# warm glow behind the lit tile
glow = Image.new("RGBA", (S, S), (0, 0, 0, 0))
ImageDraw.Draw(glow).rounded_rectangle(left, 64, fill=(255, 196, 92, 170))
img = Image.alpha_composite(img, glow.filter(ImageFilter.GaussianBlur(60)))
# lit tile with a warm gradient
lit = vgradient(left[2] - left[0], left[3] - left[1], (255, 236, 190), (255, 196, 96))
img.paste(lit, left[:2], rounded_mask(lit.size, (0, 0, lit.size[0] - 1, lit.size[1] - 1), 64))
# translucent shapes go on a layer that is blended in, not pasted over
layer = Image.new("RGBA", (S, S), (0, 0, 0, 0))
ld = ImageDraw.Draw(layer)
for box in (rt, rb):
ld.rounded_rectangle(box, 64, fill=(255, 255, 255, 26), outline=(255, 255, 255, 40), width=4)
cx = left[0] + 100
ld.ellipse((cx - 52, left[1] + 48, cx + 52, left[1] + 152), fill=(255, 255, 255, 140))
bar = (left[0] + 48, left[3] - 128, left[2] - 48, left[3] - 64)
ld.rounded_rectangle(bar, 32, fill=(122, 82, 16, 50))
img = Image.alpha_composite(img, layer)
d = ImageDraw.Draw(img)
# lamp in the badge and the filled part of the brightness bar
d.ellipse((cx - 20, left[1] + 80, cx + 20, left[1] + 120), fill=(122, 82, 16, 255))
d.rounded_rectangle((bar[0], bar[1], bar[0] + int((bar[2] - bar[0]) * 0.68), bar[3]), 32, fill=(140, 94, 20, 255))
# graph tile: a line in the accent blue
pts = [(rt[0] + 44, rt[3] - 70), (rt[0] + 110, rt[3] - 120), (rt[0] + 170, rt[3] - 96), (rt[0] + 240, rt[3] - 170), (rt[2] - 44, rt[1] + 70)]
d.line(pts, fill=(90, 170, 255, 255), width=22, joint="curve")
for p in (pts[0], pts[-1]):
d.ellipse((p[0] - 11, p[1] - 11, p[0] + 11, p[1] + 11), fill=(90, 170, 255, 255))
# switch tile: an "on" switch
sw = (rb[0] + 50, (rb[1] + rb[3]) // 2 - 46, rb[2] - 50, (rb[1] + rb[3]) // 2 + 46)
d.rounded_rectangle(sw, 46, fill=(48, 209, 88, 255))
d.ellipse((sw[2] - 86, sw[1] + 6, sw[2] - 6, sw[3] - 6), fill=(255, 255, 255, 255))
out = sys.argv[1]
img.resize((256, 256), Image.LANCZOS).save(out)
img.resize((128, 128), Image.LANCZOS).save(out.replace(".png", "-128.png"))
print("saved", out)