Skip to content

Commit 0ca10ce

Browse files
lesnik512claude
andauthored
Org favicon + social card kit (#17)
* brand: org favicon + social card kit A deterministic brand toolchain (brand/build/) generating the org favicon, GitHub/Telegram avatars, and og:image social cards from the approved two-snakes + chevron mark and a Jost wordmark lockup, plus site wiring. - brand/build/: green/cream/gold tokens; a fonttools Jost outliner (wordmark rendered as SVG paths, no font dependency at view time); geometry builders; a renderer emitting SVG + rsvg-rasterized PNG (PNGs skipped if rsvg absent). - Icon: border-reaching snakes (square block head + diagonal-cut tail) around a gold chevron, square full-bleed; one mark for favicon / apple-touch / avatar, plus a padded avatar-circle for circular crops (Telegram). - Social card: MODERN / PYTHON Jost lockup framed by the snakes; cream (primary) and green, 1280x640 og:image and a 640x640 square. - Site: new favicon + og:image (1280x640) wired into mkdocs/overrides. Design spec and plan under docs/superpowers/. Supersedes the snake mark in PR #16; the homepage hero and header logo redesign are deferred. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * brand: straighten tail inner edge (quad overlap) The seam-overlap fix moved the tail's inner vertex into the stroke, stepping the body's inner edge. Use a 4-point tail that overlaps the stroke for the seam but starts the inner diagonal at the body's inner corner, so the inner line runs straight into the tail. Applies to icon and lockup tails. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5db64cb commit 0ca10ce

41 files changed

Lines changed: 1516 additions & 4 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
.idea
22
site/
33
.superpowers/
4+
__pycache__/
5+
*.pyc
6+
.DS_Store

brand/README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# modern-python brand kit
2+
3+
Source of truth for the org's **favicon** and **social card**. Generated assets
4+
live in `brand/org/`. Regenerate with:
5+
6+
```bash
7+
uv run python -m brand.build.render
8+
```
9+
10+
PNGs require `rsvg-convert` (`brew install librsvg` / `apt install librsvg2-bin`).
11+
Without it the SVGs are still written and PNGs are skipped.
12+
13+
## Palette
14+
15+
| Token | Hex | Role |
16+
|-------|-----|------|
17+
| Green ink | `#356852` | snakes/text on cream |
18+
| Green surface | `#2f5e4a` | green backgrounds |
19+
| Gold (light) | `#c98a00` | accent on cream |
20+
| Gold (dark) | `#f0b528` | accent on green |
21+
| Cream | `#f4f1e8` | light surface; light ink on green |
22+
23+
## Mark
24+
25+
Two interlocked "snakes" (corner brackets with square heads) as a pinwheel. The
26+
**icon** wraps them around a gold core dot (favicon + avatar). The **wordmark
27+
lockup** pulls them into crop marks framing `MODERN` / `PYTHON` set in **Jost**
28+
(SIL OFL, outlined to paths at build time — see `brand/build/fonts/Jost-OFL.txt`).
29+
30+
## Outputs (`brand/org/`)
31+
32+
| File | What |
33+
|------|------|
34+
| `favicon.svg`, `favicon-16/32/48.png` | browser favicon (bold reduction, green) |
35+
| `apple-touch-icon.svg`, `apple-touch-icon-180.png` | iOS home-screen (full-bleed) |
36+
| `avatar.svg`, `avatar-1024.png` | GitHub org picture |
37+
| `social-card.svg` / `.png` | 1280×640 og:image, **cream** (primary) |
38+
| `social-card-green.svg` / `.png` | 1280×640, green alternate |
39+
| `social-square.svg` / `.png`, `social-square-green.*` | 640×640 (Telegram) |
40+
41+
## Deferred (not in this kit)
42+
43+
Per-project / per-repo marks, the subfamily system, and any inner glyphs are a
44+
later task. The header nav logo redesign is also a follow-up.

brand/__init__.py

Whitespace-only changes.

brand/build/__init__.py

Whitespace-only changes.

brand/build/fonts/Jost-OFL.txt

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
Copyright 2020 The Jost Project Authors (https://github.com/indestructible-type)
2+
3+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
4+
This license is copied below, and is also available with a FAQ at:
5+
http://scripts.sil.org/OFL
6+
7+
8+
-----------------------------------------------------------
9+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10+
-----------------------------------------------------------
11+
12+
PREAMBLE
13+
The goals of the Open Font License (OFL) are to stimulate worldwide
14+
development of collaborative font projects, to support the font creation
15+
efforts of academic and linguistic communities, and to provide a free and
16+
open framework in which fonts may be shared and improved in partnership
17+
with others.
18+
19+
The OFL allows the licensed fonts to be used, studied, modified and
20+
redistributed freely as long as they are not sold by themselves. The
21+
fonts, including any derivative works, can be bundled, embedded,
22+
redistributed and/or sold with any software provided that any reserved
23+
names are not used by derivative works. The fonts and derivatives,
24+
however, cannot be released under any other type of license. The
25+
requirement for fonts to remain under this license does not apply
26+
to any document created using the fonts or their derivatives.
27+
28+
DEFINITIONS
29+
"Font Software" refers to the set of files released by the Copyright
30+
Holder(s) under this license and clearly marked as such. This may
31+
include source files, build scripts and documentation.
32+
33+
"Reserved Font Name" refers to any names specified as such after the
34+
copyright statement(s).
35+
36+
"Original Version" refers to the collection of Font Software components as
37+
distributed by the Copyright Holder(s).
38+
39+
"Modified Version" refers to any derivative made by adding to, deleting,
40+
or substituting -- in part or in whole -- any of the components of the
41+
Original Version, by changing formats or by porting the Font Software to a
42+
new environment.
43+
44+
"Author" refers to any designer, engineer, programmer, technical
45+
writer or other person who contributed to the Font Software.
46+
47+
PERMISSION & CONDITIONS
48+
Permission is hereby granted, free of charge, to any person obtaining
49+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
50+
redistribute, and sell modified and unmodified copies of the Font
51+
Software, subject to the following conditions:
52+
53+
1) Neither the Font Software nor any of its individual components,
54+
in Original or Modified Versions, may be sold by itself.
55+
56+
2) Original or Modified Versions of the Font Software may be bundled,
57+
redistributed and/or sold with any software, provided that each copy
58+
contains the above copyright notice and this license. These can be
59+
included either as stand-alone text files, human-readable headers or
60+
in the appropriate machine-readable metadata fields within text or
61+
binary files as long as those fields can be easily viewed by the user.
62+
63+
3) No Modified Version of the Font Software may use the Reserved Font
64+
Name(s) unless explicit written permission is granted by the corresponding
65+
Copyright Holder. This restriction only applies to the primary font name as
66+
presented to the users.
67+
68+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69+
Software shall not be used to promote, endorse or advertise any
70+
Modified Version, except to acknowledge the contribution(s) of the
71+
Copyright Holder(s) and the Author(s) or with their explicit written
72+
permission.
73+
74+
5) The Font Software, modified or unmodified, in part or in whole,
75+
must be distributed entirely under this license, and must not be
76+
distributed under any other license. The requirement for fonts to
77+
remain under this license does not apply to any document created
78+
using the Font Software.
79+
80+
TERMINATION
81+
This license becomes null and void if any of the above conditions are
82+
not met.
83+
84+
DISCLAIMER
85+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93+
OTHER DEALINGS IN THE FONT SOFTWARE.

