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

[WIP] Refactor colors #718

Draft
wants to merge 17 commits into
base: canon
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 13 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
2 changes: 1 addition & 1 deletion docs/reference/scenes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ transition at any time.

.. autoattribute:: background_color

An RGB triple of the background, eg ``(0, 127, 255)``
The background color of the scene, e.g. ``ppb.Color(0, 127, 255)``

.. autoattribute:: main_camera

Expand Down
20 changes: 10 additions & 10 deletions examples/rectangular_assets/main.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import ppb


tall_rectangle = ppb.Rectangle(200, 0, 0, (1, 2))
wide_rectangle = ppb.Rectangle(100, 200, 0, (2, 1))
square = ppb.Square(200, 200, 100)
tall_triangle = ppb.Triangle(0, 200, 0, (1, 2))
wide_triangle = ppb.Triangle(0, 200, 100, (2, 1))
square_triangle = ppb.Triangle(50, 200, 150)
tall_ellipse = ppb.Ellipse(0, 0, 200, (1, 2))
wide_ellipse = ppb.Ellipse(100, 0, 200, (2, 1))
circle = ppb.Circle(150, 50, 200)
tall_rectangle = ppb.Rectangle(ppb.Color(200, 0, 0), (1, 2))
wide_rectangle = ppb.Rectangle(ppb.Color(100, 200, 0), (2, 1))
square = ppb.Square(ppb.Color(200, 200, 100))
tall_triangle = ppb.Triangle(ppb.Color(0, 200, 0), (1, 2))
wide_triangle = ppb.Triangle(ppb.Color(0, 200, 100), (2, 1))
square_triangle = ppb.Triangle(ppb.Color(50, 200, 150))
tall_ellipse = ppb.Ellipse(ppb.Color(0, 0, 200), (1, 2))
wide_ellipse = ppb.Ellipse(ppb.Color(100, 0, 200), (2, 1))
circle = ppb.Circle(ppb.Color(150, 50, 200))


def setup(scene):
scene.background_color = (0, 0, 0)
scene.background_color = ppb.Color(0, 0, 0)
scene.add(ppb.RectangleSprite(width=0.5, height=1, image=tall_rectangle, position=(-2, 2)))
scene.add(ppb.RectangleSprite(width=1, height=0.5, image=wide_rectangle, position=(0, 2)))
scene.add(ppb.Sprite(size=1, image=square, position=(2, 2)))
Expand Down
6 changes: 3 additions & 3 deletions examples/two-phase-updates/three_body.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ def on_update(self, event, signal):


def setup(scene):
scene.add(Planet(position=(3, 0), velocity=Vector(0, 1), image=ppb.Circle(40, 200, 150)))
scene.add(Planet(position=(-3, 3), velocity=Vector(1, -1), image=ppb.Circle(200, 150, 40)))
scene.add(Planet(position=(-3, -3), velocity=Vector(-1, 0), image=ppb.Circle(150, 40, 200)))
scene.add(Planet(position=(3, 0), velocity=Vector(0, 1), image=ppb.Circle(ppb.Color(40, 200, 150))))
scene.add(Planet(position=(-3, 3), velocity=Vector(1, -1), image=ppb.Circle(ppb.Color(200, 150, 40))))
scene.add(Planet(position=(-3, -3), velocity=Vector(-1, 0), image=ppb.Circle(ppb.Color(150, 40, 200))))


