Skip to content

Gradient

Module: terminaltexteffects.utils.graphics

Basic Usage

from terminaltexteffects.utils.graphics import Gradient, Color

rgb = Gradient(Color("ff0000"), Color("00ff00"), Color("0000ff"), steps=5)
for color in rgb:
    # color is a hex string
    ...

Printing Gradients

Gradients can be printed to the terminal to show information about the stops, steps, and resulting spectrum.

t


Gradient Reference

A Gradient is a list of RGB hex color strings transitioning from one color to another. The gradient color list is calculated using linear interpolation based on the provided start and end colors and the number of steps. Gradients can be iterated over to get the next color in the gradient color list. If there is only one color in the stops list, the gradient will be a list of the same color.

If multiple steps are given, the gradient between pairs of colors will be equal to the number of steps for the pair based on the order of stops and steps.

Ex: stops = ("ffffff", "aaaaaa", "000000"), steps = (6, 3)

"fffffff" -> (6 steps) -> "aaaaaa" -> (3 steps) -> "000000"

The step count includes the stop for each pair. Total number of colors in the resulting gradient spectrum is the sum of the steps between each pair of stops plus 1.

Attributes:

Name Type Description
spectrum list[str]

List (length=sum(steps) + 1) of RGB hex color strings

Source code in terminaltexteffects/utils/graphics.py
class Gradient:
    """A Gradient is a list of RGB hex color strings transitioning from one color to another. The gradient color
    list is calculated using linear interpolation based on the provided start and end colors and the number of steps. Gradients
    can be iterated over to get the next color in the gradient color list. If there is only one color in the stops list,
    the gradient will be a list of the same color.

    If multiple steps are given, the gradient between pairs of colors will be equal to the number of steps for the pair
    based on the order of stops and steps.

    Ex: stops = ("ffffff", "aaaaaa", "000000"), steps = (6, 3)

    "fffffff" -> (6 steps) -> "aaaaaa" -> (3 steps) -> "000000"

    The step count includes the stop for each pair. Total number of colors in the resulting gradient spectrum is the sum of the steps between
    each pair of stops plus 1.

    Attributes:
        spectrum (list[str]): List (length=sum(steps) + 1) of RGB hex color strings

    """

    class Direction(Enum):
        """Enum for specifying the direction of the gradient."""

        VERTICAL = auto()
        HORIZONTAL = auto()
        RADIAL = auto()
        DIAGONAL = auto()

    def __init__(self, *stops: Color, steps: tuple[int, ...] | int = 1, loop=False) -> None:
        """
        Initializes a Graphics object.

        Args:
            stops (Color): One ore more variables of type Color representing the color stops.
            steps (int | tuple[int, ...], optional): Number of steps or a tuple of step values for generating the
                spectrum. Defaults to 1.
            loop (bool, optional): Loop the gradient. This causes the final gradient color to transition back to the first gradient color. Defaults to False.

        Raises:
            ValueError: If no color stops are provided.

        Attributes:
            _stops (tuple[Color]): Tuple of Color objects representing the color stops.
            _steps (int | tuple[int, ...]): Number of steps or a tuple of step values for generating the spectrum.
            _loop (bool): Loop the gradient. This causes the final gradient color to transition back to the first gradient color.
            spectrum (list[str]): List of strings representing the generated spectrum.
            _index (int): Current index of the spectrum.

        Returns:
            None
        """
        self._stops = stops
        if len(self._stops) < 1:
            raise ValueError("At least one stop must be provided.")
        self._steps = steps
        self._loop = loop
        self.spectrum: list[Color] = self._generate(self._steps)
        self._index: int = 0

    def get_color_at_fraction(self, fraction: float) -> Color:
        """Returns the color at a fraction of the gradient.

        Args:
            fraction (float): The fraction of the gradient to get the color for.

        Returns:
            Color: The color at the fraction of the gradient.
        """
        if fraction < 0 or fraction > 1:
            raise ValueError("Fraction must be 0 <= fraction <= 1.")
        index = round(fraction * (len(self.spectrum) - 1))
        return self.spectrum[index]

    def _generate(self, steps) -> list[Color]:
        """Calculate a gradient of colors between two colors using linear interpolation. If
        there is only one color in the stops tuple, the gradient will be a list of the same color.

        If multiple steps are given, the gradient between pairs of colors will be equal to the number of steps for the pair
        based on the order of stops and steps.

        Ex: stops = ("ffffff", "aaaaaa", "000000"), steps = (6, 3)
        Distance from "ffffff" to "aaaaaa" = 6 steps (7 colors including start and end)
        Distance from "aaaaaa" to "000000" = 3 steps (4 colors including start and end)
        Total colors in the gradient spectrum = 10 ("aaaaaa" is not repeated when transitioning from "ffffff" to "aaaaaa" and from "aaaaaa" to "000000")


        The step count includes the stop for each pair. Total number of colors in the resulting gradient spectrum:
        sum(steps) + 1

        Returns:
            list[str]: List (length=sum(steps) + 1) of RGB hex color strings. The first and last colors are the start and end stops, respectively.
        """
        if isinstance(steps, int):
            steps = (steps,)
            for step in steps:
                if step < 1:
                    raise ValueError("Steps must be greater than 0.")
        spectrum: list[Color] = []
        if len(self._stops) == 1:
            color = self._stops[0]
            for _ in range(steps[0]):
                spectrum.append(color)
            return spectrum
        if self._loop:
            self._stops = self._stops + (self._stops[0],)
        a, b = itertools.tee(self._stops)
        next(b, None)
        color_pairs = list(zip(a, b))
        # color_pairs = list(itertools.pairwise(self._stops))
        steps = steps[: len(color_pairs)]
        color_pair: tuple[Color, Color]
        for color_pair, steps in itertools.zip_longest(color_pairs, steps, fillvalue=steps[-1]):
            start, end = color_pair
            start_color_ints = start.rgb_ints
            end_color_ints = end.rgb_ints
            # Initialize an empty list to store the gradient colors
            gradient_colors: list[Color] = []
            # Calculate the color deltas for each RGB value
            red_delta = (end_color_ints[0] - start_color_ints[0]) // steps
            green_delta = (end_color_ints[1] - start_color_ints[1]) // steps
            blue_delta = (end_color_ints[2] - start_color_ints[2]) // steps
            # Calculate the intermediate colors and add them to the gradient colors list
            range_start = int(len(spectrum) > 0)  # if this is the first pair, add the start color to the spectrum
            for i in range(range_start, max(steps, 0)):
                red = start_color_ints[0] + (red_delta * i)
                green = start_color_ints[1] + (green_delta * i)
                blue = start_color_ints[2] + (blue_delta * i)

                # Ensure that the RGB values are within the valid range of 0-255
                red = max(0, min(red, 255))
                green = max(0, min(green, 255))
                blue = max(0, min(blue, 255))

                # Convert the RGB values to a hex color string and add it to the gradient colors list
                gradient_colors.append(Color(f"{red:02x}{green:02x}{blue:02x}"))
            # Add the end color to the gradient colors list
            gradient_colors.append(end)
            spectrum.extend(gradient_colors)
        return spectrum

    def build_coordinate_color_mapping(
        self, max_row: int, max_column: int, direction: "Gradient.Direction"
    ) -> dict[geometry.Coord, Color]:
        """Builds a mapping of coordinates to colors based on the gradient and a direction.

        For example, a vertical gradient will have the same color for each column in a row. When applied across all characters in the canvas, the gradient will be visible as a vertical gradient.

        Args:
            max_row (int): The maximum row value.
            max_column (int): The maximum column value.
            direction (Gradient.Direction): The direction of the gradient.

        Returns:
            dict[Coord, str]: A mapping of coordinates to colors.
        """
        gradient_mapping: dict[geometry.Coord, Color] = {}
        if direction == Gradient.Direction.VERTICAL:
            for row_value in range(max_row + 1):
                if max_row == 0:
                    fraction = 1.0
                else:
                    fraction = row_value / max_row
                color = self.get_color_at_fraction(fraction)
                for column_value in range(1, max_column + 1):
                    gradient_mapping[geometry.Coord(column_value, row_value)] = color
        elif direction == Gradient.Direction.HORIZONTAL:
            for column_value in range(1, max_column + 1):
                if max_column == 0:
                    fraction = 1.0
                else:
                    fraction = column_value / max_column
                color = self.get_color_at_fraction(fraction)
                for row_value in range(max_row + 1):
                    gradient_mapping[geometry.Coord(column_value, row_value)] = color
        elif direction == Gradient.Direction.RADIAL:
            for row_value in range(max_row + 1):
                for column_value in range(1, max_column + 1):
                    distance_from_center = geometry.find_normalized_distance_from_center(
                        max_row, max_column, geometry.Coord(column_value, row_value)
                    )
                    color = self.get_color_at_fraction(distance_from_center)
                    gradient_mapping[geometry.Coord(column_value, row_value)] = color
        elif direction == Gradient.Direction.DIAGONAL:
            for row_value in range(max_row + 1):
                for column_value in range(1, max_column + 1):
                    if max_row == 0 or max_column == 0:
                        fraction = 1.0
                    else:
                        fraction = ((row_value * 2) + column_value) / ((max_row * 2) + max_column)
                    color = self.get_color_at_fraction(fraction)
                    gradient_mapping[geometry.Coord(column_value, row_value)] = color

        return gradient_mapping

    def __iter__(self) -> Iterator[Color]:
        yield from self.spectrum

    def __len__(self) -> int:
        return len(self.spectrum)

    @typing.overload
    def __getitem__(self, index: int) -> Color: ...

    @typing.overload
    def __getitem__(self, index: slice) -> list[Color]: ...

    def __getitem__(self, index: int | slice) -> Color | list[Color]:
        if isinstance(index, int):
            if index < 0 or index >= len(self.spectrum):
                raise IndexError("Index out of range.")
            return self.spectrum[index]
        elif isinstance(index, slice):
            start, stop, step = index.indices(len(self.spectrum))
            return [self.spectrum[i] for i in range(start, stop, step)]
        else:
            raise TypeError("Index must be an integer or a slice.")

    def __str__(self) -> str:
        color_blocks = [f"{colorterm.fg(color.rgb_color)}{ansitools.RESET_ALL()}" for color in self.spectrum]
        return f"Gradient: Stops({', '.join(c.rgb_color for c in self._stops)}), Steps({self._steps})\n" + "".join(
            color_blocks
        )

