Something went wrong. Try again.
My website. cameron.stream
Something went wrong. Try again.
Python
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490#!/usr/bin/env python3"""Generate Processional, a monumental unicase display typeface.
Processional translates Cosmic Liturgical Brutalism into letterforms: tallpylons carry the weight, thinner slabs establish rhythm, and circular countersbehave as apertures cut through architectural mass. The geometry is originaland built directly from the primitives below."""
from __future__ import annotations
import mathimport osfrom pathlib import Path
os.environ.setdefault("SOURCE_DATE_EPOCH", "1785028800")
from fontTools.fontBuilder import FontBuilderfrom fontTools.feaLib.builder import addOpenTypeFeaturesFromStringfrom fontTools.pens.ttGlyphPen import TTGlyphPenfrom fontTools.ttLib import TTFontfrom shapely.affinity import scale, translatefrom shapely.geometry import LineString, Point, Polygon, boxfrom shapely.geometry.polygon import orientfrom shapely.ops import unary_union
UPM = 1000CAP = 800ASCENDER = 920DESCENDER = -180PYLON = 112SLAB = 76DIAGONAL = 96OUT_DIR = Path(__file__).resolve().parents[1] / "public" / "fonts"
def line(*points: tuple[float, float]) -> list[tuple[float, float]]: return list(points)
def arc( cx: float, cy: float, rx: float, ry: float, start: float, end: float, steps: int = 128,) -> list[tuple[float, float]]: return [ ( cx + rx * math.cos(math.radians(start + (end - start) * i / steps)), cy + ry * math.sin(math.radians(start + (end - start) * i / steps)), ) for i in range(steps + 1) ]
def cubic(p0, p1, p2, p3, steps: int = 128): points = [] for index in range(steps + 1): t = index / steps u = 1 - t points.append( ( u**3 * p0[0] + 3 * u * u * t * p1[0] + 3 * u * t * t * p2[0] + t**3 * p3[0], u**3 * p0[1] + 3 * u * u * t * p1[1] + 3 * u * t * t * p2[1] + t**3 * p3[1], ) ) return points
def cubic_bspline(control_points, steps_per_segment: int = 64): """Sample one continuous cubic B-spline through guiding controls.""" points = [control_points[0]] * 3 + control_points[1:-1] + [control_points[-1]] * 3 sampled = [] for segment in range(len(points) - 3): for step in range(steps_per_segment + 1): if segment and step == 0: continue t = step / steps_per_segment weights = ( (1 - t) ** 3 / 6, (3 * t**3 - 6 * t**2 + 4) / 6, (-3 * t**3 + 3 * t**2 + 3 * t + 1) / 6, t**3 / 6, ) sampled.append( tuple( sum(weights[index] * points[segment + index][axis] for index in range(4)) for axis in range(2) ) ) return sampled
def ellipse(cx: float, cy: float, rx: float, ry: float): unit = Point(cx, cy).buffer(1, resolution=128) return scale(unit, xfact=rx, yfact=ry, origin=(cx, cy))
def circle(cx: float, cy: float, radius: float): return Point(cx, cy).buffer(radius, resolution=128)
def rounded_rect(x0: float, y0: float, x1: float, y1: float, radius: float): radius = min(radius, (x1 - x0) / 2, (y1 - y0) / 2) return unary_union( [ box(x0 + radius, y0, x1 - radius, y1), box(x0, y0 + radius, x1, y1 - radius), circle(x0 + radius, y0 + radius, radius), circle(x1 - radius, y0 + radius, radius), circle(x0 + radius, y1 - radius, radius), circle(x1 - radius, y1 - radius, radius), ] )
def stroke(points, width: float = PYLON, *, rounded: bool = False): return LineString(points).buffer( width / 2, cap_style="flat", join_style="round" if rounded else "mitre", mitre_limit=4, resolution=48, )
def strokes(*items): """Union path/width tuples into one architectural form.""" return unary_union([stroke(points, width, rounded=rounded) for points, width, rounded in items])
def aperture_form(cx: float = 300, cy: float = 400): """The characteristic outer ellipse with a true circular aperture.""" return ellipse(cx, cy, 250, 410).difference(circle(cx, cy, 138))
def upper_bowl(cy: float = 600, hole: float = 102): outer = ellipse(150, cy, 360, 205).intersection(box(150, cy - 220, 560, cy + 220)) return outer.difference(circle(300, cy, hole))
def lower_bowl(cy: float = 200, hole: float = 98): outer = ellipse(150, cy, 360, 205).intersection(box(150, cy - 220, 560, cy + 220)) return outer.difference(circle(300, cy, hole))
def raw_geometry(char: str): """Return the unspaced geometry for one drawn character.""" left = box(70, 0, 70 + PYLON, CAP) right = box(418, 0, 418 + PYLON, CAP) top = box(70, CAP - SLAB, 530, CAP) middle = box(70, 400 - SLAB / 2, 470, 400 + SLAB / 2) bottom = box(70, 0, 530, SLAB)
if char == "A": monument = Polygon([(58, 0), (205, CAP), (395, CAP), (542, 0)]) doorway = box(252, 0, 348, 205) aperture = circle(300, 465, 93) return monument.difference(unary_union([doorway, aperture])) if char == "B": return unary_union([left, upper_bowl(), lower_bowl()]).buffer(0) if char == "C": opened = aperture_form().difference(box(355, 105, 610, 695)) return opened if char == "D": outer = ellipse(150, 400, 390, 410).intersection(box(150, -20, 570, 820)) return unary_union([left, outer]).difference(circle(295, 400, 128)).buffer(0) if char == "E": return unary_union([left, top, middle, bottom]) if char == "F": return unary_union([left, top, middle]) if char == "G": opened = aperture_form().difference(box(360, 125, 610, 690)) bar = unary_union([box(300, 360, 530, 436), box(418, 160, 530, 436)]) return unary_union([opened, bar]).buffer(0) if char == "H": return unary_union([left, right, middle]) if char == "I": return unary_union([box(244, 0, 356, CAP), box(140, CAP - SLAB, 460, CAP), box(140, 0, 460, SLAB)]) if char == "J": hook = stroke(arc(300, 190, 210, 175, 0, -180), PYLON, rounded=True) return unary_union([top, box(420, 190, 532, CAP), hook]).buffer(0) if char == "K": return strokes( (line((126, 0), (126, CAP)), PYLON, False), (line((150, 365), (500, CAP)), DIAGONAL, False), (line((150, 365), (525, 0)), DIAGONAL, False), ) if char == "L": return unary_union([left, bottom]) if char == "M": return strokes( (line((105, 0), (105, CAP)), PYLON, False), (line((105, CAP), (320, 245), (535, CAP)), DIAGONAL, False), (line((535, CAP), (535, 0)), PYLON, False), ) if char == "N": return strokes( (line((115, 0), (115, CAP)), PYLON, False), (line((115, CAP), (505, 0)), DIAGONAL, False), (line((505, 0), (505, CAP)), PYLON, False), ) if char == "O": return aperture_form() if char == "P": return unary_union([left, upper_bowl()]).buffer(0) if char == "Q": return unary_union([aperture_form(), stroke(line((330, 235), (600, -80)), 82)]).buffer(0) if char == "R": return unary_union([left, upper_bowl(), stroke(line((300, 440), (535, 0)), DIAGONAL)]).buffer(0) if char == "S": centerline = cubic_bspline( [ (505, 700), (395, 850), (105, 810), (68, 590), (265, 435), (500, 330), (525, 115), (315, -45), (70, 90), ] ) return stroke(centerline, 108, rounded=True) if char == "T": return unary_union([box(45, CAP - SLAB, 555, CAP), box(244, 0, 356, CAP)]) if char == "U": centerline = line((126, CAP), (126, 225)) + arc(300, 225, 174, 170, 180, 360)[1:] + line((474, 225), (474, CAP))[1:] return stroke(centerline, PYLON, rounded=True) if char == "V": return stroke(line((80, CAP), (300, 0), (520, CAP)), DIAGONAL) if char == "W": return stroke(line((55, CAP), (175, 0), (350, 480), (525, 0), (645, CAP)), DIAGONAL) if char == "X": return strokes( (line((70, CAP), (530, 0)), DIAGONAL, False), (line((530, CAP), (70, 0)), DIAGONAL, False), ) if char == "Y": return strokes( (line((70, CAP), (300, 410), (530, CAP)), DIAGONAL, False), (line((300, 410), (300, 0)), PYLON, False), ) if char == "Z": return strokes( (line((65, 762), (535, 762)), SLAB, False), (line((520, 760), (80, 38)), DIAGONAL, False), (line((65, 38), (535, 38)), SLAB, False), )
if char == "0": return unary_union([aperture_form(), box(265, 260, 335, 540)]).buffer(0) if char == "1": return unary_union([box(244, 0, 356, CAP), stroke(line((110, 640), (300, CAP)), DIAGONAL), box(110, 0, 490, SLAB)]) if char == "2": path = cubic((72, 590), (85, 850), (505, 870), (520, 610)) path += cubic((520, 610), (520, 450), (160, 250), (70, 38))[1:] return unary_union([stroke(path, 104, rounded=True), box(70, 0, 530, SLAB)]).buffer(0) if char == "3": path = cubic((75, 700), (250, 860), (500, 825), (480, 600)) path += cubic((480, 600), (470, 465), (340, 420), (270, 400))[1:] path += cubic((270, 400), (420, 390), (525, 315), (500, 155))[1:] path += cubic((500, 155), (470, -55), (170, -60), (65, 90))[1:] return stroke(path, 105, rounded=True) if char == "4": return unary_union([box(420, 0, 532, CAP), stroke(line((420, CAP), (65, 245)), DIAGONAL), box(65, 215, 555, 305)]) if char == "5": path = line((510, 760), (100, 760), (75, 420)) curve = cubic((75, 420), (420, 520), (540, 325), (500, 145)) curve += cubic((500, 145), (445, -45), (165, -45), (62, 90))[1:] return unary_union([stroke(path, 92), stroke(curve, 104, rounded=True)]).buffer(0) if char == "6": loop = ellipse(300, 250, 235, 250).difference(circle(300, 250, 105)) sweep = stroke(cubic((500, 700), (265, 900), (70, 635), (70, 255)), 104, rounded=True) return unary_union([loop, sweep]).buffer(0) if char == "7": return unary_union([top, stroke(line((500, 760), (150, 0)), DIAGONAL)]) if char == "8": upper = ellipse(300, 595, 218, 205).difference(circle(300, 595, 98)) lower = ellipse(300, 205, 242, 215).difference(circle(300, 205, 108)) bridge = box(244, 365, 356, 435) return unary_union([upper, lower, bridge]).buffer(0) if char == "9": loop = ellipse(300, 550, 235, 250).difference(circle(300, 550, 105)) sweep = stroke(cubic((500, 545), (530, 170), (310, -100), (95, 85)), 104, rounded=True) return unary_union([loop, sweep]).buffer(0)
if char == "-": return box(130, 362, 470, 438) if char == "+": return unary_union([box(90, 362, 510, 438), box(262, 190, 338, 610)]) if char == "=": return unary_union([box(100, 470, 500, 546), box(100, 254, 500, 330)]) if char == "/": return stroke(line((95, -65), (505, 865)), 82) if char == "_": return box(70, -90, 530, -14) if char == "|": return box(244, -80, 356, 880) if char == "!": return unary_union([box(244, 190, 356, CAP), circle(300, 48, 48)]) if char == "?": hook = cubic((70, 610), (85, 865), (515, 870), (520, 610)) hook += cubic((520, 610), (520, 430), (300, 430), (300, 235))[1:] return unary_union([stroke(hook, 104, rounded=True), circle(300, 48, 48)]) if char == ".": return circle(300, 48, 48) if char == ",": return stroke(line((325, 85), (250, -105)), 72) if char == ":": return unary_union([circle(300, 555, 44), circle(300, 185, 44)]) if char == ";": return unary_union([circle(300, 555, 44), stroke(line((325, 220), (250, 20)), 70)]) if char == "(": return stroke(arc(360, 400, 210, 430, 110, 250), 76, rounded=True) if char == ")": return stroke(arc(240, 400, 210, 430, -70, 70), 76, rounded=True) if char == "[": return unary_union([box(170, -80, 270, 880), box(170, 804, 440, 880), box(170, -80, 440, -4)]) if char == "]": return unary_union([box(330, -80, 430, 880), box(160, 804, 430, 880), box(160, -80, 430, -4)]) if char == "'": return box(255, 570, 345, CAP) if char == '"': return unary_union([box(195, 570, 275, CAP), box(325, 570, 405, CAP)]) if char == "&": upper = ellipse(255, 610, 170, 190).difference(circle(255, 610, 80)) upper = upper.difference(box(315, 420, 610, 830)) lower = ellipse(280, 230, 225, 240).difference(circle(280, 230, 104)) diagonal = stroke(line((155, 525), (545, -25)), 86) exit_bar = box(330, 190, 560, 270) return unary_union([upper, lower, diagonal, exit_bar]).buffer(0) if char == "@": outer = ellipse(300, 400, 275, 400).difference(ellipse(300, 400, 190, 310)) outer = outer.difference(box(430, 105, 620, 330)) inner = ellipse(275, 405, 112, 155).difference(circle(275, 405, 58)) arm = unary_union([box(342, 360, 530, 430), box(448, 275, 530, 430)]) return unary_union([outer, inner, arm]).buffer(0) raise KeyError(char)
DRAWN = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-+=/_|!?.,:;()[]'\"&@"
def normalize_vertical(geometry, char: str): if char not in "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789": return geometry target_min, target_max = (-10, 810) if char in "BCDGJOPQRSU0235689" else (0, CAP) _, min_y, _, max_y = geometry.bounds factor = (target_max - target_min) / (max_y - min_y) return translate(scale(geometry, yfact=factor, origin=(0, 0)), yoff=target_min - min_y * factor)
def fit_geometry(geometry, char: str): min_x, _, max_x, _ = geometry.bounds narrow = set("!.,:;'\"()[]|I") diagonals = set("ATVWXYZ147") rounds = set("BCDGJOPQRSU0235689@&") if char in ".,:;": left = right = 24 elif char in narrow: left = right = 58 elif char in diagonals: left = right = 38 elif char in rounds: left = right = 46 else: left = right = 44 shifted = translate(geometry, xoff=left - min_x) return shifted, round(max_x - min_x + left + right), left
def geometry_to_glyph(geometry): pen = TTGlyphPen(None) if isinstance(geometry, Polygon): polygons = [geometry] else: polygons = [part for part in getattr(geometry, "geoms", []) if isinstance(part, Polygon)] for polygon in polygons: polygon = orient(polygon, sign=-1.0) for contour in [polygon.exterior, *polygon.interiors]: coordinates = list(contour.coords)[:-1] if len(coordinates) < 3: continue pen.moveTo(tuple(round(value) for value in coordinates[0])) for point in coordinates[1:]: pen.lineTo(tuple(round(value) for value in point)) pen.closePath() return pen.glyph()
def empty_glyph(): return TTGlyphPen(None).glyph()
def build(): OUT_DIR.mkdir(parents=True, exist_ok=True) glyph_order = [".notdef", "space"] cmap = {32: "space", 0x00A0: "space"} glyphs = {".notdef": empty_glyph(), "space": empty_glyph()} metrics = {".notdef": (540, 0), "space": (250, 0)}
for char in DRAWN: name = f"uni{ord(char):04X}" geometry = normalize_vertical(raw_geometry(char).buffer(0), char) geometry, advance, side = fit_geometry(geometry, char) glyph_order.append(name) glyphs[name] = geometry_to_glyph(geometry) metrics[name] = (advance, side) cmap[ord(char)] = name
for char in "abcdefghijklmnopqrstuvwxyz": cmap[ord(char)] = f"uni{ord(char.upper()):04X}" cmap[0x2018] = "uni0027" cmap[0x2019] = "uni0027" cmap[0x201C] = "uni0022" cmap[0x201D] = "uni0022"
builder = FontBuilder(UPM, isTTF=True) builder.setupGlyphOrder(glyph_order) builder.setupCharacterMap(cmap) builder.setupGlyf(glyphs) builder.setupHorizontalMetrics(metrics) builder.setupHorizontalHeader(ascent=ASCENDER, descent=DESCENDER) builder.setupNameTable( { "familyName": "Processional", "styleName": "Regular", "uniqueFontIdentifier": "Processional Regular 1.000; Cameron and Co", "fullName": "Processional Regular", "psName": "Processional-Regular", "version": "Version 1.000", "designer": "Co, with Cameron", "description": "A monumental unicase display face built from pylons, slabs, and circular apertures.", "licenseDescription": "Copyright 2026 Cameron. All rights reserved.", } ) builder.setupOS2( sTypoAscender=ASCENDER, sTypoDescender=DESCENDER, usWinAscent=ASCENDER, usWinDescent=abs(DESCENDER), sxHeight=CAP, sCapHeight=CAP, usWeightClass=700, usWidthClass=3, fsSelection=0x40, ) builder.setupPost(underlinePosition=-120, underlineThickness=64) builder.setupMaxp()
ttf_path = OUT_DIR / "processional-regular.ttf" woff2_path = OUT_DIR / "processional-regular.woff2" builder.save(ttf_path)
font = TTFont(ttf_path) addOpenTypeFeaturesFromString( font, """ feature kern { pos uni0041 uni0054 -28; pos uni0041 uni0056 -34; pos uni0041 uni0057 -24; pos uni0041 uni0059 -34; pos uni0046 uni0041 -26; pos uni004C uni0054 -24; pos uni0050 uni0041 -30; pos uni0054 uni0041 -28; pos uni0054 uni004F -16; pos uni0056 uni0041 -34; pos uni0057 uni0041 -24; pos uni0059 uni0041 -34; pos uni0059 uni004F -18; } kern; """, ) font.save(ttf_path) font.flavor = "woff2" font.save(woff2_path) print(f"Wrote {ttf_path.relative_to(Path.cwd())}") print(f"Wrote {woff2_path.relative_to(Path.cwd())}")
if __name__ == "__main__": build()