brand/build/fonts/Jost-Regular.ttf

59.6 KB
Binary file not shown.

brand/build/geometry.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
from brand.build.text import outline_text
2+
3+
_SVG_OPEN = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" role="img" aria-label="Modern Python">'
4+
5+
6+
def _icon_mark(struct: str, gold: str) -> str:
7+
"""The two border-reaching snakes (block head + diagonal-cut tail) + chevron.
8+
Tail polygon bases overlap the stroke by 2px to avoid a seam."""
9+
return (
10+
f'<path d="M15 68 L15 15 L68 15" fill="none" stroke="{struct}" stroke-width="11" stroke-linecap="butt" stroke-linejoin="miter"/>'
11+
f'<rect x="61" y="8" width="14" height="14" rx="2" fill="{struct}"/>'
12+
f'<polygon points="9.5,66 20.5,66 20.5,68 9.5,79" fill="{struct}"/>'
13+
f'<path d="M85 32 L85 85 L32 85" fill="none" stroke="{gold}" stroke-width="11" stroke-linecap="butt" stroke-linejoin="miter"/>'
14+
f'<rect x="25" y="78" width="14" height="14" rx="2" fill="{gold}"/>'
15+
f'<polygon points="90.5,34 79.5,34 79.5,32 90.5,21" fill="{gold}"/>'
16+
f'<polyline points="45,40 57,50 45,60" fill="none" stroke="{gold}" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>'
17+
)
18+
19+
20+
def icon(*, bg: str, struct: str, gold: str) -> str:
21+
"""Full-bleed square icon — favicon, apple-touch, GitHub avatar."""
22+
return (_SVG_OPEN.format(w=100, h=100)
23+
+ f'<rect width="100" height="100" fill="{bg}"/>'
24+
+ _icon_mark(struct, gold) + "</svg>")
25+
26+
27+
def icon_circle(*, bg: str, struct: str, gold: str, scale: float = 0.74) -> str:
28+
"""Padded variant centered for circular crops (e.g. Telegram). The mark is
29+
scaled about the center so it fits inside the inscribed circle with margin."""
30+
return (_SVG_OPEN.format(w=100, h=100)
31+
+ f'<rect width="100" height="100" fill="{bg}"/>'
32+
+ f'<g transform="translate(50,50) scale({scale}) translate(-50,-50)">{_icon_mark(struct, gold)}</g>'
33+
+ "</svg>")
34+
35+
36+
def lockup_body(*, struct: str, gold: str) -> str:
37+
"""The MODERN/PYTHON crop-mark lockup, drawn in a 540x250 coordinate space.
38+
Returned as bare markup (no <svg> wrapper, no background) for embedding."""
39+
modern, _ = outline_text("MODERN", 50, x=270, baseline_y=126, anchor="middle",
40+
color=struct, fit_width=210)
41+
python, _ = outline_text("PYTHON", 50, x=270, baseline_y=166, anchor="middle",
42+
color=gold, fit_width=210)
43+
crops = (
44+
'<g fill="none" stroke-width="8" stroke-linecap="butt" stroke-linejoin="miter">'
45+
f'<path d="M138 122 L138 50 L210 50" stroke="{struct}"/>'
46+
f'<path d="M402 128 L402 200 L330 200" stroke="{gold}"/></g>'
47+
f'<rect x="202.5" y="42.5" width="15" height="15" rx="3" fill="{struct}"/>'
48+
f'<rect x="322.5" y="192.5" width="15" height="15" rx="3" fill="{gold}"/>'
49+
f'<polygon points="134,120 142,120 142,122 134,130" fill="{struct}"/>'
50+
f'<polygon points="406,130 398,130 398,128 406,120" fill="{gold}"/>'
51+
)
52+
return crops + modern + python
53+
54+
55+
def social_card(*, bg: str, struct: str, gold: str, url_color: str) -> str:
56+
body = lockup_body(struct=struct, gold=gold)
57+
url, _ = outline_text("modern-python.org", 34, x=640, baseline_y=575,
58+
anchor="middle", color=url_color, letter_spacing=4)
59+
return (
60+
_SVG_OPEN.format(w=1280, h=640)
61+
+ f'<rect width="1280" height="640" fill="{bg}"/>'
62+
+ f'<g transform="translate(235,108) scale(1.5)">{body}</g>'
63+
+ url
64+
+ "</svg>"
65+
)
66+
67+
68+
def social_square(*, bg: str, struct: str, gold: str) -> str:
69+
body = lockup_body(struct=struct, gold=gold)
70+
s = 1.9
71+
tx = round(320 - 270 * s, 1) # box-center horizontally (lockup box is 540 wide)
72+
ty = round(320 - 125 * s, 1) # box-center vertically (lockup box is 250 tall)
73+
return (
74+
_SVG_OPEN.format(w=640, h=640)
75+
+ f'<rect width="640" height="640" fill="{bg}"/>'
76+
+ f'<g transform="translate({tx},{ty}) scale({s})">{body}</g>'
77+
+ "</svg>"
78+
)

