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