sequence.py
  1  # SPDX-FileCopyrightText: 2020 Kattni Rembor for Adafruit Industries
  2  #
  3  # SPDX-License-Identifier: MIT
  4  
  5  """
  6  `adafruit_led_animation.sequence`
  7  ================================================================================
  8  
  9  Animation sequence helper for CircuitPython helper library for LED animations.
 10  
 11  
 12  * Author(s): Kattni Rembor
 13  
 14  Implementation Notes
 15  --------------------
 16  
 17  **Hardware:**
 18  
 19  * `Adafruit NeoPixels <https://www.adafruit.com/category/168>`_
 20  * `Adafruit DotStars <https://www.adafruit.com/category/885>`_
 21  
 22  **Software and Dependencies:**
 23  
 24  * Adafruit CircuitPython firmware for the supported boards:
 25    https://circuitpython.org/downloads
 26  
 27  """
 28  
 29  import random
 30  from adafruit_led_animation.color import BLACK
 31  from . import MS_PER_SECOND, monotonic_ms
 32  
 33  __version__ = "0.0.0-auto.0"
 34  __repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_LED_Animation.git"
 35  
 36  
 37  class AnimationSequence:
 38      """
 39      A sequence of Animations to run in succession, looping forever.
 40      Advances manually, or at the specified interval.
 41  
 42      :param members: The animation objects or groups.
 43      :param int advance_interval: Time in seconds between animations if cycling
 44                                   automatically. Defaults to ``None``.
 45      :param bool auto_clear: Clear the pixels between animations. If ``True``, the current animation
 46                              will be cleared from the pixels before the next one starts.
 47                              Defaults to ``False``.
 48      :param bool random_order: Activate the animations in a random order. Defaults to ``False``.
 49      :param bool auto_reset: Automatically call reset() on animations when changing animations.
 50      :param bool advance_on_cycle_complete: Automatically advance when `on_cycle_complete` is
 51                                             triggered on member animations. All Animations must
 52                                             support on_cycle_complete to use this.
 53  
 54      .. code-block:: python
 55  
 56          import board
 57          import neopixel
 58          from adafruit_led_animation.sequence import AnimationSequence
 59          import adafruit_led_animation.animation.comet as comet_animation
 60          import adafruit_led_animation.animation.sparkle as sparkle_animation
 61          import adafruit_led_animation.animation.blink as blink_animation
 62          import adafruit_led_animation.color as color
 63  
 64          strip_pixels = neopixel.NeoPixel(board.A1, 30, brightness=1, auto_write=False)
 65  
 66          blink = blink_animation.Blink(strip_pixels, 0.2, color.RED)
 67          comet = comet_animation.Comet(strip_pixels, 0.1, color.BLUE)
 68          sparkle = sparkle_animation.Sparkle(strip_pixels, 0.05, color.GREEN)
 69  
 70          animations = AnimationSequence(blink, comet, sparkle, advance_interval=5)
 71  
 72          while True:
 73              animations.animate()
 74      """
 75  
 76      # pylint: disable=too-many-instance-attributes
 77      def __init__(
 78          self,
 79          *members,
 80          advance_interval=None,
 81          auto_clear=True,
 82          random_order=False,
 83          auto_reset=False,
 84          advance_on_cycle_complete=False,
 85          name=None
 86      ):
 87          if advance_interval and advance_on_cycle_complete:
 88              raise ValueError(
 89                  "Cannot use both advance_interval and advance_on_cycle_complete."
 90              )
 91          self._members = members
 92          self._advance_interval = (
 93              advance_interval * MS_PER_SECOND if advance_interval else None
 94          )
 95          self._last_advance = monotonic_ms()
 96          self._current = 0
 97          self.auto_clear = auto_clear
 98          self.auto_reset = auto_reset
 99          self.advance_on_cycle_complete = advance_on_cycle_complete
