1 # The MIT License (MIT)
 
   3 # Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
 
   5 # Permission is hereby granted, free of charge, to any person obtaining a copy
 
   6 # of this software and associated documentation files (the "Software"), to deal
 
   7 # in the Software without restriction, including without limitation the rights
 
   8 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 
   9 # copies of the Software, and to permit persons to whom the Software is
 
  10 # furnished to do so, subject to the following conditions:
 
  12 # The above copyright notice and this permission notice shall be included in
 
  13 # all copies or substantial portions of the Software.
 
  15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 
  16 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 
  17 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 
  18 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 
  19 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 
  20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 
  25 ================================================================================
 
  29 **Software and Dependencies:**
 
  32   https://github.com/adafruit/Adafruit_Blinka/releases
 
  34 * Author(s): Melissa LeBlanc-Williams
 
  38 from recordclass import recordclass
 
  40 from displayio.bitmap import Bitmap
 
  41 from displayio.colorconverter import ColorConverter
 
  42 from displayio.ondiskbitmap import OnDiskBitmap
 
  43 from displayio.shape import Shape
 
  44 from displayio.palette import Palette
 
  46 __version__ = "0.0.0-auto.0"
 
  47 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
  49 Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
 
  50 Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y")
 
  52 # pylint: disable=too-many-instance-attributes
 
  54     """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
 
  55     grids can share bitmaps and pixel shaders.
 
  57     A single tile grid is also known as a Sprite.
 
  73         """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is
 
  74         used to convert the value and its location to a display native pixel color. This may
 
  75         be a simple color palette lookup, a gradient, a pattern or a color transformer.
 
  77         tile_width and tile_height match the height of the bitmap by default.
 
  79         if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
 
  80             raise ValueError("Unsupported Bitmap type")
 
  82         bitmap_width = bitmap.width
 
  83         bitmap_height = bitmap.height
 
  85         if not isinstance(pixel_shader, (ColorConverter, Palette)):
 
  86             raise ValueError("Unsupported Pixel Shader type")
 
  87         self._pixel_shader = pixel_shader
 
  91         self._width = width  # Number of Tiles Wide
 
  92         self._height = height  # Number of Tiles High
 
  93         self._transpose_xy = False
 
  96         if tile_width is None:
 
  97             tile_width = bitmap_width
 
  98         if tile_height is None:
 
  99             tile_height = bitmap_height
 
 100         if bitmap_width % tile_width != 0:
 
 101             raise ValueError("Tile width must exactly divide bitmap width")
 
 102         self._tile_width = tile_width
 
 103         if bitmap_height % tile_height != 0:
 
 104             raise ValueError("Tile height must exactly divide bitmap height")
 
 105         self._tile_height = tile_height
 
 106         if not 0 <= default_tile <= 255:
 
 107             raise ValueError("Default Tile is out of range")
 
 108         self._pixel_width = width * tile_width
 
 109         self._pixel_height = height * tile_height
 
 110         self._tiles = (self._width * self._height) * [default_tile]
 
 111         self.in_group = False
 
 112         self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
 
 113         self._current_area = Rectangle(0, 0, self._pixel_width, self._pixel_height)
 
 116     def update_transform(self, absolute_transform):
 
 117         """Update the parent transform and child transforms"""
 
 118         self._absolute_transform = absolute_transform
 
 119         if self._absolute_transform is not None:
 
 120             self._update_current_x()
 
 121             self._update_current_y()
 
 123     def _update_current_x(self):
 
 124         if self._transpose_xy:
 
 125             width = self._pixel_height
 
 127             width = self._pixel_width
 
 128         if self._absolute_transform.transpose_xy:
 
 129             self._current_area.y1 = (
 
 130                 self._absolute_transform.y + self._absolute_transform.dy * self._x
 
 132             self._current_area.y2 = (
 
 133                 self._absolute_transform.y
 
 134                 + self._absolute_transform.dy * (self._x + width)
 
 136             if self._current_area.y2 < self._current_area.y1:
 
 137                 self._current_area.y1, self._current_area.y2 = (
 
 138                     self._current_area.y2,
 
 139                     self._current_area.y1,
 
 142             self._current_area.x1 = (
 
 143                 self._absolute_transform.x + self._absolute_transform.dx * self._x
 
 145             self._current_area.x2 = (
 
 146                 self._absolute_transform.x
 
 147                 + self._absolute_transform.dx * (self._x + width)
 
 149             if self._current_area.x2 < self._current_area.x1:
 
 150                 self._current_area.x1, self._current_area.x2 = (
 
 151                     self._current_area.x2,
 
 152                     self._current_area.x1,
 
 155     def _update_current_y(self):
 
 156         if self._transpose_xy:
 
 157             height = self._pixel_width
 
 159             height = self._pixel_height
 
 160         if self._absolute_transform.transpose_xy:
 
 161             self._current_area.x1 = (
 
 162                 self._absolute_transform.x + self._absolute_transform.dx * self._y
 
 164             self._current_area.x2 = (
 
 165                 self._absolute_transform.x
 
 166                 + self._absolute_transform.dx * (self._y + height)
 
 168             if self._current_area.x2 < self._current_area.x1:
 
 169                 self._current_area.x1, self._current_area.x2 = (
 
 170                     self._current_area.x2,
 
 171                     self._current_area.x1,
 
 174             self._current_area.y1 = (
 
 175                 self._absolute_transform.y + self._absolute_transform.dy * self._y
 
 177             self._current_area.y2 = (
 
 178                 self._absolute_transform.y
 
 179                 + self._absolute_transform.dy * (self._y + height)
 
 181             if self._current_area.y2 < self._current_area.y1:
 
 182                 self._current_area.y1, self._current_area.y2 = (
 
 183                     self._current_area.y2,
 
 184                     self._current_area.y1,
 
 187     # pylint: disable=too-many-locals
 
 188     def _fill_area(self, buffer):
 
 189         """Draw onto the image"""
 
 195             (self._width * self._tile_width, self._height * self._tile_height),
 
 199         tile_count_x = self._bitmap.width // self._tile_width
 
 203         for tile_x in range(self._width):
 
 204             for tile_y in range(self._height):
 
 205                 tile_index = self._tiles[tile_y * self._width + tile_x]
 
 206                 tile_index_x = tile_index % tile_count_x
 
 207                 tile_index_y = tile_index // tile_count_x
 
 208                 for pixel_x in range(self._tile_width):
 
 209                     for pixel_y in range(self._tile_height):
 
 210                         image_x = (tile_x * self._tile_width) + pixel_x
 
 211                         image_y = (tile_y * self._tile_height) + pixel_y
 
 212                         bitmap_x = (tile_index_x * self._tile_width) + pixel_x
 
 213                         bitmap_y = (tile_index_y * self._tile_height) + pixel_y
 
 214                         pixel_color = self._pixel_shader[
 
 215                             self._bitmap[bitmap_x, bitmap_y]
 
 217                         # if not pixel_color["transparent"]:
 
 218                         image.putpixel((image_x, image_y), pixel_color["rgba"])
 
 220         if self._absolute_transform is not None:
 
 221             if self._absolute_transform.scale > 1:
 
 222                 image = image.resize(
 
 224                         self._pixel_width * self._absolute_transform.scale,
 
 225                         self._pixel_height * self._absolute_transform.scale,
 
 227                     resample=Image.NEAREST,
 
 229             if self._absolute_transform.mirror_x:
 
 230                 image = image.transpose(Image.FLIP_LEFT_RIGHT)
 
 231             if self._absolute_transform.mirror_y:
 
 232                 image = image.transpose(Image.FLIP_TOP_BOTTOM)
 
 233             if self._absolute_transform.transpose_xy:
 
 234                 image = image.transpose(Image.TRANSPOSE)
 
 235             x *= self._absolute_transform.dx
 
 236             y *= self._absolute_transform.dy
 
 237             x += self._absolute_transform.x
 
 238             y += self._absolute_transform.y
 
 239         buffer.alpha_composite(image, (x, y))
 
 241     # pylint: enable=too-many-locals
 
 245         """True when the TileGrid is hidden. This may be False even
 
 246         when a part of a hidden Group."""
 
 250     def hidden(self, value):
 
 251         if not isinstance(value, (bool, int)):
 
 252             raise ValueError("Expecting a boolean or integer value")
 
 253         self._hidden = bool(value)
 
 257         """X position of the left edge in the parent."""
 
 262         if not isinstance(value, int):
 
 263             raise TypeError("X should be a integer type")
 
 266             self._update_current_x()
 
 270         """Y position of the top edge in the parent."""
 
 275         if not isinstance(value, int):
 
 276             raise TypeError("Y should be a integer type")
 
 279             self._update_current_y()
 
 283         """If true, the left edge rendered will be the right edge of the right-most tile."""
 
 287     def flip_x(self, value):
 
 288         if not isinstance(value, bool):
 
 289             raise TypeError("Flip X should be a boolean type")
 
 290         if self._flip_x != value:
 
 295         """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
 
 299     def flip_y(self, value):
 
 300         if not isinstance(value, bool):
 
 301             raise TypeError("Flip Y should be a boolean type")
 
 302         if self._flip_y != value:
 
 306     def transpose_xy(self):
 
 307         """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90
 
 308         degree rotation can be achieved along with the corresponding mirrored version.
 
 310         return self._transpose_xy
 
 313     def transpose_xy(self, value):
 
 314         if not isinstance(value, bool):
 
 315             raise TypeError("Transpose XY should be a boolean type")
 
 316         if self._transpose_xy != value:
 
 317             self._transpose_xy = value
 
 318             self._update_current_x()
 
 319             self._update_current_y()
 
 322     def pixel_shader(self):
 
 323         """The pixel shader of the tilegrid."""
 
 324         return self._pixel_shader
 
 326     def __getitem__(self, index):
 
 327         """Returns the tile index at the given index. The index can either be
 
 328         an x,y tuple or an int equal to ``y * width + x``'.
 
 330         if isinstance(index, (tuple, list)):
 
 333             index = y * self._width + x
 
 334         elif isinstance(index, int):
 
 335             x = index % self._width
 
 336             y = index // self._width
 
 337         if x > self._width or y > self._height or index >= len(self._tiles):
 
 338             raise ValueError("Tile index out of bounds")
 
 339         return self._tiles[index]
 
 341     def __setitem__(self, index, value):
 
 342         """Sets the tile index at the given index. The index can either be
 
 343         an x,y tuple or an int equal to ``y * width + x``.
 
 345         if isinstance(index, (tuple, list)):
 
 348             index = y * self._width + x
 
 349         elif isinstance(index, int):
 
 350             x = index % self._width
 
 351             y = index // self._width
 
 352         if x > self._width or y > self._height or index >= len(self._tiles):
 
 353             raise ValueError("Tile index out of bounds")
 
 354         if not 0 <= value <= 255:
 
 355             raise ValueError("Tile value out of bounds")
 
 356         self._tiles[index] = value
 
 359 # pylint: enable=too-many-instance-attributes