Merge pull request #443 from grajohnt/TrellisM4_jt
NeoTrellisM4 Grains of Sand Demo
This commit is contained in:
commit
42de0f426f
1 changed files with 289 additions and 0 deletions
289
NeoTrellis_M4_Sand/code.py
Normal file
289
NeoTrellis_M4_Sand/code.py
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
# Digital sand demo uses the accelerometer to move sand particles in a
|
||||
# realistic way. Tilt the board to see the sand grains tumble around and light
|
||||
# up LEDs. Based on the code created by Phil Burgess and Dave Astels, see:
|
||||
# https://learn.adafruit.com/digital-sand-dotstar-circuitpython-edition/code
|
||||
# https://learn.adafruit.com/animated-led-sand
|
||||
# Ported (badly) to NeoTrellis M4 by John Thurmond
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018 Tony DiCola
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import time
|
||||
import board
|
||||
import busio
|
||||
import adafruit_trellism4
|
||||
import math
|
||||
import random
|
||||
import adafruit_adxl34x
|
||||
|
||||
N_GRAINS = 8 # Number of grains of sand
|
||||
WIDTH = 8 # Display width in pixels
|
||||
HEIGHT = 4 # Display height in pixels
|
||||
NUMBER_PIXELS = WIDTH * HEIGHT
|
||||
MAX_FPS = 10 # Maximum redraw rate, frames/second
|
||||
|
||||
MAX_X = WIDTH * 256 - 1
|
||||
MAX_Y = HEIGHT * 256 - 1
|
||||
|
||||
|
||||
class Grain:
|
||||
"""A simple struct to hold position and velocity information
|
||||
for a single grain."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize grain position and velocity."""
|
||||
self.x = 0
|
||||
self.y = 0
|
||||
self.vx = 0
|
||||
self.vy = 0
|
||||
|
||||
grains = [Grain() for _ in range(N_GRAINS)]
|
||||
|
||||
color = random.randint(1, 254) # Set a random color to start
|
||||
current_press = set() # Get ready for button presses
|
||||
|
||||
# Set up Trellis and accelerometer
|
||||
trellis = adafruit_trellism4.TrellisM4Express(rotation=0)
|
||||
i2c = busio.I2C(board.ACCELEROMETER_SCL, board.ACCELEROMETER_SDA)
|
||||
sensor = adafruit_adxl34x.ADXL345(i2c)
|
||||
|
||||
oldidx = 0
|
||||
newidx = 0
|
||||
delta = 0
|
||||
newx = 0
|
||||
newy = 0
|
||||
|
||||
occupied_bits = [False for _ in range(WIDTH * HEIGHT)]
|
||||
|
||||
def index_of_xy(x, y):
|
||||
"""Convert an x/column and y/row into an index into
|
||||
a linear pixel array.
|
||||
|
||||
:param int x: column value
|
||||
:param int y: row value
|
||||
"""
|
||||
return (y >> 8) * WIDTH + (x >> 8)
|
||||
|
||||
|
||||
def already_present(limit, x, y):
|
||||
"""Check if a pixel is already used.
|
||||
|
||||
:param int limit: the index into the grain array of
|
||||
the grain being assigned a pixel Only grains already
|
||||
allocated need to be checks against.
|
||||
:param int x: proposed clumn value for the new grain
|
||||
:param int y: proposed row valuse for the new grain
|
||||
"""
|
||||
for j in range(limit):
|
||||
if x == grains[j].x or y == grains[j].y:
|
||||
return True
|
||||
return False
|
||||
|
||||
def wheel(pos):
|
||||
# Input a value 0 to 255 to get a color value.
|
||||
# The colours are a transition r - g - b - back to r.
|
||||
if pos < 0 or pos > 255:
|
||||
return 0, 0, 0
|
||||
if pos < 85:
|
||||
return int(255 - pos*3), int(pos*3), 0
|
||||
if pos < 170:
|
||||
pos -= 85
|
||||
return 0, int(255 - pos*3), int(pos*3)
|
||||
pos -= 170
|
||||
return int(pos * 3), 0, int(255 - (pos*3))
|
||||
|
||||
|
||||
|
||||
for g in grains:
|
||||
placed = False
|
||||
while not placed:
|
||||
g.x = random.randint(0, WIDTH * 256 - 1)
|
||||
g.y = random.randint(0, HEIGHT * 256 - 1)
|
||||
placed = not occupied_bits[index_of_xy(g.x, g.y)]
|
||||
occupied_bits[index_of_xy(g.x, g.y)] = True
|
||||
g.vx = 0
|
||||
g.vy = 0
|
||||
|
||||
while True:
|
||||
# Display frame rendered on prior pass. It's done immediately after the
|
||||
# FPS sync (rather than after rendering) for consistent animation timing.
|
||||
|
||||
for i in range(NUMBER_PIXELS):
|
||||
|
||||
# Some color options:
|
||||
|
||||
# Random color every refresh
|
||||
#trellis.pixels[(i%8, i//8)] = wheel(random.randint(1, 254)) if occupied_bits[i] else (0, 0, 0)
|
||||
|
||||
# Color by pixel (meh - needs work)
|
||||
#trellis.pixels[(i%8, i//8)] = wheel(i*2) if occupied_bits[i] else (0, 0, 0)
|
||||
|
||||
# Change color to random on button press, or cycle when you hold one down
|
||||
trellis.pixels[(i%8, i//8)] = wheel(color) if occupied_bits[i] else (0, 0, 0)
|
||||
|
||||
# Set as single color
|
||||
#trellis.pixels[(i//8,i%8)] = (255, 0, 0) if occupied_bits[i] else (0, 0, 0)
|
||||
|
||||
# TODO: Change color depending on which button you press?
|
||||
|
||||
# Change color to a new random color on button press
|
||||
pressed = set(trellis.pressed_keys)
|
||||
for press in pressed - current_press:
|
||||
if press:
|
||||
print("Pressed:", press)
|
||||
color = random.randint(1, 254)
|
||||
print("Color:", color)
|
||||
|
||||
# Read accelerometer...
|
||||
f_x, f_y, f_z = sensor.acceleration
|
||||
|
||||
# I had to manually scale these to get them in the -128 to 128 range-ish - should be done better
|
||||
f_x = int(f_x * 9.80665 * 16704/1000)
|
||||
f_y = int(f_y * 9.80665 * 16704/1000)
|
||||
f_z = int(f_z * 9.80665 * 16704/1000)
|
||||
|
||||
ax = f_x >> 3 # Transform accelerometer axes
|
||||
ay = f_y >> 3 # to grain coordinate space
|
||||
az = abs(f_z) >> 6 # Random motion factor
|
||||
|
||||
print("%6d %6d %6d"%(ax,ay,az))
|
||||
az = 1 if (az >= 3) else (4 - az) # Clip & invert
|
||||
ax -= az # Subtract motion factor from X, Y
|
||||
ay -= az
|
||||
az2 = (az << 1) + 1 # Range of random motion to add back in
|
||||
|
||||
# Adjust axes for the NeoTrellis M4 (probably better ways to do this)
|
||||
ax2 = ax
|
||||
ax = -ay
|
||||
ay = ax2
|
||||
|
||||
# ...and apply 2D accel vector to grain velocities...
|
||||
v2 = 0 # Velocity squared
|
||||
v = 0.0 # Absolute velociy
|
||||
for g in grains:
|
||||
|
||||
g.vx += ax + random.randint(0, az2) # A little randomness makes
|
||||
g.vy += ay + random.randint(0, az2) # tall stacks topple better!
|
||||
|
||||
# Terminal velocity (in any direction) is 256 units -- equal to
|
||||
# 1 pixel -- which keeps moving grains from passing through each other
|
||||
# and other such mayhem. Though it takes some extra math, velocity is
|
||||
# clipped as a 2D vector (not separately-limited X & Y) so that
|
||||
# diagonal movement isn't faster
|
||||
|
||||
v2 = g.vx * g.vx + g.vy * g.vy
|
||||
if v2 > 65536: # If v^2 > 65536, then v > 256
|
||||
v = math.floor(math.sqrt(v2)) # Velocity vector magnitude
|
||||
g.vx = (g.vx // v) << 8 # Maintain heading
|
||||
g.vy = (g.vy // v) << 8 # Limit magnitude
|
||||
|
||||
# ...then update position of each grain, one at a time, checking for
|
||||
# collisions and having them react. This really seems like it shouldn't
|
||||
# work, as only one grain is considered at a time while the rest are
|
||||
# regarded as stationary. Yet this naive algorithm, taking many not-
|
||||
# technically-quite-correct steps, and repeated quickly enough,
|
||||
# visually integrates into something that somewhat resembles physics.
|
||||
# (I'd initially tried implementing this as a bunch of concurrent and
|
||||
# "realistic" elastic collisions among circular grains, but the
|
||||
# calculations and volument of code quickly got out of hand for both
|
||||
# the tiny 8-bit AVR microcontroller and my tiny dinosaur brain.)
|
||||
|
||||
for g in grains:
|
||||
newx = g.x + g.vx # New position in grain space
|
||||
newy = g.y + g.vy
|
||||
if newx > MAX_X: # If grain would go out of bounds
|
||||
newx = MAX_X # keep it inside, and
|
||||
g.vx //= -2 # give a slight bounce off the wall
|
||||
elif newx < 0:
|
||||
newx = 0
|
||||
g.vx //= -2
|
||||
if newy > MAX_Y:
|
||||
newy = MAX_Y
|
||||
g.vy //= -2
|
||||
elif newy < 0:
|
||||
newy = 0
|
||||
g.vy //= -2
|
||||
|
||||
oldidx = index_of_xy(g.x, g.y) # prior pixel
|
||||
newidx = index_of_xy(newx, newy) # new pixel
|
||||
# If grain is moving to a new pixel...
|
||||
if oldidx != newidx and occupied_bits[newidx]:
|
||||
# but if that pixel is already occupied...
|
||||
# What direction when blocked?
|
||||
delta = abs(newidx - oldidx)
|
||||
if delta == 1: # 1 pixel left or right
|
||||
newx = g.x # cancel x motion
|
||||
# and bounce X velocity (Y is ok)
|
||||
g.vx //= -2
|
||||
newidx = oldidx # no pixel change
|
||||
elif delta == WIDTH: # 1 pixel up or down
|
||||
newy = g.y # cancel Y motion
|
||||
# and bounce Y velocity (X is ok)
|
||||
g.vy //= -2
|
||||
newidx = oldidx # no pixel change
|
||||
else: # Diagonal intersection is more tricky...
|
||||
# Try skidding along just one axis of motion if
|
||||
# possible (start w/ faster axis). Because we've
|
||||
# already established that diagonal (both-axis)
|
||||
# motion is occurring, moving on either axis alone
|
||||
# WILL change the pixel index, no need to check
|
||||
# that again.
|
||||
if abs(g.vx) > abs(g.vy): # x axis is faster
|
||||
newidx = index_of_xy(newx, g.y)
|
||||
# that pixel is free, take it! But...
|
||||
if not occupied_bits[newidx]:
|
||||
newy = g.y # cancel Y motion
|
||||
g.vy //= -2 # and bounce Y velocity
|
||||
else: # X pixel is taken, so try Y...
|
||||
newidx = index_of_xy(g.x, newy)
|
||||
# Pixel is free, take it, but first...
|
||||
if not occupied_bits[newidx]:
|
||||
newx = g.x # Cancel X motion
|
||||
g.vx //= -2 # Bounce X velocity
|
||||
else: # both spots are occupied
|
||||
newx = g.x # Cancel X & Y motion
|
||||
newy = g.y
|
||||
g.vx //= -2 # Bounce X & Y velocity
|
||||
g.vy //= -2
|
||||
newidx = oldidx # Not moving
|
||||
else: # y axis is faster. start there
|
||||
newidx = index_of_xy(g.x, newy)
|
||||
# Pixel's free! Take it! But...
|
||||
if not occupied_bits[newidx]:
|
||||
newx = g.x # Cancel X motion
|
||||
g.vx //= -2 # Bounce X velocity
|
||||
else: # Y pixel is taken, so try X...
|
||||
newidx = index_of_xy(newx, g.y)
|
||||
# Pixel is free, take it, but first...
|
||||
if not occupied_bits[newidx]:
|
||||
newy = g.y # cancel Y motion
|
||||
g.vy //= -2 # and bounce Y velocity
|
||||
else: # both spots are occupied
|
||||
newx = g.x # Cancel X & Y motion
|
||||
newy = g.y
|
||||
g.vx //= -2 # Bounce X & Y velocity
|
||||
g.vy //= -2
|
||||
newidx = oldidx # Not moving
|
||||
occupied_bits[oldidx] = False
|
||||
occupied_bits[newidx] = True
|
||||
g.x = newx
|
||||
g.y = newy
|
||||
Loading…
Reference in a new issue