Closed pushfoo closed 7 months ago
(I am in fact on Discord, I'm just named differently. I changed my display name now to match.)
I like making most of the constants 3-length (rgb) and I also like the suggested instance methods.
I wonder if it would be user-unfriendly to have Color be 3 or 4 length, since that means that it is not obvious whether setting the color property of a Sprite will affect the alpha or not - it would depend on the value of the Color I think it would be better than currently, where setting color to a constant always changes alpha.
I have not followed the whole "RGBA discussion" - I understand that, somewhere internally, colors make sense as RGBA, but it has never really made much sense to me to use RGBA at the top-level user-facing API of arcade, especially since properties like "color" and "alpha" imply that RGB and A are separate. For me as a user, it would make more sense if Color was length 3 RGB and alpha something completely separate. Internally, in the Sprite and below that, one could handle it as RGBA if one wanted to (but it sounds as if pyglet uses RGB...?).
Ultimately, from a game-making perspective, it feels like there are more times that I want to adjust RGB and A separately than I want to adjust them both at the same time (but maybe that's just me). Sprite having shorthand properties like "visible" which adjust alpha seems to suggest that this is known (?).
Basically, I do not have the advantages of RGBA clear to me, I just see the drawbacks in terms of user-friendliness. I have occasionally defined colors with alpha values and reused those (I think my menu windows are some transparent grey), but those are very niche cases, not the most common ones.
@pushfoo wanted to compare how other engines handle color. Here are links and summaries for a few popular engines. I didn't include Unreal because I've never used it and the docs confuse me.
mycolor = #00FF00
https://docs.godotengine.org/en/stable/classes/class_color.html
https://docs.unity3d.com/ScriptReference/Color.html https://docs.unity3d.com/ScriptReference/Material.html
https://www.pygame.org/docs/ref/color.html https://www.pygame.org/docs/ref/draw.html#pygame.draw.rect
colorRgb
and opacity
fields, no field combining the twoNot a game engine, but came across this color library: https://pkg.go.dev/image/color Python color libraries: https://github.com/waveform80/colorzero -- uses namedtuple, closest to what we want to do. But only rgb, no alpha https://grapefruit.readthedocs.io/en/latest/ https://python-colormath.readthedocs.io/en/latest/
I need to read your proposal again -- have skimmed -- but I like your proposed changes so far.
One thing I did not see mentioned:
Will users of arcade always be able to assume that colors are instances of Color
and have all the helpful properties and methods? Even when a color is assigned a non-Color
tuple, will we convert to Color
internally?
Here is an example of where this is convenient for users:
# Assign color of my_sprite_b
my_sprite.color = (255, 255, 0, 128)
# Elsewhere in the codebase, copy the color, not the alpha, from one sprite to another
other_sprite.color = my_sprite.color.rgb
# This assumes that `my_sprite.color` is an instance of `Color` despite the assignment above
Is this guarantee worth making? Is performance acceptable?
In case it helps, I tried to list all the things I imagine a beginner wanting to do easily in their games. I left the syntax column blank to avoid committing to a particular implementation, but I can fill it in with python snippets if that helps.
Use-case | Possible syntax |
---|---|
Get color and alpha of a thing | |
Get color of a thing without alpha | |
Get alpha of a thing without color | |
Set color and alpha of a thing | |
Set color of a thing, don't change alpha | |
Set alpha of a thing, don't change color | |
Tweak single color channel of a thing's color, e.g. bump up the red | |
Declare color as CSS hexcode | |
Declare color as numbers 0-255 copied from image editor | |
Transform colors, e.g. blend 2 colors, adjust saturation, fade in/out | |
Ditto, but w/normalized values | |
Get normalized (float) color from int, and vice versa | |
Use normalized (float) color to update thing w/out gotchas | sprite.color = normalized_color |
Use the same color for a pyglet thing as an arcade thing (e.g. a sprite and a pyglet shape of the same color) |
@pushfoo my 2cents for your questions
I prefer option 1. because I agree with @bunny-therapist's concern that variable length might be user-unfriendly.
It seems easier for every Color
to be guaranteed 4-length with 4 ints (never None
) so that anything hinted Color
can assume an alpha
int. Maybe I'm too biased towards static typing.
I like with_alpha(self, alpha: int) -> Color
and Color.opaque(self) -> Color
.
Color.from_hex_string()
can return a Color
with alpha defaulting to 255, then you can chain .rgb
as needed.
Agreed that helper methods are best, not additional classes. I like your example def from_hsv255(self, h: int, s: int: v: int) -> Self:)
Doesn't seem useful. IMO we subclass NamedTuple
. If it allows subclassing, great, but it's not necessary.
As nifty as this might be, it seems too complex. It sounds like a clever, fun idea, but too complex.
We already have a bunch of tuple attributes with multiple setters to modify them: position
/center_x
/center_y
, velocity
/change_x
/change_y
, scale
/scale_xy
Update tl;dr I'm not yet sure on some the questions, but i'm still working on this by making pyglet's color handling consistent
We don't need to implement HSV conversion ourselves. There's a module in the standard library which takes care of this: https://docs.python.org/3.8/library/colorsys.html
Didn't see this prior to making #1970
Is there any room for my pull or should I drop it?
Is there any room for my pull or should I drop it?
In my opinion, scale back that specific PR for now. I'll comment more on the PR itself. This issue thread is full of ambitious and complicated stuff which got us bogged down. I'm tempted to leave most of it for the future so we can get 3.0 out the door.
TL;DR: Closing this ticket for the moment, will revisit as needed.
Fixing Cameras and doc build takes priority for 3.0. We'll definitely do the following:
.rgb
property to Color
type instances which returns Tuple[int, int, int]
If time allows:
Color
-related things (probably requires search and replace)
Enhancement / Request for Feedback: Color API
tl;dr
Color
type have multiple flaws as shown by #1838My current (and loosely held) idea for fixing this while preserving our "keep the alpha unless replaced" behavior:
RGBOrA255
instead ofRGB255
for color arguments throughout the codebaseColor
type 3-length except when an alpha value is providedColor.rgb
property as beginner-friendly shorthand forcolor_instance[:3]
TRANSPARENT_BLACK
alpha
on a 3-lengthColor
, which happens:raise IndexError()
orreturn None
?OPAQUE_WHITE
color constant and use itreplace(self, r = None, g = None, b = None, alpha = None) -> Color
andopaque() -> Color
instance methods toColor
opacity
properties throughout the codebase to match pyglet's APIalpha
property toColor
to be consistent with our own propertiesPlease comment on known issues or mention ones not yet covered.
Once we reach rough consensus, we can turn the items above into a checkboxes and start making PRs.
Known Problems
As pointed out in #1838, there are three main sources of confusion:
RGBA255
as a type hintColor
doesn't provide a beginner-friendly way to use only the RGB componentsThese are not minor issues. Many users will think of color and opacity as separate because:
While reviewing arcade and pyglet's code, I also noticed pyglet uses
opacity
for its alpha-related property, while we usealpha
anda
.What we have now
To my understanding, Arcade's color handling includes alpha because:
pyglet.Sprite
is the only exception, and it will be changed shortly)Since this is supposed to be a beginner-friendly library, it was a mistake to try to force users to use RGBA. Additionally, pyglet does not seem to be planning on dropping RGB support any time soon, so we have no reason to drop it.
ChannelType = TypeVar('ChannelType')
types.py
RGB = Tuple[ChannelType, ChannelType, ChannelType]
types.py
RGBA = Tuple[ChannelType, ChannelType, ChannelType, ChannelType]
types.py
RGBOrA = Union[RGB[ChannelType], RGBA[ChannelType]]
types.py
RGBOrA255 = RGBOrA[int]
arcade.text
RGBOrANormalized = RGBOrA[float]
arcade.types
RGBA255OrNormalized = Union[RGBA255, RGBANormalized]
Window.clear()
andView.clear()
clear
shouldn't use this.class Color[RGBA255]:
arcade.color
andarcade.csscolor
There is also a paused PR for adding a float version of the
Color
class (#1772). It would also help with #1794, among other issues. I've paused for multiple reasons, some of which include uncertainty about the color API.Top
Color
Questions2. How Should
Color
Handle Length & Alpha?Length
I see the following as worth considering for our
Color
type and constants:.rgb
property to get the rgb channels, as suggested by #1838In the latter case, we could still have constants like
arcade.color.TRANSPARENT_BLACK
. However, we'd have to make the following changes:with_alpha(self, alpha: int) -> Color
andColor.opaque(self) -> Color
instance methodsColor.from_hex_string()
We can also mix it with an
.rgb
property since users may find it helpful if they write / use color mixing methods.At the moment, the 3 or 4 length option seems best to me.
Alpha
How should accessing
alpha
on a 3-length color be handled?IndexError
None
3. Should We Support Non-RGB(A)? If so, how?
@cspotcode has brought up non-RGBA support. It has also been brought up on Discord at least once.
I've argued in favor of RGB/RGBA only for the following reasons:
If we want to enable support for non-RGBA, we have the following options:
def from_hsv255(self, h: int, s: int: v: int) -> Self:
)colorsys
Helper methods seem like the best option since they can be added at any time without performance or complexity penalties.
4. Should We Support Subclassing? If so, how?
Doing so may preclude optimizations such as #1841. However, we may be able to side-step this issue entirely if we define color as a
Protocol
type as outlined below.Wish List Ideas
These are potentially thorny issues and probably best avoided.
I've mentioned them in case someone else can think of ways around their problems.
1. Color as a Protocol Type?
Using
Protocol
types could open the door to additional benefits.Pros:
Cons:
Protocol
types marked with@runtime_checkable
do not check types when used withisinstance
because the decorator does not enable checking signatures of methods1. Assignment to Slices of Color Properties?
As of now, I haven't found a good way to support syntax for the following:
By "good", I mean the following:
If we build this on the
Protocol
concept above, we might be able get the following:sprite_instance.color.rgb = 128, 0, 0
However, it would have the following downsides:
Protocols
do not appear to support it