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:
 
 103             tile_width = bitmap_width
 
 104         if tile_height is None:
 
 105             tile_height = bitmap_height
 
 106         if bitmap_width % tile_width != 0:
 
 107             raise ValueError("Tile width must exactly divide bitmap width")
 
 108         self._tile_width = tile_width
 
 109         if bitmap_height % tile_height != 0:
 
 110             raise ValueError("Tile height must exactly divide bitmap height")
 
 111         self._tile_height = tile_height
 
 112         if not 0 <= default_tile <= 255:
 
 113             raise ValueError("Default Tile is out of range")
 
 114         self._pixel_width = width * tile_width
 
 115         self._pixel_height = height * tile_height
 
 116         self._tiles = (self._width * self._height) * [default_tile]
 
 117         self.in_group = False
 
 118         self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
 
 119         self._current_area = Rectangle(0, 0, self._pixel_width, self._pixel_height)
 
 122     def update_transform(self, absolute_transform):
 
 123         """Update the parent transform and child transforms"""
 
 124         self._absolute_transform = absolute_transform
 
 125         if self._absolute_transform is not None:
 
 126             self._update_current_x()
 
 127             self._update_current_y()
 
 129     def _update_current_x(self):
 
 130         if self._transpose_xy:
 
 131             width = self._pixel_height
 
 133             width = self._pixel_width
 
 134         if self._absolute_transform.transpose_xy:
 
 135             self._current_area.y1 = (
 
 136                 self._absolute_transform.y + self._absolute_transform.dy * self._x
 
 138             self._current_area.y2 = (
 
 139                 self._absolute_transform.y
 
 140                 + self._absolute_transform.dy * (self._x + width)
 
 142             if self._current_area.y2 < self._current_area.y1:
 
 143                 self._current_area.y1, self._current_area.y2 = (
 
 144                     self._current_area.y2,
 
 145                     self._current_area.y1,
 
 148             self._current_area.x1 = (
 
 149                 self._absolute_transform.x + self._absolute_transform.dx * self._x
 
 151             self._current_area.x2 = (
 
 152                 self._absolute_transform.x
 
 153                 + self._absolute_transform.dx * (self._x + width)
 
 155             if self._current_area.x2 < self._current_area.x1:
 
 156                 self._current_area.x1, self._current_area.x2 = (
 
 157                     self._current_area.x2,
 
 158                     self._current_area.x1,
 
 161     def _update_current_y(self):
 
 162         if self._transpose_xy:
 
 163             height = self._pixel_width
 
 165             height = self._pixel_height
 
 166         if self._absolute_transform.transpose_xy:
 
 167             self._current_area.x1 = (
 
 168                 self._absolute_transform.x + self._absolute_transform.dx * self._y
 
 170             self._current_area.x2 = (
 
 171                 self._absolute_transform.x
 
 172                 + self._absolute_transform.dx * (self._y + height)
 
 174             if self._current_area.x2 < self._current_area.x1:
 
 175                 self._current_area.x1, self._current_area.x2 = (
 
 176                     self._current_area.x2,
 
 177                     self._current_area.x1,
 
 180             self._current_area.y1 = (
 
 181                 self._absolute_transform.y + self._absolute_transform.dy * self._y
 
 183             self._current_area.y2 = (
 
 184                 self._absolute_transform.y
 
 185                 + self._absolute_transform.dy * (self._y + height)
 
 187             if self._current_area.y2 < self._current_area.y1:
 
 188                 self._current_area.y1, self._current_area.y2 = (
 
 189                     self._current_area.y2,
 
 190                     self._current_area.y1,
 
 193     def _shade(self, pixel_value):
 
 194         if isinstance(self._pixel_shader, Palette):
 
 195             return self._pixel_shader[pixel_value]["rgba"]
 
 196         if isinstance(self._pixel_shader, ColorConverter):
 
 197             return self._pixel_shader.convert(pixel_value)
 
 200     # pylint: disable=too-many-locals
 
 201     def _fill_area(self, buffer):
 
 202         """Draw onto the image"""
 
 208             (self._width * self._tile_width, self._height * self._tile_height),
 
 212         tile_count_x = self._bitmap.width // self._tile_width
 
 216         for tile_x in range(self._width):
 
 217             for tile_y in range(self._height):
 
 218                 tile_index = self._tiles[tile_y * self._width + tile_x]
 
 219                 tile_index_x = tile_index % tile_count_x
 
 220                 tile_index_y = tile_index // tile_count_x
 
 221                 for pixel_x in range(self._tile_width):
 
 222                     for pixel_y in range(self._tile_height):
 
 223                         image_x = (tile_x * self._tile_width) + pixel_x
 
 224                         image_y = (tile_y * self._tile_height) + pixel_y
 
 225                         bitmap_x = (tile_index_x * self._tile_width) + pixel_x
 
 226                         bitmap_y = (tile_index_y * self._tile_height) + pixel_y
 
 227                         pixel_color = self._shade(self._bitmap[bitmap_x, bitmap_y])
 
 228                         image.putpixel((image_x, image_y), pixel_color)
 
 230         if self._absolute_transform is not None:
 
 231             if self._absolute_transform.scale > 1:
 
 232                 image = image.resize(
 
 234                         self._pixel_width * self._absolute_transform.scale,
 
 235                         self._pixel_height * self._absolute_transform.scale,
 
 237                     resample=Image.NEAREST,
 
 239             if self._absolute_transform.mirror_x:
 
 240                 image = image.transpose(Image.FLIP_LEFT_RIGHT)
 
 241             if self._absolute_transform.mirror_y:
 
 242                 image = image.transpose(Image.FLIP_TOP_BOTTOM)
 
 243             if self._absolute_transform.transpose_xy:
 
 244                 image = image.transpose(Image.TRANSPOSE)
 
 245             x *= self._absolute_transform.dx
 
 246             y *= self._absolute_transform.dy
 
 247             x += self._absolute_transform.x
 
 248             y += self._absolute_transform.y
 
 249         buffer.alpha_composite(image, (x, y))
 
 251     # pylint: enable=too-many-locals
 
 255         """True when the TileGrid is hidden. This may be False even
 
 256         when a part of a hidden Group."""
 
 260     def hidden(self, value):
 
 261         if not isinstance(value, (bool, int)):
 
 262             raise ValueError("Expecting a boolean or integer value")
 
 263         self._hidden = bool(value)
 
 267         """X position of the left edge in the parent."""
 
 272         if not isinstance(value, int):
 
 273             raise TypeError("X should be a integer type")
 
 276             self._update_current_x()
 
 280         """Y position of the top edge in the parent."""
 
 285         if not isinstance(value, int):
 
 286             raise TypeError("Y should be a integer type")
 
 289             self._update_current_y()
 
 293         """If true, the left edge rendered will be the right edge of the right-most tile."""
 
 297     def flip_x(self, value):
 
 298         if not isinstance(value, bool):
 
 299             raise TypeError("Flip X should be a boolean type")
 
 300         if self._flip_x != value:
 
 305         """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
 
 309     def flip_y(self, value):
 
 310         if not isinstance(value, bool):
 
 311             raise TypeError("Flip Y should be a boolean type")
 
 312         if self._flip_y != value:
 
 316     def transpose_xy(self):
 
 317         """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90
 
 318         degree rotation can be achieved along with the corresponding mirrored version.
 
 320         return self._transpose_xy
 
 323     def transpose_xy(self, value):
 
 324         if not isinstance(value, bool):
 
 325             raise TypeError("Transpose XY should be a boolean type")
 
 326         if self._transpose_xy != value:
 
 327             self._transpose_xy = value
 
 328             self._update_current_x()
 
 329             self._update_current_y()
 
 332     def pixel_shader(self):
 
 333         """The pixel shader of the tilegrid."""
 
 334         return self._pixel_shader
 
 336     def __getitem__(self, index):
 
 337         """Returns the tile index at the given index. The index can either be
 
 338         an x,y tuple or an int equal to ``y * width + x``'.
 
 340         if isinstance(index, (tuple, list)):
 
 343             index = y * self._width + x
 
 344         elif isinstance(index, int):
 
 345             x = index % self._width
 
 346             y = index // self._width
 
 347         if x > self._width or y > self._height or index >= len(self._tiles):
 
 348             raise ValueError("Tile index out of bounds")
 
 349         return self._tiles[index]
 
 351     def __setitem__(self, index, value):
 
 352         """Sets the tile index at the given index. The index can either be
 
 353         an x,y tuple or an int equal to ``y * width + x``.
 
 355         if isinstance(index, (tuple, list)):
 
 358             index = y * self._width + x
 
 359         elif isinstance(index, int):
 
 360             x = index % self._width
 
 361             y = index // self._width
 
 362         if x > self._width or y > self._height or index >= len(self._tiles):
 
 363             raise ValueError("Tile index out of bounds")
 
 364         if not 0 <= value <= 255:
 
 365             raise ValueError("Tile value out of bounds")
 
 366         self._tiles[index] = value
 
 369 # pylint: enable=too-many-instance-attributes