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 pixel_shader is not None and not isinstance(
 
  86             pixel_shader, (ColorConverter, Palette)
 
  88             raise ValueError("Unsupported Pixel Shader type")
 
  89         self._pixel_shader = pixel_shader
 
  90         if isinstance(self._pixel_shader, ColorConverter):
 
  91             self._pixel_shader._rgba = True  # pylint: disable=protected-access
 
  95         self._width = width  # Number of Tiles Wide
 
  96         self._height = height  # Number of Tiles High
 
  97         self._transpose_xy = False
 
 102         if tile_width is None or tile_width == 0:
 
 103             tile_width = bitmap_width
 
 104         if tile_height is None or tile_width == 0:
 
 105             tile_height = bitmap_height
 
 110         if bitmap_width % tile_width != 0:
 
 111             raise ValueError("Tile width must exactly divide bitmap width")
 
 112         self._tile_width = tile_width
 
 113         if bitmap_height % tile_height != 0:
 
 114             raise ValueError("Tile height must exactly divide bitmap height")
 
 115         self._tile_height = tile_height
 
 116         if not 0 <= default_tile <= 255:
 
 117             raise ValueError("Default Tile is out of range")
 
 118         self._pixel_width = width * tile_width
 
 119         self._pixel_height = height * tile_height
 
 120         self._tiles = (self._width * self._height) * [default_tile]
 
 121         self.in_group = False
 
 122         self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
 
 123         self._current_area = Rectangle(0, 0, self._pixel_width, self._pixel_height)
 
 126     def update_transform(self, absolute_transform):
 
 127         """Update the parent transform and child transforms"""
 
 128         self._absolute_transform = absolute_transform
 
 129         if self._absolute_transform is not None:
 
 130             self._update_current_x()
 
 131             self._update_current_y()
 
 133     def _update_current_x(self):
 
 134         if self._transpose_xy:
 
 135             width = self._pixel_height
 
 137             width = self._pixel_width
 
 138         if self._absolute_transform.transpose_xy:
 
 139             self._current_area.y1 = (
 
 140                 self._absolute_transform.y + self._absolute_transform.dy * self._x
 
 142             self._current_area.y2 = (
 
 143                 self._absolute_transform.y
 
 144                 + self._absolute_transform.dy * (self._x + width)
 
 146             if self._current_area.y2 < self._current_area.y1:
 
 147                 self._current_area.y1, self._current_area.y2 = (
 
 148                     self._current_area.y2,
 
 149                     self._current_area.y1,
 
 152             self._current_area.x1 = (
 
 153                 self._absolute_transform.x + self._absolute_transform.dx * self._x
 
 155             self._current_area.x2 = (
 
 156                 self._absolute_transform.x
 
 157                 + self._absolute_transform.dx * (self._x + width)
 
 159             if self._current_area.x2 < self._current_area.x1:
 
 160                 self._current_area.x1, self._current_area.x2 = (
 
 161                     self._current_area.x2,
 
 162                     self._current_area.x1,
 
 165     def _update_current_y(self):
 
 166         if self._transpose_xy:
 
 167             height = self._pixel_width
 
 169             height = self._pixel_height
 
 170         if self._absolute_transform.transpose_xy:
 
 171             self._current_area.x1 = (
 
 172                 self._absolute_transform.x + self._absolute_transform.dx * self._y
 
 174             self._current_area.x2 = (
 
 175                 self._absolute_transform.x
 
 176                 + self._absolute_transform.dx * (self._y + height)
 
 178             if self._current_area.x2 < self._current_area.x1:
 
 179                 self._current_area.x1, self._current_area.x2 = (
 
 180                     self._current_area.x2,
 
 181                     self._current_area.x1,
 
 184             self._current_area.y1 = (
 
 185                 self._absolute_transform.y + self._absolute_transform.dy * self._y
 
 187             self._current_area.y2 = (
 
 188                 self._absolute_transform.y
 
 189                 + self._absolute_transform.dy * (self._y + height)
 
 191             if self._current_area.y2 < self._current_area.y1:
 
 192                 self._current_area.y1, self._current_area.y2 = (
 
 193                     self._current_area.y2,
 
 194                     self._current_area.y1,
 
 197     def _shade(self, pixel_value):
 
 198         if isinstance(self._pixel_shader, Palette):
 
 199             return self._pixel_shader[pixel_value]["rgba"]
 
 200         if isinstance(self._pixel_shader, ColorConverter):
 
 201             return self._pixel_shader.convert(pixel_value)
 
 204     def _apply_palette(self, image):
 
 206             self._pixel_shader._get_palette()  # pylint: disable=protected-access
 
 209     def _add_alpha(self, image):
 
 210         alpha = self._bitmap._image.copy().convert(  # pylint: disable=protected-access
 
 214             self._pixel_shader._get_alpha_palette()  # pylint: disable=protected-access
 
 216         image.putalpha(alpha.convert("L"))
 
 218     # pylint: disable=too-many-locals,too-many-branches,too-many-statements
 
 219     def _fill_area(self, buffer):
 
 220         """Draw onto the image"""
 
 224         if self._bitmap.width <= 0 or self._bitmap.height <= 0:
 
 227         # Copy class variables to local variables in case something changes
 
 231         height = self._height
 
 232         tile_width = self._tile_width
 
 233         tile_height = self._tile_height
 
 234         bitmap_width = self._bitmap.width
 
 235         pixel_width = self._pixel_width
 
 236         pixel_height = self._pixel_height
 
 238         absolute_transform = self._absolute_transform
 
 239         pixel_shader = self._pixel_shader
 
 240         bitmap = self._bitmap
 
 243         tile_count_x = bitmap_width // tile_width
 
 246             "RGBA", (width * tile_width, height * tile_height), (0, 0, 0, 0),
 
 249         for tile_x in range(width):
 
 250             for tile_y in range(height):
 
 251                 tile_index = tiles[tile_y * width + tile_x]
 
 252                 tile_index_x = tile_index % tile_count_x
 
 253                 tile_index_y = tile_index // tile_count_x
 
 254                 tile_image = bitmap._image  # pylint: disable=protected-access
 
 255                 if isinstance(pixel_shader, Palette):
 
 256                     tile_image = tile_image.copy().convert("P")
 
 257                     self._apply_palette(tile_image)
 
 258                     tile_image = tile_image.convert("RGBA")
 
 259                     self._add_alpha(tile_image)
 
 260                 elif isinstance(pixel_shader, ColorConverter):
 
 261                     # This will be needed for eInks, grayscale, and monochrome displays
 
 263                 image.alpha_composite(
 
 265                     dest=(tile_x * tile_width, tile_y * tile_height),
 
 266                     source=(tile_index_x * tile_width, tile_index_y * tile_height,),
 
 269         if absolute_transform is not None:
 
 270             if absolute_transform.scale > 1:
 
 271                 image = image.resize(
 
 273                         pixel_width * absolute_transform.scale,
 
 274                         pixel_height * absolute_transform.scale,
 
 276                     resample=Image.NEAREST,
 
 278             if absolute_transform.mirror_x:
 
 279                 image = image.transpose(Image.FLIP_LEFT_RIGHT)
 
 280             if absolute_transform.mirror_y:
 
 281                 image = image.transpose(Image.FLIP_TOP_BOTTOM)
 
 282             if absolute_transform.transpose_xy:
 
 283                 image = image.transpose(Image.TRANSPOSE)
 
 284             x *= absolute_transform.dx
 
 285             y *= absolute_transform.dy
 
 286             x += absolute_transform.x
 
 287             y += absolute_transform.y
 
 289         source_x = source_y = 0
 
 291             source_x = round(0 - x)
 
 294             source_y = round(0 - y)
 
 302             and y <= buffer.height
 
 303             and source_x <= image.width
 
 304             and source_y <= image.height
 
 306             buffer.alpha_composite(image, (x, y), source=(source_x, source_y))
 
 308     # pylint: enable=too-many-locals,too-many-branches
 
 312         """True when the TileGrid is hidden. This may be False even
 
 313         when a part of a hidden Group."""
 
 317     def hidden(self, value):
 
 318         if not isinstance(value, (bool, int)):
 
 319             raise ValueError("Expecting a boolean or integer value")
 
 320         self._hidden = bool(value)
 
 324         """X position of the left edge in the parent."""
 
 329         if not isinstance(value, int):
 
 330             raise TypeError("X should be a integer type")
 
 333             self._update_current_x()
 
 337         """Y position of the top edge in the parent."""
 
 342         if not isinstance(value, int):
 
 343             raise TypeError("Y should be a integer type")
 
 346             self._update_current_y()
 
 350         """If true, the left edge rendered will be the right edge of the right-most tile."""
 
 354     def flip_x(self, value):
 
 355         if not isinstance(value, bool):
 
 356             raise TypeError("Flip X should be a boolean type")
 
 357         if self._flip_x != value:
 
 362         """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
 
 366     def flip_y(self, value):
 
 367         if not isinstance(value, bool):
 
 368             raise TypeError("Flip Y should be a boolean type")
 
 369         if self._flip_y != value:
 
 373     def transpose_xy(self):
 
 374         """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90
 
 375         degree rotation can be achieved along with the corresponding mirrored version.
 
 377         return self._transpose_xy
 
 380     def transpose_xy(self, value):
 
 381         if not isinstance(value, bool):
 
 382             raise TypeError("Transpose XY should be a boolean type")
 
 383         if self._transpose_xy != value:
 
 384             self._transpose_xy = value
 
 385             self._update_current_x()
 
 386             self._update_current_y()
 
 389     def pixel_shader(self):
 
 390         """The pixel shader of the tilegrid."""
 
 391         return self._pixel_shader
 
 393     def __getitem__(self, index):
 
 394         """Returns the tile index at the given index. The index can either be
 
 395         an x,y tuple or an int equal to ``y * width + x``'.
 
 397         if isinstance(index, (tuple, list)):
 
 400             index = y * self._width + x
 
 401         elif isinstance(index, int):
 
 402             x = index % self._width
 
 403             y = index // self._width
 
 404         if x > self._width or y > self._height or index >= len(self._tiles):
 
 405             raise ValueError("Tile index out of bounds")
 
 406         return self._tiles[index]
 
 408     def __setitem__(self, index, value):
 
 409         """Sets the tile index at the given index. The index can either be
 
 410         an x,y tuple or an int equal to ``y * width + x``.
 
 412         if isinstance(index, (tuple, list)):
 
 415             index = y * self._width + x
 
 416         elif isinstance(index, int):
 
 417             x = index % self._width
 
 418             y = index // self._width
 
 419         if x > self._width or y > self._height or index >= len(self._tiles):
 
 420             raise ValueError("Tile index out of bounds")
 
 421         if not 0 <= value <= 255:
 
 422             raise ValueError("Tile value out of bounds")
 
 423         self._tiles[index] = value
 
 426 # pylint: enable=too-many-instance-attributes