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
41 from collections import namedtuple
46 __version__ = "0.0.0-auto.0"
47 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
51 Rectangle = namedtuple("Rectangle", "x1 y1 x2 y2")
52 AbsoluteTransform = namedtuple("AbsoluteTransform", "scale transpose_xy flip_x flip_y")
55 def release_displays():
56 """Releases any actively used displays so their busses and pins can be used again.
58 Use this once in your code.py if you initialize a display. Place it right before the
59 initialization so the display is active as long as possible.
61 for _disp in _displays:
62 _disp._release() # pylint: disable=protected-access
67 """Stores values of a certain size in a 2D array"""
69 def __init__(self, width, height, value_count):
70 """Create a Bitmap object with the given fixed size. Each pixel stores a value that is
71 used to index into a corresponding palette. This enables differently colored sprites to
72 share the underlying Bitmap. value_count is used to minimize the memory used to store
77 self._read_only = False
80 raise ValueError("value_count must be > 0")
83 while (value_count - 1) >> bits:
89 self._bits_per_value = bits
92 self._bits_per_value > 8
93 and self._bits_per_value != 16
94 and self._bits_per_value != 32
96 raise NotImplementedError("Invalid bits per value")
98 self._data = (width * height) * [0]
99 self._dirty_area = Rectangle(0, 0, width, height)
101 def __getitem__(self, index):
103 Returns the value at the given index. The index can either be
104 an x,y tuple or an int equal to `y * width + x`.
106 if isinstance(index, (tuple, list)):
107 index = (index[1] * self._width) + index[0]
108 if index >= len(self._data):
109 raise ValueError("Index {} is out of range".format(index))
110 return self._data[index]
112 def __setitem__(self, index, value):
114 Sets the value at the given index. The index can either be
115 an x,y tuple or an int equal to `y * width + x`.
118 raise RuntimeError("Read-only object")
119 if isinstance(index, (tuple, list)):
122 index = y * self._width + x
123 elif ininstance(index, int):
124 x = index % self._width
125 y = index // self._width
126 self._data[index] = value
127 if self._dirty_area.x1 == self._dirty_area.x2:
128 self._dirty_area.x1 = x
129 self._dirty_area.x2 = x + 1
130 self._dirty_area.y1 = y
131 self._dirty_area.y2 = y + 1
133 if x < self._dirty_area.x1:
134 self._dirty_area.x1 = x
135 elif x >= self._dirty_area.x2:
136 self._dirty_area.x2 = x + 1
137 if y < self._dirty_area.y1:
138 self._dirty_area.y1 = y
139 elif y >= self._dirty_area.y2:
140 self._dirty_area.y2 = y + 1
142 def _finish_refresh(self):
143 self._dirty_area.x1 = 0
144 self._dirty_area.x2 = 0
146 def fill(self, value):
147 """Fills the bitmap with the supplied palette index value."""
148 self._data = (self._width * self._height) * [value]
149 self._dirty_area = Rectangle(0, 0, self._width, self._height)
153 """Width of the bitmap. (read only)"""
158 """Height of the bitmap. (read only)"""
162 class ColorConverter:
163 """Converts one color format to another. Color converter based on original displayio
164 code for consistency.
167 def __init__(self, *, dither=False):
168 """Create a ColorConverter object to convert color formats.
169 Only supports RGblue88 to RGB565 currently.
170 :param bool dither: Adds random noise to dither the output image
172 self._dither = dither
175 def _compute_rgb565(self, color):
177 return (color >> 19) << 11 | ((color >> 10) & 0x3F) << 5 | (color >> 3) & 0x1F
179 def _compute_luma(self, color):
181 green = (color >> 8) & 0xFF
183 return (red * 19) / 255 + (green * 182) / 255 + (blue + 54) / 255
185 def _compute_chroma(self, color):
187 green = (color >> 8) & 0xFF
189 return max(red, green, blue) - min(red, green, blue)
191 def _compute_hue(self, color):
193 green = (color >> 8) & 0xFF
195 max_color = max(red, green, blue)
196 chroma = self._compute_chroma(color)
201 hue = (((green - blue) * 40) / chroma) % 240
202 elif max_color == green:
203 hue = (((blue - red) + (2 * chroma)) * 40) / chroma
204 elif max_color == blue:
205 hue = (((red - green) + (4 * chroma)) * 40) / chroma
211 def _dither_noise_1(self, noise):
212 noise = (noise >> 13) ^ noise
213 nn = (noise * (noise * noise * 60493 + 19990303) + 1376312589) & 0x7FFFFFFF
214 return (nn / (1073741824.0 * 2)) * 255
216 def _dither_noise_2(self, x, y):
217 return self._dither_noise_1(x + y * 0xFFFF)
219 def _compute_tricolor(self):
222 def convert(self, color):
223 "Converts the given RGblue88 color to RGB565"
225 return color # To Do: return a dithered color
226 return self._compute_rgb565(color)
228 def _pil_palette(self):
233 """When true the color converter dithers the output by adding
234 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)
274 pixels_in_byte_share_row=True,
276 reverse_pixels_in_byte=False,
277 set_column_command=0x2A,
278 set_row_command=0x2B,
279 write_ram_command=0x2C,
280 set_vertical_scroll=0,
282 brightness_command=None,
284 auto_brightness=False,
285 single_byte_bounds=False,
286 data_as_commands=False,
288 native_frames_per_second=60
290 """Create a Display object on the given display bus (`displayio.FourWire` or
291 `displayio.ParallelBus`).
293 The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
294 with a command byte followed by a byte to determine the parameter count and if a
295 delay is need after. When the top bit of the second byte is 1, the next byte will be
296 the delay time in milliseconds. The remaining 7 bits are the parameter count
297 excluding any delay byte. The third through final bytes are the remaining command
298 parameters. The next byte will begin a new command definition. Here is a portion of
300 .. code-block:: python
303 b"\xe1\x0f\x00\x0E\x14\x03\x11\x07\x31\xC1\x48\x08\x0F\x0C\x31\x36\x0F"
304 b"\x11\x80\x78"# Exit Sleep then delay 0x78 (120ms)
305 b"\x29\x80\x78"# Display on then delay 0x78 (120ms)
307 display = displayio.Display(display_bus, init_sequence, width=320, height=240)
309 The first command is 0xe1 with 15 (0xf) parameters following. The second and third
310 are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
311 Multiple byte literals (b”“) are merged together on load. The parens are needed to
312 allow byte literals on subsequent lines.
314 The initialization sequence should always leave the display memory access inline with
315 the scan of the display to minimize tearing artifacts.
317 self._bus = display_bus
318 self._set_column_command = set_column_command
319 self._set_row_command = set_row_command
320 self._write_ram_command = write_ram_command
321 self._brightness_command = brightness_command
322 self._data_as_commands = data_as_commands
323 self._single_byte_bounds = single_byte_bounds
325 self._height = height
326 self._colstart = colstart
327 self._rowstart = rowstart
328 self._rotation = rotation
329 self._auto_brightness = auto_brightness
330 self._brightness = brightness
331 self._auto_refresh = auto_refresh
332 self._initialize(init_sequence)
333 self._buffer = Image.new("RGB", (width, height))
334 self._subrectangles = []
335 self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
336 self._current_group = None
337 _displays.append(self)
338 self._refresh_thread = None
339 if self._auto_refresh:
340 self.auto_refresh = True
342 def _initialize(self, init_sequence):
344 while i < len(init_sequence):
345 command = init_sequence[i]
346 data_size = init_sequence[i + 1]
347 delay = (data_size & 0x80) > 0
349 data_byte = init_sequence[i + 2]
350 self._write(command, init_sequence[i + 2 : i + 2 + data_size])
354 delay_time_ms = init_sequence[i + 1 + data_size]
355 if delay_time_ms == 255:
357 time.sleep(delay_time_ms / 1000)
360 def _write(self, command, data):
361 if self._single_byte_bounds:
362 self._bus.send(True, bytes([command]) + data, toggle_every_byte=True)
364 self._bus.send(True, bytes([command]), toggle_every_byte=True)
365 self._bus.send(False, data)
371 def show(self, group):
372 """Switches to displaying the given group of layers. When group is None, the
373 default CircuitPython terminal will be shown.
375 self._current_group = group
377 def refresh(self, *, target_frames_per_second=60, minimum_frames_per_second=1):
378 """When auto refresh is off, waits for the target frame rate and then refreshes the
379 display, returning True. If the call has taken too long since the last refresh call
380 for the given target frame rate, then the refresh returns False immediately without
381 updating the screen to hopefully help getting caught up.
383 If the time since the last successful refresh is below the minimum frame rate, then
384 an exception will be raised. Set minimum_frames_per_second to 0 to disable.
386 When auto refresh is on, updates the display immediately. (The display will also
387 update without calls to this.)
389 # Go through groups and and add each to buffer
390 if self._current_group is not None:
391 buffer = Image.new("RGB", (self._width, self._height))
392 # Recursively have everything draw to the image
393 self._current_group._fill_area(buffer) # pylint: disable=protected-access
394 # save image to buffer (or probably refresh buffer so we can compare)
395 self._buffer.paste(buffer)
398 # Eventually calculate dirty rectangles here
399 self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
401 for area in self._subrectangles:
402 self._refresh_display_area(area)
404 def _refresh_loop(self):
405 while self._auto_refresh:
408 def _refresh_display_area(self, rectangle):
409 """Loop through dirty rectangles and redraw that area."""
410 """Read or write a block of data."""
411 data = numpy.array(self._buffer.crop(rectangle).convert("RGB")).astype("uint16")
413 ((data[:, :, 0] & 0xF8) << 8)
414 | ((data[:, :, 1] & 0xFC) << 3)
415 | (data[:, :, 2] >> 3)
419 numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
423 self._set_column_command,
425 rectangle.x1 + self._colstart, rectangle.x2 + self._colstart
429 self._set_row_command,
431 rectangle.y1 + self._rowstart, rectangle.y2 + self._rowstart
434 self._write(self._write_ram_command, pixels)
436 def _encode_pos(self, x, y):
437 """Encode a postion into bytes."""
438 return struct.pack(self._bounds_encoding, x, y)
440 def fill_row(self, y, buffer):
444 def auto_refresh(self):
445 return self._auto_refresh
448 def auto_refresh(self, value):
449 self._auto_refresh = value
450 if self._refresh_thread is None:
451 self._refresh_thread = threading.Thread(
452 target=self._refresh_loop, daemon=True
454 if value and not self._refresh_thread.is_alive():
456 self._refresh_thread.start()
457 elif not value and self._refresh_thread.is_alive():
459 self._refresh_thread.join()
462 def brightness(self):
463 """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
464 When `auto_brightness` is True, the value of `brightness` will change automatically.
465 If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
467 return self._brightness
470 def brightness(self, value):
471 self._brightness = value
474 def auto_brightness(self):
475 """True when the display brightness is adjusted automatically, based on an ambient
476 light sensor or other method. Note that some displays may have this set to True by
477 default, but not actually implement automatic brightness adjustment.
478 `auto_brightness` is set to False if `brightness` is set manually.
480 return self._auto_brightness
482 @auto_brightness.setter
483 def auto_brightness(self, value):
484 self._auto_brightness = value
496 """The rotation of the display as an int in degrees."""
497 return self._rotation
500 def rotation(self, value):
501 if value not in (0, 90, 180, 270):
502 raise ValueError("Rotation must be 0/90/180/270")
503 self._rotation = value
524 set_column_window_command=None,
525 set_row_window_command=None,
526 single_byte_bounds=False,
527 write_black_ram_command,
528 black_bits_inverted=False,
529 write_color_ram_command=None,
530 color_bits_inverted=False,
531 highlight_color=0x000000,
532 refresh_display_command,
536 seconds_per_frame=180,
537 always_toggle_chip_select=False
540 Create a EPaperDisplay object on the given display bus (displayio.FourWire or
541 displayio.ParallelBus).
543 The start_sequence and stop_sequence are bitpacked to minimize the ram impact. Every
544 command begins with a command byte followed by a byte to determine the parameter
545 count and if a delay is need after. When the top bit of the second byte is 1, the
546 next byte will be the delay time in milliseconds. The remaining 7 bits are the
547 parameter count excluding any delay byte. The third through final bytes are the
548 remaining command parameters. The next byte will begin a new command definition.
552 def show(self, group):
553 """Switches to displaying the given group of layers. When group is None, the default
554 CircuitPython terminal will be shown (eventually).
559 """Refreshes the display immediately or raises an exception if too soon. Use
560 ``time.sleep(display.time_to_refresh)`` to sleep until a refresh can occur.
565 def time_to_refresh(self):
566 """Time, in fractional seconds, until the ePaper display can be refreshed."""
583 """Manage updating a display over SPI four wire protocol in the background while
584 Python code runs. It doesn’t handle display initialization.
598 """Create a FourWire object associated with the given pins.
600 The SPI bus and pins are then in use by the display until
601 displayio.release_displays() is called even after a reload. (It does this so
602 CircuitPython can use the display after your code is done.)
603 So, the first time you initialize a display bus in code.py you should call
604 :py:func`displayio.release_displays` first, otherwise it will error after the
607 self._dc = digitalio.DigitalInOut(command)
608 self._dc.switch_to_output()
609 self._chip_select = digitalio.DigitalInOut(chip_select)
610 self._chip_select.switch_to_output(value=True)
612 if reset is not None:
613 self._reset = digitalio.DigitalInOut(reset)
614 self._reset.switch_to_output(value=True)
618 while self._spi.try_lock():
620 self._spi.configure(baudrate=baudrate, polarity=polarity, phase=phase)
627 self._chip_select.deinit()
628 if self._reset is not None:
632 if self._reset is not None:
633 self._reset.value = False
635 self._reset.value = True
638 def send(self, is_command, data, *, toggle_every_byte=False):
639 while self._spi.try_lock():
641 self._dc.value = not is_command
642 if toggle_every_byte:
644 self._spi.write(bytes([byte]))
645 self._chip_select.value = True
647 self._chip_select.value = False
649 self._spi.write(data)
654 """Manage a group of sprites and groups and how they are inter-related."""
656 def __init__(self, *, max_size=4, scale=1, x=0, y=0):
657 """Create a Group of a given size and scale. Scale is in
658 one dimension. For example, scale=2 leads to a layer’s
659 pixel being 2x2 pixels when in the group.
661 if not isinstance(max_size, int) or max_size < 1:
662 raise ValueError("Max Size must be an integer and >= 1")
663 self._max_size = max_size
664 if not isinstance(scale, int) or scale < 1:
665 raise ValueError("Scale must be an integer and >= 1")
671 self._supported_types = (TileGrid, Group)
673 def append(self, layer):
674 """Append a layer to the group. It will be drawn
677 if not isinstance(layer, self._supported_types):
678 raise ValueError("Invalid Group Memeber")
679 if len(self._layers) == self._max_size:
680 raise RuntimeError("Group full")
681 self._layers.append(layer)
683 def insert(self, index, layer):
684 """Insert a layer into the group."""
685 if not isinstance(layer, self._supported_types):
686 raise ValueError("Invalid Group Memeber")
687 if len(self._layers) == self._max_size:
688 raise RuntimeError("Group full")
689 self._layers.insert(index, layer)
691 def index(self, layer):
692 """Returns the index of the first copy of layer.
693 Raises ValueError if not found.
697 def pop(self, index=-1):
698 """Remove the ith item and return it."""
699 return self._layers.pop(index)
701 def remove(self, layer):
702 """Remove the first copy of layer. Raises ValueError
703 if it is not present."""
707 """Returns the number of layers in a Group"""
708 return len(self._layers)
710 def __getitem__(self, index):
711 """Returns the value at the given index."""
712 return self._layers[index]
714 def __setitem__(self, index, value):
715 """Sets the value at the given index."""
716 self._layers[index] = value
718 def __delitem__(self, index):
719 """Deletes the value at the given index."""
720 del self._layers[index]
722 def _fill_area(self, buffer):
726 for layer in self._layers:
727 if isinstance(layer, (Group, TileGrid)):
728 layer._fill_area(buffer) # pylint: disable=protected-access
735 def hidden(self, value):
736 if not isinstance(value, (bool, int)):
737 raise ValueError("Expecting a boolean or integer value")
738 self._hidden = bool(value)
745 def scale(self, value):
746 if not isinstance(value, int) or value < 1:
747 raise ValueError("Scale must be an integer and at least 1")
756 if not isinstance(value, int):
757 raise ValueError("x must be an integer")
766 if not isinstance(value, int):
767 raise ValueError("y must be an integer")
772 """Manage updating a display over I2C in the background while Python code runs.
773 It doesn’t handle display initialization.
776 def __init__(self, i2c_bus, *, device_address, reset=None):
777 """Create a I2CDisplay object associated with the given I2C bus and reset pin.
779 The I2C bus and pins are then in use by the display until displayio.release_displays() is
780 called even after a reload. (It does this so CircuitPython can use the display after your
781 code is done.) So, the first time you initialize a display bus in code.py you should call
782 :py:func`displayio.release_displays` first, otherwise it will error after the first
790 def send(self, command, data):
796 Loads values straight from disk. This minimizes memory use but can lead to much slower
797 pixel load times. These load times may result in frame tearing where only part of the
800 def __init__(self, file):
801 self._image = Image.open(file)
805 """Width of the bitmap. (read only)"""
806 return self._image.width
810 """Height of the bitmap. (read only)"""
811 return self._image.height
815 """Map a pixel palette_index to a full color. Colors are transformed to the display’s
816 format internally to save memory.
819 def __init__(self, color_count):
820 """Create a Palette object to store a set number of colors."""
821 self._needs_refresh = False
824 for _ in range(color_count):
825 self._colors.append(self._make_color(0))
827 def _make_color(self, value):
829 "transparent": False,
832 color_converter = ColorConverter()
833 if isinstance(value, (tuple, list, bytes, bytearray)):
834 value = (value[0] & 0xFF) << 16 | (value[1] & 0xFF) << 8 | value[2] & 0xFF
835 elif isinstance(value, int):
836 if not 0 <= value <= 0xFFFFFF:
837 raise ValueError("Color must be between 0x000000 and 0xFFFFFF")
839 raise TypeError("Color buffer must be a buffer, tuple, list, or int")
840 color["rgblue88"] = value
841 self._needs_refresh = True
846 """Returns the number of colors in a Palette"""
847 return len(self._colors)
849 def __setitem__(self, index, value):
850 """Sets the pixel color at the given index. The index should be
851 an integer in the range 0 to color_count-1.
853 The value argument represents a color, and can be from 0x000000 to 0xFFFFFF
854 (to represent an RGB value). Value can be an int, bytes (3 bytes (RGB) or
855 4 bytes (RGB + pad byte)), bytearray, or a tuple or list of 3 integers.
857 if self._colors[index]["rgblue88"] != value:
858 self._colors[index] = self._make_color(value)
860 def __getitem__(self, index):
861 if not 0 <= index < len(self._colors):
862 raise ValueError("Palette index out of range")
863 return self._colors[index]
865 def make_transparent(self, palette_index):
866 self._colors[palette_index]["transparent"] = True
868 def make_opaque(self, palette_index):
869 self._colors[palette_index]["transparent"] = False
873 """Manage updating a display over 8-bit parallel bus in the background while Python code
874 runs. This protocol may be refered to as 8080-I Series Parallel Interface in datasheets.
875 It doesn’t handle display initialization.
878 def __init__(self, i2c_bus, *, device_address, reset=None):
879 """Create a ParallelBus object associated with the given pins. The
880 bus is inferred from data0 by implying the next 7 additional pins on a given GPIO
883 The parallel bus and pins are then in use by the display until
884 displayio.release_displays() is called even after a reload. (It does this so
885 CircuitPython can use the display after your code is done.) So, the first time you
886 initialize a display bus in code.py you should call
887 :py:func`displayio.release_displays` first, otherwise it will error after the first
893 """Performs a hardware reset via the reset pin. Raises an exception if called when
894 no reset pin is available.
898 def send(self, command, data):
899 """Sends the given command value followed by the full set of data. Display state,
900 such as vertical scroll, set via ``send`` may or may not be reset once the code is
907 """Create a Shape object with the given fixed size. Each pixel is one bit and is stored
908 by the column boundaries of the shape on each row. Each row’s boundary defaults to the
912 def __init__(self, width, height, *, mirror_x=False, mirror_y=False):
913 """Create a Shape object with the given fixed size. Each pixel is one bit and is
914 stored by the column boundaries of the shape on each row. Each row’s boundary
915 defaults to the full row.
919 def set_boundary(self, y, start_x, end_x):
920 """Loads pre-packed data into the given row."""
925 """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
926 grids can share bitmaps and pixel shaders.
928 A single tile grid is also known as a Sprite.
944 """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is
945 used to convert the value and its location to a display native pixel color. This may
946 be a simple color palette lookup, a gradient, a pattern or a color transformer.
948 tile_width and tile_height match the height of the bitmap by default.
950 if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
951 raise ValueError("Unsupported Bitmap type")
952 self._bitmap = bitmap
953 bitmap_width = bitmap.width
954 bitmap_height = bitmap.height
956 if not isinstance(pixel_shader, (ColorConverter, Palette)):
957 raise ValueError("Unsupported Pixel Shader type")
958 self._pixel_shader = pixel_shader
962 self._width = width # Number of Tiles Wide
963 self._height = height # Number of Tiles High
964 if tile_width is None:
965 tile_width = bitmap_width
966 if tile_height is None:
967 tile_height = bitmap_height
968 if bitmap_width % tile_width != 0:
969 raise ValueError("Tile width must exactly divide bitmap width")
970 self._tile_width = tile_width
971 if bitmap_height % tile_height != 0:
972 raise ValueError("Tile height must exactly divide bitmap height")
973 self._tile_height = tile_height
974 if not 0 <= default_tile <= 255:
975 raise ValueError("Default Tile is out of range")
976 self._tiles = (self._width * self._height) * [default_tile]
978 def _fill_area(self, buffer):
979 """Draw onto the image"""
984 "RGB", (self._width * self._tile_width, self._height * self._tile_height)
987 tile_count_x = self._bitmap.width // self._tile_width
988 tile_count_y = self._bitmap.height // self._tile_height
990 for tile_x in range(0, self._width):
991 for tile_y in range(0, self._height):
992 tile_index = self._tiles[tile_y * self._width + tile_x]
993 tile_index_x = tile_index % tile_count_x
994 tile_index_y = tile_index // tile_count_x
995 for pixel_x in range(self._tile_width):
996 for pixel_y in range(self._tile_height):
997 image_x = tile_x * self._tile_width + pixel_x
998 image_y = tile_y * self._tile_height + pixel_y
999 bitmap_x = tile_index_x * self._tile_width + pixel_x
1000 bitmap_y = tile_index_y * self._tile_height + pixel_y
1001 pixel_color = self._pixel_shader[
1002 self._bitmap[bitmap_x, bitmap_y]
1004 if not pixel_color["transparent"]:
1005 image.putpixel((image_x, image_y), pixel_color["rgblue88"])
1007 # Apply transforms or mirrors or whatever here
1008 if self._tile_width == 6:
1009 print("Putting at {}".format((self._x, self._y)))
1010 buffer.paste(image, (self._x, self._y))
1014 """True when the TileGrid is hidden. This may be False even
1015 when a part of a hidden Group."""
1019 def hidden(self, value):
1020 if not isinstance(value, (bool, int)):
1021 raise ValueError("Expecting a boolean or integer value")
1022 self._hidden = bool(value)
1026 """X position of the left edge in the parent."""
1031 """Y position of the top edge in the parent."""
1036 """If true, the left edge rendered will be the right edge of the right-most tile."""
1040 def flip_x(self, value):
1041 if not isinstance(value, bool):
1042 raise TypeError("Flip X should be a boolean type")
1043 self._flip_x = value
1047 """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
1051 def flip_y(self, value):
1052 if not isinstance(value, bool):
1053 raise TypeError("Flip Y should be a boolean type")
1054 self._flip_y = value
1057 def transpose_xy(self):
1058 """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90
1059 degree rotation can be achieved along with the corresponding mirrored version.
1061 return self._transpose_xy
1063 @transpose_xy.setter
1064 def transpose_xy(self, value):
1065 if not isinstance(value, bool):
1066 raise TypeError("Transpose XY should be a boolean type")
1067 self._transpose_xy = value
1070 def pixel_shader(self):
1071 """The pixel shader of the tilegrid."""
1074 def __getitem__(self, index):
1075 """Returns the tile index at the given index. The index can either be
1076 an x,y tuple or an int equal to ``y * width + x``'.
1078 if isinstance(index, (tuple, list)):
1081 index = y * self._width + x
1082 elif isinstance(index, int):
1083 x = index % self._width
1084 y = index // self._width
1085 if x > self._width or y > self._height or index >= len(self._tiles):
1086 raise ValueError("Tile index out of bounds")
1087 return self._tiles[index]
1089 def __setitem__(self, index, value):
1090 """Sets the tile index at the given index. The index can either be
1091 an x,y tuple or an int equal to ``y * width + x``.
1093 if isinstance(index, (tuple, list)):
1096 index = y * self._width + x
1097 elif isinstance(index, int):
1098 x = index % self._width
1099 y = index // self._width
1100 if x > self._width or y > self._height or index >= len(self._tiles):
1101 raise ValueError("Tile index out of bounds")
1102 if not 0 <= value <= 255:
1103 raise ValueError("Tile value out of bounds")
1104 self._tiles[index] = value