Source code for arcade.types

"""
Module specifying data custom types used for type hinting.
"""
from __future__ import annotations

from __future__ import annotations

import sys
from array import array
import ctypes
import random
from collections import namedtuple
from collections.abc import ByteString
from pathlib import Path
from typing import (
    Iterable,
    List,
    NamedTuple,
    Optional,
    Sequence,
    Tuple,
    Union,
    TYPE_CHECKING,
    TypeVar
)
from typing_extensions import Self

from pytiled_parser import Properties

from arcade.utils import (
    IntOutsideRangeError,
    ByteRangeError,
    NormalizedRangeError
)

if TYPE_CHECKING:
    from arcade.texture import Texture

MAX_UINT24 = 0xFFFFFF
MAX_UINT32 = 0xFFFFFFFF

ChannelType = TypeVar('ChannelType')

RGB = Tuple[ChannelType, ChannelType, ChannelType]
RGBA = Tuple[ChannelType, ChannelType, ChannelType, ChannelType]
RGBOrA = Union[RGB[ChannelType], RGBA[ChannelType]]

RGBOrA255 = RGBOrA[int]
RGBOrANormalized = RGBOrA[float]

RGBA255 = RGBA[int]
RGBANormalized = RGBA[float]

RGBA255OrNormalized = Union[RGBA255, RGBANormalized]


__all__ = [
    "BufferProtocol",
    "Color",
    "ColorLike",
    "IPoint",
    "PathOrTexture",
    "Point",
    "PointList",
    "EMPTY_POINT_LIST",
    "NamedPoint",
    "Rect",
    "RectList",
    "RGB",
    "RGBA255",
    "RGBANormalized",
    "RGBA255OrNormalized",
    "TiledObject",
    "Vector"
]


