diff --git a/docs/reference/scenes.rst b/docs/reference/scenes.rst index 67763d66..1cf290bc 100644 --- a/docs/reference/scenes.rst +++ b/docs/reference/scenes.rst @@ -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.RGBColor(0, 127, 255)`` .. autoattribute:: main_camera diff --git a/examples/rectangular_assets/main.py b/examples/rectangular_assets/main.py index bdbad43c..ddbd71c4 100644 --- a/examples/rectangular_assets/main.py +++ b/examples/rectangular_assets/main.py @@ -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.RGBColor(200, 0, 0), (1, 2)) +wide_rectangle = ppb.Rectangle(ppb.RGBColor(100, 200, 0), (2, 1)) +square = ppb.Square(ppb.RGBColor(200, 200, 100)) +tall_triangle = ppb.Triangle(ppb.RGBColor(0, 200, 0), (1, 2)) +wide_triangle = ppb.Triangle(ppb.RGBColor(0, 200, 100), (2, 1)) +square_triangle = ppb.Triangle(ppb.RGBColor(50, 200, 150)) +tall_ellipse = ppb.Ellipse(ppb.RGBColor(0, 0, 200), (1, 2)) +wide_ellipse = ppb.Ellipse(ppb.RGBColor(100, 0, 200), (2, 1)) +circle = ppb.Circle(ppb.RGBColor(150, 50, 200)) def setup(scene): - scene.background_color = (0, 0, 0) + scene.background_color = ppb.RGBColor(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))) diff --git a/examples/two-phase-updates/three_body.py b/examples/two-phase-updates/three_body.py index 38ac9ac3..0b1f8aba 100644 --- a/examples/two-phase-updates/three_body.py +++ b/examples/two-phase-updates/three_body.py @@ -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.RGBColor(40, 200, 150)))) + scene.add(Planet(position=(-3, 3), velocity=Vector(1, -1), image=ppb.Circle(ppb.RGBColor(200, 150, 40)))) + scene.add(Planet(position=(-3, -3), velocity=Vector(-1, 0), image=ppb.Circle(ppb.RGBColor(150, 40, 200)))) if __name__ == "__main__": diff --git a/ppb/__init__.py b/ppb/__init__.py index 7da0bad2..2adf18f2 100644 --- a/ppb/__init__.py +++ b/ppb/__init__.py @@ -32,6 +32,7 @@ * :mod:`buttons` * :mod:`keycodes` * :mod:`flags` +* :mod:`colors` * :mod:`directions` * :class:`Signal` """ @@ -47,6 +48,7 @@ from ppb.assets import Rectangle from ppb.assets import Square from ppb.assets import Triangle +from ppb.colors import Color, RGBColor, HSVColor from ppb.engine import GameEngine from ppb.engine import Signal from ppb.scenes import Scene @@ -62,7 +64,7 @@ # Shortcuts 'Scene', 'Sprite', 'RectangleSprite', 'Vector', 'Image', 'Circle', 'Ellipse', 'Square', 'Rectangle', 'Triangle', - 'Font', 'Text', 'Sound', + 'Color', 'RGBColor', 'HSVColor', 'Font', 'Text', 'Sound', 'events', 'buttons', 'keycodes', 'flags', 'directions', 'Signal', # Local stuff 'run', 'make_engine', diff --git a/ppb/assets.py b/ppb/assets.py index 037e9b5d..12603346 100644 --- a/ppb/assets.py +++ b/ppb/assets.py @@ -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__ = ( @@ -31,8 +32,6 @@ "Ellipse" ) -BLACK = 0, 0, 0 -MAGENTA = 255, 71, 182 DEFAULT_SPRITE_SIZE = 64 @@ -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. """ @@ -58,7 +57,7 @@ def _create_surface(color, aspect_ratio: AspectRatio = AspectRatio(1, 1)): _check_error=lambda rv: not rv ) color_key = BLACK if color != BLACK else MAGENTA - color = sdl2.ext.Color(*color_key) + color = sdl2.ext.Color(*color_key.to_rgb()) sdl_call( SDL_SetColorKey, surface, True, sdl2.ext.prepare_color(color, surface.contents), _check_error=lambda rv: rv < 0 @@ -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() @@ -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 @@ -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.to_rgb(), 255, _check_error=lambda rv: rv < 0 ) sdl_call( @@ -120,9 +119,9 @@ 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): @@ -130,7 +129,7 @@ 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 @@ -140,7 +139,7 @@ def _draw_shape(self, renderer, rgb, **_): 0, height, int(width / 2), 0, width, height, - *rgb, 255, + *color.to_rgb(), 255, _check_error=lambda rv: rv < 0 ) @@ -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) @@ -159,7 +158,7 @@ def _draw_shape(self, renderer, rgb, **_): filledEllipseRGBA, renderer, half_width, half_height, # Center half_width, half_height, # Radius - *rgb, 255, + *color.to_rgb(), 255, _check_error=lambda rv: rv < 0 ) @@ -167,6 +166,6 @@ def _draw_shape(self, renderer, rgb, **_): 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) diff --git a/ppb/colors.py b/ppb/colors.py new file mode 100644 index 00000000..4e690f9e --- /dev/null +++ b/ppb/colors.py @@ -0,0 +1,68 @@ +from dataclasses import dataclass +from abc import ABC, abstractmethod +import colorsys + +class Color(ABC): + """Abstract base class for a color.""" + @abstractmethod + def to_rgb(self): + """Convert to a tuple of red, green, and blue values.""" + pass + + +@dataclass(frozen=True) +class RGBColor(Color): + """An RGB color, with red, green, and blue values ranging from 0 to 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) + + def to_rgb(self): + return (self.red, self.green, self.blue) + + +@dataclass(frozen=True) +class HSVColor(Color): + """ + An HSV color, with hue ranging from 0 to 360, + saturation ranging from 0 to 100, and value ranging from 0 to 100. + """ + hue: float + saturation: float + value: float + + def __post_init__(self): + for key, (value, max_value) in { + 'hue': (self.hue, 360), + 'saturation': (self.saturation, 100), + 'value': (self.value, 100), + }.items(): + if value < 0: + raise ValueError(f'{key} cannot be less than 0.') + elif value > max_value: + raise ValueError(f'{key} cannot be greater than {max_value}.') + + def to_rgb(self): + red, green, blue = colorsys.hsv_to_rgb(self.hue / 360, self.saturation / 100, self.value / 100) + return (round(red * 255), round(green * 255), round(blue * 255)) + + +BLACK = RGBColor(0, 0, 0) +WHITE = RGBColor(255, 255, 255) +GRAY = RGBColor(127, 127, 127) +RED = RGBColor(255, 0, 0) +GREEN = RGBColor(0, 255, 0) +BLUE = RGBColor(0, 0, 255) +CYAN = RGBColor(0, 255, 255) +MAGENTA = RGBColor(255, 0, 255) +YELLOW = RGBColor(255, 255, 0) diff --git a/ppb/scenes.py b/ppb/scenes.py index 4b93f0e4..6c8a18ca 100644 --- a/ppb/scenes.py +++ b/ppb/scenes.py @@ -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 diff --git a/ppb/systems/renderer.py b/ppb/systems/renderer.py index 43f534d1..3c78ae89 100644 --- a/ppb/systems/renderer.py +++ b/ppb/systems/renderer.py @@ -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.to_rgb(), 255, _check_error=lambda rv: rv < 0 ) sdl_call(SDL_RenderClear, self.renderer, _check_error=lambda rv: rv < 0) diff --git a/ppb/systems/text.py b/ppb/systems/text.py index b4741574..8e530af0 100644 --- a/ppb/systems/text.py +++ b/ppb/systems/text.py @@ -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, RGBColor # 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 @@ -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=RGBColor(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 @@ -128,7 +129,7 @@ def _background(self): with _freetype_lock: return ttf_call( TTF_RenderUTF8_Blended, self.font.load(), self.txt.encode('utf-8'), - SDL_Color(*self.color), + SDL_Color(*self.color.to_rgb()), _check_error=lambda rv: not rv ) diff --git a/tests/test_colors.py b/tests/test_colors.py new file mode 100644 index 00000000..0a6979a3 --- /dev/null +++ b/tests/test_colors.py @@ -0,0 +1,47 @@ +import pytest +from ppb import Color, RGBColor, HSVColor +from contextlib import nullcontext as does_not_raise + +@pytest.mark.parametrize( + ['red', 'green', 'blue', 'should_raise'], + [ + [0, 0, 0, False], + [255, 255, 255, False], + [-1, 0, 0, True], + [256, 0, 0, True], + [0, -1, 0, True], + [0, 256, 0, True], + [0, 0, -1, True], + [0, 0, 256, True], + ] +) +def test_rgb_color_validation(red, green, blue, should_raise): + with pytest.raises(ValueError) if should_raise else does_not_raise(): + RGBColor(red, green, blue) + +@pytest.mark.parametrize( + ['hue', 'saturation', 'value', 'should_raise'], + [ + [0, 0, 0, False], + [360, 100, 100, False], + [-1, 0, 0, True], + [361, 0, 0, True], + [0, -1, 0, True], + [0, 101, 0, True], + [0, 0, -1, True], + [0, 0, 101, True], + ] +) +def test_hsv_color_validation(hue, saturation, value, should_raise): + with pytest.raises(ValueError) if should_raise else does_not_raise(): + HSVColor(hue, saturation, value) + +@pytest.mark.parametrize( + ['color', 'red', 'green', 'blue'], + [ + [RGBColor(50, 40, 30), 50, 40, 30], + [HSVColor(259, 46, 54.5), 95, 75, 139], + ] +) +def test_to_rgb(color, red, green, blue): + assert color.to_rgb() == (red, green, blue) \ No newline at end of file diff --git a/tests/test_engine.py b/tests/test_engine.py index 8cfbeaed..380bc1c2 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -3,7 +3,7 @@ import pytest -from ppb import GameEngine, Scene, Vector +from ppb import RGBColor, GameEngine, Scene, Vector from ppb import events from ppb.systemslib import System from ppb.systems import Updater @@ -18,7 +18,7 @@ def scenes(): yield Scene yield Scene() - yield Scene(background_color=(0, 0, 0)) + yield Scene(background_color=RGBColor(0, 0, 0)) @pytest.mark.parametrize("scene", scenes()) @@ -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": RGBColor(69, 69, 69), "show_cursor": False } with GameEngine(Scene, basic_systems=[Quitter], scene_kwargs=props) as ge: diff --git a/tests/test_scenes.py b/tests/test_scenes.py index ef714927..f956921a 100644 --- a/tests/test_scenes.py +++ b/tests/test_scenes.py @@ -1,5 +1,6 @@ from pytest import fixture +from ppb.colors import RGBColor from ppb.scenes import Scene from ppb.camera import Camera @@ -22,10 +23,10 @@ def test_main_camera(scene): def test_class_attrs(): class BackgroundScene(Scene): - background_color = (0, 4, 2) + background_color = RGBColor(0, 4, 2) scene = BackgroundScene() - assert scene.background_color == (0, 4, 2) + assert scene.background_color == RGBColor(0, 4, 2) - scene = BackgroundScene(background_color=(2, 4, 0)) - assert scene.background_color == (2, 4, 0) + scene = BackgroundScene(background_color=RGBColor(2, 4, 0)) + assert scene.background_color == RGBColor(2, 4, 0) diff --git a/viztests/float_colors.py b/viztests/float_colors.py index 29bb76a4..c2220faa 100644 --- a/viztests/float_colors.py +++ b/viztests/float_colors.py @@ -3,9 +3,9 @@ import ppb class MyScene(ppb.Scene): - background_color = (200.5, 125.6, 127.8) + background_color = ppb.RGBColor(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.RGBColor(123.5, 200.8, 156.22)))) ppb.run(setup, starting_scene=MyScene) \ No newline at end of file diff --git a/viztests/primitive_assets.py b/viztests/primitive_assets.py index f369d02d..b8e60faf 100644 --- a/viztests/primitive_assets.py +++ b/viztests/primitive_assets.py @@ -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.RGBColor(255, 50, 75)) class Triangle(Rotating): - image = ppb.Triangle(0, 0, 0) + image = ppb.Triangle(ppb.RGBColor(0, 0, 0)) class Circle(Rotating): - image = ppb.Circle(255, 71, 182) + image = ppb.Circle(ppb.RGBColor(255, 71, 182)) def setup(scene): - scene.background_color = (160, 155, 180) + scene.background_color = ppb.RGBColor(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))) diff --git a/viztests/rectangles.py b/viztests/rectangles.py index c9dfa543..4d3bcc10 100644 --- a/viztests/rectangles.py +++ b/viztests/rectangles.py @@ -7,7 +7,7 @@ class Square(ppb.sprites.RectangleSprite): width = 1 height = 4 - image = ppb.Square(0, 0, 255) + image = ppb.Square(ppb.RGBColor(0, 0, 255)) class Tall(ppb.sprites.RectangleSprite): diff --git a/viztests/show_cursor.py b/viztests/show_cursor.py index 58d4b12a..35b6dd98 100644 --- a/viztests/show_cursor.py +++ b/viztests/show_cursor.py @@ -28,7 +28,7 @@ def on_scene_started(self, _, __): image=ppb.Text( " ".join((self.cursor[cursor_state], self._continue)), font=font, - color=(255, 255, 255) + color=ppb.RGBColor(255, 255, 255) ) ) ) @@ -39,7 +39,7 @@ def on_button_pressed(self, event:ppb.events.ButtonPressed, signal): class NoCursorScene(RootScene): - background_color = (100, 100, 100) + background_color = ppb.RGBColor(100, 100, 100) show_cursor = False @@ -48,7 +48,7 @@ class DefaultScene(RootScene): class ExplicitVisibleCursor(RootScene): - background_color = (0, 0, 0) + background_color = ppb.RGBColor(0, 0, 0) show_cursor = True click_event = ppb.events.StartScene(DefaultScene) diff --git a/viztests/text.py b/viztests/text.py index ea1d0f9f..0a97b704 100644 --- a/viztests/text.py +++ b/viztests/text.py @@ -22,14 +22,14 @@ def on_scene_started(self, event, signal): image=ppb.Text( "Hello, PPB!", font=ppb.Font(f"resources/ubuntu_font/Ubuntu-{font}.ttf", size=72), - color=hsv2rgb(i / 10, 1.0, 75) + color=ppb.RGBColor(*hsv2rgb(i / 10, 1.0, 75)) ), position=(0, i-4.5), )) def on_update(self, event, signal): self.elapsed += event.time_delta - self.background_color = hsv2rgb(self.elapsed / 10, 1.0, 200) + self.background_color = ppb.RGBColor(*hsv2rgb(self.elapsed / 10, 1.0, 200)) ppb.run(starting_scene=TextScene) diff --git a/viztests/text_shared_font.py b/viztests/text_shared_font.py index 549b53ba..99f2faa7 100644 --- a/viztests/text_shared_font.py +++ b/viztests/text_shared_font.py @@ -1,8 +1,8 @@ import ppb font = ppb.Font("resources/ubuntu_font/Ubuntu-R.ttf", size=72) -my_first_text = ppb.Text("My first text", font=font, color=(255, 255, 255)) -my_second_text = ppb.Text("My second text", font=font, color=(255, 255, 255)) +my_first_text = ppb.Text("My first text", font=font, color=ppb.RGBColor(255, 255, 255)) +my_second_text = ppb.Text("My second text", font=font, color=ppb.RGBColor(255, 255, 255)) def setup(scene): diff --git a/viztests/triangles.py b/viztests/triangles.py index e8223bfd..b23b4779 100644 --- a/viztests/triangles.py +++ b/viztests/triangles.py @@ -16,29 +16,29 @@ def setup(scene): - scene.background_color = (0, 0, 0) + scene.background_color = ppb.RGBColor(0, 0, 0) scene.add(ppb.RectangleSprite( width=0.5, height=1, - image=ppb.Rectangle(200, 0, 0, (1, 2)), position=(-2, 2))) + image=ppb.Rectangle(ppb.RGBColor(200, 0, 0), (1, 2)), position=(-2, 2))) scene.add(ppb.RectangleSprite( width=1, height=0.5, - image=ppb.Rectangle(100, 200, 0, (2, 1)), position=(0, 2))) + image=ppb.Rectangle(ppb.RGBColor(100, 200, 0), (2, 1)), position=(0, 2))) scene.add(ppb.Sprite(size=1, - image=ppb.Square(200, 200, 100), position=(2, 2))) + image=ppb.Square(ppb.RGBColor(200, 200, 100)), position=(2, 2))) scene.add(ppb.RectangleSprite( width=0.5, height=1, - image=ppb.Triangle(0, 200, 0, (1, 2)), position=(-2, 0))) + image=ppb.Triangle(ppb.RGBColor(0, 200, 0), (1, 2)), position=(-2, 0))) scene.add(ppb.RectangleSprite( width=1, height=0.5, - image=ppb.Triangle(0, 200, 100, (2, 1)), position=(0, 0))) - scene.add(ppb.Sprite(image=ppb.Triangle(50, 200, 150), position=(2, 0))) + image=ppb.Triangle(ppb.RGBColor(0, 200, 100), (2, 1)), position=(0, 0))) + scene.add(ppb.Sprite(image=ppb.Triangle(ppb.RGBColor(50, 200, 150)), position=(2, 0))) scene.add(ppb.RectangleSprite( width=0.5, height=1, - image=ppb.Ellipse(0, 0, 200, (1, 2)), position=(-2, -2))) + image=ppb.Ellipse(ppb.RGBColor(0, 0, 200), (1, 2)), position=(-2, -2))) scene.add(ppb.RectangleSprite( width=1, height=0.5, - image=ppb.Ellipse(100, 0, 200, (2, 1)), position=(0, -2))) - scene.add(ppb.Sprite(image=ppb.Circle(150, 50, 200), position=(2, -2))) + image=ppb.Ellipse(ppb.RGBColor(100, 0, 200), (2, 1)), position=(0, -2))) + scene.add(ppb.Sprite(image=ppb.Circle(ppb.RGBColor(150, 50, 200)), position=(2, -2))) ppb.run(setup)