X-Git-Url: https://git.ayoreis.com/hackapet/Adafruit_Blinka_Displayio.git/blobdiff_plain/1ef9f0a96fa6e1cfd07144d5bdc6c123027c48f3..5cfe68b419b1e014ae334c500569d87b661e4281:/displayio/tilegrid.py?ds=sidebyside diff --git a/displayio/tilegrid.py b/displayio/tilegrid.py new file mode 100644 index 0000000..2a2222a --- /dev/null +++ b/displayio/tilegrid.py @@ -0,0 +1,356 @@ +# The MIT License (MIT) +# +# Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +""" +`displayio` +================================================================================ + +displayio for Blinka + +**Software and Dependencies:** + +* Adafruit Blinka: + https://github.com/adafruit/Adafruit_Blinka/releases + +* Author(s): Melissa LeBlanc-Williams + +""" + +from PIL import Image +from displayio.bitmap import Bitmap +from displayio.colorconverter import ColorConverter +from displayio.ondiskbitmap import OnDiskBitmap +from displayio.shape import Shape +from displayio.palette import Palette +from displayio import Rectangle +from displayio import Transform + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git" + +# pylint: disable=too-many-instance-attributes +class TileGrid: + """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple + grids can share bitmaps and pixel shaders. + + A single tile grid is also known as a Sprite. + """ + + def __init__( + self, + bitmap, + *, + pixel_shader, + width=1, + height=1, + tile_width=None, + tile_height=None, + default_tile=0, + x=0, + y=0 + ): + """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is + used to convert the value and its location to a display native pixel color. This may + be a simple color palette lookup, a gradient, a pattern or a color transformer. + + tile_width and tile_height match the height of the bitmap by default. + """ + if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)): + raise ValueError("Unsupported Bitmap type") + self._bitmap = bitmap + bitmap_width = bitmap.width + bitmap_height = bitmap.height + + if not isinstance(pixel_shader, (ColorConverter, Palette)): + raise ValueError("Unsupported Pixel Shader type") + self._pixel_shader = pixel_shader + self._hidden = False + self._x = x + self._y = y + self._width = width # Number of Tiles Wide + self._height = height # Number of Tiles High + self._transpose_xy = False + self._flip_x = False + self._flip_y = False + if tile_width is None: + tile_width = bitmap_width + if tile_height is None: + tile_height = bitmap_height + if bitmap_width % tile_width != 0: + raise ValueError("Tile width must exactly divide bitmap width") + self._tile_width = tile_width + if bitmap_height % tile_height != 0: + raise ValueError("Tile height must exactly divide bitmap height") + self._tile_height = tile_height + if not 0 <= default_tile <= 255: + raise ValueError("Default Tile is out of range") + self._pixel_width = width * tile_width + self._pixel_height = height * tile_height + self._tiles = (self._width * self._height) * [default_tile] + self.in_group = False + self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False) + self._current_area = Rectangle(0, 0, self._pixel_width, self._pixel_height) + self._moved = False + + def update_transform(self, absolute_transform): + """Update the parent transform and child transforms""" + self._absolute_transform = absolute_transform + if self._absolute_transform is not None: + self._update_current_x() + self._update_current_y() + + def _update_current_x(self): + if self._transpose_xy: + width = self._pixel_height + else: + width = self._pixel_width + if self._absolute_transform.transpose_xy: + self._current_area.y1 = ( + self._absolute_transform.y + self._absolute_transform.dy * self._x + ) + self._current_area.y2 = ( + self._absolute_transform.y + + self._absolute_transform.dy * (self._x + width) + ) + if self._current_area.y2 < self._current_area.y1: + self._current_area.y1, self._current_area.y2 = ( + self._current_area.y2, + self._current_area.y1, + ) + else: + self._current_area.x1 = ( + self._absolute_transform.x + self._absolute_transform.dx * self._x + ) + self._current_area.x2 = ( + self._absolute_transform.x + + self._absolute_transform.dx * (self._x + width) + ) + if self._current_area.x2 < self._current_area.x1: + self._current_area.x1, self._current_area.x2 = ( + self._current_area.x2, + self._current_area.x1, + ) + + def _update_current_y(self): + if self._transpose_xy: + height = self._pixel_width + else: + height = self._pixel_height + if self._absolute_transform.transpose_xy: + self._current_area.x1 = ( + self._absolute_transform.x + self._absolute_transform.dx * self._y + ) + self._current_area.x2 = ( + self._absolute_transform.x + + self._absolute_transform.dx * (self._y + height) + ) + if self._current_area.x2 < self._current_area.x1: + self._current_area.x1, self._current_area.x2 = ( + self._current_area.x2, + self._current_area.x1, + ) + else: + self._current_area.y1 = ( + self._absolute_transform.y + self._absolute_transform.dy * self._y + ) + self._current_area.y2 = ( + self._absolute_transform.y + + self._absolute_transform.dy * (self._y + height) + ) + if self._current_area.y2 < self._current_area.y1: + self._current_area.y1, self._current_area.y2 = ( + self._current_area.y2, + self._current_area.y1, + ) + + # pylint: disable=too-many-locals + def _fill_area(self, buffer): + """Draw onto the image""" + if self._hidden: + return + + image = Image.new( + "RGBA", + (self._width * self._tile_width, self._height * self._tile_height), + (0, 0, 0, 0), + ) + + tile_count_x = self._bitmap.width // self._tile_width + x = self._x + y = self._y + + for tile_x in range(0, self._width): + for tile_y in range(0, self._height): + tile_index = self._tiles[tile_y * self._width + tile_x] + tile_index_x = tile_index % tile_count_x + tile_index_y = tile_index // tile_count_x + for pixel_x in range(self._tile_width): + for pixel_y in range(self._tile_height): + image_x = tile_x * self._tile_width + pixel_x + image_y = tile_y * self._tile_height + pixel_y + bitmap_x = tile_index_x * self._tile_width + pixel_x + bitmap_y = tile_index_y * self._tile_height + pixel_y + pixel_color = self._pixel_shader[ + self._bitmap[bitmap_x, bitmap_y] + ] + if not pixel_color["transparent"]: + image.putpixel((image_x, image_y), pixel_color["rgb888"]) + if self._absolute_transform is not None: + if self._absolute_transform.scale > 1: + image = image.resize( + ( + self._pixel_width * self._absolute_transform.scale, + self._pixel_height * self._absolute_transform.scale, + ), + resample=Image.NEAREST, + ) + if self._absolute_transform.mirror_x: + image = image.transpose(Image.FLIP_LEFT_RIGHT) + if self._absolute_transform.mirror_y: + image = image.transpose(Image.FLIP_TOP_BOTTOM) + if self._absolute_transform.transpose_xy: + image = image.transpose(Image.TRANSPOSE) + x *= self._absolute_transform.dx + y *= self._absolute_transform.dy + x += self._absolute_transform.x + y += self._absolute_transform.y + buffer.alpha_composite(image, (x, y)) + + # pylint: enable=too-many-locals + + @property + def hidden(self): + """True when the TileGrid is hidden. This may be False even + when a part of a hidden Group.""" + return self._hidden + + @hidden.setter + def hidden(self, value): + if not isinstance(value, (bool, int)): + raise ValueError("Expecting a boolean or integer value") + self._hidden = bool(value) + + @property + def x(self): + """X position of the left edge in the parent.""" + return self._x + + @x.setter + def x(self, value): + if not isinstance(value, int): + raise TypeError("X should be a integer type") + if self._x != value: + self._x = value + self._update_current_x() + + @property + def y(self): + """Y position of the top edge in the parent.""" + return self._y + + @y.setter + def y(self, value): + if not isinstance(value, int): + raise TypeError("Y should be a integer type") + if self._y != value: + self._y = value + self._update_current_y() + + @property + def flip_x(self): + """If true, the left edge rendered will be the right edge of the right-most tile.""" + return self._flip_x + + @flip_x.setter + def flip_x(self, value): + if not isinstance(value, bool): + raise TypeError("Flip X should be a boolean type") + if self._flip_x != value: + self._flip_x = value + + @property + def flip_y(self): + """If true, the top edge rendered will be the bottom edge of the bottom-most tile.""" + return self._flip_y + + @flip_y.setter + def flip_y(self, value): + if not isinstance(value, bool): + raise TypeError("Flip Y should be a boolean type") + if self._flip_y != value: + self._flip_y = value + + @property + def transpose_xy(self): + """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90 + degree rotation can be achieved along with the corresponding mirrored version. + """ + return self._transpose_xy + + @transpose_xy.setter + def transpose_xy(self, value): + if not isinstance(value, bool): + raise TypeError("Transpose XY should be a boolean type") + if self._transpose_xy != value: + self._transpose_xy = value + self._update_current_x() + self._update_current_y() + + @property + def pixel_shader(self): + """The pixel shader of the tilegrid.""" + return self._pixel_shader + + def __getitem__(self, index): + """Returns the tile index at the given index. The index can either be + an x,y tuple or an int equal to ``y * width + x``'. + """ + if isinstance(index, (tuple, list)): + x = index[0] + y = index[1] + index = y * self._width + x + elif isinstance(index, int): + x = index % self._width + y = index // self._width + if x > self._width or y > self._height or index >= len(self._tiles): + raise ValueError("Tile index out of bounds") + return self._tiles[index] + + def __setitem__(self, index, value): + """Sets the tile index at the given index. The index can either be + an x,y tuple or an int equal to ``y * width + x``. + """ + if isinstance(index, (tuple, list)): + x = index[0] + y = index[1] + index = y * self._width + x + elif isinstance(index, int): + x = index % self._width + y = index // self._width + if x > self._width or y > self._height or index >= len(self._tiles): + raise ValueError("Tile index out of bounds") + if not 0 <= value <= 255: + raise ValueError("Tile value out of bounds") + self._tiles[index] = value + + +# pylint: enable=too-many-instance-attributes