Merge pull request #23 from rhooper/rainbowsparkle-fix

Rainbowsparkle fix
This commit is contained in:
Kattni 2020-05-21 12:36:44 -04:00 committed by GitHub
commit 773c5b7658
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 116 additions and 97 deletions

View file

@ -54,14 +54,13 @@ class Animation:
""" """
Base class for animations. Base class for animations.
""" """
cycle_complete_supported = False on_cycle_complete_supported = False
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
def __init__(self, pixel_object, speed, color, peers=None, paused=False, name=None): def __init__(self, pixel_object, speed, color, peers=None, paused=False, name=None):
self.pixel_object = pixel_object self.pixel_object = pixel_object
self.pixel_object.auto_write = False self.pixel_object.auto_write = False
self.peers = peers if peers else [] self._peers = [self] + peers if peers is not None else [self]
"""A sequence of animations to trigger .draw() on when this animation draws."""
self._speed_ns = 0 self._speed_ns = 0
self._color = None self._color = None
self._paused = paused self._paused = paused
@ -71,6 +70,7 @@ class Animation:
self.speed = speed # sets _speed_ns self.speed = speed # sets _speed_ns
self.color = color # Triggers _recompute_color self.color = color # Triggers _recompute_color
self.name = name self.name = name
self.cycle_complete = False
self.notify_cycles = 1 self.notify_cycles = 1
"""Number of cycles to trigger additional cycle_done notifications after""" """Number of cycles to trigger additional cycle_done notifications after"""
self.draw_count = 0 self.draw_count = 0
@ -95,13 +95,19 @@ class Animation:
if now < self._next_update: if now < self._next_update:
return False return False
self.draw()
self.draw_count += 1
# Draw related animations together # Draw related animations together
if self.peers: for anim in self._peers:
for peer in self.peers: anim.draw()
peer.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 self._next_update = now + self._speed_ns
return True return True
@ -109,16 +115,39 @@ class Animation:
def draw(self): def draw(self):
""" """
Animation subclasses must implement draw() to render the animation sequence. 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() raise NotImplementedError()
def after_draw(self):
"""
Animation subclasses may implement after_draw() to do operations after the main draw()
is called.
"""
def show(self): def show(self):
""" """
Displays the updated pixels. Called during animates with changes. Displays the updated pixels. Called during animates with changes.
""" """
self.pixel_object.show() 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): def freeze(self):
""" """
Stops the animation until resumed. Stops the animation until resumed.
@ -173,7 +202,7 @@ class Animation:
Override as needed. Override as needed.
""" """
def cycle_complete(self): def on_cycle_complete(self):
""" """
Called by some animations when they complete an animation cycle. Called by some animations when they complete an animation cycle.
Animations that support cycle complete notifications will have X property set to False. Animations that support cycle complete notifications will have X property set to False.

View file

@ -83,7 +83,7 @@ class Chase(Animation):
super().__init__(pixel_object, speed, color, name=name) super().__init__(pixel_object, speed, color, name=name)
cycle_complete_supported = True on_cycle_complete_supported = True
@property @property
def reverse(self): def reverse(self):
@ -115,10 +115,9 @@ class Chase(Animation):
colorgen = bar_colors() colorgen = bar_colors()
self.pixel_object[:] = [next(colorgen) for _ in self.pixel_object] self.pixel_object[:] = [next(colorgen) for _ in self.pixel_object]
self.show()
if self.draw_count % len(self.pixel_object) == 0: 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 self._offset = (self._offset + self._direction) % self._repeat_width
def bar_color(self, n, pixel_no=0): # pylint: disable=unused-argument def bar_color(self, n, pixel_no=0): # pylint: disable=unused-argument

View file

