| 1 | #!/usr/bin/env python3 |
| 2 | # Generate the install-window background for the Eleven DMG. |
| 3 | # Renders at 2x (retina) then downsamples for the 1x rep, so both are crisp. |
| 4 | import os |
| 5 | from PIL import Image, ImageDraw, ImageFont |
| 6 | |
| 7 | OUT = os.path.dirname(os.path.abspath(__file__)) |
| 8 | S = 2 # supersample / retina factor |
| 9 | W, H = 640 * S, 400 * S |
| 10 | |
| 11 | # Purple sampled from the app icon (AppIcon icon_1024, core of the violet |
| 12 | # gradient) so the "Eleven" wordmark matches the logo exactly. |
| 13 | PURPLE = (101, 64, 208) # #6540D0 — wordmark |
| 14 | GREY = (150, 155, 168) # the count numbers — light grey, regular weight |
| 15 | DOT_GREY = (200, 203, 214) # faint separators / arrow-lighter |
| 16 | DARK = (28, 30, 42) |
| 17 | MUTED = (122, 126, 140) |
| 18 | GRAD_TOP = (252, 252, 255) |
| 19 | GRAD_BOT = (235, 237, 249) |
| 20 | |
| 21 | |
| 22 | def font(size, bold=False): |
| 23 | size = int(size * S) |
| 24 | # Prefer SF Pro (variable font — pick the Bold instance); fall back to Arial. |
| 25 | try: |
| 26 | f = ImageFont.truetype("/System/Library/Fonts/SFNS.ttf", size) |
| 27 | if bold: |
| 28 | try: |
| 29 | f.set_variation_by_name("Bold") |
| 30 | except Exception: |
| 31 | pass |
| 32 | return f |
| 33 | except Exception: |
| 34 | p = "/System/Library/Fonts/Supplemental/Arial Bold.ttf" if bold \ |
| 35 | else "/System/Library/Fonts/Supplemental/Arial.ttf" |
| 36 | return ImageFont.truetype(p, size) |
| 37 | |
| 38 | |
| 39 | # --- base gradient --- |
| 40 | img = Image.new("RGB", (W, H), GRAD_TOP) |
| 41 | d = ImageDraw.Draw(img) |
| 42 | for y in range(H): |
| 43 | t = y / (H - 1) |
| 44 | d.line([(0, y), (W, y)], fill=tuple( |
| 45 | int(a + (b - a) * t) for a, b in zip(GRAD_TOP, GRAD_BOT))) |
| 46 | d = ImageDraw.Draw(img) |
| 47 | |
| 48 | # --- the count: 1 · 2 · … · 10 → (light grey, regular, on the icon line) --- |
| 49 | row_y = 208 * S |
| 50 | num_font = font(14.5) # match the "Drag the app…" subtitle — one body size |
| 51 | arrow_font = num_font # the → is the same size as the numbers (no size-mixing) |
| 52 | inter = 2 * S # tight spacing between tokens |
| 53 | arrow_gap = 7 * S # a little breathing room before the arrow |
| 54 | |
| 55 | parts = [] # (text, font, colour) |
| 56 | for i in range(1, 11): |
| 57 | parts.append((str(i), num_font, GREY)) |
| 58 | if i < 10: |
| 59 | parts.append(("·", num_font, DOT_GREY)) |
| 60 | widths = [d.textlength(t, font=f) for (t, f, _) in parts] |
| 61 | arrow_w = d.textlength("→", font=arrow_font) |
| 62 | total = sum(widths) + inter * (len(parts) - 1) + arrow_gap + arrow_w |
| 63 | x = W / 2 - total / 2 |
| 64 | for (t, f, c), w in zip(parts, widths): |
| 65 | d.text((x, row_y), t, font=f, fill=c, anchor="lm") |
| 66 | x += w + inter |
| 67 | x = x - inter + arrow_gap |
| 68 | d.text((x, row_y), "→", font=arrow_font, fill=GREY, anchor="lm") |
| 69 | |
| 70 | # --- title --- |
| 71 | title_r = font(27, bold=True) |
| 72 | ty = 62 * S |
| 73 | seg1, seg2 = "Install ", "Eleven" |
| 74 | w1 = d.textlength(seg1, font=title_r) |
| 75 | w2 = d.textlength(seg2, font=title_r) |
| 76 | sx = (W - (w1 + w2)) / 2 |
| 77 | d.text((sx, ty), seg1, font=title_r, fill=DARK, anchor="lm") |
| 78 | d.text((sx + w1, ty), seg2, font=title_r, fill=PURPLE, anchor="lm") |
| 79 | |
| 80 | # --- subtitle --- |
| 81 | sub = font(14.5) |
| 82 | d.text((W / 2, 98 * S), "Drag the app into your Applications folder", |
| 83 | font=sub, fill=MUTED, anchor="mm") |
| 84 | |
| 85 | img.save(os.path.join(OUT, "background@2x.png")) |
| 86 | img.resize((W // S, H // S), Image.LANCZOS).save(os.path.join(OUT, "background.png")) |
| 87 | print("wrote background.png (%dx%d) + background@2x.png (%dx%d)" % |
| 88 | (W // S, H // S, W, H)) |
| 89 | |