main
John Lauer Add KiCad symbol and footprint, viewers, provenance, README, models.json and verification log 354b2dd 11h ago
#!/usr/bin/env python3
"""Component-page billboard hero (docs/hero.png, 4000x2500) rebuilt around a new chip render.
Same layout as the pages' earlier billboards: gradient bar, COMPONENT eyebrow, MPN title, category,
"<maker> * <package>" line, provenance footer, and the chip on the right over a teal glow disc.
The chip is a depth-buffered render of the page's board-use GLB (render.py --alpha).
usage: billboard.py chip.png out.png --title T --category "C[|second line]" --meta "M" [--footer F]"""
import argparse
import numpy as np
from PIL import Image, ImageDraw, ImageFont, ImageFilter

FONTS = "/home/adom/.claude/skills/adom-wiki-hero-image/assets/fonts/"
MONO = "/home/adom/.claude/skills/adom-theme/fonts/jetbrains-mono/JetBrainsMono-Regular.woff2"
W, H = 4000, 2500

ap = argparse.ArgumentParser()
ap.add_argument("chip"); ap.add_argument("out")
ap.add_argument("--title", required=True); ap.add_argument("--category", required=True)
ap.add_argument("--meta", required=True)
ap.add_argument("--footer", default="Manufacturer datasheet   ·   KiCad package generator CAD   ·   Adom Wiki")
ap.add_argument("--chip-box", default="2120,560,3860,1980", help="x0,y0,x1,y1 the chip is fitted into")
A = ap.parse_args()

# background: near-black blue, a teal glow to the right, a faint disc behind the chip
y, x = np.mgrid[0:H, 0:W].astype(np.float64)
base = np.array([11, 15, 21.0]) + (y / H)[..., None] * np.array([0.5, 1.5, 1.5])
glow = np.exp(-(((x - 3700) / 1700) ** 2 + ((y - 1150) / 1300) ** 2))[..., None] * np.array([3, 15, 15.0])
disc = (np.hypot(x - 3220, y - 1250) < 1240)[..., None] * np.array([2, 4, 4.0])
img = Image.fromarray(np.clip(base + glow + disc, 0, 255).astype(np.uint8)).convert("RGBA")

# gradient bar along the top
stops = [(0, (0, 229, 220)), (0.25, (26, 209, 236)), (0.5, (52, 188, 252)), (0.75, (94, 149, 251)), (1, (140, 107, 247))]
t = np.linspace(0, 1, W)
bar = np.stack([np.interp(t, [s for s, _ in stops], [c[i] for _, c in stops]) for i in range(3)], 1)
img.paste(Image.fromarray(np.repeat(bar[None], 24, 0).astype(np.uint8)), (0, 0))

# chip, fitted into the box by its alpha extent, with a soft contact shadow
chip = Image.open(A.chip).convert("RGBA")
chip = chip.crop(chip.getchannel("A").getbbox())
bx0, by0, bx1, by1 = [int(v) for v in A.chip_box.split(",")]
k = min((bx1 - bx0) / chip.width, (by1 - by0) / chip.height)
chip = chip.resize((round(chip.width * k), round(chip.height * k)), Image.LANCZOS)
cx, cy = (bx0 + bx1 - chip.width) // 2, (by0 + by1 - chip.height) // 2
sh = Image.new("RGBA", img.size, (0, 0, 0, 0))
sa = chip.getchannel("A").point(lambda v: int(v * 0.55))
sh.paste(Image.new("RGBA", chip.size, (0, 0, 0, 255)), (cx + 30, cy + 70), sa)
img.alpha_composite(sh.filter(ImageFilter.GaussianBlur(60)))
img.alpha_composite(chip, (cx, cy))

d = ImageDraw.Draw(img)
def font(f, s): return ImageFont.truetype(f, s)
def ink_top(text, fnt, x, top, fill, spacing=0):
    """draw so the ink's top edge lands on `top` (the measured billboards are ink-anchored)"""
    if spacing:
        cx_ = x
        for ch in text:
            b = d.textbbox((0, 0), ch, font=fnt)
            d.text((cx_, top - d.textbbox((0, 0), text, font=fnt)[1]), ch, font=fnt, fill=fill)
            cx_ += d.textlength(ch, font=fnt) + spacing
        return
    b = d.textbbox((0, 0), text, font=fnt)
    d.text((x - b[0], top - b[1]), text, font=fnt, fill=fill)
    return d.textbbox((x - b[0], top - b[1]), text, font=fnt)

ink_top("COMPONENT", font(MONO, 64), 260, 312, (0, 230, 220), spacing=12)
# title: Familjen Grotesk 700, cap height about 180 px, shrunk to fit left of the chip
size = 250
tf = font(FONTS + "familjen-grotesk-700-normal.woff2", size)
while d.textlength(A.title, font=tf) > 1900 and size > 120:
    size -= 4; tf = font(FONTS + "familjen-grotesk-700-normal.woff2", size)
tb = ink_top(A.title, tf, 262, 560, (238, 243, 248))
yy = tb[3] + 110
sf = font("/home/adom/.claude/skills/adom-theme/fonts/familjen-grotesk/FamiljenGrotesk-Variable.woff2", 112)
lines = A.category.split("|")
for i, ln in enumerate(lines):
    d.text((258, yy + i * 140), ln, font=sf, fill=(196, 205, 216))
yy += 140 * len(lines) + 20
d.text((258, yy), A.meta, font=font("/home/adom/.claude/skills/adom-theme/fonts/familjen-grotesk/FamiljenGrotesk-Variable.woff2", 84), fill=(139, 148, 158))
ink_top(A.footer, font(MONO, 56), 258, 2294, (91, 102, 117))
img.convert("RGB").save(A.out, optimize=True)
print("OK:", A.out, img.size, "title", size, "px")