Source code for manim.utils.color.core

"""Manim's (internal) color data structure and some utilities for color conversion.

This module contains the implementation of :class:`.ManimColor`, the data structure
internally used to represent colors.

The preferred way of using these colors is by importing their constants from Manim:

.. code-block:: pycon

    >>> from manim import RED, GREEN, BLUE
    >>> print(RED)
    #FC6255

Note that this way uses the name of the colors in UPPERCASE.

.. note::

    The colors with a ``_C`` suffix have an alias equal to the colorname without a
    letter. For example, ``GREEN = GREEN_C``.

===================
Custom Color Spaces
===================

Hello, dear visitor. You seem to be interested in implementing a custom color class for
a color space we don't currently support.

The current system is using a few indirections for ensuring a consistent behavior with
all other color types in Manim.

To implement a custom color space, you must subclass :class:`ManimColor` and implement
three important methods:

- :attr:`~.ManimColor._internal_value`: a ``@property`` implemented on
  :class:`ManimColor` with the goal of keeping a consistent internal representation
  which can be referenced by other functions in :class:`ManimColor`. This property acts
  as a proxy to whatever representation you need in your class.

  - The getter should always return a NumPy array in the format ``[r,g,b,a]``, in
    accordance with the type :class:`ManimColorInternal`.

  - The setter should always accept a value in the format ``[r,g,b,a]`` which can be
    converted to whatever attributes you need.

- :attr:`~ManimColor._internal_space`: a read-only ``@property`` implemented on
  :class:`ManimColor` with the goal of providing a useful representation which can be
  used by operators, interpolation and color transform functions.

  The only constraints on this value are:

  - It must be a NumPy array.

  - The last value must be the opacity in a range ``0.0`` to ``1.0``.

  Additionally, your ``__init__`` must support this format as an initialization value
  without additional parameters to ensure correct functionality of all other methods in
  :class:`ManimColor`.

- :meth:`~ManimColor._from_internal`: a ``@classmethod`` which converts an
  ``[r,g,b,a]`` value into suitable parameters for your ``__init__`` method and calls
  the ``cls`` parameter.

"""

from __future__ import annotations

import colorsys

# logger = _config.logger
import random
import re
from collections.abc import Iterable, Sequence
from typing import Self, TypeAlias, TypeVar, overload

import numpy as np
import numpy.typing as npt
from typing_extensions import TypeIs, override

from manim.typing import (
    FloatHSL,
    FloatHSLLike,
    FloatHSV,
    FloatHSVA,
    FloatHSVALike,
    FloatHSVLike,
    FloatRGB,
    FloatRGBA,
    FloatRGBALike,
    FloatRGBLike,
    IntRGB,
    IntRGBA,
    IntRGBALike,
    IntRGBLike,
    ManimColorDType,
    ManimColorInternal,
    ManimFloat,
    Point3D,
    Vector3D,
)

from ...utils.space_ops import normalize

# import manim._config as _config

re_hex = re.compile("((?<=#)|(?<=0x))[A-F0-9]{3,8}", re.IGNORECASE)


