Files
BridgeSwarm/scripts/build-branding.py
T
Raven Scott 480f98b595
CI / Build & Test (push) Has been cancelled
Updates
2026-07-27 00:06:23 -04:00

803 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Build the BridgeSwarm branding package (peardock-branding shaped) and sync
runtime copies into the repo (extension icons, assets/brand, assets/logo, …).
Usage:
python3 scripts/build-branding.py
python3 scripts/build-branding.py --sync-only # skip regenerate, only copy
"""
from __future__ import annotations
import argparse
import json
import math
import shutil
import struct
import zlib
from pathlib import Path
from PIL import Image, ImageDraw, ImageFilter, ImageFont
ROOT = Path(__file__).resolve().parents[1]
PKG = ROOT / "bridgeswarm-branding"
ASSETS_BRAND = ROOT / "assets" / "brand"
ASSETS_LOGO = ROOT / "assets" / "logo"
ASSETS_FAV = ROOT / "assets" / "favicons"
EXT_ICONS = ROOT / "extension" / "icons"
# Brand palette (aligned with Control Center / peardock-style tokens)
ACCENT = (45, 212, 191, 255) # #2dd4bf
ACCENT_2 = (56, 189, 248, 255) # #38bdf8
ACCENT_DIM = (20, 184, 166, 255) # #14b8a6
BG_DARK = (12, 12, 16, 255) # #0c0c10
BG_CARD = (26, 26, 32, 255) # #1a1a20
TEXT = (236, 236, 241, 255) # #ececf1
WHITE = (255, 255, 255, 255)
ICON_SIZES = [16, 32, 48, 64, 128, 256, 512, 1024, 2048]
LINUX_SIZES = [16, 32, 48, 64, 128, 256, 512, 1024]
ELECTRON_SIZES = [16, 32, 48, 64, 128, 256, 512, 1024]
FAVICON_PNG = [16, 32, 48]
MAC_ICONSET = {
"icon_16x16.png": 16,
"[email protected]": 32,
"icon_32x32.png": 32,
"[email protected]": 64,
"icon_128x128.png": 128,
"[email protected]": 256,
"icon_256x256.png": 256,
"[email protected]": 512,
"icon_512x512.png": 512,
"[email protected]": 1024,
}
def ensure_dirs() -> None:
for p in [
PKG / "logo",
PKG / "brand-assets",
PKG / "favicons",
PKG / "app-icons" / "electron",
PKG / "app-icons" / "linux",
PKG / "app-icons" / "windows",
PKG / "app-icons" / "macos" / "bridgeswarm.iconset",
PKG / "website",
PKG / "social",
PKG / "docs",
ASSETS_BRAND,
ASSETS_LOGO,
ASSETS_FAV,
EXT_ICONS,
]:
p.mkdir(parents=True, exist_ok=True)
def lerp(a: float, b: float, t: float) -> float:
return a + (b - a) * t
def mix(c1, c2, t: float):
return tuple(int(lerp(c1[i], c2[i], t)) for i in range(3)) + (255,)
def draw_mark(size: int, *, padded: bool = True, mono: bool = False, on_dark: bool = False) -> Image.Image:
"""Draw the BridgeSwarm 3-node bridge mark (matches Control Center logo)."""
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# Geometry in unit space (0..1), matching dashboard SVG composition
# nodes: left (0.28, 0.50), right-upper (0.72, 0.32), right-lower (0.72, 0.68)
nodes = [(0.28, 0.50), (0.72, 0.32), (0.72, 0.68)]
edges = [(0, 1), (0, 2)]
margin = 0.14 if padded else 0.08
scale = 1.0 - 2 * margin
def xy(u, v):
return (margin + u * scale) * size, (margin + v * scale) * size
r_node = size * (0.095 if padded else 0.11)
stroke = max(2, int(size * 0.045))
if on_dark:
# rounded square plate
rad = int(size * 0.22)
plate = Image.new("RGBA", (size, size), (0, 0, 0, 0))
pd = ImageDraw.Draw(plate)
pd.rounded_rectangle([0, 0, size - 1, size - 1], radius=rad, fill=BG_DARK)
img = Image.alpha_composite(img, plate)
draw = ImageDraw.Draw(img)
# soft glow under nodes
if size >= 64 and not mono:
glow = Image.new("RGBA", (size, size), (0, 0, 0, 0))
gd = ImageDraw.Draw(glow)
for u, v in nodes:
x, y = xy(u, v)
gr = r_node * 2.2
gd.ellipse([x - gr, y - gr, x + gr, y + gr], fill=(45, 212, 191, 55))
glow = glow.filter(ImageFilter.GaussianBlur(radius=max(1, size // 48)))
img = Image.alpha_composite(img, glow)
draw = ImageDraw.Draw(img)
line_color = (200, 200, 210, 255) if mono else ACCENT
node_fill = (220, 220, 230, 255) if mono else ACCENT
node_edge = (160, 160, 170, 255) if mono else ACCENT_2
# bridges
for a, b in edges:
x1, y1 = xy(*nodes[a])
x2, y2 = xy(*nodes[b])
draw.line([(x1, y1), (x2, y2)], fill=line_color, width=stroke)
# nodes with slight gradient feel via two ellipses
for i, (u, v) in enumerate(nodes):
x, y = xy(u, v)
fill = node_fill if i == 0 or mono else mix(ACCENT, ACCENT_2, 0.35 + i * 0.15)
draw.ellipse([x - r_node, y - r_node, x + r_node, y + r_node], fill=fill)
# inner highlight
if size >= 48 and not mono:
hr = r_node * 0.35
draw.ellipse(
[x - hr * 0.6, y - r_node * 0.55, x + hr * 1.1, y - r_node * 0.05],
fill=(255, 255, 255, 70),
)
if mono:
draw.ellipse(
[x - r_node, y - r_node, x + r_node, y + r_node],
outline=node_edge,
width=max(1, stroke // 3),
)
return img
def resize_hq(im: Image.Image, size: int) -> Image.Image:
return im.resize((size, size), Image.Resampling.LANCZOS)
def save_png(im: Image.Image, path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
im.save(path, format="PNG", optimize=True)
def write_ico(path: Path, images: list[Image.Image]) -> None:
"""Minimal multi-size ICO writer (PNG-compressed entries)."""
entries = []
payloads = []
for im in images:
buf = __import__("io").BytesIO()
im.save(buf, format="PNG")
data = buf.getvalue()
w, h = im.size
entries.append((w if w < 256 else 0, h if h < 256 else 0, len(data), data))
payloads.append(data)
# ICONDIR + ICONDIRENTRY*n + data
offset = 6 + 16 * len(entries)
out = bytearray()
out += struct.pack("<HHH", 0, 1, len(entries))
for w, h, size, _ in entries:
out += struct.pack("<BBBBHHII", w, h, 0, 0, 1, 32, size, offset)
offset += size
for _, _, _, data in entries:
out += data
path.write_bytes(out)
def make_wordmark_svg(path: Path, *, dark_text: bool) -> None:
fill = "#0c0c10" if dark_text else "#ececf1"
accent = "#2dd4bf"
svg = f'''<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="720" height="160" viewBox="0 0 720 160" fill="none">
<g transform="translate(16,28)">
<circle cx="28" cy="52" r="14" fill="{accent}"/>
<circle cx="84" cy="28" r="14" fill="{accent}"/>
<circle cx="84" cy="76" r="14" fill="{accent}"/>
<path d="M40 48 L72 32" stroke="{accent}" stroke-width="8" stroke-linecap="round"/>
<path d="M40 56 L72 72" stroke="{accent}" stroke-width="8" stroke-linecap="round"/>
</g>
<text x="140" y="102" font-family="Avenir Next, Segoe UI, Helvetica Neue, sans-serif"
font-size="72" font-weight="700" fill="{fill}" letter-spacing="-1.5">Bridge</text>
<text x="370" y="102" font-family="Avenir Next, Segoe UI, Helvetica Neue, sans-serif"
font-size="72" font-weight="700" fill="{accent}" letter-spacing="-1.5">Swarm</text>
</svg>
'''
path.write_text(svg, encoding="utf-8")
def composite_on(color: tuple, mark: Image.Image, size: int) -> Image.Image:
base = Image.new("RGBA", (size, size), color)
m = resize_hq(mark, size)
return Image.alpha_composite(base, m)
def make_banner(mark: Image.Image, size: tuple[int, int], title: str) -> Image.Image:
w, h = size
img = Image.new("RGBA", (w, h), BG_DARK)
draw = ImageDraw.Draw(img)
# gradient-ish bands
for y in range(h):
t = y / max(1, h - 1)
c = mix(BG_DARK, BG_CARD, t * 0.7)
draw.line([(0, y), (w, y)], fill=c)
# accent orbs
orb = Image.new("RGBA", (w, h), (0, 0, 0, 0))
od = ImageDraw.Draw(orb)
od.ellipse([w * 0.55, -h * 0.2, w * 1.1, h * 0.7], fill=(45, 212, 191, 40))
od.ellipse([-w * 0.1, h * 0.4, w * 0.45, h * 1.2], fill=(56, 189, 248, 28))
orb = orb.filter(ImageFilter.GaussianBlur(radius=40))
img = Image.alpha_composite(img, orb)
icon = resize_hq(mark, int(h * 0.55))
ix = int(w * 0.08)
iy = (h - icon.height) // 2
img.paste(icon, (ix, iy), icon)
draw = ImageDraw.Draw(img)
try:
font = ImageFont.truetype("/System/Library/Fonts/Supplemental/Arial Bold.ttf", int(h * 0.22))
font_sm = ImageFont.truetype("/System/Library/Fonts/Supplemental/Arial.ttf", int(h * 0.09))
except Exception:
font = ImageFont.load_default()
font_sm = font
tx = ix + icon.width + int(w * 0.04)
draw.text((tx, h * 0.32), "BridgeSwarm", fill=TEXT, font=font)
draw.text((tx, h * 0.58), title, fill=(139, 139, 154, 255), font=font_sm)
return img
def write_tokens() -> None:
tokens = {
"name": "BridgeSwarm",
"version": "1.0.0",
"description": "P2P Hyperswarm in the browser via native messaging host",
"colors": {
"background": {
"primary": "#0c0c10",
"secondary": "#141418",
"tertiary": "#1a1a20",
"elevated": "#1a1a20",
"hover": "#22222a",
},
"accent": {
"primary": "#2dd4bf",
"secondary": "#14b8a6",
"success": "#34d399",
"warning": "#fbbf24",
"danger": "#fb7185",
"info": "#60a5fa",
},
"text": {
"primary": "#ececf1",
"secondary": "#c8c8d4",
"muted": "#8b8b9a",
"faint": "#5c5c6c",
},
"logo": {
"node": "#2dd4bf",
"nodeAlt": "#38bdf8",
"bridge": "#2dd4bf",
"plate": "#0c0c10",
},
"border": "rgba(255,255,255,0.1)",
},
"typography": {
"fontFamily": "\"Avenir Next\", \"Segoe UI\", \"Helvetica Neue\", sans-serif",
"monoFamily": "\"Cascadia Code\", \"SF Mono\", Consolas, monospace",
"baseSize": "14px",
"lineHeight": 1.5,
},
"radius": {"default": "10px", "small": "7px"},
"logoUsage": {
"minSize": "16px",
"clearSpace": "0.5x logo height",
"preferredBg": "dark #0c0c10 or pure white",
"source": "logo/bridgeswarm-icon-master.png — resize with LANCZOS only",
},
}
(PKG / "brand-assets" / "brand-tokens.json").write_text(json.dumps(tokens, indent=2) + "\n", encoding="utf-8")
css = """/* BridgeSwarm Brand Color Tokens
Source of truth — synced into assets/brand/colors.css
Keep in sync with extension/panel.css Control Center
*/
:root {
--bs-bg-primary: #0c0c10;
--bs-bg-secondary: #141418;
--bs-bg-tertiary: #1a1a20;
--bs-bg-elevated: #1a1a20;
--bs-bg-hover: #22222a;
--bs-accent-primary: #2dd4bf;
--bs-accent-secondary: #14b8a6;
--bs-accent-success: #34d399;
--bs-accent-warning: #fbbf24;
--bs-accent-danger: #fb7185;
--bs-accent-info: #60a5fa;
--bs-text-primary: #ececf1;
--bs-text-secondary: #c8c8d4;
--bs-text-muted: #8b8b9a;
--bs-text-faint: #5c5c6c;
--bs-border: rgba(255, 255, 255, 0.1);
--bs-border-strong: #3a3a46;
--bs-logo-node: #2dd4bf;
--bs-logo-node-alt: #38bdf8;
--bs-logo-bridge: #2dd4bf;
--bs-logo-plate: #0c0c10;
--bs-radius: 10px;
--bs-radius-sm: 7px;
--bs-font: "Avenir Next", "Segoe UI", "Helvetica Neue", sans-serif;
--bs-mono: "Cascadia Code", "SF Mono", Consolas, monospace;
}
"""
(PKG / "brand-assets" / "colors.css").write_text(css, encoding="utf-8")
def write_manifests() -> None:
(PKG / "favicons" / "site.webmanifest").write_text(
json.dumps(
{
"name": "BridgeSwarm",
"short_name": "BridgeSwarm",
"description": "P2P Hyperswarm in the browser via native messaging host",
"icons": [
{"src": "android-chrome-192x192.png", "sizes": "192x192", "type": "image/png"},
{"src": "android-chrome-512x512.png", "sizes": "512x512", "type": "image/png"},
],
"theme_color": "#2dd4bf",
"background_color": "#0c0c10",
"display": "standalone",
},
indent=2,
)
+ "\n",
encoding="utf-8",
)
(PKG / "favicons" / "browserconfig.xml").write_text(
"""<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="mstile-150x150.png"/>
<TileColor>#0c0c10</TileColor>
</tile>
</msapplication>
</browserconfig>
""",
encoding="utf-8",
)
def write_readme() -> None:
(PKG / "README.md").write_text(
"""# BridgeSwarm Branding Package v1.0.0
**Master brand package** (layout mirrors PearDocks `peardock-branding`).
Clean geometric mark: three peer nodes bridged into a swarm — teal `#2dd4bf` on transparent alpha.
Source of truth for icons, favicons, social, website, and CSS tokens.
---
## Critical Rule
**Regenerate sizes only from `logo/bridgeswarm-icon-master.png`.**
Use high-quality LANCZOS / Mitchell resizes. Do not recolor-key or re-remove backgrounds.
Rebuild everything:
```bash
python3 scripts/build-branding.py
```
---
## Structure
```
logo/
bridgeswarm-icon-master.png # Clean square icon (transparent)
bridgeswarm-icon-master-padded.png
bridgeswarm-icon-{16..2048}.png
bridgeswarm-icon-mono.png
bridgeswarm-icon-teal.png
bridgeswarm-icon-dark.png / -light.png
bridgeswarm-logo-full-transparent.png
bridgeswarm-logo-full-1200.png / -800.png
bridgeswarm-logo-dark.png / -light.png
bridgeswarm-wordmark.svg + -dark.svg
favicons/ # ico + PWA sizes + webmanifest + browserconfig
app-icons/ # electron/ linux/ windows/ macos.iconset/ store icons
website/ # og-image, twitter-card, header-banner, hero-bgs, logos
social/ # avatars + circular + banner
brand-assets/ # colors.css + brand-tokens.json
docs/ # BridgeSwarm-Brand-Guidelines.md
```
## Color Tokens
| Token | Hex | Role |
|-------|-----|------|
| `--bs-bg-primary` | `#0c0c10` | Main bg |
| `--bs-accent-primary` | `#2dd4bf` | Teal CTA / nodes |
| `--bs-accent-info` | `#60a5fa` | Secondary accent |
| `--bs-text-primary` | `#ececf1` | Body text |
Full list in `brand-assets/colors.css` and `brand-tokens.json`.
## Drop-in Usage
This package is the **source of truth**. Runtime copies live in the app tree:
| Destination | Contents |
|-------------|----------|
| `../extension/icons/{16,48,128}.png` | Chrome/Firefox extension icons |
| `../assets/logo/` | README + titlebar logos |
| `../assets/favicons/` | Favicons + webmanifest |
| `../assets/brand/` | colors.css + brand-tokens.json |
```html
<link rel="icon" href="assets/favicons/favicon.ico" sizes="any">
<link rel="apple-touch-icon" href="assets/favicons/apple-touch-icon.png">
<link rel="manifest" href="assets/favicons/site.webmanifest">
<meta name="theme-color" content="#2dd4bf">
```
Programmatic resolve:
```js
const { resolveAssets, COLORS } = require('./bridgeswarm-branding')
const paths = resolveAssets()
// paths.logo.master, paths.favicon[32], …
```
macOS `.icns` (on a Mac):
```bash
cd app-icons/macos
iconutil -c icns bridgeswarm.iconset -o ../../build/icon.icns
```
---
© 2026 snxraven · MIT
Repo: https://git.ssh.surf/snxraven/BridgeSwarm
""",
encoding="utf-8",
)
(PKG / "docs" / "BridgeSwarm-Brand-Guidelines.md").write_text(
"""# BridgeSwarm Brand Guidelines
## Mark
The BridgeSwarm mark is **three peer nodes** linked by two bridges — a compact diagram of Hyperswarm discovery and direct Noise connections. It is not a letterform monogram.
- **Primary color:** teal `#2dd4bf`
- **Secondary node tint:** sky `#38bdf8`
- **Plate (app icon):** near-black `#0c0c10`
## Clear space
Keep clear space of at least **½ the mark height** on all sides. Do not place competing UI chrome inside that margin.
## Minimum size
- UI / toolbar: **16×16** (extension)
- Marketing: prefer **128×128** or larger
- Wordmark: do not scale below ~120px wide
## Backgrounds
| Background | Use |
|------------|-----|
| `#0c0c10` / dark UI | Primary — transparent mark or dark composite |
| Pure white | Light composite (`bridgeswarm-icon-light.png`) or dark wordmark |
| Photography | Prefer mark-on-dark plate |
## Dont
- Recolor nodes to purple gradients or warm cream “AI default” palettes
- Add glow stacks, emoji, or 3D skeuomorphism
- Stretch or rotate the mark
- Replace the mark with the Hyperswarm logo
## Voice (short)
BridgeSwarm brings the Holepunch P2P stack into normal browsers via a Bare native host. Brand language should stay technical, direct, and desktop-native — not “Web3 buzzword” marketing.
## Tokens
See `brand-assets/brand-tokens.json` and `colors.css`.
""",
encoding="utf-8",
)
def write_package_js() -> None:
(PKG / "package.json").write_text(
json.dumps(
{
"name": "bridgeswarm-branding",
"version": "1.0.0",
"main": "index.js",
"type": "commonjs",
"description": "Official BridgeSwarm logos, favicons, and brand tokens (static assets)",
"license": "MIT",
},
indent=2,
)
+ "\n",
encoding="utf-8",
)
(PKG / "index.js").write_text(
"""'use strict';
const path = require('path');
const ROOT = __dirname;
const BRAND_VERSION = '1.0.0';
const COLORS = {
accentPrimary: '#2dd4bf',
accentSecondary: '#14b8a6',
accentInfo: '#60a5fa',
bgPrimary: '#0c0c10',
textPrimary: '#ececf1',
};
/** CSS custom properties for Control Center / docs (keep in sync with colors.css). */
const BRIDGESWARM_THEME_TOKENS = {
'--bs-bg-primary': COLORS.bgPrimary,
'--bs-accent-primary': COLORS.accentPrimary,
'--bs-accent-secondary': COLORS.accentSecondary,
'--bs-accent-info': COLORS.accentInfo,
'--bs-text-primary': COLORS.textPrimary,
'--bs-border': 'rgba(255,255,255,0.1)',
};
const ASSETS = {
logo: {
master: 'logo/bridgeswarm-icon-master.png',
masterPadded: 'logo/bridgeswarm-icon-master-padded.png',
icon512: 'logo/bridgeswarm-icon-512.png',
icon128: 'logo/bridgeswarm-icon-128.png',
dark: 'logo/bridgeswarm-icon-dark.png',
light: 'logo/bridgeswarm-icon-light.png',
mono: 'logo/bridgeswarm-icon-mono.png',
teal: 'logo/bridgeswarm-icon-teal.png',
full1200: 'logo/bridgeswarm-logo-full-1200.png',
full800: 'logo/bridgeswarm-logo-full-800.png',
wordmark: 'logo/bridgeswarm-wordmark.svg',
wordmarkDark: 'logo/bridgeswarm-wordmark-dark.svg',
},
favicon: {
16: 'favicons/favicon-16x16.png',
32: 'favicons/favicon-32x32.png',
48: 'favicons/favicon-48x48.png',
ico: 'favicons/favicon.ico',
apple: 'favicons/apple-touch-icon.png',
192: 'favicons/android-chrome-192x192.png',
512: 'favicons/android-chrome-512x512.png',
},
brand: {
tokens: 'brand-assets/brand-tokens.json',
colors: 'brand-assets/colors.css',
},
guidelines: 'docs/BridgeSwarm-Brand-Guidelines.md',
};
function assetPath(relativePath) {
return path.join(ROOT, String(relativePath || ''));
}
function resolveAssets() {
const out = { root: ROOT, version: BRAND_VERSION, colors: { ...COLORS } };
for (const [group, entries] of Object.entries(ASSETS)) {
if (typeof entries === 'string') {
out[group] = assetPath(entries);
continue;
}
out[group] = {};
for (const [key, rel] of Object.entries(entries)) {
out[group][key] = assetPath(rel);
}
}
return out;
}
module.exports = {
ROOT,
BRAND_VERSION,
COLORS,
BRIDGESWARM_THEME_TOKENS,
ASSETS,
assetPath,
resolveAssets,
};
""",
encoding="utf-8",
)
def build_package() -> Image.Image:
ensure_dirs()
master = draw_mark(2048, padded=True)
master_tight = draw_mark(2048, padded=False)
save_png(master, PKG / "logo" / "bridgeswarm-icon-master.png")
save_png(master, PKG / "logo" / "bridgeswarm-icon-master-padded.png")
save_png(master_tight, PKG / "logo" / "source-original.png")
for s in ICON_SIZES:
save_png(resize_hq(master, s), PKG / "logo" / f"bridgeswarm-icon-{s}.png")
save_png(draw_mark(1024, padded=True, mono=True), PKG / "logo" / "bridgeswarm-icon-mono.png")
# teal = mark on transparent (same as master) — alias for peardock parity
save_png(resize_hq(master, 1024), PKG / "logo" / "bridgeswarm-icon-teal.png")
save_png(composite_on(BG_DARK, master, 1024), PKG / "logo" / "bridgeswarm-icon-dark.png")
save_png(composite_on(WHITE, master, 1024), PKG / "logo" / "bridgeswarm-icon-light.png")
# Full logo = mark + wordmark baked as wide PNG
for w, name in [(1200, "bridgeswarm-logo-full-1200.png"), (800, "bridgeswarm-logo-full-800.png")]:
h = int(w * 0.28)
banner = make_banner(master, (w, max(h, 180)), "P2P Hyperswarm for the browser")
save_png(banner, PKG / "logo" / name)
save_png(
make_banner(master, (1200, 320), "P2P Hyperswarm for the browser"),
PKG / "logo" / "bridgeswarm-logo-full-transparent.png",
)
save_png(composite_on(BG_DARK, master, 512), PKG / "logo" / "bridgeswarm-logo-dark.png")
save_png(composite_on(WHITE, master, 512), PKG / "logo" / "bridgeswarm-logo-light.png")
make_wordmark_svg(PKG / "logo" / "bridgeswarm-wordmark.svg", dark_text=False)
make_wordmark_svg(PKG / "logo" / "bridgeswarm-wordmark-dark.svg", dark_text=True)
# Favicons
for s in FAVICON_PNG:
save_png(resize_hq(master, s), PKG / "favicons" / f"favicon-{s}x{s}.png")
save_png(resize_hq(master, 180), PKG / "favicons" / "apple-touch-icon.png")
save_png(resize_hq(master, 192), PKG / "favicons" / "android-chrome-192x192.png")
save_png(resize_hq(master, 512), PKG / "favicons" / "android-chrome-512x512.png")
save_png(resize_hq(master, 144), PKG / "favicons" / "mstile-144x144.png")
save_png(resize_hq(master, 150), PKG / "favicons" / "mstile-150x150.png")
write_ico(
PKG / "favicons" / "favicon.ico",
[resize_hq(master, s) for s in (16, 32, 48)],
)
# App icons
for s in ELECTRON_SIZES:
save_png(resize_hq(master, s), PKG / "app-icons" / "electron" / f"icon-{s}.png")
for s in LINUX_SIZES:
save_png(resize_hq(master, s), PKG / "app-icons" / "linux" / f"icon-{s}.png")
for name, s in MAC_ICONSET.items():
save_png(resize_hq(master, s), PKG / "app-icons" / "macos" / "bridgeswarm.iconset" / name)
write_ico(
PKG / "app-icons" / "windows" / "bridgeswarm.ico",
[resize_hq(master, s) for s in (16, 32, 48, 64, 128, 256)],
)
save_png(resize_hq(master, 1024), PKG / "app-icons" / "app-store-1024.png")
save_png(resize_hq(master, 512), PKG / "app-icons" / "play-store-512.png")
# Website
save_png(make_banner(master, (1200, 630), "P2P Hyperswarm for the modern browser"), PKG / "website" / "og-image.png")
save_png(make_banner(master, (1200, 600), "Native messaging · Bare · Hyperswarm"), PKG / "website" / "twitter-card.png")
save_png(make_banner(master, (1500, 500), "Control Center · Examples · Capabilities"), PKG / "website" / "header-banner.png")
save_png(make_banner(master, (1280, 720), ""), PKG / "website" / "hero-bg.png")
save_png(make_banner(master, (1280, 720), ""), PKG / "website" / "hero-bg-1280.png")
save_png(resize_hq(master, 256), PKG / "website" / "logo-header.png")
# Social
save_png(resize_hq(composite_on(BG_DARK, master, 800), 800), PKG / "social" / "avatar-800.png")
save_png(resize_hq(composite_on(BG_DARK, master, 400), 400), PKG / "social" / "avatar-400.png")
# circular avatar
circ = composite_on(BG_DARK, master, 512)
mask = Image.new("L", (512, 512), 0)
ImageDraw.Draw(mask).ellipse([0, 0, 511, 511], fill=255)
out = Image.new("RGBA", (512, 512), (0, 0, 0, 0))
out.paste(circ, (0, 0))
out.putalpha(mask)
save_png(out, PKG / "social" / "avatar-circle-dark.png")
save_png(make_banner(master, (1500, 500), "BridgeSwarm"), PKG / "social" / "social-banner.png")
write_tokens()
write_manifests()
write_readme()
write_package_js()
return master
def sync_runtime(master: Image.Image | None = None) -> None:
ensure_dirs()
if master is None:
master_path = PKG / "logo" / "bridgeswarm-icon-master.png"
if not master_path.exists():
raise SystemExit("Missing master icon — run without --sync-only first")
master = Image.open(master_path).convert("RGBA")
# Extension icons (Chrome/Firefox)
for s in (16, 48, 128):
save_png(resize_hq(master, s), EXT_ICONS / f"{s}.png")
# Runtime brand + logo + favicons
shutil.copy2(PKG / "brand-assets" / "colors.css", ASSETS_BRAND / "colors.css")
shutil.copy2(PKG / "brand-assets" / "brand-tokens.json", ASSETS_BRAND / "brand-tokens.json")
for name in (
"bridgeswarm-icon-512.png",
"bridgeswarm-icon-128.png",
"bridgeswarm-icon-dark.png",
"bridgeswarm-logo-full-1200.png",
"bridgeswarm-wordmark.svg",
"bridgeswarm-wordmark-dark.svg",
):
src = PKG / "logo" / name
if src.exists():
shutil.copy2(src, ASSETS_LOGO / name)
for name in (
"favicon.ico",
"favicon-16x16.png",
"favicon-32x32.png",
"favicon-48x48.png",
"apple-touch-icon.png",
"android-chrome-192x192.png",
"android-chrome-512x512.png",
"site.webmanifest",
"browserconfig.xml",
):
src = PKG / "favicons" / name
if src.exists():
shutil.copy2(src, ASSETS_FAV / name)
# Convenience build/ icons for future packagers
build = ROOT / "build"
build.mkdir(exist_ok=True)
# Dark-plate PNG for packagers that want an opaque square
save_png(composite_on(BG_DARK, master, 1024), build / "icon.png")
shutil.copy2(PKG / "app-icons" / "windows" / "bridgeswarm.ico", build / "icon.ico")
# macOS .icns (best-effort; requires macOS iconutil)
iconset = PKG / "app-icons" / "macos" / "bridgeswarm.iconset"
try:
import subprocess
# iconutil is happier with opaque RGB plates
for name, s in MAC_ICONSET.items():
composite_on(BG_DARK, master, s).convert("RGB").save(iconset / name, format="PNG")
icns = build / "icon.icns"
subprocess.run(
["iconutil", "-c", "icns", str(iconset), "-o", str(icns)],
check=True,
capture_output=True,
)
print("Wrote", icns)
except Exception as err:
print("Note: skipped icon.icns (%s)" % err)
print("Synced → extension/icons, assets/brand, assets/logo, assets/favicons, build/")
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--sync-only", action="store_true")
args = ap.parse_args()
if args.sync_only:
sync_runtime()
else:
master = build_package()
sync_runtime(master)
print("Built bridgeswarm-branding/ and synced runtime copies.")
if __name__ == "__main__":
main()