The outline of "G" in Inter¶

These are the instructions for the uppercase G in Inter Regular (Inter 4.001, the default instance of the variable font). TrueType fonts store quadratic Bézier curves, and a run of control points with no on-curve point between them implies an on-curve point at their midpoint. Those implied points are written out below, so every curve is a plain quadratic with one control point and one end point.

Coordinates are in font units (2048 per em) with y pointing up.

In [1]:
import brushcue

# (instruction, control point, end point). "move" and "line" have no control point.
G_INSTRUCTIONS = [
    ("move", None, (789, -20)),
    ("quad", (590, -20), (440, 73)),
    ("quad", (290, 166), (206, 337.5)),
    ("quad", (122, 509), (122, 744)),
    ("quad", (122, 981), (206, 1152.5)),
    ("quad", (290, 1324), (438, 1417)),
    ("quad", (586, 1510), (777, 1510)),
    ("quad", (895, 1510), (997.5, 1475)),
    ("quad", (1100, 1440), (1181, 1375)),
    ("quad", (1262, 1310), (1315.5, 1220.5)),
    ("quad", (1369, 1131), (1389, 1023)),
    ("line", None, (1193, 1023)),
    ("quad", (1170, 1095), (1132, 1152)),
    ("quad", (1094, 1209), (1041.5, 1250)),
    ("quad", (989, 1291), (923, 1312.5)),
    ("quad", (857, 1334), (778, 1334)),
    ("quad", (647, 1334), (540, 1266.5)),
    ("quad", (433, 1199), (370, 1067.5)),
    ("quad", (307, 936), (307, 744)),
    ("quad", (307, 554), (370.5, 423)),
    ("quad", (434, 292), (543, 224)),
    ("quad", (652, 156), (789, 156)),
    ("quad", (915, 156), (1011, 209.5)),
    ("quad", (1107, 263), (1160.5, 361.5)),
    ("quad", (1214, 460), (1214, 594)),
    ("line", None, (1271, 583)),
    ("line", None, (828, 583)),
    ("line", None, (828, 749)),
    ("line", None, (1400, 749)),
    ("line", None, (1400, 586)),
    ("quad", (1400, 401), (1321, 265)),
    ("quad", (1242, 129), (1104.5, 54.5)),
    ("quad", (967, -20), (789, -20)),
]

def to_canvas(point):
    # Fonts use y up; the canvas uses y down.
    return brushcue.Point2f.from_components(point[0], -point[1])

outline = brushcue.Path.new()
for instruction, control, end in G_INSTRUCTIONS:
    if instruction == "move":
        outline = outline.move_to_point(to_canvas(end))
    elif instruction == "line":
        outline = outline.line_to_point(to_canvas(end))
    else:
        outline = outline.quadratic_bezier_to_point(to_canvas(control), to_canvas(end))
In [2]:
def solid(r, g, b, a=1.0):
    return brushcue.ProfiledColor.from_rgba_srgb(brushcue.RGBAColor.from_components(r, g, b, a))

ON_CURVE_COLOR = solid(0.15, 0.15, 0.15)
CONTROL_COLOR = solid(0.12, 0.37, 0.75)
OUTLINE_WIDTH = 6
POINT_DIAMETER = 26

painter = brushcue.Painter.new()

# Dashed lines from each control point to the two on-curve points it sits between.
control_polygon_style = brushcue.RenderStyle.brush_only(
    brushcue.Brush.dashed(solid(0.12, 0.37, 0.75, 0.6), 14, solid(0, 0, 0, 0), 10, 3)
)
previous_end = None
for instruction, control, end in G_INSTRUCTIONS:
    if instruction == "quad":
        control_polygon = (
            brushcue.Path.new()
            .move_to_point(to_canvas(previous_end))
            .line_to_point(to_canvas(control))
            .line_to_point(to_canvas(end))
        )
        painter = painter.add_path_with_render_style(
            control_polygon, control_polygon_style, brushcue.Transform2.identity().to_list()
        )
    previous_end = end

painter = painter.add_path_with_render_style(
    outline,
    brushcue.RenderStyle.brush_only(brushcue.Brush.solid(solid(0.3, 0.3, 0.3), OUTLINE_WIDTH)),
    brushcue.Transform2.identity().to_list(),
)