[docs] class ManimColor: """Internal representation of a color. The :class:`ManimColor` class is the main class for the representation of a color. Its internal representation is an array of 4 floats corresponding to a ``[r,g,b,a]`` value where ``r,g,b,a`` can be between 0.0 and 1.0. This is done in order to reduce the amount of color inconsistencies by constantly casting between integers and floats which introduces errors. The class can accept any value of type :class:`ParsableManimColor` i.e. ``ManimColor, int, str, RGB_Tuple_Int, RGB_Tuple_Float, RGBA_Tuple_Int, RGBA_Tuple_Float, RGB_Array_Int, RGB_Array_Float, RGBA_Array_Int, RGBA_Array_Float`` :class:`ManimColor` itself only accepts singular values and will directly interpret them into a single color if possible. Be careful when passing strings to :class:`ManimColor`: it can create a big overhead for the color processing. If you want to parse a list of colors, use the :meth:`parse` method, which assumes that you're going to pass a list of colors so that arrays will not be interpreted as a single color. .. warning:: If you pass an array of numbers to :meth:`parse`, it will interpret the ``r,g,b,a`` numbers in that array as colors: Instead of the expected singular color, you will get an array with 4 colors. For conversion behaviors, see the ``_internal`` functions for further documentation. You can create a :class:`ManimColor` instance via its classmethods. See the respective methods for more info. .. code-block:: python mycolor = ManimColor.from_rgb((0, 1, 0.4, 0.5)) myothercolor = ManimColor.from_rgb((153, 255, 255)) You can also convert between different color spaces: .. code-block:: python mycolor_hex = mycolor.to_hex() myoriginalcolor = ManimColor.from_hex(mycolor_hex).to_hsv() Parameters ---------- value Some representation of a color (e.g., a string or a suitable tuple). The default ``None`` is ``BLACK``. alpha The opacity of the color. By default, colors are fully opaque (value 1.0). """ def __init__( self, value: ParsableManimColor | None, alpha: float = 1.0, ) -> None: if value is None: self._internal_value = np.array((0, 0, 0, alpha), dtype=ManimColorDType) elif isinstance(value, ManimColor): # logger.info( # "ManimColor was passed another ManimColor. This is probably not what " # "you want. Created a copy of the passed ManimColor instead." # ) self._internal_value = value._internal_value elif isinstance(value, int): self._internal_value = ManimColor._internal_from_integer(value, alpha) elif isinstance(value, str): result = re_hex.search(value) if result is not None: self._internal_value = ManimColor._internal_from_hex_string( result.group(), alpha ) else: # This is not expected to be called on module initialization time # It can be horribly slow to convert a string to a color because # it has to access the dictionary of colors and find the right color self._internal_value = ManimColor._internal_from_string(value, alpha) elif isinstance(value, (list, tuple, np.ndarray)): length = len(value) if all(isinstance(x, float) for x in value): if length == 3: self._internal_value = ManimColor._internal_from_rgb(value, alpha) elif length == 4: self._internal_value = ManimColor._internal_from_rgba(value) else: raise ValueError( f"ManimColor only accepts lists/tuples/arrays of length 3 or 4, not {length}" ) else: if length == 3: self._internal_value = ManimColor._internal_from_int_rgb( value, alpha ) elif length == 4: self._internal_value = ManimColor._internal_from_int_rgba(value) else: raise ValueError( f"ManimColor only accepts lists/tuples/arrays of length 3 or 4, not {length}" ) elif hasattr(value, "get_hex") and callable(value.get_hex): result = re_hex.search(value.get_hex()) if result is None: raise ValueError(f"Failed to parse a color from {value}") self._internal_value = ManimColor._internal_from_hex_string( result.group(), alpha ) else: # logger.error(f"Invalid color value: {value}") raise TypeError( "ManimColor only accepts int, str, list[int, int, int], " "list[int, int, int, int], list[float, float, float], " f"list[float, float, float, float], not {type(value)}" ) @property def _internal_space(self) -> npt.NDArray[ManimFloat]: """This is a readonly property which is a custom representation for color space operations. It is used for operators and can be used when implementing a custom color space. """ return self._internal_value @property def _internal_value(self) -> ManimColorInternal: """Return the internal value of the current Manim color ``[r,g,b,a]`` float array. Returns ------- ManimColorInternal Internal color representation. """ return self.__value @_internal_value.setter def _internal_value(self, value: ManimColorInternal) -> None: """Overwrite the internal color value of this :class:`ManimColor`. Parameters ---------- value The value which will overwrite the current color. Raises ------ TypeError If an invalid array is passed. """ if not isinstance(value, np.ndarray): raise TypeError("Value must be a NumPy array.") if value.shape[0] != 4: raise TypeError("Array must have exactly 4 values.") self.__value: ManimColorInternal = value
[docs] @classmethod def _construct_from_space( cls, _space: npt.NDArray[ManimFloat] | tuple[float, float, float] | tuple[float, float, float, float], ) -> Self: """This function is used as a proxy for constructing a color with an internal value. This can be used by subclasses to hook into the construction of new objects using the internal value format. """ return cls(_space)
@staticmethod def _internal_from_integer(value: int, alpha: float) -> ManimColorInternal: return np.asarray( ( ((value >> 16) & 0xFF) / 255, ((value >> 8) & 0xFF) / 255, ((value >> 0) & 0xFF) / 255, alpha, ), dtype=ManimColorDType, )
[docs] @staticmethod def _internal_from_hex_string(hex_: str, alpha: float) -> ManimColorInternal: """Internal function for converting a hex string into the internal representation of a :class:`ManimColor`. .. warning:: This does not accept any prefixes like # or similar in front of the hex string. This is just intended for the raw hex part. *For internal use only* Parameters ---------- hex Hex string to be parsed. alpha Alpha value used for the color, if the color is only 3 bytes long. Otherwise, if the color is 4 bytes long, this parameter will not be used. Returns ------- ManimColorInternal Internal color representation """ if len(hex_) in (3, 4): hex_ = "".join([x * 2 for x in hex_]) if len(hex_) == 6: hex_ += "FF" elif len(hex_) == 8: alpha = (int(hex_, 16) & 0xFF) / 255 else: raise ValueError( "Hex colors must be specified with either 0x or # as prefix and contain 6 or 8 hexadecimal numbers" ) tmp = int(hex_, 16) return np.asarray( ( ((tmp >> 24) & 0xFF) / 255, ((tmp >> 16) & 0xFF) / 255, ((tmp >> 8) & 0xFF) / 255, alpha, ), dtype=ManimColorDType, )
[docs] @staticmethod def _internal_from_int_rgb( rgb: IntRGBLike, alpha: float = 1.0 ) -> ManimColorInternal: """Internal function for converting an RGB tuple of integers into the internal representation of a :class:`ManimColor`. *For internal use only* Parameters ---------- rgb Integer RGB tuple to be parsed alpha Optional alpha value. Default is 1.0. Returns ------- ManimColorInternal Internal color representation. """ value: np.ndarray = np.asarray(rgb, dtype=ManimColorDType).copy() / 255 value.resize(4, refcheck=False) value[3] = alpha return value
[docs] @staticmethod def _internal_from_rgb(rgb: FloatRGBLike, alpha: float = 1.0) -> ManimColorInternal: """Internal function for converting a rgb tuple of floats into the internal representation of a :class:`ManimColor`. *For internal use only* Parameters ---------- rgb Float RGB tuple to be parsed. alpha Optional alpha value. Default is 1.0. Returns ------- ManimColorInternal Internal color representation. """ value: np.ndarray = np.asarray(rgb, dtype=ManimColorDType).copy() value.resize(4, refcheck=False) value[3] = alpha return value
[docs] @staticmethod def _internal_from_int_rgba(rgba: IntRGBALike) -> ManimColorInternal: """Internal function for converting an RGBA tuple of integers into the internal representation of a :class:`ManimColor`. *For internal use only* Parameters ---------- rgba Int RGBA tuple to be parsed. Returns ------- ManimColorInternal Internal color representation. """ return np.asarray(rgba, dtype=ManimColorDType) / 255
[docs] @staticmethod def _internal_from_rgba(rgba: FloatRGBALike) -> ManimColorInternal: """Internal function for converting an RGBA tuple of floats into the internal representation of a :class:`ManimColor`. *For internal use only* Parameters ---------- rgba Int RGBA tuple to be parsed. Returns ------- ManimColorInternal Internal color representation. """ return np.asarray(rgba, dtype=ManimColorDType)
[docs] @staticmethod def _internal_from_string(name: str, alpha: float) -> ManimColorInternal: """Internal function for converting a string into the internal representation of a :class:`ManimColor`. This is not used for hex strings: please refer to :meth:`_internal_from_hex` for this functionality. *For internal use only* Parameters ---------- name The color name to be parsed into a color. Refer to the different color modules in the documentation page to find the corresponding color names. Returns ------- ManimColorInternal Internal color representation. Raises ------ ValueError If the color name is not present in Manim. """ from . import _all_color_dict if tmp := _all_color_dict.get(name.upper()): tmp._internal_value[3] = alpha return tmp._internal_value.copy() else: raise ValueError(f"Color {name} not found")
[docs] def to_integer(self) -> int: """Convert the current :class:`ManimColor` into an integer. .. warning:: This will return only the RGB part of the color. Returns ------- int Integer representation of the color. """ tmp = (self._internal_value[:3] * 255).astype(dtype=np.byte).tobytes() return int.from_bytes(tmp, "big")
[docs] def to_rgb(self) -> FloatRGB: """Convert the current :class:`ManimColor` into an RGB array of floats. Returns ------- RGB_Array_Float RGB array of 3 floats from 0.0 to 1.0. """ return self._internal_value[:3]
[docs] def to_int_rgb(self) -> IntRGB: """Convert the current :class:`ManimColor` into an RGB array of integers. Returns ------- RGB_Array_Int RGB array of 3 integers from 0 to 255. """ return (self._internal_value[:3] * 255).astype(int)
[docs] def to_rgba(self) -> FloatRGBA: """Convert the current :class:`ManimColor` into an RGBA array of floats. Returns ------- RGBA_Array_Float RGBA array of 4 floats from 0.0 to 1.0. """ return self._internal_value
[docs] def to_int_rgba(self) -> IntRGBA: """Convert the current ManimColor into an RGBA array of integers. Returns ------- RGBA_Array_Int RGBA array of 4 integers from 0 to 255. """ return (self._internal_value * 255).astype(int)
[docs] def to_rgba_with_alpha(self, alpha: float) -> FloatRGBA: """Convert the current :class:`ManimColor` into an RGBA array of floats. This is similar to :meth:`to_rgba`, but you can change the alpha value. Parameters ---------- alpha Alpha value to be used in the return value. Returns ------- RGBA_Array_Float RGBA array of 4 floats from 0.0 to 1.0. """ return np.fromiter((*self._internal_value[:3], alpha), dtype=ManimColorDType)
[docs] def to_int_rgba_with_alpha(self, alpha: float) -> IntRGBA: """Convert the current :class:`ManimColor` into an RGBA array of integers. This is similar to :meth:`to_int_rgba`, but you can change the alpha value. Parameters ---------- alpha Alpha value to be used for the return value. Pass a float between 0.0 and 1.0: it will automatically be scaled to an integer between 0 and 255. Returns ------- RGBA_Array_Int RGBA array of 4 integers from 0 to 255. """ tmp = self._internal_value * 255 tmp[3] = alpha * 255 return tmp.astype(int)
[docs] def to_hex(self, with_alpha: bool = False) -> str: """Convert the :class:`ManimColor` to a hexadecimal representation of the color. Parameters ---------- with_alpha If ``True``, append 2 extra characters to the hex string which represent the alpha value of the color between 0 and 255. Default is ``False``. Returns ------- str A hex string starting with a ``#``, with either 6 or 8 nibbles depending on the ``with_alpha`` parameter. By default, it has 6 nibbles, i.e. ``#XXXXXX``. """ tmp = ( f"#{int(self._internal_value[0] * 255):02X}" f"{int(self._internal_value[1] * 255):02X}" f"{int(self._internal_value[2] * 255):02X}" ) if with_alpha: tmp += f"{int(self._internal_value[3] * 255):02X}" return tmp
[docs] def to_hsv(self) -> FloatHSV: """Convert the :class:`ManimColor` to an HSV array. .. note:: Be careful: this returns an array in the form ``[h, s, v]``, where the elements are floats. This might be confusing, because RGB can also be an array of floats. You might want to annotate the usage of this function in your code by typing your HSV array variables as :class:`HSV_Array_Float` in order to differentiate them from RGB arrays. Returns ------- HSV_Array_Float An HSV array of 3 floats from 0.0 to 1.0. """ return np.array(colorsys.rgb_to_hsv(*self.to_rgb()))
[docs] def to_hsl(self) -> FloatHSL: """Convert the :class:`ManimColor` to an HSL array. .. note:: Be careful: this returns an array in the form ``[h, s, l]``, where the elements are floats. This might be confusing, because RGB can also be an array of floats. You might want to annotate the usage of this function in your code by typing your HSL array variables as :class:`HSL_Array_Float` in order to differentiate them from RGB arrays. Returns ------- HSL_Array_Float An HSL array of 3 floats from 0.0 to 1.0. """ hls = colorsys.rgb_to_hls(*self.to_rgb()) return np.array([hls[0], hls[2], hls[1]])
[docs] def invert(self, with_alpha: bool = False) -> Self: """Return a new, linearly inverted version of this :class:`ManimColor` (no inplace changes). Parameters ---------- with_alpha If ``True``, the alpha value will be inverted too. Default is ``False``. .. note:: Setting ``with_alpha=True`` can result in unintended behavior where objects are not displayed because their new alpha value is suddenly 0 or very low. Returns ------- ManimColor The linearly inverted :class:`ManimColor`. """ if with_alpha: return self._construct_from_space(1.0 - self._internal_space) else: alpha = self._internal_space[3] new = 1.0 - self._internal_space new[-1] = alpha return self._construct_from_space(new)
[docs] def interpolate(self, other: Self, alpha: float) -> Self: """Interpolate between the current and the given :class:`ManimColor`, and return the result. Parameters ---------- other The other :class:`ManimColor` to be used for interpolation. alpha A point on the line in RGBA colorspace connecting the two colors, i.e. the interpolation point. 0.0 corresponds to the current :class:`ManimColor` and 1.0 corresponds to the other :class:`ManimColor`. Returns ------- ManimColor The interpolated :class:`ManimColor`. """ return self._construct_from_space( self._internal_space * (1 - alpha) + other._internal_space * alpha )
[docs] def darker(self, blend: float = 0.2) -> Self: """Return a new color that is darker than the current color, i.e. interpolated with ``BLACK``. The opacity is unchanged. Parameters ---------- blend The blend ratio for the interpolation, from 0.0 (the current color unchanged) to 1.0 (pure black). Default is 0.2, which results in a slightly darker color. Returns ------- ManimColor The darker :class:`ManimColor`. See Also -------- :meth:`lighter` """ from manim.utils.color.manim_colors import BLACK alpha = self._internal_space[3] black = self._from_internal(BLACK._internal_value) return self.interpolate(black, blend).opacity(alpha)
[docs] def lighter(self, blend: float = 0.2) -> Self: """Return a new color that is lighter than the current color, i.e. interpolated with ``WHITE``. The opacity is unchanged. Parameters ---------- blend The blend ratio for the interpolation, from 0.0 (the current color unchanged) to 1.0 (pure white). Default is 0.2, which results in a slightly lighter color. Returns ------- ManimColor The lighter :class:`ManimColor`. See Also -------- :meth:`darker` """ from manim.utils.color.manim_colors import WHITE alpha = self._internal_space[3] white = self._from_internal(WHITE._internal_value) return self.interpolate(white, blend).opacity(alpha)
[docs] def contrasting( self, threshold: float = 0.5, light: Self | None = None, dark: Self | None = None, ) -> Self: """Return one of two colors, light or dark (by default white or black), that contrasts with the current color (depending on its luminance). This is typically used to set text in a contrasting color that ensures it is readable against a background of the current color. Parameters ---------- threshold The luminance threshold which dictates whether the current color is considered light or dark (and thus whether to return the dark or light color, respectively). Default is 0.5. light The light color to return if the current color is considered dark. Default is ``None``: in this case, pure ``WHITE`` will be returned. dark The dark color to return if the current color is considered light, Default is ``None``: in this case, pure ``BLACK`` will be returned. Returns ------- ManimColor The contrasting :class:`ManimColor`. """ from manim.utils.color.manim_colors import BLACK, WHITE luminance, _, _ = colorsys.rgb_to_yiq(*self.to_rgb()) if luminance < threshold: if light is not None: return light return self._from_internal(WHITE._internal_value) else: if dark is not None: return dark return self._from_internal(BLACK._internal_value)
[docs] def opacity(self, opacity: float) -> Self: """Create a new :class:`ManimColor` with the given opacity and the same color values as before. Parameters ---------- opacity The new opacity value to be used. Returns ------- ManimColor The new :class:`ManimColor` with the same color values and the new opacity. """ tmp = self._internal_space.copy() tmp[-1] = opacity return self._construct_from_space(tmp)
[docs] def into(self, class_type: type[ManimColorT]) -> ManimColorT: """Convert the current color into a different colorspace given by ``class_type``, without changing the :attr:`_internal_value`. Parameters ---------- class_type The class that is used for conversion. It must be a subclass of :class:`ManimColor` which respects the specification HSV, RGBA, ... Returns ------- ManimColorT A new color object of type ``class_type`` and the same :attr:`_internal_value` as the original color. """ return class_type._from_internal(self._internal_value)
[docs] @classmethod def _from_internal(cls, value: ManimColorInternal) -> Self: """This method is intended to be overwritten by custom color space classes which are subtypes of :class:`ManimColor`. The method constructs a new object of the given class by transforming the value in the internal format ``[r,g,b,a]`` into a format which the constructor of the custom class can understand. Look at :class:`.HSV` for an example. """ return cls(value)
[docs] @classmethod def from_rgb( cls, rgb: FloatRGBLike | IntRGBLike, alpha: float = 1.0, ) -> Self: """Create a ManimColor from an RGB array. Automagically decides which type it is: ``int`` or ``float``. .. warning:: Please make sure that your elements are not floats if you want integers. A ``5.0`` will result in the input being interpreted as if it was an RGB float array with the value ``5.0`` and not the integer ``5``. Parameters ---------- rgb Any iterable of 3 floats or 3 integers. alpha Alpha value to be used in the color. Default is 1.0. Returns ------- ManimColor The :class:`ManimColor` which corresponds to the given ``rgb``. """ return cls._from_internal(ManimColor(rgb, alpha)._internal_value)
[docs] @classmethod def from_rgba(cls, rgba: FloatRGBALike | IntRGBALike) -> Self: """Create a ManimColor from an RGBA Array. Automagically decides which type it is: ``int`` or ``float``. .. warning:: Please make sure that your elements are not floats if you want integers. A ``5.0`` will result in the input being interpreted as if it was a float RGB array with the float ``5.0`` and not the integer ``5``. Parameters ---------- rgba Any iterable of 4 floats or 4 integers. Returns ------- ManimColor The :class:`ManimColor` corresponding to the given ``rgba``. """ return cls(rgba)
[docs] @classmethod def from_hex(cls, hex_str: str, alpha: float = 1.0) -> Self: """Create a :class:`ManimColor` from a hex string. Parameters ---------- hex_str The hex string to be converted. The allowed prefixes for this string are ``#`` and ``0x``. Currently, this method only supports 6 nibbles, i.e. only strings in the format ``#XXXXXX`` or ``0xXXXXXX``. alpha Alpha value to be used for the hex string. Default is 1.0. Returns ------- ManimColor The :class:`ManimColor` represented by the hex string. """ return cls._from_internal(ManimColor(hex_str, alpha)._internal_value)
[docs] @classmethod def from_hsv(cls, hsv: FloatHSVLike, alpha: float = 1.0) -> Self: """Create a :class:`ManimColor` from an HSV array. Parameters ---------- hsv Any iterable containing 3 floats from 0.0 to 1.0. alpha The alpha value to be used. Default is 1.0. Returns ------- ManimColor The :class:`ManimColor` with the corresponding RGB values to the given HSV array. """ rgb = colorsys.hsv_to_rgb(*hsv) return cls._from_internal(ManimColor(rgb, alpha)._internal_value)
[docs] @classmethod def from_hsl(cls, hsl: FloatHSLLike, alpha: float = 1.0) -> Self: """Create a :class:`ManimColor` from an HSL array. Parameters ---------- hsl Any iterable containing 3 floats from 0.0 to 1.0. alpha The alpha value to be used. Default is 1.0. Returns ------- ManimColor The :class:`ManimColor` with the corresponding RGB values to the given HSL array. """ rgb = colorsys.hls_to_rgb(hsl[0], hsl[2], hsl[1]) return cls._from_internal(ManimColor(rgb, alpha)._internal_value)
@overload @classmethod def parse( cls, color: ParsableManimColor | None, alpha: float = ..., ) -> Self: ... @overload @classmethod def parse( cls, color: Sequence[ParsableManimColor], alpha: float = ..., ) -> list[Self]: ...
[docs] @classmethod def parse( cls, color: ParsableManimColor | Sequence[ParsableManimColor] | None, alpha: float = 1.0, ) -> Self | list[Self]: """Parse one color as a :class:`ManimColor` or a sequence of colors as a list of :class:`ManimColor`'s. Parameters ---------- color The color or list of colors to parse. Note that this function can not accept tuples: it will assume that you mean ``Sequence[ParsableManimColor]`` and will return a ``list[ManimColor]``. alpha The alpha (opacity) value to use for the passed color(s). Returns ------- ManimColor | list[ManimColor] Either a list of colors or a singular color, depending on the input. """ def is_sequence( color: ParsableManimColor | Sequence[ParsableManimColor] | None, ) -> TypeIs[Sequence[ParsableManimColor]]: return isinstance(color, (list, tuple)) if is_sequence(color): return [ cls._from_internal(ManimColor(c, alpha)._internal_value) for c in color ] else: return cls._from_internal(ManimColor(color, alpha)._internal_value)
[docs] @staticmethod def gradient( colors: list[ManimColor], length: int ) -> ManimColor | list[ManimColor]: """This method is currently not implemented. Refer to :func:`color_gradient` for a working implementation for now. """ # TODO: implement proper gradient, research good implementation for this or look at 3b1b implementation raise NotImplementedError
def __repr__(self) -> str: return f"{self.__class__.__name__}('{self.to_hex()}')" def __str__(self) -> str: return f"{self.to_hex()}" def __eq__(self, other: object) -> bool: if not isinstance(other, ManimColor): raise TypeError( f"Cannot compare {self.__class__.__name__} with {other.__class__.__name__}" ) are_equal: bool = np.allclose(self._internal_value, other._internal_value) return are_equal def __add__(self, other: int | float | Self) -> Self: if isinstance(other, (int, float)): return self._construct_from_space(self._internal_space + other) else: return self._construct_from_space( self._internal_space + other._internal_space ) def __radd__(self, other: int | float | Self) -> Self: return self + other def __sub__(self, other: int | float | Self) -> Self: if isinstance(other, (int, float)): return self._construct_from_space(self._internal_space - other) else: return self._construct_from_space( self._internal_space - other._internal_space ) def __rsub__(self, other: int | float | Self) -> Self: return self - other def __mul__(self, other: int | float | Self) -> Self: if isinstance(other, (int, float)): return self._construct_from_space(self._internal_space * other) else: return self._construct_from_space( self._internal_space * other._internal_space ) def __rmul__(self, other: int | float | Self) -> Self: return self * other def __truediv__(self, other: int | float | Self) -> Self: if isinstance(other, (int, float)): return self._construct_from_space(self._internal_space / other) else: return self._construct_from_space( self._internal_space / other._internal_space ) def __rtruediv__(self, other: int | float | Self) -> Self: return self / other def __floordiv__(self, other: int | float | Self) -> Self: if isinstance(other, (int, float)): return self._construct_from_space(self._internal_space // other) else: return self._construct_from_space( self._internal_space // other._internal_space ) def __rfloordiv__(self, other: int | float | Self) -> Self: return self // other def __mod__(self, other: int | float | Self) -> Self: if isinstance(other, (int, float)): return self._construct_from_space(self._internal_space % other) else: return self._construct_from_space( self._internal_space % other._internal_space ) def __rmod__(self, other: int | float | Self) -> Self: return self % other def __pow__(self, other: int | float | Self) -> Self: if isinstance(other, (int, float)): return self._construct_from_space(self._internal_space**other) else: return self._construct_from_space( self._internal_space**other._internal_space ) def __rpow__(self, other: int | float | Self) -> Self: return self**other def __invert__(self) -> Self: return self.invert() def __int__(self) -> int: return self.to_integer() def __getitem__(self, index: int) -> float: item: float = self._internal_space[index] return item def __and__(self, other: Self) -> Self: return self._construct_from_space( self._internal_from_integer(self.to_integer() & int(other), 1.0) ) def __or__(self, other: Self) -> Self: return self._construct_from_space( self._internal_from_integer(self.to_integer() | int(other), 1.0) ) def __xor__(self, other: Self) -> Self: return self._construct_from_space( self._internal_from_integer(self.to_integer() ^ int(other), 1.0) ) def __hash__(self) -> int: return hash(self.to_hex(with_alpha=True))
RGBA = ManimColor """RGBA Color Space"""
[docs] class HSV(ManimColor): """HSV Color Space""" def __init__( self, hsv: FloatHSVLike | FloatHSVALike, alpha: float = 1.0, ) -> None: super().__init__(None) self.__hsv: FloatHSVA if len(hsv) == 3: self.__hsv = np.asarray((*hsv, alpha)) elif len(hsv) == 4: self.__hsv = np.asarray(hsv) else: raise ValueError("HSV Color must be an array of 3 values")
[docs] @classmethod @override def _from_internal(cls, value: ManimColorInternal) -> Self: hsv = colorsys.rgb_to_hsv(*value[:3]) hsva = [*hsv, value[-1]] return cls(np.array(hsva))
@property def hue(self) -> float: hue: float = self.__hsv[0] return hue @hue.setter def hue(self, hue: float) -> None: self.__hsv[0] = hue @property def saturation(self) -> float: saturation: float = self.__hsv[1] return saturation @saturation.setter def saturation(self, saturation: float) -> None: self.__hsv[1] = saturation @property def value(self) -> float: value: float = self.__hsv[2] return value @value.setter def value(self, value: float) -> None: self.__hsv[2] = value @property def h(self) -> float: hue: float = self.__hsv[0] return hue @h.setter def h(self, hue: float) -> None: self.__hsv[0] = hue @property def s(self) -> float: saturation: float = self.__hsv[1] return saturation @s.setter def s(self, saturation: float) -> None: self.__hsv[1] = saturation @property def v(self) -> float: value: float = self.__hsv[2] return value @v.setter def v(self, value: float) -> None: self.__hsv[2] = value @property def _internal_space(self) -> npt.NDArray: return self.__hsv @property def _internal_value(self) -> ManimColorInternal: """Return the internal value of the current :class:`ManimColor` as an ``[r,g,b,a]`` float array. Returns ------- ManimColorInternal Internal color representation. """ return np.array( [ *colorsys.hsv_to_rgb(self.__hsv[0], self.__hsv[1], self.__hsv[2]), self.__alpha, ], dtype=ManimColorDType, ) @_internal_value.setter def _internal_value(self, value: ManimColorInternal) -> None: """Overwrite the internal color value of this :class:`ManimColor`. Parameters ---------- value The value which will overwrite the current color. Raises ------ TypeError If an invalid array is passed. """ if not isinstance(value, np.ndarray): raise TypeError("Value must be a NumPy array.") if value.shape[0] != 4: raise TypeError("Array must have exactly 4 values.") tmp = colorsys.rgb_to_hsv(value[0], value[1], value[2]) self.__hsv = np.array(tmp) self.__alpha = value[3]
ParsableManimColor: TypeAlias = ( ManimColor | int | str | IntRGBLike | FloatRGBLike | IntRGBALike | FloatRGBALike ) """`ParsableManimColor` represents all the types which can be parsed to a :class:`ManimColor` in Manim. """ ManimColorT = TypeVar("ManimColorT", bound=ManimColor)
[docs] def color_to_rgb(color: ParsableManimColor) -> FloatRGB: """Helper function for use in functional style programming. Refer to :meth:`ManimColor.to_rgb`. Parameters ---------- color A color to convert to an RGB float array. Returns ------- RGB_Array_Float The corresponding RGB float array. """ return ManimColor(color).to_rgb()
[docs] def color_to_rgba(color: ParsableManimColor, alpha: float = 1.0) -> FloatRGBA: """Helper function for use in functional style programming. Refer to :meth:`ManimColor.to_rgba_with_alpha`. Parameters ---------- color A color to convert to an RGBA float array. alpha An alpha value between 0.0 and 1.0 to be used as opacity in the color. Default is 1.0. Returns ------- RGBA_Array_Float The corresponding RGBA float array. """ return ManimColor(color).to_rgba_with_alpha(alpha)
[docs] def color_to_int_rgb(color: ParsableManimColor) -> IntRGB: """Helper function for use in functional style programming. Refer to :meth:`ManimColor.to_int_rgb`. Parameters ---------- color A color to convert to an RGB integer array. Returns ------- RGB_Array_Int The corresponding RGB integer array. """ return ManimColor(color).to_int_rgb()
[docs] def color_to_int_rgba(color: ParsableManimColor, alpha: float = 1.0) -> IntRGBA: """Helper function for use in functional style programming. Refer to :meth:`ManimColor.to_int_rgba_with_alpha`. Parameters ---------- color A color to convert to an RGBA integer array. alpha An alpha value between 0.0 and 1.0 to be used as opacity in the color. Default is 1.0. Returns ------- RGBA_Array_Int The corresponding RGBA integer array. """ return ManimColor(color).to_int_rgba_with_alpha(alpha)
[docs] def rgb_to_color(rgb: FloatRGBLike | IntRGBLike) -> ManimColor: """Helper function for use in functional style programming. Refer to :meth:`ManimColor.from_rgb`. Parameters ---------- rgb A 3 element iterable. Returns ------- ManimColor A ManimColor with the corresponding value. """ return ManimColor.from_rgb(rgb)
[docs] def rgba_to_color(rgba: FloatRGBALike | IntRGBALike) -> ManimColor: """Helper function for use in functional style programming. Refer to :meth:`ManimColor.from_rgba`. Parameters ---------- rgba A 4 element iterable. Returns ------- ManimColor A ManimColor with the corresponding value """ return ManimColor.from_rgba(rgba)
[docs] def rgb_to_hex(rgb: FloatRGBLike | IntRGBLike) -> str: """Helper function for use in functional style programming. Refer to :meth:`ManimColor.from_rgb` and :meth:`ManimColor.to_hex`. Parameters ---------- rgb A 3 element iterable. Returns ------- str A hex representation of the color. """ return ManimColor.from_rgb(rgb).to_hex()
[docs] def hex_to_rgb(hex_code: str) -> FloatRGB: """Helper function for use in functional style programming. Refer to :meth:`ManimColor.to_rgb`. Parameters ---------- hex_code A hex string representing a color. Returns ------- RGB_Array_Float An RGB array representing the color. """ return ManimColor(hex_code).to_rgb()
[docs] def invert_color(color: ManimColorT) -> ManimColorT: """Helper function for use in functional style programming. Refer to :meth:`ManimColor.invert` Parameters ---------- color The :class:`ManimColor` to invert. Returns ------- ManimColor The linearly inverted :class:`ManimColor`. """ return color.invert()
[docs] def color_gradient( reference_colors: Iterable[ParsableManimColor], length_of_output: int, ) -> list[ManimColor]: """Create a list of colors interpolated between the input array of colors with a specific number of colors. Parameters ---------- reference_colors The colors to be interpolated between or spread apart. length_of_output The number of colors that the output should have, ideally more than the input. Returns ------- list[ManimColor] A list of interpolated :class:`ManimColor`'s. """ if length_of_output == 0: return [] parsed_colors = [ManimColor(color) for color in reference_colors] num_colors = len(parsed_colors) if num_colors == 0: raise ValueError("Expected 1 or more reference colors. Got 0 colors.") if num_colors == 1: return parsed_colors * length_of_output rgbs = [color.to_rgb() for color in parsed_colors] alphas = np.linspace(0, (num_colors - 1), length_of_output) floors = alphas.astype("int") alphas_mod1 = alphas % 1 # End edge case alphas_mod1[-1] = 1 floors[-1] = num_colors - 2 return [ rgb_to_color((rgbs[i] * (1 - alpha)) + (rgbs[i + 1] * alpha)) for i, alpha in zip(floors, alphas_mod1, strict=True) ]
[docs] def interpolate_color( color1: ManimColorT, color2: ManimColorT, alpha: float ) -> ManimColorT: """Standalone function to interpolate two ManimColors and get the result. Refer to :meth:`ManimColor.interpolate`. Parameters ---------- color1 The first :class:`ManimColor`. color2 The second :class:`ManimColor`. alpha The alpha value determining the point of interpolation between the colors. Returns ------- ManimColor The interpolated ManimColor. """ return color1.interpolate(color2, alpha)
[docs] def average_color(*colors: ParsableManimColor) -> ManimColor: """Determine the average color between the given parameters. .. note:: This operation does not consider the alphas (opacities) of the colors. The generated color has an alpha or opacity of 1.0. Returns ------- ManimColor The average color of the input. """ rgbs = np.array([color_to_rgb(color) for color in colors]) mean_rgb = np.apply_along_axis(np.mean, 0, rgbs) return rgb_to_color(mean_rgb)
[docs] def random_bright_color() -> ManimColor: """Return a random bright color: a random color averaged with ``WHITE``. .. warning:: This operation is very expensive. Please keep in mind the performance loss. Returns ------- ManimColor A random bright :class:`ManimColor`. """ curr_rgb = color_to_rgb(random_color()) new_rgb = 0.5 * (curr_rgb + np.ones(3)) return ManimColor(new_rgb)
[docs] def random_color() -> ManimColor: """Return a random :class:`ManimColor`. Returns ------- ManimColor A random :class:`ManimColor`. """ return RandomColorGenerator._random_color()
[docs] class RandomColorGenerator: """A generator for producing random colors from a given list of Manim colors, optionally in a reproducible sequence using a seed value. When initialized with a specific seed, this class produces a deterministic sequence of :class:`.ManimColor` instances. If no seed is provided, the selection is non-deterministic using Python’s global random state. Parameters ---------- seed A seed value to initialize the internal random number generator. If ``None`` (the default), colors are chosen using the global random state. sample_colors A custom list of Manim colors to sample from. Defaults to the full Manim color palette. Examples -------- Without a seed (non-deterministic):: >>> from manim import RandomColorGenerator, ManimColor, RED, GREEN, BLUE >>> rnd = RandomColorGenerator() >>> isinstance(rnd.next(), ManimColor) True With a seed (deterministic sequence):: >>> rnd = RandomColorGenerator(42) >>> rnd.next() ManimColor('#8B4513') >>> rnd.next() ManimColor('#BBBBBB') >>> rnd.next() ManimColor('#BBBBBB') Re-initializing with the same seed gives the same sequence:: >>> rnd2 = RandomColorGenerator(42) >>> rnd2.next() ManimColor('#8B4513') >>> rnd2.next() ManimColor('#BBBBBB') >>> rnd2.next() ManimColor('#BBBBBB') Using a custom color list:: >>> custom_palette = [RED, GREEN, BLUE] >>> rnd_custom = RandomColorGenerator(1, sample_colors=custom_palette) >>> rnd_custom.next() in custom_palette True >>> rnd_custom.next() in custom_palette True Without a seed and custom palette (non-deterministic):: >>> rnd_nodet = RandomColorGenerator(sample_colors=[RED]) >>> rnd_nodet.next() ManimColor('#FC6255') """ _singleton: RandomColorGenerator | None = None def __init__( self, seed: int | None = None, sample_colors: list[ManimColor] | None = None, ) -> None: from manim.utils.color.manim_colors import _all_manim_colors self.choice = random.choice if seed is None else random.Random(seed).choice self.colors = _all_manim_colors if sample_colors is None else sample_colors
[docs] def next(self) -> ManimColor: """Returns the next color from the configured color list. Returns ------- ManimColor A randomly selected color from the specified color list. Examples -------- Usage:: >>> from manim import RandomColorGenerator, RED >>> rnd = RandomColorGenerator(sample_colors=[RED]) >>> rnd.next() ManimColor('#FC6255') """ return self.choice(self.colors)
[docs] @classmethod def _random_color(cls) -> ManimColor: """Internal method to generate a random color using the singleton instance of `RandomColorGenerator`. It will be used by proxy method `random_color` publicly available and makes it backwards compatible. Returns ------- ManimColor: A randomly selected color from the configured color list of the singleton instance. """ if cls._singleton is None: cls._singleton = cls() return cls._singleton.next()
[docs] def get_shaded_rgb( rgb: FloatRGB, point: Point3D, unit_normal_vect: Vector3D, light_source: Point3D, ) -> FloatRGB: """Add light or shadow to the ``rgb`` color of some surface which is located at a given ``point`` in space and facing in the direction of ``unit_normal_vect``, depending on whether the surface is facing a ``light_source`` or away from it. Parameters ---------- rgb An RGB array of floats. point The location of the colored surface. unit_normal_vect The direction in which the colored surface is facing. light_source The location of a light source which might illuminate the surface. Returns ------- RGB_Array_Float The color with added light or shadow, depending on the direction of the colored surface. """ to_sun = normalize(light_source - point) light = 0.5 * np.dot(unit_normal_vect, to_sun) ** 3 if light < 0: light *= 0.5 shaded_rgb: FloatRGB = rgb + light return shaded_rgb
__all__ = [ "ManimColor", "ManimColorDType", "ParsableManimColor", "color_to_rgb", "color_to_rgba", "color_to_int_rgb", "color_to_int_rgba", "rgb_to_color", "rgba_to_color", "rgb_to_hex", "hex_to_rgb", "invert_color", "color_gradient", "interpolate_color", "average_color", "random_bright_color", "random_color", "RandomColorGenerator", "get_shaded_rgb", "HSV", "RGBA", ]