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.
"""
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 @@ class Animation:
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 @@ class Animation:
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 @@ class Animation:
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 @@ class Animation:
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.

View file

@ -83,7 +83,7 @@ class Chase(Animation):
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 @@ class Chase(Animation):
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

View file

@ -64,11 +64,10 @@ class ColorCycle(Animation):
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 @@ class ColorCycle(Animation):
yield
index = (index + 1) % len(self.colors)
if index == 0:
self.cycle_complete()
self.cycle_complete = True
def reset(self):
"""

View file

@ -89,7 +89,7 @@ class Comet(Animation):
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 @@ class Comet(Animation):
]
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):

View file

@ -64,12 +64,11 @@ class Pulse(Animation):
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()
self.pixel_object.fill(color)
def reset(self):
"""
@ -78,8 +77,13 @@ class Pulse(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
)
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
)

View file

@ -85,7 +85,7 @@ class Rainbow(Animation):
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 @@ class Rainbow(Animation):
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):

View file

@ -78,8 +78,8 @@ class RainbowChase(Chase):
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 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()

View file

@ -106,7 +106,8 @@ class RainbowSparkle(Rainbow):
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 @@ class RainbowSparkle(Rainbow):
self.pixel_object[pixel] = 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):
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,16 +81,18 @@ class Sparkle(Animation):
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
self.show()

View file

@ -44,14 +44,13 @@ Implementation Notes
"""
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,48 +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])
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))
self._sparkle_color = next(self._generator)
super().draw()
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
def after_draw(self):
self.show()

View file

@ -79,15 +79,15 @@ class AnimationGroup:
# 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 "<AnimationGroup %s: %s>" % (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.

View file

@ -361,12 +361,13 @@ class PixelSubset:
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
@ -381,11 +382,15 @@ 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
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)

View file

@ -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 @@ class AnimationSequence:
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 @@ class AnimationSequence:
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 @@ class AnimationSequence:
"""
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):