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