803 lines
27 KiB
Python
803 lines
27 KiB
Python
#!/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 PearDock’s `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 |
|
||
|
||
## Don’t
|
||
|
||
- 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()
|