def add_point(painter, point, color):
    return painter.add_ellipse_with_render_style(
        to_canvas(point),
        brushcue.Vector2f.from_components(POINT_DIAMETER, POINT_DIAMETER),
        0,
        brushcue.RenderStyle.fill_only(brushcue.Fill.solid(color)),
        brushcue.Transform2.identity().to_list(),
    )

for instruction, control, end in G_INSTRUCTIONS:
    if control is not None:
        painter = add_point(painter, control, CONTROL_COLOR)
    painter = add_point(painter, end, ON_CURVE_COLOR)
In [3]:
import os
from pathlib import Path

def find_repo_root():
    for directory in (Path.cwd(), *Path.cwd().parents):
        if (directory / "writing/graphics/fonts").is_dir():
            return directory
    raise FileNotFoundError("Could not find the graphics book directory")

# BrushCue reads files relative to the working directory, so run from the
# monorepo root to load the book font stored in writing/graphics/fonts.
os.chdir(find_repo_root())
ctx = brushcue.Context()

BOOK_FONT = brushcue.Font.from_path("writing/graphics/fonts/LibertinusSerif-Regular.otf")
LABEL_SIZE = 44

def label_string(text, origin, color):
    render_style = brushcue.RenderStyle.fill_only(brushcue.Fill.solid(color))

    @brushcue.brushcue_fn
    def style_label_character(character):
        return brushcue.StyledCharacter.new(character, BOOK_FONT, LABEL_SIZE, render_style)

    characters = brushcue.String(brushcue.input_parsers.parse_string_graph(text)).to_character_list()
    return brushcue.StyledString.styled_string_from_characters_free(
        characters.map(style_label_character),
        brushcue.Point2f.from_components(*origin),
    )

# A legend below the glyph, in the book's font.
LEGEND_Y = 150
LEGEND_ITEMS = (
    (220, "On-curve point", ON_CURVE_COLOR),
    (720, "Control point", CONTROL_COLOR),
)
LABEL_GAP = 24
for x, text, color in LEGEND_ITEMS:
    painter = add_point(painter, (x, -LEGEND_Y), color)
    # Measure the label at the origin so it can be centered beside its point.
    bounds = brushcue.Composition.painter(
        brushcue.Painter.new().add_styled_text(label_string(text, (0, 0), ON_CURVE_COLOR))
    ).bounds().execute(ctx)
    label_x = x + POINT_DIAMETER / 2 + LABEL_GAP - bounds.x
    label_y = LEGEND_Y - bounds.y - bounds.height / 2
    painter = painter.add_styled_text(label_string(text, (label_x, label_y), ON_CURVE_COLOR))

# A transparent rectangle that pads the image so points on the edge are not clipped.
MARGIN = 60
LEFT, RIGHT, TOP, BOTTOM = 122, 1400, -1510, LEGEND_Y + LABEL_SIZE
painter = painter.add_rectangle_with_render_style(
    brushcue.Point2f.from_components((LEFT + RIGHT) / 2, (TOP + BOTTOM) / 2),
    brushcue.Vector2f.from_components(RIGHT - LEFT + 2 * MARGIN, BOTTOM - TOP + 2 * MARGIN),
    0,
    brushcue.RenderStyle.fill_only(brushcue.Fill.solid(solid(1, 1, 1, 0))),
    brushcue.Transform2.identity().to_list(),
)
[wgpu] using backend Vulkan — adapter 'NVIDIA GeForce RTX 5070' (DiscreteGpu), driver 'NVIDIA'
In [4]:
from IPython.display import Image, display

# The working directory is the monorepo root (see the label cell above).
OUTPUT_DIRECTORY = Path("writing/graphics/chapters/font/assets/g-outline")

composition = brushcue.Composition.painter(painter)
image_bytes = composition.execute(ctx).to_image_bytes(ctx)
OUTPUT_DIRECTORY.mkdir(parents=True, exist_ok=True)
(OUTPUT_DIRECTORY / "g_outline.png").write_bytes(image_bytes)
display(Image(image_bytes))
No description has been provided for this image