@ -64,11 +64,10 @@ class ColorCycle(Animation):
self._generator = self._color_generator() self._generator = self._color_generator()
next(self._generator) next(self._generator)
cycle_complete_supported = True on_cycle_complete_supported = True
def draw(self): def draw(self):
self.pixel_object.fill(self.color) self.pixel_object.fill(self.color)
self.show()
next(self._generator) next(self._generator)
def _color_generator(self): def _color_generator(self):
@ -78,7 +77,7 @@ class ColorCycle(Animation):
yield yield
index = (index + 1) % len(self.colors) index = (index + 1) % len(self.colors)
if index == 0: if index == 0:
self.cycle_complete() self.cycle_complete = True
def reset(self): def reset(self):
""" """

View file

@ -89,7 +89,7 @@ class Comet(Animation):
self._generator = self._comet_generator() self._generator = self._comet_generator()
super().__init__(pixel_object, speed, color, name=name) super().__init__(pixel_object, speed, color, name=name)
cycle_complete_supported = True on_cycle_complete_supported = True
def _recompute_color(self, color): def _recompute_color(self, color):
pass pass
@ -130,13 +130,12 @@ class Comet(Animation):
] ]
else: else:
self.pixel_object[start : start + end] = colors[0:end] self.pixel_object[start : start + end] = colors[0:end]
self.show()
yield yield
cycle_passes += 1 cycle_passes += 1
if self.bounce: if self.bounce:
self.reverse = not self.reverse self.reverse = not self.reverse
if not self.bounce or cycle_passes == 2: if not self.bounce or cycle_passes == 2:
self.cycle_complete() self.cycle_complete = True
cycle_passes = 0 cycle_passes = 0
def draw(self): def draw(self):

View file

@ -64,12 +64,11 @@ class Pulse(Animation):
self._generator = None self._generator = None
self.reset() self.reset()
cycle_complete_supported = True on_cycle_complete_supported = True
def draw(self): def draw(self):
color = next(self._generator) color = next(self._generator)
self.fill(color) self.pixel_object.fill(color)
self.show()
def reset(self): def reset(self):
""" """
@ -78,8 +77,13 @@ class Pulse(Animation):
white = len(self.pixel_object[0]) > 3 and isinstance( white = len(self.pixel_object[0]) > 3 and isinstance(
self.pixel_object[0][-1], int 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 from adafruit_led_animation.helper import ( # pylint: disable=import-outside-toplevel
pulse_generator, pulse_generator,
) )
self._generator = pulse_generator(self._period, self, white) self._generator = pulse_generator(
self._period, self, white, dotstar_pwm=dotstar
)

View file

@ -85,7 +85,7 @@ class Rainbow(Animation):
self.colors.append(colorwheel(int(i))) self.colors.append(colorwheel(int(i)))
i += self._step i += self._step
cycle_complete_supported = True on_cycle_complete_supported = True
def _color_wheel_generator(self): def _color_wheel_generator(self):
period = int(self._period * NANOS_PER_SECOND) period = int(self._period * NANOS_PER_SECOND)
@ -113,9 +113,8 @@ class Rainbow(Animation):
colorwheel((i + wheel_index) % 255) for i in range(num_pixels) colorwheel((i + wheel_index) % 255) for i in range(num_pixels)
] ]
self._wheel_index = wheel_index self._wheel_index = wheel_index
self.show()
if cycle_completed: if cycle_completed:
self.cycle_complete() self.cycle_complete = True
yield yield
def _draw_precomputed(self, num_pixels, wheel_index): def _draw_precomputed(self, num_pixels, wheel_index):

View file

@ -78,8 +78,8 @@ class RainbowChase(Chase):
super().__init__(pixel_object, speed, 0, size, spacing, reverse, name) super().__init__(pixel_object, speed, 0, size, spacing, reverse, name)
def bar_color(self, n, pixel_no=0): 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 cycle_complete(self): def on_cycle_complete(self):
self._color_idx = (self._color_idx + self._direction) % len(self._colors) self._color_idx = (self._color_idx + self._direction) % len(self._colors)
super().cycle_complete() super().on_cycle_complete()

