]> Repositories - hackapet/Adafruit_Blinka_Displayio.git/blob - displayio/tilegrid.py
Merge pull request #10 from makermelissa/master
[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(self._width):
204             for tile_y in range(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["rgba"])
219
220         if self._absolute_transform is not None:
221             if self._absolute_transform.scale > 1:
222                 image = image.resize(
223                     (
224                         self._pixel_width * self._absolute_transform.scale,
225                         self._pixel_height * self._absolute_transform.scale,
226                     ),
227                     resample=Image.NEAREST,
228                 )
229             if self._absolute_transform.mirror_x:
230                 image = image.transpose(Image.FLIP_LEFT_RIGHT)
231             if self._absolute_transform.mirror_y:
232                 image = image.transpose(Image.FLIP_TOP_BOTTOM)
233             if self._absolute_transform.transpose_xy:
234                 image = image.transpose(Image.TRANSPOSE)
235             x *= self._absolute_transform.dx
236             y *= self._absolute_transform.dy
237             x += self._absolute_transform.x
238             y += self._absolute_transform.y
239         buffer.alpha_composite(image, (x, y))
240
241     # pylint: enable=too-many-locals
242
243     @property
244     def hidden(self):
245         """True when the TileGrid is hidden. This may be False even
246         when a part of a hidden Group."""
247         return self._hidden
248
249     @hidden.setter
250     def hidden(self, value):
251         if not isinstance(value, (bool, int)):
252             raise ValueError("Expecting a boolean or integer value")
253         self._hidden = bool(value)
254
255     @property
256     def x(self):
257         """X position of the left edge in the parent."""
258         return self._x
259
260     @x.setter
261     def x(self, value):
262         if not isinstance(value, int):
263             raise TypeError("X should be a integer type")
264         if self._x != value:
265             self._x = value
266             self._update_current_x()
267
268     @property
269     def y(self):
270         """Y position of the top edge in the parent."""
271         return self._y
272
273     @y.setter
274     def y(self, value):
275         if not isinstance(value, int):
276             raise TypeError("Y should be a integer type")
277         if self._y != value:
278             self._y = value
279             self._update_current_y()
280
281     @property
282     def flip_x(self):
283         """If true, the left edge rendered will be the right edge of the right-most tile."""
284         return self._flip_x
285
286     @flip_x.setter
287     def flip_x(self, value):
288         if not isinstance(value, bool):
289             raise TypeError("Flip X should be a boolean type")
290         if self._flip_x != value:
291             self._flip_x = value
292
293     @property
294     def flip_y(self):
295         """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
296         return self._flip_y
297
298     @flip_y.setter
299     def flip_y(self, value):
300         if not isinstance(value, bool):
301             raise TypeError("Flip Y should be a boolean type")
302         if self._flip_y != value:
303             self._flip_y = value
304
305     @property
306     def transpose_xy(self):
307         """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90
308         degree rotation can be achieved along with the corresponding mirrored version.
309         """
310         return self._transpose_xy
311
312     @transpose_xy.setter
313     def transpose_xy(self, value):
314         if not isinstance(value, bool):
315             raise TypeError("Transpose XY should be a boolean type")
316         if self._transpose_xy != value:
317             self._transpose_xy = value
318             self._update_current_x()
319             self._update_current_y()
320
321     @property
322     def pixel_shader(self):
323         """The pixel shader of the tilegrid."""
324         return self._pixel_shader
325
326     def __getitem__(self, index):
327         """Returns the tile index at the given index. The index can either be
328         an x,y tuple or an int equal to ``y * width + x``'.
329         """
330         if isinstance(index, (tuple, list)):
331             x = index[0]
332             y = index[1]
333             index = y * self._width + x
334         elif isinstance(index, int):
335             x = index % self._width
336             y = index // self._width
337         if x > self._width or y > self._height or index >= len(self._tiles):
338             raise ValueError("Tile index out of bounds")
339         return self._tiles[index]
340
341     def __setitem__(self, index, value):
342         """Sets the tile index at the given index. The index can either be
343         an x,y tuple or an int equal to ``y * width + x``.
344         """
345         if isinstance(index, (tuple, list)):
346             x = index[0]
347             y = index[1]
348             index = y * self._width + x
349         elif isinstance(index, int):
350             x = index % self._width
351             y = index // self._width
352         if x > self._width or y > self._height or index >= len(self._tiles):
353             raise ValueError("Tile index out of bounds")
354         if not 0 <= value <= 255:
355             raise ValueError("Tile value out of bounds")
356         self._tiles[index] = value
357
358
359 # pylint: enable=too-many-instance-attributes