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
 
  24 `displayio.epaperdisplay`
 
  25 ================================================================================
 
  29 **Software and Dependencies:**
 
  32   https://github.com/adafruit/Adafruit_Blinka/releases
 
  34 * Author(s): Melissa LeBlanc-Williams
 
  40 from recordclass import recordclass
 
  42 from displayio.bitmap import Bitmap
 
  43 from displayio.colorconverter import ColorConverter
 
  45 __version__ = "0.0.0-auto.0"
 
  46 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
 
  48 # pylint: disable=unnecessary-pass, unused-argument
 
  50 Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
 
  51 Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y")
 
  55     """Manage updating an epaper display over a display bus
 
  57     This initializes an epaper display and connects it into CircuitPython. Unlike other
 
  58     objects in CircuitPython, EPaperDisplay objects live until
 
  59     displayio.release_displays() is called. This is done so that CircuitPython can use
 
  62     Most people should not use this class directly. Use a specific display driver instead
 
  63     that will contain the startup and shutdown sequences at minimum.
 
  66     # pylint: disable=too-many-locals
 
  80         set_column_window_command=None,
 
  81         set_row_window_command=None,
 
  82         single_byte_bounds=False,
 
  83         write_black_ram_command,
 
  84         black_bits_inverted=False,
 
  85         write_color_ram_command=None,
 
  86         color_bits_inverted=False,
 
  87         highlight_color=0x000000,
 
  88         refresh_display_command,
 
  92         seconds_per_frame=180,
 
  93         always_toggle_chip_select=False
 
  96         Create a EPaperDisplay object on the given display bus (displayio.FourWire or
 
  97         displayio.ParallelBus).
 
  99         The start_sequence and stop_sequence are bitpacked to minimize the ram impact. Every
 
 100         command begins with a command byte followed by a byte to determine the parameter
 
 101         count and if a delay is need after. When the top bit of the second byte is 1, the
 
 102         next byte will be the delay time in milliseconds. The remaining 7 bits are the
 
 103         parameter count excluding any delay byte. The third through final bytes are the
 
 104         remaining command parameters. The next byte will begin a new command definition.
 
 108     # pylint: enable=too-many-locals
 
 110     def show(self, group):
 
 111         """Switches to displaying the given group of layers. When group is None, the default
 
 112         CircuitPython terminal will be shown (eventually).
 
 117         """Refreshes the display immediately or raises an exception if too soon. Use
 
 118         ``time.sleep(display.time_to_refresh)`` to sleep until a refresh can occur.
 
 123     def time_to_refresh(self):
 
 124         """Time, in fractional seconds, until the ePaper display can be refreshed."""
 
 139         """Current Display Bus"""
 
 144     """Manage updating a display over SPI four wire protocol in the background while
 
 145     Python code runs. It doesn’t handle display initialization.
 
 159         """Create a FourWire object associated with the given pins.
 
 161         The SPI bus and pins are then in use by the display until
 
 162         displayio.release_displays() is called even after a reload. (It does this so
 
 163         CircuitPython can use the display after your code is done.)
 
 164         So, the first time you initialize a display bus in code.py you should call
 
 165         :py:func`displayio.release_displays` first, otherwise it will error after the
 
 168         self._dc = digitalio.DigitalInOut(command)
 
 169         self._dc.switch_to_output()
 
 170         self._chip_select = digitalio.DigitalInOut(chip_select)
 
 171         self._chip_select.switch_to_output(value=True)
 
 173         if reset is not None:
 
 174             self._reset = digitalio.DigitalInOut(reset)
 
 175             self._reset.switch_to_output(value=True)
 
 179         while self._spi.try_lock():
 
 181         self._spi.configure(baudrate=baudrate, polarity=polarity, phase=phase)
 
 188         self._chip_select.deinit()
 
 189         if self._reset is not None:
 
 193         """Performs a hardware reset via the reset pin.
 
 194         Raises an exception if called when no reset pin is available.
 
 196         if self._reset is not None:
 
 197             self._reset.value = False
 
 199             self._reset.value = True
 
 202             raise RuntimeError("No reset pin defined")
 
 204     def send(self, is_command, data, *, toggle_every_byte=False):
 
 205         """Sends the given command value followed by the full set of data. Display state,
 
 206         such as vertical scroll, set via ``send`` may or may not be reset once the code is
 
 209         while self._spi.try_lock():
 
 211         self._dc.value = not is_command
 
 212         if toggle_every_byte:
 
 214                 self._spi.write(bytes([byte]))
 
 215                 self._chip_select.value = True
 
 217                 self._chip_select.value = False
 
 219             self._spi.write(data)
 
 224     """Manage a group of sprites and groups and how they are inter-related."""
 
 226     def __init__(self, *, max_size=4, scale=1, x=0, y=0):
 
 227         """Create a Group of a given size and scale. Scale is in
 
 228         one dimension. For example, scale=2 leads to a layer’s
 
 229         pixel being 2x2 pixels when in the group.
 
 231         if not isinstance(max_size, int) or max_size < 1:
 
 232             raise ValueError("Max Size must be >= 1")
 
 233         self._max_size = max_size
 
 234         if not isinstance(scale, int) or scale < 1:
 
 235             raise ValueError("Scale must be >= 1")
 
 241         self._supported_types = (TileGrid, Group)
 
 242         self._absolute_transform = None
 
 243         self.in_group = False
 
 244         self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
 
 246     def update_transform(self, parent_transform):
 
 247         """Update the parent transform and child transforms"""
 
 248         self.in_group = parent_transform is not None
 
 252             if parent_transform.transpose_xy:
 
 254             self._absolute_transform.x = parent_transform.x + parent_transform.dx * x
 
 255             self._absolute_transform.y = parent_transform.y + parent_transform.dy * y
 
 256             self._absolute_transform.dx = parent_transform.dx * self._scale
 
 257             self._absolute_transform.dy = parent_transform.dy * self._scale
 
 258             self._absolute_transform.transpose_xy = parent_transform.transpose_xy
 
 259             self._absolute_transform.mirror_x = parent_transform.mirror_x
 
 260             self._absolute_transform.mirror_y = parent_transform.mirror_y
 
 261             self._absolute_transform.scale = parent_transform.scale * self._scale
 
 262         self._update_child_transforms()
 
 264     def _update_child_transforms(self):
 
 266             for layer in self._layers:
 
 267                 layer.update_transform(self._absolute_transform)
 
 269     def _removal_cleanup(self, index):
 
 270         layer = self._layers[index]
 
 271         layer.update_transform(None)
 
 273     def _layer_update(self, index):
 
 274         layer = self._layers[index]
 
 275         layer.update_transform(self._absolute_transform)
 
 277     def append(self, layer):
 
 278         """Append a layer to the group. It will be drawn
 
 281         self.insert(len(self._layers), layer)
 
 283     def insert(self, index, layer):
 
 284         """Insert a layer into the group."""
 
 285         if not isinstance(layer, self._supported_types):
 
 286             raise ValueError("Invalid Group Member")
 
 288             raise ValueError("Layer already in a group.")
 
 289         if len(self._layers) == self._max_size:
 
 290             raise RuntimeError("Group full")
 
 291         self._layers.insert(index, layer)
 
 292         self._layer_update(index)
 
 294     def index(self, layer):
 
 295         """Returns the index of the first copy of layer.
 
 296         Raises ValueError if not found.
 
 298         return self._layers.index(layer)
 
 300     def pop(self, index=-1):
 
 301         """Remove the ith item and return it."""
 
 302         self._removal_cleanup(index)
 
 303         return self._layers.pop(index)
 
 305     def remove(self, layer):
 
 306         """Remove the first copy of layer. Raises ValueError
 
 307         if it is not present."""
 
 308         index = self.index(layer)
 
 309         self._layers.pop(index)
 
 312         """Returns the number of layers in a Group"""
 
 313         return len(self._layers)
 
 315     def __getitem__(self, index):
 
 316         """Returns the value at the given index."""
 
 317         return self._layers[index]
 
 319     def __setitem__(self, index, value):
 
 320         """Sets the value at the given index."""
 
 321         self._removal_cleanup(index)
 
 322         self._layers[index] = value
 
 323         self._layer_update(index)
 
 325     def __delitem__(self, index):
 
 326         """Deletes the value at the given index."""
 
 327         del self._layers[index]
 
 329     def _fill_area(self, buffer):
 
 333         for layer in self._layers:
 
 334             if isinstance(layer, (Group, TileGrid)):
 
 335                 layer._fill_area(buffer)  # pylint: disable=protected-access
 
 339         """True when the Group and all of it’s layers are not visible. When False, the
 
 340         Group’s layers are visible if they haven’t been hidden.
 
 345     def hidden(self, value):
 
 346         if not isinstance(value, (bool, int)):
 
 347             raise ValueError("Expecting a boolean or integer value")
 
 348         self._hidden = bool(value)
 
 352         """Scales each pixel within the Group in both directions. For example, when
 
 353         scale=2 each pixel will be represented by 2x2 pixels.
 
 358     def scale(self, value):
 
 359         if not isinstance(value, int) or value < 1:
 
 360             raise ValueError("Scale must be >= 1")
 
 361         if self._scale != value:
 
 362             parent_scale = self._absolute_transform.scale / self._scale
 
 363             self._absolute_transform.dx = (
 
 364                 self._absolute_transform.dx / self._scale * value
 
 366             self._absolute_transform.dy = (
 
 367                 self._absolute_transform.dy / self._scale * value
 
 369             self._absolute_transform.scale = parent_scale * value
 
 372             self._update_child_transforms()
 
 376         """X position of the Group in the parent."""
 
 381         if not isinstance(value, int):
 
 382             raise ValueError("x must be an integer")
 
 384             if self._absolute_transform.transpose_xy:
 
 385                 dy_value = self._absolute_transform.dy / self._scale
 
 386                 self._absolute_transform.y += dy_value * (value - self._x)
 
 388                 dx_value = self._absolute_transform.dx / self._scale
 
 389                 self._absolute_transform.x += dx_value * (value - self._x)
 
 391             self._update_child_transforms()
 
 395         """Y position of the Group in the parent."""
 
 400         if not isinstance(value, int):
 
 401             raise ValueError("y must be an integer")
 
 403             if self._absolute_transform.transpose_xy:
 
 404                 dx_value = self._absolute_transform.dx / self._scale
 
 405                 self._absolute_transform.x += dx_value * (value - self._y)
 
 407                 dy_value = self._absolute_transform.dy / self._scale
 
 408                 self._absolute_transform.y += dy_value * (value - self._y)
 
 410             self._update_child_transforms()
 
 414     """Manage updating a display over I2C in the background while Python code runs.
 
 415     It doesn’t handle display initialization.
 
 418     def __init__(self, i2c_bus, *, device_address, reset=None):
 
 419         """Create a I2CDisplay object associated with the given I2C bus and reset pin.
 
 421         The I2C bus and pins are then in use by the display until displayio.release_displays() is
 
 422         called even after a reload. (It does this so CircuitPython can use the display after your
 
 423         code is done.) So, the first time you initialize a display bus in code.py you should call
 
 424         :py:func`displayio.release_displays` first, otherwise it will error after the first
 
 430         """Performs a hardware reset via the reset pin. Raises an exception if called
 
 431         when no reset pin is available.
 
 435     def send(self, command, data):
 
 436         """Sends the given command value followed by the full set of data. Display state,
 
 437         such as vertical scroll, set via send may or may not be reset once the code is
 
 445     Loads values straight from disk. This minimizes memory use but can lead to much slower
 
 446     pixel load times. These load times may result in frame tearing where only part of the
 
 449     def __init__(self, file):
 
 450         self._image = Image.open(file)
 
 454         """Width of the bitmap. (read only)"""
 
 455         return self._image.width
 
 459         """Height of the bitmap. (read only)"""
 
 460         return self._image.height
 
 464     """Map a pixel palette_index to a full color. Colors are transformed to the display’s
 
 465     format internally to save memory.
 
 468     def __init__(self, color_count):
 
 469         """Create a Palette object to store a set number of colors."""
 
 470         self._needs_refresh = False
 
 473         for _ in range(color_count):
 
 474             self._colors.append(self._make_color(0))
 
 476     def _make_color(self, value, transparent=False):
 
 478             "transparent": transparent,
 
 481         if isinstance(value, (tuple, list, bytes, bytearray)):
 
 482             value = (value[0] & 0xFF) << 16 | (value[1] & 0xFF) << 8 | value[2] & 0xFF
 
 483         elif isinstance(value, int):
 
 484             if not 0 <= value <= 0xFFFFFF:
 
 485                 raise ValueError("Color must be between 0x000000 and 0xFFFFFF")
 
 487             raise TypeError("Color buffer must be a buffer, tuple, list, or int")
 
 488         color["rgb888"] = value
 
 489         self._needs_refresh = True
 
 494         """Returns the number of colors in a Palette"""
 
 495         return len(self._colors)
 
 497     def __setitem__(self, index, value):
 
 498         """Sets the pixel color at the given index. The index should be
 
 499         an integer in the range 0 to color_count-1.
 
 501         The value argument represents a color, and can be from 0x000000 to 0xFFFFFF
 
 502         (to represent an RGB value). Value can be an int, bytes (3 bytes (RGB) or
 
 503         4 bytes (RGB + pad byte)), bytearray, or a tuple or list of 3 integers.
 
 505         if self._colors[index]["rgb888"] != value:
 
 506             self._colors[index] = self._make_color(value)
 
 508     def __getitem__(self, index):
 
 509         if not 0 <= index < len(self._colors):
 
 510             raise ValueError("Palette index out of range")
 
 511         return self._colors[index]
 
 513     def make_transparent(self, palette_index):
 
 514         """Set the palette index to be a transparent color"""
 
 515         self._colors[palette_index]["transparent"] = True
 
 517     def make_opaque(self, palette_index):
 
 518         """Set the palette index to be an opaque color"""
 
 519         self._colors[palette_index]["transparent"] = False
 
 523     """Manage updating a display over 8-bit parallel bus in the background while Python code
 
 524     runs. This protocol may be refered to as 8080-I Series Parallel Interface in datasheets.
 
 525     It doesn’t handle display initialization.
 
 528     def __init__(self, i2c_bus, *, device_address, reset=None):
 
 529         """Create a ParallelBus object associated with the given pins. The
 
 530         bus is inferred from data0 by implying the next 7 additional pins on a given GPIO
 
 533         The parallel bus and pins are then in use by the display until
 
 534         displayio.release_displays() is called even after a reload. (It does this so
 
 535         CircuitPython can use the display after your code is done.) So, the first time you
 
 536         initialize a display bus in code.py you should call
 
 537         :py:func`displayio.release_displays` first, otherwise it will error after the first
 
 543         """Performs a hardware reset via the reset pin. Raises an exception if called when
 
 544         no reset pin is available.
 
 548     def send(self, command, data):
 
 549         """Sends the given command value followed by the full set of data. Display state,
 
 550         such as vertical scroll, set via ``send`` may or may not be reset once the code is
 
 557     """Create a Shape object with the given fixed size. Each pixel is one bit and is stored
 
 558     by the column boundaries of the shape on each row. Each row’s boundary defaults to the
 
 562     def __init__(self, width, height, *, mirror_x=False, mirror_y=False):
 
 563         """Create a Shape object with the given fixed size. Each pixel is one bit and is
 
 564         stored by the column boundaries of the shape on each row. Each row’s boundary
 
 565         defaults to the full row.
 
 567         super().__init__(width, height, 2)
 
 569     def set_boundary(self, y, start_x, end_x):
 
 570         """Loads pre-packed data into the given row."""
 
 574 # pylint: disable=too-many-instance-attributes
 
 576     """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
 
 577     grids can share bitmaps and pixel shaders.
 
 579     A single tile grid is also known as a Sprite.
 
 595         """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is
 
 596         used to convert the value and its location to a display native pixel color. This may
 
 597         be a simple color palette lookup, a gradient, a pattern or a color transformer.
 
 599         tile_width and tile_height match the height of the bitmap by default.
 
 601         if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
 
 602             raise ValueError("Unsupported Bitmap type")
 
 603         self._bitmap = bitmap
 
 604         bitmap_width = bitmap.width
 
 605         bitmap_height = bitmap.height
 
 607         if not isinstance(pixel_shader, (ColorConverter, Palette)):
 
 608             raise ValueError("Unsupported Pixel Shader type")
 
 609         self._pixel_shader = pixel_shader
 
 613         self._width = width  # Number of Tiles Wide
 
 614         self._height = height  # Number of Tiles High
 
 615         self._transpose_xy = False
 
 618         if tile_width is None:
 
 619             tile_width = bitmap_width
 
 620         if tile_height is None:
 
 621             tile_height = bitmap_height
 
 622         if bitmap_width % tile_width != 0:
 
 623             raise ValueError("Tile width must exactly divide bitmap width")
 
 624         self._tile_width = tile_width
 
 625         if bitmap_height % tile_height != 0:
 
 626             raise ValueError("Tile height must exactly divide bitmap height")
 
 627         self._tile_height = tile_height
 
 628         if not 0 <= default_tile <= 255:
 
 629             raise ValueError("Default Tile is out of range")
 
 630         self._pixel_width = width * tile_width
 
 631         self._pixel_height = height * tile_height
 
 632         self._tiles = (self._width * self._height) * [default_tile]
 
 633         self.in_group = False
 
 634         self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
 
 635         self._current_area = Rectangle(0, 0, self._pixel_width, self._pixel_height)
 
 638     def update_transform(self, absolute_transform):
 
 639         """Update the parent transform and child transforms"""
 
 640         self._absolute_transform = absolute_transform
 
 641         if self._absolute_transform is not None:
 
 642             self._update_current_x()
 
 643             self._update_current_y()
 
 645     def _update_current_x(self):
 
 646         if self._transpose_xy:
 
 647             width = self._pixel_height
 
 649             width = self._pixel_width
 
 650         if self._absolute_transform.transpose_xy:
 
 651             self._current_area.y1 = (
 
 652                 self._absolute_transform.y + self._absolute_transform.dy * self._x
 
 654             self._current_area.y2 = (
 
 655                 self._absolute_transform.y
 
 656                 + self._absolute_transform.dy * (self._x + width)
 
 658             if self._current_area.y2 < self._current_area.y1:
 
 659                 self._current_area.y1, self._current_area.y2 = (
 
 660                     self._current_area.y2,
 
 661                     self._current_area.y1,
 
 664             self._current_area.x1 = (
 
 665                 self._absolute_transform.x + self._absolute_transform.dx * self._x
 
 667             self._current_area.x2 = (
 
 668                 self._absolute_transform.x
 
 669                 + self._absolute_transform.dx * (self._x + width)
 
 671             if self._current_area.x2 < self._current_area.x1:
 
 672                 self._current_area.x1, self._current_area.x2 = (
 
 673                     self._current_area.x2,
 
 674                     self._current_area.x1,
 
 677     def _update_current_y(self):
 
 678         if self._transpose_xy:
 
 679             height = self._pixel_width
 
 681             height = self._pixel_height
 
 682         if self._absolute_transform.transpose_xy:
 
 683             self._current_area.x1 = (
 
 684                 self._absolute_transform.x + self._absolute_transform.dx * self._y
 
 686             self._current_area.x2 = (
 
 687                 self._absolute_transform.x
 
 688                 + self._absolute_transform.dx * (self._y + height)
 
 690             if self._current_area.x2 < self._current_area.x1:
 
 691                 self._current_area.x1, self._current_area.x2 = (
 
 692                     self._current_area.x2,
 
 693                     self._current_area.x1,
 
 696             self._current_area.y1 = (
 
 697                 self._absolute_transform.y + self._absolute_transform.dy * self._y
 
 699             self._current_area.y2 = (
 
 700                 self._absolute_transform.y
 
 701                 + self._absolute_transform.dy * (self._y + height)
 
 703             if self._current_area.y2 < self._current_area.y1:
 
 704                 self._current_area.y1, self._current_area.y2 = (
 
 705                     self._current_area.y2,
 
 706                     self._current_area.y1,
 
 709     # pylint: disable=too-many-locals
 
 710     def _fill_area(self, buffer):
 
 711         """Draw onto the image"""
 
 717             (self._width * self._tile_width, self._height * self._tile_height),
 
 721         tile_count_x = self._bitmap.width // self._tile_width
 
 725         for tile_x in range(0, self._width):
 
 726             for tile_y in range(0, self._height):
 
 727                 tile_index = self._tiles[tile_y * self._width + tile_x]
 
 728                 tile_index_x = tile_index % tile_count_x
 
 729                 tile_index_y = tile_index // tile_count_x
 
 730                 for pixel_x in range(self._tile_width):
 
 731                     for pixel_y in range(self._tile_height):
 
 732                         image_x = tile_x * self._tile_width + pixel_x
 
 733                         image_y = tile_y * self._tile_height + pixel_y
 
 734                         bitmap_x = tile_index_x * self._tile_width + pixel_x
 
 735                         bitmap_y = tile_index_y * self._tile_height + pixel_y
 
 736                         pixel_color = self._pixel_shader[
 
 737                             self._bitmap[bitmap_x, bitmap_y]
 
 739                         if not pixel_color["transparent"]:
 
 740                             image.putpixel((image_x, image_y), pixel_color["rgb888"])
 
 741         if self._absolute_transform is not None:
 
 742             if self._absolute_transform.scale > 1:
 
 743                 image = image.resize(
 
 745                         self._pixel_width * self._absolute_transform.scale,
 
 746                         self._pixel_height * self._absolute_transform.scale,
 
 748                     resample=Image.NEAREST,
 
 750             if self._absolute_transform.mirror_x:
 
 751                 image = image.transpose(Image.FLIP_LEFT_RIGHT)
 
 752             if self._absolute_transform.mirror_y:
 
 753                 image = image.transpose(Image.FLIP_TOP_BOTTOM)
 
 754             if self._absolute_transform.transpose_xy:
 
 755                 image = image.transpose(Image.TRANSPOSE)
 
 756             x *= self._absolute_transform.dx
 
 757             y *= self._absolute_transform.dy
 
 758             x += self._absolute_transform.x
 
 759             y += self._absolute_transform.y
 
 760         buffer.alpha_composite(image, (x, y))
 
 762     # pylint: enable=too-many-locals
 
 766         """True when the TileGrid is hidden. This may be False even
 
 767         when a part of a hidden Group."""
 
 771     def hidden(self, value):
 
 772         if not isinstance(value, (bool, int)):
 
 773             raise ValueError("Expecting a boolean or integer value")
 
 774         self._hidden = bool(value)
 
 778         """X position of the left edge in the parent."""
 
 783         if not isinstance(value, int):
 
 784             raise TypeError("X should be a integer type")
 
 787             self._update_current_x()
 
 791         """Y position of the top edge in the parent."""
 
 796         if not isinstance(value, int):
 
 797             raise TypeError("Y should be a integer type")
 
 800             self._update_current_y()
 
 804         """If true, the left edge rendered will be the right edge of the right-most tile."""
 
 808     def flip_x(self, value):
 
 809         if not isinstance(value, bool):
 
 810             raise TypeError("Flip X should be a boolean type")
 
 811         if self._flip_x != value:
 
 816         """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
 
 820     def flip_y(self, value):
 
 821         if not isinstance(value, bool):
 
 822             raise TypeError("Flip Y should be a boolean type")
 
 823         if self._flip_y != value:
 
 827     def transpose_xy(self):
 
 828         """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90
 
 829         degree rotation can be achieved along with the corresponding mirrored version.
 
 831         return self._transpose_xy
 
 834     def transpose_xy(self, value):
 
 835         if not isinstance(value, bool):
 
 836             raise TypeError("Transpose XY should be a boolean type")
 
 837         if self._transpose_xy != value:
 
 838             self._transpose_xy = value
 
 839             self._update_current_x()
 
 840             self._update_current_y()
 
 843     def pixel_shader(self):
 
 844         """The pixel shader of the tilegrid."""
 
 847     def __getitem__(self, index):
 
 848         """Returns the tile index at the given index. The index can either be
 
 849         an x,y tuple or an int equal to ``y * width + x``'.
 
 851         if isinstance(index, (tuple, list)):
 
 854             index = y * self._width + x
 
 855         elif isinstance(index, int):
 
 856             x = index % self._width
 
 857             y = index // self._width
 
 858         if x > self._width or y > self._height or index >= len(self._tiles):
 
 859             raise ValueError("Tile index out of bounds")
 
 860         return self._tiles[index]
 
 862     def __setitem__(self, index, value):
 
 863         """Sets the tile index at the given index. The index can either be
 
 864         an x,y tuple or an int equal to ``y * width + x``.
 
 866         if isinstance(index, (tuple, list)):
 
 869             index = y * self._width + x
 
 870         elif isinstance(index, int):
 
 871             x = index % self._width
 
 872             y = index // self._width
 
 873         if x > self._width or y > self._height or index >= len(self._tiles):
 
 874             raise ValueError("Tile index out of bounds")
 
 875         if not 0 <= value <= 255:
 
 876             raise ValueError("Tile value out of bounds")
 
 877         self._tiles[index] = value
 
 880 # pylint: enable=too-many-instance-attributes