brand/build/render.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import shutil
2+
import subprocess
3+
from pathlib import Path
4+
5+
from brand.build import geometry as g
6+
from brand.build import tokens as t
7+
8+
ROOT = Path(__file__).resolve().parents[2]
9+
ORG = ROOT / "brand" / "org"
10+
11+
12+
def _write(path: Path, content: str) -> None:
13+
path.parent.mkdir(parents=True, exist_ok=True)
14+
path.write_text(content + "\n", encoding="utf-8")
15+
16+
17+
def export_png(
18+
svg_path: Path,
19+
png_path: Path,
20+
*,
21+
width: int | None = None,
22+
height: int | None = None,
23+
) -> bool:
24+
exe = shutil.which("rsvg-convert")
25+
if exe is None:
26+
return False
27+
args = [exe]
28+
if width is not None:
29+
args += ["-w", str(width)]
30+
if height is not None:
31+
args += ["-h", str(height)]
32+
args += [str(svg_path), "-o", str(png_path)]
33+
subprocess.run(args, check=True)
34+
return True
35+
36+
37+
def render() -> None:
38+
ORG.mkdir(parents=True, exist_ok=True)
39+
ic = dict(bg=t.GREEN_SURFACE, struct=t.CREAM, gold=t.GOLD_DARK)
40+
# favicon
41+
_write(ORG / "favicon.svg", g.icon(**ic))
42+
for sz in (16, 32, 48):
43+
export_png(ORG / "favicon.svg", ORG / f"favicon-{sz}.png", width=sz, height=sz)
44+
# apple-touch (same mark, already full-bleed/square)
45+
_write(ORG / "apple-touch-icon.svg", g.icon(**ic))
46+
export_png(ORG / "apple-touch-icon.svg", ORG / "apple-touch-icon-180.png", width=180, height=180)
47+
# avatar (same mark, large raster)
48+
_write(ORG / "avatar.svg", g.icon(**ic))
49+
export_png(ORG / "avatar.svg", ORG / "avatar-1024.png", width=1024, height=1024)
50+
_write(ORG / "avatar-circle.svg",
51+
g.icon_circle(bg=t.GREEN_SURFACE, struct=t.CREAM, gold=t.GOLD_DARK))
52+
export_png(ORG / "avatar-circle.svg", ORG / "avatar-circle-1024.png", width=1024, height=1024)
53+
54+
# Social cards — cream (primary) + green (alternate).
55+
_write(ORG / "social-card.svg",
56+
g.social_card(bg=t.CREAM, struct=t.GREEN_INK, gold=t.GOLD_LIGHT, url_color=t.GOLD_LIGHT))
57+
export_png(ORG / "social-card.svg", ORG / "social-card.png", width=1280, height=640)
58+
_write(ORG / "social-card-green.svg",
59+
g.social_card(bg=t.GREEN_SURFACE, struct=t.CREAM, gold=t.GOLD_DARK, url_color=t.GOLD_DARK))
60+
export_png(ORG / "social-card-green.svg", ORG / "social-card-green.png", width=1280, height=640)
61+
62+
# Square (Telegram / square social) — cream + green.
63+
_write(ORG / "social-square.svg",
64+
g.social_square(bg=t.CREAM, struct=t.GREEN_INK, gold=t.GOLD_LIGHT))
65+
export_png(ORG / "social-square.svg", ORG / "social-square.png", width=640, height=640)
66+
_write(ORG / "social-square-green.svg",
67+
g.social_square(bg=t.GREEN_SURFACE, struct=t.CREAM, gold=t.GOLD_DARK))
68+
export_png(ORG / "social-square-green.svg", ORG / "social-square-green.png", width=640, height=640)
69+
70+
71+
def main() -> None:
72+
render()
73+
74+
75+
if __name__ == "__main__":
76+
main()

