| 1 | #!/usr/bin/env python3 |
| 2 | """Generate the extension icons. |
| 3 | |
| 4 | Design: a dark terminal tile with a green `>_` prompt, and a thin accent bar |
| 5 | down the right side standing for the sidebar the terminal lives in. The prompt |
| 6 | is the part that has to survive 16px, so at small sizes the accent bar and the |
| 7 | corner rounding are simplified rather than shrunk into mush. |
| 8 | |
| 9 | Colours come from the dark theme in extension/lib/theme.js. |
| 10 | |
| 11 | python3 assets/make-icons.py |
| 12 | """ |
| 13 | |
| 14 | from PIL import Image, ImageDraw |
| 15 | |
| 16 | BG = (20, 22, 26, 255) # --bg #14161a |
| 17 | EDGE = (45, 49, 56, 255) # --border #2d3138 |
| 18 | GREEN = (110, 193, 119, 255) # ok #6ec177 |
| 19 | FG = (215, 218, 224, 255) # --fg #d7dae0 |
| 20 | ACCENT = (59, 110, 165, 255) # --accent #3b6ea5 |
| 21 | |
| 22 | # Draw everything on a 128-unit grid at 8x, then downsample. |
| 23 | GRID = 128 |
| 24 | SS = 8 |
| 25 | |
| 26 | SIZES = [16, 32, 48, 96, 128] |
| 27 | |
| 28 | |
| 29 | def draw_icon(size: int) -> Image.Image: |
| 30 | small = size <= 20 |
| 31 | w = GRID * SS |
| 32 | img = Image.new("RGBA", (w, w), (0, 0, 0, 0)) |
| 33 | d = ImageDraw.Draw(img) |
| 34 | |
| 35 | def u(v: float) -> float: |
| 36 | """Grid units -> supersampled pixels.""" |
| 37 | return v * SS |
| 38 | |
| 39 | # --- tile --------------------------------------------------------------- |
| 40 | # Less rounding at 16px: a large radius eats the glyph's breathing room. |
| 41 | radius = 20 if small else 27 |
| 42 | d.rounded_rectangle( |
| 43 | [u(2), u(2), u(GRID - 2), u(GRID - 2)], |
| 44 | radius=u(radius), |
| 45 | fill=BG, |
| 46 | outline=EDGE, |
| 47 | width=int(u(2)), |
| 48 | ) |
| 49 | |
| 50 | # --- sidebar accent bar ------------------------------------------------- |
| 51 | # Dropped at 16px, where it would be a single muddy pixel column. |
| 52 | if not small: |
| 53 | d.rounded_rectangle( |
| 54 | [u(102), u(34), u(109), u(94)], |
| 55 | radius=u(3.5), |
| 56 | fill=ACCENT, |
| 57 | ) |
| 58 | |
| 59 | # --- prompt: >_ --------------------------------------------------------- |
| 60 | # Shifted right slightly when there's no accent bar, to stay centred. |
| 61 | dx = 6 if small else 0 |
| 62 | stroke = int(u(11 if small else 10)) |
| 63 | |
| 64 | d.line( |
| 65 | [(u(30 + dx), u(44)), (u(53 + dx), u(65)), (u(30 + dx), u(86))], |
| 66 | fill=GREEN, |
| 67 | width=stroke, |
| 68 | joint="curve", |
| 69 | ) |
| 70 | # Round the ends by hand; PIL only rounds joints, not caps. |
| 71 | r = stroke / 2 |
| 72 | for px, py in [(u(30 + dx), u(44)), (u(30 + dx), u(86))]: |
| 73 | d.ellipse([px - r, py - r, px + r, py + r], fill=GREEN) |
| 74 | |
| 75 | # Underscore / cursor. |
| 76 | bar_left, bar_right = u(62 + dx), u(92 + dx if not small else 96) |
| 77 | d.rounded_rectangle( |
| 78 | [bar_left, u(78), bar_right, u(88)], |
| 79 | radius=u(5), |
| 80 | fill=FG, |
| 81 | ) |
| 82 | |
| 83 | return img.resize((size, size), Image.LANCZOS) |
| 84 | |
| 85 | |
| 86 | def main() -> None: |
| 87 | import pathlib |
| 88 | |
| 89 | out = pathlib.Path(__file__).resolve().parent.parent / "extension" / "icons" |
| 90 | out.mkdir(parents=True, exist_ok=True) |
| 91 | for size in SIZES: |
| 92 | path = out / f"icon{size}.png" |
| 93 | draw_icon(size).save(path) |
| 94 | print(f"wrote {path.relative_to(out.parent.parent)}") |
| 95 | |
| 96 | |
| 97 | if __name__ == "__main__": |
| 98 | main() |