diff --git a/README.md b/README.md index 301222f..37367c4 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ consistent wherever it appears. | | Source | Style | |---|---|---| -| **Logo** | `source/logo.png` | Flat cartoon avatar — red polo, used for all **profile pictures** | -| **Hero** | `source/hero.png` | Photoreal triple-monitor desk scene — red polo, used for all **banners / covers** | +| **Logo** | `source/logo.svg` | **Vector** cartoon avatar — red polo, scales infinitely; used for all **profile pictures**. (`source/logo.png` is the original raster it was traced from.) | +| **Hero** | `source/hero.png` | Photoreal triple-monitor desk scene — red polo, used for all **banners / covers**. Raster only — photoreal imagery can't meaningfully vectorize. | > The logo (flat illustration) and the hero (photoreal render) are intentionally > kept as separate assets rather than composited together — mixing the two styles @@ -42,8 +42,9 @@ exact required pixel dimensions. ### Logo masters (`logo/`) -`logo-original.png` (640²) plus `logo-256/512/1024.png` and the source JPG. -Use these when a platform isn't listed above or you need a custom size. +`logo.svg` (vector — scale to any size) plus pre-rendered `logo-256/512/1024.png` +and `logo-original.png` (the 640² raster). Use the SVG when a platform isn't +listed above or you need a custom/large size; it never pixelates. ## Palette @@ -57,15 +58,20 @@ Use these when a platform isn't listed above or you need a custom size. ## Regenerating -All derived assets are produced from the two sources in `source/`: +All derived assets are produced from the sources in `source/`: ```bash -python3 tools/build_assets.py # requires Pillow +python3 tools/trace_logo.py # raster logo.png -> vector logo.svg (needs vtracer) +python3 tools/build_assets.py # sources -> every platform asset (needs Pillow; resvg for crisp vector logo) ``` -Edit `source/logo.png` or `source/hero.png` (or the size tables in the script), -re-run, and commit. `source/hero-alt.png` is an alternate desk composition kept -for reference. +`build_assets.py` renders each logo asset straight from `source/logo.svg` at its +exact target size (via `resvg`), so profile pictures and favicons are crisp at +any resolution. If `resvg` is unavailable it falls back to resizing the raster. + +Tooling (one-time): `cargo install vtracer resvg`. Edit `source/logo.svg`/`hero.png` +(or the size tables in the script), re-run, and commit. `source/hero-alt.png` is +an alternate desk composition kept for reference. ## Usage & rights diff --git a/avatars/bluesky-400.png b/avatars/bluesky-400.png index 5a50412..5953a0d 100644 Binary files a/avatars/bluesky-400.png and b/avatars/bluesky-400.png differ diff --git a/avatars/discord-512.png b/avatars/discord-512.png index 7464bf8..c46975d 100644 Binary files a/avatars/discord-512.png and b/avatars/discord-512.png differ diff --git a/avatars/facebook-320.png b/avatars/facebook-320.png index 79e9992..2b1347e 100644 Binary files a/avatars/facebook-320.png and b/avatars/facebook-320.png differ diff --git a/avatars/github-460.png b/avatars/github-460.png index 12475fe..93eb13b 100644 Binary files a/avatars/github-460.png and b/avatars/github-460.png differ diff --git a/avatars/instagram-320.png b/avatars/instagram-320.png index 79e9992..2b1347e 100644 Binary files a/avatars/instagram-320.png and b/avatars/instagram-320.png differ diff --git a/avatars/linkedin-400.png b/avatars/linkedin-400.png index 5a50412..5953a0d 100644 Binary files a/avatars/linkedin-400.png and b/avatars/linkedin-400.png differ diff --git a/avatars/mastodon-400.png b/avatars/mastodon-400.png index 5a50412..5953a0d 100644 Binary files a/avatars/mastodon-400.png and b/avatars/mastodon-400.png differ diff --git a/avatars/threads-320.png b/avatars/threads-320.png index 79e9992..2b1347e 100644 Binary files a/avatars/threads-320.png and b/avatars/threads-320.png differ diff --git a/avatars/tiktok-200.png b/avatars/tiktok-200.png index 431ce5b..db976a0 100644 Binary files a/avatars/tiktok-200.png and b/avatars/tiktok-200.png differ diff --git a/avatars/x-400.png b/avatars/x-400.png index 5a50412..5953a0d 100644 Binary files a/avatars/x-400.png and b/avatars/x-400.png differ diff --git a/avatars/youtube-800.png b/avatars/youtube-800.png index d606853..7f3042f 100644 Binary files a/avatars/youtube-800.png and b/avatars/youtube-800.png differ diff --git a/favicon/apple-touch-icon-180.png b/favicon/apple-touch-icon-180.png index c45e69f..c0c570f 100644 Binary files a/favicon/apple-touch-icon-180.png and b/favicon/apple-touch-icon-180.png differ diff --git a/favicon/favicon-16.png b/favicon/favicon-16.png index 02b4c59..39c63b6 100644 Binary files a/favicon/favicon-16.png and b/favicon/favicon-16.png differ diff --git a/favicon/favicon-192.png b/favicon/favicon-192.png index cb4d7d3..a47bda5 100644 Binary files a/favicon/favicon-192.png and b/favicon/favicon-192.png differ diff --git a/favicon/favicon-32.png b/favicon/favicon-32.png index bbbebdc..f01de57 100644 Binary files a/favicon/favicon-32.png and b/favicon/favicon-32.png differ diff --git a/favicon/favicon-48.png b/favicon/favicon-48.png index 9ef4a6a..51269d2 100644 Binary files a/favicon/favicon-48.png and b/favicon/favicon-48.png differ diff --git a/favicon/favicon-512.png b/favicon/favicon-512.png index 7464bf8..c46975d 100644 Binary files a/favicon/favicon-512.png and b/favicon/favicon-512.png differ diff --git a/favicon/favicon.ico b/favicon/favicon.ico index b6fffc2..6b84b2b 100644 Binary files a/favicon/favicon.ico and b/favicon/favicon.ico differ diff --git a/logo/logo-1024.png b/logo/logo-1024.png index e4f5c19..ccee7ba 100644 Binary files a/logo/logo-1024.png and b/logo/logo-1024.png differ diff --git a/logo/logo-256.png b/logo/logo-256.png index edb943b..b8c446e 100644 Binary files a/logo/logo-256.png and b/logo/logo-256.png differ diff --git a/logo/logo-512.png b/logo/logo-512.png index 7464bf8..c46975d 100644 Binary files a/logo/logo-512.png and b/logo/logo-512.png differ diff --git a/logo/logo.svg b/logo/logo.svg new file mode 100644 index 0000000..b197f3c --- /dev/null +++ b/logo/logo.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/source/logo.svg b/source/logo.svg new file mode 100644 index 0000000..b197f3c --- /dev/null +++ b/source/logo.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/tools/build_assets.py b/tools/build_assets.py index fbe2d24..2215407 100644 --- a/tools/build_assets.py +++ b/tools/build_assets.py @@ -1,22 +1,30 @@ #!/usr/bin/env python3 -"""Regenerate every platform asset in the brand kit from the two canonical sources. +"""Regenerate every platform asset in the brand kit from the canonical sources. Sources: - source/logo.png -- flat cartoon avatar (red polo), used for all profile pictures - source/hero.png -- photoreal desk scene (red polo), used for all banners/covers + source/logo.svg -- vector cartoon avatar (red polo); rendered crisply at each + target size. Falls back to source/logo.png if resvg is absent. + source/hero.png -- photoreal desk scene (red polo), used for all banners/covers. + +Profile pictures + favicons come from the logo; banners/covers from the hero. + +Requires Pillow, and (for crisp vector logo output) the `resvg` binary +(cargo install resvg). Regenerate the SVG itself with tools/trace_logo.py. Run: python3 tools/build_assets.py """ -from PIL import Image +import shutil +import subprocess +import tempfile from pathlib import Path +from PIL import Image ROOT = Path(__file__).resolve().parent.parent SRC = ROOT / "source" -LOGO = Image.open(SRC / "logo.png").convert("RGB") +LOGO_SVG = SRC / "logo.svg" +LOGO_PNG = SRC / "logo.png" HERO = Image.open(SRC / "hero.png").convert("RGB") - -# Brand background (sampled from the logo's cream field) -- used to pad where needed. -CREAM = (242, 232, 213) +HAVE_RESVG = shutil.which("resvg") is not None and LOGO_SVG.exists() def save(img, relpath): @@ -26,9 +34,17 @@ def save(img, relpath): print(f" {relpath}: {img.size[0]}x{img.size[1]}") -def square(img, size): - """Square profile asset from the (already square) logo.""" - return img.resize((size, size), Image.LANCZOS) +def logo_at(size): + """Render the vector logo crisply at size x size (raster fallback).""" + if HAVE_RESVG: + with tempfile.NamedTemporaryFile(suffix=".png") as tf: + subprocess.run( + ["resvg", "--width", str(size), "--height", str(size), + str(LOGO_SVG), tf.name], + check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + ) + return Image.open(tf.name).convert("RGB") + return Image.open(LOGO_PNG).convert("RGB").resize((size, size), Image.LANCZOS) def cover(img, tw, th, fy=0.45, fx=0.5): @@ -89,24 +105,28 @@ FAVICON_SIZES = { def main(): + print(f"logo source: {'vector (resvg)' if HAVE_RESVG else 'raster fallback'}") + print("avatars:") for path, size in AVATARS.items(): - save(square(LOGO, size), path) + save(logo_at(size), path) print("logo masters:") for path, size in LOGO_SIZES.items(): - save(square(LOGO, size), path) - save(LOGO, "logo/logo-original.png") + save(logo_at(size), path) + if LOGO_SVG.exists(): + shutil.copy(LOGO_SVG, ROOT / "logo/logo.svg") + print(" logo/logo.svg: vector") + save(Image.open(LOGO_PNG).convert("RGB"), "logo/logo-original.png") print("favicons:") for path, size in FAVICON_SIZES.items(): - save(square(LOGO, size), path) - # multi-resolution .ico + save(logo_at(size), path) ico = ROOT / "favicon/favicon.ico" - LOGO.resize((256, 256), Image.LANCZOS).save( + logo_at(256).save( ico, sizes=[(16, 16), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)] ) - print(f" favicon/favicon.ico: multi-res") + print(" favicon/favicon.ico: multi-res") print("banners:") for path, (w, h, fy) in BANNERS.items(): diff --git a/tools/trace_logo.py b/tools/trace_logo.py new file mode 100644 index 0000000..bdec92c --- /dev/null +++ b/tools/trace_logo.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +"""Vectorize the raster logo into source/logo.svg (true infinite-scale vector). + +The logo is flat cartoon line-art (~4 colors), so we: + 1. Snap every pixel to the exact canonical brand palette (no color drift, + and anti-aliasing fringes collapse into clean regions), then + 2. Trace the flat image to colored SVG paths with vtracer. + +Requires the `vtracer` binary (cargo install vtracer). +Run: python3 tools/trace_logo.py +""" +import subprocess +import tempfile +from pathlib import Path +from PIL import Image + +ROOT = Path(__file__).resolve().parent.parent +SRC = ROOT / "source" + +# Exact canonical brand palette — see README. +PALETTE = [ + 0xEF, 0xDE, 0xCC, # canvas cream (background) + 0xDC, 0x31, 0x27, # polo red + 0xF5, 0xB0, 0x79, # skin tan + 0x0A, 0x0A, 0x08, # ink (outlines + hair) +] + + +def main(): + im = Image.open(SRC / "logo.png").convert("RGB") + pal = Image.new("P", (1, 1)) + # Pad to 256 entries by repeating ink, so stray pixels never snap to a + # color outside the palette (e.g. pure black). + pal.putpalette(PALETTE + PALETTE[9:12] * (256 - 4)) + flat = im.quantize(palette=pal, dither=Image.NONE).convert("RGB") + + with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tf: + flat.save(tf.name) + subprocess.run( + ["vtracer", "--input", tf.name, "--output", str(SRC / "logo.svg"), + "--colormode", "color", "--mode", "spline", + "--filter_speckle", "8", "--color_precision", "8", + "--corner_threshold", "60", "--segment_length", "4", + "--splice_threshold", "45"], + check=True, + ) + print("wrote", SRC / "logo.svg") + + +if __name__ == "__main__": + main()