]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/_tilegrid.py
More displayio code updates
[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 import struct
21 from typing import Union, Optional, Tuple
22 from ._bitmap import Bitmap
23 from ._colorconverter import ColorConverter
24 from ._ondiskbitmap import OnDiskBitmap
25 from ._shape import Shape
26 from ._palette import Palette
27 from ._structs import TransformStruct, InputPixelStruct, OutputPixelStruct
28 from ._colorspace import Colorspace
29 from ._area import Area
30
31 __version__ = "0.0.0+auto.0"
32 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
33
34
35 class TileGrid:
36     # pylint: disable=too-many-instance-attributes
37     """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
38     grids can share bitmaps and pixel shaders.
39
40     A single tile grid is also known as a Sprite.
41     """
42
43     def __init__(
44         self,
45         bitmap: Union[Bitmap, OnDiskBitmap, Shape],
46         *,
47         pixel_shader: Union[ColorConverter, Palette],
48         width: int = 1,
49         height: int = 1,
50         tile_width: Optional[int] = None,
51         tile_height: Optional[int] = None,
52         default_tile: int = 0,
53         x: int = 0,
54         y: int = 0,
55     ):
56         """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is
57         used to convert the value and its location to a display native pixel color. This may
58         be a simple color palette lookup, a gradient, a pattern or a color transformer.
59
60         tile_width and tile_height match the height of the bitmap by default.
61         """
62         if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
63             raise ValueError("Unsupported Bitmap type")
64         self._bitmap = bitmap
65         bitmap_width = bitmap.width
66         bitmap_height = bitmap.height
67
68         if pixel_shader is not None and not isinstance(
69             pixel_shader, (ColorConverter, Palette)
70         ):
71             raise ValueError("Unsupported Pixel Shader type")
72         self._pixel_shader = pixel_shader
73         if isinstance(self._pixel_shader, ColorConverter):
74             self._pixel_shader._rgba = True  # pylint: disable=protected-access
75         self._hidden_tilegrid = False
76         self._x = x
77         self._y = y
78         self._width_in_tiles = width
79         self._height_in_tiles = height
80         self._transpose_xy = False
81         self._flip_x = False
82         self._flip_y = False
83         self._top_left_x = 0
84         self._top_left_y = 0
85         if tile_width is None or tile_width == 0:
86             tile_width = bitmap_width
87         if tile_height is None or tile_width == 0:
88             tile_height = bitmap_height
89         if tile_width < 1 or tile_height < 1:
90             raise ValueError("Tile width and height must be greater than 0")
91         if bitmap_width % tile_width != 0:
92             raise ValueError("Tile width must exactly divide bitmap width")
93         self._tile_width = tile_width
94         if bitmap_height % tile_height != 0:
95             raise ValueError("Tile height must exactly divide bitmap height")
96         self._tile_height = tile_height
97         if not 0 <= default_tile <= 255:
98             raise ValueError("Default Tile is out of range")
99         self._pixel_width = width * tile_width
100         self._pixel_height = height * tile_height
101         self._tiles = (self._width_in_tiles * self._height_in_tiles) * [default_tile]
102         self._in_group = False
103         self._absolute_transform = TransformStruct(0, 0, 1, 1, 1, False, False, False)
104         self._current_area = Area(0, 0, self._pixel_width, self._pixel_height)
105         self._moved = False
106         self._bitmap_width_in_tiles = bitmap_width // tile_width
107         self._tiles_in_bitmap = self._bitmap_width_in_tiles * (
108             bitmap_height // tile_height
109         )
110         self.inline_tiles = False  # We have plenty of memory
111
112     def _update_transform(self, absolute_transform):
113         """Update the parent transform and child transforms"""
114         self._absolute_transform = absolute_transform
115         if self._absolute_transform is not None:
116             self._update_current_x()
117             self._update_current_y()
118
119     def _update_current_x(self):
120         if self._transpose_xy:
121             width = self._pixel_height
122         else:
123             width = self._pixel_width
124         if self._absolute_transform.transpose_xy:
125             self._current_area.y1 = (
126                 self._absolute_transform.y + self._absolute_transform.dy * self._x
127             )
128             self._current_area.y2 = (
129                 self._absolute_transform.y
130                 + self._absolute_transform.dy * (self._x + width)
131             )
132             if self._current_area.y2 < self._current_area.y1:
133                 self._current_area.y1, self._current_area.y2 = (
134                     self._current_area.y2,
135                     self._current_area.y1,
136                 )
137         else:
138             self._current_area.x1 = (
139                 self._absolute_transform.x + self._absolute_transform.dx * self._x
140             )
141             self._current_area.x2 = (
142                 self._absolute_transform.x
143                 + self._absolute_transform.dx * (self._x + width)
144             )
145             if self._current_area.x2 < self._current_area.x1:
146                 self._current_area.x1, self._current_area.x2 = (
147                     self._current_area.x2,
148                     self._current_area.x1,
149                 )
150
151     def _update_current_y(self):
152         if self._transpose_xy:
153             height = self._pixel_width
154         else:
155             height = self._pixel_height
156         if self._absolute_transform.transpose_xy:
157             self._current_area.x1 = (
158                 self._absolute_transform.x + self._absolute_transform.dx * self._y
159             )
160             self._current_area.x2 = (
161                 self._absolute_transform.x
162                 + self._absolute_transform.dx * (self._y + height)
163             )
164             if self._current_area.x2 < self._current_area.x1:
165                 self._current_area.x1, self._current_area.x2 = (
166                     self._current_area.x2,
167                     self._current_area.x1,
168                 )
169         else:
170             self._current_area.y1 = (
171                 self._absolute_transform.y + self._absolute_transform.dy * self._y
172             )
173             self._current_area.y2 = (
174                 self._absolute_transform.y
175                 + self._absolute_transform.dy * (self._y + height)
176             )
177             if self._current_area.y2 < self._current_area.y1:
178                 self._current_area.y1, self._current_area.y2 = (
179                     self._current_area.y2,
180                     self._current_area.y1,
181                 )
182
183     def _shade(self, pixel_value):
184         if isinstance(self._pixel_shader, Palette):
185             return self._pixel_shader[pixel_value]
186         if isinstance(self._pixel_shader, ColorConverter):
187             return self._pixel_shader.convert(pixel_value)
188         return pixel_value
189
190     def _apply_palette(self, image):
191         image.putpalette(
192             self._pixel_shader._get_palette()  # pylint: disable=protected-access
193         )
194
195     def _add_alpha(self, image):
196         alpha = self._bitmap._image.copy().convert(  # pylint: disable=protected-access
197             "P"
198         )
199         alpha.putpalette(
200             self._pixel_shader._get_alpha_palette()  # pylint: disable=protected-access
201         )
202         image.putalpha(alpha.convert("L"))
203
204     def _fill_area(
205         self, colorspace: Colorspace, area: Area, mask: bytearray, buffer: bytearray
206     ) -> bool:
207         """Draw onto the image"""
208         # pylint: disable=too-many-locals,too-many-branches,too-many-statements
209
210         # If no tiles are present we have no impact
211         tiles = self._tiles
212
213         if self._hidden_tilegrid:
214             return False
215
216         overlap = Area()
217         if not self._current_area.compute_overlap(area, overlap):
218             return False
219
220         if self._bitmap.width <= 0 or self._bitmap.height <= 0:
221             return False
222
223         x_stride = 1
224         y_stride = area.width()
225
226         flip_x = self._flip_x
227         flip_y = self._flip_y
228         if self._transpose_xy != self._absolute_transform.transpose_xy:
229             flip_x, flip_y = flip_y, flip_x
230
231         start = 0
232         if (self._absolute_transform.dx < 0) != flip_x:
233             start += (area.width() - 1) * x_stride
234             x_stride *= -1
235         if (self._absolute_transform.dy < 0) != flip_y:
236             start += (area.height() - 1) * y_stride
237             y_stride *= -1
238
239         full_coverage = area == overlap
240
241         transformed = Area()
242         area.transform_within(
243             flip_x != (self._absolute_transform.dx < 0),
244             flip_y != (self._absolute_transform.dy < 0),
245             self.transpose_xy != self._absolute_transform.transpose_xy,
246             overlap,
247             self._current_area,
248             transformed,
249         )
250
251         start_x = transformed.x1 - self._current_area.x1
252         end_x = transformed.x2 - self._current_area.x1
253         start_y = transformed.y1 - self._current_area.y1
254         end_y = transformed.y2 - self._current_area.y1
255
256         if (self._absolute_transform.dx < 0) != flip_x:
257             x_shift = area.x2 - overlap.x2
258         else:
259             x_shift = overlap.x1 - area.x1
260         if (self._absolute_transform.dy < 0) != flip_y:
261             y_shift = area.y2 - overlap.y2
262         else:
263             y_shift = overlap.y1 - area.y1
264
265         if self._transpose_xy != self._absolute_transform.transpose_xy:
266             x_stride, y_stride = y_stride, x_stride
267             x_shift, y_shift = y_shift, x_shift
268
269         pixels_per_byte = 8 // colorspace.depth
270
271         input_pixel = InputPixelStruct()
272         output_pixel = OutputPixelStruct()
273         for input_pixel.y in range(start_y, end_y):
274             row_start = (
275                 start + (input_pixel.y - start_y + y_shift) * y_stride
276             )  # In Pixels
277             local_y = input_pixel.y // self._absolute_transform.scale
278             for input_pixel.x in range(start_x, end_x):
279                 offset = (
280                     row_start + (input_pixel.x - start_x + x_shift) * x_stride
281                 )  # In Pixels
282
283                 # Check the mask first to see if the pixel has already been set
284                 if mask[offset // 32] & (1 << (offset % 32)):
285                     continue
286                 local_x = input_pixel.x // self._absolute_transform.scale
287                 tile_location = (
288                     (local_y // self._tile_height + self._top_left_y)
289                     % self._height_in_tiles
290                 ) * self._width_in_tiles + (
291                     local_x // self._tile_width + self._top_left_x
292                 ) % self._width_in_tiles
293                 input_pixel.tile = tiles[tile_location]
294                 input_pixel.tile_x = (
295                     input_pixel.tile % self._bitmap_width_in_tiles
296                 ) * self._tile_width + local_x % self._tile_width
297                 input_pixel.tile_y = (
298                     input_pixel.tile // self._bitmap_width_in_tiles
299                 ) * self._tile_height + local_y % self._tile_height
300
301                 input_pixel.pixel = self.bitmap[input_pixel.tile_x, input_pixel.tile_y]
302                 output_pixel.opaque = True
303
304                 if self._pixel_shader is None:
305                     output_pixel.pixel = input_pixel.pixel
306                 elif isinstance(self._pixel_shader, Palette):
307                     self._pixel_shader._get_color(  # pylint: disable=protected-access
308                         colorspace, input_pixel, output_pixel
309                     )
310                 elif isinstance(self._pixel_shader, ColorConverter):
311                     self._pixel_shader._convert(  # pylint: disable=protected-access
312                         colorspace, input_pixel, output_pixel
313                     )
314
315                 if not output_pixel.opaque:
316                     full_coverage = False
317                 else:
318                     mask[offset // 32] |= 1 << (offset % 32)
319                     if colorspace.depth == 16:
320                         buffer = (
321                             buffer[:offset]
322                             + struct.pack("H", output_pixel.pixel)
323                             + buffer[offset + 2 :]
324                         )
325                     elif colorspace.depth == 32:
326                         buffer = (
327                             buffer[:offset]
328                             + struct.pack("I", output_pixel.pixel)
329                             + buffer[offset + 4 :]
330                         )
331                     elif colorspace.depth == 8:
332                         buffer[offset] = output_pixel.pixel & 0xFF
333                     elif colorspace.depth < 8:
334                         # Reorder the offsets to pack multiple rows into
335                         # a byte (meaning they share a column).
336                         if not colorspace.pixels_in_byte_share_row:
337                             width = area.width()
338                             row = offset // width
339                             col = offset % width
340                             # Dividing by pixels_per_byte does truncated division
341                             # even if we multiply it back out
342                             offset = (
343                                 col * pixels_per_byte
344                                 + (row // pixels_per_byte) * width
345                                 + (row % pixels_per_byte)
346                             )
347                         shift = (offset % pixels_per_byte) * colorspace.depth
348                         if colorspace.reverse_pixels_in_byte:
349                             # Reverse the shift by subtracting it from the leftmost shift
350                             shift = (pixels_per_byte - 1) * colorspace.depth - shift
351                         buffer[offset // pixels_per_byte] |= output_pixel.pixel << shift
352         return full_coverage
353
354     def _finish_refresh(self):
355         pass
356
357     @property
358     def hidden(self) -> bool:
359         """True when the TileGrid is hidden. This may be False even
360         when a part of a hidden Group."""
361         return self._hidden_tilegrid
362
363     @hidden.setter
364     def hidden(self, value: bool):
365         if not isinstance(value, (bool, int)):
366             raise ValueError("Expecting a boolean or integer value")
367         self._hidden_tilegrid = bool(value)
368
369     @property
370     def x(self) -> int:
371         """X position of the left edge in the parent."""
372         return self._x
373
374     @x.setter
375     def x(self, value: int):
376         if not isinstance(value, int):
377             raise TypeError("X should be a integer type")
378         if self._x != value:
379             self._x = value
380             self._update_current_x()
381
382     @property
383     def y(self) -> int:
384         """Y position of the top edge in the parent."""
385         return self._y
386
387     @y.setter
388     def y(self, value: int):
389         if not isinstance(value, int):
390             raise TypeError("Y should be a integer type")
391         if self._y != value:
392             self._y = value
393             self._update_current_y()
394
395     @property
396     def flip_x(self) -> bool:
397         """If true, the left edge rendered will be the right edge of the right-most tile."""
398         return self._flip_x
399
400     @flip_x.setter
401     def flip_x(self, value: bool):
402         if not isinstance(value, bool):
403             raise TypeError("Flip X should be a boolean type")
404         if self._flip_x != value:
405             self._flip_x = value
406
407     @property
408     def flip_y(self) -> bool:
409         """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
410         return self._flip_y
411
412     @flip_y.setter
413     def flip_y(self, value: bool):
414         if not isinstance(value, bool):
415             raise TypeError("Flip Y should be a boolean type")
416         if self._flip_y != value:
417             self._flip_y = value
418
419     @property
420     def transpose_xy(self) -> bool:
421         """If true, the TileGrid's axis will be swapped. When combined with mirroring, any 90
422         degree rotation can be achieved along with the corresponding mirrored version.
423         """
424         return self._transpose_xy
425
426     @transpose_xy.setter
427     def transpose_xy(self, value: bool):
428         if not isinstance(value, bool):
429             raise TypeError("Transpose XY should be a boolean type")
430         if self._transpose_xy != value:
431             self._transpose_xy = value
432             self._update_current_x()
433             self._update_current_y()
434
435     @property
436     def pixel_shader(self) -> Union[ColorConverter, Palette]:
437         """The pixel shader of the tilegrid."""
438         return self._pixel_shader
439
440     @pixel_shader.setter
441     def pixel_shader(self, new_pixel_shader: Union[ColorConverter, Palette]) -> None:
442         if not isinstance(new_pixel_shader, ColorConverter) and not isinstance(
443             new_pixel_shader, Palette
444         ):
445             raise TypeError(
446                 "Unsupported Type: new_pixel_shader must be ColorConverter or Palette"
447             )
448
449         self._pixel_shader = new_pixel_shader
450
451     @property
452     def bitmap(self) -> Union[Bitmap, OnDiskBitmap, Shape]:
453         """The Bitmap, OnDiskBitmap, or Shape that is assigned to this TileGrid"""
454         return self._bitmap
455
456     @bitmap.setter
457     def bitmap(self, new_bitmap: Union[Bitmap, OnDiskBitmap, Shape]) -> None:
458         if (
459             not isinstance(new_bitmap, Bitmap)
460             and not isinstance(new_bitmap, OnDiskBitmap)
461             and not isinstance(new_bitmap, Shape)
462         ):
463             raise TypeError(
464                 "Unsupported Type: new_bitmap must be Bitmap, OnDiskBitmap, or Shape"
465             )
466
467         if (
468             new_bitmap.width != self.bitmap.width
469             or new_bitmap.height != self.bitmap.height
470         ):
471             raise ValueError("New bitmap must be same size as old bitmap")
472
473         self._bitmap = new_bitmap
474
475     def _extract_and_check_index(self, index):
476         if isinstance(index, (tuple, list)):
477             x = index[0]
478             y = index[1]
479             index = y * self._width_in_tiles + x
480         elif isinstance(index, int):
481             x = index % self._width_in_tiles
482             y = index // self._width_in_tiles
483         if (
484             x > self._width_in_tiles
485             or y > self._height_in_tiles
486             or index >= len(self._tiles)
487         ):
488             raise ValueError("Tile index out of bounds")
489         return index
490
491     def __getitem__(self, index: Union[Tuple[int, int], int]) -> int:
492         """Returns the tile index at the given index. The index can either be
493         an x,y tuple or an int equal to ``y * width + x``'.
494         """
495         index = self._extract_and_check_index(index)
496         return self._tiles[index]
497
498     def __setitem__(self, index: Union[Tuple[int, int], int], value: int) -> None:
499         """Sets the tile index at the given index. The index can either be
500         an x,y tuple or an int equal to ``y * width + x``.
501         """
502         index = self._extract_and_check_index(index)
503         if not 0 <= value <= 255:
504             raise ValueError("Tile value out of bounds")
505         self._tiles[index] = value
506
507     @property
508     def width(self) -> int:
509         """Width in tiles"""
510         return self._width_in_tiles
511
512     @property
513     def height(self) -> int:
514         """Height in tiles"""
515         return self._height_in_tiles
516
517     @property
518     def tile_width(self) -> int:
519         """Width of each tile in pixels"""
520         return self._tile_width
521
522     @property
523     def tile_height(self) -> int:
524         """Height of each tile in pixels"""
525         return self._tile_height