diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 4d3272a43..41d2c2f33 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Iterable, List, TypeVar, Any +from typing import TYPE_CHECKING, Iterable, List, TypeVar, Any, Tuple import arcade from arcade.types import Point, Color, RGBA255, RGBOrA255, PointList @@ -337,6 +337,44 @@ def visible(self, value: bool): for sprite_list in self.sprite_lists: sprite_list._update_color(self) + @property + def rgb(self) -> Tuple[int, int, int]: + """Get or set only the sprite's RGB color components. + + If a 4-color RGBA tuple is passed: + + * The new color's alpha value will be ignored + * The old alpha value will be preserved + + """ + return self._color[:3] + + @rgb.setter + def rgb(self, color: RGBOrA255): + + # Fast validation of size by unpacking channel values + try: + r, g, b, *_a = color + if len(_a) > 1: # Alpha's only used to validate here + raise ValueError() + + except ValueError as _: # It's always a length issue + raise ValueError(( + f"{self.__class__.__name__},rgb takes 3 or 4 channel" + f" colors, but got {len(color)} channels")) + + # Unpack to avoid index / . overhead & prep for repack + current_r, current_b, current_g, a = self._color + + # Do nothing if equivalent to current color + if current_r == r and current_g == g and current_b == b: + return + + # Preserve the current alpha value & update sprite lists + self._color = Color(r, g, b, a) + for sprite_list in self.sprite_lists: + sprite_list._update_color(self) + @property def color(self) -> Color: """ diff --git a/arcade/types.py b/arcade/types.py index 36f2dc8db..b8e328fa4 100644 --- a/arcade/types.py +++ b/arcade/types.py @@ -159,6 +159,26 @@ def b(self) -> int: def a(self) -> int: return self[3] + @property + def rgb(self) -> Tuple[int, int, int]: + """Return only a color's RGB components. + + This is syntactic sugar for slice indexing as below: + + .. code-block:: python + + >>> from arcade.color import WHITE + >>> WHITE[:3] + (255, 255, 255) + # Equivalent but slower than the above + >>> (WHITE.r, WHITE.g, WHITE.b) + (255, 255, 255) + + To reorder the channels as you retrieve them, see + :meth:`.swizzle`. + """ + return self[:3] + @classmethod def from_iterable(cls, iterable: Iterable[int]) -> Self: """ diff --git a/tests/unit/color/test_color_type.py b/tests/unit/color/test_color_type.py index cdf063930..7bf5dc11f 100644 --- a/tests/unit/color/test_color_type.py +++ b/tests/unit/color/test_color_type.py @@ -190,6 +190,16 @@ def test_color_normalized_property(): assert colors.GRAY.normalized == (128 / 255, 128 / 255, 128 / 255, 1.0) +def test_color_rgb_property(): + # Try some bounds + assert colors.WHITE.rgb == (255, 255, 255) + assert colors.BLACK.rgb == (0, 0, 0) + + # Spot check unique colors + assert colors.COBALT.rgb == (0, 71, 171) + assert Color(1,3,5,7).rgb == (1, 3, 5) + + def test_deepcopy_color_values(): expected_color = Color(255, 255, 255, 255) assert deepcopy(expected_color) == expected_color diff --git a/tests/unit/sprite/test_sprite.py b/tests/unit/sprite/test_sprite.py index 1e88f6221..9e33b27c4 100644 --- a/tests/unit/sprite/test_sprite.py +++ b/tests/unit/sprite/test_sprite.py @@ -336,6 +336,54 @@ def test_visible(): assert sprite.alpha == 100 +def test_sprite_rgb_property_basics(): + sprite = arcade.Sprite(":resources:images/animated_characters/female_person/femalePerson_idle.png") + + # Initial multiply tint is white + assert sprite.rgb == (255, 255, 255) + + # Values which are too short are not allowed + with pytest.raises(ValueError): + sprite.rgb = (1,2) + with pytest.raises(ValueError): + sprite.rgb = (0,) + + # Nor are values which are too long + with pytest.raises(ValueError): + sprite.rgb = (100,100,100,100,100) + + # Test color setting + .rgb report when .visible == True + sprite.rgb = (1, 3, 5, 7) + assert sprite.color.r == 1 + assert sprite.color.g == 3 + assert sprite.color.b == 5 + assert sprite.rgb[0] == 1 + assert sprite.rgb[1] == 3 + assert sprite.rgb[2] == 5 + + # Test alpha preservation + assert sprite.color.a == 255 + assert sprite.alpha == 255 + + # Test .rgb sets rgb chanels when visible == False as with .color, + # but also still preserves original alpha values. + sprite.visible = False + sprite.color = (9, 11, 13, 15) + sprite.rgb = (17, 21, 23, 25) + + # Check the color channels + assert sprite.color.r == 17 + assert sprite.color.g == 21 + assert sprite.color.b == 23 + assert sprite.rgb[0] == 17 + assert sprite.rgb[1] == 21 + assert sprite.rgb[2] == 23 + + # Alpha preserved? + assert sprite.color.a == 15 + assert sprite.alpha == 15 + + def test_sprite_scale_xy(window): sprite = arcade.SpriteSolidColor(20, 20, color=arcade.color.WHITE)