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
44 from collections import namedtuple
45 from PIL import Image, ImageDraw, ImagePalette
47 __version__ = "0.0.0-auto.0"
48 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
52 Rectangle = namedtuple("Rectangle", "x1 y1 x2 y2")
53 AbsoluteTransform = namedtuple("AbsoluteTransform", "scale transposexy")
56 def release_displays():
57 """Releases any actively used displays so their busses and pins can be used again.
59 Use this once in your code.py if you initialize a display. Place it right before the
60 initialization so the display is active as long as possible.
62 for _disp in _displays:
68 """Stores values of a certain size in a 2D array"""
70 def __init__(self, width, height, value_count):
71 """Create a Bitmap object with the given fixed size. Each pixel stores a value that is
72 used to index into a corresponding palette. This enables differently colored sprites to
73 share the underlying Bitmap. value_count is used to minimize the memory used to store
78 self._read_only = False
81 raise ValueError("value_count must be > 0")
84 while (value_count - 1) >> bits:
90 self._bits_per_value = bits
93 self._bits_per_value > 8
94 and self._bits_per_value != 16
95 and self._bits_per_value != 32
97 raise NotImplementedError("Invalid bits per value")
99 self._data = (width * height) * [0]
100 self._dirty_area = Rectangle(0, 0, width, height)
102 def __getitem__(self, index):
104 Returns the value at the given index. The index can either be
105 an x,y tuple or an int equal to `y * width + x`.
107 if isinstance(index, (tuple, list)):
108 index = (index[1] * self._width) + index[0]
109 if index >= len(self._data):
110 raise ValueError("Index {} is out of range".format(index))
111 return self._data[index]
113 def __setitem__(self, index, value):
115 Sets the value at the given index. The index can either be
116 an x,y tuple or an int equal to `y * width + x`.
119 raise RuntimeError("Read-only object")
120 if isinstance(index, (tuple, list)):
123 index = y * self._width + x
124 elif ininstance(index, int):
125 x = index % self._width
126 y = index // self._width
127 self._data[index] = value
128 if self._dirty_area.x1 == self._dirty_area.x2:
129 self._dirty_area.x1 = x
130 self._dirty_area.x2 = x + 1
131 self._dirty_area.y1 = y
132 self._dirty_area.y2 = y + 1
134 if x < self._dirty_area.x1:
135 self._dirty_area.x1 = x
136 elif x >= self._dirty_area.x2:
137 self._dirty_area.x2 = x + 1
138 if y < self._dirty_area.y1:
139 self._dirty_area.y1 = y
140 elif y >= self._dirty_area.y2:
141 self._dirty_area.y2 = y + 1
143 def _finish_refresh(self):
144 self._dirty_area.x1 = 0
145 self._dirty_area.x2 = 0
147 def fill(self, value):
148 """Fills the bitmap with the supplied palette index value."""
149 self._data = (self._width * self._height) * [value]
150 self._dirty_area = Rectangle(0, 0, self._width, self._height)
154 """Width of the bitmap. (read only)"""
159 """Height of the bitmap. (read only)"""
163 class ColorConverter:
164 """Converts one color format to another. Color converter based on original displayio
165 code for consistency.
168 def __init__(self, *, dither=False):
169 """Create a ColorConverter object to convert color formats.
170 Only supports RGB888 to RGB565 currently.
171 :param bool dither: Adds random noise to dither the output image
173 self._dither = dither
176 def _compute_rgb565(self, color):
178 return (color >> 19) << 11 | ((color >> 10) & 0x3F) << 5 | (color >> 3) & 0x1F
180 def _compute_luma(self, color):
182 g8 = (color >> 8) & 0xFF
184 return (r8 * 19) / 255 + (g8 * 182) / 255 + (b8 + 54) / 255
186 def _compute_chroma(self, color):
188 g8 = (color >> 8) & 0xFF
190 return max(r8, g8, b8) - min(r8, g8, b8)
192 def _compute_hue(self, color):
194 g8 = (color >> 8) & 0xFF
196 max_color = max(r8, g8, b8)
197 chroma = self._compute_chroma(color)
202 hue = (((g8 - b8) * 40) / chroma) % 240
203 elif max_color == g8:
204 hue = (((b8 - r8) + (2 * chroma)) * 40) / chroma
205 elif max_color == b8:
206 hue = (((r8 - g8) + (4 * chroma)) * 40) / chroma
212 def _dither_noise_1(self, noise):
214 nn = (n * (n * n * 60493 + 19990303) + 1376312589) & 0x7FFFFFFF
215 return (nn / (1073741824.0 * 2)) * 255
217 def _dither_noise_2(self, x, y):
218 return self._dither_noise_1(x + y * 0xFFFF)
220 def _compute_tricolor(self):
223 def convert(self, color):
224 "Converts the given RGB888 color to RGB565"
226 return color # To Do: return a dithered color
228 return self._compute_rgb565(color)
230 def _pil_palette(self):
235 "When true the color converter dithers the output by adding random noise when truncating to display bitdepth"
239 def dither(self, value):
240 if not isinstance(value, bool):
241 raise ValueError("Value should be boolean")
246 """This initializes a display and connects it into CircuitPython. Unlike other objects
247 in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
248 This is done so that CircuitPython can use the display itself.
250 Most people should not use this class directly. Use a specific display driver instead
251 that will contain the initialization sequence at minimum.
254 Display(display_bus, init_sequence, *, width, height, colstart=0, rowstart=0, rotation=0,
255 color_depth=16, grayscale=False, pixels_in_byte_share_row=True, bytes_per_cell=1,
256 reverse_pixels_in_byte=False, set_column_command=0x2a, set_row_command=0x2b,
257 write_ram_command=0x2c, set_vertical_scroll=0, backlight_pin=None, brightness_command=None,
258 brightness=1.0, auto_brightness=False, single_byte_bounds=False, data_as_commands=False,
259 auto_refresh=True, native_frames_per_second=60)
275 pixels_in_byte_share_row=True,
277 reverse_pixels_in_byte=False,
278 set_column_command=0x2A,
279 set_row_command=0x2B,
280 write_ram_command=0x2C,
281 set_vertical_scroll=0,
283 brightness_command=None,
285 auto_brightness=False,
286 single_byte_bounds=False,
287 data_as_commands=False,
289 native_frames_per_second=60
291 """Create a Display object on the given display bus (`displayio.FourWire` or `displayio.ParallelBus`).
293 The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins with a command byte
294 followed by a byte to determine the parameter count and if a delay is need after. When the top bit of the
295 second byte is 1, the next byte will be the delay time in milliseconds. The remaining 7 bits are the
296 parameter count excluding any delay byte. The third through final bytes are the remaining command
297 parameters. The next byte will begin a new command definition. Here is a portion of ILI9341 init code:
298 .. code-block:: python
300 init_sequence = (b"\xe1\x0f\x00\x0E\x14\x03\x11\x07\x31\xC1\x48\x08\x0F\x0C\x31\x36\x0F" # Set Gamma
301 b"\x11\x80\x78"# Exit Sleep then delay 0x78 (120ms)
302 b"\x29\x80\x78"# Display on then delay 0x78 (120ms)
304 display = displayio.Display(display_bus, init_sequence, width=320, height=240)
306 The first command is 0xe1 with 15 (0xf) parameters following. The second and third are 0x11 and 0x29
307 respectively with delays (0x80) of 120ms (0x78) and no parameters. Multiple byte literals (b”“) are
308 merged together on load. The parens are needed to allow byte literals on subsequent lines.
310 The initialization sequence should always leave the display memory access inline with the scan of
311 the display to minimize tearing artifacts.
313 self._bus = display_bus
314 self._set_column_command = set_column_command
315 self._set_row_command = set_row_command
316 self._write_ram_command = write_ram_command
317 self._brightness_command = brightness_command
318 self._data_as_commands = data_as_commands
319 self._single_byte_bounds = single_byte_bounds
321 self._height = height
322 self._colstart = colstart
323 self._rowstart = rowstart
324 self._rotation = rotation
325 self._auto_brightness = auto_brightness
326 self._brightness = brightness
327 self._auto_refresh = auto_refresh
328 self._initialize(init_sequence)
329 self._buffer = Image.new("RGB", (width, height))
330 self._subrectangles = []
331 self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
332 self._current_group = None
333 _displays.append(self)
334 self._refresh_thread = None
335 if self._auto_refresh:
336 self.auto_refresh = True
338 def _initialize(self, init_sequence):
340 while i < len(init_sequence):
341 command = init_sequence[i]
342 data_size = init_sequence[i + 1]
343 delay = (data_size & 0x80) > 0
345 data_byte = init_sequence[i + 2]
346 self._write(command, init_sequence[i + 2 : i + 2 + data_size])
350 delay_time_ms = init_sequence[i + 1 + data_size]
351 if delay_time_ms == 255:
353 time.sleep(delay_time_ms / 1000)
356 def _write(self, command, data):
357 if self._single_byte_bounds:
358 self._bus.send(True, bytes([command]) + data, toggle_every_byte=True)
360 self._bus.send(True, bytes([command]), toggle_every_byte=True)
361 self._bus.send(False, data)
367 def show(self, group):
368 """Switches to displaying the given group of layers. When group is None, the
369 default CircuitPython terminal will be shown.
371 self._current_group = group
373 def refresh(self, *, target_frames_per_second=60, minimum_frames_per_second=1):
374 """When auto refresh is off, waits for the target frame rate and then refreshes the display,
375 returning True. If the call has taken too long since the last refresh call for the given target
376 frame rate, then the refresh returns False immediately without updating the screen to hopefully
377 help getting caught up.
379 If the time since the last successful refresh is below the minimum frame rate, then an exception
380 will be raised. Set minimum_frames_per_second to 0 to disable.
382 When auto refresh is on, updates the display immediately. (The display will also update without
385 # Go through groups and and add each to buffer
386 if self._current_group is not None:
387 buffer = Image.new("RGB", (self._width, self._height))
388 # Recursively have everything draw to the image
389 self._current_group._fill_area(buffer)
390 # save image to buffer (or probably refresh buffer so we can compare)
391 self._buffer.paste(buffer)
394 # Eventually calculate dirty rectangles here
395 self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
397 for area in self._subrectangles:
398 self._refresh_display_area(area)
400 def _refresh_loop(self):
401 while self._auto_refresh:
404 def _refresh_display_area(self, rectangle):
405 """Loop through dirty rectangles and redraw that area."""
406 """Read or write a block of data."""
407 data = numpy.array(self._buffer.crop(rectangle).convert("RGB")).astype("uint16")
409 ((data[:, :, 0] & 0xF8) << 8)
410 | ((data[:, :, 1] & 0xFC) << 3)
411 | (data[:, :, 2] >> 3)
415 numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
419 self._set_column_command,
421 rectangle.x1 + self._colstart, rectangle.x2 + self._colstart
425 self._set_row_command,
427 rectangle.y1 + self._rowstart, rectangle.y2 + self._rowstart
430 self._write(self._write_ram_command, pixels)
432 def _encode_pos(self, x, y):
433 """Encode a postion into bytes."""
434 return struct.pack(self._bounds_encoding, x, y)
436 def fill_row(self, y, buffer):
440 def auto_refresh(self):
441 return self._auto_refresh
444 def auto_refresh(self, value):
445 self._auto_refresh = value
446 if self._refresh_thread is None:
447 self._refresh_thread = threading.Thread(
448 target=self._refresh_loop, daemon=True
450 if value and not self._refresh_thread.is_alive():
452 self._refresh_thread.start()
453 elif not value and self._refresh_thread.is_alive():
455 self._refresh_thread.join()
458 def brightness(self):
459 """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`. When
460 `auto_brightness` is True, the value of `brightness` will change automatically. If `brightness`
461 is set, `auto_brightness` will be disabled and will be set to False.
463 return self._brightness
466 def brightness(self, value):
467 self._brightness = value
470 def auto_brightness(self):
471 """True when the display brightness is adjusted automatically, based on an ambient light sensor
472 or other method. Note that some displays may have this set to True by default, but not actually
473 implement automatic brightness adjustment. `auto_brightness` is set to False if `brightness`
476 return self._auto_brightness
478 @auto_brightness.setter
479 def auto_brightness(self, value):
480 self._auto_brightness = value
492 """The rotation of the display as an int in degrees."""
493 return self._rotation
496 def rotation(self, value):
497 if value not in (0, 90, 180, 270):
498 raise ValueError("Rotation must be 0/90/180/270")
499 self._rotation = value
520 set_column_window_command=None,
521 set_row_window_command=None,
522 single_byte_bounds=False,
523 write_black_ram_command,
524 black_bits_inverted=False,
525 write_color_ram_command=None,
526 color_bits_inverted=False,
527 highlight_color=0x000000,
528 refresh_display_command,
532 seconds_per_frame=180,
533 always_toggle_chip_select=False
536 Create a EPaperDisplay object on the given display bus (displayio.FourWire or displayio.ParallelBus).
538 The start_sequence and stop_sequence are bitpacked to minimize the ram impact. Every command
539 begins with a command byte followed by a byte to determine the parameter count and if a delay
540 is need after. When the top bit of the second byte is 1, the next byte will be the delay time
541 in milliseconds. The remaining 7 bits are the parameter count excluding any delay byte. The
542 third through final bytes are the remaining command parameters. The next byte will begin a
543 new command definition.
547 def show(self, group):
548 """Switches to displaying the given group of layers. When group is None, the default CircuitPython
549 terminal will be shown.
554 """Refreshes the display immediately or raises an exception if too soon. Use
555 ``time.sleep(display.time_to_refresh)`` to sleep until a refresh can occur.
560 def time_to_refresh(self):
561 """Time, in fractional seconds, until the ePaper display can be refreshed."""
578 """Manage updating a display over SPI four wire protocol in the background while
579 Python code runs. It doesn’t handle display initialization.
593 """Create a FourWire object associated with the given pins.
595 The SPI bus and pins are then in use by the display until displayio.release_displays() is called
596 even after a reload. (It does this so CircuitPython can use the display after your code is done.)
597 So, the first time you initialize a display bus in code.py you should call
598 :py:func`displayio.release_displays` first, otherwise it will error after the first code.py run.
600 self._dc = digitalio.DigitalInOut(command)
601 self._dc.switch_to_output()
602 self._chip_select = digitalio.DigitalInOut(chip_select)
603 self._chip_select.switch_to_output(value=True)
605 if reset is not None:
606 self._reset = digitalio.DigitalInOut(reset)
607 self._reset.switch_to_output(value=True)
611 while self._spi.try_lock():
613 self._spi.configure(baudrate=baudrate, polarity=polarity, phase=phase)
620 self._chip_select.deinit()
621 if self._reset is not None:
625 if self._reset is not None:
626 self._reset.value = False
628 self._reset.value = True
631 def send(self, is_command, data, *, toggle_every_byte=False):
632 while self._spi.try_lock():
634 self._dc.value = not is_command
635 if toggle_every_byte:
637 self._spi.write(bytes([byte]))
638 self._chip_select.value = True
640 self._chip_select.value = False
642 self._spi.write(data)
647 """Manage a group of sprites and groups and how they are inter-related."""
649 def __init__(self, *, max_size=4, scale=1, x=0, y=0):
650 """Create a Group of a given size and scale. Scale is in
651 one dimension. For example, scale=2 leads to a layer’s
652 pixel being 2x2 pixels when in the group.
654 if not isinstance(max_size, int) or max_size < 1:
655 raise ValueError("Max Size must be an integer and >= 1")
656 self._max_size = max_size
657 if not isinstance(scale, int) or scale < 1:
658 raise ValueError("Scale must be an integer and >= 1")
664 self._supported_types = (TileGrid, Group)
666 def append(self, layer):
667 """Append a layer to the group. It will be drawn
670 if not isinstance(layer, self._supported_types):
671 raise ValueError("Invalid Group Memeber")
672 if len(self._layers) == self._max_size:
673 raise RuntimeError("Group full")
674 self._layers.append(layer)
676 def insert(self, index, layer):
677 """Insert a layer into the group."""
678 if not isinstance(layer, self._supported_types):
679 raise ValueError("Invalid Group Memeber")
680 if len(self._layers) == self._max_size:
681 raise RuntimeError("Group full")
682 self._layers.insert(index, layer)
684 def index(self, layer):
685 """Returns the index of the first copy of layer.
686 Raises ValueError if not found.
690 def pop(self, index=-1):
691 """Remove the ith item and return it."""
692 return self._layers.pop(index)
694 def remove(self, layer):
695 """Remove the first copy of layer. Raises ValueError
696 if it is not present."""
700 """Returns the number of layers in a Group"""
701 return len(self._layers)
703 def __getitem__(self, index):
704 """Returns the value at the given index."""
705 return self._layers[index]
707 def __setitem__(self, index, value):
708 """Sets the value at the given index."""
709 self._layers[index] = value
711 def __delitem__(self, index):
712 """Deletes the value at the given index."""
713 del self._layers[index]
715 def _fill_area(self, buffer):
719 for layer in self._layers:
720 if isinstance(layer, (Group, TileGrid)):
721 layer._fill_area(buffer)
728 def hidden(self, value):
729 if not isinstance(value, (bool, int)):
730 raise ValueError("Expecting a boolean or integer value")
731 self._hidden = bool(value)
738 def scale(self, value):
739 if not isinstance(value, int) or value < 1:
740 raise ValueError("Scale must be an integer and at least 1")
749 if not isinstance(value, int):
750 raise ValueError("x must be an integer")
759 if not isinstance(value, int):
760 raise ValueError("y must be an integer")
765 """Manage updating a display over I2C in the background while Python code runs.
766 It doesn’t handle display initialization.
769 def __init__(self, i2c_bus, *, device_address, reset=None):
770 """Create a I2CDisplay object associated with the given I2C bus and reset pin.
772 The I2C bus and pins are then in use by the display until displayio.release_displays() is
773 called even after a reload. (It does this so CircuitPython can use the display after your
774 code is done.) So, the first time you initialize a display bus in code.py you should call
775 :py:func`displayio.release_displays` first, otherwise it will error after the first
783 def send(self, command, data):
789 Loads values straight from disk. This minimizes memory use but can lead to much slower pixel load times.
790 These load times may result in frame tearing where only part of the image is visible."""
792 def __init__(self, file):
793 self._image = Image.open(file)
797 """Width of the bitmap. (read only)"""
798 return self._image.width
802 """Height of the bitmap. (read only)"""
803 return self._image.height
807 """Map a pixel palette_index to a full color. Colors are transformed to the display’s
808 format internally to save memory.
811 def __init__(self, color_count):
812 """Create a Palette object to store a set number of colors."""
813 self._needs_refresh = False
816 for _ in range(color_count):
817 self._colors.append(self._make_color(0))
819 def _make_color(self, value):
821 "transparent": False,
824 color_converter = ColorConverter()
825 if isinstance(value, (tuple, list, bytes, bytearray)):
826 value = (value[0] & 0xFF) << 16 | (value[1] & 0xFF) << 8 | value[2] & 0xFF
827 elif isinstance(value, int):
828 if not 0 <= value <= 0xFFFFFF:
829 raise ValueError("Color must be between 0x000000 and 0xFFFFFF")
831 raise TypeError("Color buffer must be a buffer, tuple, list, or int")
832 color["rgb888"] = value
833 self._needs_refresh = True
838 """Returns the number of colors in a Palette"""
839 return len(self._colors)
841 def __setitem__(self, index, value):
842 """Sets the pixel color at the given index. The index should be
843 an integer in the range 0 to color_count-1.
845 The value argument represents a color, and can be from 0x000000 to 0xFFFFFF
846 (to represent an RGB value). Value can be an int, bytes (3 bytes (RGB) or
847 4 bytes (RGB + pad byte)), bytearray, or a tuple or list of 3 integers.
849 if self._colors[index]["rgb888"] != value:
850 self._colors[index] = self._make_color(value)
852 def __getitem__(self, index):
853 if not 0 <= index < len(self._colors):
854 raise ValueError("Palette index out of range")
855 return self._colors[index]
857 def make_transparent(self, palette_index):
858 self._colors[palette_index]["transparent"] = True
860 def make_opaque(self, palette_index):
861 self._colors[palette_index]["transparent"] = False
865 """Manage updating a display over 8-bit parallel bus in the background while Python code runs.
866 This protocol may be refered to as 8080-I Series Parallel Interface in datasheets.
867 It doesn’t handle display initialization.
870 def __init__(self, i2c_bus, *, device_address, reset=None):
871 """Create a ParallelBus object associated with the given pins. The
872 bus is inferred from data0 by implying the next 7 additional pins on a given GPIO port.
874 The parallel bus and pins are then in use by the display until displayio.release_displays()
875 is called even after a reload. (It does this so CircuitPython can use the display after your
876 code is done.) So, the first time you initialize a display bus in code.py you should call
877 :py:func`displayio.release_displays` first, otherwise it will error after the first
883 """Performs a hardware reset via the reset pin. Raises an exception if called when
884 no reset pin is available.
888 def send(self, command, data):
889 """Sends the given command value followed by the full set of data. Display state, such as
890 vertical scroll, set via ``send`` may or may not be reset once the code is done.
896 """Create a Shape object with the given fixed size. Each pixel is one bit and is stored by the column
897 boundaries of the shape on each row. Each row’s boundary defaults to the full row.
900 def __init__(self, width, height, *, mirror_x=False, mirror_y=False):
901 """Create a Shape object with the given fixed size. Each pixel is one bit and is stored by the
902 column boundaries of the shape on each row. Each row’s boundary defaults to the full row.
906 def set_boundary(self, y, start_x, end_x):
907 """Loads pre-packed data into the given row."""
912 """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
913 grids can share bitmaps and pixel shaders.
915 A single tile grid is also known as a Sprite.
931 """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is used to convert
932 the value and its location to a display native pixel color. This may be a simple color palette lookup,
933 a gradient, a pattern or a color transformer.
935 tile_width and tile_height match the height of the bitmap by default.
937 if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
938 raise ValueError("Unsupported Bitmap type")
939 self._bitmap = bitmap
940 bitmap_width = bitmap.width
941 bitmap_height = bitmap.height
943 if not isinstance(pixel_shader, (ColorConverter, Palette)):
944 raise ValueError("Unsupported Pixel Shader type")
945 self._pixel_shader = pixel_shader
949 self._width = width # Number of Tiles Wide
950 self._height = height # Number of Tiles High
951 if tile_width is None:
952 tile_width = bitmap_width
953 if tile_height is None:
954 tile_height = bitmap_height
955 if bitmap_width % tile_width != 0:
956 raise ValueError("Tile width must exactly divide bitmap width")
957 self._tile_width = tile_width
958 if bitmap_height % tile_height != 0:
959 raise ValueError("Tile height must exactly divide bitmap height")
960 self._tile_height = tile_height
961 if not 0 <= default_tile <= 255:
962 raise ValueError("Default Tile is out of range")
963 self._tiles = (self._width * self._height) * [default_tile]
965 def _fill_area(self, buffer):
966 """Draw onto the image"""
971 "RGB", (self._width * self._tile_width, self._height * self._tile_height)
974 tile_count_x = self._bitmap.width // self._tile_width
975 tile_count_y = self._bitmap.height // self._tile_height
977 for tile_x in range(0, self._width):
978 for tile_y in range(0, self._height):
979 tile_index = self._tiles[tile_y * self._width + tile_x]
980 tile_index_x = tile_index % tile_count_x
981 tile_index_y = tile_index // tile_count_x
982 for pixel_x in range(self._tile_width):
983 for pixel_y in range(self._tile_height):
984 image_x = tile_x * self._tile_width + pixel_x
985 image_y = tile_y * self._tile_height + pixel_y
986 bitmap_x = tile_index_x * self._tile_width + pixel_x
987 bitmap_y = tile_index_y * self._tile_height + pixel_y
988 pixel_color = self._pixel_shader[
989 self._bitmap[bitmap_x, bitmap_y]
991 if not pixel_color["transparent"]:
992 image.putpixel((image_x, image_y), pixel_color["rgb888"])
994 # Apply transforms or mirrors or whatever here
995 if self._tile_width == 6:
996 print("Putting at {}".format((self._x, self._y)))
997 buffer.paste(image, (self._x, self._y))
1001 """True when the TileGrid is hidden. This may be False even when a part of a hidden Group."""
1005 def hidden(self, value):
1006 if not isinstance(value, (bool, int)):
1007 raise ValueError("Expecting a boolean or integer value")
1008 self._hidden = bool(value)
1012 """X position of the left edge in the parent."""
1017 """Y position of the top edge in the parent."""
1022 """If true, the left edge rendered will be the right edge of the right-most tile."""
1026 def flip_x(self, value):
1027 if not isinstance(value, bool):
1028 raise TypeError("Flip X should be a boolean type")
1029 self._flip_x = value
1033 """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
1037 def flip_y(self, value):
1038 if not isinstance(value, bool):
1039 raise TypeError("Flip Y should be a boolean type")
1040 self._flip_y = value
1043 def transpose_xy(self):
1044 """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90 degree
1045 rotation can be achieved along with the corresponding mirrored version.
1047 return self._transpose_xy
1049 @transpose_xy.setter
1050 def transpose_xy(self, value):
1051 if not isinstance(value, bool):
1052 raise TypeError("Transpose XY should be a boolean type")
1053 self._transpose_xy = value
1056 def pixel_shader(self):
1057 """The pixel shader of the tilegrid."""
1060 def __getitem__(self, index):
1061 """Returns the tile index at the given index. The index can either be
1062 an x,y tuple or an int equal to ``y * width + x``'.
1064 if isinstance(index, (tuple, list)):
1067 index = y * self._width + x
1068 elif ininstance(index, int):
1069 x = index % self._width
1070 y = index // self._width
1071 if x > self._width or y > self._height:
1072 raise ValueError("Tile index out of bounds")
1073 return self._tiles[index]
1075 def __setitem__(self, index, value):
1076 """Sets the tile index at the given index. The index can either be
1077 an x,y tuple or an int equal to ``y * width + x``.
1079 if isinstance(index, (tuple, list)):
1082 index = y * self._width + x
1083 elif ininstance(index, int):
1084 x = index % self._width
1085 y = index // self._width
1086 if x > width or y > self._height or index >= len(self._tiles):
1087 raise ValueError("Tile index out of bounds")
1088 if not 0 <= value <= 255:
1089 raise ValueError("Tile value out of bounds")
1090 self._tiles[index] = value