Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add animations. #8

Merged
merged 52 commits into from
Dec 11, 2019
Merged

Add animations. #8

merged 52 commits into from
Dec 11, 2019

Conversation

kattni
Copy link
Contributor

@kattni kattni commented Dec 6, 2019

Created animations.py and added animations and animation features:

  • Animations: ColorCycle, Solid, Blink, Comet, Sparkle
  • AnimationGroup - ability to add multiple LED instances (such as a strip and the CPB onboard LEDs) to a group to animate both simultaneously
  • AnimationSequence - cycle through animations
  • Animate features: fill, freeze, resume, change_color

Added RAINBOW color list to color.py.

def draw(self):
"""
Animation subclasses must implement draw() to render the animation sequence.
"""

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a raise NotImplementedError() here instead of leaving it empty

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch.

self._next_update = monotonic_ns()
self.pixel_object.auto_write = False
self.color = color
self.peers = peers if peers else []

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be done by just having the default for the parameter be [] instead of None

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pylint hates [] as the default for a param.
W: 70, 4: Dangerous default value [] as argument (dangerous-default-value)

def change_color(self, color):
"""
By default changing color uses the color property.
"""

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed? Why not just use the color property setter?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's difficult to override a property setter. I'm trying to remember why I needed it. I think _recompute_color is what needs overriding and I forgot I put it there. Will try to squash this.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, now I remember - it ended up there because I didn't use a property on an animation sequence, time to fix that.

def change_color(self, color):
"""
ColorCycle doesn't support change_color.
"""

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should an exception be raised here?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code removed.

