Source code for excolor.gradient

#!/usr/bin/env python
# -*- coding: utf8 -*-

"""
This module contains functions to manipulate gradients.
"""

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as mc
from matplotlib.colors import ListedColormap, Colormap
from matplotlib.patches import Rectangle
from matplotlib.axes import Axes
from .colortools import lighten, darken
from .colortypes import _get_color_type
from .cmaptools import get_bgcolor
from .utils import interpolate_colors
from typing import Union, Tuple, List, Optional
from PIL import Image

import warnings
warnings.filterwarnings("ignore")


def _get_gradient_colors(c: Union[List[str], str, Colormap], n: int = None) -> List[str]:
    """
    Converts a color string, a cmap, or a list of colors to a list of gradient colors.

    Parameters
    ----------
    c : list of str, str, or matplotlib.colors.Colormap
        Input colors or colormap. Can be:
        - A list of color strings
        - A color name (str)
        - A colormap name (str)
        - A Colormap instance
    n : int or None
        Number of colors to generate. Used if list of colors is not provided explicitly.

    Returns
    -------
    colors : list of str
        List of gradient colors

    Examples
    --------
    >>> colors = get_gradient_colors('gruvbox')
    >>> print(colors)
    """
    if _get_color_type(c) is None and isinstance(c, list):
        colors = c
    else:
        try:
            color = get_bgcolor(c)
        except:
            color = c
        colors = [lighten(color, 0.15), color, darken(color, 0.15), darken(color, 0.3)]
    if n is not None:
        if n <= 1:
            colors = [colors[max(0,1)]]
        else:
            colors = interpolate_colors(colors, n)
    return colors


def _set_gradient_sources(n: int, nx: int, ny: int, angle: float = 0) -> Tuple[np.ndarray, np.ndarray]:
    """
    Sets coordinates of color sources for background gradients in a circular pattern.
    
    Parameters
    ----------
    n : int
        Number of color sources to place around the circle
    nx : int
        Number of pixels along horizontal axis
    ny : int
        Number of pixels along vertical axis
    angle : float, default=0
        Angle = 0 means that first color source is at the right edge of the image.
        The angle increases in math style (counter-clockwise). 

    Returns
    -------
    x : numpy.ndarray
        X-coordinates of sources, centered around the middle of the image
    y : numpy.ndarray
        Y-coordinates of sources, centered around the middle of the image

    Notes
    -----
    The sources are placed on a circle with radius equal to half the diagonal
    of the image. The circle is centered at the middle of the image.
    """
    r = np.sqrt(nx**2 + ny**2) / 2
    phi0 = np.pi * angle / 180.0
    phi = phi0 + np.arange(n) * 2 * np.pi / n
    phi = - phi # reverse the direction to count angle in math style
    z = r * np.exp(1j * phi)
    x = z.real
    y = z.imag
    return x, y


[docs] def show_gradient_sources(c: Union[List[str], str, Colormap], nx: int = 160, ny: int = 90, angle: float = 0) -> None: """ Visualizes the positions of color sources on a background grid. This function creates a plot showing where color sources would be placed for a given set of colors and image dimensions. Parameters ---------- c : list of str, str, or matplotlib.colors.Colormap Input colors or colormap. Can be: - A list of color strings - A color name (str) - A colormap name (str) - A Colormap instance nx : int, default=160 Number of pixels along horizontal axis ny : int, default=90 Number of pixels along vertical axis angle : float, default=0 Angle = 0 means that first color source is at the right edge of the image. The angle increases in math style (counter-clockwise). Returns ------- None Displays a matplotlib figure showing the color source positions Examples -------- >>> show_gradient_sources(['#FF0000', '#00FF00', '#0000FF']) # Shows a plot with three color sources arranged in a circle """ colors = _get_gradient_colors(c) n = len(colors) x, y = _set_gradient_sources(n, nx, ny, angle) x += nx/2 y += ny/2 plt.figure(figsize=(6,5), facecolor="w") r = Rectangle((0,0), nx, ny, edgecolor="k", fill=False) plt.gca().add_patch(r) cmap = ListedColormap(colors, "cmap") plt.scatter(x, y, c=np.arange(n), cmap=cmap, s=100) for i in range(n): dx = min(nx, ny) / 25 plt.text(x[i] + dx, y[i], i + 1) plt.colorbar() plt.gca().invert_yaxis() plt.show() return
[docs] def fill_gradient( c: Union[List[str], str, Colormap], size: Tuple[int, int] = (1280, 720), angle: float = 0, show: Optional[bool] = None, fname: Optional[str] = None ) -> Image.Image: """ Creates a gradient background image based on input colors or colormap. This function generates a smooth gradient background using color sources arranged in a circular pattern. The gradient is created by interpolating between the color sources using inverse distance weighting. Parameters ---------- c : list of str, str, or matplotlib.colors.Colormap Input colors or colormap. Can be: - A list of color strings - A color name (str) - A colormap name (str) - A Colormap instance size : tuple of int, default=(1280, 720) Size of the output image in pixels (width, height) angle : float, default=0 Angle = 0 means that first color source is at the right edge of the image. The angle increases in math style (counter-clockwise). show : bool, optional Whether to display the generated image using matplotlib. If None, will show only if fname is None fname : str, optional If provided, saves the image to the specified file path. Supported formats: .png, .jpg, .svg Returns ------- PIL.Image.Image The generated gradient background image in RGBA format Notes ----- The function: 1. Processes input colors to get a suitable palette 2. Places color sources in a circular pattern 3. Creates a gradient by interpolating between sources 4. Optionally displays or saves the result Examples -------- >>> img = background_gradient('viridis', size=(800, 600)) >>> img.show() """ epsilon = 1e-5 # If only one color is given, expand it to darker and lighter colors = _get_gradient_colors(c) nx, ny = size n = len(colors) # Set color source coordinates x0, y0 = _set_gradient_sources(n, nx, ny, angle) a0 = np.arctan2(y0, x0) r0 = np.sqrt(x0**2 + y0**2) # Set pixel coordinates x = np.arange(nx) - (nx - 1) / 2 y = np.arange(ny) - (ny - 1) / 2 x, y = np.meshgrid(x, y, indexing="xy") x, y = x.flatten(), y.flatten() a = np.arctan2(y, x) r = np.sqrt(x**2 + y**2) # Iterate through color sources dist = [] for i in range(n): a_ = a - a0[i] r_ = r * np.cos(a_) d = r0[i] - r_ dist.append(d) d = np.stack(dist) f = np.power(d + epsilon, -1.2) f = f / np.sum(f, axis=0) # Calc color fractions rgb = np.stack([mc.to_rgb(color) for color in colors]).T rgba = [np.dot(rgb[i], f) for i in range(3)] + [np.ones(nx * ny)] rgba = np.stack([rgba[i].reshape(ny,nx) for i in range(4)], axis=2) rgba = np.clip(255 * rgba, 0, 255).astype(np.uint8) img = Image.fromarray(rgba, "RGBA") if fname is not None: if len(fname) < 5 or fname[-4:] not in [".png", ".jpg", ".svg"]: fname = f"{fname}.png" img.save(fname) if show is None and fname is None: show = True if show == True: plt.figure(facecolor="#00000000") plt.imshow(img) plt.gca().set_axis_off() plt.show() plt.close() return img
import types __all__ = [name for name, thing in globals().items() if not (name.startswith("_") or isinstance(thing, types.ModuleType))] del types