1 # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
 
   3 # SPDX-License-Identifier: MIT
 
   7 ================================================================================
 
  11 **Software and Dependencies:**
 
  14   https://github.com/adafruit/Adafruit_Blinka/releases
 
  16 * Author(s): Melissa LeBlanc-Williams
 
  20 from recordclass import recordclass
 
  22 from displayio.bitmap import Bitmap
 
  23 from displayio.colorconverter import ColorConverter
 
  24 from displayio.ondiskbitmap import OnDiskBitmap
 
  25 from displayio.shape import Shape
 
  26 from displayio.palette import Palette
 
  28 __version__ = "0.0.0-auto.0"
 
  29 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
  31 Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
 
  32 Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y")
 
  36     # pylint: disable=too-many-instance-attributes
 
  37     """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
 
  38     grids can share bitmaps and pixel shaders.
 
  40     A single tile grid is also known as a Sprite.
 
  56         """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is
 
  57         used to convert the value and its location to a display native pixel color. This may
 
  58         be a simple color palette lookup, a gradient, a pattern or a color transformer.
 
  60         tile_width and tile_height match the height of the bitmap by default.
 
  62         if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
 
  63             raise ValueError("Unsupported Bitmap type")
 
  65         bitmap_width = bitmap.width
 
  66         bitmap_height = bitmap.height
 
  68         if pixel_shader is not None and not isinstance(
 
  69             pixel_shader, (ColorConverter, Palette)
 
  71             raise ValueError("Unsupported Pixel Shader type")
 
  72         self._pixel_shader = pixel_shader
 
  73         if isinstance(self._pixel_shader, ColorConverter):
 
  74             self._pixel_shader._rgba = True  # pylint: disable=protected-access
 
  75         self._hidden_tilegrid = False
 
  78         self._width = width  # Number of Tiles Wide
 
  79         self._height = height  # Number of Tiles High
 
  80         self._transpose_xy = False
 
  85         if tile_width is None or tile_width == 0:
 
  86             tile_width = bitmap_width
 
  87         if tile_height is None or tile_width == 0:
 
  88             tile_height = bitmap_height
 
  93         if bitmap_width % tile_width != 0:
 
  94             raise ValueError("Tile width must exactly divide bitmap width")
 
  95         self._tile_width = tile_width
 
  96         if bitmap_height % tile_height != 0:
 
  97             raise ValueError("Tile height must exactly divide bitmap height")
 
  98         self._tile_height = tile_height
 
  99         if not 0 <= default_tile <= 255:
 
 100             raise ValueError("Default Tile is out of range")
 
 101         self._pixel_width = width * tile_width
 
 102         self._pixel_height = height * tile_height
 
 103         self._tiles = (self._width * self._height) * [default_tile]
 
 104         self.in_group = False
 
 105         self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
 
 106         self._current_area = Rectangle(0, 0, self._pixel_width, self._pixel_height)
 
 109     def update_transform(self, absolute_transform):
 
 110         """Update the parent transform and child transforms"""
 
 111         self._absolute_transform = absolute_transform
 
 112         if self._absolute_transform is not None:
 
 113             self._update_current_x()
 
 114             self._update_current_y()
 
 116     def _update_current_x(self):
 
 117         if self._transpose_xy:
 
 118             width = self._pixel_height
 
 120             width = self._pixel_width
 
 121         if self._absolute_transform.transpose_xy:
 
 122             self._current_area.y1 = (
 
 123                 self._absolute_transform.y + self._absolute_transform.dy * self._x
 
 125             self._current_area.y2 = (
 
 126                 self._absolute_transform.y
 
 127                 + self._absolute_transform.dy * (self._x + width)
 
 129             if self._current_area.y2 < self._current_area.y1:
 
 130                 self._current_area.y1, self._current_area.y2 = (
 
 131                     self._current_area.y2,
 
 132                     self._current_area.y1,
 
 135             self._current_area.x1 = (
 
 136                 self._absolute_transform.x + self._absolute_transform.dx * self._x
 
 138             self._current_area.x2 = (
 
 139                 self._absolute_transform.x
 
 140                 + self._absolute_transform.dx * (self._x + width)
 
 142             if self._current_area.x2 < self._current_area.x1:
 
 143                 self._current_area.x1, self._current_area.x2 = (
 
 144                     self._current_area.x2,
 
 145                     self._current_area.x1,
 
 148     def _update_current_y(self):
 
 149         if self._transpose_xy:
 
 150             height = self._pixel_width
 
 152             height = self._pixel_height
 
 153         if self._absolute_transform.transpose_xy:
 
 154             self._current_area.x1 = (
 
 155                 self._absolute_transform.x + self._absolute_transform.dx * self._y
 
 157             self._current_area.x2 = (
 
 158                 self._absolute_transform.x
 
 159                 + self._absolute_transform.dx * (self._y + height)
 
 161             if self._current_area.x2 < self._current_area.x1:
 
 162                 self._current_area.x1, self._current_area.x2 = (
 
 163                     self._current_area.x2,
 
 164                     self._current_area.x1,
 
 167             self._current_area.y1 = (
 
 168                 self._absolute_transform.y + self._absolute_transform.dy * self._y
 
 170             self._current_area.y2 = (
 
 171                 self._absolute_transform.y
 
 172                 + self._absolute_transform.dy * (self._y + height)
 
 174             if self._current_area.y2 < self._current_area.y1:
 
 175                 self._current_area.y1, self._current_area.y2 = (
 
 176                     self._current_area.y2,
 
 177                     self._current_area.y1,
 
 180     def _shade(self, pixel_value):
 
 181         if isinstance(self._pixel_shader, Palette):
 
 182             return self._pixel_shader[pixel_value]["rgba"]
 
 183         if isinstance(self._pixel_shader, ColorConverter):
 
 184             return self._pixel_shader.convert(pixel_value)
 
 187     def _apply_palette(self, image):
 
 189             self._pixel_shader._get_palette()  # pylint: disable=protected-access
 
 192     def _add_alpha(self, image):
 
 193         alpha = self._bitmap._image.copy().convert(  # pylint: disable=protected-access
 
 197             self._pixel_shader._get_alpha_palette()  # pylint: disable=protected-access
 
 199         image.putalpha(alpha.convert("L"))
 
 201     def _fill_area(self, buffer):
 
 202         # pylint: disable=too-many-locals,too-many-branches,too-many-statements
 
 203         """Draw onto the image"""
 
 204         if self._hidden_tilegrid:
 
 207         if self._bitmap.width <= 0 or self._bitmap.height <= 0:
 
 210         # Copy class variables to local variables in case something changes
 
 214         height = self._height
 
 215         tile_width = self._tile_width
 
 216         tile_height = self._tile_height
 
 217         bitmap_width = self._bitmap.width
 
 218         pixel_width = self._pixel_width
 
 219         pixel_height = self._pixel_height
 
 221         absolute_transform = self._absolute_transform
 
 222         pixel_shader = self._pixel_shader
 
 223         bitmap = self._bitmap
 
 226         tile_count_x = bitmap_width // tile_width
 
 230             (width * tile_width, height * tile_height),
 
 234         for tile_x in range(width):
 
 235             for tile_y in range(height):
 
 236                 tile_index = tiles[tile_y * width + tile_x]
 
 237                 tile_index_x = tile_index % tile_count_x
 
 238                 tile_index_y = tile_index // tile_count_x
 
 239                 tile_image = bitmap._image  # pylint: disable=protected-access
 
 240                 if isinstance(pixel_shader, Palette):
 
 241                     tile_image = tile_image.copy().convert("P")
 
 242                     self._apply_palette(tile_image)
 
 243                     tile_image = tile_image.convert("RGBA")
 
 244                     self._add_alpha(tile_image)
 
 245                 elif isinstance(pixel_shader, ColorConverter):
 
 246                     # This will be needed for eInks, grayscale, and monochrome displays
 
 248                 image.alpha_composite(
 
 250                     dest=(tile_x * tile_width, tile_y * tile_height),
 
 252                         tile_index_x * tile_width,
 
 253                         tile_index_y * tile_height,
 
 254                         tile_index_x * tile_width + tile_width,
 
 255                         tile_index_y * tile_height + tile_height,
 
 259         if absolute_transform is not None:
 
 260             if absolute_transform.scale > 1:
 
 261                 image = image.resize(
 
 263                         int(pixel_width * absolute_transform.scale),
 
 265                             pixel_height * absolute_transform.scale,
 
 268                     resample=Image.NEAREST,
 
 270             if absolute_transform.mirror_x:
 
 271                 image = image.transpose(Image.FLIP_LEFT_RIGHT)
 
 272             if absolute_transform.mirror_y:
 
 273                 image = image.transpose(Image.FLIP_TOP_BOTTOM)
 
 274             if absolute_transform.transpose_xy:
 
 275                 image = image.transpose(Image.TRANSPOSE)
 
 276             x *= absolute_transform.dx
 
 277             y *= absolute_transform.dy
 
 278             x += absolute_transform.x
 
 279             y += absolute_transform.y
 
 281         source_x = source_y = 0
 
 283             source_x = round(0 - x)
 
 286             source_y = round(0 - y)
 
 294             and y <= buffer.height
 
 295             and source_x <= image.width
 
 296             and source_y <= image.height
 
 298             buffer.alpha_composite(image, (x, y), source=(source_x, source_y))
 
 302         """True when the TileGrid is hidden. This may be False even
 
 303         when a part of a hidden Group."""
 
 304         return self._hidden_tilegrid
 
 307     def hidden(self, value):
 
 308         if not isinstance(value, (bool, int)):
 
 309             raise ValueError("Expecting a boolean or integer value")
 
 310         self._hidden_tilegrid = bool(value)
 
 314         """X position of the left edge in the parent."""
 
 319         if not isinstance(value, int):
 
 320             raise TypeError("X should be a integer type")
 
 323             self._update_current_x()
 
 327         """Y position of the top edge in the parent."""
 
 332         if not isinstance(value, int):
 
 333             raise TypeError("Y should be a integer type")
 
 336             self._update_current_y()
 
 340         """If true, the left edge rendered will be the right edge of the right-most tile."""
 
 344     def flip_x(self, value):
 
 345         if not isinstance(value, bool):
 
 346             raise TypeError("Flip X should be a boolean type")
 
 347         if self._flip_x != value:
 
 352         """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
 
 356     def flip_y(self, value):
 
 357         if not isinstance(value, bool):
 
 358             raise TypeError("Flip Y should be a boolean type")
 
 359         if self._flip_y != value:
 
 363     def transpose_xy(self):
 
 364         """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90
 
 365         degree rotation can be achieved along with the corresponding mirrored version.
 
 367         return self._transpose_xy
 
 370     def transpose_xy(self, value):
 
 371         if not isinstance(value, bool):
 
 372             raise TypeError("Transpose XY should be a boolean type")
 
 373         if self._transpose_xy != value:
 
 374             self._transpose_xy = value
 
 375             self._update_current_x()
 
 376             self._update_current_y()
 
 379     def pixel_shader(self):
 
 380         """The pixel shader of the tilegrid."""
 
 381         return self._pixel_shader
 
 383     def _extract_and_check_index(self, index):
 
 384         if isinstance(index, (tuple, list)):
 
 387             index = y * self._width + x
 
 388         elif isinstance(index, int):
 
 389             x = index % self._width
 
 390             y = index // self._width
 
 391         if x > self._width or y > self._height or index >= len(self._tiles):
 
 392             raise ValueError("Tile index out of bounds")
 
 395     def __getitem__(self, index):
 
 396         """Returns the tile index at the given index. The index can either be
 
 397         an x,y tuple or an int equal to ``y * width + x``'.
 
 399         index = self._extract_and_check_index(index)
 
 400         return self._tiles[index]
 
 402     def __setitem__(self, index, value):
 
 403         """Sets the tile index at the given index. The index can either be
 
 404         an x,y tuple or an int equal to ``y * width + x``.
 
 406         index = self._extract_and_check_index(index)
 
 407         if not 0 <= value <= 255:
 
 408             raise ValueError("Tile value out of bounds")
 
 409         self._tiles[index] = value