]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio.py
c6528c1d65b64599aeafd0f99b6a93975d6a4785
[hackapet/Adafruit_Blinka_Displayio.git] / displayio.py
1 # The MIT License (MIT)
2 #
3 # Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
4 #
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:
11 #
12 # The above copyright notice and this permission notice shall be included in
13 # all copies or substantial portions of the Software.
14 #
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
21 # THE SOFTWARE.
22
23 """
24 `displayio`
25 ================================================================================
26
27 displayio for Blinka
28
29 **Software and Dependencies:**
30
31 * Adafruit Blinka:
32   https://github.com/adafruit/Adafruit_Blinka/releases
33
34 * Author(s): Melissa LeBlanc-Williams
35
36 """
37
38 import time
39 import struct
40 import threading
41 import digitalio
42 from PIL import Image
43 from recordclass import recordclass
44 import numpy
45
46 __version__ = "0.0.0-auto.0"
47 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
48
49 # pylint: disable=unnecessary-pass, unused-argument, too-many-lines
50
51 _displays = []
52
53 Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
54 Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y")
55
56
57 def release_displays():
58     """Releases any actively used displays so their busses and pins can be used again.
59
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.
62     """
63     for _disp in _displays:
64         _disp._release()  # pylint: disable=protected-access
65     _displays.clear()
66
67
68 class Bitmap:
69     """Stores values of a certain size in a 2D array"""
70
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
75         the Bitmap.
76         """
77         self._width = width
78         self._height = height
79         self._read_only = False
80
81         if value_count < 0:
82             raise ValueError("value_count must be > 0")
83
84         bits = 1
85         while (value_count - 1) >> bits:
86             if bits < 8:
87                 bits = bits << 1
88             else:
89                 bits += 8
90
91         self._bits_per_value = bits
92
93         if (
94             self._bits_per_value > 8
95             and self._bits_per_value != 16
96             and self._bits_per_value != 32
97         ):
98             raise NotImplementedError("Invalid bits per value")
99
100         self._data = (width * height) * [0]
101         self._dirty_area = Rectangle(0, 0, width, height)
102
103     def __getitem__(self, index):
104         """
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`.
107         """
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]
113
114     def __setitem__(self, index, value):
115         """
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`.
118         """
119         if self._read_only:
120             raise RuntimeError("Read-only object")
121         if isinstance(index, (tuple, list)):
122             x = index[0]
123             y = index[1]
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
134         else:
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
143
144     def _finish_refresh(self):
145         self._dirty_area.x1 = 0
146         self._dirty_area.x2 = 0
147
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)
152
153     @property
154     def width(self):
155         """Width of the bitmap. (read only)"""
156         return self._width
157
158     @property
159     def height(self):
160         """Height of the bitmap. (read only)"""
161         return self._height
162
163
164 class ColorConverter:
165     """Converts one color format to another. Color converter based on original displayio
166     code for consistency.
167     """
168
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
173         """
174         self._dither = dither
175         self._depth = 16
176
177     # pylint: disable=no-self-use
178     def _compute_rgb565(self, color):
179         self._depth = 16
180         return (color >> 19) << 11 | ((color >> 10) & 0x3F) << 5 | (color >> 3) & 0x1F
181
182     def _compute_luma(self, color):
183         red = color >> 16
184         green = (color >> 8) & 0xFF
185         blue = color & 0xFF
186         return (red * 19) / 255 + (green * 182) / 255 + (blue + 54) / 255
187
188     def _compute_chroma(self, color):
189         red = color >> 16
190         green = (color >> 8) & 0xFF
191         blue = color & 0xFF
192         return max(red, green, blue) - min(red, green, blue)
193
194     def _compute_hue(self, color):
195         red = color >> 16
196         green = (color >> 8) & 0xFF
197         blue = color & 0xFF
198         max_color = max(red, green, blue)
199         chroma = self._compute_chroma(color)
200         if chroma == 0:
201             return 0
202         hue = 0
203         if max_color == red:
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
209         if hue < 0:
210             hue += 240
211
212         return hue
213
214     def _dither_noise_1(self, noise):
215         noise = (noise >> 13) ^ noise
216         more_noise = (
217             noise * (noise * noise * 60493 + 19990303) + 1376312589
218         ) & 0x7FFFFFFF
219         return (more_noise / (1073741824.0 * 2)) * 255
220
221     def _dither_noise_2(self, x, y):
222         return self._dither_noise_1(x + y * 0xFFFF)
223
224     def _compute_tricolor(self):
225         pass
226
227     def convert(self, color):
228         "Converts the given rgb888 color to RGB565"
229         if self._dither:
230             return color  # To Do: return a dithered color
231         return self._compute_rgb565(color)
232
233     # pylint: enable=no-self-use
234
235     @property
236     def dither(self):
237         """When true the color converter dithers the output by adding
238         random noise when truncating to display bitdepth
239         """
240         return self._dither
241
242     @dither.setter
243     def dither(self, value):
244         if not isinstance(value, bool):
245             raise ValueError("Value should be boolean")
246         self._dither = value
247
248
249 # pylint: disable=too-many-instance-attributes
250 class Display:
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.
254
255     Most people should not use this class directly. Use a specific display driver instead
256     that will contain the initialization sequence at minimum.
257
258     .. class::
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)
265     """
266
267     # pylint: disable=too-many-locals
268     def __init__(
269         self,
270         display_bus,
271         init_sequence,
272         *,
273         width,
274         height,
275         colstart=0,
276         rowstart=0,
277         rotation=0,
278         color_depth=16,
279         grayscale=False,
280         pixels_in_byte_share_row=True,
281         bytes_per_cell=1,
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,
287         backlight_pin=None,
288         brightness_command=None,
289         brightness=1.0,
290         auto_brightness=False,
291         single_byte_bounds=False,
292         data_as_commands=False,
293         auto_refresh=True,
294         native_frames_per_second=60
295     ):
296         """Create a Display object on the given display bus (`displayio.FourWire` or
297         `displayio.ParallelBus`).
298
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
305         ILI9341 init code:
306         .. code-block:: python
307
308             init_sequence = (
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)
312             )
313             display = displayio.Display(display_bus, init_sequence, width=320, height=240)
314
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.
319
320         The initialization sequence should always leave the display memory access inline with
321         the scan of the display to minimize tearing artifacts.
322         """
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
330         self._width = width
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
347
348     # pylint: enable=too-many-locals
349
350     def _initialize(self, init_sequence):
351         i = 0
352         while i < len(init_sequence):
353             command = init_sequence[i]
354             data_size = init_sequence[i + 1]
355             delay = (data_size & 0x80) > 0
356             data_size &= ~0x80
357             self._write(command, init_sequence[i + 2 : i + 2 + data_size])
358             delay_time_ms = 10
359             if delay:
360                 data_size += 1
361                 delay_time_ms = init_sequence[i + 1 + data_size]
362                 if delay_time_ms == 255:
363                     delay_time_ms = 500
364             time.sleep(delay_time_ms / 1000)
365             i += 2 + data_size
366
367     def _write(self, command, data):
368         if self._single_byte_bounds:
369             self._bus.send(True, bytes([command]) + data, toggle_every_byte=True)
370         else:
371             self._bus.send(True, bytes([command]), toggle_every_byte=True)
372             self._bus.send(False, data)
373
374     def _release(self):
375         self._bus.release()
376         self._bus = None
377
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.
381         """
382         self._current_group = group
383
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.
389
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.
392
393         When auto refresh is on, updates the display immediately. (The display will also
394         update without calls to this.)
395         """
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)
403         time.sleep(1)
404         # Eventually calculate dirty rectangles here
405         self._subrectangles.append(Rectangle(0, 0, self._width, self._height))
406
407         for area in self._subrectangles:
408             self._refresh_display_area(area)
409
410     def _refresh_loop(self):
411         while self._auto_refresh:
412             self.refresh()
413
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")
417         color = (
418             ((data[:, :, 0] & 0xF8) << 8)
419             | ((data[:, :, 1] & 0xFC) << 3)
420             | (data[:, :, 2] >> 3)
421         )
422
423         pixels = list(
424             numpy.dstack(((color >> 8) & 0xFF, color & 0xFF)).flatten().tolist()
425         )
426
427         self._write(
428             self._set_column_command,
429             self._encode_pos(
430                 rectangle.x1 + self._colstart, rectangle.x2 + self._colstart
431             ),
432         )
433         self._write(
434             self._set_row_command,
435             self._encode_pos(
436                 rectangle.y1 + self._rowstart, rectangle.y2 + self._rowstart
437             ),
438         )
439         self._write(self._write_ram_command, pixels)
440
441     def _encode_pos(self, x, y):
442         """Encode a postion into bytes."""
443         return struct.pack(self._bounds_encoding, x, y)
444
445     def fill_row(self, y, buffer):
446         """Extract the pixels from a single row"""
447         pass
448
449     @property
450     def auto_refresh(self):
451         """True when the display is refreshed automatically."""
452         return self._auto_refresh
453
454     @auto_refresh.setter
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
460             )
461         if value and not self._refresh_thread.is_alive():
462             # Start the thread
463             self._refresh_thread.start()
464         elif not value and self._refresh_thread.is_alive():
465             # Stop the thread
466             self._refresh_thread.join()
467
468     @property
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.
473         """
474         return self._brightness
475
476     @brightness.setter
477     def brightness(self, value):
478         self._brightness = value
479
480     @property
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.
486         """
487         return self._auto_brightness
488
489     @auto_brightness.setter
490     def auto_brightness(self, value):
491         self._auto_brightness = value
492
493     @property
494     def width(self):
495         """Display Width"""
496         return self._width
497
498     @property
499     def height(self):
500         """Display Height"""
501         return self._height
502
503     @property
504     def rotation(self):
505         """The rotation of the display as an int in degrees."""
506         return self._rotation
507
508     @rotation.setter
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
513
514     @property
515     def bus(self):
516         """Current Display Bus"""
517         return self._bus
518
519
520 # pylint: enable=too-many-instance-attributes
521
522
523 class EPaperDisplay:
524     """Manage updating an epaper display over a display bus
525
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
529     the display itself.
530
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.
533     """
534
535     # pylint: disable=too-many-locals
536     def __init__(
537         self,
538         display_bus,
539         start_sequence,
540         stop_sequence,
541         *,
542         width,
543         height,
544         ram_width,
545         ram_height,
546         colstart=0,
547         rowstart=0,
548         rotation=0,
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,
558         refresh_time=40,
559         busy_pin=None,
560         busy_state=True,
561         seconds_per_frame=180,
562         always_toggle_chip_select=False
563     ):
564         """
565         Create a EPaperDisplay object on the given display bus (displayio.FourWire or
566         displayio.ParallelBus).
567
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.
574         """
575         pass
576
577     # pylint: enable=too-many-locals
578
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).
582         """
583         pass
584
585     def refresh(self):
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.
588         """
589         pass
590
591     @property
592     def time_to_refresh(self):
593         """Time, in fractional seconds, until the ePaper display can be refreshed."""
594         return 0
595
596     @property
597     def width(self):
598         """Display Width"""
599         pass
600
601     @property
602     def height(self):
603         """Display Height"""
604         pass
605
606     @property
607     def bus(self):
608         """Current Display Bus"""
609         pass
610
611
612 class FourWire:
613     """Manage updating a display over SPI four wire protocol in the background while
614     Python code runs. It doesn’t handle display initialization.
615     """
616
617     def __init__(
618         self,
619         spi_bus,
620         *,
621         command,
622         chip_select,
623         reset=None,
624         baudrate=24000000,
625         polarity=0,
626         phase=0
627     ):
628         """Create a FourWire object associated with the given pins.
629
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
635         first code.py run.
636         """
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)
641
642         if reset is not None:
643             self._reset = digitalio.DigitalInOut(reset)
644             self._reset.switch_to_output(value=True)
645         else:
646             self._reset = None
647         self._spi = spi_bus
648         while self._spi.try_lock():
649             pass
650         self._spi.configure(baudrate=baudrate, polarity=polarity, phase=phase)
651         self._spi.unlock()
652
653     def _release(self):
654         self.reset()
655         self._spi.deinit()
656         self._dc.deinit()
657         self._chip_select.deinit()
658         if self._reset is not None:
659             self._reset.deinit()
660
661     def reset(self):
662         """Performs a hardware reset via the reset pin.
663         Raises an exception if called when no reset pin is available.
664         """
665         if self._reset is not None:
666             self._reset.value = False
667             time.sleep(0.001)
668             self._reset.value = True
669             time.sleep(0.001)
670         else:
671             raise RuntimeError("No reset pin defined")
672
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
676         done.
677         """
678         while self._spi.try_lock():
679             pass
680         self._dc.value = not is_command
681         if toggle_every_byte:
682             for byte in data:
683                 self._spi.write(bytes([byte]))
684                 self._chip_select.value = True
685                 time.sleep(0.000001)
686                 self._chip_select.value = False
687         else:
688             self._spi.write(data)
689         self._spi.unlock()
690
691
692 class Group:
693     """Manage a group of sprites and groups and how they are inter-related."""
694
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.
699         """
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")
705         self._scale = scale
706         self._x = x
707         self._y = y
708         self._hidden = False
709         self._layers = []
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)
714
715     def update_transform(self, parent_transform):
716         """Update the parent transform and child transforms"""
717         self.in_group = parent_transform is not None
718         if self.in_group:
719             x = self._x
720             y = self._y
721             if parent_transform.transpose_xy:
722                 x, y = y, x
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()
732
733     def _update_child_transforms(self):
734         if self.in_group:
735             for layer in self._layers:
736                 layer.update_transform(self._absolute_transform)
737
738     def _removal_cleanup(self, index):
739         layer = self._layers[index]
740         layer.update_transform(None)
741
742     def _layer_update(self, index):
743         layer = self._layers[index]
744         layer.update_transform(self._absolute_transform)
745
746     def append(self, layer):
747         """Append a layer to the group. It will be drawn
748         above other layers.
749         """
750         self.insert(len(self._layers), layer)
751
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")
756         if layer.in_group:
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)
762
763     def index(self, layer):
764         """Returns the index of the first copy of layer.
765         Raises ValueError if not found.
766         """
767         return self._layers.index(layer)
768
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)
773
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)
779
780     def __len__(self):
781         """Returns the number of layers in a Group"""
782         return len(self._layers)
783
784     def __getitem__(self, index):
785         """Returns the value at the given index."""
786         return self._layers[index]
787
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)
793
794     def __delitem__(self, index):
795         """Deletes the value at the given index."""
796         del self._layers[index]
797
798     def _fill_area(self, buffer):
799         if self._hidden:
800             return
801
802         for layer in self._layers:
803             if isinstance(layer, (Group, TileGrid)):
804                 layer._fill_area(buffer)  # pylint: disable=protected-access
805
806     @property
807     def hidden(self):
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.
810         """
811         return self._hidden
812
813     @hidden.setter
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)
818
819     @property
820     def scale(self):
821         """Scales each pixel within the Group in both directions. For example, when
822         scale=2 each pixel will be represented by 2x2 pixels.
823         """
824         return self._scale
825
826     @scale.setter
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
834             )
835             self._absolute_transform.dy = (
836                 self._absolute_transform.dy / self._scale * value
837             )
838             self._absolute_transform.scale = parent_scale * value
839
840             self._scale = value
841             self._update_child_transforms()
842
843     @property
844     def x(self):
845         """X position of the Group in the parent."""
846         return self._x
847
848     @x.setter
849     def x(self, value):
850         if not isinstance(value, int):
851             raise ValueError("x must be an integer")
852         if self._x != value:
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)
856             else:
857                 dx_value = self._absolute_transform.dx / self._scale
858                 self._absolute_transform.x += dx_value * (value - self._x)
859             self._x = value
860             self._update_child_transforms()
861
862     @property
863     def y(self):
864         """Y position of the Group in the parent."""
865         return self._y
866
867     @y.setter
868     def y(self, value):
869         if not isinstance(value, int):
870             raise ValueError("y must be an integer")
871         if self._y != value:
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)
875             else:
876                 dy_value = self._absolute_transform.dy / self._scale
877                 self._absolute_transform.y += dy_value * (value - self._y)
878             self._y = value
879             self._update_child_transforms()
880
881
882 class I2CDisplay:
883     """Manage updating a display over I2C in the background while Python code runs.
884     It doesn’t handle display initialization.
885     """
886
887     def __init__(self, i2c_bus, *, device_address, reset=None):
888         """Create a I2CDisplay object associated with the given I2C bus and reset pin.
889
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
894         code.py run.
895         """
896         pass
897
898     def reset(self):
899         """Performs a hardware reset via the reset pin. Raises an exception if called
900         when no reset pin is available.
901         """
902         pass
903
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
907         done.
908         """
909         pass
910
911
912 class OnDiskBitmap:
913     """
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
916     image is visible."""
917
918     def __init__(self, file):
919         self._image = Image.open(file)
920
921     @property
922     def width(self):
923         """Width of the bitmap. (read only)"""
924         return self._image.width
925
926     @property
927     def height(self):
928         """Height of the bitmap. (read only)"""
929         return self._image.height
930
931
932 class Palette:
933     """Map a pixel palette_index to a full color. Colors are transformed to the display’s
934     format internally to save memory.
935     """
936
937     def __init__(self, color_count):
938         """Create a Palette object to store a set number of colors."""
939         self._needs_refresh = False
940
941         self._colors = []
942         for _ in range(color_count):
943             self._colors.append(self._make_color(0))
944
945     def _make_color(self, value, transparent=False):
946         color = {
947             "transparent": transparent,
948             "rgb888": 0,
949         }
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")
955         else:
956             raise TypeError("Color buffer must be a buffer, tuple, list, or int")
957         color["rgb888"] = value
958         self._needs_refresh = True
959
960         return color
961
962     def __len__(self):
963         """Returns the number of colors in a Palette"""
964         return len(self._colors)
965
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.
969
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.
973         """
974         if self._colors[index]["rgb888"] != value:
975             self._colors[index] = self._make_color(value)
976
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]
981
982     def make_transparent(self, palette_index):
983         """Set the palette index to be a transparent color"""
984         self._colors[palette_index]["transparent"] = True
985
986     def make_opaque(self, palette_index):
987         """Set the palette index to be an opaque color"""
988         self._colors[palette_index]["transparent"] = False
989
990
991 class ParallelBus:
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.
995     """
996
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
1000         port.
1001
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
1007         code.py run.
1008         """
1009         pass
1010
1011     def reset(self):
1012         """Performs a hardware reset via the reset pin. Raises an exception if called when
1013         no reset pin is available.
1014         """
1015         pass
1016
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
1020         done.
1021         """
1022         pass
1023
1024
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
1028     full row.
1029     """
1030
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.
1035         """
1036         super().__init__(width, height, 2)
1037
1038     def set_boundary(self, y, start_x, end_x):
1039         """Loads pre-packed data into the given row."""
1040         pass
1041
1042
1043 # pylint: disable=too-many-instance-attributes
1044 class TileGrid:
1045     """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
1046     grids can share bitmaps and pixel shaders.
1047
1048     A single tile grid is also known as a Sprite.
1049     """
1050
1051     def __init__(
1052         self,
1053         bitmap,
1054         *,
1055         pixel_shader,
1056         width=1,
1057         height=1,
1058         tile_width=None,
1059         tile_height=None,
1060         default_tile=0,
1061         x=0,
1062         y=0
1063     ):
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.
1067
1068         tile_width and tile_height match the height of the bitmap by default.
1069         """
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
1075
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
1080         self._x = x
1081         self._y = y
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)
1105         self._moved = False
1106
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()
1113
1114     def _update_current_x(self):
1115         if self._transpose_xy:
1116             width = self._pixel_height
1117         else:
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
1122             )
1123             self._current_area.y2 = (
1124                 self._absolute_transform.y
1125                 + self._absolute_transform.dy * (self._x + width)
1126             )
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,
1131                 )
1132         else:
1133             self._current_area.x1 = (
1134                 self._absolute_transform.x + self._absolute_transform.dx * self._x
1135             )
1136             self._current_area.x2 = (
1137                 self._absolute_transform.x
1138                 + self._absolute_transform.dx * (self._x + width)
1139             )
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,
1144                 )
1145
1146     def _update_current_y(self):
1147         if self._transpose_xy:
1148             height = self._pixel_width
1149         else:
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
1154             )
1155             self._current_area.x2 = (
1156                 self._absolute_transform.x
1157                 + self._absolute_transform.dx * (self._y + height)
1158             )
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,
1163                 )
1164         else:
1165             self._current_area.y1 = (
1166                 self._absolute_transform.y + self._absolute_transform.dy * self._y
1167             )
1168             self._current_area.y2 = (
1169                 self._absolute_transform.y
1170                 + self._absolute_transform.dy * (self._y + height)
1171             )
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,
1176                 )
1177
1178     # pylint: disable=too-many-locals
1179     def _fill_area(self, buffer):
1180         """Draw onto the image"""
1181         if self._hidden:
1182             return
1183
1184         image = Image.new(
1185             "RGBA",
1186             (self._width * self._tile_width, self._height * self._tile_height),
1187             (0, 0, 0, 0),
1188         )
1189
1190         tile_count_x = self._bitmap.width // self._tile_width
1191         x = self._x
1192         y = self._y
1193
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]
1207                         ]
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(
1213                     (
1214                         self._pixel_width * self._absolute_transform.scale,
1215                         self._pixel_height * self._absolute_transform.scale,
1216                     ),
1217                     resample=Image.NEAREST,
1218                 )
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))
1230
1231     # pylint: enable=too-many-locals
1232
1233     @property
1234     def hidden(self):
1235         """True when the TileGrid is hidden. This may be False even
1236         when a part of a hidden Group."""
1237         return self._hidden
1238
1239     @hidden.setter
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)
1244
1245     @property
1246     def x(self):
1247         """X position of the left edge in the parent."""
1248         return self._x
1249
1250     @x.setter
1251     def x(self, value):
1252         if not isinstance(value, int):
1253             raise TypeError("X should be a integer type")
1254         if self._x != value:
1255             self._x = value
1256             self._update_current_x()
1257
1258     @property
1259     def y(self):
1260         """Y position of the top edge in the parent."""
1261         return self._y
1262
1263     @y.setter
1264     def y(self, value):
1265         if not isinstance(value, int):
1266             raise TypeError("Y should be a integer type")
1267         if self._y != value:
1268             self._y = value
1269             self._update_current_y()
1270
1271     @property
1272     def flip_x(self):
1273         """If true, the left edge rendered will be the right edge of the right-most tile."""
1274         return self._flip_x
1275
1276     @flip_x.setter
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
1282
1283     @property
1284     def flip_y(self):
1285         """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
1286         return self._flip_y
1287
1288     @flip_y.setter
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
1294
1295     @property
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.
1299         """
1300         return self._transpose_xy
1301
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()
1310
1311     @property
1312     def pixel_shader(self):
1313         """The pixel shader of the tilegrid."""
1314         pass
1315
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``'.
1319         """
1320         if isinstance(index, (tuple, list)):
1321             x = index[0]
1322             y = index[1]
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]
1330
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``.
1334         """
1335         if isinstance(index, (tuple, list)):
1336             x = index[0]
1337             y = index[1]
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
1347
1348
1349 # pylint: enable=too-many-instance-attributes