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
21 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 TransformStruct, InputPixelStruct, OutputPixelStruct
28 from ._colorspace import Colorspace
29 from ._area import Area
31 __version__ = "0.0.0+auto.0"
32 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
36 # pylint: disable=too-many-instance-attributes
37 """Position a grid of tiles sourced from a bitmap and pixel_shader combination. Multiple
38 grids can share bitmaps and pixel shaders.
40 A single tile grid is also known as a Sprite.
45 bitmap: Union[Bitmap, OnDiskBitmap, Shape],
47 pixel_shader: Union[ColorConverter, Palette],
50 tile_width: Optional[int] = None,
51 tile_height: Optional[int] = None,
52 default_tile: int = 0,
56 """Create a TileGrid object. The bitmap is source for 2d pixels. The pixel_shader is
57 used to convert the value and its location to a display native pixel color. This may
58 be a simple color palette lookup, a gradient, a pattern or a color transformer.
60 tile_width and tile_height match the height of the bitmap by default.
62 if not isinstance(bitmap, (Bitmap, OnDiskBitmap, Shape)):
63 raise ValueError("Unsupported Bitmap type")
65 bitmap_width = bitmap.width
66 bitmap_height = bitmap.height
68 if pixel_shader is not None and not isinstance(
69 pixel_shader, (ColorConverter, Palette)
71 raise ValueError("Unsupported Pixel Shader type")
72 self._pixel_shader = pixel_shader
73 if isinstance(self._pixel_shader, ColorConverter):
74 self._pixel_shader._rgba = True # pylint: disable=protected-access
75 self._hidden_tilegrid = False
78 self._width_in_tiles = width
79 self._height_in_tiles = height
80 self._transpose_xy = False
85 if tile_width is None or tile_width == 0:
86 tile_width = bitmap_width
87 if tile_height is None or tile_width == 0:
88 tile_height = bitmap_height
89 if tile_width < 1 or tile_height < 1:
90 raise ValueError("Tile width and height must be greater than 0")
91 if bitmap_width % tile_width != 0:
92 raise ValueError("Tile width must exactly divide bitmap width")
93 self._tile_width = tile_width
94 if bitmap_height % tile_height != 0:
95 raise ValueError("Tile height must exactly divide bitmap height")
96 self._tile_height = tile_height
97 if not 0 <= default_tile <= 255:
98 raise ValueError("Default Tile is out of range")
99 self._pixel_width = width * tile_width
100 self._pixel_height = height * tile_height
101 self._tiles = (self._width_in_tiles * self._height_in_tiles) * [default_tile]
102 self._in_group = False
103 self._absolute_transform = TransformStruct(0, 0, 1, 1, 1, False, False, False)
104 self._current_area = Area(0, 0, self._pixel_width, self._pixel_height)
106 self._bitmap_width_in_tiles = bitmap_width // tile_width
107 self._tiles_in_bitmap = self._bitmap_width_in_tiles * (
108 bitmap_height // tile_height
110 self.inline_tiles = False # We have plenty of memory
112 def _update_transform(self, absolute_transform):
113 """Update the parent transform and child transforms"""
114 self._absolute_transform = absolute_transform
115 if self._absolute_transform is not None:
116 self._update_current_x()
117 self._update_current_y()
119 def _update_current_x(self):
120 if self._transpose_xy:
121 width = self._pixel_height
123 width = self._pixel_width
124 if self._absolute_transform.transpose_xy:
125 self._current_area.y1 = (
126 self._absolute_transform.y + self._absolute_transform.dy * self._x
128 self._current_area.y2 = (
129 self._absolute_transform.y
130 + self._absolute_transform.dy * (self._x + width)
132 if self._current_area.y2 < self._current_area.y1:
133 self._current_area.y1, self._current_area.y2 = (
134 self._current_area.y2,
135 self._current_area.y1,
138 self._current_area.x1 = (
139 self._absolute_transform.x + self._absolute_transform.dx * self._x
141 self._current_area.x2 = (
142 self._absolute_transform.x
143 + self._absolute_transform.dx * (self._x + width)
145 if self._current_area.x2 < self._current_area.x1:
146 self._current_area.x1, self._current_area.x2 = (
147 self._current_area.x2,
148 self._current_area.x1,
151 def _update_current_y(self):
152 if self._transpose_xy:
153 height = self._pixel_width
155 height = self._pixel_height
156 if self._absolute_transform.transpose_xy:
157 self._current_area.x1 = (
158 self._absolute_transform.x + self._absolute_transform.dx * self._y
160 self._current_area.x2 = (
161 self._absolute_transform.x
162 + self._absolute_transform.dx * (self._y + height)
164 if self._current_area.x2 < self._current_area.x1:
165 self._current_area.x1, self._current_area.x2 = (
166 self._current_area.x2,
167 self._current_area.x1,
170 self._current_area.y1 = (
171 self._absolute_transform.y + self._absolute_transform.dy * self._y
173 self._current_area.y2 = (
174 self._absolute_transform.y
175 + self._absolute_transform.dy * (self._y + height)
177 if self._current_area.y2 < self._current_area.y1:
178 self._current_area.y1, self._current_area.y2 = (
179 self._current_area.y2,
180 self._current_area.y1,
183 def _shade(self, pixel_value):
184 if isinstance(self._pixel_shader, Palette):
185 return self._pixel_shader[pixel_value]
186 if isinstance(self._pixel_shader, ColorConverter):
187 return self._pixel_shader.convert(pixel_value)
190 def _apply_palette(self, image):
192 self._pixel_shader._get_palette() # pylint: disable=protected-access
195 def _add_alpha(self, image):
196 alpha = self._bitmap._image.copy().convert( # pylint: disable=protected-access
200 self._pixel_shader._get_alpha_palette() # pylint: disable=protected-access
202 image.putalpha(alpha.convert("L"))
205 self, colorspace: Colorspace, area: Area, mask: bytearray, buffer: bytearray
207 """Draw onto the image"""
208 # pylint: disable=too-many-locals,too-many-branches,too-many-statements
210 # If no tiles are present we have no impact
213 if self._hidden_tilegrid:
217 if not self._current_area.compute_overlap(area, overlap):
220 if self._bitmap.width <= 0 or self._bitmap.height <= 0:
224 y_stride = area.width()
226 flip_x = self._flip_x
227 flip_y = self._flip_y
228 if self._transpose_xy != self._absolute_transform.transpose_xy:
229 flip_x, flip_y = flip_y, flip_x
232 if (self._absolute_transform.dx < 0) != flip_x:
233 start += (area.width() - 1) * x_stride
235 if (self._absolute_transform.dy < 0) != flip_y:
236 start += (area.height() - 1) * y_stride
239 full_coverage = area == overlap
242 area.transform_within(
243 flip_x != (self._absolute_transform.dx < 0),
244 flip_y != (self._absolute_transform.dy < 0),
245 self.transpose_xy != self._absolute_transform.transpose_xy,
251 start_x = transformed.x1 - self._current_area.x1
252 end_x = transformed.x2 - self._current_area.x1
253 start_y = transformed.y1 - self._current_area.y1
254 end_y = transformed.y2 - self._current_area.y1
256 if (self._absolute_transform.dx < 0) != flip_x:
257 x_shift = area.x2 - overlap.x2
259 x_shift = overlap.x1 - area.x1
260 if (self._absolute_transform.dy < 0) != flip_y:
261 y_shift = area.y2 - overlap.y2
263 y_shift = overlap.y1 - area.y1
265 if self._transpose_xy != self._absolute_transform.transpose_xy:
266 x_stride, y_stride = y_stride, x_stride
267 x_shift, y_shift = y_shift, x_shift
269 pixels_per_byte = 8 // colorspace.depth
271 input_pixel = InputPixelStruct()
272 output_pixel = OutputPixelStruct()
273 for input_pixel.y in range(start_y, end_y):
275 start + (input_pixel.y - start_y + y_shift) * y_stride
277 local_y = input_pixel.y // self._absolute_transform.scale
278 for input_pixel.x in range(start_x, end_x):
280 row_start + (input_pixel.x - start_x + x_shift) * x_stride
283 # Check the mask first to see if the pixel has already been set
284 if mask[offset // 32] & (1 << (offset % 32)):
286 local_x = input_pixel.x // self._absolute_transform.scale
288 (local_y // self._tile_height + self._top_left_y)
289 % self._height_in_tiles
290 ) * self._width_in_tiles + (
291 local_x // self._tile_width + self._top_left_x
292 ) % self._width_in_tiles
293 input_pixel.tile = tiles[tile_location]
294 input_pixel.tile_x = (
295 input_pixel.tile % self._bitmap_width_in_tiles
296 ) * self._tile_width + local_x % self._tile_width
297 input_pixel.tile_y = (
298 input_pixel.tile // self._bitmap_width_in_tiles
299 ) * self._tile_height + local_y % self._tile_height
301 input_pixel.pixel = self.bitmap[input_pixel.tile_x, input_pixel.tile_y]
302 output_pixel.opaque = True
304 if self._pixel_shader is None:
305 output_pixel.pixel = input_pixel.pixel
306 elif isinstance(self._pixel_shader, Palette):
307 self._pixel_shader._get_color( # pylint: disable=protected-access
308 colorspace, input_pixel, output_pixel
310 elif isinstance(self._pixel_shader, ColorConverter):
311 self._pixel_shader._convert( # pylint: disable=protected-access
312 colorspace, input_pixel, output_pixel
315 if not output_pixel.opaque:
316 full_coverage = False
318 mask[offset // 32] |= 1 << (offset % 32)
319 if colorspace.depth == 16:
322 + struct.pack("H", output_pixel.pixel)
323 + buffer[offset + 2 :]
325 elif colorspace.depth == 32:
328 + struct.pack("I", output_pixel.pixel)
329 + buffer[offset + 4 :]
331 elif colorspace.depth == 8:
332 buffer[offset] = output_pixel.pixel & 0xFF
333 elif colorspace.depth < 8:
334 # Reorder the offsets to pack multiple rows into
335 # a byte (meaning they share a column).
336 if not colorspace.pixels_in_byte_share_row:
338 row = offset // width
340 # Dividing by pixels_per_byte does truncated division
341 # even if we multiply it back out
343 col * pixels_per_byte
344 + (row // pixels_per_byte) * width
345 + (row % pixels_per_byte)
347 shift = (offset % pixels_per_byte) * colorspace.depth
348 if colorspace.reverse_pixels_in_byte:
349 # Reverse the shift by subtracting it from the leftmost shift
350 shift = (pixels_per_byte - 1) * colorspace.depth - shift
351 buffer[offset // pixels_per_byte] |= output_pixel.pixel << shift
354 def _finish_refresh(self):
358 def hidden(self) -> bool:
359 """True when the TileGrid is hidden. This may be False even
360 when a part of a hidden Group."""
361 return self._hidden_tilegrid
364 def hidden(self, value: bool):
365 if not isinstance(value, (bool, int)):
366 raise ValueError("Expecting a boolean or integer value")
367 self._hidden_tilegrid = bool(value)
371 """X position of the left edge in the parent."""
375 def x(self, value: int):
376 if not isinstance(value, int):
377 raise TypeError("X should be a integer type")
380 self._update_current_x()
384 """Y position of the top edge in the parent."""
388 def y(self, value: int):
389 if not isinstance(value, int):
390 raise TypeError("Y should be a integer type")
393 self._update_current_y()
396 def flip_x(self) -> bool:
397 """If true, the left edge rendered will be the right edge of the right-most tile."""
401 def flip_x(self, value: bool):
402 if not isinstance(value, bool):
403 raise TypeError("Flip X should be a boolean type")
404 if self._flip_x != value:
408 def flip_y(self) -> bool:
409 """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
413 def flip_y(self, value: bool):
414 if not isinstance(value, bool):
415 raise TypeError("Flip Y should be a boolean type")
416 if self._flip_y != value:
420 def transpose_xy(self) -> bool:
421 """If true, the TileGrid's axis will be swapped. When combined with mirroring, any 90
422 degree rotation can be achieved along with the corresponding mirrored version.
424 return self._transpose_xy
427 def transpose_xy(self, value: bool):
428 if not isinstance(value, bool):
429 raise TypeError("Transpose XY should be a boolean type")
430 if self._transpose_xy != value:
431 self._transpose_xy = value
432 self._update_current_x()
433 self._update_current_y()
436 def pixel_shader(self) -> Union[ColorConverter, Palette]:
437 """The pixel shader of the tilegrid."""
438 return self._pixel_shader
441 def pixel_shader(self, new_pixel_shader: Union[ColorConverter, Palette]) -> None:
442 if not isinstance(new_pixel_shader, ColorConverter) and not isinstance(
443 new_pixel_shader, Palette
446 "Unsupported Type: new_pixel_shader must be ColorConverter or Palette"
449 self._pixel_shader = new_pixel_shader
452 def bitmap(self) -> Union[Bitmap, OnDiskBitmap, Shape]:
453 """The Bitmap, OnDiskBitmap, or Shape that is assigned to this TileGrid"""
457 def bitmap(self, new_bitmap: Union[Bitmap, OnDiskBitmap, Shape]) -> None:
459 not isinstance(new_bitmap, Bitmap)
460 and not isinstance(new_bitmap, OnDiskBitmap)
461 and not isinstance(new_bitmap, Shape)
464 "Unsupported Type: new_bitmap must be Bitmap, OnDiskBitmap, or Shape"
468 new_bitmap.width != self.bitmap.width
469 or new_bitmap.height != self.bitmap.height
471 raise ValueError("New bitmap must be same size as old bitmap")
473 self._bitmap = new_bitmap
475 def _extract_and_check_index(self, index):
476 if isinstance(index, (tuple, list)):
479 index = y * self._width_in_tiles + x
480 elif isinstance(index, int):
481 x = index % self._width_in_tiles
482 y = index // self._width_in_tiles
484 x > self._width_in_tiles
485 or y > self._height_in_tiles
486 or index >= len(self._tiles)
488 raise ValueError("Tile index out of bounds")
491 def __getitem__(self, index: Union[Tuple[int, int], int]) -> int:
492 """Returns the tile index at the given index. The index can either be
493 an x,y tuple or an int equal to ``y * width + x``'.
495 index = self._extract_and_check_index(index)
496 return self._tiles[index]
498 def __setitem__(self, index: Union[Tuple[int, int], int], value: int) -> None:
499 """Sets the tile index at the given index. The index can either be
500 an x,y tuple or an int equal to ``y * width + x``.
502 index = self._extract_and_check_index(index)
503 if not 0 <= value <= 255:
504 raise ValueError("Tile value out of bounds")
505 self._tiles[index] = value
508 def width(self) -> int:
510 return self._width_in_tiles
513 def height(self) -> int:
514 """Height in tiles"""
515 return self._height_in_tiles
518 def tile_width(self) -> int:
519 """Width of each tile in pixels"""
520 return self._tile_width
523 def tile_height(self) -> int:
524 """Height of each tile in pixels"""
525 return self._tile_height