"""
self._next_update = monotonic_ns() + self._time_left_at_pause
self._time_left_at_pause = 0
self._paused = False

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about replacing freeze() and resume() with a paused property that can be set or return current state?

# to pause
animation.paused = True
# to check
if animation.paused:
    print("Animation currently paused.")
# to un-pause
animation.paused = False

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The freeze/resume protocol on the Animation children grew out of the AnimationSequence and AnimationGroup. It would be possible to use a paused property, but it doesn't behave cleanly:

AnimationSequence freezes/resumes the current animation in the sequence.
AnimationGroup freezes/resumes all members of the group.

Querying AnimationSequence.paused would return the sequence paused state so that wouldn't be too bad.

Querying AnimationGroup.paused however would need to return all() or any() on the paused state of all members of the group - as it doesn't itself know if they're paused.

I intentionally made the basic signatures for Animations, AnimationGroup and AnimationSequence identical so you can mix and match them in any order.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So is the only potential confusion when querying the current state of an AnimationGroup? If freeze/resume acts on all members, how do you get to a state where they are mixed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a mixed state in an AnimationGroup seems to defeat the purpose of the Group. If your end goal is a mixed state, you'd want to handle things separately.

@caternuson What is your reasoning behind suggesting this change? I'm trying to understand the purpose of the suggestion.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i guess the more important thing here is what do we want the interface to be for pausing and resuming. a property or two methods? @caternuson @kattni

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was thinking it would be more Pythonic. Also, to have a way to get current state. But this could be refactored later.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactoring it later not the right choice as it would change the protocol people use.... So I'll get this refactored ASAP, as @kattni needs this for her latest guide.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say keep as is then. The audioio API has separate functions for play/stop/pause:
https://circuitpython.readthedocs.io/en/latest/shared-bindings/audioio/AudioOut.html
so seems OK.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted back to the freeze/resume methods.

@rhooper
Copy link

rhooper commented Dec 10, 2019

@caternuson @kattni Let me know which you prefer.

@caternuson
Copy link

@rhooper Now that I've looked at the audioio API, what do you think about mimicking that? Is your animation control similar enough to audio control?

@rhooper
Copy link

rhooper commented Dec 10, 2019

@caternuson I'm not sure, I'll have to look at the AudioOut API when I have time.

@caternuson
Copy link

caternuson commented Dec 10, 2019

@rhooper @kattni If this is time critical, just go with what you had originally, the freeze() and resume() functions.

"""
The animation speed in fractional seconds.
"""
return self._speed_ns / 1000000000
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is technically a "magic" number and appears more than once, consider pulling this into a module-level constant, like https://github.com/theacodes/Winterbloom-Sol-software/blob/ce2b8b07f19b5f5c20cd32fa50e2ef7e7007c603/winterbloom_sol/adsr.py#L30

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had thought about that, but I am also concerned about performance. I vaguely remember there's a constant thing in CircuitPython that might be faster than a bare module variable, but I need to benchmark. Having worked a lot in the CircuitPython core with a JLink and gdb lately makes me think about these things :)

I'll look into the performance of both.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From my experience moving things to module-level variables doesn't impact performance, and might even help, considering that CP doesn't need to do an allocation for the module-level variable every time you use it.

If performance is a big concern we should do another pass at this once it's merged. There's lots of use of for x in y, range, and slicing that outright murder performance on CP compared to manipulating index variables.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another pass at it was already planned. The initial code was written to make it work. Making it better is the next step.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@theacodes there is indeed plenty of room for performance fixes. Two different things happened while writing this: 1) Making it work. 2) A lot of the code I wrote was written with _pixelbuf in mind, which perform faster with slices.. Running the code without _pixelbuf is, as you said, much slower. Once we get this merged, there's a number of issues to file regarding improvements, additions, and design documentation.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved this to a constant in the module.

self._reverse_comet_colors = None
self.reverse = reverse
self.bounce = bounce
super(Comet, self).__init__(pixel_object, speed, color)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it's unusual to see the superclass __init__ so far down. I've generally seen this done as soon as possible, like in the very first statement in the __init__ method.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I usually do it first, but in this case, when I did this, the color setup work needed doing before the rest of super is run. I'll check if that's still true. If not, a comment would be useful.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments added and init for both the base class and this class reorganized slightly.

self._dim_color = dim_color

def draw(self):
pixel = random.randint(0, (len(self.pixel_object) - 2))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if the pixel object has less than 2 pixels?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then it would crash. Good call. (note: this was based on code from elsewhere, and this animation doesn't support single pixels -- init should check that).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Init now checks that the strip is big enough.

self._last_update = now
self._cycle_position = (self._cycle_position + time_since_last_draw) % self._period
intensity = self.min_intensity + (
sin(self._radians_per_second * self._cycle_position) * self._intensity_delta)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sin's range is -1.0 to 1.0, so this could end up setting intensity to a negative number.

For example:

min_intensity = 0.0
max_intensity = 1.0
intensity_delta = max_intensity - min_intensity  # 1.0

# assume radians per second and cycle position end up passing 1.5pi to sin.
intensity = min_intensity + (sin(1.5 * pi) * intensity_delta)
intensity = 0.0 + (-1.0 * 1.0)
intensity = -1.0

The pixel colors would then be multiplied by -1 which likely isn't what we want.

You can solve this by moving sin's range to be 0 - 1.0 like this:

def normalized_sin(angle):
    return 0.5 + (sin(angle) * 0.5)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm planning to remove the use of sin() very soon, as the performance is poor. The code, however, shouldn't produce a negative brightness because it's clamped to 0-180 degrees by self._radians_per_second and % self._period set during init.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sin removed in favour of a linear function for now. Code still needs significant optimization as expected.

@kattni
Copy link
Contributor Author

kattni commented Dec 11, 2019

@theacodes Thank you for the review notes. @rhooper addressed them. As getting this merged is time-sensitive, I'm going to go ahead and merge it. If you have further suggestions, please file an issue. We intend to file separate issues for each of the improvements we want to make to make it easier to discuss them in the thread - please consider doing the same if you decide to file any issues. Thanks!

@kattni kattni merged commit ef103a0 into adafruit:master Dec 11, 2019
adafruit-adabot added a commit to adafruit/Adafruit_CircuitPython_Bundle that referenced this pull request Dec 11, 2019
Updating https://github.com/adafruit/Adafruit_CircuitPython_BLE to 3.1.1 from 3.1.0:
  > Merge pull request adafruit/Adafruit_CircuitPython_BLE#43 from makermelissa/master

Updating https://github.com/adafruit/Adafruit_CircuitPython_LED_Animation to 1.0.0 from 0.8.2:
  > Merge pull request adafruit/Adafruit_CircuitPython_LED_Animation#8 from kattni/animations
  > Merge pull request adafruit/Adafruit_CircuitPython_LED_Animation#7 from makermelissa/master
  > Merge pull request adafruit/Adafruit_CircuitPython_LED_Animation#5 from demophoon/add-blinka-to-requirements-txt
@kattni kattni deleted the animations branch November 3, 2021 20:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants