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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
.idea
site/
.superpowers/
__pycache__/
*.pyc
.DS_Store
44 changes: 44 additions & 0 deletions brand/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# modern-python brand kit

Source of truth for the org's **favicon** and **social card**. Generated assets
live in `brand/org/`. Regenerate with:

```bash
uv run python -m brand.build.render
```

PNGs require `rsvg-convert` (`brew install librsvg` / `apt install librsvg2-bin`).
Without it the SVGs are still written and PNGs are skipped.

## Palette

| Token | Hex | Role |
|-------|-----|------|
| Green ink | `#356852` | snakes/text on cream |
| Green surface | `#2f5e4a` | green backgrounds |
| Gold (light) | `#c98a00` | accent on cream |
| Gold (dark) | `#f0b528` | accent on green |
| Cream | `#f4f1e8` | light surface; light ink on green |

## Mark

Two interlocked "snakes" (corner brackets with square heads) as a pinwheel. The
**icon** wraps them around a gold core dot (favicon + avatar). The **wordmark
lockup** pulls them into crop marks framing `MODERN` / `PYTHON` set in **Jost**
(SIL OFL, outlined to paths at build time — see `brand/build/fonts/Jost-OFL.txt`).

## Outputs (`brand/org/`)

| File | What |
|------|------|
| `favicon.svg`, `favicon-16/32/48.png` | browser favicon (bold reduction, green) |
| `apple-touch-icon.svg`, `apple-touch-icon-180.png` | iOS home-screen (full-bleed) |
| `avatar.svg`, `avatar-1024.png` | GitHub org picture |
| `social-card.svg` / `.png` | 1280×640 og:image, **cream** (primary) |
| `social-card-green.svg` / `.png` | 1280×640, green alternate |
| `social-square.svg` / `.png`, `social-square-green.*` | 640×640 (Telegram) |

## 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.
Empty file added brand/__init__.py
Empty file.
Empty file added brand/build/__init__.py
Empty file.
93 changes: 93 additions & 0 deletions brand/build/fonts/Jost-OFL.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
Copyright 2020 The Jost Project Authors (https://github.com/indestructible-type)

This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL


-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------

PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.

The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.

DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.

"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).

"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).

"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.

"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.

PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:

1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.

2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.

3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.

5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.

TERMINATION
This license becomes null and void if any of the above conditions are
not met.

DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file added brand/build/fonts/Jost-Regular.ttf
Binary file not shown.
78 changes: 78 additions & 0 deletions brand/build/geometry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from brand.build.text import outline_text

_SVG_OPEN = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" role="img" aria-label="Modern Python">'


def _icon_mark(struct: str, gold: str) -> str:
"""The two border-reaching snakes (block head + diagonal-cut tail) + chevron.
Tail polygon bases overlap the stroke by 2px to avoid a seam."""
return (
f'<path d="M15 68 L15 15 L68 15" fill="none" stroke="{struct}" stroke-width="11" stroke-linecap="butt" stroke-linejoin="miter"/>'
f'<rect x="61" y="8" width="14" height="14" rx="2" fill="{struct}"/>'
f'<polygon points="9.5,66 20.5,66 20.5,68 9.5,79" fill="{struct}"/>'
f'<path d="M85 32 L85 85 L32 85" fill="none" stroke="{gold}" stroke-width="11" stroke-linecap="butt" stroke-linejoin="miter"/>'
f'<rect x="25" y="78" width="14" height="14" rx="2" fill="{gold}"/>'
f'<polygon points="90.5,34 79.5,34 79.5,32 90.5,21" fill="{gold}"/>'
f'<polyline points="45,40 57,50 45,60" fill="none" stroke="{gold}" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>'
)


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>")


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>")


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)
crops = (
'<g fill="none" stroke-width="8" stroke-linecap="butt" stroke-linejoin="miter">'
f'<path d="M138 122 L138 50 L210 50" stroke="{struct}"/>'
f'<path d="M402 128 L402 200 L330 200" stroke="{gold}"/></g>'
f'<rect x="202.5" y="42.5" width="15" height="15" rx="3" fill="{struct}"/>'
f'<rect x="322.5" y="192.5" width="15" height="15" rx="3" fill="{gold}"/>'
f'<polygon points="134,120 142,120 142,122 134,130" fill="{struct}"/>'
f'<polygon points="406,130 398,130 398,128 406,120" fill="{gold}"/>'
)
return crops + modern + python


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)
return (
_SVG_OPEN.format(w=1280, h=640)
+ f'<rect width="1280" height="640" fill="{bg}"/>'
+ f'<g transform="translate(235,108) scale(1.5)">{body}</g>'
+ url
+ "</svg>"
)