View file

@ -106,7 +106,8 @@ class RainbowSparkle(Rainbow):
int(self._background_brightness * color[2]), int(self._background_brightness * color[2]),
) )
def show(self): def after_draw(self):
self.show()
pixels = [ pixels = [
random.randint(0, len(self.pixel_object) - 1) random.randint(0, len(self.pixel_object) - 1)
for n in range(self._num_sparkles) for n in range(self._num_sparkles)
@ -115,4 +116,3 @@ class RainbowSparkle(Rainbow):
self.pixel_object[pixel] = self._bright_colors[ self.pixel_object[pixel] = self._bright_colors[
(self._wheel_index + pixel) % len(self._bright_colors) (self._wheel_index + pixel) % len(self._bright_colors)
] ]
super().show()

View file

@ -64,9 +64,11 @@ class Sparkle(Animation):
def __init__(self, pixel_object, speed, color, num_sparkles=1, name=None): def __init__(self, pixel_object, speed, color, num_sparkles=1, name=None):
if len(pixel_object) < 2: if len(pixel_object) < 2:
raise ValueError("Sparkle needs at least 2 pixels") raise ValueError("Sparkle needs at least 2 pixels")
self._half_color = None self._half_color = color
self._dim_color = None self._dim_color = color
self._sparkle_color = color
self._num_sparkles = num_sparkles self._num_sparkles = num_sparkles
self._pixels = []
super().__init__(pixel_object, speed, color, name=name) super().__init__(pixel_object, speed, color, name=name)
def _recompute_color(self, color): def _recompute_color(self, color):
@ -79,16 +81,18 @@ class Sparkle(Animation):
self.pixel_object[pixel] = dim_color self.pixel_object[pixel] = dim_color
self._half_color = half_color self._half_color = half_color
self._dim_color = dim_color self._dim_color = dim_color
self._sparkle_color = color
def draw(self): def draw(self):
pixels = [ self._pixels = [
random.randint(0, (len(self.pixel_object) - 2)) 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: for pixel in self._pixels:
self.pixel_object[pixel] = self._color self.pixel_object[pixel] = self._sparkle_color
def after_draw(self):
self.show() self.show()
for pixel in pixels: for pixel in self._pixels:
self.pixel_object[pixel] = self._half_color self.pixel_object[pixel] = self._half_color
self.pixel_object[pixel + 1] = self._dim_color self.pixel_object[pixel + 1] = self._dim_color
self.show()

View file

@ -44,14 +44,13 @@ Implementation Notes
""" """
import random from adafruit_led_animation.animation.sparkle import Sparkle
from adafruit_led_animation import NANOS_PER_SECOND, monotonic_ns from adafruit_led_animation.helper import pulse_generator
from adafruit_led_animation.animation import Animation
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 pixel_object: The initialised LED object.
:param int speed: Animation refresh rate in seconds, e.g. ``0.1``. :param int speed: Animation refresh rate in seconds, e.g. ``0.1``.
@ -63,48 +62,30 @@ class SparklePulse(Animation):
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
def __init__( 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: self._max_intensity = max_intensity
raise ValueError("Sparkle needs at least 2 pixels") self._min_intensity = min_intensity
self.max_intensity = max_intensity
self.min_intensity = min_intensity
self._period = period self._period = period
self._intensity_delta = max_intensity - min_intensity white = len(pixel_object) == 4 and isinstance(pixel_object[0][-1], int)
self._half_period = period / 2 dotstar = len(pixel_object) == 4 and isinstance(pixel_object[0][-1], float)
self._position_factor = 1 / self._half_period super().__init__(
self._bpp = len(pixel_object[0]) pixel_object, speed=speed, color=color, num_sparkles=1, name=name
self._last_update = monotonic_ns() )
self._cycle_position = 0 self._generator = pulse_generator(
self._half_color = None self._period, self, white, dotstar_pwm=dotstar
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
def draw(self): def draw(self):
pixel = random.randint(0, (len(self.pixel_object) - 2)) self._sparkle_color = next(self._generator)
super().draw()
now = monotonic_ns() def after_draw(self):
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.show() self.show()

View file

@ -79,15 +79,15 @@ class AnimationGroup:
# Catch cycle_complete on the last animation. # Catch cycle_complete on the last animation.
self._members[-1].add_cycle_complete_receiver(self._group_done) 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): def __str__(self):
return "<AnimationGroup %s: %s>" % (self.__class__.__name__, self.name) return "<AnimationGroup %s: %s>" % (self.__class__.__name__, self.name)
def _group_done(self, animation): # pylint: disable=unused-argument 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. Called by some animations when they complete an animation cycle.
Animations that support cycle complete notifications will have X property set to False. Animations that support cycle complete notifications will have X property set to False.

View file

@ -361,12 +361,13 @@ class PixelSubset:
self._pixels.auto_write = 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. Generates a sequence of colors for a pulse, based on the time period specified.
:param period: Pulse duration in seconds. :param period: Pulse duration in seconds.
:param animation_object: An animation object to interact with. :param animation_object: An animation object to interact with.
:param white: Whether the pixel strip has a white pixel. :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) period = int(period * NANOS_PER_SECOND)
half_period = period // 2 half_period = period // 2
@ -381,11 +382,15 @@ def pulse_generator(period: float, animation_object, white=False):
last_update = now last_update = now
pos = cycle_position = (cycle_position + time_since_last_draw) % period pos = cycle_position = (cycle_position + time_since_last_draw) % period
if pos < last_pos: if pos < last_pos:
animation_object.cycle_complete() animation_object.on_cycle_complete()
last_pos = pos last_pos = pos
if pos > half_period: if pos > half_period:
pos = period - pos pos = period - pos
intensity = pos / half_period 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: if white:
fill_color[3] = int(fill_color[3] * intensity) fill_color[3] = int(fill_color[3] * intensity)
fill_color[0] = int(fill_color[0] * intensity) fill_color[0] = int(fill_color[0] * intensity)

View file

@ -65,9 +65,9 @@ class AnimationSequence:
Defaults to ``False``. Defaults to ``False``.
:param bool random_order: Activate the animations in a random order. 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 auto_reset: Automatically call reset() on animations when changing animations.
:param bool advance_on_cycle_complete: Automatically advance when `cycle_complete` is triggered :param bool advance_on_cycle_complete: Automatically advance when `on_cycle_complete` is
on member animations. All Animations must support triggered on member animations. All Animations must
cycle_complete to use this. support on_cycle_complete to use this.
.. code-block:: python .. code-block:: python
@ -126,14 +126,14 @@ class AnimationSequence:
self._color = None self._color = None
for member in self._members: for member in self._members:
member.add_cycle_complete_receiver(self._sequence_complete) 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): def __str__(self):
return "<%s: %s>" % (self.__class__.__name__, self.name) 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. Called by some animations when they complete an animation cycle.
Animations that support cycle complete notifications will have X property set to False. Animations that support cycle complete notifications will have X property set to False.
@ -145,7 +145,7 @@ class AnimationSequence:
callback(self) callback(self)
def _sequence_complete(self, animation): # pylint: disable=unused-argument def _sequence_complete(self, animation): # pylint: disable=unused-argument
self.cycle_complete() self.on_cycle_complete()
if self.advance_on_cycle_complete: if self.advance_on_cycle_complete:
self._advance() self._advance()
@ -194,7 +194,7 @@ class AnimationSequence:
""" """
current = self._current current = self._current
if current > self._current: if current > self._current:
self.cycle_complete() self.on_cycle_complete()
self.activate((self._current + 1) % len(self._members)) self.activate((self._current + 1) % len(self._members))
def random(self): def random(self):