diff --git a/brand/make_screenshots.py b/brand/make_screenshots.py new file mode 100644 index 0000000..4a22e6b --- /dev/null +++ b/brand/make_screenshots.py @@ -0,0 +1,118 @@ +"""Turns raw screenshots into 1600x900 Shopify App Store listing images in the Offerlyst brand. + +Usage: python brand/make_screenshots.py +Input: brand/screenshots/raw/-.png (any size; crop out the browser bar first) +Output: brand/screenshots/listing/-.png + alt-text.txt +Captions and alt text come from SHOTS below, matched by the leading number. +Rules (Shopify): 1600x900, no pricing, reviews or outcome claims on the image. +""" +from pathlib import Path +from PIL import Image, ImageDraw, ImageFilter, ImageFont + +ROOT = Path(__file__).resolve().parent +RAW = ROOT / "screenshots" / "raw" +OUT = ROOT / "screenshots" / "listing" +W, H = 1600, 900 + +DEEP_TEAL = (4, 91, 89) +DARK_TEAL = (2, 52, 54) +MINT = (44, 216, 193) +SOFT_ICE = (244, 251, 250) + +SHOTS = { + 1: ("Run every bundle deal at once", "Each item counts towards one offer, in the order you set", + "Offerlyst offers list showing running bundle offers with status badges and priority order"), + 2: ("Build a bundle in minutes", "Fixed bundles, mix & match or buy X get Y, with a live example cart", + "Offer editor with bundle type, discount, products and a live example cart preview"), + 3: ("A bundles page for your store", "Shoppers add the whole bundle to the cart in one click", + "Storefront bundle page with product list, bundle price and Add bundle to cart button"), + 4: ("Discounts apply automatically", "Native Shopify discount in cart and checkout, no codes", + "Shopify cart and checkout showing the bundle discount applied automatically"), + 5: ("Plan for every festival", "Templates and a calendar for Diwali, Eid, Christmas and more", + "Festival templates and calendar with ready-made bundle offers"), + 6: ("Set up in a few clicks", "A guided checklist gets your first offer live", + "Offerlyst home page with setup checklist and recent bundle results"), +} + + +def font(size, bold=False): + for name in (["seguisb.ttf", "segoeuib.ttf"] if bold else ["segoeui.ttf"]) + ["arial.ttf", "DejaVuSans.ttf"]: + for base in ("C:/Windows/Fonts/", "/usr/share/fonts/truetype/dejavu/", ""): + try: + return ImageFont.truetype(base + name, size) + except OSError: + continue + return ImageFont.load_default() + + +def background(): + bg = Image.new("RGB", (W, H), DEEP_TEAL) + px = bg.load() + for y in range(H): + for x in range(0, W, 2): + t = min(1.0, (x / W) * 0.55 + (y / H) * 0.6) + c = tuple(int(DEEP_TEAL[i] * (1 - t) + DARK_TEAL[i] * t) for i in range(3)) + px[x, y] = c + if x + 1 < W: + px[x + 1, y] = c + # soft mint glow, top-left (echoes the icon) + glow = Image.new("L", (W, H), 0) + ImageDraw.Draw(glow).ellipse((-420, -520, 620, 360), fill=90) + glow = glow.filter(ImageFilter.GaussianBlur(160)) + bg = Image.composite(Image.new("RGB", (W, H), MINT), bg, glow) + return bg + + +def rounded(img, radius): + mask = Image.new("L", img.size, 0) + ImageDraw.Draw(mask).rounded_rectangle((0, 0, img.width - 1, img.height - 1), radius, fill=255) + out = Image.new("RGBA", img.size, (0, 0, 0, 0)) + out.paste(img, (0, 0), mask) + return out + + +def compose(raw_path, title, subtitle): + bg = background().convert("RGBA") + d = ImageDraw.Draw(bg) + d.text((80, 64), title, font=font(52, bold=True), fill=(255, 255, 255)) + d.text((80, 132), subtitle, font=font(28), fill=(200, 238, 232)) + + shot = Image.open(raw_path).convert("RGB") + box_w, box_h = W - 160, H - 200 - 50 # area under the caption + scale = min(box_w / shot.width, box_h / shot.height) + shot = shot.resize((int(shot.width * scale), int(shot.height * scale)), Image.LANCZOS) + x = (W - shot.width) // 2 + y = 200 + (box_h - shot.height) // 2 + + shadow = Image.new("RGBA", (shot.width + 80, shot.height + 80), (0, 0, 0, 0)) + ImageDraw.Draw(shadow).rounded_rectangle((40, 50, shot.width + 40, shot.height + 40), 18, fill=(0, 0, 0, 110)) + shadow = shadow.filter(ImageFilter.GaussianBlur(18)) + bg.alpha_composite(shadow, (x - 40, y - 40)) + bg.alpha_composite(rounded(shot.convert("RGBA"), 14), (x, y)) + return bg.convert("RGB") + + +def main(): + OUT.mkdir(parents=True, exist_ok=True) + raws = sorted(p for p in RAW.glob("*") if p.suffix.lower() in (".png", ".jpg", ".jpeg")) + if not raws: + print(f"No screenshots in {RAW}. Name them like 1-offers.png, 2-editor.png ...") + return + alt_lines = [] + for p in raws: + try: + n = int(p.stem.split("-")[0]) + except ValueError: + print(f"skip {p.name}: name must start with a number 1-6") + continue + title, subtitle, alt = SHOTS.get(n, ("Offerlyst", "", "Offerlyst screenshot")) + out = OUT / f"{p.stem}.png" + compose(p, title, subtitle).save(out, optimize=True) + alt_lines.append(f"{out.name}: {alt}") + print(f"made {out.relative_to(ROOT.parent)}") + (OUT / "alt-text.txt").write_text("\n".join(alt_lines) + "\n", encoding="utf-8") + print(f"alt text: {(OUT / 'alt-text.txt').relative_to(ROOT.parent)}") + + +if __name__ == "__main__": + main() diff --git a/brand/screenshots/listing/6-home.png b/brand/screenshots/listing/6-home.png new file mode 100644 index 0000000..6f18258 Binary files /dev/null and b/brand/screenshots/listing/6-home.png differ diff --git a/brand/screenshots/listing/alt-text.txt b/brand/screenshots/listing/alt-text.txt new file mode 100644 index 0000000..09142d8 --- /dev/null +++ b/brand/screenshots/listing/alt-text.txt @@ -0,0 +1 @@ +6-home.png: Offerlyst home page with setup checklist and recent bundle results diff --git a/brand/screenshots/raw/6-home.png b/brand/screenshots/raw/6-home.png new file mode 100644 index 0000000..f3d373f Binary files /dev/null and b/brand/screenshots/raw/6-home.png differ