1 # The MIT License (MIT)
3 # Copyright (c) 2020 Melissa LeBlanc-Williams for Adafruit Industries
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:
12 # The above copyright notice and this permission notice shall be included in
13 # all copies or substantial portions of the Software.
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
25 ================================================================================
29 **Software and Dependencies:**
32 https://github.com/adafruit/Adafruit_Blinka/releases
34 * Author(s): Melissa LeBlanc-Williams
38 from recordclass import recordclass
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
46 __version__ = "0.0.0-auto.0"
47 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
49 Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
50 Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y")
52 # pylint: disable=too-many-instance-attributes
54 """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
55 grids can share bitmaps and pixel shaders.
57 A single tile grid is also known as a Sprite.
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.
77 tile_width and tile_height match the height of the bitmap by default.
79 if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
80 raise ValueError("Unsupported Bitmap type")
82 bitmap_width = bitmap.width
83 bitmap_height = bitmap.height
85 if not isinstance(pixel_shader, (ColorConverter, Palette)):
86 raise ValueError("Unsupported Pixel Shader type")
87 self._pixel_shader = pixel_shader
91 self._width = width # Number of Tiles Wide
92 self._height = height # Number of Tiles High
93 self._transpose_xy = 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)
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()
123 def _update_current_x(self):
124 if self._transpose_xy:
125 width = self._pixel_height
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
132 self._current_area.y2 = (
133 self._absolute_transform.y
134 + self._absolute_transform.dy * (self._x + width)
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,
142 self._current_area.x1 = (
143 self._absolute_transform.x + self._absolute_transform.dx * self._x
145 self._current_area.x2 = (
146 self._absolute_transform.x
147 + self._absolute_transform.dx * (self._x + width)
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,
155 def _update_current_y(self):
156 if self._transpose_xy:
157 height = self._pixel_width
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
164 self._current_area.x2 = (
165 self._absolute_transform.x
166 + self._absolute_transform.dx * (self._y + height)
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,
174 self._current_area.y1 = (
175 self._absolute_transform.y + self._absolute_transform.dy * self._y
177 self._current_area.y2 = (
178 self._absolute_transform.y
179 + self._absolute_transform.dy * (self._y + height)
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,
187 # pylint: disable=too-many-locals
188 def _fill_area(self, buffer):
189 """Draw onto the image"""
195 (self._width * self._tile_width, self._height * self._tile_height),
199 tile_count_x = self._bitmap.width // self._tile_width
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]
217 # if not pixel_color["transparent"]:
218 image.putpixel((image_x, image_y), pixel_color["rgba"])
220 if self._absolute_transform is not None:
221 if self._absolute_transform.scale > 1:
222 image = image.resize(
224 self._pixel_width * self._absolute_transform.scale,
225 self._pixel_height * self._absolute_transform.scale,
227 resample=Image.NEAREST,
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))
241 # pylint: enable=too-many-locals
245 """True when the TileGrid is hidden. This may be False even
246 when a part of a hidden Group."""
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)
257 """X position of the left edge in the parent."""
262 if not isinstance(value, int):
263 raise TypeError("X should be a integer type")
266 self._update_current_x()
270 """Y position of the top edge in the parent."""
275 if not isinstance(value, int):
276 raise TypeError("Y should be a integer type")
279 self._update_current_y()
283 """If true, the left edge rendered will be the right edge of the right-most tile."""
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:
295 """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
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:
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.
310 return self._transpose_xy
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()
322 def pixel_shader(self):
323 """The pixel shader of the tilegrid."""
324 return self._pixel_shader
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``'.
330 if isinstance(index, (tuple, list)):
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]
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``.
345 if isinstance(index, (tuple, list)):
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
359 # pylint: enable=too-many-instance-attributes