diff --git a/adafruit_led_animation/animation/grid_rain.py b/adafruit_led_animation/animation/grid_rain.py new file mode 100644 index 0000000..adf3aee --- /dev/null +++ b/adafruit_led_animation/animation/grid_rain.py @@ -0,0 +1,147 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019-2020 Roy Hooper +# Copyright (c) 2020 Kattni Rembor for Adafruit Industries +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_led_animation.animation.grid_rain` +================================================================================ + +Rain animations for CircuitPython helper library for LED animations. + +* Author(s): Roy Hooper, Kattni Rembor + +Implementation Notes +-------------------- + +**Hardware:** + +* `Adafruit NeoPixels `_ +* `Adafruit DotStars `_ + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://circuitpython.org/downloads + +""" + +import random +from adafruit_led_animation.animation import Animation + +__version__ = "0.0.0-auto.0" +__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_LED_Animation.git" + +from adafruit_led_animation.color import BLACK, colorwheel, calculate_intensity, GREEN + + +class Rain(Animation): + """ + Droplets of rain. + + :param grid_object: The initialised PixelGrid object. + :param float speed: Animation speed in seconds, e.g. ``0.1``. + :param color: Animation color in ``(r, g, b)`` tuple, or ``0x000000`` hex format. + :param count: Number of sparkles to generate per animation cycle. + :param length: Number of pixels per raindrop (Default 3) + :param background: Background color (Default BLACK). + """ + + # pylint: disable=too-many-arguments + def __init__( + self, grid_object, speed, color, count=1, length=3, background=BLACK, name=None + ): + self._count = count + self._length = length + self._background = background + self._raindrops = [] + super().__init__(grid_object, speed, color, name=name) + + def draw(self): + + # Move raindrops down + keep = [] + for raindrop in self._raindrops: + pixels = [] + if raindrop[1][0][0] >= 0: + self.pixel_object[raindrop[0], raindrop[1][0][0]] = self._background + for pixel in raindrop[1]: + pixel[0] += 1 + if pixel[0] < self.pixel_object.height: + pixels.append(pixel) + if pixels: + keep.append([raindrop[0], pixels]) + self._raindrops = keep + + # Add a raindrop + if len(self._raindrops) < self._count: + x = random.randint(0, self.pixel_object.width - 1) + self._raindrops.append([x, self._generate_droplet(x, self._length)]) + + # Draw raindrops + for x, pixels in self._raindrops: + for y, color in pixels: + if y >= 0: + self.pixel_object[x, y] = color + + def _generate_droplet(self, x, length): # pylint: disable=unused-argument + return [[n, self.color] for n in range(-length, 0)] + + +class RainbowRain(Rain): + """ + Rainbow Rain animation. + """ + + def __init__( # pylint: disable=too-many-arguments + self, grid_object, speed, count=1, length=3, background=BLACK, name=None + ): + super().__init__(grid_object, speed, BLACK, count, length, background, name) + + def _generate_droplet(self, x, length): + color = colorwheel(random.randint(0, 255)) + return [ + [n, calculate_intensity(color, 1.0 - -((n + 1) / (length + 1)))] + for n in range(-length, 0) + ] + + +class MatrixRain(Rain): + """ + The Matrix style animation. + """ + + def __init__( # pylint: disable=too-many-arguments + self, + grid_object, + speed, + color=GREEN, + count=1, + length=6, + background=(0, 32, 0), + name=None, + ): + super().__init__(grid_object, speed, color, count, length, background, name) + + def _generate_droplet(self, x, length): + return [ + [n, calculate_intensity(self.color, random.randint(10, 100) * 1.0)] + for n in range(-length, 0) + ] diff --git a/adafruit_led_animation/animation/pulse.py b/adafruit_led_animation/animation/pulse.py index af7c0e6..e788d6d 100644 --- a/adafruit_led_animation/animation/pulse.py +++ b/adafruit_led_animation/animation/pulse.py @@ -74,9 +74,6 @@ def reset(self): """ Resets the animation. """ - white = len(self.pixel_object[0]) > 3 and isinstance( - self.pixel_object[0][-1], int - ) dotstar = len(self.pixel_object[0]) == 4 and isinstance( self.pixel_object[0][-1], float ) @@ -84,6 +81,4 @@ def reset(self): pulse_generator, ) - self._generator = pulse_generator( - self._period, self, white, dotstar_pwm=dotstar - ) + self._generator = pulse_generator(self._period, self, dotstar_pwm=dotstar) diff --git a/adafruit_led_animation/animation/sparklepulse.py b/adafruit_led_animation/animation/sparklepulse.py index b2e59b4..8e41bf2 100644 --- a/adafruit_led_animation/animation/sparklepulse.py +++ b/adafruit_led_animation/animation/sparklepulse.py @@ -74,14 +74,11 @@ def __init__( self._max_intensity = max_intensity self._min_intensity = min_intensity self._period = period - white = len(pixel_object) == 4 and isinstance(pixel_object[0][-1], int) dotstar = len(pixel_object) == 4 and isinstance(pixel_object[0][-1], float) super().__init__( pixel_object, speed=speed, color=color, num_sparkles=1, name=name ) - self._generator = pulse_generator( - self._period, self, white, dotstar_pwm=dotstar - ) + self._generator = pulse_generator(self._period, self, dotstar_pwm=dotstar) def draw(self): self._sparkle_color = next(self._generator) diff --git a/adafruit_led_animation/grid.py b/adafruit_led_animation/grid.py new file mode 100644 index 0000000..6000d70 --- /dev/null +++ b/adafruit_led_animation/grid.py @@ -0,0 +1,233 @@ +# The MIT License (MIT) +# +# Copyright (c) 2019 Roy Hooper +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +""" +`adafruit_led_animation.grid` +================================================================================ + +PixelGrid helper for 2D animations. + +* Author(s): Roy Hooper + +Implementation Notes +-------------------- + +**Hardware:** + +* `Adafruit NeoPixels `_ +* `Adafruit DotStars `_ + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://circuitpython.org/downloads + +""" +from micropython import const + +from .helper import PixelMap, horizontal_strip_gridmap, vertical_strip_gridmap + + +HORIZONTAL = const(1) +VERTICAL = const(2) + + +class PixelGrid: + """ + PixelGrid lets you address a pixel strip with x and y coordinates. + + :param strip: An object that implements the Neopixel or Dotstar protocol. + :param width: Grid width. + :param height: Grid height. + :param orientation: Orientation of the strip pixels - HORIZONTAL (default) or VERTICAL. + :param alternating: Whether the strip alternates direction from row to row (default True). + :param reverse_x: Whether the strip X origin is on the right side (default False). + :param reverse_y: Whether the strip Y origin is on the bottom (default False). + :param tuple top: (x, y) coordinates of grid top left corner (Optional) + :param tuple bottom: (x, y) coordinates of grid bottom right corner (Optional) + + To use with individual pixels: + + .. code-block:: python + + import board + import neopixel + import time + from adafruit_led_animation.grid import PixelGrid, VERTICAL + + pixels = neopixel.NeoPixel(board.D11, 256, auto_write=False) + + grid = PixelGrid(pixels, 32, 8, orientation=VERTICAL, alternating=True) + + for x in range(32): + for y in range(8): + # pg[x, y] = (y*32) + x + pg[x][y] = ((y*32) + x) << 8 + pg.show() + + """ + + def __init__( + self, + strip, + width, + height, + orientation=HORIZONTAL, + alternating=True, + reverse_x=False, + reverse_y=False, + top=0, + bottom=0, + ): # pylint: disable=too-many-arguments,too-many-locals + self._pixels = strip + self._x = [] + self.height = height + self.width = width + + if orientation == HORIZONTAL: + mapper = horizontal_strip_gridmap(width, alternating) + else: + mapper = vertical_strip_gridmap(height, alternating) + + if reverse_x: + mapper = reverse_x_mapper(width, mapper) + + if reverse_y: + mapper = reverse_y_mapper(height, mapper) + + x_start = 0 + x_end = width + y_start = 0 + y_end = height + if top: + x_start, y_start = top + if bottom: + x_end, y_end = bottom + + self.height = y_end - y_start + self.width = x_end - x_start + + for x in range(x_start, x_end): + self._x.append( + PixelMap( + strip, + [mapper(x, y) for y in range(y_start, y_end)], + individual_pixels=True, + ) + ) + self.n = len(self._x) + + def __repr__(self): + return "[" + ", ".join([str(self[x]) for x in range(self.n)]) + "]" + + def __setitem__(self, index, val): + if isinstance(index, slice): + raise NotImplementedError("PixelGrid does not support slices") + + if isinstance(index, tuple): + self._x[index[0]][index[1]] = val + else: + raise ValueError("PixelGrid assignment needs a sub-index or x,y coordinate") + + if self._pixels.auto_write: + self.show() + + def __getitem__(self, index): + if isinstance(index, slice): + raise NotImplementedError("PixelGrid does not support slices") + if index < 0: + index += len(self) + if index >= self.n or index < 0: + raise IndexError("x is out of range") + return self._x[index] + + def __len__(self): + return self.n + + @property + def brightness(self): + """ + brightness from the underlying strip. + """ + return self._pixels.brightness + + @brightness.setter + def brightness(self, brightness): + # pylint: disable=attribute-defined-outside-init + self._pixels.brightness = min(max(brightness, 0.0), 1.0) + + def fill(self, color): + """ + Fill the PixelGrid with the specified color. + + :param color: Color to use. + """ + for strip in self._x: + strip.fill(color) + + def show(self): + """ + Shows the pixels on the underlying strip. + """ + self._pixels.show() + + @property + def auto_write(self): + """ + auto_write from the underlying strip. + """ + return self._pixels.auto_write + + @auto_write.setter + def auto_write(self, value): + self._pixels.auto_write = value + + +def reverse_x_mapper(width, mapper): + """ + Returns a coordinate mapper function for grids with reversed X coordinates. + + :param width: width of strip + :param mapper: grid mapper to wrap + :return: mapper(x, y) + """ + max_x = width - 1 + + def x_mapper(x, y): + return mapper(max_x - x, y) + + return x_mapper + + +def reverse_y_mapper(height, mapper): + """ + Returns a coordinate mapper function for grids with reversed Y coordinates. + + :param height: width of strip + :param mapper: grid mapper to wrap + :return: mapper(x, y) + """ + max_y = height - 1 + + def y_mapper(x, y): + return mapper(x, max_y - y) + + return y_mapper diff --git a/adafruit_led_animation/helper.py b/adafruit_led_animation/helper.py index 255a2d0..3e02913 100644 --- a/adafruit_led_animation/helper.py +++ b/adafruit_led_animation/helper.py @@ -45,6 +45,7 @@ import math from . import NANOS_PER_SECOND, monotonic_ns +from .color import calculate_intensity class PixelMap: @@ -69,7 +70,7 @@ class PixelMap: pixel_wing_horizontal[0] = (255, 255, 0) pixel_wing_horizontal.show() - To use with individual pixels: + To use with groups of individual pixels: .. code-block:: python @@ -92,24 +93,57 @@ class PixelMap: pixel_wing_vertical[0] = (255, 255, 0) pixel_wing_vertical.show() + To use with individual pixels: + + .. code-block:: python + + import board + import neopixel + import time + from adafruit_led_animation.helper import PixelMap + + pixels = neopixel.NeoPixel(board.D6, 8, auto_write=False) + + pixel_map = PixelMap(pixels, [ + 0, 7, 1, 6, 2, 5, 3, 4 + ], individual_pixels=True) + + n = 0 + while True: + pixel_map[n] = AMBER + pixel_map.show() + n = n + 1 + if n > 7: + n = 0 + pixel_map.fill(0) + time.sleep(0.25) + + """ def __init__(self, strip, pixel_ranges, individual_pixels=False): self._pixels = strip self._ranges = pixel_ranges + self.n = len(self._ranges) + if self.n == 0: + raise ValueError("A PixelMap must have at least one pixel defined") self._individual_pixels = individual_pixels + self._expand_ranges() + + def _expand_ranges(self): + if not self._individual_pixels: + self._ranges = [list(range(start, end)) for start, end in self._ranges] + return + if isinstance(self._ranges[0], int): + self._ranges = [[n] for n in self._ranges] def __repr__(self): - return "[" + ", ".join([str(x) for x in self]) + "]" + return "[" + ", ".join([str(self[x]) for x in range(self.n)]) + "]" def _set_pixels(self, index, val): - if self._individual_pixels: - for pixel in self._ranges[index]: - self._pixels[pixel] = val - else: - range_start, range_stop = self._ranges[index] - self._pixels[range_start:range_stop] = [val] * (range_stop - range_start) + for pixel in self._ranges[index]: + self._pixels[pixel] = val def __setitem__(self, index, val): if isinstance(index, slice): @@ -124,7 +158,7 @@ def __setitem__(self, index, val): else: self._set_pixels(index, val) - if not self._pixels.auto_write: + if self._pixels.auto_write: self.show() def __getitem__(self, index): @@ -160,13 +194,9 @@ def fill(self, color): :param color: Color to fill all pixels referenced by this PixelMap definition with. """ - if self._individual_pixels: - for pixels in self._ranges: - for pixel in pixels: - self._pixels[pixel] = color - else: - for start, stop in self._ranges: - self._pixels[start:stop] = [color] * (stop - start) + for pixels in self._ranges: + for pixel in pixels: + self._pixels[pixel] = color def show(self): """ @@ -273,7 +303,7 @@ def mapper(x, y): return mapper -class PixelSubset: +class PixelSubset(PixelMap): """ PixelSubset lets you work with a subset of a pixel object. @@ -295,78 +325,18 @@ class PixelSubset: """ def __init__(self, pixel_object, start, end): - self._pixels = pixel_object - self._start = start - self._end = end - self.n = self._end - self._start - - def __repr__(self): - return "[" + ", ".join([str(x) for x in self]) + "]" - - def __setitem__(self, index, val): - if isinstance(index, slice): - start, stop, step = index.indices(self.n) - self._pixels[start + self._start : stop + self._start : step] = val - else: - self._pixels[index + self._start] = val - - if not self._pixels.auto_write: - self.show() - - def __getitem__(self, index): - if isinstance(index, slice): - start, stop, step = index.indices(self.n) - return self._pixels[start + self._start : stop + self._start : step] - if index < 0: - index += len(self) - if index >= self.n or index < 0: - raise IndexError - return self._pixels[index] - - def __len__(self): - return self.n - - @property - def brightness(self): - """ - brightness from the underlying strip. - """ - return self._pixels.brightness - - @brightness.setter - def brightness(self, brightness): - self._pixels.brightness = min(max(brightness, 0.0), 1.0) - - def fill(self, color): - """ - Fill the used pixel ranges with color. - """ - self._pixels[self._start : self._end] = [color] * (self.n) - - def show(self): - """ - Shows the pixels on the underlying strip. - """ - self._pixels.show() - - @property - def auto_write(self): - """ - auto_write from the underlying strip. - """ - return self._pixels.auto_write - - @auto_write.setter - def auto_write(self, value): - self._pixels.auto_write = value + super().__init__( + pixel_object, + pixel_ranges=[[n] for n in range(start, end)], + individual_pixels=True, + ) -def pulse_generator(period: float, animation_object, white=False, dotstar_pwm=False): +def pulse_generator(period: float, animation_object, dotstar_pwm=False): """ Generates a sequence of colors for a pulse, based on the time period specified. :param period: Pulse duration in seconds. :param animation_object: An animation object to interact with. - :param white: Whether the pixel strip has a white pixel. :param dotstar_pwm: Whether to use the dostar per pixel PWM value for brightness control. """ period = int(period * NANOS_PER_SECOND) @@ -376,7 +346,6 @@ def pulse_generator(period: float, animation_object, white=False, dotstar_pwm=Fa cycle_position = 0 last_pos = 0 while True: - fill_color = list(animation_object.color) now = monotonic_ns() time_since_last_draw = now - last_update last_update = now @@ -388,12 +357,12 @@ def pulse_generator(period: float, animation_object, white=False, dotstar_pwm=Fa pos = period - pos intensity = pos / half_period if dotstar_pwm: - fill_color = (fill_color[0], fill_color[1], fill_color[2], intensity) + fill_color = ( + animation_object.color[0], + animation_object.color[1], + animation_object.color[2], + intensity, + ) yield fill_color continue - if white and len(fill_color) == 4: - fill_color[3] = int(fill_color[3] * intensity) - fill_color[0] = int(fill_color[0] * intensity) - fill_color[1] = int(fill_color[1] * intensity) - fill_color[2] = int(fill_color[2] * intensity) - yield fill_color + yield calculate_intensity(animation_object.color, intensity)