[docs] class Color(RGBA255): """ A :py:class:`tuple` subclass representing an RGBA Color. This class provides helpful utility methods and properties. When performance or brevity matters, arcade will usually allow you to use an ordinary :py:class:`tuple` of RGBA values instead. All channels are byte values from 0 to 255, inclusive. If any are outside this range, a :py:class:`~arcade.utils.ByteRangeError` will be raised, which can be handled as a :py:class:`ValueError`. Examples:: >>> from arcade.types import Color >>> Color(255, 0, 0) Color(r=255, g=0, b=0, a=0) >>> Color(*rgb_green_tuple, 127) Color(r=0, g=255, b=0, a=127) :param r: the red channel of the color, between 0 and 255 :param g: the green channel of the color, between 0 and 255 :param b: the blue channel of the color, between 0 and 255 :param a: the alpha or transparency channel of the color, between 0 and 255 """ def __new__(cls, r: int, g: int, b: int, a: int = 255): if not 0 <= r <= 255: raise ByteRangeError("r", r) if not 0 <= g <= 255: raise ByteRangeError("g", g) if not 0 <= g <= 255: raise ByteRangeError("b", b) if not 0 <= a <= 255: raise ByteRangeError("a", a) # Typechecking is ignored because of a mypy bug involving # tuples & super: # https://github.com/python/mypy/issues/8541 return super().__new__(cls, (r, g, b, a)) # type: ignore def __deepcopy__(self, _) -> Self: """Allow :py:func:`~copy.deepcopy` to be used with Color""" return self.__class__(r=self.r, g=self.g, b=self.b, a=self.a) def __repr__(self) -> str: return f"{self.__class__.__name__}(r={self.r}, g={self.g}, b={self.b}, a={self.a})" @property def r(self) -> int: return self[0] @property def g(self) -> int: return self[1] @property def b(self) -> int: return self[2] @property def a(self) -> int: return self[3]
[docs] @classmethod def from_iterable(cls, iterable: Iterable[int]) -> Self: """ Create a color from an :py:class`Iterable` with 3-4 elements If the passed iterable is already a Color instance, it will be returned unchanged. If the iterable has less than 3 or more than 4 elements, a ValueError will be raised. Otherwise, the function will attempt to create a new Color instance. The usual rules apply, ie all values must be between 0 and 255, inclusive. :param iterable: An iterable which unpacks to 3 or 4 elements, each between 0 and 255, inclusive. """ if isinstance(iterable, cls): return iterable # We use unpacking because there isn't a good way of specifying # lengths for sequences as of 3.8, our minimum Python version as # of March 2023: https://github.com/python/typing/issues/786 r, g, b, *_a = iterable if _a: if len(_a) > 1: raise ValueError("iterable must unpack to 3 or 4 values") a = _a[0] else: a = 255 return cls(r, g, b, a=a)
@property def normalized(self) -> RGBANormalized: """ Return this color as a tuple of 4 normalized floats. Examples:: >>> arcade.color.WHITE.normalized (1.0, 1.0, 1.0, 1.0) >>> arcade.color.BLACK.normalized (0.0, 0.0, 0.0, 1.0) >>> arcade.color.TRANSPARENT_BLACK.normalized (0.0, 0.0, 0.0, 0.0) """ return self[0] / 255, self[1] / 255, self[2] / 255, self[3] / 255
[docs] @classmethod def from_gray(cls, brightness: int, a: int = 255) -> Self: """ Return a shade of gray of the given brightness. Example:: >>> custom_white = Color.from_gray(255) >>> print(custom_white) Color(r=255, g=255, b=255, a=255) >>> half_opacity_gray = Color.from_gray(128, 128) >>> print(half_opacity_gray) Color(r=128, g=128, b=128, a=128) :param brightness: How bright the shade should be :param a: a transparency value, fully opaque by default :return: """ if not 0 <= brightness <= 255: raise ByteRangeError("brightness", brightness) if not 0 <= a <= 255: raise ByteRangeError("a", a) return cls(brightness, brightness, brightness, a=a)
[docs] @classmethod def from_uint24(cls, color: int, a: int = 255) -> Self: """ Return a Color from an unsigned 3-byte (24 bit) integer. These ints may be between 0 and 16777215 (``0xFFFFFF``), inclusive. Example:: >>> Color.from_uint24(16777215) Color(r=255, g=255, b=255, a=255) >>> Color.from_uint24(0xFF0000) Color(r=255, g=0, b=0, a=255) :param color: a 3-byte int between 0 and 16777215 (``0xFFFFFF``) :param a: an alpha value to use between 0 and 255, inclusive. """ if not 0 <= color <= MAX_UINT24: raise IntOutsideRangeError("color", color, 0, MAX_UINT24) if not 0 <= a <= 255: raise ByteRangeError("a", a) return cls( (color & 0xFF0000) >> 16, (color & 0xFF00) >> 8, color & 0xFF, a=a )
[docs] @classmethod def from_uint32(cls, color: int) -> Self: """ Return a Color tuple for a given unsigned 4-byte (32-bit) integer The bytes are interpreted as R, G, B, A. Examples:: >>> Color.from_uint32(4294967295) Color(r=255, g=255, b=255, a=255) >>> Color.from_uint32(0xFF0000FF) Color(r=255, g=0, b=0, a=255) :param color: An int between 0 and 4294967295 (``0xFFFFFFFF``) """ if not 0 <= color <= MAX_UINT32: raise IntOutsideRangeError("color", color, 0, MAX_UINT32) return cls( (color & 0xFF000000) >> 24, (color & 0xFF0000) >> 16, (color & 0xFF00) >> 8, a=(color & 0xFF) )
[docs] @classmethod def from_normalized(cls, color_normalized: RGBANormalized) -> Self: """ Convert normalized (0.0 to 1.0) channels into an RGBA Color If the input channels aren't normalized, a :py:class:`arcade.utils.NormalizedRangeError` will be raised. This is a subclass of :py:class`ValueError` and can be handled as such. Examples:: >>> Color.from_normalized((1.0, 0.0, 0.0, 1.0)) Color(r=255, g=0, b=0, a=255) >>> normalized_half_opacity_green = (0.0, 1.0, 0.0, 0.5) >>> Color.from_normalized(normalized_half_opacity_green) Color(r=0, g=255, b=0, a=127) :param color_normalized: The color as normalized (0.0 to 1.0) RGBA values. :return: """ r, g, b, *_a = color_normalized if _a: if len(_a) > 1: raise ValueError("color_normalized must unpack to 3 or 4 values") a = _a[0] if not 0.0 <= a <= 1.0: raise NormalizedRangeError("a", a) else: a = 1.0 if not 0.0 <= r <= 1.0: raise NormalizedRangeError("r", r) if not 0.0 <= g <= 1.0: raise NormalizedRangeError("g", g) if not 0.0 <= b <= 1.0: raise NormalizedRangeError("b", b) return cls(int(255 * r), int(255 * g), int(255 * b), a=int(255 * a))
[docs] @classmethod def from_hex_string(cls, code: str) -> Self: """ Make a color from a hex code that is 3, 4, 6, or 8 hex digits long Prefixing it with a pound sign (``#`` / hash symbol) is optional. It will be ignored if present. The capitalization of the hex digits (``'f'`` vs ``'F'``) does not matter. 3 and 6 digit hex codes will be treated as if they have an opacity of 255. 3 and 4 digit hex codes will be expanded. Examples:: >>> Color.from_hex_string("#ff00ff") Color(r=255, g=0, b=255, a=255) >>> Color.from_hex_string("#ff00ff00") Color(r=255, g=0, b=255, a=0) >>> Color.from_hex_string("#FFF") Color(r=255, g=255, b=255, a=255) >>> Color.from_hex_string("FF0A") Color(r=255, g=255, b=0, a=170) """ code = code.lstrip("#") # This looks unusual, but it matches CSS color code expansion # behavior for 3 and 4 digit hex codes. if len(code) <= 4: code = "".join(char * 2 for char in code) if len(code) == 6: # full opacity if no alpha specified return cls(int(code[:2], 16), int(code[2:4], 16), int(code[4:6], 16), 255) elif len(code) == 8: return cls(int(code[:2], 16), int(code[2:4], 16), int(code[4:6], 16), int(code[6:8], 16)) raise ValueError(f"Improperly formatted color: '{code}'")
[docs] @classmethod def random( cls, r: Optional[int] = None, g: Optional[int] = None, b: Optional[int] = None, a: Optional[int] = None, ) -> Self: """ Return a random color. The parameters are optional and can be used to fix the value of a particular channel. If a channel is not fixed, it will be randomly generated. Examples:: # Randomize all channels >>> Color.random() Color(r=35, g=145, b=4, a=200) # Random color with fixed alpha >>> Color.random(a=255) Color(r=25, g=99, b=234, a=255) :param r: Fixed value for red channel :param g: Fixed value for green channel :param b: Fixed value for blue channel :param a: Fixed value for alpha channel """ if r is None: r = random.randint(0, 255) if g is None: g = random.randint(0, 255) if b is None: b = random.randint(0, 255) if a is None: a = random.randint(0, 255) return cls(r, g, b, a)
ColorLike = Union[RGB, RGBA255] # Point = Union[Tuple[float, float], List[float]] # Vector = Point Point = Tuple[float, float] Point3 = Tuple[float, float, float] IPoint = Tuple[int, int] Vector = Point NamedPoint = namedtuple("NamedPoint", ["x", "y"]) PointList = Sequence[Point] # Speed / typing workaround: # 1. Eliminate extra allocations # 2. Allows type annotation to be cleaner, primarily for HitBox & subclasses EMPTY_POINT_LIST: PointList = tuple() Rect = Union[Tuple[int, int, int, int], List[int]] # x, y, width, height RectList = Union[Tuple[Rect, ...], List[Rect]] FloatRect = Union[Tuple[float, float, float, float], List[float]] # x, y, width, height PathOrTexture = Optional[Union[str, Path, "Texture"]]
[docs] class TiledObject(NamedTuple): shape: Union[Point, PointList, Rect] properties: Optional[Properties] = None name: Optional[str] = None type: Optional[str] = None
if sys.version_info >= (3, 12): from collections.abc import Buffer as BufferProtocol else: # This is used instead of the typing_extensions version since they # use an ABC which registers virtual subclasses. This will not work # with ctypes.Array since virtual subclasses must be concrete. # See: https://peps.python.org/pep-0688/ BufferProtocol = Union[ByteString, memoryview, array, ctypes.Array]