Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add .rgb properties + tests for Color, Sprite, and BasicSprite #2060

Merged
merged 14 commits into from
Apr 16, 2024
40 changes: 39 additions & 1 deletion arcade/sprite/base.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
"""
Expand Down
20 changes: 20 additions & 0 deletions arcade/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down
10 changes: 10 additions & 0 deletions tests/unit/color/test_color_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 48 additions & 0 deletions tests/unit/sprite/test_sprite.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading