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()