The adafruit_led_animation
library contains a built-in helper class for dealing with 2D Grids of pixels, PixelGrid
. We can use this class inside of our custom Animation to be able to refer to the pixels with x, y coordinates in a grid rather than just an index within a strand like normal. This will only make sense to use if your pixels are also physically arranged in a grid, the Neopixel FeatherWing or Dotstar FeatherWing provide an easy to use grid to run them on.
Snake Animation
The SnakeAnimation
will make a snake of the specified size and color that will slither around the grid in random directions. If the snake gets stuck in a corner a new one will spawn and carry on slithering around. The current snake segment locations are stored in a list variable. During each animation step the snake will decide randomly whether to continue the direction it's going, or to change to a different random direction. Much of the movement logic is delegated into helper functions _can_move()
and _choose_direction()
.
# 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")
Conways Game Of Life Animation
The ConwaysLifeAnimation
 is an implementation of Conway's Game of Life. It is a basic simulation of cells that live and die based on simple rules about how many neighbors they have. You get to set the location of a set of live cells at the start of the simulation and then it will step through simulation ticks applying the population rules. During each step the code will iterate over every pixel in the grid and count the number of live and dead neighbor cells, deciding based on the giving rules the fate of the current cell and storing it in a list. After it's made it through the whole grid then it works it's way through the lists created above doing the actual turning on and off of pixels to signify cells spawning and dying.Â
# 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
Both Together
The following code.py will run both animations at the same time. By default it expects to find one Neopixel Featherwing, and one Dotstar Featherwing. You can update the code to use a different configuration of featherwings, or some other compatible grid. Remember to update the pins referenced and initialization as needed.Â
# 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()
Text editor powered by tinymce.