Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions architecture/brand-marks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Brand marks

The org's logo assets, generated by `brand/build/` (no frontmatter; living prose).

## Org marks (`brand/org/`)
Favicon, avatar, social cards — the interlocked-snakes pinwheel with a chevron.
Used everywhere small (favicons, avatars). See `site-branding.md` for site wiring.

## Per-project marks (`brand/projects/<repo>/`)
One large-format logo per repo: the constant green+gold snake-frame
(`geometry.py::project_frame`, margin 9 / arm 53 / stroke 11) with a single
gold inner symbol (`symbols.py`) chosen per repo in `projects.py::MANIFEST`.
Two-colour (green + gold); repos differ by symbol shape, not colour. The two
project templates reuse the org chevron. `modern-di-faststream` is the only
mark using a partner's literal logo path (FastStream's, recoloured); other
integration cues are redrawn evocations. Outputs: `mark.svg`, `lockup.svg`
(+ `mark-512/1024.png`). Regenerate via `uv run python -m brand.build.render`.
11 changes: 9 additions & 2 deletions brand/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,14 @@ lockup** pulls them into crop marks framing `MODERN` / `PYTHON` set in **Jost**
| `social-card-green.svg` / `.png` | 1280×640, green alternate |
| `social-square.svg` / `.png`, `social-square-green.*` | 640×640 (Telegram) |

## Per-project marks (`brand/projects/`)

Each repo gets a large-format mark: the constant green+gold snake-frame with
one gold inner symbol (see `brand/build/projects.py::MANIFEST`). Regenerate
with `uv run python -m brand.build.render`; outputs land in
`brand/projects/<repo>/` as `mark.svg`, `lockup.svg` (+ PNGs). These are
large-format only — every repo's favicon/avatar stays the org mark.

## Deferred (not in this kit)

Per-project / per-repo marks, the subfamily system, and any inner glyphs are a
later task. The header nav logo redesign is also a follow-up.
The header nav logo redesign is a follow-up.
71 changes: 58 additions & 13 deletions brand/build/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,40 @@ def _icon_mark(struct: str, gold: str) -> str:

def icon(*, bg: str, struct: str, gold: str) -> str:
"""Full-bleed square icon — favicon, apple-touch, GitHub avatar."""
return (_SVG_OPEN.format(w=100, h=100)
+ f'<rect width="100" height="100" fill="{bg}"/>'
+ _icon_mark(struct, gold) + "</svg>")
return (
_SVG_OPEN.format(w=100, h=100)
+ f'<rect width="100" height="100" fill="{bg}"/>'
+ _icon_mark(struct, gold)
+ "</svg>"
)


def icon_circle(*, bg: str, struct: str, gold: str, scale: float = 0.74) -> str:
"""Padded variant centered for circular crops (e.g. Telegram). The mark is
scaled about the center so it fits inside the inscribed circle with margin."""
return (_SVG_OPEN.format(w=100, h=100)
+ f'<rect width="100" height="100" fill="{bg}"/>'
+ f'<g transform="translate(50,50) scale({scale}) translate(-50,-50)">{_icon_mark(struct, gold)}</g>'
+ "</svg>")
return (
_SVG_OPEN.format(w=100, h=100)
+ f'<rect width="100" height="100" fill="{bg}"/>'
+ f'<g transform="translate(50,50) scale({scale}) translate(-50,-50)">{_icon_mark(struct, gold)}</g>'
+ "</svg>"
)


