Skip to content

Commit 57f5b16

Browse files
authored
Merge pull request #2917 from FoamyGuy/custom_animations
Custom LED animations guide code
2 parents c408781 + cf348e0 commit 57f5b16

File tree

10 files changed

+780
-0
lines changed

10 files changed

+780
-0
lines changed

Custom_LED_Animations/conways/code.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
import board
5+
import neopixel
6+
7+
from conways import ConwaysLifeAnimation
8+
9+
# Update to match the pin connected to your NeoPixels
10+
pixel_pin = board.D10
11+
# Update to match the number of NeoPixels you have connected
12+
pixel_num = 32
13+
14+
# initialize the neopixels. Change out for dotstars if needed.
15+
pixels = neopixel.NeoPixel(pixel_pin, pixel_num, brightness=0.02, auto_write=False)
16+
17+
initial_cells = [
18+
(2, 1),
19+
(3, 1),
20+
(4, 1),
21+
(5, 1),
22+
(6, 1),
23+
]
24+
25+
# initialize the animation
26+
conways = ConwaysLifeAnimation(pixels, 1.0, 0xff00ff, 8, 4, initial_cells)
27+
28+
while True:
29+
# call animation to show the next animation frame
30+
conways.animate()
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
# SPDX-FileCopyrightText: 2024 Tim Cocks
2+
#
3+
# SPDX-License-Identifier: MIT
4+
"""
5+
ConwaysLifeAnimation helper class
6+
"""
7+
from micropython import const
8+
9+
from adafruit_led_animation.animation import Animation
10+
from adafruit_led_animation.grid import PixelGrid, HORIZONTAL
11+
12+
13+
def _is_pixel_off(pixel):
14+
return pixel[0] == 0 and pixel[1] == 0 and pixel[2] == 0
15+
16+
17+
class ConwaysLifeAnimation(Animation):
18+
# Constants
19+
DIRECTION_OFFSETS = [
20+
(0, 1),
21+
(0, -1),
22+
(1, 0),
23+
(-1, 0),
24+
(1, 1),
25+
(-1, 1),
26+
(1, -1),
27+
(-1, -1),
28+
]
29+
LIVE = const(0x01)
30+
DEAD = const(0x00)
31+
32+
def __init__(
33+
self,
34+
pixel_object,
35+
speed,
36+
color,
37+
width,
38+
height,
39+
initial_cells,
40+
equilibrium_restart=True,
41+
):
42+
"""
43+
Conway's Game of Life implementation. Watch the cells
44+
live and die based on the classic rules.
45+
46+
:param pixel_object: The initialised LED object.
47+
:param float speed: Animation refresh rate in seconds, e.g. ``0.1``.
48+
:param color: the color to use for live cells
49+
:param width: the width of the grid
50+
:param height: the height of the grid
51+
:param initial_cells: list of initial cells to be live
52+
:param equilibrium_restart: whether to restart when the simulation gets stuck unchanging
53+
"""
54+
super().__init__(pixel_object, speed, color)
55+
56+
# list to hold which cells are live
57+
self.drawn_pixels = []
58+
59+
# store the initial cells
60+
self.initial_cells = initial_cells
61+
62+
# PixelGrid helper to access the strand as a 2D grid
63+
self.pixel_grid = PixelGrid(
64+
pixel_object, width, height, orientation=HORIZONTAL, alternating=False
65+
)
66+
67+
# size of the grid
68+
self.width = width
69+
self.height = height
70+
71+
# equilibrium restart boolean
72+
self.equilibrium_restart = equilibrium_restart
73+
74+
# counter to store how many turns since the last change
75+
self.equilibrium_turns = 0
76+
77+
# self._init_cells()
78+
79+
def _is_grid_empty(self):
80+
"""
81+
Checks if the grid is empty.
82+
83+
:return: True if there are no live cells, False otherwise
84+
"""
85+
for y in range(self.height):
86+
for x in range(self.width):
87+
if not _is_pixel_off(self.pixel_grid[x, y]):
88+
return False
89+
90+
return True
91+
92+
def _init_cells(self):
93+
"""
94+
Turn off all LEDs then turn on ones cooresponding to the initial_cells
95+
96+
:return: None
97+
"""
98+
self.pixel_grid.fill(0x000000)
99+
for cell in self.initial_cells:
100+
self.pixel_grid[cell] = self.color
101+
102+
def _count_neighbors(self, cell):
103+
"""
104+
Check how many live cell neighbors are found at the given location
105+
:param cell: the location to check
106+
:return: the number of live cell neighbors
107+
"""
108+
neighbors = 0
109+
for direction in ConwaysLifeAnimation.DIRECTION_OFFSETS:
110+
try:
111+
if not _is_pixel_off(
112+
self.pixel_grid[cell[0] + direction[0], cell[1] + direction[1]]
113+
):
114+
neighbors += 1
115+
except IndexError:
116+
pass
117+
return neighbors
118+
119+
def draw(self):
120+
# pylint: disable=too-many-branches
121+
"""
122+
draw the current frame of the animation
123+
124+
:return: None
125+
"""
126+
# if there are no live cells
127+
if self._is_grid_empty():
128+
# spawn the inital_cells and return
129+
self._init_cells()
130+
return
131+
132+
# list to hold locations to despawn live cells
133+
despawning_cells = []
134+
135+
# list to hold locations spawn new live cells
136+
spawning_cells = []
137+
138+
# loop over the grid
139+
for y in range(self.height):
140+
for x in range(self.width):
141+
142+
# check and set the current cell type, live or dead
143+
if _is_pixel_off(self.pixel_grid[x, y]):
144+
cur_cell_type = ConwaysLifeAnimation.DEAD
145+
else:
146+
cur_cell_type = ConwaysLifeAnimation.LIVE
147+
148+
# get a count of the neigbors
149+
neighbors = self._count_neighbors((x, y))
150+
151+
# if the current cell is alive
152+
if cur_cell_type == ConwaysLifeAnimation.LIVE:
153+
# if it has fewer than 2 neighbors
154+
if neighbors < 2:
155+
# add its location to the despawn list
156+
despawning_cells.append((x, y))
157+
158+
# if it has more than 3 neighbors
159+
if neighbors > 3:
160+
# add its location to the despawn list
161+
despawning_cells.append((x, y))
162+
163+
# if the current location is not a living cell
164+
elif cur_cell_type == ConwaysLifeAnimation.DEAD:
165+
# if it has exactly 3 neighbors
166+
if neighbors == 3:
167+
# add the current location to the spawn list
168+
spawning_cells.append((x, y))
169+
170+
# loop over the despawn locations
171+
for cell in despawning_cells:
172+
# turn off LEDs at each location
173+
self.pixel_grid[cell] = 0x000000
174+
175+
# loop over the spawn list
176+
for cell in spawning_cells:
177+
# turn on LEDs at each location
178+
self.pixel_grid[cell] = self.color
179+
180+
# if equilibrium restart mode is enabled
181+
if self.equilibrium_restart:
182+
# if there were no cells spawned or despaned this round
183+
if len(despawning_cells) == 0 and len(spawning_cells) == 0:
184+
# increment equilibrium turns counter
185+
self.equilibrium_turns += 1
186+
# if the counter is 3 or higher
187+
if self.equilibrium_turns >= 3:
188+
# go back to the initial_cells
189+
self._init_cells()
190+
191+
# reset the turns counter to zero
192+
self.equilibrium_turns = 0
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
import board
5+
import neopixel
6+
7+
from rainbowsweep import RainbowSweepAnimation
8+
9+
# Update to match the pin connected to your NeoPixels
10+
pixel_pin = board.D10
11+
# Update to match the number of NeoPixels you have connected
12+
pixel_num = 32
13+
14+
# initialize the neopixels. Change out for dotstars if needed.
15+
pixels = neopixel.NeoPixel(pixel_pin, pixel_num, brightness=0.02, auto_write=False)
16+
17+
# initialize the animation
18+
rainbowsweep = RainbowSweepAnimation(pixels, speed=0.05, color=0x000000, sweep_speed=0.1,
19+
sweep_direction=RainbowSweepAnimation.DIRECTION_END_TO_START)
20+
21+
while True:
22+
# call animation to show the next animation frame
23+
rainbowsweep.animate()
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# SPDX-FileCopyrightText: 2024 Tim Cocks for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
"""
6+
Adapted From `adafruit_led_animation.animation.rainbow`
7+
"""
8+
9+
from adafruit_led_animation.animation import Animation
10+
from adafruit_led_animation.color import colorwheel
11+
from adafruit_led_animation import MS_PER_SECOND, monotonic_ms
12+
13+
14+
class RainbowSweepAnimation(Animation):
15+
"""
16+
The classic rainbow color wheel that gets swept across by another specified color.
17+
18+
:param pixel_object: The initialised LED object.
19+
:param float speed: Animation refresh rate in seconds, e.g. ``0.1``.
20+
:param float sweep_speed: How long in seconds to wait between sweep steps
21+
:param float period: Period to cycle the rainbow over in seconds. Default 1.
22+
:param sweep_direction: which way to sweep across the rainbow. Must be one of
23+
DIRECTION_START_TO_END or DIRECTION_END_TO_START
24+
:param str name: Name of animation (optional, useful for sequences and debugging).
25+
26+
"""
27+
28+
# constants to represent the different directions
29+
DIRECTION_START_TO_END = 0
30+
DIRECTION_END_TO_START = 1
31+
# pylint: disable=too-many-arguments
32+
def __init__(
33+
self, pixel_object, speed, color, sweep_speed=0.3, period=1,
34+
name=None, sweep_direction=DIRECTION_START_TO_END
35+
):
36+
super().__init__(pixel_object, speed, color, name=name)
37+
self._period = period
38+
# internal var step used inside of color generator
39+
self._step = 256 // len(pixel_object)
40+
41+
# internal var wheel_index used inside of color generator
42+
self._wheel_index = 0
43+
44+
# instance of the generator
45+
self._generator = self._color_wheel_generator()
46+
47+
# convert swap speed from seconds to ms and store it
48+
self._sweep_speed = sweep_speed * 1000
49+
50+
# set the initial sweep index
51+
self.sweep_index = len(pixel_object)
52+
53+
# internal variable to store the timestamp of when a sweep step occurs
54+
self._last_sweep_time = 0
55+
56+
# store the direction argument
57+
self.direction = sweep_direction
58+
59+
# this animation supports on cycle complete callbacks
60+
on_cycle_complete_supported = True
61+
62+
def _color_wheel_generator(self):
63+
# convert period to ms
64+
period = int(self._period * MS_PER_SECOND)
65+
66+
# how many pixels in the strand
67+
num_pixels = len(self.pixel_object)
68+
69+
# current timestamp
70+
last_update = monotonic_ms()
71+
72+
cycle_position = 0
73+
last_pos = 0
74+
while True:
75+
cycle_completed = False
76+
# time vars
77+
now = monotonic_ms()
78+
time_since_last_draw = now - last_update
79+
last_update = now
80+
81+
# cycle position vars
82+
pos = cycle_position = (cycle_position + time_since_last_draw) % period
83+
84+
# if it's time to signal cycle complete
85+
if pos < last_pos:
86+
cycle_completed = True
87+
88+
# update position var for next iteration
89+
last_pos = pos
90+
91+
# calculate wheel_index
92+
wheel_index = int((pos / period) * 256)
93+
94+
# set all pixels to their color based on the wheel color and step
95+
self.pixel_object[:] = [
96+
colorwheel(((i * self._step) + wheel_index) % 255) for i in range(num_pixels)
97+
]
98+
99+
# if it's time for a sweep step
100+
if self._last_sweep_time + self._sweep_speed <= now:
101+
102+
# udpate sweep timestamp
103+
self._last_sweep_time = now
104+
105+
# decrement the sweep index
106+
self.sweep_index -= 1
107+
108+
# if it's finished the last step
109+
if self.sweep_index == -1:
110+
# reset it to the number of pixels in the strand
111+
self.sweep_index = len(self.pixel_object)
112+
113+
# if end to start direction
114+
if self.direction == self.DIRECTION_END_TO_START:
115+
# set the current pixels at the end of the strand to the specified color
116+
self.pixel_object[self.sweep_index:] = (
117+
[self.color] * (len(self.pixel_object) - self.sweep_index))
118+
119+
# if start to end direction
120+
elif self.direction == self.DIRECTION_START_TO_END:
121+
# set the pixels at the begining of the strand to the specified color
122+
inverse_index = len(self.pixel_object) - self.sweep_index
123+
self.pixel_object[:inverse_index] = [self.color] * (inverse_index)
124+
125+
# update the wheel index
126+
self._wheel_index = wheel_index
127+
128+
# signal cycle complete if it's time
129+
if cycle_completed:
130+
self.cycle_complete = True
131+
yield
132+
133+
134+
def draw(self):
135+
"""
136+
draw the current frame of the animation
137+
:return:
138+
"""
139+
next(self._generator)
140+
141+
def reset(self):
142+
"""
143+
Resets the animation.
144+
"""
145+
self._generator = self._color_wheel_generator()

0 commit comments

Comments
 (0)