/ adafruit_led_animation / sequence.py
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