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
21 from recordclass import recordclass
23 from ._bitmap import Bitmap
24 from ._colorconverter import ColorConverter
25 from ._ondiskbitmap import OnDiskBitmap
26 from ._shape import Shape
27 from ._palette import Palette
29 __version__ = "0.0.0-auto.0"
30 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
32 Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
33 Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y")
37 # pylint: disable=too-many-instance-attributes
38 """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
39 grids can share bitmaps and pixel shaders.
41 A single tile grid is also known as a Sprite.
46 bitmap: Union[Bitmap, OnDiskBitmap, Shape],
48 pixel_shader: Union[ColorConverter, Palette],
51 tile_width: Optional[int] = None,
52 tile_height: Optional[int] = None,
53 default_tile: int = 0,
57 """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is
58 used to convert the value and its location to a display native pixel color. This may
59 be a simple color palette lookup, a gradient, a pattern or a color transformer.
61 tile_width and tile_height match the height of the bitmap by default.
63 if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
64 raise ValueError("Unsupported Bitmap type")
66 bitmap_width = bitmap.width
67 bitmap_height = bitmap.height
69 if pixel_shader is not None and not isinstance(
70 pixel_shader, (ColorConverter, Palette)
72 raise ValueError("Unsupported Pixel Shader type")
73 self._pixel_shader = pixel_shader
74 if isinstance(self._pixel_shader, ColorConverter):
75 self._pixel_shader._rgba = True # pylint: disable=protected-access
76 self._hidden_tilegrid = False
79 self._width = width # Number of Tiles Wide
80 self._height = height # Number of Tiles High
81 self._transpose_xy = False
86 if tile_width is None or tile_width == 0:
87 tile_width = bitmap_width
88 if tile_height is None or tile_width == 0:
89 tile_height = bitmap_height
90 if tile_width < 1 or tile_height < 1:
91 raise ValueError("Tile width and height must be greater than 0")
92 if bitmap_width % tile_width != 0:
93 raise ValueError("Tile width must exactly divide bitmap width")
94 self._tile_width = tile_width
95 if bitmap_height % tile_height != 0:
96 raise ValueError("Tile height must exactly divide bitmap height")
97 self._tile_height = tile_height
98 if not 0 <= default_tile <= 255:
99 raise ValueError("Default Tile is out of range")
100 self._pixel_width = width * tile_width
101 self._pixel_height = height * tile_height
102 self._tiles = (self._width * self._height) * [default_tile]
103 self.in_group = False
104 self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
105 self._current_area = Rectangle(0, 0, self._pixel_width, self._pixel_height)
108 def _update_transform(self, absolute_transform):
109 """Update the parent transform and child transforms"""
110 self._absolute_transform = absolute_transform
111 if self._absolute_transform is not None:
112 self._update_current_x()
113 self._update_current_y()
115 def _update_current_x(self):
116 if self._transpose_xy:
117 width = self._pixel_height
119 width = self._pixel_width
120 if self._absolute_transform.transpose_xy:
121 self._current_area.y1 = (
122 self._absolute_transform.y + self._absolute_transform.dy * self._x
124 self._current_area.y2 = (
125 self._absolute_transform.y
126 + self._absolute_transform.dy * (self._x + width)
128 if self._current_area.y2 < self._current_area.y1:
129 self._current_area.y1, self._current_area.y2 = (
130 self._current_area.y2,
131 self._current_area.y1,
134 self._current_area.x1 = (
135 self._absolute_transform.x + self._absolute_transform.dx * self._x
137 self._current_area.x2 = (
138 self._absolute_transform.x
139 + self._absolute_transform.dx * (self._x + width)
141 if self._current_area.x2 < self._current_area.x1:
142 self._current_area.x1, self._current_area.x2 = (
143 self._current_area.x2,
144 self._current_area.x1,
147 def _update_current_y(self):
148 if self._transpose_xy:
149 height = self._pixel_width
151 height = self._pixel_height
152 if self._absolute_transform.transpose_xy:
153 self._current_area.x1 = (
154 self._absolute_transform.x + self._absolute_transform.dx * self._y
156 self._current_area.x2 = (
157 self._absolute_transform.x
158 + self._absolute_transform.dx * (self._y + height)
160 if self._current_area.x2 < self._current_area.x1:
161 self._current_area.x1, self._current_area.x2 = (
162 self._current_area.x2,
163 self._current_area.x1,
166 self._current_area.y1 = (
167 self._absolute_transform.y + self._absolute_transform.dy * self._y
169 self._current_area.y2 = (
170 self._absolute_transform.y
171 + self._absolute_transform.dy * (self._y + height)
173 if self._current_area.y2 < self._current_area.y1:
174 self._current_area.y1, self._current_area.y2 = (
175 self._current_area.y2,
176 self._current_area.y1,
179 def _shade(self, pixel_value):
180 if isinstance(self._pixel_shader, Palette):
181 return self._pixel_shader[pixel_value]
182 if isinstance(self._pixel_shader, ColorConverter):
183 return self._pixel_shader.convert(pixel_value)
186 def _apply_palette(self, image):
188 self._pixel_shader._get_palette() # pylint: disable=protected-access
191 def _add_alpha(self, image):
192 alpha = self._bitmap._image.copy().convert( # pylint: disable=protected-access
196 self._pixel_shader._get_alpha_palette() # pylint: disable=protected-access
198 image.putalpha(alpha.convert("L"))
200 def _fill_area(self, buffer):
201 # pylint: disable=too-many-locals,too-many-branches,too-many-statements
202 """Draw onto the image"""
203 if self._hidden_tilegrid:
206 if self._bitmap.width <= 0 or self._bitmap.height <= 0:
209 # Copy class variables to local variables in case something changes
213 height = self._height
214 tile_width = self._tile_width
215 tile_height = self._tile_height
216 bitmap_width = self._bitmap.width
217 pixel_width = self._pixel_width
218 pixel_height = self._pixel_height
220 absolute_transform = self._absolute_transform
221 pixel_shader = self._pixel_shader
222 bitmap = self._bitmap
225 tile_count_x = bitmap_width // tile_width
229 (width * tile_width, height * tile_height),
233 for tile_x in range(width):
234 for tile_y in range(height):
235 tile_index = tiles[tile_y * width + tile_x]
236 tile_index_x = tile_index % tile_count_x
237 tile_index_y = tile_index // tile_count_x
238 tile_image = bitmap._image # pylint: disable=protected-access
239 if isinstance(pixel_shader, Palette):
240 tile_image = tile_image.copy().convert("P")
241 self._apply_palette(tile_image)
242 tile_image = tile_image.convert("RGBA")
243 self._add_alpha(tile_image)
244 elif isinstance(pixel_shader, ColorConverter):
245 # This will be needed for eInks, grayscale, and monochrome displays
247 image.alpha_composite(
249 dest=(tile_x * tile_width, tile_y * tile_height),
251 tile_index_x * tile_width,
252 tile_index_y * tile_height,
253 tile_index_x * tile_width + tile_width,
254 tile_index_y * tile_height + tile_height,
258 if absolute_transform is not None:
259 if absolute_transform.scale > 1:
260 image = image.resize(
262 int(pixel_width * absolute_transform.scale),
264 pixel_height * absolute_transform.scale,
267 resample=Image.NEAREST,
269 if absolute_transform.mirror_x:
270 image = image.transpose(Image.FLIP_LEFT_RIGHT)
271 if absolute_transform.mirror_y:
272 image = image.transpose(Image.FLIP_TOP_BOTTOM)
273 if absolute_transform.transpose_xy:
274 image = image.transpose(Image.TRANSPOSE)
275 x *= absolute_transform.dx
276 y *= absolute_transform.dy
277 x += absolute_transform.x
278 y += absolute_transform.y
280 source_x = source_y = 0
282 source_x = round(0 - x)
285 source_y = round(0 - y)
293 and y <= buffer.height
294 and source_x <= image.width
295 and source_y <= image.height
297 buffer.alpha_composite(image, (x, y), source=(source_x, source_y))
300 def hidden(self) -> bool:
301 """True when the TileGrid is hidden. This may be False even
302 when a part of a hidden Group."""
303 return self._hidden_tilegrid
306 def hidden(self, value: bool):
307 if not isinstance(value, (bool, int)):
308 raise ValueError("Expecting a boolean or integer value")
309 self._hidden_tilegrid = bool(value)
313 """X position of the left edge in the parent."""
317 def x(self, value: int):
318 if not isinstance(value, int):
319 raise TypeError("X should be a integer type")
322 self._update_current_x()
326 """Y position of the top edge in the parent."""
330 def y(self, value: int):
331 if not isinstance(value, int):
332 raise TypeError("Y should be a integer type")
335 self._update_current_y()
338 def flip_x(self) -> bool:
339 """If true, the left edge rendered will be the right edge of the right-most tile."""
343 def flip_x(self, value: bool):
344 if not isinstance(value, bool):
345 raise TypeError("Flip X should be a boolean type")
346 if self._flip_x != value:
350 def flip_y(self) -> bool:
351 """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
355 def flip_y(self, value: bool):
356 if not isinstance(value, bool):
357 raise TypeError("Flip Y should be a boolean type")
358 if self._flip_y != value:
362 def transpose_xy(self) -> bool:
363 """If true, the TileGrid's axis will be swapped. When combined with mirroring, any 90
364 degree rotation can be achieved along with the corresponding mirrored version.
366 return self._transpose_xy
369 def transpose_xy(self, value: bool):
370 if not isinstance(value, bool):
371 raise TypeError("Transpose XY should be a boolean type")
372 if self._transpose_xy != value:
373 self._transpose_xy = value
374 self._update_current_x()
375 self._update_current_y()
378 def pixel_shader(self) -> Union[ColorConverter, Palette]:
379 """The pixel shader of the tilegrid."""
380 return self._pixel_shader
382 def _extract_and_check_index(self, index):
383 if isinstance(index, (tuple, list)):
386 index = y * self._width + x
387 elif isinstance(index, int):
388 x = index % self._width
389 y = index // self._width
390 if x > self._width or y > self._height or index >= len(self._tiles):
391 raise ValueError("Tile index out of bounds")
394 def __getitem__(self, index: Union[Tuple[int, int], int]) -> int:
395 """Returns the tile index at the given index. The index can either be
396 an x,y tuple or an int equal to ``y * width + x``'.
398 index = self._extract_and_check_index(index)
399 return self._tiles[index]
401 def __setitem__(self, index: Union[Tuple[int, int], int], value: int) -> None:
402 """Sets the tile index at the given index. The index can either be
403 an x,y tuple or an int equal to ``y * width + x``.
405 index = self._extract_and_check_index(index)
406 if not 0 <= value <= 255:
407 raise ValueError("Tile value out of bounds")
408 self._tiles[index] = value