diff --git a/adafruit_display_shapes/filled_polygon.py b/adafruit_display_shapes/filled_polygon.py new file mode 100644 index 0000000..8a1289d --- /dev/null +++ b/adafruit_display_shapes/filled_polygon.py @@ -0,0 +1,142 @@ +# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +`filled_polygon` +================================================================================ + +Various common shapes for use with displayio - Polygon that supports +both fill and outline + + +* Author(s): Bernhard Bablok, Tim Cocks + +Implementation Notes +-------------------- + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" + +try: + from typing import Optional, List, Tuple +except ImportError: + pass + +import displayio +from adafruit_display_shapes.polygon import Polygon + +try: + import vectorio + + HAVE_VECTORIO = True +except ImportError: + HAVE_VECTORIO = False + +__version__ = "0.0.0+auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_Display_Shapes.git" + + +class FilledPolygon(displayio.Group): + # pylint: disable=too-few-public-methods, invalid-name + """A filled polygon. Technically, an FilledPolygon is a Group with one or two polygons. + + :param list points: A list of (x, y) tuples of the points + :param int|None outline: The outline of the arc. Can be a hex value for a color or + ``None`` for no outline. + :param int|None fill: The fill-color of the arc. Can be a hex value for a color or + ``None`` for no filling. Ignored if port does not support vectorio. + :param bool close: (Optional) Wether to connect first and last point. (True) + :param int stroke: Thickness of the outline. + + """ + + def __init__( + self, + points: List[Tuple[int, int]], + *, + outline: Optional[int] = None, + fill: Optional[int] = None, + close: Optional[bool] = True, + stroke: int = 1, + ) -> None: + super().__init__() + self._points = points + self._outline = outline + self._fill = fill + self.close = close + self.stroke = stroke + + self.palette = None + self.vector_polygon = None + self.outline_polygon = None + + self._init_polygon() + + def _init_polygon(self): + # create polygon(s) and add to ourselves + if HAVE_VECTORIO and self._fill is not None: + if self.palette is None: + self.palette = displayio.Palette(1) + self.palette[0] = self._fill + if self.vector_polygon is None: + self.vector_polygon = vectorio.Polygon( + pixel_shader=self.palette, points=self.points, x=0, y=0 + ) + self.append(self.vector_polygon) + else: + self.vector_polygon.points = self.points + + if self._outline is not None: + if self.outline_polygon is None: + self.outline_polygon = Polygon( + self.points, + outline=self._outline, + colors=1, + close=self.close, + stroke=self.stroke, + ) + else: + self.remove(self.outline_polygon) + self.outline_polygon = Polygon( + self.points, + outline=self._outline, + colors=1, + close=self.close, + stroke=self.stroke, + ) + self.append(self.outline_polygon) + + @property + def points(self): + """The points that make up the polygon""" + return self._points + + @points.setter + def points(self, points): + self._points = points + self._init_polygon() + + @property + def outline(self): + """The outline color. None for no outline""" + return self._outline + + @outline.setter + def outline(self, value): + self._outline = value + self._init_polygon() + + @property + def fill(self): + """The fill color. None for no fill""" + return self._fill + + @fill.setter + def fill(self, value): + self._fill = value + self._init_polygon() diff --git a/adafruit_display_shapes/polygon.py b/adafruit_display_shapes/polygon.py index f9ece68..1909653 100644 --- a/adafruit_display_shapes/polygon.py +++ b/adafruit_display_shapes/polygon.py @@ -26,6 +26,7 @@ except ImportError: pass +import bitmaptools import displayio __version__ = "0.0.0+auto.0" @@ -42,6 +43,7 @@ class Polygon(displayio.TileGrid): :param int colors: (Optional) Number of colors to use. Most polygons would use two, one for outline and one for fill. If you're not filling your polygon, set this to 1 for smaller memory footprint. (2) + :param int stroke: Thickness of the outline. """ _OUTLINE = 1 @@ -54,6 +56,8 @@ def __init__( outline: Optional[int] = None, close: Optional[bool] = True, colors: Optional[int] = 2, + stroke: int = 1, + # pylint: disable=too-many-arguments ) -> None: (x_s, y_s) = zip(*points) @@ -66,13 +70,14 @@ def __init__( self._palette = displayio.Palette(colors + 1) self._palette.make_transparent(0) - self._bitmap = displayio.Bitmap(width, height, colors + 1) + self._bitmap = displayio.Bitmap(width + stroke, height + stroke, colors + 1) + self._stroke = stroke shifted = [(x - x_offset, y - y_offset) for (x, y) in points] if outline is not None: self.outline = outline - self.draw(self._bitmap, shifted, self._OUTLINE, close) + self.draw(self._bitmap, shifted, self._OUTLINE, close, stroke) super().__init__( self._bitmap, pixel_shader=self._palette, x=x_offset, y=y_offset @@ -84,6 +89,7 @@ def draw( points: List[Tuple[int, int]], color_id: int, close: Optional[bool] = True, + stroke=1, ) -> None: """Draw a polygon conecting points on provided bitmap with provided color_id @@ -97,7 +103,7 @@ def draw( points.append(points[0]) for index in range(len(points) - 1): - Polygon._line_on(bitmap, points[index], points[index + 1], color_id) + Polygon._line_on(bitmap, points[index], points[index + 1], color_id, stroke) # pylint: disable=too-many-arguments def _line( @@ -129,23 +135,36 @@ def _line_on( p_0: Tuple[int, int], p_1: Tuple[int, int], color: int, + stroke: int = 1, ) -> None: (x_0, y_0) = p_0 (x_1, y_1) = p_1 - def pt_on(x, y): - Polygon._safe_draw(bitmap, (x, y), color) + def pt_on(x, y, pt_size=1): + if pt_size > 1: + x = x + pt_size // 2 + y = y + pt_size // 2 + bitmaptools.fill_region( + bitmap, + x - (pt_size // 2), + y - (pt_size // 2), + x + (pt_size // 2), + y + (pt_size // 2), + color, + ) + else: + Polygon._safe_draw(bitmap, (x, y), color) if x_0 == x_1: if y_0 > y_1: y_0, y_1 = y_1, y_0 for _h in range(y_0, y_1 + 1): - pt_on(x_0, _h) + pt_on(x_0, _h, stroke) elif y_0 == y_1: if x_0 > x_1: x_0, x_1 = x_1, x_0 for _w in range(x_0, x_1 + 1): - pt_on(_w, y_0) + pt_on(_w, y_0, stroke) else: steep = abs(y_1 - y_0) > abs(x_1 - x_0) if steep: @@ -168,9 +187,9 @@ def pt_on(x, y): for x in range(x_0, x_1 + 1): if steep: - pt_on(y_0, x) + pt_on(y_0, x, stroke) else: - pt_on(x, y_0) + pt_on(x, y_0, stroke) err -= d_y if err < 0: y_0 += ystep diff --git a/docs/api.rst b/docs/api.rst index 5abe902..572a17e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -22,6 +22,9 @@ .. automodule:: adafruit_display_shapes.polygon :members: +.. automodule:: adafruit_display_shapes.filled_polygon + :members: + .. automodule:: adafruit_display_shapes.sparkline :members: diff --git a/docs/conf.py b/docs/conf.py index c5fcded..6d31dca 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,7 +27,7 @@ # Uncomment the below if you use native CircuitPython modules such as # digitalio, micropython and busio. List the modules you use. Without it, the # autodoc module docs will fail to generate with a warning. -autodoc_mock_imports = ["displayio"] +autodoc_mock_imports = ["displayio", "bitmaptools"] intersphinx_mapping = { diff --git a/docs/examples.rst b/docs/examples.rst index 643da63..7b4f940 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -60,3 +60,12 @@ Example demonstrating various arcs. .. literalinclude:: ../examples/display_shapes_arc.py :caption: examples/display_shapes_arc.py :linenos: + +Filled Polygon Simple Test +-------------------------- + +Example demonstrating a filled polygon + +.. literalinclude:: ../examples/display_shapes_filled_polygon_simpletest.py + :caption: examples/display_shapes_filled_polygon_simpletest.py + :linenos: diff --git a/examples/display_shapes_filled_polygon_simpletest.py b/examples/display_shapes_filled_polygon_simpletest.py new file mode 100644 index 0000000..921b353 --- /dev/null +++ b/examples/display_shapes_filled_polygon_simpletest.py @@ -0,0 +1,41 @@ +# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries +# SPDX-License-Identifier: MIT + +import board +import displayio +from adafruit_display_shapes.filled_polygon import FilledPolygon + +# Make the display context +splash = displayio.Group() +board.DISPLAY.root_group = splash + +# Make a background color fill +color_bitmap = displayio.Bitmap(320, 240, 1) +color_palette = displayio.Palette(1) +color_palette[0] = 0xFFFFFF +bg_sprite = displayio.TileGrid(color_bitmap, x=0, y=0, pixel_shader=color_palette) +splash.append(bg_sprite) +########################################################################## + +# Draw a star with blue outline and pink fill +polygon = FilledPolygon( + [ + (55, 40), + (62, 62), + (85, 62), + (65, 76), + (75, 100), + (55, 84), + (35, 100), + (45, 76), + (25, 62), + (48, 62), + ], + outline=0x0000FF, + stroke=4, + fill=0xFF00FF, +) +splash.append(polygon) + +while True: + pass