def lockup_body(*, struct: str, gold: str) -> str:
"""The MODERN/PYTHON crop-mark lockup, drawn in a 540x250 coordinate space.
Returned as bare markup (no <svg> wrapper, no background) for embedding."""
modern, _ = outline_text("MODERN", 50, x=270, baseline_y=126, anchor="middle",
color=struct, fit_width=210)
python, _ = outline_text("PYTHON", 50, x=270, baseline_y=166, anchor="middle",
color=gold, fit_width=210)
modern, _ = outline_text(
"MODERN",
50,
x=270,
baseline_y=126,
anchor="middle",
color=struct,
fit_width=210,
)
python, _ = outline_text(
"PYTHON", 50, x=270, baseline_y=166, anchor="middle", color=gold, fit_width=210
)
crops = (
'<g fill="none" stroke-width="8" stroke-linecap="butt" stroke-linejoin="miter">'
f'<path d="M138 122 L138 50 L210 50" stroke="{struct}"/>'
Expand Down Expand Up @@ -72,8 +85,15 @@ def mark(*, struct: str, gold: str) -> str:

def social_card(*, bg: str, struct: str, gold: str, url_color: str) -> str:
body = lockup_body(struct=struct, gold=gold)
url, _ = outline_text("modern-python.org", 34, x=640, baseline_y=575,
anchor="middle", color=url_color, letter_spacing=4)
url, _ = outline_text(
"modern-python.org",
34,
x=640,
baseline_y=575,
anchor="middle",
color=url_color,
letter_spacing=4,
)
return (
_SVG_OPEN.format(w=1280, h=640)
+ f'<rect width="1280" height="640" fill="{bg}"/>'
Expand All @@ -94,3 +114,28 @@ def social_square(*, bg: str, struct: str, gold: str) -> str:
+ f'<g transform="translate({tx},{ty}) scale({s})">{body}</g>'
+ "</svg>"
)


def project_frame(
*,
struct: str,
accent: str,
w: int = 100,
h: int = 100,
m: int = 9,
lx: int = 53,
ly: int = 53,
s: int = 11,
) -> str:
"""Two pinwheeled L-snakes in opposite corners — the constant project frame.
Returns bare markup (no <svg> wrapper)."""
hs = s + 3
parts = [
f'<path d="M{m} {m + ly} L{m} {m} L{m + lx} {m}" fill="none" stroke="{struct}" stroke-width="{s}" stroke-linejoin="miter"/>',
f'<rect x="{m + lx - hs / 2:.1f}" y="{m - hs / 2:.1f}" width="{hs}" height="{hs}" rx="2" fill="{struct}"/>',
f'<polygon points="{m - s / 2:.1f},{m + ly - 2:.1f} {m + s / 2:.1f},{m + ly - 2:.1f} {m + s / 2:.1f},{m + ly:.1f} {m - s / 2:.1f},{m + ly + s:.1f}" fill="{struct}"/>',
f'<path d="M{w - m} {h - m - ly} L{w - m} {h - m} L{w - m - lx} {h - m}" fill="none" stroke="{accent}" stroke-width="{s}" stroke-linejoin="miter"/>',
f'<rect x="{w - m - lx - hs / 2:.1f}" y="{h - m - hs / 2:.1f}" width="{hs}" height="{hs}" rx="2" fill="{accent}"/>',
f'<polygon points="{w - m + s / 2:.1f},{h - m - ly + 2:.1f} {w - m - s / 2:.1f},{h - m - ly + 2:.1f} {w - m - s / 2:.1f},{h - m - ly:.1f} {w - m + s / 2:.1f},{h - m - ly - s:.1f}" fill="{accent}"/>',
]
return "".join(parts)
96 changes: 96 additions & 0 deletions brand/build/projects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from collections.abc import Callable
from pathlib import Path

from brand.build import geometry as g
from brand.build import symbols as sym
from brand.build import tokens as t
from brand.build.raster import export_png
from brand.build.text import outline_text

R = 23
_CX = _CY = 50

ALLOWED_COLORS: frozenset[str] = frozenset(
c.lower() for c in (t.GREEN_INK, t.GOLD_LIGHT, t.CREAM, *sym._BAR_TINTS)
)

MANIFEST: dict[str, Callable[[], str]] = {
# dependency injection
"modern-di": lambda: sym.graph(_CX, _CY, R, dashed=True),
"that-depends": lambda: sym.graph(_CX, _CY, R, dashed=False),
"modern-di-fastapi": lambda: sym.bolt_disc(_CX, _CY, R),
"modern-di-litestar": lambda: sym.star_disc(_CX, _CY, R),
"modern-di-faststream": lambda: sym.faststream(_CX, _CY, R),
"modern-di-typer": lambda: sym.terminal(_CX, _CY, R),
"modern-di-pytest": lambda: sym.bars(_CX, _CY, R),
# templates — reuse the org chevron
"fastapi-sqlalchemy-template": lambda: sym.chevron(_CX, _CY, R - 1),
"litestar-sqlalchemy-template": lambda: sym.chevron(_CX, _CY, R - 1),
# microservices, http & messaging
"lite-bootstrap": lambda: sym.rocket(_CX, _CY, R),
"httpware": lambda: sym.chain(_CX, _CY, R),
"faststream-redis-timers": lambda: sym.stopwatch(_CX, _CY, R),
"faststream-concurrent-aiokafka": lambda: sym.lanes(_CX, _CY, R),
"faststream-outbox": lambda: sym.outbox(_CX, _CY, R),
# utilities
"db-retry": lambda: sym.db_retry(_CX, _CY, R),
"eof-fixer": lambda: sym.eof_fixer(_CX, _CY, R),
"semvertag": lambda: sym.tag(_CX, _CY, R),
}


ROOT = Path(__file__).resolve().parents[2]
PROJECTS = ROOT / "brand" / "projects"
_PNG_SIZES = (512, 1024)

_LOCKUP_H = 100
_NAME_SIZE = 34
_GAP = 18


def project_mark(repo: str) -> str:
"""Full <svg> for a repo: constant frame + its gold inner symbol."""
frame = g.project_frame(struct=t.GREEN_INK, accent=t.GOLD_LIGHT)
inner = MANIFEST[repo]()
return (
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" '
f'role="img" aria-label="{repo}">{frame}{inner}</svg>'
)


def project_lockup(repo: str) -> str:
"""Framed mark on the left + the repo name in Jost (green) to its right."""
mark_frame = g.project_frame(struct=t.GREEN_INK, accent=t.GOLD_LIGHT)
inner = MANIFEST[repo]()
name_x = _LOCKUP_H + _GAP
name_svg, name_w = outline_text(
repo,
_NAME_SIZE,
x=name_x,
baseline_y=_LOCKUP_H / 2 + _NAME_SIZE * 0.34,
anchor="start",
color=t.GREEN_INK,
)
total_w = round(name_x + name_w + _GAP)
return (
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {total_w} {_LOCKUP_H}" '
f'role="img" aria-label="{repo}">'
f"<g>{mark_frame}{inner}</g>"
f"{name_svg}</svg>"
)


def render_projects(out_dir: Path | None = None) -> list[Path]:
"""Write mark.svg (+ PNGs) for every repo under out_dir/<repo>/."""
base = out_dir if out_dir is not None else PROJECTS
written: list[Path] = []
for repo in MANIFEST:
d = base / repo
d.mkdir(parents=True, exist_ok=True)
svg = d / "mark.svg"
svg.write_text(project_mark(repo) + "\n", encoding="utf-8")
for sz in _PNG_SIZES:
export_png(svg, d / f"mark-{sz}.png", width=sz, height=sz)
(d / "lockup.svg").write_text(project_lockup(repo) + "\n", encoding="utf-8")
written.append(svg)
return written
23 changes: 23 additions & 0 deletions brand/build/raster.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import shutil
import subprocess
from pathlib import Path


def export_png(
svg_path: Path,
png_path: Path,
*,
width: int | None = None,
height: int | None = None,
) -> bool:
exe = shutil.which("rsvg-convert")
if exe is None:
return False
args = [exe]
if width is not None:
args += ["-w", str(width)]
if height is not None:
args += ["-h", str(height)]
args += [str(svg_path), "-o", str(png_path)]
subprocess.run(args, check=True)
return True
93 changes: 56 additions & 37 deletions brand/build/render.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import shutil
import subprocess
from pathlib import Path

from brand.build import geometry as g
from brand.build import tokens as t
from brand.build.projects import render_projects
from brand.build.raster import export_png

ROOT = Path(__file__).resolve().parents[2]
ORG = ROOT / "brand" / "org"
Expand All @@ -14,26 +14,6 @@ def _write(path: Path, content: str) -> None:
path.write_text(content + "\n", encoding="utf-8")


def export_png(
svg_path: Path,
png_path: Path,
*,
width: int | None = None,
height: int | None = None,
) -> bool:
exe = shutil.which("rsvg-convert")
if exe is None:
return False
args = [exe]
if width is not None:
args += ["-w", str(width)]
if height is not None:
args += ["-h", str(height)]
args += [str(svg_path), "-o", str(png_path)]
subprocess.run(args, check=True)
return True


def render() -> None:
ORG.mkdir(parents=True, exist_ok=True)
ic = dict(bg=t.GREEN_SURFACE, struct=t.CREAM, gold=t.GOLD_DARK)
Expand All @@ -43,13 +23,25 @@ def render() -> None:
export_png(ORG / "favicon.svg", ORG / f"favicon-{sz}.png", width=sz, height=sz)
# apple-touch (same mark, already full-bleed/square)
_write(ORG / "apple-touch-icon.svg", g.icon(**ic))
export_png(ORG / "apple-touch-icon.svg", ORG / "apple-touch-icon-180.png", width=180, height=180)
export_png(
ORG / "apple-touch-icon.svg",
ORG / "apple-touch-icon-180.png",
width=180,
height=180,
)
# avatar (same mark, large raster)
_write(ORG / "avatar.svg", g.icon(**ic))
export_png(ORG / "avatar.svg", ORG / "avatar-1024.png", width=1024, height=1024)
_write(ORG / "avatar-circle.svg",
g.icon_circle(bg=t.GREEN_SURFACE, struct=t.CREAM, gold=t.GOLD_DARK))
export_png(ORG / "avatar-circle.svg", ORG / "avatar-circle-1024.png", width=1024, height=1024)
_write(
ORG / "avatar-circle.svg",
g.icon_circle(bg=t.GREEN_SURFACE, struct=t.CREAM, gold=t.GOLD_DARK),
)
export_png(
ORG / "avatar-circle.svg",
ORG / "avatar-circle-1024.png",
width=1024,
height=1024,
)

# Site logos — transparent, no background.
# wordmark (hero): two-color lockup, light + dark variants
Expand All @@ -59,20 +51,47 @@ def render() -> None:
_write(ORG / "mark.svg", g.mark(struct=t.CREAM, gold=t.GOLD_DARK))

# Social cards — cream (primary) + green (alternate).
_write(ORG / "social-card.svg",
g.social_card(bg=t.CREAM, struct=t.GREEN_INK, gold=t.GOLD_LIGHT, url_color=t.GOLD_LIGHT))
_write(
ORG / "social-card.svg",
g.social_card(
bg=t.CREAM, struct=t.GREEN_INK, gold=t.GOLD_LIGHT, url_color=t.GOLD_LIGHT
),
)
export_png(ORG / "social-card.svg", ORG / "social-card.png", width=1280, height=640)
_write(ORG / "social-card-green.svg",
g.social_card(bg=t.GREEN_SURFACE, struct=t.CREAM, gold=t.GOLD_DARK, url_color=t.GOLD_DARK))
export_png(ORG / "social-card-green.svg", ORG / "social-card-green.png", width=1280, height=640)
_write(
ORG / "social-card-green.svg",
g.social_card(
bg=t.GREEN_SURFACE, struct=t.CREAM, gold=t.GOLD_DARK, url_color=t.GOLD_DARK
),
)
export_png(
ORG / "social-card-green.svg",
ORG / "social-card-green.png",
width=1280,
height=640,
)

# Square (Telegram / square social) — cream + green.
_write(ORG / "social-square.svg",
g.social_square(bg=t.CREAM, struct=t.GREEN_INK, gold=t.GOLD_LIGHT))
export_png(ORG / "social-square.svg", ORG / "social-square.png", width=640, height=640)
_write(ORG / "social-square-green.svg",
g.social_square(bg=t.GREEN_SURFACE, struct=t.CREAM, gold=t.GOLD_DARK))
export_png(ORG / "social-square-green.svg", ORG / "social-square-green.png", width=640, height=640)
_write(
ORG / "social-square.svg",
g.social_square(bg=t.CREAM, struct=t.GREEN_INK, gold=t.GOLD_LIGHT),
)
export_png(
ORG / "social-square.svg", ORG / "social-square.png", width=640, height=640
)
_write(
ORG / "social-square-green.svg",
g.social_square(bg=t.GREEN_SURFACE, struct=t.CREAM, gold=t.GOLD_DARK),
)
export_png(
ORG / "social-square-green.svg",
ORG / "social-square-green.png",
width=640,
height=640,
)

# Per-project marks (brand/projects/<repo>/).
render_projects()


def main() -> None:
Expand Down
Loading