diff --git a/data/waveshare13e6-v1/ap_bg.bin b/data/waveshare13e6-v1/ap_bg.bin index a2075d2..d86001d 100644 Binary files a/data/waveshare13e6-v1/ap_bg.bin and b/data/waveshare13e6-v1/ap_bg.bin differ diff --git a/data/waveshare13e6-v1/ap_bg_preview.png b/data/waveshare13e6-v1/ap_bg_preview.png index 41ddfcd..09670f7 100644 Binary files a/data/waveshare13e6-v1/ap_bg_preview.png and b/data/waveshare13e6-v1/ap_bg_preview.png differ diff --git a/data/waveshare13e6-v1/ap_bg_retry.bin b/data/waveshare13e6-v1/ap_bg_retry.bin index a72ab66..20ed18c 100644 Binary files a/data/waveshare13e6-v1/ap_bg_retry.bin and b/data/waveshare13e6-v1/ap_bg_retry.bin differ diff --git a/data/waveshare13e6-v1/ap_bg_retry_preview.png b/data/waveshare13e6-v1/ap_bg_retry_preview.png index d4a0550..655d35f 100644 Binary files a/data/waveshare13e6-v1/ap_bg_retry_preview.png and b/data/waveshare13e6-v1/ap_bg_retry_preview.png differ diff --git a/data/waveshare13e6-v1/setup_bg.bin b/data/waveshare13e6-v1/setup_bg.bin index 7249fd2..0b2ba27 100644 Binary files a/data/waveshare13e6-v1/setup_bg.bin and b/data/waveshare13e6-v1/setup_bg.bin differ diff --git a/data/waveshare13e6-v1/setup_bg_preview.png b/data/waveshare13e6-v1/setup_bg_preview.png index 13a315b..6dee72c 100644 Binary files a/data/waveshare13e6-v1/setup_bg_preview.png and b/data/waveshare13e6-v1/setup_bg_preview.png differ diff --git a/scripts/gen_screens_13e6.py b/scripts/gen_screens_13e6.py index bb6bf2e..fb68c16 100644 --- a/scripts/gen_screens_13e6.py +++ b/scripts/gen_screens_13e6.py @@ -80,8 +80,17 @@ F_URL = ttf("DejaVuSans.ttf", 22) # URL bar mono # ── Layout constants ───────────────────────────────────────────────────────── -HEADER_H = 130 # header band height -BODY_Y = HEADER_H +# Hero treatment: full-bleed harbour photo banner (460 tall) + slim accent +# band (70 tall) carrying the section title. Replaces the prior 130-tall +# single header band — at 110×110 the harbour photo couldn't render +# recognisably under the panel's 6-colour palette, so the brand moment now +# takes the whole top of the screen. 320×320 logo card variant is kept in +# compose_logo() for web/email use. +HERO_BANNER_H = 460 +HERO_BAND_H = 70 +HERO_H = HERO_BANNER_H + HERO_BAND_H # 530 +HEADER_H = HERO_H # alias kept for grep +BODY_Y = HERO_H DIV_X = 600 # vertical divider between left + right columns LEFT_X = 0; LEFT_W = 600 RIGHT_X = 602; RIGHT_W = W - RIGHT_X # 598 @@ -91,17 +100,22 @@ RIGHT_PAD = 36 # ── QR overlay regions — MUST match the panel driver ───────────────────────── # Dynamic QRs (left as WHITE rectangles in the .bin so firmware can overlay). +# Coords shifted to accommodate the taller HERO_H=530 (was HEADER_H=130); +# epd_driver.cpp epd_draw_*_screen calls must update to match. AP_QR_MODS = 37 # version 5, ECC_LOW AP_QR_CELL = 14 # 37 × 14 = 518 px AP_QR_PX = AP_QR_MODS * AP_QR_CELL AP_QR_X = RIGHT_X + (RIGHT_W - AP_QR_PX) // 2 # 642 -AP_QR_Y = BODY_Y + 100 # 230 +AP_QR_Y = BODY_Y + 60 # 590 +# Setup QR shrunk from 16 to 14 cells (656 → 574 px) so the post-hero body +# can still fit the QR + MAC chip + caption + progress bar within the +# remaining 1070 px below the hero. SETUP_QR_MODS = 41 # version 6, ECC_LOW -SETUP_QR_CELL = 16 # 41 × 16 = 656 px +SETUP_QR_CELL = 14 # 41 × 14 = 574 px SETUP_QR_PX = SETUP_QR_MODS * SETUP_QR_CELL -SETUP_QR_X = (W - SETUP_QR_PX) // 2 # 272 -SETUP_QR_Y = BODY_Y + 360 # 490 +SETUP_QR_X = (W - SETUP_QR_PX) // 2 # 313 +SETUP_QR_Y = BODY_Y + 220 # 750 def text_center(draw, cx, y, text, font, fill): @@ -139,70 +153,109 @@ def draw_qr_frame(draw, qx, qy, qp, accent): draw.rectangle([qx - 4, qy - 4, qx + qp + 3, qy + qp + 3], outline=BK, width=4) -def draw_header(draw, accent, header_text): - """Coloured band along the top with section title on the left. Logo - placeholder is rendered separately by draw_logo_placeholder() so the - brand mark sits in the top right of every setup screen.""" - draw.rectangle([0, 0, W - 1, HEADER_H - 1], fill=accent) - draw.text((LEFT_PAD, 40), header_text, font=F_BAR, fill=BK) +def draw_hero(img, accent, header_text): + """Full-bleed photo banner + wordmark + slim accent band carrying the + section title. The brand moment occupies the top 530 px of the panel.""" + banner = compose_hero_banner(W, HERO_BANNER_H) + img.paste(banner, (0, 0)) + + draw = ImageDraw.Draw(img) + band_y = HERO_BANNER_H + draw.rectangle([0, band_y, W - 1, HERO_H - 1], fill=accent) + # F_BAR is 34pt, ~38 px tall — vertically centred in the band. + text_y = band_y + (HERO_BAND_H - 38) // 2 + draw.text((LEFT_PAD, text_y), header_text, font=F_BAR, fill=BK) -# ── 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") +# ── Brand assets ───────────────────────────────────────────────────────────── +# Source photo (5712×4284 original — gives us latitude for landscape crops +# without having to upscale the 900-square version). The square JPEG is +# used by compose_logo() for the web/email logo card; the hires original +# is used by compose_hero_banner() for the panel hero banner. +LOGO_SRC_SQUARE = os.path.join(os.path.dirname(os.path.abspath(__file__)), + "..", "..", "webApp", "brand", + "IMG_2524-square900.jpg") +LOGO_SRC_HIRES = os.path.join(os.path.dirname(os.path.abspath(__file__)), + "..", "..", "webApp", "brand", + "IMG_2524.jpg") -def compose_logo(size): - """Returns a size×size RGB PIL image of the WeVisto wordmark over the - 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. - overlay = Image.new("RGBA", (SRC, SRC), (0, 0, 0, 0)) - od = ImageDraw.Draw(overlay) - for y in range(SRC): - t = y / (SRC - 1) +def _gradient_overlay(w, h): + """Black-fade overlay matching webApp/frontend/public/logo.svg: 0% + opaque at top/bottom, 45% in the middle 42-58% band. Used to anchor + the wordmark legibly against varying photo content underneath.""" + layer = Image.new("RGBA", (w, h), (0, 0, 0, 0)) + od = ImageDraw.Draw(layer) + for y in range(h): + t = y / max(1, h - 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), (SRC, y)], fill=(0, 0, 0, int(a * 255))) - bg = Image.alpha_composite(bg.convert("RGBA"), overlay).convert("RGB") + od.line([(0, y), (w, y)], fill=(0, 0, 0, int(a * 255))) + return layer - draw = ImageDraw.Draw(bg) - font = ttf("DejaVuSans-Bold.ttf", 62) +def _render_wordmark(canvas, cx, cy, font_size, stroke=2): + """We[white] V[yellow] isto[white] centred at (cx, cy). stroke_width + fakes font-weight 900 — DejaVuSans-Bold is only weight 700.""" + draw = ImageDraw.Draw(canvas) + font = ttf("DejaVuSans-Bold.ttf", font_size) parts = [("We", WH), ("V", YL), ("isto", WH)] - widths = [draw.textbbox((0, 0), t, font=font, stroke_width=2)[2] for t, _ in parts] + widths = [draw.textbbox((0, 0), t, font=font, stroke_width=stroke)[2] for t, _ in parts] total_w = sum(widths) - x = (SRC - total_w) // 2 - y = 175 - 62 // 2 - 16 # matches SVG translate(160 175) baseline + x = cx - total_w // 2 + y = cy - font_size // 2 - max(2, font_size // 16) for (t, fill), w in zip(parts, widths): draw.text((x, y), t, font=font, fill=fill, - stroke_width=2, stroke_fill=fill) + stroke_width=stroke, stroke_fill=fill) x += w - return bg.resize((size, size), Image.LANCZOS) +def compose_logo(size): + """Square logo card (photo + gradient + wordmark) at `size`×`size`. + Composes at SVG-native 320×320 then LANCZOS-downsamples for clean + anti-aliasing. Kept for any future use that wants the square mark on + the panel; the active hero uses compose_hero_banner() instead.""" + SRC = 320 + bg = Image.open(LOGO_SRC_SQUARE).convert("RGB").resize((SRC, SRC), Image.LANCZOS) + bg = Image.alpha_composite(bg.convert("RGBA"), _gradient_overlay(SRC, SRC)).convert("RGB") + _render_wordmark(bg, SRC // 2, int(0.547 * SRC), 62, stroke=2) + return bg.resize((size, size), Image.LANCZOS) if size != SRC else bg -def draw_logo_placeholder(img): - """Paste the composed WeVisto logo in the top-right of the header.""" - img.paste(compose_logo(LOGO_SIDE), (LOGO_X, LOGO_Y)) +def compose_hero_banner(banner_w, banner_h): + """Wide hero banner: harbour photo cropped from the hi-res original to + the target aspect, gradient overlay for legibility, Floyd-Steinberg + dithered to the Spectra-6 palette (so the photo reads as continuous + tone instead of nearest-neighbour colour blocks), then a big WeVisto + wordmark composited on top in crisp palette-exact colours.""" + src = Image.open(LOGO_SRC_HIRES).convert("RGB") + sw, sh = src.size + src_ar = sw / sh + tgt_ar = banner_w / banner_h + if src_ar > tgt_ar: + new_w = int(sh * tgt_ar) + x0 = (sw - new_w) // 2 + src = src.crop((x0, 0, x0 + new_w, sh)) + else: + new_h = int(sw / tgt_ar) + y0 = int(sh * 0.42 - new_h // 2) + y0 = max(0, min(y0, sh - new_h)) + src = src.crop((0, y0, sw, y0 + new_h)) + banner = src.resize((banner_w, banner_h), Image.LANCZOS) + banner = Image.alpha_composite(banner.convert("RGBA"), + _gradient_overlay(banner_w, banner_h)).convert("RGB") + + # FS-dither the photo + gradient against the Spectra-6 palette. Without + # this, the harbour collapses to blocky nearest-neighbour colour fields + # when pack()'s nearest() runs over the final panel image. + pal = Image.new("P", (1, 1)) + pal.putpalette([*BK, *WH, *YL, *RD, *BL, *GR, *([0, 0, 0] * 250)]) + banner = banner.quantize(palette=pal, dither=Image.FLOYDSTEINBERG).convert("RGB") + + # Wordmark rendered AFTER the dither so text edges stay crisp instead + # of getting stippled. Colours used (WH, YL) are already palette-exact. + _render_wordmark(banner, banner_w // 2, int(0.55 * banner_h), 200, stroke=4) + return banner def draw_divider(draw): @@ -244,7 +297,7 @@ def paste_rotated_text(img, text, font, fill, anchor_xy, ccw_degrees=90): img.paste(rotated, anchor_xy, rotated) -def orientation_diagrams(img, cx, top_y, label_color=None): +def orientation_diagrams(img, cx, top_y, label_color=None, compact=False): """ Side-by-side PORTRAIT / LANDSCAPE diagrams illustrating the two ways the user can hang the frame. Drawn in current portrait-view coords: @@ -273,12 +326,19 @@ def orientation_diagrams(img, cx, top_y, label_color=None): # Same external dimensions for both diagrams (the LANDSCAPE is a 90°-CCW # rotation of an upright wide rect — its bounding box is square'd to - # match portrait so the pair sits in a clean two-up grid). - diag_w, diag_h = 130, 200 - ribbon_thick = 14 - pair_gap = 100 # extra room so the rotated label doesn't crowd the divider + # match portrait so the pair sits in a clean two-up grid). `compact` + # shrinks the diagrams ~35% so they fit alongside the manual QR in the + # AP screen's tighter post-hero body. + if compact: + diag_w, diag_h = 90, 140 + ribbon_thick = 10 + pair_gap = 70 + else: + diag_w, diag_h = 130, 200 + ribbon_thick = 14 + pair_gap = 100 pair_w = diag_w * 2 + pair_gap - base_y = top_y + 100 + base_y = top_y + 60 if compact else top_y + 100 pt_x = cx - pair_w // 2 ls_x = pt_x + diag_w + pair_gap @@ -328,30 +388,31 @@ def gen_ap(accent=YL, img = Image.new("RGB", (W, H), WH) draw = ImageDraw.Draw(img) - draw_header(draw, accent, header_text) - draw_logo_placeholder(img) + draw_hero(img, accent, header_text) draw_divider(draw) # ── LEFT COLUMN ────────────────────────────────────────────────────────── - # Big "Connect to WiFi" heading with accent underline. + # Single-line "Connect to WiFi" heading — was a two-line block under the + # old shallow header; the new 530-tall hero swallowed the vertical budget + # so the body collapses to one line at a smaller F_TITLE size. head_y = BODY_Y + 30 - draw.text((LEFT_PAD, head_y), "Connect to", font=F_HEAD, fill=BK) - draw.text((LEFT_PAD, head_y + 90), "WiFi", font=F_HEAD, fill=BK) - bb = draw.textbbox((0, 0), "WiFi", font=F_HEAD) - underline_y = head_y + 90 + bb[3] + 6 + f_head = ttf("DejaVuSans-Bold.ttf", 60) + draw.text((LEFT_PAD, head_y), "Connect to WiFi", font=f_head, fill=BK) + bb = draw.textbbox((0, 0), "Connect to WiFi", font=f_head) + underline_y = head_y + bb[3] + 6 draw.rectangle([LEFT_PAD, underline_y, LEFT_PAD + bb[2] + 4, underline_y + 6], fill=accent) - # 5 numbered steps — same instructions as the 7.3" but with the larger - # type that fits comfortably on a 13.3" portrait body. + # 4 numbered steps. step_pitch tightened from 112 → 108 so the steps + + # orientation diagram + manual QR all fit in the post-hero 1070-px body. steps = [ ("Turn on your WeVisto", ""), ("Unlock your phone", ""), ("Scan QR 1", "This will connect your phone to the WeVisto"), ("Scan QR 2", "This will open the WeVisto setup page"), ] - step_y0 = head_y + 240 - step_pitch = 112 + step_y0 = underline_y + 60 + step_pitch = 108 box = 50 # numbered black box size text_x = LEFT_PAD + box + 22 text_max_w = DIV_X - text_x - 18 # don't cross the column divider @@ -366,16 +427,19 @@ def gen_ap(accent=YL, for j, line in enumerate(wrap_to_width(draw, l2, F_STEP, text_max_w)): draw.text((text_x, by + 32 + j * line_h), line, font=F_STEP, fill=BK) - # Orientation diagrams — tucked between the steps and the manual QR so - # the user sees both possible hanging positions before they commit. - orientation_diagrams(img, LEFT_X + LEFT_W // 2, step_y0 + len(steps) * step_pitch + 60) + # Orientation diagrams — between the steps and the manual QR. Compact + # variant (diag_h=130) so the LEFT column fits inside the smaller body. + orientation_diagrams(img, LEFT_X + LEFT_W // 2, + step_y0 + len(steps) * step_pitch + 30, + compact=True) # Manual QR bottom-left — covers "captive portal didn't open" + general - # troubleshooting. Same intent as the 7.3" but bigger box, more room. + # troubleshooting. Shrunk to box_size=5 (~205 px) to share the column + # with the orientation diagrams above it. manual_qr = qrcode.QRCode( version=None, error_correction=qrcode.constants.ERROR_CORRECT_L, - box_size=7, + box_size=5, border=2, ) manual_qr.add_data(MANUAL_URL) @@ -383,15 +447,14 @@ def gen_ap(accent=YL, manual_img = manual_qr.make_image(fill_color=BK, back_color=WH).convert("RGB") mw, mh = manual_img.size manual_x = LEFT_PAD - manual_y = H - mh - 60 + manual_y = H - mh - 30 img.paste(manual_img, (manual_x, manual_y)) - # Side label aligned with manual QR. - label_x = manual_x + mw + 28 - label_y = manual_y + (mh - 80) // 2 + label_x = manual_x + mw + 24 + label_y = manual_y + (mh - 64) // 2 draw.text((label_x, label_y), "Need help?", font=F_STEP_B, fill=BK) - draw.text((label_x, label_y + 38), "Scan for setup", font=F_STEP, fill=BK) - draw.text((label_x, label_y + 70), "& troubleshoot", font=F_STEP, fill=BK) + draw.text((label_x, label_y + 32), "Scan for setup", font=F_STEP, fill=BK) + draw.text((label_x, label_y + 58), "& troubleshoot", font=F_STEP, fill=BK) # ── RIGHT COLUMN ───────────────────────────────────────────────────────── rcx = RIGHT_X + RIGHT_W // 2 @@ -448,29 +511,27 @@ def gen_setup(): img = Image.new("RGB", (W, H), WH) draw = ImageDraw.Draw(img) - draw_header(draw, GR, "WIFI CONNECTED — STEP 2 OF 2") - draw_logo_placeholder(img) + draw_hero(img, GR, "WIFI CONNECTED — STEP 2 OF 2") - # Centered heading - text_center(draw, W // 2, BODY_Y + 30, "Almost ready", F_HEAD, BK) + # Centered heading. Two-line instructions that used to sit beneath the + # underline were dropped in the redesign — the hero already establishes + # WeVisto, the "SCAN TO FINISH" label sits right above the QR, and the + # MAC chip below identifies the device. Three labels for one QR was + # redundant under the tighter post-hero body height. + text_center(draw, W // 2, BODY_Y + 30, "Almost ready", F_HEAD, BK) bb = draw.textbbox((0, 0), "Almost ready", font=F_HEAD) - underline_w = bb[2] + 4 text_w = bb[2] - bb[0] underline_x = (W - text_w) // 2 underline_y = BODY_Y + 30 + bb[3] + 6 draw.rectangle([underline_x, underline_y, underline_x + text_w, underline_y + 6], fill=GR) - text_center(draw, W // 2, BODY_Y + 170, "Scan the QR to link this frame", F_STEP_B, BK) - text_center(draw, W // 2, BODY_Y + 210, "to your wevisto.com account.", F_STEP, BK) - # Setup QR with decorative border + green/black double frame draw_qr_frame(draw, SETUP_QR_X, SETUP_QR_Y, SETUP_QR_PX, GR) leave_qr_white(draw, SETUP_QR_X, SETUP_QR_Y, SETUP_QR_PX) - text_center(draw, W // 2, - SETUP_QR_Y - 56, - "SCAN TO FINISH", - F_LABEL, BK) + text_center(draw, W // 2, SETUP_QR_Y - 56, + "Scan the QR to link your wevisto.com account", + F_STEP_B, BK) # MAC chip below QR — frame's identifier so the user knows which device # they're claiming. Static placeholder until firmware writes text here. diff --git a/src/panels/waveshare13e6/v1/epd_driver.cpp b/src/panels/waveshare13e6/v1/epd_driver.cpp index 9967673..0e203e8 100644 --- a/src/panels/waveshare13e6/v1/epd_driver.cpp +++ b/src/panels/waveshare13e6/v1/epd_driver.cpp @@ -428,13 +428,13 @@ static void draw_from_lfs(const char* path, uint8_t fallback_color, // firmware paints the live QR into it. Mismatch = the QR draws over // decorative borders or the QR placeholder shows through. void epd_draw_ap_screen(QRCode* qr) { - draw_from_lfs("/ap_bg.bin", COLOR_YELLOW, qr, 642, 230, 14); + draw_from_lfs("/ap_bg.bin", COLOR_YELLOW, qr, 642, 590, 14); } void epd_draw_ap_screen_retry(QRCode* qr) { - draw_from_lfs("/ap_bg_retry.bin", COLOR_RED, qr, 642, 230, 14); + draw_from_lfs("/ap_bg_retry.bin", COLOR_RED, qr, 642, 590, 14); } void epd_draw_setup_screen(QRCode* qr) { - draw_from_lfs("/setup_bg.bin", COLOR_GREEN, qr, 272, 490, 16); + draw_from_lfs("/setup_bg.bin", COLOR_GREEN, qr, 313, 750, 14); }