brand/build/text.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from functools import lru_cache
2+
from pathlib import Path
3+
4+
from fontTools.pens.svgPathPen import SVGPathPen
5+
from fontTools.ttLib import TTFont
6+
7+
_FONTS = Path(__file__).parent / "fonts"
8+
JOST = _FONTS / "Jost-Regular.ttf"
9+
10+
11+
@lru_cache(maxsize=4)
12+
def _font(path: Path) -> TTFont:
13+
return TTFont(path)
14+
15+
16+
def _upm(path: Path) -> int:
17+
return _font(path)["head"].unitsPerEm # ty: ignore[unresolved-attribute]
18+
19+
20+
def outline_text(
21+
text: str,
22+
size: float,
23+
*,
24+
x: float,
25+
baseline_y: float,
26+
anchor: str = "start",
27+
color: str = "#000000",
28+
fit_width: float | None = None,
29+
letter_spacing: float = 0.0,
30+
font_path: Path = JOST,
31+
) -> tuple[str, float]:
32+
"""Outline `text` to SVG <path>s. Returns (group_svg, rendered_width).
33+
34+
fit_width pins the rendered width (like SVG textLength): glyphs+spacing are
35+
scaled horizontally to exactly fit_width. letter_spacing is extra px between
36+
glyphs. anchor="middle" centers on x.
37+
"""
38+
f = _font(font_path)
39+
glyphset = f.getGlyphSet()
40+
cmap = f.getBestCmap()
41+
hmtx = f["hmtx"]
42+
upm = _upm(font_path)
43+
s = size / upm
44+
assert cmap is not None, "font has no Unicode cmap"
45+
46+
ls_units = letter_spacing / s if s else 0.0
47+
parts: list[str] = []
48+
cursor = 0.0
49+
for ch in text:
50+
gname = cmap.get(ord(ch))
51+
if gname is None:
52+
raise ValueError(f"character {ch!r} (U+{ord(ch):04X}) not in {font_path.name} cmap")
53+
pen = SVGPathPen(glyphset)
54+
glyphset[gname].draw(pen)
55+
d = pen.getCommands()
56+
if d:
57+
parts.append(f'<path d="{d}" transform="translate({cursor:.2f},0)"/>')
58+
cursor += hmtx[gname][0] + ls_units
59+
cursor -= ls_units # drop trailing letter-spacing
60+
61+
natural = cursor * s
62+
sx = s * (fit_width / natural) if (fit_width and natural) else s
63+
eff = fit_width if fit_width else natural
64+
ox = x - eff / 2 if anchor == "middle" else x
65+
g = (
66+
f'<g fill="{color}" transform="translate({ox:.3f},{baseline_y:.3f}) '
67+
f'scale({sx:.6f},{-s:.6f})">{"".join(parts)}</g>'
68+
)
69+
return g, eff

brand/build/tokens.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Palette — concrete colors (no CSS variables anywhere downstream).
2+
GREEN_INK = "#356852" # snakes/text on cream (the "structure" color on light)
3+
GREEN_SURFACE = "#2f5e4a" # green backgrounds (favicon/avatar/green card)
4+
GOLD_LIGHT = "#c98a00" # gold accent on cream
5+
GOLD_DARK = "#f0b528" # gold accent on green/dark
6+
CREAM = "#f4f1e8" # light surface; also the light "ink" on green (not pure white)

0 commit comments

Comments
 (0)