diff --git a/doc/builtins.rst b/doc/builtins.rst index 4ac0e0fd..f1e8bdd5 100644 --- a/doc/builtins.rst +++ b/doc/builtins.rst @@ -660,6 +660,47 @@ of transparency: through a ghost. +.. _simple-shapes: + +Simple shapes +''''''''''''' + +If you don't have images yet, not to worry! You can start by creating actors +from simple colored shapes like a rectangle or a circle. Instead of calling +``Actor("image_name")`` you instead call +``Actor.Shape(width, height, color)``. + +Here's an example:: + + character = Actor.Rectangle(100, 100, "red") + shot = Actor.Ellipse(60, 20, "blue") + sword = Actor.Triangle(50, 15, "green") + +The following shapes can be created this way: + +.. method:: Actor.Rectangle(width, height, color) + + Creates an actor with a filled rectangle of the given color as its + image. To create a perfect square, simply give the same value for both + width and height. + +.. method:: Actor.Ellipse(width, height, color) + + Creates an actor with a filled circular shape of the given color as its + image. The background of the image is transparent. To create a perfect + circle, simply give the same value for both width and height. + +.. method:: Actor.Triangle(width, height, color) + + Creates an actor with a filled triangle of the given color as its image. + The triangle points to the right so it always points in the direction of + the ``angle`` of the actor. + +If you wanted to define other parameters like anchor or position when creating +the actors, you can still do so just like with a normal Actor construction:: + + balloon = Actor.Ellipse(50, 50, "red", (150, 150), anchor=("center", "bottom")) + The Keyboard ------------ diff --git a/src/pgzero/actor.py b/src/pgzero/actor.py index e8697e9f..fbf769de 100644 --- a/src/pgzero/actor.py +++ b/src/pgzero/actor.py @@ -220,6 +220,58 @@ def _update_transform(self, function): "function {!r} does not have a registered order." "".format(function)) + @classmethod + def _make_shape_image(self, kind, width, height, color): + """Creates a new shape image and loads it into resources. If an image + of the exact parameters already exists, creation is not repeated.""" + # Create image name and resource cache key from parameters. + name = kind + str(width) + "x" + str(height) + "_" + str(color) + key = (name, (), ()) + # Return without costly image creation if image already exists. + if key in loaders.images._cache: + return name + # Creates the image with transparency (for non-rects) and fills them + # with the appropriate shape. + s = pygame.Surface((width, height), pygame.SRCALPHA) + match kind: + case "__SHAPE_ELLIPSE__": + pygame.draw.ellipse(s, color, + pygame.Rect((0, 0), (width, height))) + case "__SHAPE_TRIANGLE__": + pygame.draw.polygon(s, color, + ((0, 0), (width, height / 2), (0, height))) + case _: + s.fill(color) + # Saves the created image in the resource cache for use. This ensures + # smooth interoperability with the normal Actor construction. + loaders.images._cache[key] = s + # Returns the name for use in the Actor construction. + return name + + @classmethod + def Rectangle(self, width, height, color, pos=POS_TOPLEFT, + anchor=ANCHOR_CENTER, **kwargs): + """Creates an actor with a rectangle as an image.""" + name = self._make_shape_image("__SHAPE_RECTANGLE__", width, height, + color) + return Actor(name, pos, anchor, **kwargs) + + @classmethod + def Ellipse(self, width, height, color, pos=POS_TOPLEFT, + anchor=ANCHOR_CENTER, **kwargs): + """Creates an actor with an ellipse as an image.""" + name = self._make_shape_image("__SHAPE_ELLIPSE__", width, height, + color) + return Actor(name, pos, anchor, **kwargs) + + @classmethod + def Triangle(self, width, height, color, pos=POS_TOPLEFT, + anchor=ANCHOR_CENTER, **kwargs): + """Creates an actor with a triangle as an image.""" + name = self._make_shape_image("__SHAPE_TRIANGLE__", width, height, + color) + return Actor(name, pos, anchor, **kwargs) + @property def anchor(self): return self._anchor_value diff --git a/test/test_actor.py b/test/test_actor.py index b74410c4..a5f991dd 100644 --- a/test/test_actor.py +++ b/test/test_actor.py @@ -4,6 +4,7 @@ from pgzero.actor import calculate_anchor, Actor from pgzero.loaders import set_root +from pgzero.loaders import images TEST_MODULE = "pgzero.actor" @@ -146,3 +147,78 @@ def test_dir_correct(self): a = Actor("alien") for attribute in dir(a): a.__getattr__(attribute) + + def test_actor_square(self): + """The square image is created correctly and the result is a valid + actor.""" + square = Actor.Rectangle(10, 10, "red") + name = "__SHAPE_RECTANGLE__10x10_red" + self.assertIn((name, (), ()), images._cache) + surf = images.load(name) + width, height = surf.get_size() + self.assertEqual(width, 10) + self.assertEqual(height, 10) + self.assertEqual( + surf.get_at((width//2, height//2)), (255, 0, 0, 255) + ) + self.assertEqual(type(square), Actor) + + def test_actor_rectangle(self): + """The rectangle image is created correctly and the result is a valid + actor.""" + square = Actor.Rectangle(10, 5, "green") + name = "__SHAPE_RECTANGLE__10x5_green" + self.assertIn((name, (), ()), images._cache) + surf = images.load(name) + width, height = surf.get_size() + self.assertEqual(width, 10) + self.assertEqual(height, 5) + self.assertEqual( + surf.get_at((width//2, height//2)), (0, 255, 0, 255) + ) + self.assertEqual(type(square), Actor) + + def test_actor_circle(self): + """The circular image is created correctly and the result is a valid + actor.""" + square = Actor.Ellipse(5, 5, "blue") + name = "__SHAPE_ELLIPSE__5x5_blue" + self.assertIn((name, (), ()), images._cache) + surf = images.load(name) + width, height = surf.get_size() + self.assertEqual(width, 5) + self.assertEqual(height, 5) + self.assertEqual( + surf.get_at((width//2, height//2)), (0, 0, 255, 255) + ) + self.assertEqual(type(square), Actor) + + def test_actor_ellipse(self): + """The elliptical image is created correctly and the result is a valid + actor.""" + square = Actor.Ellipse(5, 10, "yellow") + name = "__SHAPE_ELLIPSE__5x10_yellow" + self.assertIn((name, (), ()), images._cache) + surf = images.load(name) + width, height = surf.get_size() + self.assertEqual(width, 5) + self.assertEqual(height, 10) + self.assertEqual( + surf.get_at((width//2, height//2)), (255, 255, 0, 255) + ) + self.assertEqual(type(square), Actor) + + def test_actor_triangle(self): + """The triangular image is created correctly and the result is a valid + actor.""" + square = Actor.Triangle(15, 15, "fuchsia") + name = "__SHAPE_TRIANGLE__15x15_fuchsia" + self.assertIn((name, (), ()), images._cache) + surf = images.load(name) + width, height = surf.get_size() + self.assertEqual(width, 15) + self.assertEqual(height, 15) + self.assertEqual( + surf.get_at((width//2, height//2)), (255, 0, 255, 255) + ) + self.assertEqual(type(square), Actor)