1 # SPDX-FileCopyrightText: 2020 Melissa LeBlanc-Williams for Adafruit Industries
3 # SPDX-License-Identifier: MIT
6 `vectorio._vectorshape`
7 ================================================================================
11 **Software and Dependencies:**
14 https://github.com/adafruit/Adafruit_Blinka/releases
16 * Author(s): Melissa LeBlanc-Williams
21 from typing import Union, Tuple
22 from circuitpython_typing import WriteableBuffer
23 from displayio._colorconverter import ColorConverter
24 from displayio._colorspace import Colorspace
25 from displayio._palette import Palette
26 from displayio._area import Area
27 from displayio._structs import null_transform, InputPixelStruct, OutputPixelStruct
29 __version__ = "0.0.0+auto.0"
30 __repo__ = "https://github.com/adafruit/Adafruit_Blinka_displayio.git"
36 pixel_shader: Union[ColorConverter, Palette],
42 self._pixel_shader = pixel_shader
44 self._current_area_dirty = True
45 self._current_area = Area(0, 0, 0, 0)
46 self._ephemeral_dirty_area = Area(0, 0, 0, 0)
47 self._absolute_transform = null_transform
48 self._get_screen_area(self._current_area)
52 """X position of the center point of the circle in the parent."""
56 def x(self, value: int) -> None:
60 self._shape_set_dirty()
64 """Y position of the center point of the circle in the parent."""
68 def y(self, value: int) -> None:
72 self._shape_set_dirty()
75 def hidden(self) -> bool:
76 """Hide the circle or not."""
80 def hidden(self, value: bool) -> None:
82 self._shape_set_dirty()
85 def location(self) -> Tuple[int, int]:
86 """(X,Y) position of the center point of the circle in the parent."""
87 return (self._x, self._y)
90 def location(self, value: Tuple[int, int]) -> None:
92 raise ValueError("location must be a list or tuple with exactly 2 integers")
103 self._shape_set_dirty()
106 def pixel_shader(self) -> Union[ColorConverter, Palette]:
107 """The pixel shader of the circle."""
108 return self._pixel_shader
111 def pixel_shader(self, value: Union[ColorConverter, Palette]) -> None:
112 self._pixel_shader = value
114 def _get_area(self, _out_area: Area) -> Area:
115 raise NotImplementedError("Subclass must implement _get_area")
117 def _get_pixel(self, _x: int, _y: int) -> int:
118 raise NotImplementedError("Subclass must implement _get_pixel")
120 def _shape_set_dirty(self) -> None:
121 current_area = Area()
122 self._get_screen_area(current_area)
123 moved = current_area != self._current_area
125 # This will add _current_area (the old position) to dirty area
126 self._current_area.union(
127 self._ephemeral_dirty_area, self._ephemeral_dirty_area
129 # This will add the new position to the dirty area
130 current_area.union(self._ephemeral_dirty_area, self._ephemeral_dirty_area)
131 # Dirty area tracks the shape's footprint between draws. It's reset on refresh finish.
132 current_area.copy_into(self._current_area)
133 self._current_area_dirty = True
135 def _get_dirty_area(self, out_area: Area) -> Area:
136 out_area.x1 = out_area.x2
137 self._ephemeral_dirty_area.union(self._current_area, out_area)
138 return True # For now just always redraw.
140 def _get_screen_area(self, out_area) -> Area:
141 self._get_area(out_area)
142 if self._absolute_transform.transpose_xy:
143 x = self._absolute_transform.x + self._absolute_transform.dx * self._y
144 y = self._absolute_transform.y + self._absolute_transform.dy * self._x
145 if self._absolute_transform.dx < 1:
146 out_area.y1 = out_area.y1 * -1 + 1
147 out_area.y2 = out_area.y2 * -1 + 1
148 if self._absolute_transform.dy < 1:
149 out_area.x1 = out_area.x1 * -1 + 1
150 out_area.x2 = out_area.x2 * -1 + 1
151 self._area_transpose(out_area)
153 x = self._absolute_transform.x + self._absolute_transform.dx * self._x
154 y = self._absolute_transform.y + self._absolute_transform.dy * self._y
155 if self._absolute_transform.dx < 1:
156 out_area.x1 = out_area.x1 * -1 + 1
157 out_area.x2 = out_area.x2 * -1 + 1
158 if self._absolute_transform.dy < 1:
159 out_area.y1 = out_area.y1 * -1 + 1
160 out_area.y2 = out_area.y2 * -1 + 1
165 def _area_transpose(to_transpose: Area) -> Area:
166 to_transpose.x1, to_transpose.y1 = to_transpose.y1, to_transpose.x1
167 to_transpose.x2, to_transpose.y2 = to_transpose.y2, to_transpose.x2
169 def _screen_to_shape_coordinates(self, x: int, y: int) -> Tuple[int, int]:
170 """Get the target pixel based on the shape's coordinate space"""
171 if self._absolute_transform.transpose_xy:
173 y - self._absolute_transform.y - self._absolute_transform.dy * self._x
176 x - self._absolute_transform.x - self._absolute_transform.dx * self._y
179 if self._absolute_transform.dx < 1:
181 if self._absolute_transform.dy < 1:
185 x - self._absolute_transform.x - self._absolute_transform.dx * self._x
188 y - self._absolute_transform.y - self._absolute_transform.dy * self._y
191 if self._absolute_transform.dx < 1:
193 if self._absolute_transform.dy < 1:
196 # It's mirrored via dx. Maybe we need to add support for also separately mirroring?
197 # if self.absolute_transform.mirror_x:
199 # (shape_area.x2 - shape_area.x1)
200 # - (pixel_to_get_x - shape_area.x1)
204 # if self.absolute_transform.mirror_y:
206 # (shape_area.y2 - shape_area.y1)
207 # - (pixel_to_get_y - shape_area.y1)
212 return out_shape_x, out_shape_y
214 def _shape_contains(self, x: int, y: int) -> bool:
215 shape_x, shape_y = self._screen_to_shape_coordinates(x, y)
216 return self._get_pixel(shape_x, shape_y) != 0
220 colorspace: Colorspace,
222 mask: WriteableBuffer,
223 buffer: WriteableBuffer,
225 # pylint: disable=too-many-locals,too-many-branches,too-many-statements
230 if not area.compute_overlap(self._current_area, overlap):
233 full_coverage = area == overlap
234 pixels_per_byte = 8 // colorspace.depth
235 linestride_px = area.width()
236 line_dirty_offset_px = (overlap.y1 - area.y1) * linestride_px
237 column_dirty_offset_px = overlap.x1 - area.x1
239 input_pixel = InputPixelStruct()
240 output_pixel = OutputPixelStruct()
243 self._get_area(shape_area)
245 mask_start_px = line_dirty_offset_px
247 for input_pixel.y in range(overlap.y1, overlap.y2):
248 mask_start_px += column_dirty_offset_px
249 for input_pixel.x in range(overlap.x1, overlap.x2):
250 # Check the mask first to see if the pixel has already been set.
251 pixel_index = mask_start_px + (input_pixel.x - overlap.x1)
252 mask_doubleword = mask[pixel_index // 32]
253 mask_bit = pixel_index % 32
254 if (mask_doubleword & (1 << mask_bit)) != 0:
256 output_pixel.pixel = 0
258 # Cast input screen coordinates to shape coordinates to pick the pixel to draw
259 pixel_to_get_x, pixel_to_get_y = self._screen_to_shape_coordinates(
260 input_pixel.x, input_pixel.y
262 input_pixel.pixel = self._get_pixel(pixel_to_get_x, pixel_to_get_y)
264 # vectorio shapes use 0 to mean "area is not covered."
265 # We can skip all the rest of the work for this pixel
266 # if it's not currently covered by the shape.
267 if input_pixel.pixel == 0:
268 full_coverage = False
270 # Pixel is not transparent. Let's pull the pixel value index down
271 # to 0-base for more error-resistant palettes.
272 input_pixel.pixel -= 1
273 output_pixel.opaque = True
274 if self._pixel_shader is None:
275 output_pixel.pixel = input_pixel.pixel
276 elif isinstance(self._pixel_shader, Palette):
277 self._pixel_shader._get_color( # pylint: disable=protected-access
278 colorspace, input_pixel, output_pixel
280 elif isinstance(self._pixel_shader, ColorConverter):
281 self._pixel_shader._convert( # pylint: disable=protected-access
282 colorspace, input_pixel, output_pixel
285 if not output_pixel.opaque:
286 full_coverage = False
288 mask[pixel_index // 32] |= 1 << (pixel_index % 32)
289 if colorspace.depth == 16:
296 elif colorspace.depth == 32:
303 elif colorspace.depth == 8:
304 buffer.cast("B")[pixel_index] = output_pixel.pixel & 0xFF
305 elif colorspace.depth < 8:
306 # Reorder the offsets to pack multiple rows into
307 # a byte (meaning they share a column).
308 if not colorspace.pixels_in_byte_share_row:
309 row = pixel_index // linestride_px
310 col = pixel_index % linestride_px
311 # Dividing by pixels_per_byte does truncated division
312 # even if we multiply it back out
314 col * pixels_per_byte
315 + (row // pixels_per_byte)
318 + (row % pixels_per_byte)
320 shift = (pixel_index % pixels_per_byte) * colorspace.depth
321 if colorspace.reverse_pixels_in_byte:
322 # Reverse the shift by subtracting it from the leftmost shift
323 shift = (pixels_per_byte - 1) * colorspace.depth - shift
324 buffer.cast("B")[pixel_index // pixels_per_byte] |= (
325 output_pixel.pixel << shift
327 mask_start_px += linestride_px - column_dirty_offset_px
331 def _finish_refresh(self) -> None:
332 if self._ephemeral_dirty_area.empty() and not self._current_area_dirty:
334 # Reset dirty area to nothing
335 self._ephemeral_dirty_area.x1 = self._ephemeral_dirty_area.x2
336 self._current_area_dirty = False
338 if isinstance(self._pixel_shader, (Palette, ColorConverter)):
339 self._pixel_shader._finish_refresh() # pylint: disable=protected-access
341 def _get_refresh_areas(self, areas: list[Area]) -> None:
342 if self._current_area_dirty or (
343 isinstance(self._pixel_shader, (Palette, ColorConverter))
344 and self._pixel_shader._needs_refresh # pylint: disable=protected-access
346 if not self._ephemeral_dirty_area.empty():
347 # Both are dirty, check if we should combine the areas or draw separately
348 # Draws as few pixels as possible both when animations move short distances
349 # and large distances. The display core implementation currently doesn't
350 # combine areas to reduce redrawing of masked areas. If it does, this could
351 # be simplified to just return the 2 possibly overlapping areas.
353 self._ephemeral_dirty_area.compute_overlap(
354 self._current_area, area_swap
356 overlap_size = area_swap.size()
357 self._ephemeral_dirty_area.union(self._current_area, area_swap)
358 union_size = area_swap.size()
359 current_size = self._current_area.size()
360 dirty_size = self._ephemeral_dirty_area.size()
362 if union_size - dirty_size - current_size + overlap_size <= min(
363 dirty_size, current_size
365 # The excluded / non-overlapping area from the disjoint dirty and current
366 # areas is smaller than the smallest area we need to draw. Redrawing the
367 # overlapping area would cost more than just drawing the union disjoint
369 area_swap.copy_into(self._ephemeral_dirty_area)
371 # The excluded area between the 2 dirty areas is larger than the smallest
372 # dirty area. It would be more costly to combine these areas than possibly
373 # redraw some overlap.
374 areas.append(self._current_area)
375 areas.append(self._ephemeral_dirty_area)
377 areas.append(self._current_area)
378 elif not self._ephemeral_dirty_area.empty():
379 areas.append(self._ephemeral_dirty_area)
381 def _update_transform(self, group_transform) -> None:
382 self._absolute_transform = (
383 null_transform if group_transform is None else group_transform
385 self._shape_set_dirty()