feat(brand): real logo composited onto setup screens

Replaces the bordered text placeholder with the composed WeVisto logo
(harbour photo + dark gradient + 'WeVisto' wordmark with the yellow V)
in the top-right of every setup screen. Pure-PIL composition mirroring
webApp/frontend/public/logo.svg — no cairosvg/rsvg dependency needed.

Source asset: webApp/brand/IMG_2524-square900.jpg. Logo box went from a
300×92 wide placeholder to an 110×110 square on 13.3 and a 44×44 square
on 7.3 — matches Matt's request for a 'nice square, readable rendition'
and keeps a comfortable margin within each panel's header band.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 22:38:13 -04:00
parent c5bd0458ab
commit eff34717c9
14 changed files with 90 additions and 51 deletions
File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 37 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 37 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

+39 -22
View File
@@ -111,30 +111,47 @@ def leave_qr_white(draw, qr_x, qr_y, qr_px):
"""Blank the QR overlay region so firmware can write the real QR."""
draw.rectangle([qr_x, qr_y, qr_x+qr_px-1, qr_y+qr_px-1], fill=WH)
# ── Logo placeholder ─────────────────────────────────────────────────────────
# Top-right brand placeholder for both setup screens. When the real logo
# lands, replace draw_logo_placeholder() with a paste of assets/logo.png
# (or similar) at the same coordinates so the layout stays stable.
LOGO_W = 200
LOGO_H = 40
LOGO_X = 800 - LOGO_W - 16
LOGO_Y = (52 - LOGO_H) // 2 # BAR_H=52
# ── Logo ─────────────────────────────────────────────────────────────────────
# Composed mirror of webApp/frontend/public/logo.svg, rendered in pure PIL.
# See gen_screens_13e6.py for the full rationale.
LOGO_SIDE = 44 # square; fits within BAR_H=52 with 4-px margin
LOGO_X = 800 - LOGO_SIDE - 16
LOGO_Y = (52 - LOGO_SIDE) // 2
LOGO_SRC = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"..", "..", "webApp", "brand",
"IMG_2524-square900.jpg")
def compose_logo(size):
"""Returns a size×size RGB PIL image — harbour photo + black-fade + wordmark."""
bg = Image.open(LOGO_SRC).convert("RGB").resize((size, size), Image.LANCZOS)
overlay = Image.new("RGBA", (size, size), (0, 0, 0, 0))
od = ImageDraw.Draw(overlay)
for y in range(size):
t = y / max(1, size - 1)
if t < 0.42:
a = 0.45 * (t / 0.42)
elif t < 0.58:
a = 0.45
else:
a = 0.45 * ((1 - t) / 0.42)
od.line([(0, y), (size, y)], fill=(0, 0, 0, int(a * 255)))
bg = Image.alpha_composite(bg.convert("RGBA"), overlay).convert("RGB")
draw = ImageDraw.Draw(bg)
font_px = max(8, int(62 * size / 320))
font = ttf("DejaVuSans-Bold.ttf", font_px)
parts = [("We", WH), ("V", YL), ("isto", WH)]
widths = [draw.textbbox((0, 0), t, font=font)[2] for t, _ in parts]
total_w = sum(widths)
x = (size - total_w) // 2
y = int(0.547 * size) - font_px // 2 - 2
for (t, fill), w in zip(parts, widths):
draw.text((x, y), t, font=font, fill=fill)
x += w
return bg
def draw_logo_placeholder(img):
draw = ImageDraw.Draw(img)
draw.rectangle([LOGO_X, LOGO_Y, LOGO_X + LOGO_W - 1, LOGO_Y + LOGO_H - 1], fill=WH)
draw.rectangle([LOGO_X, LOGO_Y, LOGO_X + LOGO_W - 1, LOGO_Y + LOGO_H - 1],
outline=BK, width=2)
f_logo = ttf("DejaVuSans-Bold.ttf", 18)
bb = draw.textbbox((0, 0), "WeVisto", font=f_logo)
tw = bb[2] - bb[0]
draw.text((LOGO_X + (LOGO_W - tw) // 2, LOGO_Y + 4), "WeVisto",
font=f_logo, fill=BK)
f_hint = ttf("DejaVuSans-Bold.ttf", 8)
bb = draw.textbbox((0, 0), "PLACEHOLDER", font=f_hint)
pw = bb[2] - bb[0]
draw.text((LOGO_X + (LOGO_W - pw) // 2, LOGO_Y + LOGO_H - 12),
"PLACEHOLDER", font=f_hint, fill=BK)
img.paste(compose_logo(LOGO_SIDE), (LOGO_X, LOGO_Y))
def wrap_to_width(draw, text, font, max_w):
+48 -26
View File
@@ -147,34 +147,56 @@ def draw_header(draw, accent, header_text):
draw.text((LEFT_PAD, 40), header_text, font=F_BAR, fill=BK)
# ── Logo placeholder ─────────────────────────────────────────────────────────
# Box dimensions for the top-right placeholder. When the real brand mark
# lands, replace draw_logo_placeholder() with a paste of assets/logo.png
# (or similar) at these same coordinates so the layout stays stable.
LOGO_W = 300
LOGO_H = 92
LOGO_X = W - LOGO_W - LEFT_PAD # 864
LOGO_Y = (HEADER_H - LOGO_H) // 2 # 19
# ── Logo ─────────────────────────────────────────────────────────────────────
# Composed mirror of webApp/frontend/public/logo.svg: harbour photo +
# black-fade gradient + "WeVisto" wordmark (V in yellow). Rendered in
# pure PIL because cairosvg / rsvg / imagemagick aren't installed on the
# build host. Source photo lives in webApp/brand/.
LOGO_SIDE = 110 # square; fits within HEADER_H=130 with padding
LOGO_X = W - LOGO_SIDE - LEFT_PAD # 1054
LOGO_Y = (HEADER_H - LOGO_SIDE) // 2 # 10
LOGO_SRC = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"..", "..", "webApp", "brand",
"IMG_2524-square900.jpg")
def compose_logo(size):
"""Returns a size×size RGB PIL image of the WeVisto wordmark over the
Camogli harbour photo. Matches the SVG layout in
webApp/frontend/public/logo.svg (320×320 viewBox)."""
bg = Image.open(LOGO_SRC).convert("RGB").resize((size, size), Image.LANCZOS)
# Black overlay band: 45% opacity peak at 42-58% height, fading at edges.
# Matches the linearGradient stops in the SVG.
overlay = Image.new("RGBA", (size, size), (0, 0, 0, 0))
od = ImageDraw.Draw(overlay)
for y in range(size):
t = y / max(1, size - 1)
if t < 0.42:
a = 0.45 * (t / 0.42)
elif t < 0.58:
a = 0.45
else:
a = 0.45 * ((1 - t) / 0.42)
od.line([(0, y), (size, y)], fill=(0, 0, 0, int(a * 255)))
bg = Image.alpha_composite(bg.convert("RGBA"), overlay).convert("RGB")
# Wordmark: "We" white + "V" yellow + "isto" white, centred at y≈0.547.
draw = ImageDraw.Draw(bg)
font_px = max(10, int(62 * size / 320))
font = ttf("DejaVuSans-Bold.ttf", font_px)
parts = [("We", WH), ("V", YL), ("isto", WH)]
widths = [draw.textbbox((0, 0), t, font=font)[2] for t, _ in parts]
total_w = sum(widths)
x = (size - total_w) // 2
y = int(0.547 * size) - font_px // 2 - 4 # nudged up to match SVG
for (t, fill), w in zip(parts, widths):
draw.text((x, y), t, font=font, fill=fill)
x += w
return bg
def draw_logo_placeholder(img):
"""White rounded-look box with 'WeVisto' brand text in the top right of
the header. Stand-in for the real logo — same position will be reused."""
draw = ImageDraw.Draw(img)
draw.rectangle([LOGO_X, LOGO_Y, LOGO_X + LOGO_W - 1, LOGO_Y + LOGO_H - 1], fill=WH)
draw.rectangle([LOGO_X, LOGO_Y, LOGO_X + LOGO_W - 1, LOGO_Y + LOGO_H - 1],
outline=BK, width=4)
f_logo = ttf("DejaVuSans-Bold.ttf", 44)
bb = draw.textbbox((0, 0), "WeVisto", font=f_logo)
tw = bb[2] - bb[0]
draw.text((LOGO_X + (LOGO_W - tw) // 2, LOGO_Y + 12), "WeVisto",
font=f_logo, fill=BK)
f_hint = ttf("DejaVuSans-Bold.ttf", 14)
bb = draw.textbbox((0, 0), "PLACEHOLDER", font=f_hint)
pw = bb[2] - bb[0]
draw.text((LOGO_X + (LOGO_W - pw) // 2, LOGO_Y + LOGO_H - 22),
"PLACEHOLDER", font=f_hint, fill=BK)
"""Paste the composed WeVisto logo in the top-right of the header."""
img.paste(compose_logo(LOGO_SIDE), (LOGO_X, LOGO_Y))
def draw_divider(draw):