Direction

Bases: Enum

Enum for specifying the direction of the gradient.

Source code in terminaltexteffects/utils/graphics.py
class Direction(Enum):
    """Enum for specifying the direction of the gradient."""

    VERTICAL = auto()
    HORIZONTAL = auto()
    RADIAL = auto()
    DIAGONAL = auto()

__init__(*stops, steps=1, loop=False)

Initializes a Graphics object.

Parameters:

Name Type Description Default
stops Color

One ore more variables of type Color representing the color stops.

()
steps int | tuple[int, ...]

Number of steps or a tuple of step values for generating the spectrum. Defaults to 1.

1
loop bool

Loop the gradient. This causes the final gradient color to transition back to the first gradient color. Defaults to False.

False

Raises:

Type Description
ValueError

If no color stops are provided.

Attributes:

Name Type Description
_stops tuple[Color]

Tuple of Color objects representing the color stops.

_steps int | tuple[int, ...]

Number of steps or a tuple of step values for generating the spectrum.

_loop bool

Loop the gradient. This causes the final gradient color to transition back to the first gradient color.

spectrum list[str]

List of strings representing the generated spectrum.

_index int

Current index of the spectrum.

Returns:

Type Description
None

None

Source code in terminaltexteffects/utils/graphics.py
def __init__(self, *stops: Color, steps: tuple[int, ...] | int = 1, loop=False) -> None:
    """
    Initializes a Graphics object.

    Args:
        stops (Color): One ore more variables of type Color representing the color stops.
        steps (int | tuple[int, ...], optional): Number of steps or a tuple of step values for generating the
            spectrum. Defaults to 1.
        loop (bool, optional): Loop the gradient. This causes the final gradient color to transition back to the first gradient color. Defaults to False.

    Raises:
        ValueError: If no color stops are provided.

    Attributes:
        _stops (tuple[Color]): Tuple of Color objects representing the color stops.
        _steps (int | tuple[int, ...]): Number of steps or a tuple of step values for generating the spectrum.
        _loop (bool): Loop the gradient. This causes the final gradient color to transition back to the first gradient color.
        spectrum (list[str]): List of strings representing the generated spectrum.
        _index (int): Current index of the spectrum.

    Returns:
        None
    """
    self._stops = stops
    if len(self._stops) < 1:
        raise ValueError("At least one stop must be provided.")
    self._steps = steps
    self._loop = loop
    self.spectrum: list[Color] = self._generate(self._steps)
    self._index: int = 0

