Skip to content

Geometry

Module: terminaltexteffects.utils.geometry

Balanced Grids

New in 0.16.0

find_balanced_grid() and GridLayout are available on development main and are not in the published 0.15.0 package.

find_balanced_grid(left, bottom, right, top) returns a GridLayout covering inclusive bounds. It accounts for terminal character height when choosing row and column counts, then divides each axis so cell sizes differ by at most one position. target_cells, min_cell_width, and min_cell_height control the preferred size.

The layout's column_boundaries and row_boundaries include an exclusive final boundary, one beyond the canvas edge. Each cell includes its lower boundaries and excludes its upper boundaries. For example, an 80-column by 24-row canvas produces five columns of cells and three rows, with cells measuring 16 by 8.

Utility functions for geometric calculations and operations.

The purpose of these functions is to find terminal coordinates that fall within certain regions or along certain paths. These functions are used by effects to enable more complex animations and movement paths.

Functions:

Name Description
find_balanced_grid

Divides inclusive bounds into nearly square cells of balanced size.

find_coords_on_circle

Finds points on a circle given the origin, radius, and number of points.

find_coords_in_circle

Finds coordinates within a terminal-adjusted circle given its center and radius.

find_coords_in_rect

Finds coordinates within a rectangle given the origin and distance.

rasterize_line

Rasterizes a thin line between two terminal coordinates.

rasterize_supercover_line

Rasterizes every terminal cell touched by a line.

extrapolate_along_ray

Finds the coordinate past a target along the ray from an origin.

find_coord_on_bezier_curve

Evaluates a Bézier curve at a parameter value.

interpolate_coord

Linearly interpolates or extrapolates between two coordinates.

find_coord_on_line

Compatibility alias for interpolate_coord.

find_length_of_bezier_curve

Approximates the length of a Bézier curve of any degree.

find_length_of_line

Finds the length of a line intersecting two coordinates.

find_normalized_distance_from_center

Returns the normalized distance from the center of the Canvas.

Constants

TERMINAL_ROW_SCALE: Approximate terminal-cell height relative to its width.

TERMINAL_ROW_SCALE = 2 module-attribute

Approximate terminal-cell height relative to its width.

extrapolate_along_ray = functools.wraps(extrapolate_along_ray)(functools.lru_cache(maxsize=8192)(extrapolate_along_ray)) module-attribute

Return the point offset_from_target units past target along the origin -> target ray.

A positive offset continues past target away from origin, while a negative offset moves back toward and potentially past origin. If origin and target coincide, the direction is undefined and target is returned.

Parameters:

Name Type Description Default
origin Coord

origin coordinate (a)

required
target Coord

target coordinate (b)

required
offset_from_target float

Signed distance from the target coordinate (b).

required
terminal_adjusted bool

Whether distance accounts for terminal cell height. Defaults to True.

required

Returns:

Name Type Description
Coord Coord

Coordinate at the given distance (c).

find_coord_on_bezier_curve = functools.wraps(find_coord_on_bezier_curve)(functools.lru_cache(maxsize=16384)(find_coord_on_bezier_curve)) module-attribute

Evaluate a Bézier curve of any degree at parameter t.

Values of t between 0 and 1 evaluate the curve from start to end. Values outside that interval intentionally extrapolate beyond the curve endpoints. An empty control-point tuple describes a linear Bézier curve.

Parameters:

Name Type Description Default
start Coord

The starting coordinate of the curve.

required
control tuple[Coord, ...] | Coord

A single control point or tuple of control points.

required
end Coord

The ending coordinate of the curve.

required
t float

The unrestricted Bézier parameter.

required

Returns:

Name Type Description
Coord Coord

The rounded coordinate at t.

find_coord_on_line = interpolate_coord module-attribute

Compatibility alias for interpolate_coord.

find_coords_in_circle = _cache_coordinate_list(_CIRCLE_AREA_CACHE_SIZE)(find_coords_in_circle) module-attribute

Find coordinates within a terminal-adjusted circle.

The generated coordinate-space ellipse has a horizontal radius of radius columns and a vertical radius of radius / TERMINAL_ROW_SCALE rows. With terminal cells approximately TERMINAL_ROW_SCALE times as tall as they are wide, this ellipse appears circular. A radius of 0 returns only center.

Parameters:

Name Type Description Default
center Coord

The center coordinate of the circle.

required
radius int

The terminal-adjusted circle radius, measured in column-distance units.

required

Returns:

Type Description
list[Coord]

list[Coord]: A list of coordinates within the circle.

Raises:

Type Description
ValueError

If radius is negative.

