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