build_coordinate_color_mapping(max_row, max_column, direction)

Builds a mapping of coordinates to colors based on the gradient and a direction.

For example, a vertical gradient will have the same color for each column in a row. When applied across all characters in the canvas, the gradient will be visible as a vertical gradient.

Parameters:

Name Type Description Default
max_row int

The maximum row value.

required
max_column int

The maximum column value.

required
direction Direction

The direction of the gradient.

required

Returns:

Type Description
dict[Coord, Color]

dict[Coord, str]: A mapping of coordinates to colors.

Source code in terminaltexteffects/utils/graphics.py
def build_coordinate_color_mapping(
    self, max_row: int, max_column: int, direction: "Gradient.Direction"
) -> dict[geometry.Coord, Color]:
    """Builds a mapping of coordinates to colors based on the gradient and a direction.

    For example, a vertical gradient will have the same color for each column in a row. When applied across all characters in the canvas, the gradient will be visible as a vertical gradient.

    Args:
        max_row (int): The maximum row value.
        max_column (int): The maximum column value.
        direction (Gradient.Direction): The direction of the gradient.

    Returns:
        dict[Coord, str]: A mapping of coordinates to colors.
    """
    gradient_mapping: dict[geometry.Coord, Color] = {}
    if direction == Gradient.Direction.VERTICAL:
        for row_value in range(max_row + 1):
            if max_row == 0:
                fraction = 1.0
            else:
                fraction = row_value / max_row
            color = self.get_color_at_fraction(fraction)
            for column_value in range(1, max_column + 1):
                gradient_mapping[geometry.Coord(column_value, row_value)] = color
    elif direction == Gradient.Direction.HORIZONTAL:
        for column_value in range(1, max_column + 1):
            if max_column == 0:
                fraction = 1.0
            else:
                fraction = column_value / max_column
            color = self.get_color_at_fraction(fraction)
            for row_value in range(max_row + 1):
                gradient_mapping[geometry.Coord(column_value, row_value)] = color
    elif direction == Gradient.Direction.RADIAL:
        for row_value in range(max_row + 1):
            for column_value in range(1, max_column + 1):
                distance_from_center = geometry.find_normalized_distance_from_center(
                    max_row, max_column, geometry.Coord(column_value, row_value)
                )
                color = self.get_color_at_fraction(distance_from_center)
                gradient_mapping[geometry.Coord(column_value, row_value)] = color
    elif direction == Gradient.Direction.DIAGONAL:
        for row_value in range(max_row + 1):
            for column_value in range(1, max_column + 1):
                if max_row == 0 or max_column == 0:
                    fraction = 1.0
                else:
                    fraction = ((row_value * 2) + column_value) / ((max_row * 2) + max_column)
                color = self.get_color_at_fraction(fraction)
                gradient_mapping[geometry.Coord(column_value, row_value)] = color

    return gradient_mapping

get_color_at_fraction(fraction)

Returns the color at a fraction of the gradient.

Parameters:

Name Type Description Default
fraction float

The fraction of the gradient to get the color for.

required

Returns:

Name Type Description
Color Color

The color at the fraction of the gradient.

Source code in terminaltexteffects/utils/graphics.py
def get_color_at_fraction(self, fraction: float) -> Color:
    """Returns the color at a fraction of the gradient.

    Args:
        fraction (float): The fraction of the gradient to get the color for.

    Returns:
        Color: The color at the fraction of the gradient.
    """
    if fraction < 0 or fraction > 1:
        raise ValueError("Fraction must be 0 <= fraction <= 1.")
    index = round(fraction * (len(self.spectrum) - 1))
    return self.spectrum[index]