100          self.clear_color = BLACK
101          self._paused = False
102          self._paused_at = 0
103          self._random = random_order
104          self._also_notify = []
105          self.cycle_count = 0
106          self.notify_cycles = 1
107          self.name = name
108          if random_order:
109              self._current = random.randint(0, len(self._members) - 1)
110          self._color = None
111          for member in self._members:
112              member.add_cycle_complete_receiver(self._sequence_complete)
113          self.on_cycle_complete_supported = self._members[-1].on_cycle_complete_supported
114  
115      on_cycle_complete_supported = True
116  
117      def __str__(self):
118          return "<%s: %s>" % (self.__class__.__name__, self.name)
119  
120      def on_cycle_complete(self):
121          """
122          Called by some animations when they complete an animation cycle.
123          Animations that support cycle complete notifications will have X property set to False.
124          Override as needed.
125          """
126          self.cycle_count += 1
127          if self.cycle_count % self.notify_cycles == 0:
128              for callback in self._also_notify:
129                  callback(self)
130  
131      def _sequence_complete(self, animation):  # pylint: disable=unused-argument
132          if self.advance_on_cycle_complete:
133              self._advance()
134  
135      def add_cycle_complete_receiver(self, callback):
136          """
137          Adds an additional callback when the cycle completes.
138  
139          :param callback: Additional callback to trigger when a cycle completes.  The callback
140                           is passed the animation object instance.
141          """
142          self._also_notify.append(callback)
143  
144      def _auto_advance(self):
145          if not self._advance_interval:
146              return
147          now = monotonic_ms()
148          if now - self._last_advance > self._advance_interval:
149              self._last_advance = now
150              self._advance()
151  
152      def _advance(self):
153          if self.auto_reset:
154              self.current_animation.reset()
155          if self.auto_clear:
156              self.current_animation.fill(self.clear_color)
157          if self._random:
158              self.random()
159          else:
160              self.next()
161  
162      def activate(self, index):
163          """
164          Activates a specific animation.
165          """
166          if isinstance(index, str):
167              self._current = [member.name for member in self._members].index(index)
168          else:
169              self._current = index
170          if self._color:
171              self.current_animation.color = self._color
172  
173      def next(self):
174          """
175          Jump to the next animation.
176          """
177          current = self._current + 1
178          if current >= len(self._members):
179              self.on_cycle_complete()
180          self.activate(current % len(self._members))
181  
182      def random(self):
183          """
184          Jump to a random animation.
185          """
186          self.activate(random.randint(0, len(self._members) - 1))
187  
188      def animate(self, show=True):
189          """
190          Call animate() from your code's main loop.  It will draw the current animation
191          or go to the next animation based on the advance_interval if set.
192  
193          :return: True if the animation draw cycle was triggered, otherwise False.
194          """
195          if not self._paused and self._advance_interval:
196              self._auto_advance()
197          return self.current_animation.animate(show)
198  
199      @property
200      def current_animation(self):
201          """
202          Returns the current animation in the sequence.
203          """
204          return self._members[self._current]
205  
206      @property
207      def color(self):
208          """
209          Use this property to change the color of all animations in the sequence.
210          """
211          return self._color
212  
213      @color.setter
214      def color(self, color):
215          self._color = color
216          self.current_animation.color = color
217  
218      def fill(self, color):
219          """
220          Fills the current animation with a color.
221          """
222          self.current_animation.fill(color)
223  
224      def freeze(self):
225          """
226          Freeze the current animation in the sequence.
227          Also stops auto_advance.
228          """
229          if self._paused:
230              return
231          self._paused = True
232          self._paused_at = monotonic_ms()
233          self.current_animation.freeze()
234  
235      def resume(self):
236          """
237          Resume the current animation in the sequence, and resumes auto advance if enabled.
238          """
239          if not self._paused:
240              return
241          self._paused = False
242          now = monotonic_ms()
243          self._last_advance += now - self._paused_at
244          self._paused_at = 0
245          self.current_animation.resume()
246  
247      def reset(self):
248          """
249          Resets the current animation.
250          """
251          self.current_animation.reset()
252  
253      def show(self):
254          """
255          Draws the current animation group members.
256          """
257          self.current_animation.show()
258  
259  
260  class AnimateOnce(AnimationSequence):
261      """
262      Wrapper around AnimationSequence that returns False to animate() until a sequence has completed.
263      Takes the same arguments as AnimationSequence, but overrides advance_on_cycle_complete=True
264      and advance_interval=0
265  
266      Example:
267  
268      This example animates a comet in one direction then pulses red momentarily
269  
270      .. code-block:: python
271  
272          import board
273          import neopixel
274          from adafruit_led_animation.animation.comet import Comet
275          from adafruit_led_animation.animation.pulse import Pulse
276          from adafruit_led_animation.color import BLUE, RED
277          from adafruit_led_animation.sequence import AnimateOnce
278  
279          strip_pixels = neopixel.NeoPixel(board.A1, 30, brightness=0.5, auto_write=False)
280  
281          comet = Comet(strip_pixels, 0.01, color=BLUE, bounce=False)
282          pulse = Pulse(strip_pixels, 0.01, color=RED, period=2)
283  
284          animations = AnimateOnce(comet, pulse)
285  
286          while animations.animate():
287              pass
288  
289      """
290  
291      def __init__(self, *members, **kwargs):
292          kwargs["advance_on_cycle_complete"] = True
293          kwargs["advance_interval"] = 0
294          super().__init__(*members, **kwargs)
295          self._running = True
296  
297      def on_cycle_complete(self):
298          super().on_cycle_complete()
299          self._running = False
300  
301      def animate(self, show=True):
302          super().animate(show)
303          return self._running