]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_tilegrid.py
Update pylint and re-lint
[hackapet/Adafruit_Blinka_Displayio.git] / displayio / _tilegrid.py
1 # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
2 #
3 # SPDX-License-Identifier: MIT
4
5 """
6 `displayio.tilegrid`
7 ================================================================================
8
9 displayio for Blinka
10
11 **Software and Dependencies:**
12
13 * Adafruit Blinka:
14   https://github.com/adafruit/Adafruit_Blinka/releases
15
16 * Author(s): Melissa LeBlanc-Williams
17
18 """
19
20 from typing import Union, Optional, Tuple
21 from recordclass import recordclass
22 from PIL import Image
23 from ._bitmap import Bitmap
24 from ._colorconverter import ColorConverter
25 from ._ondiskbitmap import OnDiskBitmap
26 from ._shape import Shape
27 from ._palette import Palette
28
29 __version__ = "0.0.0-auto.0"
30 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
31
32 Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
33 Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y")
34
35
36 class TileGrid:
37     # pylint: disable=too-many-instance-attributes
38     """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
39     grids can share bitmaps and pixel shaders.
40
41     A single tile grid is also known as a Sprite.
42     """
43
44     def __init__(
45         self,
46         bitmap: Union[Bitmap, OnDiskBitmap, Shape],
47         *,
48         pixel_shader: Union[ColorConverter, Palette],
49         width: int = 1,
50         height: int = 1,
51         tile_width: Optional[int] = None,
52         tile_height: Optional[int] = None,
53         default_tile: int = 0,
54         x: int = 0,
55         y: int = 0,
56     ):
57         """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is
58         used to convert the value and its location to a display native pixel color. This may
59         be a simple color palette lookup, a gradient, a pattern or a color transformer.
60
61         tile_width and tile_height match the height of the bitmap by default.
62         """
63         if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
64             raise ValueError("Unsupported Bitmap type")
65         self._bitmap = bitmap
66         bitmap_width = bitmap.width
67         bitmap_height = bitmap.height
68
69         if pixel_shader is not None and not isinstance(
70             pixel_shader, (ColorConverter, Palette)
71         ):
72             raise ValueError("Unsupported Pixel Shader type")
73         self._pixel_shader = pixel_shader
74         if isinstance(self._pixel_shader, ColorConverter):
75             self._pixel_shader._rgba = True  # pylint: disable=protected-access
76         self._hidden_tilegrid = False
77         self._x = x
78         self._y = y
79         self._width = width  # Number of Tiles Wide
80         self._height = height  # Number of Tiles High
81         self._transpose_xy = False
82         self._flip_x = False
83         self._flip_y = False
84         self._top_left_x = 0
85         self._top_left_y = 0
86         if tile_width is None or tile_width == 0:
87             tile_width = bitmap_width
88         if tile_height is None or tile_width == 0:
89             tile_height = bitmap_height
90         if tile_width < 1 or tile_height < 1:
91             raise ValueError("Tile width and height must be greater than 0")
92         if bitmap_width % tile_width != 0:
93             raise ValueError("Tile width must exactly divide bitmap width")
94         self._tile_width = tile_width
95         if bitmap_height % tile_height != 0:
96             raise ValueError("Tile height must exactly divide bitmap height")
97         self._tile_height = tile_height
98         if not 0 <= default_tile <= 255:
99             raise ValueError("Default Tile is out of range")
100         self._pixel_width = width * tile_width
101         self._pixel_height = height * tile_height
102         self._tiles = (self._width * self._height) * [default_tile]
103         self.in_group = False
104         self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
105         self._current_area = Rectangle(0, 0, self._pixel_width, self._pixel_height)
106         self._moved = False
107
108     def _update_transform(self, absolute_transform):
109         """Update the parent transform and child transforms"""
110         self._absolute_transform = absolute_transform
111         if self._absolute_transform is not None:
112             self._update_current_x()
113             self._update_current_y()
114
115     def _update_current_x(self):
116         if self._transpose_xy:
117             width = self._pixel_height
118         else:
119             width = self._pixel_width
120         if self._absolute_transform.transpose_xy:
121             self._current_area.y1 = (
122                 self._absolute_transform.y + self._absolute_transform.dy * self._x
123             )
124             self._current_area.y2 = (
125                 self._absolute_transform.y
126                 + self._absolute_transform.dy * (self._x + width)
127             )
128             if self._current_area.y2 < self._current_area.y1:
129                 self._current_area.y1, self._current_area.y2 = (
130                     self._current_area.y2,
131                     self._current_area.y1,
132                 )
133         else:
134             self._current_area.x1 = (
135                 self._absolute_transform.x + self._absolute_transform.dx * self._x
136             )
137             self._current_area.x2 = (
138                 self._absolute_transform.x
139                 + self._absolute_transform.dx * (self._x + width)
140             )
141             if self._current_area.x2 < self._current_area.x1:
142                 self._current_area.x1, self._current_area.x2 = (
143                     self._current_area.x2,
144                     self._current_area.x1,
145                 )
146
147     def _update_current_y(self):
148         if self._transpose_xy:
149             height = self._pixel_width
150         else:
151             height = self._pixel_height
152         if self._absolute_transform.transpose_xy:
153             self._current_area.x1 = (
154                 self._absolute_transform.x + self._absolute_transform.dx * self._y
155             )
156             self._current_area.x2 = (
157                 self._absolute_transform.x
158                 + self._absolute_transform.dx * (self._y + height)
159             )
160             if self._current_area.x2 < self._current_area.x1:
161                 self._current_area.x1, self._current_area.x2 = (
162                     self._current_area.x2,
163                     self._current_area.x1,
164                 )
165         else:
166             self._current_area.y1 = (
167                 self._absolute_transform.y + self._absolute_transform.dy * self._y
168             )
169             self._current_area.y2 = (
170                 self._absolute_transform.y
171                 + self._absolute_transform.dy * (self._y + height)
172             )
173             if self._current_area.y2 < self._current_area.y1:
174                 self._current_area.y1, self._current_area.y2 = (
175                     self._current_area.y2,
176                     self._current_area.y1,
177                 )
178
179     def _shade(self, pixel_value):
180         if isinstance(self._pixel_shader, Palette):
181             return self._pixel_shader[pixel_value]
182         if isinstance(self._pixel_shader, ColorConverter):
183             return self._pixel_shader.convert(pixel_value)
184         return pixel_value
185
186     def _apply_palette(self, image):
187         image.putpalette(
188             self._pixel_shader._get_palette()  # pylint: disable=protected-access
189         )
190
191     def _add_alpha(self, image):
192         alpha = self._bitmap._image.copy().convert(  # pylint: disable=protected-access
193             "P"
194         )
195         alpha.putpalette(
196             self._pixel_shader._get_alpha_palette()  # pylint: disable=protected-access
197         )
198         image.putalpha(alpha.convert("L"))
199
200     def _fill_area(self, buffer):
201         # pylint: disable=too-many-locals,too-many-branches,too-many-statements
202         """Draw onto the image"""
203         if self._hidden_tilegrid:
204             return
205
206         if self._bitmap.width <= 0 or self._bitmap.height <= 0:
207             return
208
209         # Copy class variables to local variables in case something changes
210         x = self._x
211         y = self._y
212         width = self._width
213         height = self._height
214         tile_width = self._tile_width
215         tile_height = self._tile_height
216         bitmap_width = self._bitmap.width
217         pixel_width = self._pixel_width
218         pixel_height = self._pixel_height
219         tiles = self._tiles
220         absolute_transform = self._absolute_transform
221         pixel_shader = self._pixel_shader
222         bitmap = self._bitmap
223         tiles = self._tiles
224
225         tile_count_x = bitmap_width // tile_width
226
227         image = Image.new(
228             "RGBA",
229             (width * tile_width, height * tile_height),
230             (0, 0, 0, 0),
231         )
232
233         for tile_x in range(width):
234             for tile_y in range(height):
235                 tile_index = tiles[tile_y * width + tile_x]
236                 tile_index_x = tile_index % tile_count_x
237                 tile_index_y = tile_index // tile_count_x
238                 tile_image = bitmap._image  # pylint: disable=protected-access
239                 if isinstance(pixel_shader, Palette):
240                     tile_image = tile_image.copy().convert("P")
241                     self._apply_palette(tile_image)
242                     tile_image = tile_image.convert("RGBA")
243                     self._add_alpha(tile_image)
244                 elif isinstance(pixel_shader, ColorConverter):
245                     # This will be needed for eInks, grayscale, and monochrome displays
246                     pass
247                 image.alpha_composite(
248                     tile_image,
249                     dest=(tile_x * tile_width, tile_y * tile_height),
250                     source=(
251                         tile_index_x * tile_width,
252                         tile_index_y * tile_height,
253                         tile_index_x * tile_width + tile_width,
254                         tile_index_y * tile_height + tile_height,
255                     ),
256                 )
257
258         if absolute_transform is not None:
259             if absolute_transform.scale > 1:
260                 image = image.resize(
261                     (
262                         int(pixel_width * absolute_transform.scale),
263                         int(
264                             pixel_height * absolute_transform.scale,
265                         ),
266                     ),
267                     resample=Image.NEAREST,
268                 )
269             if absolute_transform.mirror_x:
270                 image = image.transpose(Image.FLIP_LEFT_RIGHT)
271             if absolute_transform.mirror_y:
272                 image = image.transpose(Image.FLIP_TOP_BOTTOM)
273             if absolute_transform.transpose_xy:
274                 image = image.transpose(Image.TRANSPOSE)
275             x *= absolute_transform.dx
276             y *= absolute_transform.dy
277             x += absolute_transform.x
278             y += absolute_transform.y
279
280         source_x = source_y = 0
281         if x < 0:
282             source_x = round(0 - x)
283             x = 0
284         if y < 0:
285             source_y = round(0 - y)
286             y = 0
287
288         x = round(x)
289         y = round(y)
290
291         if (
292             x <= buffer.width
293             and y <= buffer.height
294             and source_x <= image.width
295             and source_y <= image.height
296         ):
297             buffer.alpha_composite(image, (x, y), source=(source_x, source_y))
298
299     @property
300     def hidden(self) -> bool:
301         """True when the TileGrid is hidden. This may be False even
302         when a part of a hidden Group."""
303         return self._hidden_tilegrid
304
305     @hidden.setter
306     def hidden(self, value: bool):
307         if not isinstance(value, (bool, int)):
308             raise ValueError("Expecting a boolean or integer value")
309         self._hidden_tilegrid = bool(value)
310
311     @property
312     def x(self) -> int:
313         """X position of the left edge in the parent."""
314         return self._x
315
316     @x.setter
317     def x(self, value: int):
318         if not isinstance(value, int):
319             raise TypeError("X should be a integer type")
320         if self._x != value:
321             self._x = value
322             self._update_current_x()
323
324     @property
325     def y(self) -> int:
326         """Y position of the top edge in the parent."""
327         return self._y
328
329     @y.setter
330     def y(self, value: int):
331         if not isinstance(value, int):
332             raise TypeError("Y should be a integer type")
333         if self._y != value:
334             self._y = value
335             self._update_current_y()
336
337     @property
338     def flip_x(self) -> bool:
339         """If true, the left edge rendered will be the right edge of the right-most tile."""
340         return self._flip_x
341
342     @flip_x.setter
343     def flip_x(self, value: bool):
344         if not isinstance(value, bool):
345             raise TypeError("Flip X should be a boolean type")
346         if self._flip_x != value:
347             self._flip_x = value
348
349     @property
350     def flip_y(self) -> bool:
351         """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
352         return self._flip_y
353
354     @flip_y.setter
355     def flip_y(self, value: bool):
356         if not isinstance(value, bool):
357             raise TypeError("Flip Y should be a boolean type")
358         if self._flip_y != value:
359             self._flip_y = value
360
361     @property
362     def transpose_xy(self) -> bool:
363         """If true, the TileGrid's axis will be swapped. When combined with mirroring, any 90
364         degree rotation can be achieved along with the corresponding mirrored version.
365         """
366         return self._transpose_xy
367
368     @transpose_xy.setter
369     def transpose_xy(self, value: bool):
370         if not isinstance(value, bool):
371             raise TypeError("Transpose XY should be a boolean type")
372         if self._transpose_xy != value:
373             self._transpose_xy = value
374             self._update_current_x()
375             self._update_current_y()
376
377     @property
378     def pixel_shader(self) -> Union[ColorConverter, Palette]:
379         """The pixel shader of the tilegrid."""
380         return self._pixel_shader
381
382     def _extract_and_check_index(self, index):
383         if isinstance(index, (tuple, list)):
384             x = index[0]
385             y = index[1]
386             index = y * self._width + x
387         elif isinstance(index, int):
388             x = index % self._width
389             y = index // self._width
390         if x > self._width or y > self._height or index >= len(self._tiles):
391             raise ValueError("Tile index out of bounds")
392         return index
393
394     def __getitem__(self, index: Union[Tuple[int, int], int]) -> int:
395         """Returns the tile index at the given index. The index can either be
396         an x,y tuple or an int equal to ``y * width + x``'.
397         """
398         index = self._extract_and_check_index(index)
399         return self._tiles[index]
400
401     def __setitem__(self, index: Union[Tuple[int, int], int], value: int) -> None:
402         """Sets the tile index at the given index. The index can either be
403         an x,y tuple or an int equal to ``y * width + x``.
404         """
405         index = self._extract_and_check_index(index)
406         if not 0 <= value <= 255:
407             raise ValueError("Tile value out of bounds")
408         self._tiles[index] = value