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
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
47 __version__ = "0.0.0-auto.0"
48 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
50 # pylint: disable=too-many-instance-attributes
52 """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
53 grids can share bitmaps and pixel shaders.
55 A single tile grid is also known as a Sprite.
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.
75 tile_width and tile_height match the height of the bitmap by default.
77 if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
78 raise ValueError("Unsupported Bitmap type")
80 bitmap_width = bitmap.width
81 bitmap_height = bitmap.height
83 if not isinstance(pixel_shader, (ColorConverter, Palette)):
84 raise ValueError("Unsupported Pixel Shader type")
85 self._pixel_shader = pixel_shader
89 self._width = width # Number of Tiles Wide
90 self._height = height # Number of Tiles High
91 self._transpose_xy = 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)
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()
121 def _update_current_x(self):
122 if self._transpose_xy:
123 width = self._pixel_height
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
130 self._current_area.y2 = (
131 self._absolute_transform.y
132 + self._absolute_transform.dy * (self._x + width)
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,
140 self._current_area.x1 = (
141 self._absolute_transform.x + self._absolute_transform.dx * self._x
143 self._current_area.x2 = (
144 self._absolute_transform.x
145 + self._absolute_transform.dx * (self._x + width)
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,
153 def _update_current_y(self):
154 if self._transpose_xy:
155 height = self._pixel_width
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
162 self._current_area.x2 = (
163 self._absolute_transform.x
164 + self._absolute_transform.dx * (self._y + height)
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,
172 self._current_area.y1 = (
173 self._absolute_transform.y + self._absolute_transform.dy * self._y
175 self._current_area.y2 = (
176 self._absolute_transform.y
177 + self._absolute_transform.dy * (self._y + height)
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,
185 # pylint: disable=too-many-locals
186 def _fill_area(self, buffer):
187 """Draw onto the image"""
193 (self._width * self._tile_width, self._height * self._tile_height),
197 tile_count_x = self._bitmap.width // self._tile_width
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]
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(
221 self._pixel_width * self._absolute_transform.scale,
222 self._pixel_height * self._absolute_transform.scale,
224 resample=Image.NEAREST,
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))
238 # pylint: enable=too-many-locals
242 """True when the TileGrid is hidden. This may be False even
243 when a part of a hidden Group."""
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)
254 """X position of the left edge in the parent."""
259 if not isinstance(value, int):
260 raise TypeError("X should be a integer type")
263 self._update_current_x()
267 """Y position of the top edge in the parent."""
272 if not isinstance(value, int):
273 raise TypeError("Y should be a integer type")
276 self._update_current_y()
280 """If true, the left edge rendered will be the right edge of the right-most tile."""
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:
292 """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
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:
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.
307 return self._transpose_xy
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()
319 def pixel_shader(self):
320 """The pixel shader of the tilegrid."""
321 return self._pixel_shader
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``'.
327 if isinstance(index, (tuple, list)):
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]
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``.
342 if isinstance(index, (tuple, list)):
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
356 # pylint: enable=too-many-instance-attributes