#!/usr/bin/env python3
"""Generate the InternetPressure.io brand assets from one geometric definition.
The mark is a *pressure dial*: a 240° ring cut into the seven pressure bands (calm → extreme, the palette used across
the site), a needle and a hub. It works in colour on dark, and in a single colour for tiny sizes.
Outputs (run from the repo root, needs rsvg-convert + Pillow):
apps/web/public/brand/mark.svg the dial, 64×64, transparent
apps/web/public/brand/mark-mono.svg single-colour variant (currentColor)
apps/web/public/logo.svg dial + wordmark, 360×64
apps/web/src/app/icon.svg favicon (dial on a dark rounded tile)
apps/web/src/app/icon.png 512×512
apps/web/src/app/apple-icon.png 180×180
apps/web/src/app/favicon.ico 16/32/48
apps/web/public/brand/og-wallpaper.png 1200×630 static share image (used when the live one is unavailable)
apps/web/src/components/chrome/Logo.tsx React component with the same geometry
"""
from __future__ import annotations
import math
import pathlib
import subprocess
ROOT = pathlib.Path(__file__).resolve().parents[1]
WEB = ROOT / "apps/web"
PALETTE = ["#4CC9F0", "#7FB77E", "#E9C46A", "#F4A261", "#E76F51", "#D62828", "#F72585"]
INK = "#E6EDF3"
INK2 = "#8B98A5"
BG = "#070A0F"
TILE = "#0C1117"
ACCENT = "#5B8DEF"
CX = CY = 32.0
R = 24.0 # ring radius
W = 6.5 # ring stroke width
START, SWEEP = 150.0, 240.0 # gauge from 150° (bottom-left) clockwise 240° to 30° (bottom-right)
GAP = 3.0 # degrees between bands
NEEDLE_DEG = START + SWEEP * 0.43 # needle at ~ "elevated" — the state the gauge is built to show
NEEDLE_LEN = 17.0
def pt(deg: float, r: float) -> tuple[float, float]:
a = math.radians(deg)
return CX + r * math.cos(a), CY + r * math.sin(a)
def arc(d0: float, d1: float, r: float) -> str:
x0, y0 = pt(d0, r)
x1, y1 = pt(d1, r)
large = 1 if (d1 - d0) > 180 else 0
return f"M{x0:.2f} {y0:.2f} A{r} {r} 0 {large} 1 {x1:.2f} {y1:.2f}"
def bands() -> list[tuple[str, str]]:
n = len(PALETTE)
span = (SWEEP - GAP * (n - 1)) / n
out = []
for i, col in enumerate(PALETTE):
d0 = START + i * (span + GAP)
out.append((arc(d0, d0 + span, R), col))
return out
def needle() -> tuple[tuple[float, float], tuple[float, float]]:
return (CX, CY), pt(NEEDLE_DEG, NEEDLE_LEN)
def dial_svg_inner(*, mono: str | None = None, needle_color: str = INK, hub: str = INK) -> str:
parts = []
for d, col in bands():
parts.append(f'')
(x0, y0), (x1, y1) = needle()
# needle: a tapered line — drawn as a polygon so it stays crisp at 16 px
a = math.radians(NEEDLE_DEG)
px, py = -math.sin(a) * 2.2, math.cos(a) * 2.2
parts.append(f'')
parts.append(f'')
parts.append(f'')
return "\n ".join(parts)
def mark_svg(mono: bool = False) -> str:
inner = dial_svg_inner(mono="currentColor" if mono else None)
return f'\n'
def icon_svg() -> str:
inner = dial_svg_inner()
return (f'\n')
def logo_svg() -> str:
inner = dial_svg_inner()
return (f'\n')
def og_svg() -> str:
"""Static wallpaper: a big dial ghosted on the right, the wordmark and tagline on the left."""
scale = 8.5
inner = dial_svg_inner()
return f'''
'''
def logo_tsx() -> str:
paths = "\n".join(f' ' for d, col in bands())
(x0, y0), (x1, y1) = needle()
a = math.radians(NEEDLE_DEG)
px, py = -math.sin(a) * 2.2, math.cos(a) * 2.2
return f'''/** InternetPressure.io mark — the pressure dial (generated by scripts/brand.py; same geometry as public/brand/mark.svg).
* Seven bands = the pressure palette calm → extreme, a needle and a hub. `mono` renders everything in `color`. */
export function Logo({{ size = 22, className, color = 'currentColor', mono = false }}: {{ size?: number; className?: string; color?: string; mono?: boolean }}) {{
return (
);
}}
'''
def main() -> None:
brand = WEB / "public/brand"
brand.mkdir(parents=True, exist_ok=True)
(brand / "mark.svg").write_text(mark_svg())
(brand / "mark-mono.svg").write_text(mark_svg(mono=True))
(WEB / "public/logo.svg").write_text(logo_svg())
(WEB / "src/app/icon.svg").write_text(icon_svg())
(WEB / "src/components/chrome/Logo.tsx").write_text(logo_tsx())
og = brand / "og-wallpaper.svg"
og.write_text(og_svg())
def raster(src: pathlib.Path, dst: pathlib.Path, w: int, h: int | None = None) -> None:
subprocess.run(["rsvg-convert", "-w", str(w), "-h", str(h or w), str(src), "-o", str(dst)], check=True)
raster(WEB / "src/app/icon.svg", WEB / "src/app/icon.png", 512)
raster(WEB / "src/app/icon.svg", WEB / "src/app/apple-icon.png", 180)
raster(og, brand / "og-wallpaper.png", 1200, 630)
raster(brand / "mark.svg", brand / "mark-256.png", 256)
from PIL import Image
base = Image.open(WEB / "src/app/icon.png").convert("RGBA")
base.save(WEB / "src/app/favicon.ico", sizes=[(16, 16), (32, 32), (48, 48)])
og.unlink() # keep only the PNG wallpaper (the SVG relies on system fonts)
print("brand assets written")
if __name__ == "__main__":
main()