Skip to content

Custom LED animations guide code #2917

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

Merged
merged 5 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions Custom_LED_Animations/conways/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import board
import neopixel

from conways import ConwaysLifeAnimation

# 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. Change out for dotstars if needed.
pixels = neopixel.NeoPixel(pixel_pin, pixel_num, brightness=0.02, auto_write=False)

initial_cells = [
(2, 1),
(3, 1),
(4, 1),
(5, 1),
(6, 1),
]

# initialize the animation
conways = ConwaysLifeAnimation(pixels, 1.0, 0xff00ff, 8, 4, initial_cells)

while True:
# call animation to show the next animation frame
conways.animate()
192 changes: 192 additions & 0 deletions Custom_LED_Animations/conways/conways.py
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions Custom_LED_Animations/rainbow_sweep/code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import board
import neopixel

from rainbowsweep import RainbowSweepAnimation

# 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. Change out for dotstars if needed.
pixels = neopixel.NeoPixel(pixel_pin, pixel_num, brightness=0.02, auto_write=False)

# initialize the animation
rainbowsweep = RainbowSweepAnimation(pixels, speed=0.05, color=0x000000, sweep_speed=0.1,
sweep_direction=RainbowSweepAnimation.DIRECTION_END_TO_START)

while True:
# call animation to show the next animation frame
rainbowsweep.animate()
145 changes: 145 additions & 0 deletions Custom_LED_Animations/rainbow_sweep/rainbowsweep.py
Original file line number Diff line number Diff line change
@@ -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()
Loading