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 recordclass import recordclass
22 from displayio.bitmap import Bitmap
23 from displayio.colorconverter import ColorConverter
24 from displayio.ondiskbitmap import OnDiskBitmap
25 from displayio.shape import Shape
26 from displayio.palette import Palette
28 __version__ = "0.0.0-auto.0"
29 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
31 Rectangle = recordclass("Rectangle", "x1 y1 x2 y2")
32 Transform = recordclass("Transform", "x y dx dy scale transpose_xy mirror_x mirror_y")
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.
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 = width # Number of Tiles Wide
79 self._height = height # Number of Tiles High
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
93 if bitmap_width % tile_width != 0:
94 raise ValueError("Tile width must exactly divide bitmap width")
95 self._tile_width = tile_width
96 if bitmap_height % tile_height != 0:
97 raise ValueError("Tile height must exactly divide bitmap height")
98 self._tile_height = tile_height
99 if not 0 <= default_tile <= 255:
100 raise ValueError("Default Tile is out of range")
101 self._pixel_width = width * tile_width
102 self._pixel_height = height * tile_height
103 self._tiles = (self._width * self._height) * [default_tile]
104 self.in_group = False
105 self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
106 self._current_area = Rectangle(0, 0, self._pixel_width, self._pixel_height)
109 def update_transform(self, absolute_transform):
110 """Update the parent transform and child transforms"""
111 self._absolute_transform = absolute_transform
112 if self._absolute_transform is not None:
113 self._update_current_x()
114 self._update_current_y()
116 def _update_current_x(self):
117 if self._transpose_xy:
118 width = self._pixel_height
120 width = self._pixel_width
121 if self._absolute_transform.transpose_xy:
122 self._current_area.y1 = (
123 self._absolute_transform.y + self._absolute_transform.dy * self._x
125 self._current_area.y2 = (
126 self._absolute_transform.y
127 + self._absolute_transform.dy * (self._x + width)
129 if self._current_area.y2 < self._current_area.y1:
130 self._current_area.y1, self._current_area.y2 = (
131 self._current_area.y2,
132 self._current_area.y1,
135 self._current_area.x1 = (
136 self._absolute_transform.x + self._absolute_transform.dx * self._x
138 self._current_area.x2 = (
139 self._absolute_transform.x
140 + self._absolute_transform.dx * (self._x + width)
142 if self._current_area.x2 < self._current_area.x1:
143 self._current_area.x1, self._current_area.x2 = (
144 self._current_area.x2,
145 self._current_area.x1,
148 def _update_current_y(self):
149 if self._transpose_xy:
150 height = self._pixel_width
152 height = self._pixel_height
153 if self._absolute_transform.transpose_xy:
154 self._current_area.x1 = (
155 self._absolute_transform.x + self._absolute_transform.dx * self._y
157 self._current_area.x2 = (
158 self._absolute_transform.x
159 + self._absolute_transform.dx * (self._y + height)
161 if self._current_area.x2 < self._current_area.x1:
162 self._current_area.x1, self._current_area.x2 = (
163 self._current_area.x2,
164 self._current_area.x1,
167 self._current_area.y1 = (
168 self._absolute_transform.y + self._absolute_transform.dy * self._y
170 self._current_area.y2 = (
171 self._absolute_transform.y
172 + self._absolute_transform.dy * (self._y + height)
174 if self._current_area.y2 < self._current_area.y1:
175 self._current_area.y1, self._current_area.y2 = (
176 self._current_area.y2,
177 self._current_area.y1,
180 def _shade(self, pixel_value):
181 if isinstance(self._pixel_shader, Palette):
182 return self._pixel_shader[pixel_value]["rgba"]
183 if isinstance(self._pixel_shader, ColorConverter):
184 return self._pixel_shader.convert(pixel_value)
187 def _apply_palette(self, image):
189 self._pixel_shader._get_palette() # pylint: disable=protected-access
192 def _add_alpha(self, image):
193 alpha = self._bitmap._image.copy().convert( # pylint: disable=protected-access
197 self._pixel_shader._get_alpha_palette() # pylint: disable=protected-access
199 image.putalpha(alpha.convert("L"))
201 def _fill_area(self, buffer):
202 # pylint: disable=too-many-locals,too-many-branches,too-many-statements
203 """Draw onto the image"""
204 if self._hidden_tilegrid:
207 if self._bitmap.width <= 0 or self._bitmap.height <= 0:
210 # Copy class variables to local variables in case something changes
214 height = self._height
215 tile_width = self._tile_width
216 tile_height = self._tile_height
217 bitmap_width = self._bitmap.width
218 pixel_width = self._pixel_width
219 pixel_height = self._pixel_height
221 absolute_transform = self._absolute_transform
222 pixel_shader = self._pixel_shader
223 bitmap = self._bitmap
226 tile_count_x = bitmap_width // tile_width
230 (width * tile_width, height * tile_height),
234 for tile_x in range(width):
235 for tile_y in range(height):
236 tile_index = tiles[tile_y * width + tile_x]
237 tile_index_x = tile_index % tile_count_x
238 tile_index_y = tile_index // tile_count_x
239 tile_image = bitmap._image # pylint: disable=protected-access
240 if isinstance(pixel_shader, Palette):
241 tile_image = tile_image.copy().convert("P")
242 self._apply_palette(tile_image)
243 tile_image = tile_image.convert("RGBA")
244 self._add_alpha(tile_image)
245 elif isinstance(pixel_shader, ColorConverter):
246 # This will be needed for eInks, grayscale, and monochrome displays
248 image.alpha_composite(
250 dest=(tile_x * tile_width, tile_y * tile_height),
252 tile_index_x * tile_width,
253 tile_index_y * tile_height,
254 tile_index_x * tile_width + tile_width,
255 tile_index_y * tile_height + tile_height,
259 if absolute_transform is not None:
260 if absolute_transform.scale > 1:
261 image = image.resize(
263 int(pixel_width * absolute_transform.scale),
265 pixel_height * absolute_transform.scale,
268 resample=Image.NEAREST,
270 if absolute_transform.mirror_x:
271 image = image.transpose(Image.FLIP_LEFT_RIGHT)
272 if absolute_transform.mirror_y:
273 image = image.transpose(Image.FLIP_TOP_BOTTOM)
274 if absolute_transform.transpose_xy:
275 image = image.transpose(Image.TRANSPOSE)
276 x *= absolute_transform.dx
277 y *= absolute_transform.dy
278 x += absolute_transform.x
279 y += absolute_transform.y
281 source_x = source_y = 0
283 source_x = round(0 - x)
286 source_y = round(0 - y)
294 and y <= buffer.height
295 and source_x <= image.width
296 and source_y <= image.height
298 buffer.alpha_composite(image, (x, y), source=(source_x, source_y))
302 """True when the TileGrid is hidden. This may be False even
303 when a part of a hidden Group."""
304 return self._hidden_tilegrid
307 def hidden(self, value):
308 if not isinstance(value, (bool, int)):
309 raise ValueError("Expecting a boolean or integer value")
310 self._hidden_tilegrid = bool(value)
314 """X position of the left edge in the parent."""
319 if not isinstance(value, int):
320 raise TypeError("X should be a integer type")
323 self._update_current_x()
327 """Y position of the top edge in the parent."""
332 if not isinstance(value, int):
333 raise TypeError("Y should be a integer type")
336 self._update_current_y()
340 """If true, the left edge rendered will be the right edge of the right-most tile."""
344 def flip_x(self, value):
345 if not isinstance(value, bool):
346 raise TypeError("Flip X should be a boolean type")
347 if self._flip_x != value:
352 """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
356 def flip_y(self, value):
357 if not isinstance(value, bool):
358 raise TypeError("Flip Y should be a boolean type")
359 if self._flip_y != value:
363 def transpose_xy(self):
364 """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90
365 degree rotation can be achieved along with the corresponding mirrored version.
367 return self._transpose_xy
370 def transpose_xy(self, value):
371 if not isinstance(value, bool):
372 raise TypeError("Transpose XY should be a boolean type")
373 if self._transpose_xy != value:
374 self._transpose_xy = value
375 self._update_current_x()
376 self._update_current_y()
379 def pixel_shader(self):
380 """The pixel shader of the tilegrid."""
381 return self._pixel_shader
383 def _extract_and_check_index(self, index):
384 if isinstance(index, (tuple, list)):
387 index = y * self._width + x
388 elif isinstance(index, int):
389 x = index % self._width
390 y = index // self._width
391 if x > self._width or y > self._height or index >= len(self._tiles):
392 raise ValueError("Tile index out of bounds")
395 def __getitem__(self, index):
396 """Returns the tile index at the given index. The index can either be
397 an x,y tuple or an int equal to ``y * width + x``'.
399 index = self._extract_and_check_index(index)
400 return self._tiles[index]
402 def __setitem__(self, index, value):
403 """Sets the tile index at the given index. The index can either be
404 an x,y tuple or an int equal to ``y * width + x``.
406 index = self._extract_and_check_index(index)
407 if not 0 <= value <= 255:
408 raise ValueError("Tile value out of bounds")
409 self._tiles[index] = value