|
| 1 | +"""Generate a 240x240 Product Hunt thumbnail for PRISM. |
| 2 | +
|
| 3 | +Run: python scripts/gen_producthunt_thumbnail.py |
| 4 | +Output: assets/producthunt-thumbnail.png |
| 5 | +""" |
| 6 | +from __future__ import annotations |
| 7 | + |
| 8 | +import math |
| 9 | +from pathlib import Path |
| 10 | + |
| 11 | +from PIL import Image, ImageDraw, ImageFont |
| 12 | + |
| 13 | + |
| 14 | +def glow_polygon(draw: ImageDraw.ImageDraw, pts: list, color: tuple) -> None: |
| 15 | + r, g, b = color |
| 16 | + draw.polygon(pts, fill=(r, g, b, 10)) |
| 17 | + draw.polygon(pts, outline=(r, g, b, 13), width=14) |
| 18 | + draw.polygon(pts, outline=(r, g, b, 36), width=6) |
| 19 | + draw.polygon(pts, outline=(r, g, b, 242), width=2) |
| 20 | + |
| 21 | + |
| 22 | +def glow_line( |
| 23 | + draw: ImageDraw.ImageDraw, |
| 24 | + p0: tuple, |
| 25 | + p1: tuple, |
| 26 | + color: tuple, |
| 27 | + wide: int = 12, |
| 28 | + mid: int = 5, |
| 29 | +) -> None: |
| 30 | + r, g, b = color |
| 31 | + draw.line([p0, p1], fill=(r, g, b, 18), width=wide) |
| 32 | + draw.line([p0, p1], fill=(r, g, b, 45), width=mid) |
| 33 | + draw.line([p0, p1], fill=(r, g, b, 220), width=2) |
| 34 | + |
| 35 | + |
| 36 | +def main() -> None: |
| 37 | + W, H = 240, 240 |
| 38 | + BG = (13, 17, 23) |
| 39 | + BLUE = (88, 166, 255) |
| 40 | + |
| 41 | + img = Image.new("RGBA", (W, H), (*BG, 255)) |
| 42 | + draw = ImageDraw.Draw(img, "RGBA") |
| 43 | + |
| 44 | + # --- Triangle --- |
| 45 | + # apex top-center, base roughly at y=132 |
| 46 | + apex = (120, 18) |
| 47 | + bl = (62, 134) |
| 48 | + br = (178, 134) |
| 49 | + glow_polygon(draw, [apex, bl, br], BLUE) |
| 50 | + |
| 51 | + # Apex dot |
| 52 | + ax, ay = apex |
| 53 | + draw.ellipse((ax - 6, ay - 6, ax + 6, ay + 6), fill=(*BLUE, 20)) |
| 54 | + draw.ellipse((ax - 3, ay - 3, ax + 3, ay + 3), fill=(*BLUE, 230)) |
| 55 | + |
| 56 | + # --- Incoming white beam (left edge → left face midpoint) --- |
| 57 | + left_mid = ((apex[0] + bl[0]) // 2, (apex[1] + bl[1]) // 2) # ~(91, 76) |
| 58 | + beam_start = (0, left_mid[1]) |
| 59 | + draw.line([beam_start, left_mid], fill=(255, 255, 255, 13), width=14) |
| 60 | + draw.line([beam_start, left_mid], fill=(255, 255, 255, 25), width=6) |
| 61 | + draw.line([beam_start, left_mid], fill=(255, 255, 255, 180), width=2) |
| 62 | + |
| 63 | + # --- Refracted rays from right-face exit point --- |
| 64 | + exit_pt = ((apex[0] + br[0]) // 2, (apex[1] + br[1]) // 2) # ~(149, 76) |
| 65 | + |
| 66 | + rays = [ |
| 67 | + ((248, 81, 73), (240, 28)), # red – up-right |
| 68 | + ((209, 134, 22), (240, 52)), # orange |
| 69 | + ((210, 153, 34), (240, 76)), # yellow – near horizontal |
| 70 | + ((63, 185, 80), (240, 100)), # green |
| 71 | + ((88, 166, 255), (240, 122)), # blue |
| 72 | + ((188, 140, 255), (240, 142)), # violet – down-right |
| 73 | + ] |
| 74 | + for color, end in rays: |
| 75 | + glow_line(draw, exit_pt, end, color) |
| 76 | + |
| 77 | + # --- Text --- |
| 78 | + font_bold = ImageFont.truetype(r"C:\Windows\Fonts\consolab.ttf", 38) |
| 79 | + font_tag = ImageFont.truetype(r"C:\Windows\Fonts\consola.ttf", 13) |
| 80 | + |
| 81 | + # "PRISM" |
| 82 | + prism_text = "PRISM" |
| 83 | + bb = draw.textbbox((0, 0), prism_text, font=font_bold) |
| 84 | + tw = bb[2] - bb[0] |
| 85 | + draw.text(((W - tw) // 2, 152), prism_text, font=font_bold, fill=(230, 237, 243, 255)) |
| 86 | + |
| 87 | + # "session intelligence" |
| 88 | + tag_text = "session intelligence" |
| 89 | + bb2 = draw.textbbox((0, 0), tag_text, font=font_tag) |
| 90 | + tw2 = bb2[2] - bb2[0] |
| 91 | + draw.text(((W - tw2) // 2, 201), tag_text, font=font_tag, fill=(139, 148, 158, 255)) |
| 92 | + |
| 93 | + # --- Save --- |
| 94 | + out = Image.new("RGB", (W, H), BG) |
| 95 | + out.paste(img, mask=img.split()[3]) |
| 96 | + |
| 97 | + out_path = Path(__file__).parent.parent / "assets" / "producthunt-thumbnail.png" |
| 98 | + out.save(str(out_path), optimize=True) |
| 99 | + size_kb = out_path.stat().st_size // 1024 |
| 100 | + print(f"Saved {out_path} ({W}x{H}, {size_kb} KB)") |
| 101 | + |
| 102 | + |
| 103 | +if __name__ == "__main__": |
| 104 | + main() |
0 commit comments