11 from collections import namedtuple
12 from PIL import Image, ImageDraw, ImagePalette
20 # Don't import pillow if we're running in the CI. We could mock it out but that
21 # would require mocking in all reverse dependencies.
22 if "GITHUB_ACTION" not in os.environ and "READTHEDOCS" not in os.environ:
23 # This will only work on Linux
26 # this would be for Github Actions
27 utils = None # pylint: disable=invalid-name
29 __version__ = "0.0.0-auto.0"
30 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
34 Rectangle = namedtuple("Rectangle", "x1 y1 x2 y2")
35 AbsoluteTransform = namedtuple("AbsoluteTransform", "scale transposexy")
38 def release_displays():
39 """Releases any actively used displays so their busses and pins can be used again.
41 Use this once in your code.py if you initialize a display. Place it right before the
42 initialization so the display is active as long as possible.
44 for _disp in _displays:
50 """Stores values of a certain size in a 2D array"""
52 def __init__(self, width, height, value_count):
53 """Create a Bitmap object with the given fixed size. Each pixel stores a value that is
54 used to index into a corresponding palette. This enables differently colored sprites to
55 share the underlying Bitmap. value_count is used to minimize the memory used to store
60 self._read_only = False
63 raise ValueError("value_count must be > 0")
66 while (value_count - 1) >> bits:
72 self._bits_per_value = bits
75 self._bits_per_value > 8
76 and self._bits_per_value != 16
77 and self._bits_per_value != 32
79 raise NotImplementedError("Invalid bits per value")
81 self._data = (width * height) * [0]
82 self._dirty_area = Rectangle(0, 0, width, height)
84 def __getitem__(self, index):
86 Returns the value at the given index. The index can either be
87 an x,y tuple or an int equal to `y * width + x`.
89 if isinstance(index, (tuple, list)):
90 index = (index[1] * self._width) + index[0]
91 if index >= len(self._data):
92 raise ValueError("Index {} is out of range".format(index))
93 return self._data[index]
95 def __setitem__(self, index, value):
97 Sets the value at the given index. The index can either be
98 an x,y tuple or an int equal to `y * width + x`.
101 raise RuntimeError("Read-only object")
102 if isinstance(index, (tuple, list)):
105 index = y * self._width + x
106 elif ininstance(index, int):
107 x = index % self._width
108 y = index // self._width
109 self._data[index] = value
110 if self._dirty_area.x1 == self._dirty_area.x2:
111 self._dirty_area.x1 = x
112 self._dirty_area.x2 = x + 1
113 self._dirty_area.y1 = y
114 self._dirty_area.y2 = y + 1
116 if x < self._dirty_area.x1:
117 self._dirty_area.x1 = x
118 elif x >= self._dirty_area.x2:
119 self._dirty_area.x2 = x + 1
120 if y < self._dirty_area.y1:
121 self._dirty_area.y1 = y
122 elif y >= self._dirty_area.y2:
123 self._dirty_area.y2 = y + 1
125 def _finish_refresh(self):
126 self._dirty_area.x1 = 0
127 self._dirty_area.x2 = 0
129 def fill(self, value):
130 """Fills the bitmap with the supplied palette index value."""
131 self._data = (self._width * self._height) * [value]
132 self._dirty_area = Rectangle(0, 0, self._width, self._height)
136 """Width of the bitmap. (read only)"""
141 """Height of the bitmap. (read only)"""
145 class ColorConverter:
146 """Converts one color format to another. Color converter based on original displayio
147 code for consistency.
150 def __init__(self, *, dither=False):
151 """Create a ColorConverter object to convert color formats.
152 Only supports RGB888 to RGB565 currently.
153 :param bool dither: Adds random noise to dither the output image
155 self._dither = dither
158 def _compute_rgb565(self, color):
160 return (color >> 19) << 11 | ((color >> 10) & 0x3F) << 5 | (color >> 3) & 0x1F
162 def _compute_luma(self, color):
164 g8 = (color >> 8) & 0xFF
166 return (r8 * 19) / 255 + (g8 * 182) / 255 + (b8 + 54) / 255
168 def _compute_chroma(self, color):
170 g8 = (color >> 8) & 0xFF
172 return max(r8, g8, b8) - min(r8, g8, b8)
174 def _compute_hue(self, color):
176 g8 = (color >> 8) & 0xFF
178 max_color = max(r8, g8, b8)
179 chroma = self._compute_chroma(color)
184 hue = (((g8 - b8) * 40) / chroma) % 240
185 elif max_color == g8:
186 hue = (((b8 - r8) + (2 * chroma)) * 40) / chroma
187 elif max_color == b8:
188 hue = (((r8 - g8) + (4 * chroma)) * 40) / chroma
194 def _dither_noise_1(self, noise):
196 nn = (n * (n * n * 60493 + 19990303) + 1376312589) & 0x7FFFFFFF
197 return (nn / (1073741824.0 * 2)) * 255
199 def _dither_noise_2(self, x, y):
200 return self._dither_noise_1(x + y * 0xFFFF)
202 def _compute_tricolor(self):
205 def convert(self, color):
206 "Converts the given RGB888 color to RGB565"
208 return color # To Do: return a dithered color
210 return self._compute_rgb565(color)
212 def _pil_palette(self):
217 "When true the color converter dithers the output by adding random noise when truncating to display bitdepth"
221 def dither(self, value):
222 if not isinstance(value, bool):
223 raise ValueError("Value should be boolean")
228 """This initializes a display and connects it into CircuitPython. Unlike other objects
229 in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
230 This is done so that CircuitPython can use the display itself.
232 Most people should not use this class directly. Use a specific display driver instead
233 that will contain the initialization sequence at minimum.
236 Display(display_bus, init_sequence, *, width, height, colstart=0, rowstart=0, rotation=0,
237 color_depth=16, grayscale=False, pixels_in_byte_share_row=True, bytes_per_cell=1,
238 reverse_pixels_in_byte=False, set_column_command=0x2a, set_row_command=0x2b,
239 write_ram_command=0x2c, set_vertical_scroll=0, backlight_pin=None, brightness_command=None,
240 brightness=1.0, auto_brightness=False, single_byte_bounds=False, data_as_commands=False,
241 auto_refresh=True, native_frames_per_second=60)
257 pixels_in_byte_share_row=True,
259 reverse_pixels_in_byte=False,
260 set_column_command=0x2A,
261 set_row_command=0x2B,
262 write_ram_command=0x2C,
263 set_vertical_scroll=0,
265 brightness_command=None,
267 auto_brightness=False,
268 single_byte_bounds=False,
269 data_as_commands=False,
271 native_frames_per_second=60
273 """Create a Display object on the given display bus (`displayio.FourWire` or `displayio.ParallelBus`).
275 The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins with a command byte
276 followed by a byte to determine the parameter count and if a delay is need after. When the top bit of the
277 second byte is 1, the next byte will be the delay time in milliseconds. The remaining 7 bits are the
278 parameter count excluding any delay byte. The third through final bytes are the remaining command
279 parameters. The next byte will begin a new command definition. Here is a portion of ILI9341 init code:
280 .. code-block:: python
282 init_sequence = (b"\xe1\x0f\x00\x0E\x14\x03\x11\x07\x31\xC1\x48\x08\x0F\x0C\x31\x36\x0F" # Set Gamma
283 b"\x11\x80\x78"# Exit Sleep then delay 0x78 (120ms)
284 b"\x29\x80\x78"# Display on then delay 0x78 (120ms)
286 display = displayio.Display(display_bus, init_sequence, width=320, height=240)
288 The first command is 0xe1 with 15 (0xf) parameters following. The second and third are 0x11 and 0x29
289 respectively with delays (0x80) of 120ms (0x78) and no parameters. Multiple byte literals (b”“) are
290 merged together on load. The parens are needed to allow byte literals on subsequent lines.
292 The initialization sequence should always leave the display memory access inline with the scan of
293 the display to minimize tearing artifacts.
295 self._bus = display_bus
296 self._set_column_command = set_column_command
297 self._set_row_command = set_row_command
298 self._write_ram_command = write_ram_command
299 self._brightness_command = brightness_command
300 self._data_as_commands = data_as_commands
301 self._single_byte_bounds = single_byte_bounds
303 self._height = height
304 self._colstart = colstart
305 self._rowstart = rowstart
306 self._rotation = rotation
307 self._auto_brightness = auto_brightness
308 self._brightness = brightness
309 self._auto_refresh = auto_refresh
310 self._initialize(init_sequence)
311 self._buffer = Image.new("RGB", (width, height))
312 self._subrectangles = []
313 self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
314 self._current_group = None
315 _displays.append(self)
316 self._refresh_thread = None
317 if self._auto_refresh:
318 self.auto_refresh = True
320 def _initialize(self, init_sequence):
322 while i < len(init_sequence):
323 command = init_sequence[i]
324 data_size = init_sequence[i + 1]
325 delay = (data_size & 0x80) > 0
327 data_byte = init_sequence[i + 2]
328 self._write(command, init_sequence[i + 2 : i + 2 + data_size])
332 delay_time_ms = init_sequence[i + 1 + data_size]
333 if delay_time_ms == 255:
335 time.sleep(delay_time_ms / 1000)
338 def _write(self, command, data):
339 if self._single_byte_bounds:
340 self._bus.send(True, bytes([command]) + data, toggle_every_byte=True)
342 self._bus.send(True, bytes([command]), toggle_every_byte=True)
343 self._bus.send(False, data)
349 def show(self, group):
350 """Switches to displaying the given group of layers. When group is None, the
351 default CircuitPython terminal will be shown.
353 self._current_group = group
355 def refresh(self, *, target_frames_per_second=60, minimum_frames_per_second=1):
356 """When auto refresh is off, waits for the target frame rate and then refreshes the display,
357 returning True. If the call has taken too long since the last refresh call for the given target
358 frame rate, then the refresh returns False immediately without updating the screen to hopefully
359 help getting caught up.
361 If the time since the last successful refresh is below the minimum frame rate, then an exception
362 will be raised. Set minimum_frames_per_second to 0 to disable.
364 When auto refresh is on, updates the display immediately. (The display will also update without
367 # Go through groups and and add each to buffer
368 if self._current_group is not None:
369 buffer = Image.new("RGB", (self._width, self._height))
370 # Recursively have everything draw to the image
371 self._current_group._fill_area(buffer)
372 # save image to buffer (or probably refresh buffer so we can compare)
373 self._buffer.paste(buffer)
376 # Eventually calculate dirty rectangles here
377 self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
379 for area in self._subrectangles:
380 self._refresh_display_area(area)
382 def _refresh_loop(self):
383 while self._auto_refresh:
386 def _refresh_display_area(self, rectangle):
387 """Loop through dirty rectangles and redraw that area."""
388 """Read or write a block of data."""
389 data = numpy.array(self._buffer.crop(rectangle).convert("RGB")).astype("uint16")
391 ((data[:, :, 0] & 0xF8) << 8)
392 | ((data[:, :, 1] & 0xFC) << 3)
393 | (data[:, :, 2] >> 3)
397 numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
401 self._set_column_command,
403 rectangle.x1 + self._colstart, rectangle.x2 + self._colstart
407 self._set_row_command,
409 rectangle.y1 + self._rowstart, rectangle.y2 + self._rowstart
412 self._write(self._write_ram_command, pixels)
414 def _encode_pos(self, x, y):
415 """Encode a postion into bytes."""
416 return struct.pack(self._bounds_encoding, x, y)
418 def fill_row(self, y, buffer):
422 def auto_refresh(self):
423 return self._auto_refresh
426 def auto_refresh(self, value):
427 self._auto_refresh = value
428 if self._refresh_thread is None:
429 self._refresh_thread = threading.Thread(
430 target=self._refresh_loop, daemon=True
432 if value and not self._refresh_thread.is_alive():
434 self._refresh_thread.start()
435 elif not value and self._refresh_thread.is_alive():
437 self._refresh_thread.join()
440 def brightness(self):
441 """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`. When
442 `auto_brightness` is True, the value of `brightness` will change automatically. If `brightness`
443 is set, `auto_brightness` will be disabled and will be set to False.
445 return self._brightness
448 def brightness(self, value):
449 self._brightness = value
452 def auto_brightness(self):
453 """True when the display brightness is adjusted automatically, based on an ambient light sensor
454 or other method. Note that some displays may have this set to True by default, but not actually
455 implement automatic brightness adjustment. `auto_brightness` is set to False if `brightness`
458 return self._auto_brightness
460 @auto_brightness.setter
461 def auto_brightness(self, value):
462 self._auto_brightness = value
474 """The rotation of the display as an int in degrees."""
475 return self._rotation
478 def rotation(self, value):
479 if value not in (0, 90, 180, 270):
480 raise ValueError("Rotation must be 0/90/180/270")
481 self._rotation = value
502 set_column_window_command=None,
503 set_row_window_command=None,
504 single_byte_bounds=False,
505 write_black_ram_command,
506 black_bits_inverted=False,
507 write_color_ram_command=None,
508 color_bits_inverted=False,
509 highlight_color=0x000000,
510 refresh_display_command,
514 seconds_per_frame=180,
515 always_toggle_chip_select=False
518 Create a EPaperDisplay object on the given display bus (displayio.FourWire or displayio.ParallelBus).
520 The start_sequence and stop_sequence are bitpacked to minimize the ram impact. Every command
521 begins with a command byte followed by a byte to determine the parameter count and if a delay
522 is need after. When the top bit of the second byte is 1, the next byte will be the delay time
523 in milliseconds. The remaining 7 bits are the parameter count excluding any delay byte. The
524 third through final bytes are the remaining command parameters. The next byte will begin a
525 new command definition.
529 def show(self, group):
530 """Switches to displaying the given group of layers. When group is None, the default CircuitPython
531 terminal will be shown.
536 """Refreshes the display immediately or raises an exception if too soon. Use
537 ``time.sleep(display.time_to_refresh)`` to sleep until a refresh can occur.
542 def time_to_refresh(self):
543 """Time, in fractional seconds, until the ePaper display can be refreshed."""
560 """Manage updating a display over SPI four wire protocol in the background while
561 Python code runs. It doesn’t handle display initialization.
575 """Create a FourWire object associated with the given pins.
577 The SPI bus and pins are then in use by the display until displayio.release_displays() is called
578 even after a reload. (It does this so CircuitPython can use the display after your code is done.)
579 So, the first time you initialize a display bus in code.py you should call
580 :py:func`displayio.release_displays` first, otherwise it will error after the first code.py run.
582 self._dc = digitalio.DigitalInOut(command)
583 self._dc.switch_to_output()
584 self._chip_select = digitalio.DigitalInOut(chip_select)
585 self._chip_select.switch_to_output(value=True)
587 if reset is not None:
588 self._reset = digitalio.DigitalInOut(reset)
589 self._reset.switch_to_output(value=True)
593 while self._spi.try_lock():
595 self._spi.configure(baudrate=baudrate, polarity=polarity, phase=phase)
602 self._chip_select.deinit()
603 if self._reset is not None:
607 if self._reset is not None:
608 self._reset.value = False
610 self._reset.value = True
613 def send(self, is_command, data, *, toggle_every_byte=False):
614 while self._spi.try_lock():
616 self._dc.value = not is_command
617 if toggle_every_byte:
619 self._spi.write(bytes([byte]))
620 self._chip_select.value = True
622 self._chip_select.value = False
624 self._spi.write(data)
629 """Manage a group of sprites and groups and how they are inter-related."""
631 def __init__(self, *, max_size=4, scale=1, x=0, y=0):
632 """Create a Group of a given size and scale. Scale is in
633 one dimension. For example, scale=2 leads to a layer’s
634 pixel being 2x2 pixels when in the group.
636 if not isinstance(max_size, int) or max_size < 1:
637 raise ValueError("Max Size must be an integer and >= 1")
638 self._max_size = max_size
639 if not isinstance(scale, int) or scale < 1:
640 raise ValueError("Scale must be an integer and >= 1")
646 self._supported_types = (TileGrid, Group)
648 def append(self, layer):
649 """Append a layer to the group. It will be drawn
652 if not isinstance(layer, self._supported_types):
653 raise ValueError("Invalid Group Memeber")
654 if len(self._layers) == self._max_size:
655 raise RuntimeError("Group full")
656 self._layers.append(layer)
658 def insert(self, index, layer):
659 """Insert a layer into the group."""
660 if not isinstance(layer, self._supported_types):
661 raise ValueError("Invalid Group Memeber")
662 if len(self._layers) == self._max_size:
663 raise RuntimeError("Group full")
664 self._layers.insert(index, layer)
666 def index(self, layer):
667 """Returns the index of the first copy of layer.
668 Raises ValueError if not found.
672 def pop(self, index=-1):
673 """Remove the ith item and return it."""
674 return self._layers.pop(index)
676 def remove(self, layer):
677 """Remove the first copy of layer. Raises ValueError
678 if it is not present."""
682 """Returns the number of layers in a Group"""
683 return len(self._layers)
685 def __getitem__(self, index):
686 """Returns the value at the given index."""
687 return self._layers[index]
689 def __setitem__(self, index, value):
690 """Sets the value at the given index."""
691 self._layers[index] = value
693 def __delitem__(self, index):
694 """Deletes the value at the given index."""
695 del self._layers[index]
697 def _fill_area(self, buffer):
701 for layer in self._layers:
702 if isinstance(layer, (Group, TileGrid)):
703 layer._fill_area(buffer)
710 def hidden(self, value):
711 if not isinstance(value, (bool, int)):
712 raise ValueError("Expecting a boolean or integer value")
713 self._hidden = bool(value)
720 def scale(self, value):
721 if not isinstance(value, int) or value < 1:
722 raise ValueError("Scale must be an integer and at least 1")
731 if not isinstance(value, int):
732 raise ValueError("x must be an integer")
741 if not isinstance(value, int):
742 raise ValueError("y must be an integer")
747 """Manage updating a display over I2C in the background while Python code runs.
748 It doesn’t handle display initialization.
751 def __init__(self, i2c_bus, *, device_address, reset=None):
752 """Create a I2CDisplay object associated with the given I2C bus and reset pin.
754 The I2C bus and pins are then in use by the display until displayio.release_displays() is
755 called even after a reload. (It does this so CircuitPython can use the display after your
756 code is done.) So, the first time you initialize a display bus in code.py you should call
757 :py:func`displayio.release_displays` first, otherwise it will error after the first
765 def send(self, command, data):
771 Loads values straight from disk. This minimizes memory use but can lead to much slower pixel load times.
772 These load times may result in frame tearing where only part of the image is visible."""
774 def __init__(self, file):
775 self._image = Image.open(file)
779 """Width of the bitmap. (read only)"""
780 return self._image.width
784 """Height of the bitmap. (read only)"""
785 return self._image.height
789 """Map a pixel palette_index to a full color. Colors are transformed to the display’s
790 format internally to save memory.
793 def __init__(self, color_count):
794 """Create a Palette object to store a set number of colors."""
795 self._needs_refresh = False
798 for _ in range(color_count):
799 self._colors.append(self._make_color(0))
801 def _make_color(self, value):
803 "transparent": False,
806 color_converter = ColorConverter()
807 if isinstance(value, (tuple, list, bytes, bytearray)):
808 value = (value[0] & 0xFF) << 16 | (value[1] & 0xFF) << 8 | value[2] & 0xFF
809 elif isinstance(value, int):
810 if not 0 <= value <= 0xFFFFFF:
811 raise ValueError("Color must be between 0x000000 and 0xFFFFFF")
813 raise TypeError("Color buffer must be a buffer, tuple, list, or int")
814 color["rgb888"] = value
815 self._needs_refresh = True
820 """Returns the number of colors in a Palette"""
821 return len(self._colors)
823 def __setitem__(self, index, value):
824 """Sets the pixel color at the given index. The index should be
825 an integer in the range 0 to color_count-1.
827 The value argument represents a color, and can be from 0x000000 to 0xFFFFFF
828 (to represent an RGB value). Value can be an int, bytes (3 bytes (RGB) or
829 4 bytes (RGB + pad byte)), bytearray, or a tuple or list of 3 integers.
831 if self._colors[index]["rgb888"] != value:
832 self._colors[index] = self._make_color(value)
834 def __getitem__(self, index):
835 if not 0 <= index < len(self._colors):
836 raise ValueError("Palette index out of range")
837 return self._colors[index]
839 def make_transparent(self, palette_index):
840 self._colors[palette_index]["transparent"] = True
842 def make_opaque(self, palette_index):
843 self._colors[palette_index]["transparent"] = False
847 """Manage updating a display over 8-bit parallel bus in the background while Python code runs.
848 This protocol may be refered to as 8080-I Series Parallel Interface in datasheets.
849 It doesn’t handle display initialization.
852 def __init__(self, i2c_bus, *, device_address, reset=None):
853 """Create a ParallelBus object associated with the given pins. The
854 bus is inferred from data0 by implying the next 7 additional pins on a given GPIO port.
856 The parallel bus and pins are then in use by the display until displayio.release_displays()
857 is called even after a reload. (It does this so CircuitPython can use the display after your
858 code is done.) So, the first time you initialize a display bus in code.py you should call
859 :py:func`displayio.release_displays` first, otherwise it will error after the first
865 """Performs a hardware reset via the reset pin. Raises an exception if called when
866 no reset pin is available.
870 def send(self, command, data):
871 """Sends the given command value followed by the full set of data. Display state, such as
872 vertical scroll, set via ``send`` may or may not be reset once the code is done.
878 """Create a Shape object with the given fixed size. Each pixel is one bit and is stored by the column
879 boundaries of the shape on each row. Each row’s boundary defaults to the full row.
882 def __init__(self, width, height, *, mirror_x=False, mirror_y=False):
883 """Create a Shape object with the given fixed size. Each pixel is one bit and is stored by the
884 column boundaries of the shape on each row. Each row’s boundary defaults to the full row.
888 def set_boundary(self, y, start_x, end_x):
889 """Loads pre-packed data into the given row."""
894 """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
895 grids can share bitmaps and pixel shaders.
897 A single tile grid is also known as a Sprite.
913 """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is used to convert
914 the value and its location to a display native pixel color. This may be a simple color palette lookup,
915 a gradient, a pattern or a color transformer.
917 tile_width and tile_height match the height of the bitmap by default.
919 if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
920 raise ValueError("Unsupported Bitmap type")
921 self._bitmap = bitmap
922 bitmap_width = bitmap.width
923 bitmap_height = bitmap.height
925 if not isinstance(pixel_shader, (ColorConverter, Palette)):
926 raise ValueError("Unsupported Pixel Shader type")
927 self._pixel_shader = pixel_shader
931 self._width = width # Number of Tiles Wide
932 self._height = height # Number of Tiles High
933 if tile_width is None:
934 tile_width = bitmap_width
935 if tile_height is None:
936 tile_height = bitmap_height
937 if bitmap_width % tile_width != 0:
938 raise ValueError("Tile width must exactly divide bitmap width")
939 self._tile_width = tile_width
940 if bitmap_height % tile_height != 0:
941 raise ValueError("Tile height must exactly divide bitmap height")
942 self._tile_height = tile_height
943 if not 0 <= default_tile <= 255:
944 raise ValueError("Default Tile is out of range")
945 self._tiles = (self._width * self._height) * [default_tile]
947 def _fill_area(self, buffer):
948 """Draw onto the image"""
953 "RGB", (self._width * self._tile_width, self._height * self._tile_height)
956 tile_count_x = self._bitmap.width // self._tile_width
957 tile_count_y = self._bitmap.height // self._tile_height
959 for tile_x in range(0, self._width):
960 for tile_y in range(0, self._height):
961 tile_index = self._tiles[tile_y * self._width + tile_x]
962 tile_index_x = tile_index % tile_count_x
963 tile_index_y = tile_index // tile_count_x
964 for pixel_x in range(self._tile_width):
965 for pixel_y in range(self._tile_height):
966 image_x = tile_x * self._tile_width + pixel_x
967 image_y = tile_y * self._tile_height + pixel_y
968 bitmap_x = tile_index_x * self._tile_width + pixel_x
969 bitmap_y = tile_index_y * self._tile_height + pixel_y
970 pixel_color = self._pixel_shader[
971 self._bitmap[bitmap_x, bitmap_y]
973 if not pixel_color["transparent"]:
974 image.putpixel((image_x, image_y), pixel_color["rgb888"])
976 # Apply transforms or mirrors or whatever here
977 if self._tile_width == 6:
978 print("Putting at {}".format((self._x, self._y)))
979 buffer.paste(image, (self._x, self._y))
983 """True when the TileGrid is hidden. This may be False even when a part of a hidden Group."""
987 def hidden(self, value):
988 if not isinstance(value, (bool, int)):
989 raise ValueError("Expecting a boolean or integer value")
990 self._hidden = bool(value)
994 """X position of the left edge in the parent."""
999 """Y position of the top edge in the parent."""
1004 """If true, the left edge rendered will be the right edge of the right-most tile."""
1008 def flip_x(self, value):
1009 if not isinstance(value, bool):
1010 raise TypeError("Flip X should be a boolean type")
1011 self._flip_x = value
1015 """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
1019 def flip_y(self, value):
1020 if not isinstance(value, bool):
1021 raise TypeError("Flip Y should be a boolean type")
1022 self._flip_y = value
1025 def transpose_xy(self):
1026 """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90 degree
1027 rotation can be achieved along with the corresponding mirrored version.
1029 return self._transpose_xy
1031 @transpose_xy.setter
1032 def transpose_xy(self, value):
1033 if not isinstance(value, bool):
1034 raise TypeError("Transpose XY should be a boolean type")
1035 self._transpose_xy = value
1038 def pixel_shader(self):
1039 """The pixel shader of the tilegrid."""
1042 def __getitem__(self, index):
1043 """Returns the tile index at the given index. The index can either be
1044 an x,y tuple or an int equal to ``y * width + x``'.
1046 if isinstance(index, (tuple, list)):
1049 index = y * self._width + x
1050 elif ininstance(index, int):
1051 x = index % self._width
1052 y = index // self._width
1053 if x > self._width or y > self._height:
1054 raise ValueError("Tile index out of bounds")
1055 return self._tiles[index]
1057 def __setitem__(self, index, value):
1058 """Sets the tile index at the given index. The index can either be
1059 an x,y tuple or an int equal to ``y * width + x``.
1061 if isinstance(index, (tuple, list)):
1064 index = y * self._width + x
1065 elif ininstance(index, int):
1066 x = index % self._width
1067 y = index // self._width
1068 if x > width or y > self._height or index >= len(self._tiles):
1069 raise ValueError("Tile index out of bounds")
1070 if not 0 <= value <= 255:
1071 raise ValueError("Tile value out of bounds")
1072 self._tiles[index] = value