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 pixel_shader is not None and not isinstance(
86 pixel_shader, (ColorConverter, Palette)
88 raise ValueError("Unsupported Pixel Shader type")
89 self._pixel_shader = pixel_shader
90 if isinstance(self._pixel_shader, ColorConverter):
91 self._pixel_shader._rgba = True # pylint: disable=protected-access
92 self._hidden_tilegrid = False
95 self._width = width # Number of Tiles Wide
96 self._height = height # Number of Tiles High
97 self._transpose_xy = False
102 if tile_width is None or tile_width == 0:
103 tile_width = bitmap_width
104 if tile_height is None or tile_width == 0:
105 tile_height = bitmap_height
110 if bitmap_width % tile_width != 0:
111 raise ValueError("Tile width must exactly divide bitmap width")
112 self._tile_width = tile_width
113 if bitmap_height % tile_height != 0:
114 raise ValueError("Tile height must exactly divide bitmap height")
115 self._tile_height = tile_height
116 if not 0 <= default_tile <= 255:
117 raise ValueError("Default Tile is out of range")
118 self._pixel_width = width * tile_width
119 self._pixel_height = height * tile_height
120 self._tiles = (self._width * self._height) * [default_tile]
121 self.in_group = False
122 self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
123 self._current_area = Rectangle(0, 0, self._pixel_width, self._pixel_height)
126 def update_transform(self, absolute_transform):
127 """Update the parent transform and child transforms"""
128 self._absolute_transform = absolute_transform
129 if self._absolute_transform is not None:
130 self._update_current_x()
131 self._update_current_y()
133 def _update_current_x(self):
134 if self._transpose_xy:
135 width = self._pixel_height
137 width = self._pixel_width
138 if self._absolute_transform.transpose_xy:
139 self._current_area.y1 = (
140 self._absolute_transform.y + self._absolute_transform.dy * self._x
142 self._current_area.y2 = (
143 self._absolute_transform.y
144 + self._absolute_transform.dy * (self._x + width)
146 if self._current_area.y2 < self._current_area.y1:
147 self._current_area.y1, self._current_area.y2 = (
148 self._current_area.y2,
149 self._current_area.y1,
152 self._current_area.x1 = (
153 self._absolute_transform.x + self._absolute_transform.dx * self._x
155 self._current_area.x2 = (
156 self._absolute_transform.x
157 + self._absolute_transform.dx * (self._x + width)
159 if self._current_area.x2 < self._current_area.x1:
160 self._current_area.x1, self._current_area.x2 = (
161 self._current_area.x2,
162 self._current_area.x1,
165 def _update_current_y(self):
166 if self._transpose_xy:
167 height = self._pixel_width
169 height = self._pixel_height
170 if self._absolute_transform.transpose_xy:
171 self._current_area.x1 = (
172 self._absolute_transform.x + self._absolute_transform.dx * self._y
174 self._current_area.x2 = (
175 self._absolute_transform.x
176 + self._absolute_transform.dx * (self._y + height)
178 if self._current_area.x2 < self._current_area.x1:
179 self._current_area.x1, self._current_area.x2 = (
180 self._current_area.x2,
181 self._current_area.x1,
184 self._current_area.y1 = (
185 self._absolute_transform.y + self._absolute_transform.dy * self._y
187 self._current_area.y2 = (
188 self._absolute_transform.y
189 + self._absolute_transform.dy * (self._y + height)
191 if self._current_area.y2 < self._current_area.y1:
192 self._current_area.y1, self._current_area.y2 = (
193 self._current_area.y2,
194 self._current_area.y1,
197 def _shade(self, pixel_value):
198 if isinstance(self._pixel_shader, Palette):
199 return self._pixel_shader[pixel_value]["rgba"]
200 if isinstance(self._pixel_shader, ColorConverter):
201 return self._pixel_shader.convert(pixel_value)
204 def _apply_palette(self, image):
206 self._pixel_shader._get_palette() # pylint: disable=protected-access
209 def _add_alpha(self, image):
210 alpha = self._bitmap._image.copy().convert( # pylint: disable=protected-access
214 self._pixel_shader._get_alpha_palette() # pylint: disable=protected-access
216 image.putalpha(alpha.convert("L"))
218 # pylint: disable=too-many-locals,too-many-branches,too-many-statements
219 def _fill_area(self, buffer):
220 """Draw onto the image"""
221 if self._hidden_tilegrid:
224 if self._bitmap.width <= 0 or self._bitmap.height <= 0:
227 # Copy class variables to local variables in case something changes
231 height = self._height
232 tile_width = self._tile_width
233 tile_height = self._tile_height
234 bitmap_width = self._bitmap.width
235 pixel_width = self._pixel_width
236 pixel_height = self._pixel_height
238 absolute_transform = self._absolute_transform
239 pixel_shader = self._pixel_shader
240 bitmap = self._bitmap
243 tile_count_x = bitmap_width // tile_width
247 (width * tile_width, height * tile_height),
251 for tile_x in range(width):
252 for tile_y in range(height):
253 tile_index = tiles[tile_y * width + tile_x]
254 tile_index_x = tile_index % tile_count_x
255 tile_index_y = tile_index // tile_count_x
256 tile_image = bitmap._image # pylint: disable=protected-access
257 if isinstance(pixel_shader, Palette):
258 tile_image = tile_image.copy().convert("P")
259 self._apply_palette(tile_image)
260 tile_image = tile_image.convert("RGBA")
261 self._add_alpha(tile_image)
262 elif isinstance(pixel_shader, ColorConverter):
263 # This will be needed for eInks, grayscale, and monochrome displays
265 image.alpha_composite(
267 dest=(tile_x * tile_width, tile_y * tile_height),
269 tile_index_x * tile_width,
270 tile_index_y * tile_height,
271 tile_index_x * tile_width + tile_width,
272 tile_index_y * tile_height + tile_height,
276 if absolute_transform is not None:
277 if absolute_transform.scale > 1:
278 image = image.resize(
280 int(pixel_width * absolute_transform.scale),
282 pixel_height * absolute_transform.scale,
285 resample=Image.NEAREST,
287 if absolute_transform.mirror_x:
288 image = image.transpose(Image.FLIP_LEFT_RIGHT)
289 if absolute_transform.mirror_y:
290 image = image.transpose(Image.FLIP_TOP_BOTTOM)
291 if absolute_transform.transpose_xy:
292 image = image.transpose(Image.TRANSPOSE)
293 x *= absolute_transform.dx
294 y *= absolute_transform.dy
295 x += absolute_transform.x
296 y += absolute_transform.y
298 source_x = source_y = 0
300 source_x = round(0 - x)
303 source_y = round(0 - y)
311 and y <= buffer.height
312 and source_x <= image.width
313 and source_y <= image.height
315 buffer.alpha_composite(image, (x, y), source=(source_x, source_y))
317 # pylint: enable=too-many-locals,too-many-branches
321 """True when the TileGrid is hidden. This may be False even
322 when a part of a hidden Group."""
323 return self._hidden_tilegrid
326 def hidden(self, value):
327 if not isinstance(value, (bool, int)):
328 raise ValueError("Expecting a boolean or integer value")
329 self._hidden_tilegrid = bool(value)
333 """X position of the left edge in the parent."""
338 if not isinstance(value, int):
339 raise TypeError("X should be a integer type")
342 self._update_current_x()
346 """Y position of the top edge in the parent."""
351 if not isinstance(value, int):
352 raise TypeError("Y should be a integer type")
355 self._update_current_y()
359 """If true, the left edge rendered will be the right edge of the right-most tile."""
363 def flip_x(self, value):
364 if not isinstance(value, bool):
365 raise TypeError("Flip X should be a boolean type")
366 if self._flip_x != value:
371 """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
375 def flip_y(self, value):
376 if not isinstance(value, bool):
377 raise TypeError("Flip Y should be a boolean type")
378 if self._flip_y != value:
382 def transpose_xy(self):
383 """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90
384 degree rotation can be achieved along with the corresponding mirrored version.
386 return self._transpose_xy
389 def transpose_xy(self, value):
390 if not isinstance(value, bool):
391 raise TypeError("Transpose XY should be a boolean type")
392 if self._transpose_xy != value:
393 self._transpose_xy = value
394 self._update_current_x()
395 self._update_current_y()
398 def pixel_shader(self):
399 """The pixel shader of the tilegrid."""
400 return self._pixel_shader
402 def _extract_and_check_index(self, index):
403 if isinstance(index, (tuple, list)):
406 index = y * self._width + x
407 elif isinstance(index, int):
408 x = index % self._width
409 y = index // self._width
410 if x > self._width or y > self._height or index >= len(self._tiles):
411 raise ValueError("Tile index out of bounds")
414 def __getitem__(self, index):
415 """Returns the tile index at the given index. The index can either be
416 an x,y tuple or an int equal to ``y * width + x``'.
418 index = self._extract_and_check_index(index)
419 return self._tiles[index]
421 def __setitem__(self, index, value):
422 """Sets the tile index at the given index. The index can either be
423 an x,y tuple or an int equal to ``y * width + x``.
425 index = self._extract_and_check_index(index)
426 if not 0 <= value <= 255:
427 raise ValueError("Tile value out of bounds")
428 self._tiles[index] = value
431 # pylint: enable=too-many-instance-attributes