if __name__ == "__main__":
Expand Down
4 changes: 3 additions & 1 deletion ppb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
* :mod:`buttons`
* :mod:`keycodes`
* :mod:`flags`
* :mod:`colors`
* :mod:`directions`
* :class:`Signal`
"""
Expand All @@ -47,6 +48,7 @@
from ppb.assets import Rectangle
from ppb.assets import Square
from ppb.assets import Triangle
from ppb.colors import Color
from ppb.engine import GameEngine
from ppb.engine import Signal
from ppb.scenes import Scene
Expand All @@ -62,7 +64,7 @@
# Shortcuts
'Scene', 'Sprite', 'RectangleSprite', 'Vector',
'Image', 'Circle', 'Ellipse', 'Square', 'Rectangle', 'Triangle',
'Font', 'Text', 'Sound',
'Color', 'Font', 'Text', 'Sound',
'events', 'buttons', 'keycodes', 'flags', 'directions', 'Signal',
# Local stuff
'run', 'make_engine',
Expand Down
31 changes: 15 additions & 16 deletions ppb/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
)

from ppb.assetlib import BackgroundMixin, FreeingMixin, AbstractAsset
from ppb.colors import Color, BLACK, MAGENTA
from ppb.systems.sdl_utils import sdl_call

__all__ = (
Expand All @@ -31,8 +32,6 @@
"Ellipse"
)

BLACK = 0, 0, 0
MAGENTA = 255, 71, 182
DEFAULT_SPRITE_SIZE = 64


Expand All @@ -41,7 +40,7 @@ class AspectRatio(NamedTuple):
height: Union[int, float]


def _create_surface(color, aspect_ratio: AspectRatio = AspectRatio(1, 1)):
def _create_surface(color: Color, aspect_ratio: AspectRatio = AspectRatio(1, 1)):
"""
Creates a surface for assets and sets the color key.
"""
Expand Down Expand Up @@ -72,8 +71,8 @@ def _create_surface(color, aspect_ratio: AspectRatio = AspectRatio(1, 1)):

class Shape(BackgroundMixin, FreeingMixin, AbstractAsset):
"""Shapes are drawing primitives that are good for rapid prototyping."""
def __init__(self, red: int, green: int, blue: int, aspect_ratio: aspect_ratio_type = AspectRatio(1, 1)):
self.color = red, green, blue
def __init__(self, color: Color, aspect_ratio: aspect_ratio_type = AspectRatio(1, 1)):
self.color = color
self.aspect_ratio = AspectRatio(*aspect_ratio)
self._start()

Expand All @@ -85,7 +84,7 @@ def _background(self):
_check_error=lambda rv: not rv
)
try:
self._draw_shape(renderer, rgb=self.color)
self._draw_shape(renderer, color=self.color)
finally:
sdl_call(SDL_DestroyRenderer, renderer)
return surface
Expand All @@ -104,9 +103,9 @@ class Rectangle(Shape):
A rectangle image of a single color.
"""

def _draw_shape(self, renderer, rgb, **_):
def _draw_shape(self, renderer, color: Color, **_):
sdl_call(
SDL_SetRenderDrawColor, renderer, *(int(c) for c in rgb), 255,
SDL_SetRenderDrawColor, renderer, *color, 255,
_check_error=lambda rv: rv < 0
)
sdl_call(
Expand All @@ -120,17 +119,17 @@ class Square(Rectangle):
A constructor for :class:`~ppb.Rectangle` that produces a square image.
"""

def __init__(self, r, g, b):
def __init__(self, color: Color):
# This cuts out the aspect_ratio parameter
super().__init__(r, g, b)
super().__init__(color)


class Triangle(Shape):
"""
A triangle image of a single color.
"""

def _draw_shape(self, renderer, rgb, **_):
def _draw_shape(self, renderer, color: Color, **_):
w, h = c_int(), c_int()
sdl_call(SDL_GetRendererOutputSize, renderer, byref(w), byref(h))
width, height = w.value, h.value
Expand All @@ -140,7 +139,7 @@ def _draw_shape(self, renderer, rgb, **_):
0, height,
int(width / 2), 0,
width, height,
*rgb, 255,
*color, 255,
_check_error=lambda rv: rv < 0
)

Expand All @@ -150,7 +149,7 @@ class Ellipse(Shape):
An ellipse image of a single color.
"""

def _draw_shape(self, renderer, rgb, **_):
def _draw_shape(self, renderer, color: Color, **_):
w, h = c_int(), c_int()
sdl_call(SDL_GetRendererOutputSize, renderer, byref(w), byref(h))
half_width, half_height = int(w.value / 2), int(h.value / 2)
Expand All @@ -159,14 +158,14 @@ def _draw_shape(self, renderer, rgb, **_):
filledEllipseRGBA, renderer,
half_width, half_height, # Center
half_width, half_height, # Radius
*rgb, 255,
*color, 255,
_check_error=lambda rv: rv < 0
)


