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
43 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("RGB", (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))
945 def _make_color(self, value, transparent=False):
947 "transparent": transparent,
950 if isinstance(value, (tuple, list, bytes, bytearray)):
951 value = (value[0] & 0xFF) << 16 | (value[1] & 0xFF) << 8 | value[2] & 0xFF
952 elif isinstance(value, int):
953 if not 0 <= value <= 0xFFFFFF:
954 raise ValueError("Color must be between 0x000000 and 0xFFFFFF")
956 raise TypeError("Color buffer must be a buffer, tuple, list, or int")
957 color["rgb888"] = value
958 self._needs_refresh = True
963 """Returns the number of colors in a Palette"""
964 return len(self._colors)
966 def __setitem__(self, index, value):
967 """Sets the pixel color at the given index. The index should be
968 an integer in the range 0 to color_count-1.
970 The value argument represents a color, and can be from 0x000000 to 0xFFFFFF
971 (to represent an RGB value). Value can be an int, bytes (3 bytes (RGB) or
972 4 bytes (RGB + pad byte)), bytearray, or a tuple or list of 3 integers.
974 if self._colors[index]["rgb888"] != value:
975 self._colors[index] = self._make_color(value)
977 def __getitem__(self, index):
978 if not 0 <= index < len(self._colors):
979 raise ValueError("Palette index out of range")
980 return self._colors[index]
982 def make_transparent(self, palette_index):
983 """Set the palette index to be a transparent color"""
984 self._colors[palette_index]["transparent"] = True
986 def make_opaque(self, palette_index):
987 """Set the palette index to be an opaque color"""
988 self._colors[palette_index]["transparent"] = False
992 """Manage updating a display over 8-bit parallel bus in the background while Python code
993 runs. This protocol may be refered to as 8080-I Series Parallel Interface in datasheets.
994 It doesn’t handle display initialization.
997 def __init__(self, i2c_bus, *, device_address, reset=None):
998 """Create a ParallelBus object associated with the given pins. The
999 bus is inferred from data0 by implying the next 7 additional pins on a given GPIO
1002 The parallel bus and pins are then in use by the display until
1003 displayio.release_displays() is called even after a reload. (It does this so
1004 CircuitPython can use the display after your code is done.) So, the first time you
1005 initialize a display bus in code.py you should call
1006 :py:func`displayio.release_displays` first, otherwise it will error after the first
1012 """Performs a hardware reset via the reset pin. Raises an exception if called when
1013 no reset pin is available.
1017 def send(self, command, data):
1018 """Sends the given command value followed by the full set of data. Display state,
1019 such as vertical scroll, set via ``send`` may or may not be reset once the code is
1025 class Shape(Bitmap):
1026 """Create a Shape object with the given fixed size. Each pixel is one bit and is stored
1027 by the column boundaries of the shape on each row. Each row’s boundary defaults to the
1031 def __init__(self, width, height, *, mirror_x=False, mirror_y=False):
1032 """Create a Shape object with the given fixed size. Each pixel is one bit and is
1033 stored by the column boundaries of the shape on each row. Each row’s boundary
1034 defaults to the full row.
1036 super().__init__(width, height, 2)
1038 def set_boundary(self, y, start_x, end_x):
1039 """Loads pre-packed data into the given row."""
1043 # pylint: disable=too-many-instance-attributes
1045 """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
1046 grids can share bitmaps and pixel shaders.
1048 A single tile grid is also known as a Sprite.
1064 """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is
1065 used to convert the value and its location to a display native pixel color. This may
1066 be a simple color palette lookup, a gradient, a pattern or a color transformer.
1068 tile_width and tile_height match the height of the bitmap by default.
1070 if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
1071 raise ValueError("Unsupported Bitmap type")
1072 self._bitmap = bitmap
1073 bitmap_width = bitmap.width
1074 bitmap_height = bitmap.height
1076 if not isinstance(pixel_shader, (ColorConverter, Palette)):
1077 raise ValueError("Unsupported Pixel Shader type")
1078 self._pixel_shader = pixel_shader
1079 self._hidden = False
1082 self._width = width # Number of Tiles Wide
1083 self._height = height # Number of Tiles High
1084 self._transpose_xy = False
1085 self._flip_x = False
1086 self._flip_y = False
1087 if tile_width is None:
1088 tile_width = bitmap_width
1089 if tile_height is None:
1090 tile_height = bitmap_height
1091 if bitmap_width % tile_width != 0:
1092 raise ValueError("Tile width must exactly divide bitmap width")
1093 self._tile_width = tile_width
1094 if bitmap_height % tile_height != 0:
1095 raise ValueError("Tile height must exactly divide bitmap height")
1096 self._tile_height = tile_height
1097 if not 0 <= default_tile <= 255:
1098 raise ValueError("Default Tile is out of range")
1099 self._pixel_width = width * tile_width
1100 self._pixel_height = height * tile_height
1101 self._tiles = (self._width * self._height) * [default_tile]
1102 self.in_group = False
1103 self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
1104 self._current_area = Rectangle(0, 0, self._pixel_width, self._pixel_height)
1107 def update_transform(self, absolute_transform):
1108 """Update the parent transform and child transforms"""
1109 self._absolute_transform = absolute_transform
1110 if self._absolute_transform is not None:
1111 self._update_current_x()
1112 self._update_current_y()
1114 def _update_current_x(self):
1115 if self._transpose_xy:
1116 width = self._pixel_height
1118 width = self._pixel_width
1119 if self._absolute_transform.transpose_xy:
1120 self._current_area.y1 = (
1121 self._absolute_transform.y + self._absolute_transform.dy * self._x
1123 self._current_area.y2 = (
1124 self._absolute_transform.y
1125 + self._absolute_transform.dy * (self._x + width)
1127 if self._current_area.y2 < self._current_area.y1:
1128 self._current_area.y1, self._current_area.y2 = (
1129 self._current_area.y2,
1130 self._current_area.y1,
1133 self._current_area.x1 = (
1134 self._absolute_transform.x + self._absolute_transform.dx * self._x
1136 self._current_area.x2 = (
1137 self._absolute_transform.x
1138 + self._absolute_transform.dx * (self._x + width)
1140 if self._current_area.x2 < self._current_area.x1:
1141 self._current_area.x1, self._current_area.x2 = (
1142 self._current_area.x2,
1143 self._current_area.x1,
1146 def _update_current_y(self):
1147 if self._transpose_xy:
1148 height = self._pixel_width
1150 height = self._pixel_height
1151 if self._absolute_transform.transpose_xy:
1152 self._current_area.x1 = (
1153 self._absolute_transform.x + self._absolute_transform.dx * self._y
1155 self._current_area.x2 = (
1156 self._absolute_transform.x
1157 + self._absolute_transform.dx * (self._y + height)
1159 if self._current_area.x2 < self._current_area.x1:
1160 self._current_area.x1, self._current_area.x2 = (
1161 self._current_area.x2,
1162 self._current_area.x1,
1165 self._current_area.y1 = (
1166 self._absolute_transform.y + self._absolute_transform.dy * self._y
1168 self._current_area.y2 = (
1169 self._absolute_transform.y
1170 + self._absolute_transform.dy * (self._y + height)
1172 if self._current_area.y2 < self._current_area.y1:
1173 self._current_area.y1, self._current_area.y2 = (
1174 self._current_area.y2,
1175 self._current_area.y1,
1178 # pylint: disable=too-many-locals
1179 def _fill_area(self, buffer):
1180 """Draw onto the image"""
1186 (self._width * self._tile_width, self._height * self._tile_height),
1190 tile_count_x = self._bitmap.width // self._tile_width
1194 for tile_x in range(0, self._width):
1195 for tile_y in range(0, self._height):
1196 tile_index = self._tiles[tile_y * self._width + tile_x]
1197 tile_index_x = tile_index % tile_count_x
1198 tile_index_y = tile_index // tile_count_x
1199 for pixel_x in range(self._tile_width):
1200 for pixel_y in range(self._tile_height):
1201 image_x = tile_x * self._tile_width + pixel_x
1202 image_y = tile_y * self._tile_height + pixel_y
1203 bitmap_x = tile_index_x * self._tile_width + pixel_x
1204 bitmap_y = tile_index_y * self._tile_height + pixel_y
1205 pixel_color = self._pixel_shader[
1206 self._bitmap[bitmap_x, bitmap_y]
1208 if not pixel_color["transparent"]:
1209 image.putpixel((image_x, image_y), pixel_color["rgb888"])
1210 if self._absolute_transform is not None:
1211 if self._absolute_transform.scale > 1:
1212 image = image.resize(
1214 self._pixel_width * self._absolute_transform.scale,
1215 self._pixel_height * self._absolute_transform.scale,
1217 resample=Image.NEAREST,
1219 if self._absolute_transform.mirror_x:
1220 image = image.transpose(Image.FLIP_LEFT_RIGHT)
1221 if self._absolute_transform.mirror_y:
1222 image = image.transpose(Image.FLIP_TOP_BOTTOM)
1223 if self._absolute_transform.transpose_xy:
1224 image = image.transpose(Image.TRANSPOSE)
1225 x *= self._absolute_transform.dx
1226 y *= self._absolute_transform.dy
1227 x += self._absolute_transform.x
1228 y += self._absolute_transform.y
1229 buffer.alpha_composite(image, (x, y))
1231 # pylint: enable=too-many-locals
1235 """True when the TileGrid is hidden. This may be False even
1236 when a part of a hidden Group."""
1240 def hidden(self, value):
1241 if not isinstance(value, (bool, int)):
1242 raise ValueError("Expecting a boolean or integer value")
1243 self._hidden = bool(value)
1247 """X position of the left edge in the parent."""
1252 if not isinstance(value, int):
1253 raise TypeError("X should be a integer type")
1254 if self._x != value:
1256 self._update_current_x()
1260 """Y position of the top edge in the parent."""
1265 if not isinstance(value, int):
1266 raise TypeError("Y should be a integer type")
1267 if self._y != value:
1269 self._update_current_y()
1273 """If true, the left edge rendered will be the right edge of the right-most tile."""
1277 def flip_x(self, value):
1278 if not isinstance(value, bool):
1279 raise TypeError("Flip X should be a boolean type")
1280 if self._flip_x != value:
1281 self._flip_x = value
1285 """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
1289 def flip_y(self, value):
1290 if not isinstance(value, bool):
1291 raise TypeError("Flip Y should be a boolean type")
1292 if self._flip_y != value:
1293 self._flip_y = value
1296 def transpose_xy(self):
1297 """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90
1298 degree rotation can be achieved along with the corresponding mirrored version.
1300 return self._transpose_xy
1302 @transpose_xy.setter
1303 def transpose_xy(self, value):
1304 if not isinstance(value, bool):
1305 raise TypeError("Transpose XY should be a boolean type")
1306 if self._transpose_xy != value:
1307 self._transpose_xy = value
1308 self._update_current_x()
1309 self._update_current_y()
1312 def pixel_shader(self):
1313 """The pixel shader of the tilegrid."""
1316 def __getitem__(self, index):
1317 """Returns the tile index at the given index. The index can either be
1318 an x,y tuple or an int equal to ``y * width + x``'.
1320 if isinstance(index, (tuple, list)):
1323 index = y * self._width + x
1324 elif isinstance(index, int):
1325 x = index % self._width
1326 y = index // self._width
1327 if x > self._width or y > self._height or index >= len(self._tiles):
1328 raise ValueError("Tile index out of bounds")
1329 return self._tiles[index]
1331 def __setitem__(self, index, value):
1332 """Sets 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 if not 0 <= value <= 255:
1345 raise ValueError("Tile value out of bounds")
1346 self._tiles[index] = value
1349 # pylint: enable=too-many-instance-attributes