find_coords_in_rect = _cache_coordinate_list(_COORDINATE_LIST_CACHE_SIZE)(find_coords_in_rect) module-attribute

Find coords that fall within a rectangle.

Distance specifies the number of units in each direction from the origin. The resulting rectangle has width and height 2 * distance + 1, so a distance of 0 returns only origin.

Parameters:

Name Type Description Default
origin Coord

center of the rectangle

required
distance int

distance from the origin

required

Returns:

Type Description
list[Coord]

list[Coord]: list of Coord points in the rectangle

Raises:

Type Description
ValueError

If distance is negative.

find_coords_on_circle = _cache_coordinate_list(_COORDINATE_LIST_CACHE_SIZE)(find_coords_on_circle) module-attribute

Find points on a terminal-adjusted circle.

The generated coordinate-space ellipse has a horizontal radius of radius columns and a vertical radius of radius // TERMINAL_ROW_SCALE rows. With terminal cells approximately TERMINAL_ROW_SCALE times as tall as they are wide, this ellipse appears circular. A radius of 0 returns only origin.

Parameters:

Name Type Description Default
origin Coord

origin of the circle

required
radius int

terminal-adjusted circle radius, measured in column-distance units

required
coords_limit int

limit the number of coords returned, if 0, the number of points is calculated based on the circumference of the circle

required
unique bool

whether to remove duplicate points. Defaults to True.

required

Returns:

Name Type Description
list Coord

list of Coord points on the circle

Raises:

Type Description
ValueError

If radius or coords_limit is negative.

find_coords_on_rect = _cache_coordinate_list(_COORDINATE_LIST_CACHE_SIZE)(find_coords_on_rect) module-attribute

Find coords on the perimeter of a rectangle.

Half width and half height specify the distance in each direction from the origin. Returns coordinates that fall on the perimeter (edges) of the rectangle only. If one half-dimension is 0, the rectangle collapses to a vertical or horizontal line. If both are 0, only origin is returned.

Parameters:

Name Type Description Default
origin Coord

center of the rectangle

required
half_width int

half the width of the rectangle

required
half_height int

half the height of the rectangle

required

Returns:

Type Description
list[Coord]

list[Coord]: list of Coord points in the rectangle

Raises:

Type Description
ValueError

If half_width or half_height is negative.

find_length_of_bezier_curve = functools.wraps(find_length_of_bezier_curve)(functools.lru_cache(maxsize=4096)(find_length_of_bezier_curve)) module-attribute

Approximate the length of a Bézier curve of any degree.

The curve length is calculated from unrounded floating-point points using adaptive subdivision. A single Coord or a tuple of control points is accepted, matching find_coord_on_bezier_curve; an empty tuple describes a linear Bézier curve. By default, row distances are scaled according to the terminal character aspect ratio.

Parameters:

Name Type Description Default
start Coord

The starting coordinate of the curve.

required
control tuple[Coord, ...] | Coord

A single control point or tuple of control points.

required
end Coord

The ending coordinate of the curve.

required
terminal_adjusted bool

Whether distance accounts for terminal cell height. Defaults to True.

required

Returns:

Name Type Description
float float

The approximate length of the Bézier curve.

find_length_of_line = functools.wraps(find_length_of_line)(functools.lru_cache(maxsize=8192)(find_length_of_line)) module-attribute

Return the distance between two coordinates.

By default, row distances are scaled according to the terminal character aspect ratio.

Parameters:

Name Type Description Default
coord1 Coord

first coordinate.

required
coord2 Coord

second coordinate.

required
terminal_adjusted bool

Whether distance accounts for terminal cell height. Defaults to True.

required

Returns:

Name Type Description
float float

length of the line

find_normalized_distance_from_center = functools.wraps(find_normalized_distance_from_center)(functools.lru_cache(maxsize=8192)(find_normalized_distance_from_center)) module-attribute

Return the normalized distance from the center of a rectangle on the Canvas as a float between 0 and 1.

The distance is calculated using the Pythagorean theorem and accounts for the aspect ratio of the terminal.

Parameters:

Name Type Description Default
bottom int

Bottom row of the rectangle on the Canvas.

required
top int

Top row of the rectangle on the Canvas.

required
left int

Left column of the rectangle on the Canvas.

required
right int

Right column of the rectangle on the Canvas.

required
other_coord Coord

Other coordinate from which to calculate the distance to the center of the rectangle.

required

Returns:

Name Type Description
float float

Normalized distance from the center of the rectangle on the Canvas, float between 0 and 1.

