/ adafruit_led_animation / helper.py
helper.py
1 # SPDX-FileCopyrightText: 2019 Kattni Rembor for Adafruit Industries 2 # 3 # SPDX-License-Identifier: MIT 4 5 """ 6 `adafruit_led_animation.helper` 7 ================================================================================ 8 9 Helper classes for making complex animations using CircuitPython LED animations library. 10 11 * Author(s): Kattni Rembor 12 13 Implementation Notes 14 -------------------- 15 16 **Hardware:** 17 18 * `Adafruit NeoPixels <https://www.adafruit.com/category/168>`_ 19 * `Adafruit DotStars <https://www.adafruit.com/category/885>`_ 20 21 **Software and Dependencies:** 22 23 * Adafruit CircuitPython firmware for the supported boards: 24 https://circuitpython.org/downloads 25 26 """ 27 28 import math 29 30 from . import MS_PER_SECOND, monotonic_ms 31 from .color import calculate_intensity 32 33 34 class PixelMap: 35 """ 36 PixelMap lets you treat ranges of pixels as single pixels for animation purposes. 37 38 :param strip: An object that implements the Neopixel or Dotstar protocol. 39 :param iterable pixel_ranges: Pixel ranges (or individual pixels). 40 :param bool individual_pixels: Whether pixel_ranges are individual pixels. 41 42 To use with ranges of pixels: 43 44 .. code-block:: python 45 46 import board 47 import neopixel 48 from adafruit_led_animation.helper import PixelMap 49 pixels = neopixel.NeoPixel(board.D6, 32, auto_write=False) 50 51 pixel_wing_horizontal = PixelMap(pixels, [(0, 8), (8, 16), (16, 24), (24, 32)]) 52 53 pixel_wing_horizontal[0] = (255, 255, 0) 54 pixel_wing_horizontal.show() 55 56 To use with groups of individual pixels: 57 58 .. code-block:: python 59 60 import board 61 import neopixel 62 from adafruit_led_animation.helper import PixelMap 63 pixels = neopixel.NeoPixel(board.D6, 32, auto_write=False) 64 65 pixel_wing_vertical = PixelMap(pixels, [ 66 (0, 8, 16, 24), 67 (1, 9, 17, 25), 68 (2, 10, 18, 26), 69 (3, 11, 19, 27), 70 (4, 12, 20, 28), 71 (5, 13, 21, 29), 72 (6, 14, 22, 30), 73 (7, 15, 23, 31), 74 ], individual_pixels=True) 75 76 pixel_wing_vertical[0] = (255, 255, 0) 77 pixel_wing_vertical.show() 78 79 To use with individual pixels: 80 81 .. code-block:: python 82 83 import board 84 import neopixel 85 import time 86 from adafruit_led_animation.helper import PixelMap 87 88 pixels = neopixel.NeoPixel(board.D6, 8, auto_write=False) 89 90 pixel_map = PixelMap(pixels, [ 91 0, 7, 1, 6, 2, 5, 3, 4 92 ], individual_pixels=True) 93 94 n = 0 95 while True: 96 pixel_map[n] = AMBER 97 pixel_map.show() 98 n = n + 1 99 if n > 7: 100 n = 0 101 pixel_map.fill(0) 102 time.sleep(0.25) 103 104 105 """ 106 107 def __init__(self, strip, pixel_ranges, individual_pixels=False): 108 self._pixels = strip 109 self._ranges = pixel_ranges 110 111 self.n = len(self._ranges) 112 if self.n == 0: 113 raise ValueError("A PixelMap must have at least one pixel defined") 114 self._individual_pixels = individual_pixels 115 self._expand_ranges() 116 117 def _expand_ranges(self): 118 if not self._individual_pixels: 119 self._ranges = [list(range(start, end)) for start, end in self._ranges] 120 return 121 if isinstance(self._ranges[0], int): 122 self._ranges = [[n] for n in self._ranges] 123 124 def __repr__(self): 125 return "[" + ", ".join([str(self[x]) for x in range(self.n)]) + "]" 126 127 def _set_pixels(self, index, val): 128 for pixel in self._ranges[index]: 129 self._pixels[pixel] = val 130 131 def __setitem__(self, index, val): 132 if isinstance(index, slice): 133 start, stop, step = index.indices(len(self._ranges)) 134 length = stop - start 135 if step != 0: 136 length = math.ceil(length / step) 137 if len(val) != length: 138 raise ValueError("Slice and input sequence size do not match.") 139 for val_i, in_i in enumerate(range(start, stop, step)): 140 self._set_pixels(in_i, val[val_i]) 141 else: 142 self._set_pixels(index, val) 143 144 if self._pixels.auto_write: 145 self.show() 146 147 def __getitem__(self, index): 148 if isinstance(index, slice): 149 out = [] 150 for in_i in range(*index.indices(len(self._ranges))): 151 out.append(self._pixels[self._ranges[in_i][0]]) 152 return out 153 if index < 0: 154 index += len(self) 155 if index >= self.n or index < 0: 156 raise IndexError 157 return self._pixels[self._ranges[index][0]] 158 159 def __len__(self): 160 return len(self._ranges) 161 162 @property 163 def brightness(self): 164 """ 165 brightness from the underlying strip. 166 """ 167 return self._pixels.brightness 168 169 @brightness.setter 170 def brightness(self, brightness): 171 # pylint: disable=attribute-defined-outside-init 172 self._pixels.brightness = min(max(brightness, 0.0), 1.0) 173 174 def fill(self, color): 175 """ 176 Fill the used pixel ranges with color. 177 178 :param color: Color to fill all pixels referenced by this PixelMap definition with. 179 """ 180 for pixels in self._ranges: 181 for pixel in pixels: 182 self._pixels[pixel] = color 183 184 def show(self): 185 """ 186 Shows the pixels on the underlying strip. 187 """ 188 self._pixels.show() 189 190 @property 191 def auto_write(self): 192 """ 193 auto_write from the underlying strip. 194 """ 195 return self._pixels.auto_write 196 197 @auto_write.setter 198 def auto_write(self, value): 199 self._pixels.auto_write = value 200 201 @classmethod 202 def vertical_lines(cls, pixel_object, width, height, gridmap): 203 """ 204 Generate a PixelMap of horizontal lines on a strip arranged in a grid. 205 206 :param pixel_object: pixel object 207 :param width: width of grid 208 :param height: height of grid 209 :param gridmap: a function to map x and y coordinates to the grid 210 see vertical_strip_gridmap and horizontal_strip_gridmap 211 :return: PixelMap 212 213 Example: Vertical lines on a 32x8 grid with the pixel rows oriented vertically, 214 alternating direction every row. 215 216 .. code-block:: python 217 218 PixelMap.vertical_lines(pixels, 32, 8, vertical_strip_gridmap(8)) 219 220 """ 221 if len(pixel_object) < width * height: 222 raise ValueError("number of pixels is less than width x height") 223 mapping = [] 224 for x in range(width): 225 mapping.append([gridmap(x, y) for y in range(height)]) 226 return cls(pixel_object, mapping, individual_pixels=True) 227 228 @classmethod 229 def horizontal_lines(cls, pixel_object, width, height, gridmap): 230 """ 231 Generate a PixelMap of horizontal lines on a strip arranged in a grid. 232 233 :param pixel_object: pixel object 234 :param width: width of grid 235 :param height: height of grid 236 :param gridmap: a function to map x and y coordinates to the grid 237 see vertical_strip_gridmap and horizontal_strip_gridmap 238 :return: PixelMap 239 240 Example: Horizontal lines on a 16x16 grid with the pixel rows oriented vertically, 241 alternating direction every row. 242 243 .. code-block:: python 244 245 PixelMap.horizontal_lines(pixels, 16, 16, vertical_strip_gridmap(16)) 246 """ 247 if len(pixel_object) < width * height: 248 raise ValueError("number of pixels is less than width x height") 249 mapping = [] 250 for y in range(height): 251 mapping.append([gridmap(x, y) for x in range(width)]) 252 return cls(pixel_object, mapping, individual_pixels=True) 253 254 255 def vertical_strip_gridmap(height, alternating=True): 256 """ 257 Returns a function that determines the pixel number for a grid with strips arranged vertically. 258 259 :param height: grid height in pixels 260 :param alternating: Whether or not the lines in the grid run alternate directions in a zigzag 261 :return: mapper(x, y) 262 """ 263 264 def mapper(x, y): 265 if alternating and x % 2: 266 return x * height + (height - 1 - y) 267 return x * height + y 268 269 return mapper 270 271 272 def horizontal_strip_gridmap(width, alternating=True): 273 """ 274 Determines the pixel number for a grid with strips arranged horizontally. 275 276 :param width: grid width in pixels 277 :param alternating: Whether or not the lines in the grid run alternate directions in a zigzag 278 :return: mapper(x, y) 279 """ 280 281 def mapper(x, y): 282 if alternating and y % 2: 283 return y * width + (width - 1 - x) 284 return y * width + x 285 286 return mapper 287 288 289 class PixelSubset(PixelMap): 290 """ 291 PixelSubset lets you work with a subset of a pixel object. 292 293 :param pixel_object: An object that implements the Neopixel or Dotstar protocol. 294 :param int start: Starting pixel number. 295 :param int end: Ending pixel number. 296 297 .. code-block:: python 298 299 import board 300 import neopixel 301 from adafruit_led_animation.helper import PixelSubset 302 pixels = neopixel.NeoPixel(board.D12, 307, auto_write=False) 303 304 star_start = 260 305 star_arm = PixelSubset(pixels, star_start + 7, star_start + 15) 306 star_arm.fill((255, 0, 255)) 307 pixels.show() 308 """ 309 310 def __init__(self, pixel_object, start, end): 311 super().__init__( 312 pixel_object, 313 pixel_ranges=[[n] for n in range(start, end)], 314 individual_pixels=True, 315 ) 316 317 318 def pulse_generator(period: float, animation_object, dotstar_pwm=False): 319 """ 320 Generates a sequence of colors for a pulse, based on the time period specified. 321 :param period: Pulse duration in seconds. 322 :param animation_object: An animation object to interact with. 323 :param dotstar_pwm: Whether to use the dostar per pixel PWM value for brightness control. 324 """ 325 period = int(period * MS_PER_SECOND) 326 half_period = period // 2 327 328 last_update = monotonic_ms() 329 cycle_position = 0 330 last_pos = 0 331 while True: 332 now = monotonic_ms() 333 time_since_last_draw = now - last_update 334 last_update = now 335 pos = cycle_position = (cycle_position + time_since_last_draw) % period 336 if pos < last_pos: 337 animation_object.cycle_complete = True 338 last_pos = pos 339 if pos > half_period: 340 pos = period - pos 341 intensity = pos / half_period 342 if dotstar_pwm: 343 fill_color = ( 344 animation_object.color[0], 345 animation_object.color[1], 346 animation_object.color[2], 347 intensity, 348 ) 349 yield fill_color 350 continue 351 yield calculate_intensity(animation_object.color, intensity)