Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 38 additions & 15 deletions arcade/sprite/base.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
"""
Expand All @@ -342,28 +362,31 @@ 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) == 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._rgb[0] == color[0]
and self._rgb[1] == color[1]
and self._rgb[2] == color[2]
):
return
self._color = Color.from_iterable(color)
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) == 3:
elif len(color) == 4:
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]
and self._rgb[3] == color[3]
):
return
self._color = Color(color[0], color[1], color[2], self._color[3])
self._rgb = color
self._color = Color.from_iterable(color)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is cleaner, but it's worth doing benchmarks to see if it's slower. The reason pyglet code and the from_iterable method use the strange-looking unpacking method is that it's faster.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"I'm not sure I understand all the changes intended or mentioned."

The intent is to make it so RGB and alpha are used together to create color and by default not changing the alpha value of a sprite unless specifically called on.

The idea being that RGB can more easily be manipulated separately.


else:
raise ValueError("Color must be three or four ints from 0-255")

Expand Down
106 changes: 95 additions & 11 deletions arcade/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

from __future__ import annotations

import sys
from array import array
import ctypes
import random
Expand All @@ -20,8 +19,7 @@
Sequence,
Tuple,
Union,
TYPE_CHECKING,
TypeVar
TYPE_CHECKING, TypeVar
)
from typing_extensions import Self

Expand Down Expand Up @@ -75,6 +73,92 @@
]


class RGB(Tuple):
"""
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`.

: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
"""

def __new__(cls, r: int, g: int, b: int):

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)

# 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)) # 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)

def __repr__(self) -> str:
return f"{self.__class__.__name__}(r={self.r}, g={self.g}, b={self.b})"

@property
def r(self) -> int:
return self[0]

@property
def g(self) -> int:
return self[1]

@property
def b(self) -> int:
return self[2]

@classmethod
def random(
cls,
r: Optional[int] = None,
g: Optional[int] = None,
b: 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
"""
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)

return cls(r, g, b)


class Color(RGBA255):
"""
A :py:class:`tuple` subclass representing an RGBA Color.
Expand Down Expand Up @@ -450,11 +534,11 @@ class TiledObject(NamedTuple):
type: Optional[str] = None


if sys.version_info >= (3, 12):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why remove this? My concerns:

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wasn't intentional, I still have to clean it up before it is ready.

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]