interpolate_coord = functools.wraps(interpolate_coord)(functools.lru_cache(maxsize=16384)(interpolate_coord)) module-attribute

Linearly interpolate or extrapolate between two coordinates.

A value of 0 returns start, 1 returns end, and values outside the inclusive interval [0, 1] intentionally extrapolate beyond the endpoints.

Parameters:

Name Type Description Default
start Coord

The starting coordinate of the line.

required
end Coord

The ending coordinate of the line.

required
t float

The unrestricted linear interpolation parameter.

required

Returns:

Name Type Description
Coord Coord

The rounded coordinate at t.

rasterize_line = _cache_coordinate_list(_COORDINATE_LIST_CACHE_SIZE)(rasterize_line) module-attribute

Return a thin, one-cell-wide rasterization from start to end.

Bresenham's line algorithm selects one terminal-grid coordinate at each step along the dominant axis. Both endpoints are included, coordinates are unique and ordered from start to end, and reversing the endpoints reverses the result. Coincident endpoints return a one-coordinate list. Terminal aspect-ratio scaling is not applied.

Parameters:

Name Type Description Default
start Coord

The first endpoint.

required
end Coord

The second endpoint.

required

Returns:

Type Description
list[Coord]

list[Coord]: Ordered coordinates forming the rasterized line.

rasterize_supercover_line = _cache_coordinate_list(_COORDINATE_LIST_CACHE_SIZE)(rasterize_supercover_line) module-attribute

Return every terminal-grid cell touched by the line from start to end.

Both endpoints are included, coordinates are unique and ordered from start to end, and reversing the endpoints reverses the result. When the line passes exactly through a grid corner, both adjacent cells are included to preserve complete coverage. Coincident endpoints return a one-coordinate list. Terminal aspect-ratio scaling is not applied.

Parameters:

Name Type Description Default
start Coord

The first endpoint.

required
end Coord

The second endpoint.

required

Returns:

Type Description
list[Coord]

list[Coord]: Ordered coordinates forming the supercover line.

Coord dataclass

A coordinate with row and column values.

Parameters:

Name Type Description Default
column int

column value

required
row int

row value

required
Source code in terminaltexteffects/utils/geometry.py
@dataclass(eq=True, frozen=True)
class Coord:
    """A coordinate with row and column values.

    Args:
        column (int): column value
        row (int): row value

    """

    def __iter__(self) -> Iterator[int]:
        """Allow tuple unpacking by yielding the column and row.

        Yields:
            column, row: yield the column, followed by the row

        """
        yield self.column
        yield self.row

    column: int
    row: int

__iter__()

Allow tuple unpacking by yielding the column and row.

Yields:

Type Description
int

column, row: yield the column, followed by the row

Source code in terminaltexteffects/utils/geometry.py
def __iter__(self) -> Iterator[int]:
    """Allow tuple unpacking by yielding the column and row.

    Yields:
        column, row: yield the column, followed by the row

    """
    yield self.column
    yield self.row

GridLayout dataclass

Describe rectangular cells using increasing column and row boundaries.

Each cell includes its lower boundaries and excludes its upper boundaries. The last boundary is therefore one beyond the inclusive canvas edge.

Attributes:

Name Type Description
column_boundaries tuple[int, ...]

Left-to-right column boundaries.

row_boundaries tuple[int, ...]

Bottom-to-top row boundaries.

Source code in terminaltexteffects/utils/geometry.py
@dataclass(frozen=True)
class GridLayout:
    """Describe rectangular cells using increasing column and row boundaries.

    Each cell includes its lower boundaries and excludes its upper boundaries.
    The last boundary is therefore one beyond the inclusive canvas edge.

    Attributes:
        column_boundaries (tuple[int, ...]): Left-to-right column boundaries.
        row_boundaries (tuple[int, ...]): Bottom-to-top row boundaries.

    """

    column_boundaries: tuple[int, ...]
    row_boundaries: tuple[int, ...]

    def __post_init__(self) -> None:
        """Reject boundaries that would produce empty or overlapping cells."""
        for boundaries in (self.column_boundaries, self.row_boundaries):
            if (
                not isinstance(boundaries, tuple)
                or len(boundaries) < 2
                or any(type(boundary) is not int for boundary in boundaries)
                or any(left >= right for left, right in zip(boundaries, boundaries[1:]))
            ):
                msg = "Grid boundaries must be tuples of at least two strictly increasing integers."
                raise ValueError(msg)

__post_init__()

Reject boundaries that would produce empty or overlapping cells.

