From 52b3d600679acbba688fd9a2e9f7a6d5952c660e Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Mon, 18 May 2020 15:46:40 -0400 Subject: [PATCH 1/5] Fix --- adafruit_led_animation/animation/sparklepulse.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/adafruit_led_animation/animation/sparklepulse.py b/adafruit_led_animation/animation/sparklepulse.py index 26ab1e9..8897a4c 100644 --- a/adafruit_led_animation/animation/sparklepulse.py +++ b/adafruit_led_animation/animation/sparklepulse.py @@ -74,6 +74,8 @@ def __init__( self._half_period = period / 2 self._position_factor = 1 / self._half_period self._bpp = len(pixel_object[0]) + if isinstance(pixel_object[-1], float): + self._bpp = 3 self._last_update = monotonic_ns() self._cycle_position = 0 self._half_color = None @@ -107,4 +109,3 @@ def draw(self): ) color = [int(self.color[n] * intensity) for n in range(self._bpp)] self.pixel_object[pixel] = color - self.show() From 76a3e43969722eb885082c959e59eddbfee09552 Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Mon, 18 May 2020 15:51:40 -0400 Subject: [PATCH 2/5] a bunch of protocol changes to allow a second stage of drawing, and to move the cycle done callback out to animate() --- adafruit_led_animation/animation/__init__.py | 51 +++++++++++++++---- adafruit_led_animation/animation/chase.py | 5 +- .../animation/colorcycle.py | 5 +- adafruit_led_animation/animation/comet.py | 5 +- adafruit_led_animation/animation/pulse.py | 3 +- adafruit_led_animation/animation/rainbow.py | 5 +- .../animation/rainbowchase.py | 4 +- .../animation/rainbowsparkle.py | 4 +- adafruit_led_animation/animation/sparkle.py | 1 - .../animation/sparklepulse.py | 2 +- adafruit_led_animation/group.py | 6 +-- adafruit_led_animation/helper.py | 2 +- adafruit_led_animation/sequence.py | 16 +++--- 13 files changed, 66 insertions(+), 43 deletions(-) diff --git a/adafruit_led_animation/animation/__init__.py b/adafruit_led_animation/animation/__init__.py index 83a2aa3..6a52005 100644 --- a/adafruit_led_animation/animation/__init__.py +++ b/adafruit_led_animation/animation/__init__.py @@ -54,14 +54,13 @@ class Animation: """ Base class for animations. """ - cycle_complete_supported = False + on_cycle_complete_supported = False # pylint: disable=too-many-arguments def __init__(self, pixel_object, speed, color, peers=None, paused=False, name=None): self.pixel_object = pixel_object self.pixel_object.auto_write = False - self.peers = peers if peers else [] - """A sequence of animations to trigger .draw() on when this animation draws.""" + self._peers = [self] + peers if peers is not None else [self] self._speed_ns = 0 self._color = None self._paused = paused @@ -71,6 +70,7 @@ def __init__(self, pixel_object, speed, color, peers=None, paused=False, name=No self.speed = speed # sets _speed_ns self.color = color # Triggers _recompute_color self.name = name + self.cycle_complete = False self.notify_cycles = 1 """Number of cycles to trigger additional cycle_done notifications after""" self.draw_count = 0 @@ -95,13 +95,19 @@ def animate(self): if now < self._next_update: return False - self.draw() - self.draw_count += 1 - # Draw related animations together - if self.peers: - for peer in self.peers: - peer.draw() + for anim in self._peers: + anim.draw() + anim.after_draw() + + for anim in self._peers: + anim.show() + + # Note that the main animation cycle_complete flag is used, not the peer flag. + for anim in self._peers: + if self.cycle_complete: + anim.on_cycle_complete() + anim.cycle_complete = False self._next_update = now + self._speed_ns return True @@ -109,16 +115,39 @@ def animate(self): def draw(self): """ Animation subclasses must implement draw() to render the animation sequence. - Draw must call show(). + Animations should not call show(), as animate() will do so, after after_draw(). + Animations should set .cycle_done = True when an animation cycle is completed. """ raise NotImplementedError() + def after_draw(self): + """ + Animation subclasses may implement after_draw() to do operations after the main draw() + is called. + """ + def show(self): """ Displays the updated pixels. Called during animates with changes. """ self.pixel_object.show() + @property + def peers(self): + """ + Get the animation's peers. Peers are drawn, then shown together. + """ + return self._peers[1:] + + @peers.setter + def peers(self, peer_list): + """ + Set the animation's peers. + :param list peer_list: List of peer animations. + """ + if peer_list is not None: + self._peers = [self] + peer_list + def freeze(self): """ Stops the animation until resumed. @@ -173,7 +202,7 @@ def _recompute_color(self, color): Override as needed. """ - def cycle_complete(self): + def on_cycle_complete(self): """ Called by some animations when they complete an animation cycle. Animations that support cycle complete notifications will have X property set to False. diff --git a/adafruit_led_animation/animation/chase.py b/adafruit_led_animation/animation/chase.py index bd7a308..c15c6c1 100644 --- a/adafruit_led_animation/animation/chase.py +++ b/adafruit_led_animation/animation/chase.py @@ -83,7 +83,7 @@ def _resetter(): super().__init__(pixel_object, speed, color, name=name) - cycle_complete_supported = True + on_cycle_complete_supported = True @property def reverse(self): @@ -115,10 +115,9 @@ def bar_colors(): colorgen = bar_colors() self.pixel_object[:] = [next(colorgen) for _ in self.pixel_object] - self.show() if self.draw_count % len(self.pixel_object) == 0: - self.cycle_complete() + self.cycle_complete = True self._offset = (self._offset + self._direction) % self._repeat_width def bar_color(self, n, pixel_no=0): # pylint: disable=unused-argument diff --git a/adafruit_led_animation/animation/colorcycle.py b/adafruit_led_animation/animation/colorcycle.py index 42b590e..6408713 100644 --- a/adafruit_led_animation/animation/colorcycle.py +++ b/adafruit_led_animation/animation/colorcycle.py @@ -64,11 +64,10 @@ def __init__(self, pixel_object, speed, colors=RAINBOW, name=None): self._generator = self._color_generator() next(self._generator) - cycle_complete_supported = True + on_cycle_complete_supported = True def draw(self): self.pixel_object.fill(self.color) - self.show() next(self._generator) def _color_generator(self): @@ -78,7 +77,7 @@ def _color_generator(self): yield index = (index + 1) % len(self.colors) if index == 0: - self.cycle_complete() + self.cycle_complete = True def reset(self): """ diff --git a/adafruit_led_animation/animation/comet.py b/adafruit_led_animation/animation/comet.py index 3ea28a3..f61969c 100644 --- a/adafruit_led_animation/animation/comet.py +++ b/adafruit_led_animation/animation/comet.py @@ -89,7 +89,7 @@ def __init__( self._generator = self._comet_generator() super().__init__(pixel_object, speed, color, name=name) - cycle_complete_supported = True + on_cycle_complete_supported = True def _recompute_color(self, color): pass @@ -130,13 +130,12 @@ def _comet_generator(self): ] else: self.pixel_object[start : start + end] = colors[0:end] - self.show() yield cycle_passes += 1 if self.bounce: self.reverse = not self.reverse if not self.bounce or cycle_passes == 2: - self.cycle_complete() + self.cycle_complete = True cycle_passes = 0 def draw(self): diff --git a/adafruit_led_animation/animation/pulse.py b/adafruit_led_animation/animation/pulse.py index 1b3aa33..9f8e047 100644 --- a/adafruit_led_animation/animation/pulse.py +++ b/adafruit_led_animation/animation/pulse.py @@ -64,12 +64,11 @@ def __init__(self, pixel_object, speed, color, period=5, name=None): self._generator = None self.reset() - cycle_complete_supported = True + on_cycle_complete_supported = True def draw(self): color = next(self._generator) self.fill(color) - self.show() def reset(self): """ diff --git a/adafruit_led_animation/animation/rainbow.py b/adafruit_led_animation/animation/rainbow.py index c102dd5..ef83a4c 100644 --- a/adafruit_led_animation/animation/rainbow.py +++ b/adafruit_led_animation/animation/rainbow.py @@ -85,7 +85,7 @@ def generate_rainbow(self): self.colors.append(colorwheel(int(i))) i += self._step - cycle_complete_supported = True + on_cycle_complete_supported = True def _color_wheel_generator(self): period = int(self._period * NANOS_PER_SECOND) @@ -113,9 +113,8 @@ def _color_wheel_generator(self): colorwheel((i + wheel_index) % 255) for i in range(num_pixels) ] self._wheel_index = wheel_index - self.show() if cycle_completed: - self.cycle_complete() + self.cycle_complete = True yield def _draw_precomputed(self, num_pixels, wheel_index): diff --git a/adafruit_led_animation/animation/rainbowchase.py b/adafruit_led_animation/animation/rainbowchase.py index 8394fdb..52d6088 100644 --- a/adafruit_led_animation/animation/rainbowchase.py +++ b/adafruit_led_animation/animation/rainbowchase.py @@ -80,6 +80,6 @@ def __init__( def bar_color(self, n, pixel_no=0): return self._colors[self._color_idx - n] - def cycle_complete(self): + def on_cycle_complete(self): self._color_idx = (self._color_idx + self._direction) % len(self._colors) - super().cycle_complete() + super().on_cycle_complete() diff --git a/adafruit_led_animation/animation/rainbowsparkle.py b/adafruit_led_animation/animation/rainbowsparkle.py index c6be5e0..232fb90 100644 --- a/adafruit_led_animation/animation/rainbowsparkle.py +++ b/adafruit_led_animation/animation/rainbowsparkle.py @@ -106,7 +106,8 @@ def generate_rainbow(self): int(self._background_brightness * color[2]), ) - def show(self): + def after_draw(self): + self.show() pixels = [ random.randint(0, len(self.pixel_object) - 1) for n in range(self._num_sparkles) @@ -115,4 +116,3 @@ def show(self): self.pixel_object[pixel] = self._bright_colors[ (self._wheel_index + pixel) % len(self._bright_colors) ] - super().show() diff --git a/adafruit_led_animation/animation/sparkle.py b/adafruit_led_animation/animation/sparkle.py index 81c57a4..716841b 100644 --- a/adafruit_led_animation/animation/sparkle.py +++ b/adafruit_led_animation/animation/sparkle.py @@ -91,4 +91,3 @@ def draw(self): for pixel in pixels: self.pixel_object[pixel] = self._half_color self.pixel_object[pixel + 1] = self._dim_color - self.show() diff --git a/adafruit_led_animation/animation/sparklepulse.py b/adafruit_led_animation/animation/sparklepulse.py index 8897a4c..ce706d2 100644 --- a/adafruit_led_animation/animation/sparklepulse.py +++ b/adafruit_led_animation/animation/sparklepulse.py @@ -74,7 +74,7 @@ def __init__( self._half_period = period / 2 self._position_factor = 1 / self._half_period self._bpp = len(pixel_object[0]) - if isinstance(pixel_object[-1], float): + if self._bpp == 4 and isinstance(pixel_object[0][3], float): self._bpp = 3 self._last_update = monotonic_ns() self._cycle_position = 0 diff --git a/adafruit_led_animation/group.py b/adafruit_led_animation/group.py index 90cee46..769d5e4 100644 --- a/adafruit_led_animation/group.py +++ b/adafruit_led_animation/group.py @@ -79,15 +79,15 @@ def __init__(self, *members, sync=False, name=None): # Catch cycle_complete on the last animation. self._members[-1].add_cycle_complete_receiver(self._group_done) - self.cycle_complete_supported = self._members[-1].cycle_complete_supported + self.on_cycle_complete_supported = self._members[-1].on_cycle_complete_supported def __str__(self): return "" % (self.__class__.__name__, self.name) def _group_done(self, animation): # pylint: disable=unused-argument - self.cycle_complete() + self.on_cycle_complete() - def cycle_complete(self): + def on_cycle_complete(self): """ Called by some animations when they complete an animation cycle. Animations that support cycle complete notifications will have X property set to False. diff --git a/adafruit_led_animation/helper.py b/adafruit_led_animation/helper.py index f38367f..e37d97a 100644 --- a/adafruit_led_animation/helper.py +++ b/adafruit_led_animation/helper.py @@ -381,7 +381,7 @@ def pulse_generator(period: float, animation_object, white=False): last_update = now pos = cycle_position = (cycle_position + time_since_last_draw) % period if pos < last_pos: - animation_object.cycle_complete() + animation_object.on_cycle_complete() last_pos = pos if pos > half_period: pos = period - pos diff --git a/adafruit_led_animation/sequence.py b/adafruit_led_animation/sequence.py index 0725955..5ee06d8 100644 --- a/adafruit_led_animation/sequence.py +++ b/adafruit_led_animation/sequence.py @@ -65,9 +65,9 @@ class AnimationSequence: Defaults to ``False``. :param bool random_order: Activate the animations in a random order. Defaults to ``False``. :param bool auto_reset: Automatically call reset() on animations when changing animations. - :param bool advance_on_cycle_complete: Automatically advance when `cycle_complete` is triggered - on member animations. All Animations must support - cycle_complete to use this. + :param bool advance_on_cycle_complete: Automatically advance when `on_cycle_complete` is + triggered on member animations. All Animations must + support on_cycle_complete to use this. .. code-block:: python @@ -126,14 +126,14 @@ def __init__( self._color = None for member in self._members: member.add_cycle_complete_receiver(self._sequence_complete) - self.cycle_complete_supported = self._members[-1].cycle_complete_supported + self.on_cycle_complete_supported = self._members[-1].on_cycle_complete_supported - cycle_complete_supported = True + on_cycle_complete_supported = True def __str__(self): return "<%s: %s>" % (self.__class__.__name__, self.name) - def cycle_complete(self): + def on_cycle_complete(self): """ Called by some animations when they complete an animation cycle. Animations that support cycle complete notifications will have X property set to False. @@ -145,7 +145,7 @@ def cycle_complete(self): callback(self) def _sequence_complete(self, animation): # pylint: disable=unused-argument - self.cycle_complete() + self.on_cycle_complete() if self.advance_on_cycle_complete: self._advance() @@ -194,7 +194,7 @@ def next(self): """ current = self._current if current > self._current: - self.cycle_complete() + self.on_cycle_complete() self.activate((self._current + 1) % len(self._members)) def random(self): From d408102aadb288df4a309949e3cfe2993d8c792e Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Mon, 18 May 2020 16:05:59 -0400 Subject: [PATCH 3/5] fix sparklepulse on dotstar --- adafruit_led_animation/animation/sparklepulse.py | 1 + 1 file changed, 1 insertion(+) diff --git a/adafruit_led_animation/animation/sparklepulse.py b/adafruit_led_animation/animation/sparklepulse.py index ce706d2..4e8e9a3 100644 --- a/adafruit_led_animation/animation/sparklepulse.py +++ b/adafruit_led_animation/animation/sparklepulse.py @@ -74,6 +74,7 @@ def __init__( self._half_period = period / 2 self._position_factor = 1 / self._half_period self._bpp = len(pixel_object[0]) + # Handle dotstars if self._bpp == 4 and isinstance(pixel_object[0][3], float): self._bpp = 3 self._last_update = monotonic_ns() From be18d57233e317e9ef2b853dd8b602425e0a16b3 Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Mon, 18 May 2020 16:15:40 -0400 Subject: [PATCH 4/5] fix rainbowchase on large strips (eg 512) --- adafruit_led_animation/animation/rainbowchase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adafruit_led_animation/animation/rainbowchase.py b/adafruit_led_animation/animation/rainbowchase.py index 52d6088..3024419 100644 --- a/adafruit_led_animation/animation/rainbowchase.py +++ b/adafruit_led_animation/animation/rainbowchase.py @@ -78,7 +78,7 @@ def __init__( super().__init__(pixel_object, speed, 0, size, spacing, reverse, name) def bar_color(self, n, pixel_no=0): - return self._colors[self._color_idx - n] + return self._colors[self._color_idx - (n % len(self._colors))] def on_cycle_complete(self): self._color_idx = (self._color_idx + self._direction) % len(self._colors) From 5f5d51899ad0e0121ed9e460dfaaf861ac662128 Mon Sep 17 00:00:00 2001 From: Roy Hooper Date: Tue, 19 May 2020 14:03:18 -0400 Subject: [PATCH 5/5] refactor SparklePulse to inherit Sparkle and use the pulse generator --- adafruit_led_animation/animation/pulse.py | 9 ++- adafruit_led_animation/animation/sparkle.py | 19 +++-- .../animation/sparklepulse.py | 75 +++++++------------ adafruit_led_animation/helper.py | 7 +- 4 files changed, 52 insertions(+), 58 deletions(-) diff --git a/adafruit_led_animation/animation/pulse.py b/adafruit_led_animation/animation/pulse.py index 9f8e047..af7c0e6 100644 --- a/adafruit_led_animation/animation/pulse.py +++ b/adafruit_led_animation/animation/pulse.py @@ -68,7 +68,7 @@ def __init__(self, pixel_object, speed, color, period=5, name=None): def draw(self): color = next(self._generator) - self.fill(color) + self.pixel_object.fill(color) def reset(self): """ @@ -77,8 +77,13 @@ def reset(self): 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 + ) from adafruit_led_animation.helper import ( # pylint: disable=import-outside-toplevel pulse_generator, ) - self._generator = pulse_generator(self._period, self, white) + self._generator = pulse_generator( + self._period, self, white, dotstar_pwm=dotstar + ) diff --git a/adafruit_led_animation/animation/sparkle.py b/adafruit_led_animation/animation/sparkle.py index 716841b..8f0e691 100644 --- a/adafruit_led_animation/animation/sparkle.py +++ b/adafruit_led_animation/animation/sparkle.py @@ -64,9 +64,11 @@ class Sparkle(Animation): def __init__(self, pixel_object, speed, color, num_sparkles=1, name=None): if len(pixel_object) < 2: raise ValueError("Sparkle needs at least 2 pixels") - self._half_color = None - self._dim_color = None + self._half_color = color + self._dim_color = color + self._sparkle_color = color self._num_sparkles = num_sparkles + self._pixels = [] super().__init__(pixel_object, speed, color, name=name) def _recompute_color(self, color): @@ -79,15 +81,18 @@ def _recompute_color(self, color): self.pixel_object[pixel] = dim_color self._half_color = half_color self._dim_color = dim_color + self._sparkle_color = color def draw(self): - pixels = [ + self._pixels = [ random.randint(0, (len(self.pixel_object) - 2)) - for n in range(self._num_sparkles) + for _ in range(self._num_sparkles) ] - for pixel in pixels: - self.pixel_object[pixel] = self._color + for pixel in self._pixels: + self.pixel_object[pixel] = self._sparkle_color + + def after_draw(self): self.show() - for pixel in pixels: + for pixel in self._pixels: self.pixel_object[pixel] = self._half_color self.pixel_object[pixel + 1] = self._dim_color diff --git a/adafruit_led_animation/animation/sparklepulse.py b/adafruit_led_animation/animation/sparklepulse.py index 4e8e9a3..b2e59b4 100644 --- a/adafruit_led_animation/animation/sparklepulse.py +++ b/adafruit_led_animation/animation/sparklepulse.py @@ -44,14 +44,13 @@ """ -import random -from adafruit_led_animation import NANOS_PER_SECOND, monotonic_ns -from adafruit_led_animation.animation import Animation +from adafruit_led_animation.animation.sparkle import Sparkle +from adafruit_led_animation.helper import pulse_generator -class SparklePulse(Animation): +class SparklePulse(Sparkle): """ - Combination of the Spark and Pulse animations. + Combination of the Sparkle and Pulse animations. :param pixel_object: The initialised LED object. :param int speed: Animation refresh rate in seconds, e.g. ``0.1``. @@ -63,50 +62,30 @@ class SparklePulse(Animation): # pylint: disable=too-many-arguments def __init__( - self, pixel_object, speed, color, period=5, max_intensity=1, min_intensity=0 + self, + pixel_object, + speed, + color, + period=5, + max_intensity=1, + min_intensity=0, + name=None, ): - if len(pixel_object) < 2: - raise ValueError("Sparkle needs at least 2 pixels") - self.max_intensity = max_intensity - self.min_intensity = min_intensity + self._max_intensity = max_intensity + self._min_intensity = min_intensity self._period = period - self._intensity_delta = max_intensity - min_intensity - self._half_period = period / 2 - self._position_factor = 1 / self._half_period - self._bpp = len(pixel_object[0]) - # Handle dotstars - if self._bpp == 4 and isinstance(pixel_object[0][3], float): - self._bpp = 3 - self._last_update = monotonic_ns() - self._cycle_position = 0 - self._half_color = None - self._dim_color = None - super().__init__(pixel_object, speed, color) - - def _recompute_color(self, color): - half_color = tuple(color[rgb] // 4 for rgb in range(len(color))) - dim_color = tuple(color[rgb] // 10 for rgb in range(len(color))) - for pixel in range(len(self.pixel_object)): - if self.pixel_object[pixel] == self._half_color: - self.pixel_object[pixel] = half_color - elif self.pixel_object[pixel] == self._dim_color: - self.pixel_object[pixel] = dim_color - self._half_color = half_color - self._dim_color = dim_color + 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 + ) def draw(self): - pixel = random.randint(0, (len(self.pixel_object) - 2)) - - now = monotonic_ns() - time_since_last_draw = (now - self._last_update) / NANOS_PER_SECOND - self._last_update = now - pos = self._cycle_position = ( - self._cycle_position + time_since_last_draw - ) % self._period - if pos > self._half_period: - pos = self._period - pos - intensity = self.min_intensity + ( - pos * self._intensity_delta * self._position_factor - ) - color = [int(self.color[n] * intensity) for n in range(self._bpp)] - self.pixel_object[pixel] = color + self._sparkle_color = next(self._generator) + super().draw() + + def after_draw(self): + self.show() diff --git a/adafruit_led_animation/helper.py b/adafruit_led_animation/helper.py index e37d97a..bd3d3e7 100644 --- a/adafruit_led_animation/helper.py +++ b/adafruit_led_animation/helper.py @@ -361,12 +361,13 @@ def auto_write(self, value): self._pixels.auto_write = value -def pulse_generator(period: float, animation_object, white=False): +def pulse_generator(period: float, animation_object, white=False, 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) half_period = period // 2 @@ -386,6 +387,10 @@ def pulse_generator(period: float, animation_object, white=False): if pos > half_period: pos = period - pos intensity = pos / half_period + if dotstar_pwm: + fill_color = (fill_color[0], fill_color[1], fill_color[2], intensity) + yield fill_color + continue if white: fill_color[3] = int(fill_color[3] * intensity) fill_color[0] = int(fill_color[0] * intensity)