class Circle(Ellipse):
"""A convenience constructor for :class:`~ppb.Ellipse` that is a perfect circle."""

def __init__(self, r, g, b):
def __init__(self, color: Color):
# This cuts out the aspect_ratio parameter
super().__init__(r, g, b)
super().__init__(color)
36 changes: 36 additions & 0 deletions ppb/colors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from dataclasses import dataclass
import colorsys

@dataclass()
class Color():
Copy link
Member

Choose a reason for hiding this comment

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

Do we want to call this RGB instead?

"""An RGB color, with red, green, and blue values ranging from 0-255."""
red: int
green: int
blue: int

def __post_init__(self):
for key, value in {'red': self.red, 'green': self.green, 'blue': self.blue}.items():
if value < 0:
raise ValueError(f'{key} cannot be less than 0.')
elif value > 255:
raise ValueError(f'{key} cannot be greater than 255.')

def __iter__(self):
return (self.red, self.green, self.blue)

@staticmethod
def from_hsv(hue: float, saturation: float, value: float):
Copy link
Member

Choose a reason for hiding this comment

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

I would rather have HSV().to_rgb()?

@pathunstrom your thoughts?

Copy link
Author

Choose a reason for hiding this comment

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

Concerned about that approach - I think there should only be one class for color so that it is intuitive to users that that’s the class they should plug in whenever they need to use a color. Don’t want people to accidentally use an HSV color when the class is expecting an RGB color, for example.

Copy link
Author

Choose a reason for hiding this comment

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

I decided to refactor it into an abstract base class Color, with RGBColor and HSVColor subclasses that all have a to_rgb() method that should generally be abstracted away from the user. Let me know what you think!

"""Convert the given HSV color values to an RGB color."""
red, green, blue = colorsys.hsv_to_rgb(hue, saturation, value)
return Color(int(red * 256), int(green * 256), int(blue * 256))


BLACK = Color(0, 0, 0)
WHITE = Color(255, 255, 255)
GRAY = Color(127, 127, 127)
RED = Color(255, 0, 0)
GREEN = Color(0, 255, 0)
BLUE = Color(0, 0, 255)
CYAN = Color(0, 255, 255)
MAGENTA = Color(255, 0, 255)
YELLOW = Color(255, 255, 0)
3 changes: 2 additions & 1 deletion ppb/scenes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
from typing import Sequence

from ppb.camera import Camera
from ppb.colors import Color
from ppb.gomlib import GameObject


class Scene(GameObject):
# Background color, in RGB, each channel is 0-255
background_color: Sequence[int] = (0, 0, 100)
background_color: Color
camera_class = Camera
show_cursor = True

Expand Down
4 changes: 2 additions & 2 deletions ppb/systems/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,9 +227,9 @@ def on_render(self, render_event, signal):
sdl_call(SDL_RenderPresent, self.renderer)

def render_background(self, scene):
bg = scene.background_color
background_color = scene.background_color
sdl_call(
SDL_SetRenderDrawColor, self.renderer, int(bg[0]), int(bg[1]), int(bg[2]), 255,
SDL_SetRenderDrawColor, self.renderer, *background_color, 255,
_check_error=lambda rv: rv < 0
)
sdl_call(SDL_RenderClear, self.renderer, _check_error=lambda rv: rv < 0)
Expand Down
7 changes: 4 additions & 3 deletions ppb/systems/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from ppb.assetlib import Asset, ChainingMixin, AbstractAsset, FreeingMixin
from ppb.systems.sdl_utils import ttf_call
from ppb.colors import Color

