From e2b3774780d497efa813e5d272e9851d557cadd8 Mon Sep 17 00:00:00 2001 From: FriendlyGecko <68018798+FriendlyGecko@users.noreply.github.com> Date: Sun, 11 Feb 2024 22:17:45 +0200 Subject: [PATCH 1/5] Updated the color setter so that it does not change the current alpha --- arcade/sprite/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 68e0d57b30..83c511b339 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -363,7 +363,7 @@ def color(self, color: RGBA255): and self._color[2] == color[2] ): return - self._color = Color(color[0], color[1], color[2], self._color[3]) + self._color = Color(color[0], color[1], color[2], self._alpha) else: raise ValueError("Color must be three or four ints from 0-255") From 11efaab602cda0896079066f161a18754485614b Mon Sep 17 00:00:00 2001 From: FriendlyGecko <68018798+FriendlyGecko@users.noreply.github.com> Date: Sun, 11 Feb 2024 22:39:20 +0200 Subject: [PATCH 2/5] Updated it to check for use arcade.Color Added an isintance check to the len3 check and moved those before the len4 check. So now if the user does a tuple of 3 (ie (255,255,255)) or uses the arcade.Color type (i.e. arcade.Color.BLACK) it will just reuse the old alpha. --- arcade/sprite/base.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 83c511b339..2dd1483cdd 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -346,24 +346,25 @@ def color(self) -> Color: @color.setter def color(self, color: RGBA255): - if len(color) == 4: + if len(color) == 3 or isinstance(color, Color): if ( - self._color[0] == color[0] - and self._color[1] == color[1] - and self._color[2] == color[2] - and self._color[3] == color[3] + self._color[0] == color[0] + and self._color[1] == color[1] + and self._color[2] == color[2] ): return - self._color = Color.from_iterable(color) + self._color = Color(color[0], color[1], color[2], self.alpha) - elif len(color) == 3: + elif len(color) == 4: if ( self._color[0] == color[0] and self._color[1] == color[1] and self._color[2] == color[2] + and self._color[3] == color[3] ): return - self._color = Color(color[0], color[1], color[2], self._alpha) + self._color = Color.from_iterable(color) + else: raise ValueError("Color must be three or four ints from 0-255") From 6a1d2b43341e4edffa1571e1c52b87ed0679db96 Mon Sep 17 00:00:00 2001 From: FriendlyGecko <68018798+FriendlyGecko@users.noreply.github.com> Date: Mon, 12 Feb 2024 11:09:26 +0200 Subject: [PATCH 3/5] Demo 1 --- arcade/sprite/base.py | 46 ++++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 2dd1483cdd..92cd2d42e8 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -1,9 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Iterable, List, TypeVar, Any +from typing import TYPE_CHECKING, Iterable, List, TypeVar import arcade -from arcade.types import Point, Color, RGBA255, PointList +from arcade.types import Point, Color, RGBA255, PointList, RGB from arcade.color import BLACK from arcade.hitbox import HitBox from arcade.texture import Texture @@ -49,7 +49,7 @@ def __init__( scale: float = 1.0, center_x: float = 0, center_y: float = 0, - **kwargs: Any, + **kwargs, ) -> None: self._position = (center_x, center_y) self._depth = 0.0 @@ -58,6 +58,7 @@ def __init__( self._height = texture.height * scale self._scale = scale, scale self._color: Color = Color(255, 255, 255, 255) + self._rgb: RGB = RGB(255, 255, 255) self.sprite_lists: List["SpriteList"] = [] # Core properties we don't use, but spritelist expects it @@ -319,6 +320,25 @@ def visible(self, value: bool): for sprite_list in self.sprite_lists: sprite_list._update_color(self) + @property + def rgb(self) -> RGB: + """ Gets the sprites RGB. """ + return self._rgb + + @rgb.setter + def rgb(self, color: RGB): + if ( + self._rgb[0] == color[0] + and self._rgb[1] == color[1] + and self._rgb[2] == color[2] + ): + return + self._rgb = RGB(color[0], color[1], color[2]) + self._color = Color(self._rgb[0], self._rgb[1], self._rgb[2], self.alpha) + + for sprite_list in self.sprite_lists: + sprite_list._update_color(self) + @property def color(self) -> Color: """ @@ -342,27 +362,29 @@ def color(self) -> Color: >>> sprite.color = 255, 0, 0, 128 """ - return self._color + return Color(self.rgb[0], self.rgb[1], self.rgb[2], self.alpha) @color.setter def color(self, color: RGBA255): if len(color) == 3 or isinstance(color, Color): if ( - self._color[0] == color[0] - and self._color[1] == color[1] - and self._color[2] == color[2] + self._rgb[0] == color[0] + and self._rgb[1] == color[1] + and self._rgb[2] == color[2] ): return - self._color = Color(color[0], color[1], color[2], self.alpha) + self._rgb = RGB(color[0], color[1], color[2]) + self._color = Color(self._rgb[0], self._rgb[1], self._rgb[2], self.alpha) elif len(color) == 4: if ( - self._color[0] == color[0] - and self._color[1] == color[1] - and self._color[2] == color[2] - and self._color[3] == color[3] + self._rgb[0] == color[0] + and self._rgb[1] == color[1] + and self._rgb[2] == color[2] + and self._rgb[3] == color[3] ): return + self._rgb = color self._color = Color.from_iterable(color) else: From d8ae39b05608bf792a7c9bedd0a072e0bdbb3958 Mon Sep 17 00:00:00 2001 From: FriendlyGecko <68018798+FriendlyGecko@users.noreply.github.com> Date: Mon, 12 Feb 2024 11:10:50 +0200 Subject: [PATCH 4/5] Added deeper RGB class --- arcade/types.py | 361 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 350 insertions(+), 11 deletions(-) diff --git a/arcade/types.py b/arcade/types.py index f904c5b2ff..421f0f6ac7 100644 --- a/arcade/types.py +++ b/arcade/types.py @@ -5,7 +5,6 @@ from __future__ import annotations -import sys from array import array import ctypes import random @@ -20,8 +19,7 @@ Sequence, Tuple, Union, - TYPE_CHECKING, - TypeVar + TYPE_CHECKING, TypeVar ) from typing_extensions import Self @@ -74,6 +72,347 @@ "Vector" ] +class RGB(Tuple): + """ + 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] + + @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 + + @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) + + @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 + ) + + @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) + ) + + @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)) + + @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}'") + + @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) class Color(RGBA255): """ @@ -450,11 +789,11 @@ class TiledObject(NamedTuple): 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] +# This is a temporary workaround for the lack of a way to type annotate +# objects implementing the buffer protocol. Although there is a PEP to +# add typing, it is scheduled for 3.12. Since that is years away from +# being our minimum Python version, we have to use a workaround. See +# the PEP and Python doc for more information: +# https://peps.python.org/pep-0688/ +# https://docs.python.org/3/c-api/buffer.html +BufferProtocol = Union[ByteString, memoryview, array, ctypes.Array] From 29c14b0001305aa255e6b85f4e6dc523975b9dc2 Mon Sep 17 00:00:00 2001 From: FriendlyGecko <68018798+FriendlyGecko@users.noreply.github.com> Date: Mon, 12 Feb 2024 14:11:42 +0200 Subject: [PATCH 5/5] cleaned up RGB --- arcade/types.py | 279 +++--------------------------------------------- 1 file changed, 12 insertions(+), 267 deletions(-) diff --git a/arcade/types.py b/arcade/types.py index 421f0f6ac7..5315600059 100644 --- a/arcade/types.py +++ b/arcade/types.py @@ -72,35 +72,21 @@ "Vector" ] + class RGB(Tuple): """ - 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. + A :py:class:`tuple` subclass representing an RGB color. 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): + def __new__(cls, r: int, g: int, b: int): if not 0 <= r <= 255: raise ByteRangeError("r", r) @@ -111,20 +97,17 @@ def __new__(cls, r: int, g: int, b: int, a: int = 255): 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 + return super().__new__(cls, (r, g, b)) # 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) + return self.__class__(r=self.r, g=self.g, b=self.b) def __repr__(self) -> str: - return f"{self.__class__.__name__}(r={self.r}, g={self.g}, b={self.b}, a={self.a})" + return f"{self.__class__.__name__}(r={self.r}, g={self.g}, b={self.b})" @property def r(self) -> int: @@ -138,248 +121,12 @@ def g(self) -> int: def b(self) -> int: return self[2] - @property - def a(self) -> int: - return self[3] - - @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 - - @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) - - @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 - ) - - @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) - ) - - @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)) - - @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}'") - @classmethod def random( - cls, - r: Optional[int] = None, - g: Optional[int] = None, - b: Optional[int] = None, - a: Optional[int] = None, + cls, + r: Optional[int] = None, + g: Optional[int] = None, + b: Optional[int] = None, ) -> Self: """ Return a random color. @@ -401,7 +148,6 @@ def random( :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) @@ -409,10 +155,9 @@ def random( 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) + return cls(r, g, b) + class Color(RGBA255): """