Source code in terminaltexteffects/utils/geometry.py
def __post_init__(self) -> None:
    """Reject boundaries that would produce empty or overlapping cells."""
    for boundaries in (self.column_boundaries, self.row_boundaries):
        if (
            not isinstance(boundaries, tuple)
            or len(boundaries) < 2
            or any(type(boundary) is not int for boundary in boundaries)
            or any(left >= right for left, right in zip(boundaries, boundaries[1:]))
        ):
            msg = "Grid boundaries must be tuples of at least two strictly increasing integers."
            raise ValueError(msg)

find_balanced_grid(left, bottom, right, top, *, target_cells=5, min_cell_width=4, min_cell_height=2)

Divide inclusive bounds into balanced, approximately square terminal cells.

Aim for target_cells along the longer visual axis, reducing the target cell size for narrow canvases. Compare nearby row and column counts for squareness and proximity to that size, using TERMINAL_ROW_SCALE. Cell widths and heights each differ by at most one position. Minimum sizes limit subdivision; a smaller canvas retains one cell along that axis.

Parameters:

Name Type Description Default
left int

Inclusive left column.

required
bottom int

Inclusive bottom row.

required
right int

Inclusive right column.

required
top int

Inclusive top row.

required
target_cells int

Preferred cell count along the longer visual axis.

5
min_cell_width int

Minimum columns per cell when space permits.

4
min_cell_height int

Minimum rows per cell when space permits.

2

Returns:

Name Type Description
GridLayout GridLayout

Boundaries covering the complete rectangle.

Raises:

Type Description
ValueError

If bounds are not ordered integers or sizing options are not positive integers.

Source code in terminaltexteffects/utils/geometry.py
def find_balanced_grid(
    left: int,
    bottom: int,
    right: int,
    top: int,
    *,
    target_cells: int = 5,
    min_cell_width: int = 4,
    min_cell_height: int = 2,
) -> GridLayout:
    """Divide inclusive bounds into balanced, approximately square terminal cells.

    Aim for `target_cells` along the longer visual axis, reducing the target
    cell size for narrow canvases. Compare nearby row and column counts for
    squareness and proximity to that size, using `TERMINAL_ROW_SCALE`.
    Cell widths and heights each differ by at most one position. Minimum sizes
    limit subdivision; a smaller canvas retains one cell along that axis.

    Args:
        left (int): Inclusive left column.
        bottom (int): Inclusive bottom row.
        right (int): Inclusive right column.
        top (int): Inclusive top row.
        target_cells (int): Preferred cell count along the longer visual axis.
        min_cell_width (int): Minimum columns per cell when space permits.
        min_cell_height (int): Minimum rows per cell when space permits.

    Returns:
        GridLayout: Boundaries covering the complete rectangle.

    Raises:
        ValueError: If bounds are not ordered integers or sizing options are not positive integers.

    """
    if any(type(bound) is not int for bound in (left, bottom, right, top)) or right < left or top < bottom:
        msg = "Grid bounds must be ordered integers."
        raise ValueError(msg)
    for name, value in (
        ("target_cells", target_cells),
        ("min_cell_width", min_cell_width),
        ("min_cell_height", min_cell_height),
    ):
        if type(value) is not int or value <= 0:
            msg = f"{name} must be a positive integer."
            raise ValueError(msg)
    width, height = right - left + 1, top - bottom + 1
    visual_height = height * TERMINAL_ROW_SCALE
    target_size = max(
        min_cell_width,
        min_cell_height * TERMINAL_ROW_SCALE,
        min(max(width, visual_height) / target_cells, width, visual_height),
    )

    def candidates(length: int, visual_length: int, minimum: int) -> set[int]:
        maximum = max(1, length // minimum)
        ideal = visual_length / target_size
        return {max(1, min(maximum, count)) for count in range(math.floor(ideal) - 1, math.ceil(ideal) + 2)}

    def score(counts: tuple[int, int]) -> tuple[float, int, int]:
        columns, rows = counts
        cell_width, cell_height = width / columns, visual_height / rows
        shape_error = abs(math.log(cell_width / cell_height))
        size_error = abs(math.log(cell_width / target_size)) + abs(math.log(cell_height / target_size))
        return shape_error + size_error / 4, abs(max(columns, rows) - target_cells), columns * rows

    columns, rows = min(
        (
            (columns, rows)
            for columns in sorted(candidates(width, width, min_cell_width))
            for rows in sorted(candidates(height, visual_height, min_cell_height))
        ),
        key=score,
    )
    return GridLayout(
        tuple(left + index * width // columns for index in range(columns + 1)),
        tuple(bottom + index * height // rows for index in range(rows + 1)),
    )