# From https://www.freetype.org/freetype2/docs/reference/ft2-base_interface.html:
# [Since 2.5.6] In multi-threaded applications it is easiest to use one
Expand Down Expand Up @@ -109,11 +110,11 @@ class Text(ChainingMixin, FreeingMixin, AbstractAsset):
"""
A bit of rendered text.
"""
def __init__(self, txt, *, font, color=(0, 0, 0)):
def __init__(self, txt, *, font, color=Color(0, 0, 0)):
"""
:param txt: The text to display.
:param font: The font to use (a :py:class:`ppb.Font`)
:param color: The color to use.
:param font: The font to use (a :py:class:`ppb.Font`).
:param color: The color to use (a :py:class:`ppb.Color`).
"""
self.txt = txt
self.font = font
Expand Down
6 changes: 3 additions & 3 deletions tests/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import pytest

from ppb import GameEngine, Scene, Vector
from ppb import Color, GameEngine, Scene, Vector
from ppb import events
from ppb.systemslib import System
from ppb.systems import Updater
Expand All @@ -18,7 +18,7 @@
def scenes():
yield Scene
yield Scene()
yield Scene(background_color=(0, 0, 0))
yield Scene(background_color=Color(0, 0, 0))


@pytest.mark.parametrize("scene", scenes())
Expand All @@ -31,7 +31,7 @@ def test_engine_initial_scene(scene):

def test_game_engine_with_scene_class():
props = {
"background_color": (69, 69, 69),
"background_color": Color(69, 69, 69),
"show_cursor": False
}
with GameEngine(Scene, basic_systems=[Quitter], scene_kwargs=props) as ge:
Expand Down
9 changes: 5 additions & 4 deletions tests/test_scenes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from pytest import fixture

from ppb.colors import Color
from ppb.scenes import Scene
from ppb.camera import Camera

Expand All @@ -22,10 +23,10 @@ def test_main_camera(scene):

def test_class_attrs():
class BackgroundScene(Scene):
background_color = (0, 4, 2)
background_color = Color(0, 4, 2)

scene = BackgroundScene()
assert scene.background_color == (0, 4, 2)
assert scene.background_color == Color(0, 4, 2)

scene = BackgroundScene(background_color=(2, 4, 0))
assert scene.background_color == (2, 4, 0)
scene = BackgroundScene(background_color=Color(2, 4, 0))
assert scene.background_color == Color(2, 4, 0)
4 changes: 2 additions & 2 deletions viztests/float_colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import ppb

class MyScene(ppb.Scene):
background_color = (200.5, 125.6, 127.8)
background_color = ppb.Color(200.5, 125.6, 127.8)

def setup(scene):
scene.add(ppb.Sprite(image=ppb.Square(123.5, 200.8, 156.22)))
scene.add(ppb.Sprite(image=ppb.Square(ppb.Color(123.5, 200.8, 156.22))))

ppb.run(setup, starting_scene=MyScene)
8 changes: 4 additions & 4 deletions viztests/primitive_assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,19 @@ def on_update(self, event: ppb.events.Update, signal):


class Square(Rotating):
image = ppb.Square(255, 50, 75)
image = ppb.Square(ppb.Color(255, 50, 75))


class Triangle(Rotating):
image = ppb.Triangle(0, 0, 0)
image = ppb.Triangle(ppb.Color(0, 0, 0))


class Circle(Rotating):
image = ppb.Circle(255, 71, 182)
image = ppb.Circle(ppb.Color(255, 71, 182))


def setup(scene):
scene.background_color = (160, 155, 180)
scene.background_color = ppb.Color(160, 155, 180)
scene.add(Square(position=ppb.Vector(-2, 0)))
scene.add(Triangle(position=ppb.Vector(0, 2)))
scene.add(Circle(position=ppb.Vector(2, 0)))
Expand Down
2 changes: 1 addition & 1 deletion viztests/rectangles.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class Square(ppb.sprites.RectangleSprite):
width = 1
height = 4

image = ppb.Square(0, 0, 255)
image = ppb.Square(ppb.Color(0, 0, 255))


class Tall(ppb.sprites.RectangleSprite):
Expand Down
Loading