]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/tilegrid.py
Divide by zero and speed up bug fixes
[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 = 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:
103             tile_width = bitmap_width
104         if tile_height is None:
105             tile_height = bitmap_height
106         if tile_width > 0 and bitmap_width % tile_width != 0:
107             raise ValueError("Tile width must exactly divide bitmap width")
108         self._tile_width = tile_width
109         if tile_height > 0 and bitmap_height % tile_height != 0:
110             raise ValueError("Tile height must exactly divide bitmap height")
111         self._tile_height = tile_height
112         if not 0 <= default_tile <= 255:
113             raise ValueError("Default Tile is out of range")
114         self._pixel_width = width * tile_width
115         self._pixel_height = height * tile_height
116         self._tiles = (self._width * self._height) * [default_tile]
117         self.in_group = False
118         self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
119         self._current_area = Rectangle(0, 0, self._pixel_width, self._pixel_height)
120         self._moved = False
121
122     def update_transform(self, absolute_transform):
123         """Update the parent transform and child transforms"""
124         self._absolute_transform = absolute_transform
125         if self._absolute_transform is not None:
126             self._update_current_x()
127             self._update_current_y()
128
129     def _update_current_x(self):
130         if self._transpose_xy:
131             width = self._pixel_height
132         else:
133             width = self._pixel_width
134         if self._absolute_transform.transpose_xy:
135             self._current_area.y1 = (
136                 self._absolute_transform.y + self._absolute_transform.dy * self._x
137             )
138             self._current_area.y2 = (
139                 self._absolute_transform.y
140                 + self._absolute_transform.dy * (self._x + width)
141             )
142             if self._current_area.y2 < self._current_area.y1:
143                 self._current_area.y1, self._current_area.y2 = (
144                     self._current_area.y2,
145                     self._current_area.y1,
146                 )
147         else:
148             self._current_area.x1 = (
149                 self._absolute_transform.x + self._absolute_transform.dx * self._x
150             )
151             self._current_area.x2 = (
152                 self._absolute_transform.x
153                 + self._absolute_transform.dx * (self._x + width)
154             )
155             if self._current_area.x2 < self._current_area.x1:
156                 self._current_area.x1, self._current_area.x2 = (
157                     self._current_area.x2,
158                     self._current_area.x1,
159                 )
160
161     def _update_current_y(self):
162         if self._transpose_xy:
163             height = self._pixel_width
164         else:
165             height = self._pixel_height
166         if self._absolute_transform.transpose_xy:
167             self._current_area.x1 = (
168                 self._absolute_transform.x + self._absolute_transform.dx * self._y
169             )
170             self._current_area.x2 = (
171                 self._absolute_transform.x
172                 + self._absolute_transform.dx * (self._y + height)
173             )
174             if self._current_area.x2 < self._current_area.x1:
175                 self._current_area.x1, self._current_area.x2 = (
176                     self._current_area.x2,
177                     self._current_area.x1,
178                 )
179         else:
180             self._current_area.y1 = (
181                 self._absolute_transform.y + self._absolute_transform.dy * self._y
182             )
183             self._current_area.y2 = (
184                 self._absolute_transform.y
185                 + self._absolute_transform.dy * (self._y + height)
186             )
187             if self._current_area.y2 < self._current_area.y1:
188                 self._current_area.y1, self._current_area.y2 = (
189                     self._current_area.y2,
190                     self._current_area.y1,
191                 )
192
193     def _shade(self, pixel_value):
194         if isinstance(self._pixel_shader, Palette):
195             return self._pixel_shader[pixel_value]["rgba"]
196         if isinstance(self._pixel_shader, ColorConverter):
197             return self._pixel_shader.convert(pixel_value)
198         return pixel_value
199
200     def _apply_palette(self, image):
201         image.putpalette(
202             self._pixel_shader._get_palette()  # pylint: disable=protected-access
203         )
204
205     def _add_alpha(self, image):
206         alpha = self._bitmap._image.copy().convert(  # pylint: disable=protected-access
207             "P"
208         )
209         alpha.putpalette(
210             self._pixel_shader._get_alpha_palette()  # pylint: disable=protected-access
211         )
212         image.putalpha(alpha.convert("L"))
213
214     # pylint: disable=too-many-locals
215     def _fill_area(self, buffer):
216         """Draw onto the image"""
217         if self._hidden:
218             return
219
220         image = Image.new(
221             "RGBA",
222             (self._width * self._tile_width, self._height * self._tile_height),
223             (0, 0, 0, 0),
224         )
225
226         tile_count_x = self._bitmap.width // self._tile_width
227         x = self._x
228         y = self._y
229
230         for tile_x in range(self._width):
231             for tile_y in range(self._height):
232                 tile_index = self._tiles[tile_y * self._width + tile_x]
233                 tile_index_x = tile_index % tile_count_x
234                 tile_index_y = tile_index // tile_count_x
235                 tile_image = self._bitmap._image  # pylint: disable=protected-access
236                 if isinstance(self._pixel_shader, Palette):
237                     tile_image = tile_image.copy().convert("P")
238                     self._apply_palette(tile_image)
239                     tile_image = tile_image.convert("RGBA")
240                     self._add_alpha(tile_image)
241                 elif isinstance(self._pixel_shader, ColorConverter):
242                     # This will be needed for eInks, grayscale, and monochrome displays
243                     pass
244                 image.alpha_composite(
245                     tile_image,
246                     dest=(tile_x * self._tile_width, tile_y * self._tile_height),
247                     source=(
248                         tile_index_x * self._tile_width,
249                         tile_index_y * self._tile_height,
250                     ),
251                 )
252
253         if self._absolute_transform is not None:
254             if self._absolute_transform.scale > 1:
255                 image = image.resize(
256                     (
257                         self._pixel_width * self._absolute_transform.scale,
258                         self._pixel_height * self._absolute_transform.scale,
259                     ),
260                     resample=Image.NEAREST,
261                 )
262             if self._absolute_transform.mirror_x:
263                 image = image.transpose(Image.FLIP_LEFT_RIGHT)
264             if self._absolute_transform.mirror_y:
265                 image = image.transpose(Image.FLIP_TOP_BOTTOM)
266             if self._absolute_transform.transpose_xy:
267                 image = image.transpose(Image.TRANSPOSE)
268             x *= self._absolute_transform.dx
269             y *= self._absolute_transform.dy
270             x += self._absolute_transform.x
271             y += self._absolute_transform.y
272         buffer.alpha_composite(image, (x, y))
273
274     # pylint: enable=too-many-locals
275
276     @property
277     def hidden(self):
278         """True when the TileGrid is hidden. This may be False even
279         when a part of a hidden Group."""
280         return self._hidden
281
282     @hidden.setter
283     def hidden(self, value):
284         if not isinstance(value, (bool, int)):
285             raise ValueError("Expecting a boolean or integer value")
286         self._hidden = bool(value)
287
288     @property
289     def x(self):
290         """X position of the left edge in the parent."""
291         return self._x
292
293     @x.setter
294     def x(self, value):
295         if not isinstance(value, int):
296             raise TypeError("X should be a integer type")
297         if self._x != value:
298             self._x = value
299             self._update_current_x()
300
301     @property
302     def y(self):
303         """Y position of the top edge in the parent."""
304         return self._y
305
306     @y.setter
307     def y(self, value):
308         if not isinstance(value, int):
309             raise TypeError("Y should be a integer type")
310         if self._y != value:
311             self._y = value
312             self._update_current_y()
313
314     @property
315     def flip_x(self):
316         """If true, the left edge rendered will be the right edge of the right-most tile."""
317         return self._flip_x
318
319     @flip_x.setter
320     def flip_x(self, value):
321         if not isinstance(value, bool):
322             raise TypeError("Flip X should be a boolean type")
323         if self._flip_x != value:
324             self._flip_x = value
325
326     @property
327     def flip_y(self):
328         """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
329         return self._flip_y
330
331     @flip_y.setter
332     def flip_y(self, value):
333         if not isinstance(value, bool):
334             raise TypeError("Flip Y should be a boolean type")
335         if self._flip_y != value:
336             self._flip_y = value
337
338     @property
339     def transpose_xy(self):
340         """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90
341         degree rotation can be achieved along with the corresponding mirrored version.
342         """
343         return self._transpose_xy
344
345     @transpose_xy.setter
346     def transpose_xy(self, value):
347         if not isinstance(value, bool):
348             raise TypeError("Transpose XY should be a boolean type")
349         if self._transpose_xy != value:
350             self._transpose_xy = value
351             self._update_current_x()
352             self._update_current_y()
353
354     @property
355     def pixel_shader(self):
356         """The pixel shader of the tilegrid."""
357         return self._pixel_shader
358
359     def __getitem__(self, index):
360         """Returns the tile index at the given index. The index can either be
361         an x,y tuple or an int equal to ``y * width + x``'.
362         """
363         if isinstance(index, (tuple, list)):
364             x = index[0]
365             y = index[1]
366             index = y * self._width + x
367         elif isinstance(index, int):
368             x = index % self._width
369             y = index // self._width
370         if x > self._width or y > self._height or index >= len(self._tiles):
371             raise ValueError("Tile index out of bounds")
372         return self._tiles[index]
373
374     def __setitem__(self, index, value):
375         """Sets the tile index at the given index. The index can either be
376         an x,y tuple or an int equal to ``y * width + x``.
377         """
378         if isinstance(index, (tuple, list)):
379             x = index[0]
380             y = index[1]
381             index = y * self._width + x
382         elif isinstance(index, int):
383             x = index % self._width
384             y = index // self._width
385         if x > self._width or y > self._height or index >= len(self._tiles):
386             raise ValueError("Tile index out of bounds")
387         if not 0 <= value <= 255:
388             raise ValueError("Tile value out of bounds")
389         self._tiles[index] = value
390
391
392 # pylint: enable=too-many-instance-attributes