diff --git a/Makefile b/Makefile index 8090b6e..8bd97d2 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,7 @@ # make svgs # only SVGs # make pngs # only PNGs (requires SVGs to exist) # make favicons # only favicons +# make og # only Open Graph / social-share cards # make tokens # only color tokens (brand.css + brand.json) # make docs # only BRANDING.md # make clean # remove all generated assets @@ -17,7 +18,7 @@ PYTHON := python3 SCRIPTS := scripts ASSETS := logos -.PHONY: all build svgs pngs favicons tokens docs clean check install help +.PHONY: all build svgs pngs favicons og tokens docs clean check install help all: build docs @@ -28,6 +29,7 @@ help: @echo " svgs - generate only SVGs" @echo " pngs - generate only PNGs (requires SVGs)" @echo " favicons - generate only favicons" + @echo " og - generate Open Graph / social-share cards" @echo " tokens - generate color tokens (brand.css + brand.json)" @echo " docs - regenerate BRANDING.md from brand.toml" @echo " clean - remove all generated assets" @@ -46,6 +48,9 @@ pngs: favicons: $(PYTHON) $(SCRIPTS)/build.py favicon +og: + $(PYTHON) $(SCRIPTS)/build.py og + tokens: $(PYTHON) $(SCRIPTS)/build.py tokens diff --git a/brand.toml b/brand.toml index 04e1acc..7767fe6 100644 --- a/brand.toml +++ b/brand.toml @@ -148,6 +148,24 @@ px = [200, 400, 800] px = [16, 32, 48, 64, 180, 192, 512] ico_sizes = [16, 32, 48, 64] # multi-resolution .ico bundle +# ────────────────────────────────────────────────────────────────────── +# Open Graph / social-share cards +# +# The image link-unfurlers (Slack, Discord, iMessage, Twitter) and +# GitHub's repo/org social-preview pull. The dark-bg lockup centered on +# the dark wash, with safe margins. `make og` renders one PNG per size. +# ────────────────────────────────────────────────────────────────────── + +[og] +background = "wash_dark" # full-bleed background color (palette key) +lockup_variant = "dark_bg" # which lockup treatment to center +margin_ratio = 0.14 # safe padding as a fraction of the shorter axis + +[og.sizes] +# name = [width, height] +web = [1200, 630] # og:image / Twitter summary_large_image +github = [1280, 640] # GitHub repo + org social preview (2:1) + # ────────────────────────────────────────────────────────────────────── # Variants (which color treatments to generate for which assets) # ────────────────────────────────────────────────────────────────────── diff --git a/logos/og/audiophore-og-1200x630.png b/logos/og/audiophore-og-1200x630.png new file mode 100644 index 0000000..661d597 Binary files /dev/null and b/logos/og/audiophore-og-1200x630.png differ diff --git a/logos/og/audiophore-og-1280x640.png b/logos/og/audiophore-og-1280x640.png new file mode 100644 index 0000000..f75f2ae Binary files /dev/null and b/logos/og/audiophore-og-1280x640.png differ diff --git a/scripts/build.py b/scripts/build.py index ce0b693..7abfcbe 100644 --- a/scripts/build.py +++ b/scripts/build.py @@ -7,6 +7,7 @@ - logos/png/*.png (all rasters at all configured sizes) - logos/favicon/*.png (favicon sizes) - logos/favicon/favicon.ico (multi-resolution legacy favicon) + - logos/og/*.png (Open Graph / social-share cards) - tokens/brand.css (palette as :root CSS custom properties) - tokens/brand.json (palette as structured JSON) @@ -24,6 +25,7 @@ import argparse import json import os +import re import shutil import subprocess import sys @@ -44,6 +46,7 @@ SVG_DIR = ASSETS_DIR / "svg" PNG_DIR = ASSETS_DIR / "png" FAVICON_DIR = ASSETS_DIR / "favicon" +OG_DIR = ASSETS_DIR / "og" # Non-logo machine-readable exports (palette tokens) live one level up from # logos/ so consumers can grab just the tokens without the whole asset tree. TOKENS_DIR = ROOT / "tokens" @@ -380,6 +383,46 @@ def build_favicons(cfg: dict) -> list[Path]: return written +# ─── Open Graph / social cards ────────────────────────────────────── + +def build_og(cfg: dict) -> list[Path]: + """Render social-share cards: the lockup centered on a full-bleed wash.""" + og = cfg["og"] + bg = color(cfg, og["background"]) + margin_ratio = og["margin_ratio"] + + # Source lockup + its intrinsic dimensions (from its own viewBox). + lockup = lockup_svg(cfg, og["lockup_variant"]) + m = re.search(r'viewBox="0 0 (\d+(?:\.\d+)?) (\d+(?:\.\d+)?)"', lockup) + lk_w, lk_h = float(m.group(1)), float(m.group(2)) + lk_inner = extract_inner(lockup) + + written = [] + for name, (W, H) in og["sizes"].items(): + margin = margin_ratio * min(W, H) + scale = min((W - 2 * margin) / lk_w, (H - 2 * margin) / lk_h) + draw_w, draw_h = lk_w * scale, lk_h * scale + tx, ty = (W - draw_w) / 2, (H - draw_h) / 2 + + svg = ( + f'\n' + f'\n' + f' \n' + f' \n' + f' {lk_inner}\n' + f' \n' + f'\n' + ) + dst = OG_DIR / f"audiophore-og-{W}x{H}.png" + cairosvg.svg2png( + bytestring=svg.encode(), write_to=str(dst), + output_width=W, output_height=H, + ) + written.append(dst) + return written + + # ─── Token export (CSS + JSON) ────────────────────────────────────── def _css_var(prefix: str, key: str) -> str: @@ -437,16 +480,16 @@ def write_tokens(cfg: dict) -> list[Path]: def clean(): """Remove all generated assets.""" - for d in (SVG_DIR, PNG_DIR, FAVICON_DIR, TOKENS_DIR): + for d in (SVG_DIR, PNG_DIR, FAVICON_DIR, OG_DIR, TOKENS_DIR): if d.exists(): shutil.rmtree(d) print(f" removed {d.relative_to(ROOT)}") - for d in (SVG_DIR, PNG_DIR, FAVICON_DIR, TOKENS_DIR): + for d in (SVG_DIR, PNG_DIR, FAVICON_DIR, OG_DIR, TOKENS_DIR): d.mkdir(parents=True, exist_ok=True) def ensure_dirs(): - for d in (SVG_DIR, PNG_DIR, FAVICON_DIR, TOKENS_DIR): + for d in (SVG_DIR, PNG_DIR, FAVICON_DIR, OG_DIR, TOKENS_DIR): d.mkdir(parents=True, exist_ok=True) @@ -454,7 +497,7 @@ def main(): parser = argparse.ArgumentParser(description=__doc__.strip().split("\n")[0]) parser.add_argument( "targets", nargs="*", - choices=["all", "symbol", "wordmark", "lockup", "png", "favicon", "tokens", []], + choices=["all", "symbol", "wordmark", "lockup", "png", "favicon", "og", "tokens", []], default=[], help="Which assets to build (default: all)", ) @@ -470,7 +513,7 @@ def main(): targets = set(args.targets) if args.targets else {"all"} if "all" in targets: - targets = {"symbol", "wordmark", "lockup", "png", "favicon", "tokens"} + targets = {"symbol", "wordmark", "lockup", "png", "favicon", "og", "tokens"} if "symbol" in targets: print("Building symbol SVGs…") @@ -492,6 +535,10 @@ def main(): print("Building favicons…") for p in build_favicons(cfg): print(f" {p.relative_to(ROOT)}") + if "og" in targets: + print("Building OG social cards…") + for p in build_og(cfg): + print(f" {p.relative_to(ROOT)}") if "tokens" in targets: print("Exporting color tokens…") for p in write_tokens(cfg):