def social_square(*, bg: str, struct: str, gold: str) -> str:
body = lockup_body(struct=struct, gold=gold)
s = 1.9
tx = round(320 - 270 * s, 1) # box-center horizontally (lockup box is 540 wide)
ty = round(320 - 125 * s, 1) # box-center vertically (lockup box is 250 tall)
return (
_SVG_OPEN.format(w=640, h=640)
+ f'<rect width="640" height="640" fill="{bg}"/>'
+ f'<g transform="translate({tx},{ty}) scale({s})">{body}</g>'
+ "</svg>"
)
76 changes: 76 additions & 0 deletions brand/build/render.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import shutil
import subprocess
from pathlib import Path

from brand.build import geometry as g
from brand.build import tokens as t

ROOT = Path(__file__).resolve().parents[2]
ORG = ROOT / "brand" / "org"


def _write(path: Path, content: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
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)
# favicon
_write(ORG / "favicon.svg", g.icon(**ic))
for sz in (16, 32, 48):
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)
# 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)

# 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))
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)

# 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)


def main() -> None:
render()


if __name__ == "__main__":
main()
69 changes: 69 additions & 0 deletions brand/build/text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from functools import lru_cache
from pathlib import Path

from fontTools.pens.svgPathPen import SVGPathPen
from fontTools.ttLib import TTFont

_FONTS = Path(__file__).parent / "fonts"
JOST = _FONTS / "Jost-Regular.ttf"


@lru_cache(maxsize=4)
def _font(path: Path) -> TTFont:
return TTFont(path)


def _upm(path: Path) -> int:
return _font(path)["head"].unitsPerEm # ty: ignore[unresolved-attribute]


def outline_text(
text: str,
size: float,
*,
x: float,
baseline_y: float,
anchor: str = "start",
color: str = "#000000",
fit_width: float | None = None,
letter_spacing: float = 0.0,
font_path: Path = JOST,
) -> tuple[str, float]:
"""Outline `text` to SVG <path>s. Returns (group_svg, rendered_width).

fit_width pins the rendered width (like SVG textLength): glyphs+spacing are
scaled horizontally to exactly fit_width. letter_spacing is extra px between
glyphs. anchor="middle" centers on x.
"""
f = _font(font_path)
glyphset = f.getGlyphSet()
cmap = f.getBestCmap()
hmtx = f["hmtx"]
upm = _upm(font_path)
s = size / upm
assert cmap is not None, "font has no Unicode cmap"

ls_units = letter_spacing / s if s else 0.0
parts: list[str] = []
cursor = 0.0
for ch in text:
gname = cmap.get(ord(ch))
if gname is None:
raise ValueError(f"character {ch!r} (U+{ord(ch):04X}) not in {font_path.name} cmap")
pen = SVGPathPen(glyphset)
glyphset[gname].draw(pen)
d = pen.getCommands()
if d:
parts.append(f'<path d="{d}" transform="translate({cursor:.2f},0)"/>')
cursor += hmtx[gname][0] + ls_units
cursor -= ls_units # drop trailing letter-spacing

natural = cursor * s
sx = s * (fit_width / natural) if (fit_width and natural) else s
eff = fit_width if fit_width else natural
ox = x - eff / 2 if anchor == "middle" else x
g = (
f'<g fill="{color}" transform="translate({ox:.3f},{baseline_y:.3f}) '
f'scale({sx:.6f},{-s:.6f})">{"".join(parts)}</g>'
)
return g, eff
6 changes: 6 additions & 0 deletions brand/build/tokens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Palette — concrete colors (no CSS variables anywhere downstream).
GREEN_INK = "#356852" # snakes/text on cream (the "structure" color on light)
GREEN_SURFACE = "#2f5e4a" # green backgrounds (favicon/avatar/green card)
GOLD_LIGHT = "#c98a00" # gold accent on cream
GOLD_DARK = "#f0b528" # gold accent on green/dark
CREAM = "#f4f1e8" # light surface; also the light "ink" on green (not pure white)
Binary file added brand/org/apple-touch-icon-180.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions brand/org/apple-touch-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added brand/org/avatar-1024.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added brand/org/avatar-circle-1024.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions brand/org/avatar-circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions brand/org/avatar.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added brand/org/favicon-16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added brand/org/favicon-32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added brand/org/favicon-48.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions brand/org/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added brand/org/social-card-green.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading