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 recordclass import recordclass
46 __version__ = "0.0.0-auto.0"
47 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
49 # pylint: disable=unnecessary-pass, unused-argument, too-many-lines
53 Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
54 Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y")
57 def release_displays():
58 """Releases any actively used displays so their busses and pins can be used again.
60 Use this once in your code.py if you initialize a display. Place it right before the
61 initialization so the display is active as long as possible.
63 for _disp in _displays:
64 _disp._release() # pylint: disable=protected-access
69 """Stores values of a certain size in a 2D array"""
71 def __init__(self, width, height, value_count):
72 """Create a Bitmap object with the given fixed size. Each pixel stores a value that is
73 used to index into a corresponding palette. This enables differently colored sprites to
74 share the underlying Bitmap. value_count is used to minimize the memory used to store
79 self._read_only = False
82 raise ValueError("value_count must be > 0")
85 while (value_count - 1) >> bits:
91 self._bits_per_value = bits
94 self._bits_per_value > 8
95 and self._bits_per_value != 16
96 and self._bits_per_value != 32
98 raise NotImplementedError("Invalid bits per value")
100 self._data = (width * height) * [0]
101 self._dirty_area = Rectangle(0, 0, width, height)
103 def __getitem__(self, index):
105 Returns the value at the given index. The index can either be
106 an x,y tuple or an int equal to `y * width + x`.
108 if isinstance(index, (tuple, list)):
109 index = (index[1] * self._width) + index[0]
110 if index >= len(self._data):
111 raise ValueError("Index {} is out of range".format(index))
112 return self._data[index]
114 def __setitem__(self, index, value):
116 Sets the value at the given index. The index can either be
117 an x,y tuple or an int equal to `y * width + x`.
120 raise RuntimeError("Read-only object")
121 if isinstance(index, (tuple, list)):
124 index = y * self._width + x
125 elif isinstance(index, int):
126 x = index % self._width
127 y = index // self._width
128 self._data[index] = value
129 if self._dirty_area.x1 == self._dirty_area.x2:
130 self._dirty_area.x1 = x
131 self._dirty_area.x2 = x + 1
132 self._dirty_area.y1 = y
133 self._dirty_area.y2 = y + 1
135 if x < self._dirty_area.x1:
136 self._dirty_area.x1 = x
137 elif x >= self._dirty_area.x2:
138 self._dirty_area.x2 = x + 1
139 if y < self._dirty_area.y1:
140 self._dirty_area.y1 = y
141 elif y >= self._dirty_area.y2:
142 self._dirty_area.y2 = y + 1
144 def _finish_refresh(self):
145 self._dirty_area.x1 = 0
146 self._dirty_area.x2 = 0
148 def fill(self, value):
149 """Fills the bitmap with the supplied palette index value."""
150 self._data = (self._width * self._height) * [value]
151 self._dirty_area = Rectangle(0, 0, self._width, self._height)
155 """Width of the bitmap. (read only)"""
160 """Height of the bitmap. (read only)"""
164 class ColorConverter:
165 """Converts one color format to another. Color converter based on original displayio
166 code for consistency.
169 def __init__(self, *, dither=False):
170 """Create a ColorConverter object to convert color formats.
171 Only supports rgb888 to RGB565 currently.
172 :param bool dither: Adds random noise to dither the output image
174 self._dither = dither
177 # pylint: disable=no-self-use
178 def _compute_rgb565(self, color):
180 return (color >> 19) << 11 | ((color >> 10) & 0x3F) << 5 | (color >> 3) & 0x1F
182 def _compute_luma(self, color):
184 green = (color >> 8) & 0xFF
186 return (red * 19) / 255 + (green * 182) / 255 + (blue + 54) / 255
188 def _compute_chroma(self, color):
190 green = (color >> 8) & 0xFF
192 return max(red, green, blue) - min(red, green, blue)
194 def _compute_hue(self, color):
196 green = (color >> 8) & 0xFF
198 max_color = max(red, green, blue)
199 chroma = self._compute_chroma(color)
204 hue = (((green - blue) * 40) / chroma) % 240
205 elif max_color == green:
206 hue = (((blue - red) + (2 * chroma)) * 40) / chroma
207 elif max_color == blue:
208 hue = (((red - green) + (4 * chroma)) * 40) / chroma
214 def _dither_noise_1(self, noise):
215 noise = (noise >> 13) ^ noise
217 noise * (noise * noise * 60493 + 19990303) + 1376312589
219 return (more_noise / (1073741824.0 * 2)) * 255
221 def _dither_noise_2(self, x, y):
222 return self._dither_noise_1(x + y * 0xFFFF)
224 def _compute_tricolor(self):
227 def convert(self, color):
228 "Converts the given rgb888 color to RGB565"
230 return color # To Do: return a dithered color
231 return self._compute_rgb565(color)
233 # pylint: enable=no-self-use
237 """When true the color converter dithers the output by adding
238 random noise when truncating to display bitdepth
243 def dither(self, value):
244 if not isinstance(value, bool):
245 raise ValueError("Value should be boolean")
249 # pylint: disable=too-many-instance-attributes
251 """This initializes a display and connects it into CircuitPython. Unlike other objects
252 in CircuitPython, Display objects live until ``displayio.release_displays()`` is called.
253 This is done so that CircuitPython can use the display itself.
255 Most people should not use this class directly. Use a specific display driver instead
256 that will contain the initialization sequence at minimum.
259 Display(display_bus, init_sequence, *, width, height, colstart=0, rowstart=0, rotation=0,
260 color_depth=16, grayscale=False, pixels_in_byte_share_row=True, bytes_per_cell=1,
261 reverse_pixels_in_byte=False, set_column_command=0x2a, set_row_command=0x2b,
262 write_ram_command=0x2c, set_vertical_scroll=0, backlight_pin=None, brightness_command=None,
263 brightness=1.0, auto_brightness=False, single_byte_bounds=False, data_as_commands=False,
264 auto_refresh=True, native_frames_per_second=60)
267 # pylint: disable=too-many-locals
280 pixels_in_byte_share_row=True,
282 reverse_pixels_in_byte=False,
283 set_column_command=0x2A,
284 set_row_command=0x2B,
285 write_ram_command=0x2C,
286 set_vertical_scroll=0,
288 brightness_command=None,
290 auto_brightness=False,
291 single_byte_bounds=False,
292 data_as_commands=False,
294 native_frames_per_second=60
296 """Create a Display object on the given display bus (`displayio.FourWire` or
297 `displayio.ParallelBus`).
299 The ``init_sequence`` is bitpacked to minimize the ram impact. Every command begins
300 with a command byte followed by a byte to determine the parameter count and if a
301 delay is need after. When the top bit of the second byte is 1, the next byte will be
302 the delay time in milliseconds. The remaining 7 bits are the parameter count
303 excluding any delay byte. The third through final bytes are the remaining command
304 parameters. The next byte will begin a new command definition. Here is a portion of
306 .. code-block:: python
309 b"\xe1\x0f\x00\x0E\x14\x03\x11\x07\x31\xC1\x48\x08\x0F\x0C\x31\x36\x0F"
310 b"\x11\x80\x78"# Exit Sleep then delay 0x78 (120ms)
311 b"\x29\x80\x78"# Display on then delay 0x78 (120ms)
313 display = displayio.Display(display_bus, init_sequence, width=320, height=240)
315 The first command is 0xe1 with 15 (0xf) parameters following. The second and third
316 are 0x11 and 0x29 respectively with delays (0x80) of 120ms (0x78) and no parameters.
317 Multiple byte literals (b”“) are merged together on load. The parens are needed to
318 allow byte literals on subsequent lines.
320 The initialization sequence should always leave the display memory access inline with
321 the scan of the display to minimize tearing artifacts.
323 self._bus = display_bus
324 self._set_column_command = set_column_command
325 self._set_row_command = set_row_command
326 self._write_ram_command = write_ram_command
327 self._brightness_command = brightness_command
328 self._data_as_commands = data_as_commands
329 self._single_byte_bounds = single_byte_bounds
331 self._height = height
332 self._colstart = colstart
333 self._rowstart = rowstart
334 self._rotation = rotation
335 self._auto_brightness = auto_brightness
336 self._brightness = brightness
337 self._auto_refresh = auto_refresh
338 self._initialize(init_sequence)
339 self._buffer = Image.new("RGBA", (width, height))
340 self._subrectangles = []
341 self._bounds_encoding = ">BB" if single_byte_bounds else ">HH"
342 self._current_group = None
343 _displays.append(self)
344 self._refresh_thread = None
345 if self._auto_refresh:
346 self.auto_refresh = True
348 # pylint: enable=too-many-locals
350 def _initialize(self, init_sequence):
352 while i < len(init_sequence):
353 command = init_sequence[i]
354 data_size = init_sequence[i + 1]
355 delay = (data_size & 0x80) > 0
357 self._write(command, init_sequence[i + 2 : i + 2 + data_size])
361 delay_time_ms = init_sequence[i + 1 + data_size]
362 if delay_time_ms == 255:
364 time.sleep(delay_time_ms / 1000)
367 def _write(self, command, data):
368 if self._single_byte_bounds:
369 self._bus.send(True, bytes([command]) + data, toggle_every_byte=True)
371 self._bus.send(True, bytes([command]), toggle_every_byte=True)
372 self._bus.send(False, data)
378 def show(self, group):
379 """Switches to displaying the given group of layers. When group is None, the
380 default CircuitPython terminal will be shown.
382 self._current_group = group
384 def refresh(self, *, target_frames_per_second=60, minimum_frames_per_second=1):
385 """When auto refresh is off, waits for the target frame rate and then refreshes the
386 display, returning True. If the call has taken too long since the last refresh call
387 for the given target frame rate, then the refresh returns False immediately without
388 updating the screen to hopefully help getting caught up.
390 If the time since the last successful refresh is below the minimum frame rate, then
391 an exception will be raised. Set minimum_frames_per_second to 0 to disable.
393 When auto refresh is on, updates the display immediately. (The display will also
394 update without calls to this.)
396 # Go through groups and and add each to buffer
397 if self._current_group is not None:
398 buffer = Image.new("RGBA", (self._width, self._height))
399 # Recursively have everything draw to the image
400 self._current_group._fill_area(buffer) # pylint: disable=protected-access
401 # save image to buffer (or probably refresh buffer so we can compare)
402 self._buffer.paste(buffer)
404 # Eventually calculate dirty rectangles here
405 self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
407 for area in self._subrectangles:
408 self._refresh_display_area(area)
410 def _refresh_loop(self):
411 while self._auto_refresh:
414 def _refresh_display_area(self, rectangle):
415 """Loop through dirty rectangles and redraw that area."""
416 data = numpy.array(self._buffer.crop(rectangle).convert("RGB")).astype("uint16")
418 ((data[:, :, 0] & 0xF8) << 8)
419 | ((data[:, :, 1] & 0xFC) << 3)
420 | (data[:, :, 2] >> 3)
424 numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
428 self._set_column_command,
430 rectangle.x1 + self._colstart, rectangle.x2 + self._colstart
434 self._set_row_command,
436 rectangle.y1 + self._rowstart, rectangle.y2 + self._rowstart
439 self._write(self._write_ram_command, pixels)
441 def _encode_pos(self, x, y):
442 """Encode a postion into bytes."""
443 return struct.pack(self._bounds_encoding, x, y)
445 def fill_row(self, y, buffer):
446 """Extract the pixels from a single row"""
450 def auto_refresh(self):
451 """True when the display is refreshed automatically."""
452 return self._auto_refresh
455 def auto_refresh(self, value):
456 self._auto_refresh = value
457 if self._refresh_thread is None:
458 self._refresh_thread = threading.Thread(
459 target=self._refresh_loop, daemon=True
461 if value and not self._refresh_thread.is_alive():
463 self._refresh_thread.start()
464 elif not value and self._refresh_thread.is_alive():
466 self._refresh_thread.join()
469 def brightness(self):
470 """The brightness of the display as a float. 0.0 is off and 1.0 is full `brightness`.
471 When `auto_brightness` is True, the value of `brightness` will change automatically.
472 If `brightness` is set, `auto_brightness` will be disabled and will be set to False.
474 return self._brightness
477 def brightness(self, value):
478 self._brightness = value
481 def auto_brightness(self):
482 """True when the display brightness is adjusted automatically, based on an ambient
483 light sensor or other method. Note that some displays may have this set to True by
484 default, but not actually implement automatic brightness adjustment.
485 `auto_brightness` is set to False if `brightness` is set manually.
487 return self._auto_brightness
489 @auto_brightness.setter
490 def auto_brightness(self, value):
491 self._auto_brightness = value
505 """The rotation of the display as an int in degrees."""
506 return self._rotation
509 def rotation(self, value):
510 if value not in (0, 90, 180, 270):
511 raise ValueError("Rotation must be 0/90/180/270")
512 self._rotation = value
516 """Current Display Bus"""
520 # pylint: enable=too-many-instance-attributes
524 """Manage updating an epaper display over a display bus
526 This initializes an epaper display and connects it into CircuitPython. Unlike other
527 objects in CircuitPython, EPaperDisplay objects live until
528 displayio.release_displays() is called. This is done so that CircuitPython can use
531 Most people should not use this class directly. Use a specific display driver instead
532 that will contain the startup and shutdown sequences at minimum.
535 # pylint: disable=too-many-locals
549 set_column_window_command=None,
550 set_row_window_command=None,
551 single_byte_bounds=False,
552 write_black_ram_command,
553 black_bits_inverted=False,
554 write_color_ram_command=None,
555 color_bits_inverted=False,
556 highlight_color=0x000000,
557 refresh_display_command,
561 seconds_per_frame=180,
562 always_toggle_chip_select=False
565 Create a EPaperDisplay object on the given display bus (displayio.FourWire or
566 displayio.ParallelBus).
568 The start_sequence and stop_sequence are bitpacked to minimize the ram impact. Every
569 command begins with a command byte followed by a byte to determine the parameter
570 count and if a delay is need after. When the top bit of the second byte is 1, the
571 next byte will be the delay time in milliseconds. The remaining 7 bits are the
572 parameter count excluding any delay byte. The third through final bytes are the
573 remaining command parameters. The next byte will begin a new command definition.
577 # pylint: enable=too-many-locals
579 def show(self, group):
580 """Switches to displaying the given group of layers. When group is None, the default
581 CircuitPython terminal will be shown (eventually).
586 """Refreshes the display immediately or raises an exception if too soon. Use
587 ``time.sleep(display.time_to_refresh)`` to sleep until a refresh can occur.
592 def time_to_refresh(self):
593 """Time, in fractional seconds, until the ePaper display can be refreshed."""
608 """Current Display Bus"""
613 """Manage updating a display over SPI four wire protocol in the background while
614 Python code runs. It doesn’t handle display initialization.
628 """Create a FourWire object associated with the given pins.
630 The SPI bus and pins are then in use by the display until
631 displayio.release_displays() is called even after a reload. (It does this so
632 CircuitPython can use the display after your code is done.)
633 So, the first time you initialize a display bus in code.py you should call
634 :py:func`displayio.release_displays` first, otherwise it will error after the
637 self._dc = digitalio.DigitalInOut(command)
638 self._dc.switch_to_output()
639 self._chip_select = digitalio.DigitalInOut(chip_select)
640 self._chip_select.switch_to_output(value=True)
642 if reset is not None:
643 self._reset = digitalio.DigitalInOut(reset)
644 self._reset.switch_to_output(value=True)
648 while self._spi.try_lock():
650 self._spi.configure(baudrate=baudrate, polarity=polarity, phase=phase)
657 self._chip_select.deinit()
658 if self._reset is not None:
662 """Performs a hardware reset via the reset pin.
663 Raises an exception if called when no reset pin is available.
665 if self._reset is not None:
666 self._reset.value = False
668 self._reset.value = True
671 raise RuntimeError("No reset pin defined")
673 def send(self, is_command, data, *, toggle_every_byte=False):
674 """Sends the given command value followed by the full set of data. Display state,
675 such as vertical scroll, set via ``send`` may or may not be reset once the code is
678 while self._spi.try_lock():
680 self._dc.value = not is_command
681 if toggle_every_byte:
683 self._spi.write(bytes([byte]))
684 self._chip_select.value = True
686 self._chip_select.value = False
688 self._spi.write(data)
693 """Manage a group of sprites and groups and how they are inter-related."""
695 def __init__(self, *, max_size=4, scale=1, x=0, y=0):
696 """Create a Group of a given size and scale. Scale is in
697 one dimension. For example, scale=2 leads to a layer’s
698 pixel being 2x2 pixels when in the group.
700 if not isinstance(max_size, int) or max_size < 1:
701 raise ValueError("Max Size must be >= 1")
702 self._max_size = max_size
703 if not isinstance(scale, int) or scale < 1:
704 raise ValueError("Scale must be >= 1")
710 self._supported_types = (TileGrid, Group)
711 self._absolute_transform = None
712 self.in_group = False
713 self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
715 def update_transform(self, parent_transform):
716 """Update the parent transform and child transforms"""
717 self.in_group = parent_transform is not None
721 if parent_transform.transpose_xy:
723 self._absolute_transform.x = parent_transform.x + parent_transform.dx * x
724 self._absolute_transform.y = parent_transform.y + parent_transform.dy * y
725 self._absolute_transform.dx = parent_transform.dx * self._scale
726 self._absolute_transform.dy = parent_transform.dy * self._scale
727 self._absolute_transform.transpose_xy = parent_transform.transpose_xy
728 self._absolute_transform.mirror_x = parent_transform.mirror_x
729 self._absolute_transform.mirror_y = parent_transform.mirror_y
730 self._absolute_transform.scale = parent_transform.scale * self._scale
731 self._update_child_transforms()
733 def _update_child_transforms(self):
735 for layer in self._layers:
736 layer.update_transform(self._absolute_transform)
738 def _removal_cleanup(self, index):
739 layer = self._layers[index]
740 layer.update_transform(None)
742 def _layer_update(self, index):
743 layer = self._layers[index]
744 layer.update_transform(self._absolute_transform)
746 def append(self, layer):
747 """Append a layer to the group. It will be drawn
750 self.insert(len(self._layers), layer)
752 def insert(self, index, layer):
753 """Insert a layer into the group."""
754 if not isinstance(layer, self._supported_types):
755 raise ValueError("Invalid Group Member")
757 raise ValueError("Layer already in a group.")
758 if len(self._layers) == self._max_size:
759 raise RuntimeError("Group full")
760 self._layers.insert(index, layer)
761 self._layer_update(index)
763 def index(self, layer):
764 """Returns the index of the first copy of layer.
765 Raises ValueError if not found.
767 return self._layers.index(layer)
769 def pop(self, index=-1):
770 """Remove the ith item and return it."""
771 self._removal_cleanup(index)
772 return self._layers.pop(index)
774 def remove(self, layer):
775 """Remove the first copy of layer. Raises ValueError
776 if it is not present."""
777 index = self.index(layer)
778 self._layers.pop(index)
781 """Returns the number of layers in a Group"""
782 return len(self._layers)
784 def __getitem__(self, index):
785 """Returns the value at the given index."""
786 return self._layers[index]
788 def __setitem__(self, index, value):
789 """Sets the value at the given index."""
790 self._removal_cleanup(index)
791 self._layers[index] = value
792 self._layer_update(index)
794 def __delitem__(self, index):
795 """Deletes the value at the given index."""
796 del self._layers[index]
798 def _fill_area(self, buffer):
802 for layer in self._layers:
803 if isinstance(layer, (Group, TileGrid)):
804 layer._fill_area(buffer) # pylint: disable=protected-access
808 """True when the Group and all of it’s layers are not visible. When False, the
809 Group’s layers are visible if they haven’t been hidden.
814 def hidden(self, value):
815 if not isinstance(value, (bool, int)):
816 raise ValueError("Expecting a boolean or integer value")
817 self._hidden = bool(value)
821 """Scales each pixel within the Group in both directions. For example, when
822 scale=2 each pixel will be represented by 2x2 pixels.
827 def scale(self, value):
828 if not isinstance(value, int) or value < 1:
829 raise ValueError("Scale must be >= 1")
830 if self._scale != value:
831 parent_scale = self._absolute_transform.scale / self._scale
832 self._absolute_transform.dx = (
833 self._absolute_transform.dx / self._scale * value
835 self._absolute_transform.dy = (
836 self._absolute_transform.dy / self._scale * value
838 self._absolute_transform.scale = parent_scale * value
841 self._update_child_transforms()
845 """X position of the Group in the parent."""
850 if not isinstance(value, int):
851 raise ValueError("x must be an integer")
853 if self._absolute_transform.transpose_xy:
854 dy_value = self._absolute_transform.dy / self._scale
855 self._absolute_transform.y += dy_value * (value - self._x)
857 dx_value = self._absolute_transform.dx / self._scale
858 self._absolute_transform.x += dx_value * (value - self._x)
860 self._update_child_transforms()
864 """Y position of the Group in the parent."""
869 if not isinstance(value, int):
870 raise ValueError("y must be an integer")
872 if self._absolute_transform.transpose_xy:
873 dx_value = self._absolute_transform.dx / self._scale
874 self._absolute_transform.x += dx_value * (value - self._y)
876 dy_value = self._absolute_transform.dy / self._scale
877 self._absolute_transform.y += dy_value * (value - self._y)
879 self._update_child_transforms()
883 """Manage updating a display over I2C in the background while Python code runs.
884 It doesn’t handle display initialization.
887 def __init__(self, i2c_bus, *, device_address, reset=None):
888 """Create a I2CDisplay object associated with the given I2C bus and reset pin.
890 The I2C bus and pins are then in use by the display until displayio.release_displays() is
891 called even after a reload. (It does this so CircuitPython can use the display after your
892 code is done.) So, the first time you initialize a display bus in code.py you should call
893 :py:func`displayio.release_displays` first, otherwise it will error after the first
899 """Performs a hardware reset via the reset pin. Raises an exception if called
900 when no reset pin is available.
904 def send(self, command, data):
905 """Sends the given command value followed by the full set of data. Display state,
906 such as vertical scroll, set via send may or may not be reset once the code is
914 Loads values straight from disk. This minimizes memory use but can lead to much slower
915 pixel load times. These load times may result in frame tearing where only part of the
918 def __init__(self, file):
919 self._image = Image.open(file)
923 """Width of the bitmap. (read only)"""
924 return self._image.width
928 """Height of the bitmap. (read only)"""
929 return self._image.height
933 """Map a pixel palette_index to a full color. Colors are transformed to the display’s
934 format internally to save memory.
937 def __init__(self, color_count):
938 """Create a Palette object to store a set number of colors."""
939 self._needs_refresh = False
942 for _ in range(color_count):
943 self._colors.append(self._make_color(0))
944 self._update_rgba(len(self._colors) - 1)
946 def _update_rgba(self, index):
947 color = self._colors[index]["rgb888"]
948 transparent = self._colors[index]["transparent"]
949 self._colors[index]["rgba"] = (
953 0 if transparent else 0xFF,
956 def _make_color(self, value, transparent=False):
958 "transparent": transparent,
960 "rgba": (0, 0, 0, 255),
962 if isinstance(value, (tuple, list, bytes, bytearray)):
963 value = (value[0] & 0xFF) << 16 | (value[1] & 0xFF) << 8 | value[2] & 0xFF
964 elif isinstance(value, int):
965 if not 0 <= value <= 0xFFFFFF:
966 raise ValueError("Color must be between 0x000000 and 0xFFFFFF")
968 raise TypeError("Color buffer must be a buffer, tuple, list, or int")
969 color["rgb888"] = value
970 self._needs_refresh = True
975 """Returns the number of colors in a Palette"""
976 return len(self._colors)
978 def __setitem__(self, index, value):
979 """Sets the pixel color at the given index. The index should be
980 an integer in the range 0 to color_count-1.
982 The value argument represents a color, and can be from 0x000000 to 0xFFFFFF
983 (to represent an RGB value). Value can be an int, bytes (3 bytes (RGB) or
984 4 bytes (RGB + pad byte)), bytearray, or a tuple or list of 3 integers.
986 if self._colors[index]["rgb888"] != value:
987 self._colors[index] = self._make_color(value)
988 self._update_rgba(index)
990 def __getitem__(self, index):
991 if not 0 <= index < len(self._colors):
992 raise ValueError("Palette index out of range")
993 return self._colors[index]
995 def make_transparent(self, palette_index):
996 """Set the palette index to be a transparent color"""
997 self._colors[palette_index]["transparent"] = True
998 self._update_rgba(palette_index)
1000 def make_opaque(self, palette_index):
1001 """Set the palette index to be an opaque color"""
1002 self._colors[palette_index]["transparent"] = False
1003 self._update_rgba(palette_index)
1007 """Manage updating a display over 8-bit parallel bus in the background while Python code
1008 runs. This protocol may be refered to as 8080-I Series Parallel Interface in datasheets.
1009 It doesn’t handle display initialization.
1012 def __init__(self, i2c_bus, *, device_address, reset=None):
1013 """Create a ParallelBus object associated with the given pins. The
1014 bus is inferred from data0 by implying the next 7 additional pins on a given GPIO
1017 The parallel bus and pins are then in use by the display until
1018 displayio.release_displays() is called even after a reload. (It does this so
1019 CircuitPython can use the display after your code is done.) So, the first time you
1020 initialize a display bus in code.py you should call
1021 :py:func`displayio.release_displays` first, otherwise it will error after the first
1027 """Performs a hardware reset via the reset pin. Raises an exception if called when
1028 no reset pin is available.
1032 def send(self, command, data):
1033 """Sends the given command value followed by the full set of data. Display state,
1034 such as vertical scroll, set via ``send`` may or may not be reset once the code is
1040 class Shape(Bitmap):
1041 """Create a Shape object with the given fixed size. Each pixel is one bit and is stored
1042 by the column boundaries of the shape on each row. Each row’s boundary defaults to the
1046 def __init__(self, width, height, *, mirror_x=False, mirror_y=False):
1047 """Create a Shape object with the given fixed size. Each pixel is one bit and is
1048 stored by the column boundaries of the shape on each row. Each row’s boundary
1049 defaults to the full row.
1051 super().__init__(width, height, 2)
1053 def set_boundary(self, y, start_x, end_x):
1054 """Loads pre-packed data into the given row."""
1058 # pylint: disable=too-many-instance-attributes
1060 """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
1061 grids can share bitmaps and pixel shaders.
1063 A single tile grid is also known as a Sprite.
1079 """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is
1080 used to convert the value and its location to a display native pixel color. This may
1081 be a simple color palette lookup, a gradient, a pattern or a color transformer.
1083 tile_width and tile_height match the height of the bitmap by default.
1085 if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
1086 raise ValueError("Unsupported Bitmap type")
1087 self._bitmap = bitmap
1088 bitmap_width = bitmap.width
1089 bitmap_height = bitmap.height
1091 if not isinstance(pixel_shader, (ColorConverter, Palette)):
1092 raise ValueError("Unsupported Pixel Shader type")
1093 self._pixel_shader = pixel_shader
1094 self._hidden = False
1097 self._width = width # Number of Tiles Wide
1098 self._height = height # Number of Tiles High
1099 self._transpose_xy = False
1100 self._flip_x = False
1101 self._flip_y = False
1102 if tile_width is None:
1103 tile_width = bitmap_width
1104 if tile_height is None:
1105 tile_height = bitmap_height
1106 if bitmap_width % tile_width != 0:
1107 raise ValueError("Tile width must exactly divide bitmap width")
1108 self._tile_width = tile_width
1109 if bitmap_height % tile_height != 0:
1110 raise ValueError("Tile height must exactly divide bitmap height")
1111 self._tile_height = tile_height
1112 if not 0 <= default_tile <= 255:
1113 raise ValueError("Default Tile is out of range")
1114 self._pixel_width = width * tile_width
1115 self._pixel_height = height * tile_height
1116 self._tiles = (self._width * self._height) * [default_tile]
1117 self.in_group = False
1118 self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
1119 self._current_area = Rectangle(0, 0, self._pixel_width, self._pixel_height)
1122 def update_transform(self, absolute_transform):
1123 """Update the parent transform and child transforms"""
1124 self._absolute_transform = absolute_transform
1125 if self._absolute_transform is not None:
1126 self._update_current_x()
1127 self._update_current_y()
1129 def _update_current_x(self):
1130 if self._transpose_xy:
1131 width = self._pixel_height
1133 width = self._pixel_width
1134 if self._absolute_transform.transpose_xy:
1135 self._current_area.y1 = (
1136 self._absolute_transform.y + self._absolute_transform.dy * self._x
1138 self._current_area.y2 = (
1139 self._absolute_transform.y
1140 + self._absolute_transform.dy * (self._x + width)
1142 if self._current_area.y2 < self._current_area.y1:
1143 self._current_area.y1, self._current_area.y2 = (
1144 self._current_area.y2,
1145 self._current_area.y1,
1148 self._current_area.x1 = (
1149 self._absolute_transform.x + self._absolute_transform.dx * self._x
1151 self._current_area.x2 = (
1152 self._absolute_transform.x
1153 + self._absolute_transform.dx * (self._x + width)
1155 if self._current_area.x2 < self._current_area.x1:
1156 self._current_area.x1, self._current_area.x2 = (
1157 self._current_area.x2,
1158 self._current_area.x1,
1161 def _update_current_y(self):
1162 if self._transpose_xy:
1163 height = self._pixel_width
1165 height = self._pixel_height
1166 if self._absolute_transform.transpose_xy:
1167 self._current_area.x1 = (
1168 self._absolute_transform.x + self._absolute_transform.dx * self._y
1170 self._current_area.x2 = (
1171 self._absolute_transform.x
1172 + self._absolute_transform.dx * (self._y + height)
1174 if self._current_area.x2 < self._current_area.x1:
1175 self._current_area.x1, self._current_area.x2 = (
1176 self._current_area.x2,
1177 self._current_area.x1,
1180 self._current_area.y1 = (
1181 self._absolute_transform.y + self._absolute_transform.dy * self._y
1183 self._current_area.y2 = (
1184 self._absolute_transform.y
1185 + self._absolute_transform.dy * (self._y + height)
1187 if self._current_area.y2 < self._current_area.y1:
1188 self._current_area.y1, self._current_area.y2 = (
1189 self._current_area.y2,
1190 self._current_area.y1,
1193 # pylint: disable=too-many-locals
1194 def _fill_area(self, buffer):
1195 """Draw onto the image"""
1200 "RGBA", (self._width * self._tile_width, self._height * self._tile_height)
1203 tile_count_x = self._bitmap.width // self._tile_width
1207 # TODO: Fix transparency
1209 for tile_x in range(0, self._width):
1210 for tile_y in range(0, self._height):
1211 tile_index = self._tiles[tile_y * self._width + tile_x]
1212 tile_index_x = tile_index % tile_count_x
1213 tile_index_y = tile_index // tile_count_x
1214 for pixel_x in range(self._tile_width):
1215 for pixel_y in range(self._tile_height):
1216 image_x = tile_x * self._tile_width + pixel_x
1217 image_y = tile_y * self._tile_height + pixel_y
1218 bitmap_x = tile_index_x * self._tile_width + pixel_x
1219 bitmap_y = tile_index_y * self._tile_height + pixel_y
1220 pixel_color = self._pixel_shader[
1221 self._bitmap[bitmap_x, bitmap_y]
1223 image.putpixel((image_x, image_y), pixel_color["rgba"])
1225 if self._absolute_transform is not None:
1226 if self._absolute_transform.scale > 1:
1227 image = image.resize(
1229 self._pixel_width * self._absolute_transform.scale,
1230 self._pixel_height * self._absolute_transform.scale,
1232 resample=Image.NEAREST,
1234 if self._absolute_transform.mirror_x:
1235 image = image.transpose(Image.FLIP_LEFT_RIGHT)
1236 if self._absolute_transform.mirror_y:
1237 image = image.transpose(Image.FLIP_TOP_BOTTOM)
1238 if self._absolute_transform.transpose_xy:
1239 image = image.transpose(Image.TRANSPOSE)
1240 x *= self._absolute_transform.dx
1241 y *= self._absolute_transform.dy
1242 x += self._absolute_transform.x
1243 y += self._absolute_transform.y
1244 buffer.paste(image, (x, y))
1246 # pylint: enable=too-many-locals
1250 """True when the TileGrid is hidden. This may be False even
1251 when a part of a hidden Group."""
1255 def hidden(self, value):
1256 if not isinstance(value, (bool, int)):
1257 raise ValueError("Expecting a boolean or integer value")
1258 self._hidden = bool(value)
1262 """X position of the left edge in the parent."""
1267 if not isinstance(value, int):
1268 raise TypeError("X should be a integer type")
1269 if self._x != value:
1271 self._update_current_x()
1275 """Y position of the top edge in the parent."""
1280 if not isinstance(value, int):
1281 raise TypeError("Y should be a integer type")
1282 if self._y != value:
1284 self._update_current_y()
1288 """If true, the left edge rendered will be the right edge of the right-most tile."""
1292 def flip_x(self, value):
1293 if not isinstance(value, bool):
1294 raise TypeError("Flip X should be a boolean type")
1295 if self._flip_x != value:
1296 self._flip_x = value
1300 """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
1304 def flip_y(self, value):
1305 if not isinstance(value, bool):
1306 raise TypeError("Flip Y should be a boolean type")
1307 if self._flip_y != value:
1308 self._flip_y = value
1311 def transpose_xy(self):
1312 """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90
1313 degree rotation can be achieved along with the corresponding mirrored version.
1315 return self._transpose_xy
1317 @transpose_xy.setter
1318 def transpose_xy(self, value):
1319 if not isinstance(value, bool):
1320 raise TypeError("Transpose XY should be a boolean type")
1321 if self._transpose_xy != value:
1322 self._transpose_xy = value
1323 self._update_current_x()
1324 self._update_current_y()
1327 def pixel_shader(self):
1328 """The pixel shader of the tilegrid."""
1331 def __getitem__(self, index):
1332 """Returns the tile index at the given index. The index can either be
1333 an x,y tuple or an int equal to ``y * width + x``'.
1335 if isinstance(index, (tuple, list)):
1338 index = y * self._width + x
1339 elif isinstance(index, int):
1340 x = index % self._width
1341 y = index // self._width
1342 if x > self._width or y > self._height or index >= len(self._tiles):
1343 raise ValueError("Tile index out of bounds")
1344 return self._tiles[index]
1346 def __setitem__(self, index, value):
1347 """Sets the tile index at the given index. The index can either be
1348 an x,y tuple or an int equal to ``y * width + x``.
1350 if isinstance(index, (tuple, list)):
1353 index = y * self._width + x
1354 elif isinstance(index, int):
1355 x = index % self._width
1356 y = index // self._width
1357 if x > self._width or y > self._height or index >= len(self._tiles):
1358 raise ValueError("Tile index out of bounds")
1359 if not 0 <= value <= 255:
1360 raise ValueError("Tile value out of bounds")
1361 self._tiles[index] = value
1364 # pylint: enable=too-many-instance-attributes