Merge pull request #2922 from FoamyGuy/custom_animations
Custom LED Animations Updates
This commit is contained in:
commit
81ba1ce023
7 changed files with 736 additions and 0 deletions
43
Custom_LED_Animations/grid_both/code.py
Normal file
43
Custom_LED_Animations/grid_both/code.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
"""
|
||||||
|
Uses NeoPixel Featherwing connected to D10 and
|
||||||
|
Dotstar Featherwing connected to D13, and D11.
|
||||||
|
Update pins as needed for your connections.
|
||||||
|
"""
|
||||||
|
import board
|
||||||
|
import neopixel
|
||||||
|
import adafruit_dotstar as dotstar
|
||||||
|
from conways import ConwaysLifeAnimation
|
||||||
|
from snake import SnakeAnimation
|
||||||
|
|
||||||
|
# Update to match the pin connected to your NeoPixels
|
||||||
|
pixel_pin = board.D10
|
||||||
|
# Update to match the number of NeoPixels you have connected
|
||||||
|
pixel_num = 32
|
||||||
|
|
||||||
|
# initialize the neopixels featherwing
|
||||||
|
pixels = neopixel.NeoPixel(pixel_pin, pixel_num, brightness=0.02, auto_write=False)
|
||||||
|
|
||||||
|
# initialize the dotstar featherwing
|
||||||
|
dots = dotstar.DotStar(board.D13, board.D11, 72, brightness=0.02)
|
||||||
|
|
||||||
|
# initial live cells for conways
|
||||||
|
initial_cells = [
|
||||||
|
(2, 1),
|
||||||
|
(3, 1),
|
||||||
|
(4, 1),
|
||||||
|
(5, 1),
|
||||||
|
(6, 1),
|
||||||
|
]
|
||||||
|
|
||||||
|
# initialize the animations
|
||||||
|
conways = ConwaysLifeAnimation(dots, 0.1, 0xff00ff, 12, 6, initial_cells)
|
||||||
|
|
||||||
|
snake = SnakeAnimation(pixels, speed=0.1, color=0xff00ff, width=8, height=4)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# call animate to show the next animation frames
|
||||||
|
conways.animate()
|
||||||
|
snake.animate()
|
||||||
192
Custom_LED_Animations/grid_both/conways.py
Normal file
192
Custom_LED_Animations/grid_both/conways.py
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
# SPDX-FileCopyrightText: 2024 Tim Cocks
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
"""
|
||||||
|
ConwaysLifeAnimation helper class
|
||||||
|
"""
|
||||||
|
from micropython import const
|
||||||
|
|
||||||
|
from adafruit_led_animation.animation import Animation
|
||||||
|
from adafruit_led_animation.grid import PixelGrid, HORIZONTAL
|
||||||
|
|
||||||
|
|
||||||
|
def _is_pixel_off(pixel):
|
||||||
|
return pixel[0] == 0 and pixel[1] == 0 and pixel[2] == 0
|
||||||
|
|
||||||
|
|
||||||
|
class ConwaysLifeAnimation(Animation):
|
||||||
|
# Constants
|
||||||
|
DIRECTION_OFFSETS = [
|
||||||
|
(0, 1),
|
||||||
|
(0, -1),
|
||||||
|
(1, 0),
|
||||||
|
(-1, 0),
|
||||||
|
(1, 1),
|
||||||
|
(-1, 1),
|
||||||
|
(1, -1),
|
||||||
|
(-1, -1),
|
||||||
|
]
|
||||||
|
LIVE = const(0x01)
|
||||||
|
DEAD = const(0x00)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
pixel_object,
|
||||||
|
speed,
|
||||||
|
color,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
initial_cells,
|
||||||
|
equilibrium_restart=True,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Conway's Game of Life implementation. Watch the cells
|
||||||
|
live and die based on the classic rules.
|
||||||
|
|
||||||
|
:param pixel_object: The initialised LED object.
|
||||||
|
:param float speed: Animation refresh rate in seconds, e.g. ``0.1``.
|
||||||
|
:param color: the color to use for live cells
|
||||||
|
:param width: the width of the grid
|
||||||
|
:param height: the height of the grid
|
||||||
|
:param initial_cells: list of initial cells to be live
|
||||||
|
:param equilibrium_restart: whether to restart when the simulation gets stuck unchanging
|
||||||
|
"""
|
||||||
|
super().__init__(pixel_object, speed, color)
|
||||||
|
|
||||||
|
# list to hold which cells are live
|
||||||
|
self.drawn_pixels = []
|
||||||
|
|
||||||
|
# store the initial cells
|
||||||
|
self.initial_cells = initial_cells
|
||||||
|
|
||||||
|
# PixelGrid helper to access the strand as a 2D grid
|
||||||
|
self.pixel_grid = PixelGrid(
|
||||||
|
pixel_object, width, height, orientation=HORIZONTAL, alternating=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# size of the grid
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
|
||||||
|
# equilibrium restart boolean
|
||||||
|
self.equilibrium_restart = equilibrium_restart
|
||||||
|
|
||||||
|
# counter to store how many turns since the last change
|
||||||
|
self.equilibrium_turns = 0
|
||||||
|
|
||||||
|
# self._init_cells()
|
||||||
|
|
||||||
|
def _is_grid_empty(self):
|
||||||
|
"""
|
||||||
|
Checks if the grid is empty.
|
||||||
|
|
||||||
|
:return: True if there are no live cells, False otherwise
|
||||||
|
"""
|
||||||
|
for y in range(self.height):
|
||||||
|
for x in range(self.width):
|
||||||
|
if not _is_pixel_off(self.pixel_grid[x, y]):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _init_cells(self):
|
||||||
|
"""
|
||||||
|
Turn off all LEDs then turn on ones cooresponding to the initial_cells
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
self.pixel_grid.fill(0x000000)
|
||||||
|
for cell in self.initial_cells:
|
||||||
|
self.pixel_grid[cell] = self.color
|
||||||
|
|
||||||
|
def _count_neighbors(self, cell):
|
||||||
|
"""
|
||||||
|
Check how many live cell neighbors are found at the given location
|
||||||
|
:param cell: the location to check
|
||||||
|
:return: the number of live cell neighbors
|
||||||
|
"""
|
||||||
|
neighbors = 0
|
||||||
|
for direction in ConwaysLifeAnimation.DIRECTION_OFFSETS:
|
||||||
|
try:
|
||||||
|
if not _is_pixel_off(
|
||||||
|
self.pixel_grid[cell[0] + direction[0], cell[1] + direction[1]]
|
||||||
|
):
|
||||||
|
neighbors += 1
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
return neighbors
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
# pylint: disable=too-many-branches
|
||||||
|
"""
|
||||||
|
draw the current frame of the animation
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
# if there are no live cells
|
||||||
|
if self._is_grid_empty():
|
||||||
|
# spawn the inital_cells and return
|
||||||
|
self._init_cells()
|
||||||
|
return
|
||||||
|
|
||||||
|
# list to hold locations to despawn live cells
|
||||||
|
despawning_cells = []
|
||||||
|
|
||||||
|
# list to hold locations spawn new live cells
|
||||||
|
spawning_cells = []
|
||||||
|
|
||||||
|
# loop over the grid
|
||||||
|
for y in range(self.height):
|
||||||
|
for x in range(self.width):
|
||||||
|
|
||||||
|
# check and set the current cell type, live or dead
|
||||||
|
if _is_pixel_off(self.pixel_grid[x, y]):
|
||||||
|
cur_cell_type = ConwaysLifeAnimation.DEAD
|
||||||
|
else:
|
||||||
|
cur_cell_type = ConwaysLifeAnimation.LIVE
|
||||||
|
|
||||||
|
# get a count of the neigbors
|
||||||
|
neighbors = self._count_neighbors((x, y))
|
||||||
|
|
||||||
|
# if the current cell is alive
|
||||||
|
if cur_cell_type == ConwaysLifeAnimation.LIVE:
|
||||||
|
# if it has fewer than 2 neighbors
|
||||||
|
if neighbors < 2:
|
||||||
|
# add its location to the despawn list
|
||||||
|
despawning_cells.append((x, y))
|
||||||
|
|
||||||
|
# if it has more than 3 neighbors
|
||||||
|
if neighbors > 3:
|
||||||
|
# add its location to the despawn list
|
||||||
|
despawning_cells.append((x, y))
|
||||||
|
|
||||||
|
# if the current location is not a living cell
|
||||||
|
elif cur_cell_type == ConwaysLifeAnimation.DEAD:
|
||||||
|
# if it has exactly 3 neighbors
|
||||||
|
if neighbors == 3:
|
||||||
|
# add the current location to the spawn list
|
||||||
|
spawning_cells.append((x, y))
|
||||||
|
|
||||||
|
# loop over the despawn locations
|
||||||
|
for cell in despawning_cells:
|
||||||
|
# turn off LEDs at each location
|
||||||
|
self.pixel_grid[cell] = 0x000000
|
||||||
|
|
||||||
|
# loop over the spawn list
|
||||||
|
for cell in spawning_cells:
|
||||||
|
# turn on LEDs at each location
|
||||||
|
self.pixel_grid[cell] = self.color
|
||||||
|
|
||||||
|
# if equilibrium restart mode is enabled
|
||||||
|
if self.equilibrium_restart:
|
||||||
|
# if there were no cells spawned or despaned this round
|
||||||
|
if len(despawning_cells) == 0 and len(spawning_cells) == 0:
|
||||||
|
# increment equilibrium turns counter
|
||||||
|
self.equilibrium_turns += 1
|
||||||
|
# if the counter is 3 or higher
|
||||||
|
if self.equilibrium_turns >= 3:
|
||||||
|
# go back to the initial_cells
|
||||||
|
self._init_cells()
|
||||||
|
|
||||||
|
# reset the turns counter to zero
|
||||||
|
self.equilibrium_turns = 0
|
||||||
171
Custom_LED_Animations/grid_both/snake.py
Normal file
171
Custom_LED_Animations/grid_both/snake.py
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
# SPDX-FileCopyrightText: 2024 Tim Cocks
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
"""
|
||||||
|
SnakeAnimation helper class
|
||||||
|
"""
|
||||||
|
import random
|
||||||
|
from micropython import const
|
||||||
|
|
||||||
|
from adafruit_led_animation.animation import Animation
|
||||||
|
from adafruit_led_animation.grid import PixelGrid, HORIZONTAL
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class SnakeAnimation(Animation):
|
||||||
|
UP = const(0x00)
|
||||||
|
DOWN = const(0x01)
|
||||||
|
LEFT = const(0x02)
|
||||||
|
RIGHT = const(0x03)
|
||||||
|
ALL_DIRECTIONS = [UP, DOWN, LEFT, RIGHT]
|
||||||
|
DIRECTION_OFFSETS = {
|
||||||
|
DOWN: (0, 1),
|
||||||
|
UP: (0, -1),
|
||||||
|
RIGHT: (1, 0),
|
||||||
|
LEFT: (-1, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, pixel_object, speed, color, width, height, snake_length=3):
|
||||||
|
"""
|
||||||
|
Renders a snake that slithers around the 2D grid of pixels
|
||||||
|
"""
|
||||||
|
super().__init__(pixel_object, speed, color)
|
||||||
|
|
||||||
|
# how many segments the snake will have
|
||||||
|
self.snake_length = snake_length
|
||||||
|
|
||||||
|
# create a PixelGrid helper to access our strand as a 2D grid
|
||||||
|
self.pixel_grid = PixelGrid(pixel_object, width, height,
|
||||||
|
orientation=HORIZONTAL, alternating=False)
|
||||||
|
|
||||||
|
# size variables
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
|
||||||
|
# list that will hold locations of snake segments
|
||||||
|
self.snake_pixels = []
|
||||||
|
|
||||||
|
self.direction = None
|
||||||
|
|
||||||
|
# initialize the snake
|
||||||
|
self._new_snake()
|
||||||
|
|
||||||
|
def _clear_snake(self):
|
||||||
|
"""
|
||||||
|
Clear the snake segments and turn off all pixels
|
||||||
|
"""
|
||||||
|
while len(self.snake_pixels) > 0:
|
||||||
|
self.pixel_grid[self.snake_pixels.pop()] = 0x000000
|
||||||
|
|
||||||
|
def _new_snake(self):
|
||||||
|
"""
|
||||||
|
Create a new single segment snake. The snake has a random
|
||||||
|
direction and location. Turn on the pixel representing the snake.
|
||||||
|
"""
|
||||||
|
# choose a random direction and store it
|
||||||
|
self.direction = random.choice(SnakeAnimation.ALL_DIRECTIONS)
|
||||||
|
|
||||||
|
# choose a random starting tile
|
||||||
|
starting_tile = (random.randint(0, self.width - 1), random.randint(0, self.height - 1))
|
||||||
|
|
||||||
|
# add the starting tile to the list of segments
|
||||||
|
self.snake_pixels.append(starting_tile)
|
||||||
|
|
||||||
|
# turn on the pixel at the chosen location
|
||||||
|
self.pixel_grid[self.snake_pixels[0]] = self.color
|
||||||
|
|
||||||
|
def _can_move(self, direction):
|
||||||
|
"""
|
||||||
|
returns true if the snake can move in the given direction
|
||||||
|
"""
|
||||||
|
# location of the next tile if we would move that direction
|
||||||
|
next_tile = tuple(map(sum, zip(
|
||||||
|
SnakeAnimation.DIRECTION_OFFSETS[direction], self.snake_pixels[0])))
|
||||||
|
|
||||||
|
# if the tile is one of the snake segments
|
||||||
|
if next_tile in self.snake_pixels:
|
||||||
|
# can't move there
|
||||||
|
return False
|
||||||
|
|
||||||
|
# if the tile is within the bounds of the grid
|
||||||
|
if 0 <= next_tile[0] < self.width and 0 <= next_tile[1] < self.height:
|
||||||
|
# can move there
|
||||||
|
return True
|
||||||
|
|
||||||
|
# return false if any other conditions not met
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _choose_direction(self):
|
||||||
|
"""
|
||||||
|
Choose a direction to go in. Could continue in same direction
|
||||||
|
as it's already going, or decide to turn to a dirction that
|
||||||
|
will allow movement.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# copy of all directions in a list
|
||||||
|
directions_to_check = list(SnakeAnimation.ALL_DIRECTIONS)
|
||||||
|
|
||||||
|
# if we can move the direction we're currently going
|
||||||
|
if self._can_move(self.direction):
|
||||||
|
# "flip a coin"
|
||||||
|
if random.random() < 0.5:
|
||||||
|
# on "heads" we stay going the same direction
|
||||||
|
return self.direction
|
||||||
|
|
||||||
|
# loop over the copied list of directions to check
|
||||||
|
while len(directions_to_check) > 0:
|
||||||
|
# choose a random one from the list and pop it out of the list
|
||||||
|
possible_direction = directions_to_check.pop(
|
||||||
|
random.randint(0, len(directions_to_check)-1))
|
||||||
|
# if we can move the chosen direction
|
||||||
|
if self._can_move(possible_direction):
|
||||||
|
# return the chosen direction
|
||||||
|
return possible_direction
|
||||||
|
|
||||||
|
# if we made it through all directions and couldn't move in any of them
|
||||||
|
# then raise the SnakeStuckException
|
||||||
|
raise SnakeAnimation.SnakeStuckException
|
||||||
|
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
"""
|
||||||
|
Draw the current frame of the animation
|
||||||
|
"""
|
||||||
|
# if the snake is currently the desired length
|
||||||
|
if len(self.snake_pixels) == self.snake_length:
|
||||||
|
# remove the last segment from the list and turn it's LED off
|
||||||
|
self.pixel_grid[self.snake_pixels.pop()] = 0x000000
|
||||||
|
|
||||||
|
# if the snake is less than the desired length
|
||||||
|
# e.g. because we removed one in the previous step
|
||||||
|
if len(self.snake_pixels) < self.snake_length:
|
||||||
|
# wrap with try to catch the SnakeStuckException
|
||||||
|
try:
|
||||||
|
# update the direction, could continue straight, or could change
|
||||||
|
self.direction = self._choose_direction()
|
||||||
|
|
||||||
|
# the location of the next tile where the head of the snake will move to
|
||||||
|
next_tile = tuple(map(sum, zip(
|
||||||
|
SnakeAnimation.DIRECTION_OFFSETS[self.direction], self.snake_pixels[0])))
|
||||||
|
|
||||||
|
# insert the next tile at list index 0
|
||||||
|
self.snake_pixels.insert(0, next_tile)
|
||||||
|
|
||||||
|
# turn on the LED for the tile
|
||||||
|
self.pixel_grid[next_tile] = self.color
|
||||||
|
|
||||||
|
# if the snake exception is caught
|
||||||
|
except SnakeAnimation.SnakeStuckException:
|
||||||
|
# clear the snake to get rid of the old one
|
||||||
|
self._clear_snake()
|
||||||
|
|
||||||
|
# make a new snake
|
||||||
|
self._new_snake()
|
||||||
|
|
||||||
|
class SnakeStuckException(RuntimeError):
|
||||||
|
"""
|
||||||
|
Exception indicating the snake is stuck and can't move in any direction
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__("SnakeStuckException")
|
||||||
36
Custom_LED_Animations/strand_sequence/code.py
Normal file
36
Custom_LED_Animations/strand_sequence/code.py
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
import board
|
||||||
|
|
||||||
|
import neopixel
|
||||||
|
from adafruit_led_animation.color import PINK, JADE
|
||||||
|
from adafruit_led_animation.sequence import AnimationSequence
|
||||||
|
|
||||||
|
from rainbowsweep import RainbowSweepAnimation
|
||||||
|
from sweep import SweepAnimation
|
||||||
|
from zipper import ZipperAnimation
|
||||||
|
|
||||||
|
# Update to match the pin connected to your NeoPixels
|
||||||
|
pixel_pin = board.A1
|
||||||
|
# Update to match the number of NeoPixels you have connected
|
||||||
|
pixel_num = 30
|
||||||
|
|
||||||
|
# initialize the neopixels. Change out for dotstars if needed.
|
||||||
|
pixels = neopixel.NeoPixel(pixel_pin, pixel_num, brightness=0.02, auto_write=False)
|
||||||
|
|
||||||
|
# initialize the animations
|
||||||
|
sweep = SweepAnimation(pixels, speed=0.05, color=PINK)
|
||||||
|
|
||||||
|
zipper = ZipperAnimation(pixels, speed=0.1, color=PINK, alternate_color=JADE)
|
||||||
|
|
||||||
|
rainbowsweep = RainbowSweepAnimation(pixels, speed=0.05, color=0x000000, sweep_speed=0.1,
|
||||||
|
sweep_direction=RainbowSweepAnimation.DIRECTION_END_TO_START)
|
||||||
|
|
||||||
|
# sequence to play them all one after another
|
||||||
|
animations = AnimationSequence(
|
||||||
|
sweep, zipper, rainbowsweep, advance_interval=6, auto_clear=True
|
||||||
|
)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
animations.animate()
|
||||||
145
Custom_LED_Animations/strand_sequence/rainbowsweep.py
Normal file
145
Custom_LED_Animations/strand_sequence/rainbowsweep.py
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
"""
|
||||||
|
Adapted From `adafruit_led_animation.animation.rainbow`
|
||||||
|
"""
|
||||||
|
|
||||||
|
from adafruit_led_animation.animation import Animation
|
||||||
|
from adafruit_led_animation.color import colorwheel
|
||||||
|
from adafruit_led_animation import MS_PER_SECOND, monotonic_ms
|
||||||
|
|
||||||
|
|
||||||
|
class RainbowSweepAnimation(Animation):
|
||||||
|
"""
|
||||||
|
The classic rainbow color wheel that gets swept across by another specified color.
|
||||||
|
|
||||||
|
:param pixel_object: The initialised LED object.
|
||||||
|
:param float speed: Animation refresh rate in seconds, e.g. ``0.1``.
|
||||||
|
:param float sweep_speed: How long in seconds to wait between sweep steps
|
||||||
|
:param float period: Period to cycle the rainbow over in seconds. Default 1.
|
||||||
|
:param sweep_direction: which way to sweep across the rainbow. Must be one of
|
||||||
|
DIRECTION_START_TO_END or DIRECTION_END_TO_START
|
||||||
|
:param str name: Name of animation (optional, useful for sequences and debugging).
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# constants to represent the different directions
|
||||||
|
DIRECTION_START_TO_END = 0
|
||||||
|
DIRECTION_END_TO_START = 1
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
|
def __init__(
|
||||||
|
self, pixel_object, speed, color, sweep_speed=0.3, period=1,
|
||||||
|
name=None, sweep_direction=DIRECTION_START_TO_END
|
||||||
|
):
|
||||||
|
super().__init__(pixel_object, speed, color, name=name)
|
||||||
|
self._period = period
|
||||||
|
# internal var step used inside of color generator
|
||||||
|
self._step = 256 // len(pixel_object)
|
||||||
|
|
||||||
|
# internal var wheel_index used inside of color generator
|
||||||
|
self._wheel_index = 0
|
||||||
|
|
||||||
|
# instance of the generator
|
||||||
|
self._generator = self._color_wheel_generator()
|
||||||
|
|
||||||
|
# convert swap speed from seconds to ms and store it
|
||||||
|
self._sweep_speed = sweep_speed * 1000
|
||||||
|
|
||||||
|
# set the initial sweep index
|
||||||
|
self.sweep_index = len(pixel_object)
|
||||||
|
|
||||||
|
# internal variable to store the timestamp of when a sweep step occurs
|
||||||
|
self._last_sweep_time = 0
|
||||||
|
|
||||||
|
# store the direction argument
|
||||||
|
self.direction = sweep_direction
|
||||||
|
|
||||||
|
# this animation supports on cycle complete callbacks
|
||||||
|
on_cycle_complete_supported = True
|
||||||
|
|
||||||
|
def _color_wheel_generator(self):
|
||||||
|
# convert period to ms
|
||||||
|
period = int(self._period * MS_PER_SECOND)
|
||||||
|
|
||||||
|
# how many pixels in the strand
|
||||||
|
num_pixels = len(self.pixel_object)
|
||||||
|
|
||||||
|
# current timestamp
|
||||||
|
last_update = monotonic_ms()
|
||||||
|
|
||||||
|
cycle_position = 0
|
||||||
|
last_pos = 0
|
||||||
|
while True:
|
||||||
|
cycle_completed = False
|
||||||
|
# time vars
|
||||||
|
now = monotonic_ms()
|
||||||
|
time_since_last_draw = now - last_update
|
||||||
|
last_update = now
|
||||||
|
|
||||||
|
# cycle position vars
|
||||||
|
pos = cycle_position = (cycle_position + time_since_last_draw) % period
|
||||||
|
|
||||||
|
# if it's time to signal cycle complete
|
||||||
|
if pos < last_pos:
|
||||||
|
cycle_completed = True
|
||||||
|
|
||||||
|
# update position var for next iteration
|
||||||
|
last_pos = pos
|
||||||
|
|
||||||
|
# calculate wheel_index
|
||||||
|
wheel_index = int((pos / period) * 256)
|
||||||
|
|
||||||
|
# set all pixels to their color based on the wheel color and step
|
||||||
|
self.pixel_object[:] = [
|
||||||
|
colorwheel(((i * self._step) + wheel_index) % 255) for i in range(num_pixels)
|
||||||
|
]
|
||||||
|
|
||||||
|
# if it's time for a sweep step
|
||||||
|
if self._last_sweep_time + self._sweep_speed <= now:
|
||||||
|
|
||||||
|
# udpate sweep timestamp
|
||||||
|
self._last_sweep_time = now
|
||||||
|
|
||||||
|
# decrement the sweep index
|
||||||
|
self.sweep_index -= 1
|
||||||
|
|
||||||
|
# if it's finished the last step
|
||||||
|
if self.sweep_index == -1:
|
||||||
|
# reset it to the number of pixels in the strand
|
||||||
|
self.sweep_index = len(self.pixel_object)
|
||||||
|
|
||||||
|
# if end to start direction
|
||||||
|
if self.direction == self.DIRECTION_END_TO_START:
|
||||||
|
# set the current pixels at the end of the strand to the specified color
|
||||||
|
self.pixel_object[self.sweep_index:] = (
|
||||||
|
[self.color] * (len(self.pixel_object) - self.sweep_index))
|
||||||
|
|
||||||
|
# if start to end direction
|
||||||
|
elif self.direction == self.DIRECTION_START_TO_END:
|
||||||
|
# set the pixels at the begining of the strand to the specified color
|
||||||
|
inverse_index = len(self.pixel_object) - self.sweep_index
|
||||||
|
self.pixel_object[:inverse_index] = [self.color] * (inverse_index)
|
||||||
|
|
||||||
|
# update the wheel index
|
||||||
|
self._wheel_index = wheel_index
|
||||||
|
|
||||||
|
# signal cycle complete if it's time
|
||||||
|
if cycle_completed:
|
||||||
|
self.cycle_complete = True
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
"""
|
||||||
|
draw the current frame of the animation
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
next(self._generator)
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""
|
||||||
|
Resets the animation.
|
||||||
|
"""
|
||||||
|
self._generator = self._color_wheel_generator()
|
||||||
68
Custom_LED_Animations/strand_sequence/sweep.py
Normal file
68
Custom_LED_Animations/strand_sequence/sweep.py
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
# SPDX-FileCopyrightText: 2024 Tim Cocks
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
"""
|
||||||
|
SweepAnimation helper class
|
||||||
|
"""
|
||||||
|
from adafruit_led_animation.animation import Animation
|
||||||
|
|
||||||
|
|
||||||
|
class SweepAnimation(Animation):
|
||||||
|
|
||||||
|
def __init__(self, pixel_object, speed, color):
|
||||||
|
"""
|
||||||
|
Sweeps across the strand lighting up one pixel at a time.
|
||||||
|
Once the full strand is lit, sweeps across again turning off
|
||||||
|
each pixel one at a time.
|
||||||
|
|
||||||
|
:param pixel_object: The initialized pixel object
|
||||||
|
:param speed: The speed to run the animation
|
||||||
|
:param color: The color the pixels will be lit up.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Call super class initialization
|
||||||
|
super().__init__(pixel_object, speed, color)
|
||||||
|
|
||||||
|
# custom variable to store the current step of the animation
|
||||||
|
self.current_step = 0
|
||||||
|
|
||||||
|
# one step per pixel
|
||||||
|
self.last_step = len(pixel_object)
|
||||||
|
|
||||||
|
# boolean indicating whether we're currently sweeping LEDs on or off
|
||||||
|
self.sweeping_on = True
|
||||||
|
|
||||||
|
self.cycle_complete = False
|
||||||
|
|
||||||
|
# This animation supports the cycle complete callback
|
||||||
|
on_cycle_complete_supported = True
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
"""
|
||||||
|
Display the current frame of the animation
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
if self.sweeping_on:
|
||||||
|
# Turn on the next LED
|
||||||
|
self.pixel_object[self.current_step] = self.color
|
||||||
|
else: # sweeping off
|
||||||
|
# Turn off the next LED
|
||||||
|
self.pixel_object[self.current_step] = 0x000000
|
||||||
|
|
||||||
|
# increment the current step variable
|
||||||
|
self.current_step += 1
|
||||||
|
|
||||||
|
# if we've reached the last step
|
||||||
|
if self.current_step >= self.last_step:
|
||||||
|
|
||||||
|
# if we are currently sweeping off
|
||||||
|
if not self.sweeping_on:
|
||||||
|
# signal that the cycle is complete
|
||||||
|
self.cycle_complete = True
|
||||||
|
|
||||||
|
# reset the step variable to 0
|
||||||
|
self.current_step = 0
|
||||||
|
|
||||||
|
# flop sweeping on/off indicator variable
|
||||||
|
self.sweeping_on = not self.sweeping_on
|
||||||
81
Custom_LED_Animations/strand_sequence/zipper.py
Normal file
81
Custom_LED_Animations/strand_sequence/zipper.py
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
# SPDX-FileCopyrightText: 2024 Tim Cocks
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
"""
|
||||||
|
ZipperAnimation helper class
|
||||||
|
"""
|
||||||
|
from adafruit_led_animation.animation import Animation
|
||||||
|
|
||||||
|
|
||||||
|
class ZipperAnimation(Animation):
|
||||||
|
|
||||||
|
def __init__(self, pixel_object, speed, color, alternate_color=None):
|
||||||
|
"""
|
||||||
|
Lights up every other LED from each ends of the strand, passing each
|
||||||
|
other in the middle and resulting in the full strand being lit at the
|
||||||
|
end of the cycle.
|
||||||
|
|
||||||
|
:param pixel_object: The initialized pixel object
|
||||||
|
:param speed: The speed to run the animation
|
||||||
|
:param color: The color the pixels will be lit up.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Call super class initialization
|
||||||
|
super().__init__(pixel_object, speed, color)
|
||||||
|
|
||||||
|
# if alternate color is None then use single color
|
||||||
|
if alternate_color is None:
|
||||||
|
self.alternate_color = color
|
||||||
|
else:
|
||||||
|
self.alternate_color = alternate_color
|
||||||
|
|
||||||
|
# custom variable to store the current step of the animation
|
||||||
|
self.current_step = 0
|
||||||
|
|
||||||
|
# We're lighting up every other LED, so we have half the strand
|
||||||
|
# length in steps.
|
||||||
|
self.last_step = len(pixel_object) // 2
|
||||||
|
|
||||||
|
self.cycle_complete = False
|
||||||
|
|
||||||
|
# This animation supports the cycle complete callback
|
||||||
|
on_cycle_complete_supported = True
|
||||||
|
|
||||||
|
def draw(self):
|
||||||
|
"""
|
||||||
|
Display the current frame of the animation
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Use try/except to ignore indexes outside the strand
|
||||||
|
try:
|
||||||
|
# Turn on 1 even indexed pixel starting from the start of the strand
|
||||||
|
self.pixel_object[self.current_step * 2] = self.color
|
||||||
|
|
||||||
|
# Turn on 1 odd indexed pixel starting from the end of the strand
|
||||||
|
self.pixel_object[-(self.current_step * 2) - 1] = self.alternate_color
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# increment the current step variable
|
||||||
|
self.current_step += 1
|
||||||
|
|
||||||
|
# if we've reached the last step
|
||||||
|
if self.current_step > self.last_step:
|
||||||
|
# signal that the cycle is complete
|
||||||
|
self.cycle_complete = True
|
||||||
|
|
||||||
|
# call internal reset() function
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""
|
||||||
|
Turns all the LEDs off and resets the current step variable to 0
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
# turn LEDs off
|
||||||
|
self.pixel_object.fill(0x000000)
|
||||||
|
|
||||||
|
# reset current step variable
|
||||||
|
self.current_step = 0
|
||||||
Loading…
Reference in a new issue