fix(brand): logo composited at SVG-native res, then downsampled
Previous render composed directly at the target size (110×110 / 44×44), which produced thin text and lost the SVG's typographic intent. Now the composition runs at SVG-native 320×320 — same coordinates as webApp/frontend/public/logo.svg — and downsamples to the panel logo size with LANCZOS. Adds stroke_width=2 around the wordmark to fake font-weight 900 (DejaVuSans is only weight 700; no Black face is on the build host, so this is the best approximation without bundling a font binary into the firmware repo). The yellow V comes through, the wordmark is heavier, and the harbour background still palette-quantizes recognisably to Spectra-6. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
@@ -122,33 +122,35 @@ LOGO_SRC = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"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))
|
||||
"""Build at SVG-native 320×320, downsample to `size`. See
|
||||
gen_screens_13e6.py for the full rationale."""
|
||||
SRC = 320
|
||||
bg = Image.open(LOGO_SRC).convert("RGB").resize((SRC, SRC), Image.LANCZOS)
|
||||
overlay = Image.new("RGBA", (SRC, SRC), (0, 0, 0, 0))
|
||||
od = ImageDraw.Draw(overlay)
|
||||
for y in range(size):
|
||||
t = y / max(1, size - 1)
|
||||
for y in range(SRC):
|
||||
t = y / (SRC - 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)))
|
||||
od.line([(0, y), (SRC, 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)
|
||||
font = ttf("DejaVuSans-Bold.ttf", 62)
|
||||
parts = [("We", WH), ("V", YL), ("isto", WH)]
|
||||
widths = [draw.textbbox((0, 0), t, font=font)[2] for t, _ in parts]
|
||||
widths = [draw.textbbox((0, 0), t, font=font, stroke_width=2)[2] for t, _ in parts]
|
||||
total_w = sum(widths)
|
||||
x = (size - total_w) // 2
|
||||
y = int(0.547 * size) - font_px // 2 - 2
|
||||
x = (SRC - total_w) // 2
|
||||
y = 175 - 62 // 2 - 16
|
||||
for (t, fill), w in zip(parts, widths):
|
||||
draw.text((x, y), t, font=font, fill=fill)
|
||||
draw.text((x, y), t, font=font, fill=fill,
|
||||
stroke_width=2, stroke_fill=fill)
|
||||
x += w
|
||||
return bg
|
||||
return bg.resize((size, size), Image.LANCZOS)
|
||||
|
||||
def draw_logo_placeholder(img):
|
||||
img.paste(compose_logo(LOGO_SIDE), (LOGO_X, LOGO_Y))
|
||||
|
||||
@@ -161,38 +161,44 @@ LOGO_SRC = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
|
||||
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)
|
||||
Camogli harbour photo. Mirrors webApp/frontend/public/logo.svg (320×320
|
||||
viewBox).
|
||||
|
||||
The composition is done at SVG-native 320×320 then LANCZOS-downsampled
|
||||
to `size` — gets the same anti-aliasing the browser would produce, much
|
||||
cleaner text than rendering directly at the small target size.
|
||||
stroke_width=2 fakes font-weight 900 (DejaVuSans-Bold is only weight
|
||||
700; no Black face is installed on the build host)."""
|
||||
SRC = 320
|
||||
bg = Image.open(LOGO_SRC).convert("RGB").resize((SRC, SRC), 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))
|
||||
overlay = Image.new("RGBA", (SRC, SRC), (0, 0, 0, 0))
|
||||
od = ImageDraw.Draw(overlay)
|
||||
for y in range(size):
|
||||
t = y / max(1, size - 1)
|
||||
for y in range(SRC):
|
||||
t = y / (SRC - 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)))
|
||||
od.line([(0, y), (SRC, 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)
|
||||
font = ttf("DejaVuSans-Bold.ttf", 62)
|
||||
parts = [("We", WH), ("V", YL), ("isto", WH)]
|
||||
widths = [draw.textbbox((0, 0), t, font=font)[2] for t, _ in parts]
|
||||
widths = [draw.textbbox((0, 0), t, font=font, stroke_width=2)[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
|
||||
x = (SRC - total_w) // 2
|
||||
y = 175 - 62 // 2 - 16 # matches SVG translate(160 175) baseline
|
||||
for (t, fill), w in zip(parts, widths):
|
||||
draw.text((x, y), t, font=font, fill=fill)
|
||||
draw.text((x, y), t, font=font, fill=fill,
|
||||
stroke_width=2, stroke_fill=fill)
|
||||
x += w
|
||||
return bg
|
||||
|
||||
return bg.resize((size, size), Image.LANCZOS)
|
||||
|
||||
def draw_logo_placeholder(img):
|
||||
"""Paste the composed WeVisto logo in the top-right of the header."""
|
||||
|
||||