1 # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
3 # SPDX-License-Identifier: MIT
7 ================================================================================
11 **Software and Dependencies:**
14 https://github.com/adafruit/Adafruit_Blinka/releases
16 * Author(s): Melissa LeBlanc-Williams
20 from typing import Union, Optional, Tuple
22 from ._bitmap import Bitmap
23 from ._colorconverter import ColorConverter
24 from ._ondiskbitmap import OnDiskBitmap
25 from ._shape import Shape
26 from ._palette import Palette
27 from ._structs import RectangleStruct, TransformStruct
29 __version__ = "0.0.0-auto.0"
30 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
34 # pylint: disable=too-many-instance-attributes
35 """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
36 grids can share bitmaps and pixel shaders.
38 A single tile grid is also known as a Sprite.
43 bitmap: Union[Bitmap, OnDiskBitmap, Shape],
45 pixel_shader: Union[ColorConverter, Palette],
48 tile_width: Optional[int] = None,
49 tile_height: Optional[int] = None,
50 default_tile: int = 0,
54 """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is
55 used to convert the value and its location to a display native pixel color. This may
56 be a simple color palette lookup, a gradient, a pattern or a color transformer.
58 tile_width and tile_height match the height of the bitmap by default.
60 if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
61 raise ValueError("Unsupported Bitmap type")
63 bitmap_width = bitmap.width
64 bitmap_height = bitmap.height
66 if pixel_shader is not None and not isinstance(
67 pixel_shader, (ColorConverter, Palette)
69 raise ValueError("Unsupported Pixel Shader type")
70 self._pixel_shader = pixel_shader
71 if isinstance(self._pixel_shader, ColorConverter):
72 self._pixel_shader._rgba = True # pylint: disable=protected-access
73 self._hidden_tilegrid = False
76 self._width = width # Number of Tiles Wide
77 self._height = height # Number of Tiles High
78 self._transpose_xy = False
83 if tile_width is None or tile_width == 0:
84 tile_width = bitmap_width
85 if tile_height is None or tile_width == 0:
86 tile_height = bitmap_height
87 if tile_width < 1 or tile_height < 1:
88 raise ValueError("Tile width and height must be greater than 0")
89 if bitmap_width % tile_width != 0:
90 raise ValueError("Tile width must exactly divide bitmap width")
91 self._tile_width = tile_width
92 if bitmap_height % tile_height != 0:
93 raise ValueError("Tile height must exactly divide bitmap height")
94 self._tile_height = tile_height
95 if not 0 <= default_tile <= 255:
96 raise ValueError("Default Tile is out of range")
97 self._pixel_width = width * tile_width
98 self._pixel_height = height * tile_height
99 self._tiles = (self._width * self._height) * [default_tile]
100 self._in_group = False
101 self._absolute_transform = TransformStruct(0, 0, 1, 1, 1, False, False, False)
102 self._current_area = RectangleStruct(
103 0, 0, self._pixel_width, self._pixel_height
107 def _update_transform(self, absolute_transform):
108 """Update the parent transform and child transforms"""
109 self._absolute_transform = absolute_transform
110 if self._absolute_transform is not None:
111 self._update_current_x()
112 self._update_current_y()
114 def _update_current_x(self):
115 if self._transpose_xy:
116 width = self._pixel_height
118 width = self._pixel_width
119 if self._absolute_transform.transpose_xy:
120 self._current_area.y1 = (
121 self._absolute_transform.y + self._absolute_transform.dy * self._x
123 self._current_area.y2 = (
124 self._absolute_transform.y
125 + self._absolute_transform.dy * (self._x + width)
127 if self._current_area.y2 < self._current_area.y1:
128 self._current_area.y1, self._current_area.y2 = (
129 self._current_area.y2,
130 self._current_area.y1,
133 self._current_area.x1 = (
134 self._absolute_transform.x + self._absolute_transform.dx * self._x
136 self._current_area.x2 = (
137 self._absolute_transform.x
138 + self._absolute_transform.dx * (self._x + width)
140 if self._current_area.x2 < self._current_area.x1:
141 self._current_area.x1, self._current_area.x2 = (
142 self._current_area.x2,
143 self._current_area.x1,
146 def _update_current_y(self):
147 if self._transpose_xy:
148 height = self._pixel_width
150 height = self._pixel_height
151 if self._absolute_transform.transpose_xy:
152 self._current_area.x1 = (
153 self._absolute_transform.x + self._absolute_transform.dx * self._y
155 self._current_area.x2 = (
156 self._absolute_transform.x
157 + self._absolute_transform.dx * (self._y + height)
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 self._current_area.y1 = (
166 self._absolute_transform.y + self._absolute_transform.dy * self._y
168 self._current_area.y2 = (
169 self._absolute_transform.y
170 + self._absolute_transform.dy * (self._y + height)
172 if self._current_area.y2 < self._current_area.y1:
173 self._current_area.y1, self._current_area.y2 = (
174 self._current_area.y2,
175 self._current_area.y1,
178 def _shade(self, pixel_value):
179 if isinstance(self._pixel_shader, Palette):
180 return self._pixel_shader[pixel_value]
181 if isinstance(self._pixel_shader, ColorConverter):
182 return self._pixel_shader.convert(pixel_value)
185 def _apply_palette(self, image):
187 self._pixel_shader._get_palette() # pylint: disable=protected-access
190 def _add_alpha(self, image):
191 alpha = self._bitmap._image.copy().convert( # pylint: disable=protected-access
195 self._pixel_shader._get_alpha_palette() # pylint: disable=protected-access
197 image.putalpha(alpha.convert("L"))
199 def _fill_area(self, buffer):
200 # pylint: disable=too-many-locals,too-many-branches,too-many-statements
201 """Draw onto the image"""
202 if self._hidden_tilegrid:
205 if self._bitmap.width <= 0 or self._bitmap.height <= 0:
208 # Copy class variables to local variables in case something changes
212 height = self._height
213 tile_width = self._tile_width
214 tile_height = self._tile_height
215 bitmap_width = self._bitmap.width
216 pixel_width = self._pixel_width
217 pixel_height = self._pixel_height
219 absolute_transform = self._absolute_transform
220 pixel_shader = self._pixel_shader
221 bitmap = self._bitmap
224 tile_count_x = bitmap_width // tile_width
228 (width * tile_width, height * tile_height),
232 for tile_x in range(width):
233 for tile_y in range(height):
234 tile_index = tiles[tile_y * width + tile_x]
235 tile_index_x = tile_index % tile_count_x
236 tile_index_y = tile_index // tile_count_x
237 tile_image = bitmap._image # pylint: disable=protected-access
238 if isinstance(pixel_shader, Palette):
239 tile_image = tile_image.copy().convert("P")
240 self._apply_palette(tile_image)
241 tile_image = tile_image.convert("RGBA")
242 self._add_alpha(tile_image)
243 elif isinstance(pixel_shader, ColorConverter):
244 # This will be needed for eInks, grayscale, and monochrome displays
246 image.alpha_composite(
248 dest=(tile_x * tile_width, tile_y * tile_height),
250 tile_index_x * tile_width,
251 tile_index_y * tile_height,
252 tile_index_x * tile_width + tile_width,
253 tile_index_y * tile_height + tile_height,
257 if absolute_transform is not None:
258 if absolute_transform.scale > 1:
259 image = image.resize(
261 int(pixel_width * absolute_transform.scale),
263 pixel_height * absolute_transform.scale,
266 resample=Image.NEAREST,
268 if absolute_transform.mirror_x:
269 image = image.transpose(Image.FLIP_LEFT_RIGHT)
270 if absolute_transform.mirror_y:
271 image = image.transpose(Image.FLIP_TOP_BOTTOM)
272 if absolute_transform.transpose_xy:
273 image = image.transpose(Image.TRANSPOSE)
274 x *= absolute_transform.dx
275 y *= absolute_transform.dy
276 x += absolute_transform.x
277 y += absolute_transform.y
279 source_x = source_y = 0
281 source_x = round(0 - x)
284 source_y = round(0 - y)
292 and y <= buffer.height
293 and source_x <= image.width
294 and source_y <= image.height
296 buffer.alpha_composite(image, (x, y), source=(source_x, source_y))
298 def _finish_refresh(self):
302 def hidden(self) -> bool:
303 """True when the TileGrid is hidden. This may be False even
304 when a part of a hidden Group."""
305 return self._hidden_tilegrid
308 def hidden(self, value: bool):
309 if not isinstance(value, (bool, int)):
310 raise ValueError("Expecting a boolean or integer value")
311 self._hidden_tilegrid = bool(value)
315 """X position of the left edge in the parent."""
319 def x(self, value: int):
320 if not isinstance(value, int):
321 raise TypeError("X should be a integer type")
324 self._update_current_x()
328 """Y position of the top edge in the parent."""
332 def y(self, value: int):
333 if not isinstance(value, int):
334 raise TypeError("Y should be a integer type")
337 self._update_current_y()
340 def flip_x(self) -> bool:
341 """If true, the left edge rendered will be the right edge of the right-most tile."""
345 def flip_x(self, value: bool):
346 if not isinstance(value, bool):
347 raise TypeError("Flip X should be a boolean type")
348 if self._flip_x != value:
352 def flip_y(self) -> bool:
353 """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
357 def flip_y(self, value: bool):
358 if not isinstance(value, bool):
359 raise TypeError("Flip Y should be a boolean type")
360 if self._flip_y != value:
364 def transpose_xy(self) -> bool:
365 """If true, the TileGrid's axis will be swapped. When combined with mirroring, any 90
366 degree rotation can be achieved along with the corresponding mirrored version.
368 return self._transpose_xy
371 def transpose_xy(self, value: bool):
372 if not isinstance(value, bool):
373 raise TypeError("Transpose XY should be a boolean type")
374 if self._transpose_xy != value:
375 self._transpose_xy = value
376 self._update_current_x()
377 self._update_current_y()
380 def pixel_shader(self) -> Union[ColorConverter, Palette]:
381 """The pixel shader of the tilegrid."""
382 return self._pixel_shader
385 def pixel_shader(self, new_pixel_shader: Union[ColorConverter, Palette]) -> None:
386 if not isinstance(new_pixel_shader, ColorConverter) and not isinstance(
387 new_pixel_shader, Palette
390 "Unsupported Type: new_pixel_shader must be ColorConverter or Palette"
393 self._pixel_shader = new_pixel_shader
396 def bitmap(self) -> Union[Bitmap, OnDiskBitmap, Shape]:
397 """The Bitmap, OnDiskBitmap, or Shape that is assigned to this TileGrid"""
401 def bitmap(self, new_bitmap: Union[Bitmap, OnDiskBitmap, Shape]) -> None:
404 not isinstance(new_bitmap, Bitmap)
405 and not isinstance(new_bitmap, OnDiskBitmap)
406 and not isinstance(new_bitmap, Shape)
409 "Unsupported Type: new_bitmap must be Bitmap, OnDiskBitmap, or Shape"
413 new_bitmap.width != self.bitmap.width
414 or new_bitmap.height != self.bitmap.height
416 raise ValueError("New bitmap must be same size as old bitmap")
418 self._bitmap = new_bitmap
420 def _extract_and_check_index(self, index):
421 if isinstance(index, (tuple, list)):
424 index = y * self._width + x
425 elif isinstance(index, int):
426 x = index % self._width
427 y = index // self._width
428 if x > self._width or y > self._height or index >= len(self._tiles):
429 raise ValueError("Tile index out of bounds")
432 def __getitem__(self, index: Union[Tuple[int, int], int]) -> int:
433 """Returns the tile index at the given index. The index can either be
434 an x,y tuple or an int equal to ``y * width + x``'.
436 index = self._extract_and_check_index(index)
437 return self._tiles[index]
439 def __setitem__(self, index: Union[Tuple[int, int], int], value: int) -> None:
440 """Sets the tile index at the given index. The index can either be
441 an x,y tuple or an int equal to ``y * width + x``.
443 index = self._extract_and_check_index(index)
444 if not 0 <= value <= 255:
445 raise ValueError("Tile value out of bounds")
446 self._tiles[index] = value