Skip to content

Commit

Permalink
Add .rgb properties + tests for Color, Sprite, and BasicSprite (#2060)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
pushfoo and FriendlyGecko committed Apr 16, 2024
1 parent 42c5419 commit d4f61e1
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 1 deletion.
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

0 comments on commit d4f61e1

Please sign in to comment.