From d4f61e11aa6ca9e0a56294d9ca75935543b235c3 Mon Sep 17 00:00:00 2001 From: Paul <36696816+pushfoo@users.noreply.github.com> Date: Mon, 15 Apr 2024 22:05:05 -0400 Subject: [PATCH] Add .rgb properties + tests for Color, Sprite, and BasicSprite (#2060) * FriendlyGecko's initial Color RGB PR * Updated the color setter so that it does not change the current alpha * 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. * Demo 1 * Added deeper RGB class * cleaned up RGB * Imports + clean up .rgb property * Revert BasicSprite.color changes * Revert RGB type since 0 tests + conflicts w/ existing type alias * Revert whitespace change * Rephrase BasicSprite.rgb docstring * Revert sys removal * Revert typing import noise * Revert BufferProtocol change * Add Color.rgb property * Add quick tests for Color.rgb property * Add tests for BasicSprite / Sprite .rgb property * Bug/typo fix + test tweaks * Fix typo in color type tests --------- Co-authored-by: FriendlyGecko <68018798+FriendlyGecko@users.noreply.github.com> --- arcade/sprite/base.py | 40 +++++++++++++++++++++++- arcade/types.py | 20 ++++++++++++ tests/unit/color/test_color_type.py | 10 ++++++ tests/unit/sprite/test_sprite.py | 48 +++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 1 deletion(-) 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)