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
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:
103 tile_width = bitmap_width
104 if tile_height is None:
105 tile_height = bitmap_height
106 if tile_width > 0 and bitmap_width % tile_width != 0:
107 raise ValueError("Tile width must exactly divide bitmap width")
108 self._tile_width = tile_width
109 if tile_height > 0 and bitmap_height % tile_height != 0:
110 raise ValueError("Tile height must exactly divide bitmap height")
111 self._tile_height = tile_height
112 if not 0 <= default_tile <= 255:
113 raise ValueError("Default Tile is out of range")
114 self._pixel_width = width * tile_width
115 self._pixel_height = height * tile_height
116 self._tiles = (self._width * self._height) * [default_tile]
117 self.in_group = False
118 self._absolute_transform = Transform(0, 0, 1, 1, 1, False, False, False)
119 self._current_area = Rectangle(0, 0, self._pixel_width, self._pixel_height)
122 def update_transform(self, absolute_transform):
123 """Update the parent transform and child transforms"""
124 self._absolute_transform = absolute_transform
125 if self._absolute_transform is not None:
126 self._update_current_x()
127 self._update_current_y()
129 def _update_current_x(self):
130 if self._transpose_xy:
131 width = self._pixel_height
133 width = self._pixel_width
134 if self._absolute_transform.transpose_xy:
135 self._current_area.y1 = (
136 self._absolute_transform.y + self._absolute_transform.dy * self._x
138 self._current_area.y2 = (
139 self._absolute_transform.y
140 + self._absolute_transform.dy * (self._x + width)
142 if self._current_area.y2 < self._current_area.y1:
143 self._current_area.y1, self._current_area.y2 = (
144 self._current_area.y2,
145 self._current_area.y1,
148 self._current_area.x1 = (
149 self._absolute_transform.x + self._absolute_transform.dx * self._x
151 self._current_area.x2 = (
152 self._absolute_transform.x
153 + self._absolute_transform.dx * (self._x + width)
155 if self._current_area.x2 < self._current_area.x1:
156 self._current_area.x1, self._current_area.x2 = (
157 self._current_area.x2,
158 self._current_area.x1,
161 def _update_current_y(self):
162 if self._transpose_xy:
163 height = self._pixel_width
165 height = self._pixel_height
166 if self._absolute_transform.transpose_xy:
167 self._current_area.x1 = (
168 self._absolute_transform.x + self._absolute_transform.dx * self._y
170 self._current_area.x2 = (
171 self._absolute_transform.x
172 + self._absolute_transform.dx * (self._y + height)
174 if self._current_area.x2 < self._current_area.x1:
175 self._current_area.x1, self._current_area.x2 = (
176 self._current_area.x2,
177 self._current_area.x1,
180 self._current_area.y1 = (
181 self._absolute_transform.y + self._absolute_transform.dy * self._y
183 self._current_area.y2 = (
184 self._absolute_transform.y
185 + self._absolute_transform.dy * (self._y + height)
187 if self._current_area.y2 < self._current_area.y1:
188 self._current_area.y1, self._current_area.y2 = (
189 self._current_area.y2,
190 self._current_area.y1,
193 def _shade(self, pixel_value):
194 if isinstance(self._pixel_shader, Palette):
195 return self._pixel_shader[pixel_value]["rgba"]
196 if isinstance(self._pixel_shader, ColorConverter):
197 return self._pixel_shader.convert(pixel_value)
200 def _apply_palette(self, image):
202 self._pixel_shader._get_palette() # pylint: disable=protected-access
205 def _add_alpha(self, image):
206 alpha = self._bitmap._image.copy().convert( # pylint: disable=protected-access
210 self._pixel_shader._get_alpha_palette() # pylint: disable=protected-access
212 image.putalpha(alpha.convert("L"))
214 # pylint: disable=too-many-locals
215 def _fill_area(self, buffer):
216 """Draw onto the image"""
222 (self._width * self._tile_width, self._height * self._tile_height),
226 tile_count_x = self._bitmap.width // self._tile_width
230 for tile_x in range(self._width):
231 for tile_y in range(self._height):
232 tile_index = self._tiles[tile_y * self._width + tile_x]
233 tile_index_x = tile_index % tile_count_x
234 tile_index_y = tile_index // tile_count_x
235 tile_image = self._bitmap._image # pylint: disable=protected-access
236 if isinstance(self._pixel_shader, Palette):
237 tile_image = tile_image.copy().convert("P")
238 self._apply_palette(tile_image)
239 tile_image = tile_image.convert("RGBA")
240 self._add_alpha(tile_image)
241 elif isinstance(self._pixel_shader, ColorConverter):
242 # This will be needed for eInks, grayscale, and monochrome displays
244 image.alpha_composite(
246 dest=(tile_x * self._tile_width, tile_y * self._tile_height),
248 tile_index_x * self._tile_width,
249 tile_index_y * self._tile_height,
253 if self._absolute_transform is not None:
254 if self._absolute_transform.scale > 1:
255 image = image.resize(
257 self._pixel_width * self._absolute_transform.scale,
258 self._pixel_height * self._absolute_transform.scale,
260 resample=Image.NEAREST,
262 if self._absolute_transform.mirror_x:
263 image = image.transpose(Image.FLIP_LEFT_RIGHT)
264 if self._absolute_transform.mirror_y:
265 image = image.transpose(Image.FLIP_TOP_BOTTOM)
266 if self._absolute_transform.transpose_xy:
267 image = image.transpose(Image.TRANSPOSE)
268 x *= self._absolute_transform.dx
269 y *= self._absolute_transform.dy
270 x += self._absolute_transform.x
271 y += self._absolute_transform.y
272 buffer.alpha_composite(image, (x, y))
274 # pylint: enable=too-many-locals
278 """True when the TileGrid is hidden. This may be False even
279 when a part of a hidden Group."""
283 def hidden(self, value):
284 if not isinstance(value, (bool, int)):
285 raise ValueError("Expecting a boolean or integer value")
286 self._hidden = bool(value)
290 """X position of the left edge in the parent."""
295 if not isinstance(value, int):
296 raise TypeError("X should be a integer type")
299 self._update_current_x()
303 """Y position of the top edge in the parent."""
308 if not isinstance(value, int):
309 raise TypeError("Y should be a integer type")
312 self._update_current_y()
316 """If true, the left edge rendered will be the right edge of the right-most tile."""
320 def flip_x(self, value):
321 if not isinstance(value, bool):
322 raise TypeError("Flip X should be a boolean type")
323 if self._flip_x != value:
328 """If true, the top edge rendered will be the bottom edge of the bottom-most tile."""
332 def flip_y(self, value):
333 if not isinstance(value, bool):
334 raise TypeError("Flip Y should be a boolean type")
335 if self._flip_y != value:
339 def transpose_xy(self):
340 """If true, the TileGrid’s axis will be swapped. When combined with mirroring, any 90
341 degree rotation can be achieved along with the corresponding mirrored version.
343 return self._transpose_xy
346 def transpose_xy(self, value):
347 if not isinstance(value, bool):
348 raise TypeError("Transpose XY should be a boolean type")
349 if self._transpose_xy != value:
350 self._transpose_xy = value
351 self._update_current_x()
352 self._update_current_y()
355 def pixel_shader(self):
356 """The pixel shader of the tilegrid."""
357 return self._pixel_shader
359 def __getitem__(self, index):
360 """Returns the tile index at the given index. The index can either be
361 an x,y tuple or an int equal to ``y * width + x``'.
363 if isinstance(index, (tuple, list)):
366 index = y * self._width + x
367 elif isinstance(index, int):
368 x = index % self._width
369 y = index // self._width
370 if x > self._width or y > self._height or index >= len(self._tiles):
371 raise ValueError("Tile index out of bounds")
372 return self._tiles[index]
374 def __setitem__(self, index, value):
375 """Sets the tile index at the given index. The index can either be
376 an x,y tuple or an int equal to ``y * width + x``.
378 if isinstance(index, (tuple, list)):
381 index = y * self._width + x
382 elif isinstance(index, int):
383 x = index % self._width
384 y = index // self._width
385 if x > self._width or y > self._height or index >= len(self._tiles):
386 raise ValueError("Tile index out of bounds")
387 if not 0 <= value <= 255:
388 raise ValueError("Tile value out of bounds")
389 self._tiles[index] = value
392 # pylint: enable=too-many-instance-attributes