/ Digital_Sand_Dotstar_Circuitpython_Edition / code.py
code.py
1 # SPDX-FileCopyrightText: 2018 Dave Astels for Adafruit Industries 2 # 3 # SPDX-License-Identifier: MIT 4 5 # The MIT License (MIT) 6 # 7 # Copyright (c) 2018 Dave Astels 8 # 9 # Permission is hereby granted, free of charge, to any person obtaining a copy 10 # of this software and associated documentation files (the "Software"), to deal 11 # in the Software without restriction, including without limitation the rights 12 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 # copies of the Software, and to permit persons to whom the Software is 14 # furnished to do so, subject to the following conditions: 15 # 16 # The above copyright notice and this permission notice shall be included in 17 # all copies or substantial portions of the Software. 18 # 19 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 # THE SOFTWARE. 26 27 """ 28 Ported from the C code writen by Phillip Burgess 29 as used in https://learn.adafruit.com/animated-led-sand 30 Explainatory comments are used verbatim from that code. 31 """ 32 33 import math 34 import random 35 36 import adafruit_dotstar 37 import adafruit_lsm303 38 import board 39 import busio 40 41 N_GRAINS = 10 # Number of grains of sand 42 WIDTH = 12 # Display width in pixels 43 HEIGHT = 6 # Display height in pixels 44 NUMBER_PIXELS = WIDTH * HEIGHT 45 MAX_FPS = 45 # Maximum redraw rate, frames/second 46 GRAIN_COLOR = (0, 0, 16) 47 MAX_X = WIDTH * 256 - 1 48 MAX_Y = HEIGHT * 256 - 1 49 50 51 class Grain: 52 """A simple struct to hold position and velocity information 53 for a single grain.""" 54 55 def __init__(self): 56 """Initialize grain position and velocity.""" 57 self.x = 0 58 self.y = 0 59 self.vx = 0 60 self.vy = 0 61 62 63 grains = [Grain() for _ in range(N_GRAINS)] 64 i2c = busio.I2C(board.SCL, board.SDA) 65 sensor = adafruit_lsm303.LSM303(i2c) 66 wing = adafruit_dotstar.DotStar( 67 board.D13, board.D11, WIDTH * HEIGHT, 0.25, False) 68 69 oldidx = 0 70 newidx = 0 71 delta = 0 72 newx = 0 73 newy = 0 74 75 occupied_bits = [False for _ in range(WIDTH * HEIGHT)] 76 77 78 def index_of_xy(x, y): 79 """Convert an x/column and y/row into an index into 80 a linear pixel array. 81 82 :param int x: column value 83 :param int y: row value 84 """ 85 return (y >> 8) * WIDTH + (x >> 8) 86 87 88 def already_present(limit, x, y): 89 """Check if a pixel is already used. 90 91 :param int limit: the index into the grain array of 92 the grain being assigned a pixel Only grains already 93 allocated need to be checks against. 94 :param int x: proposed clumn value for the new grain 95 :param int y: proposed row valuse for the new grain 96 """ 97 for j in range(limit): 98 if x == grains[j].x or y == grains[j].y: 99 return True 100 return False 101 102 103 for g in grains: 104 placed = False 105 while not placed: 106 g.x = random.randint(0, WIDTH * 256 - 1) 107 g.y = random.randint(0, HEIGHT * 256 - 1) 108 placed = not occupied_bits[index_of_xy(g.x, g.y)] 109 occupied_bits[index_of_xy(g.x, g.y)] = True 110 g.vx = 0 111 g.vy = 0 112 113 while True: 114 # Display frame rendered on prior pass. It's done immediately after the 115 # FPS sync (rather than after rendering) for consistent animation timing. 116 117 for i in range(NUMBER_PIXELS): 118 wing[i] = GRAIN_COLOR if occupied_bits[i] else (0, 0, 0) 119 wing.show() 120 121 # Read accelerometer... 122 f_x, f_y, f_z = sensor.raw_acceleration 123 ax = f_x >> 8 # Transform accelerometer axes 124 ay = f_y >> 8 # to grain coordinate space 125 az = abs(f_z) >> 11 # Random motion factor 126 az = 1 if (az >= 3) else (4 - az) # Clip & invert 127 ax -= az # Subtract motion factor from X, Y 128 ay -= az 129 az2 = (az << 1) + 1 # Range of random motion to add back in 130 131 # ...and apply 2D accel vector to grain velocities... 132 v2 = 0 # Velocity squared 133 v = 0.0 # Absolute velociy 134 for g in grains: 135 g.vx += ax + random.randint(0, az2) # A little randomness makes 136 g.vy += ay + random.randint(0, az2) # tall stacks topple better! 137 138 # Terminal velocity (in any direction) is 256 units -- equal to 139 # 1 pixel -- which keeps moving grains from passing through each other 140 # and other such mayhem. Though it takes some extra math, velocity is 141 # clipped as a 2D vector (not separately-limited X & Y) so that 142 # diagonal movement isn't faster 143 144 v2 = g.vx * g.vx + g.vy * g.vy 145 if v2 > 65536: # If v^2 > 65536, then v > 256 146 v = math.floor(math.sqrt(v2)) # Velocity vector magnitude 147 g.vx = (g.vx // v) << 8 # Maintain heading 148 g.vy = (g.vy // v) << 8 # Limit magnitude 149 150 # ...then update position of each grain, one at a time, checking for 151 # collisions and having them react. This really seems like it shouldn't 152 # work, as only one grain is considered at a time while the rest are 153 # regarded as stationary. Yet this naive algorithm, taking many not- 154 # technically-quite-correct steps, and repeated quickly enough, 155 # visually integrates into something that somewhat resembles physics. 156 # (I'd initially tried implementing this as a bunch of concurrent and 157 # "realistic" elastic collisions among circular grains, but the 158 # calculations and volument of code quickly got out of hand for both 159 # the tiny 8-bit AVR microcontroller and my tiny dinosaur brain.) 160 161 for g in grains: 162 newx = g.x + g.vx # New position in grain space 163 newy = g.y + g.vy 164 if newx > MAX_X: # If grain would go out of bounds 165 newx = MAX_X # keep it inside, and 166 g.vx //= -2 # give a slight bounce off the wall 167 elif newx < 0: 168 newx = 0 169 g.vx //= -2 170 if newy > MAX_Y: 171 newy = MAX_Y 172 g.vy //= -2 173 elif newy < 0: 174 newy = 0 175 g.vy //= -2 176 177 oldidx = index_of_xy(g.x, g.y) # prior pixel 178 newidx = index_of_xy(newx, newy) # new pixel 179 # If grain is moving to a new pixel... 180 if oldidx != newidx and occupied_bits[newidx]: 181 # but if that pixel is already occupied... 182 # What direction when blocked? 183 delta = abs(newidx - oldidx) 184 if delta == 1: # 1 pixel left or right 185 newx = g.x # cancel x motion 186 # and bounce X velocity (Y is ok) 187 g.vx //= -2 188 newidx = oldidx # no pixel change 189 elif delta == WIDTH: # 1 pixel up or down 190 newy = g.y # cancel Y motion 191 # and bounce Y velocity (X is ok) 192 g.vy //= -2 193 newidx = oldidx # no pixel change 194 else: # Diagonal intersection is more tricky... 195 # Try skidding along just one axis of motion if 196 # possible (start w/ faster axis). Because we've 197 # already established that diagonal (both-axis) 198 # motion is occurring, moving on either axis alone 199 # WILL change the pixel index, no need to check 200 # that again. 201 if abs(g.vx) > abs(g.vy): # x axis is faster 202 newidx = index_of_xy(newx, g.y) 203 # that pixel is free, take it! But... 204 if not occupied_bits[newidx]: 205 newy = g.y # cancel Y motion 206 g.vy //= -2 # and bounce Y velocity 207 else: # X pixel is taken, so try Y... 208 newidx = index_of_xy(g.x, newy) 209 # Pixel is free, take it, but first... 210 if not occupied_bits[newidx]: 211 newx = g.x # Cancel X motion 212 g.vx //= -2 # Bounce X velocity 213 else: # both spots are occupied 214 newx = g.x # Cancel X & Y motion 215 newy = g.y 216 g.vx //= -2 # Bounce X & Y velocity 217 g.vy //= -2 218 newidx = oldidx # Not moving 219 else: # y axis is faster. start there 220 newidx = index_of_xy(g.x, newy) 221 # Pixel's free! Take it! But... 222 if not occupied_bits[newidx]: 223 newx = g.x # Cancel X motion 224 g.vx //= -2 # Bounce X velocity 225 else: # Y pixel is taken, so try X... 226 newidx = index_of_xy(newx, g.y) 227 # Pixel is free, take it, but first... 228 if not occupied_bits[newidx]: 229 newy = g.y # cancel Y motion 230 g.vy //= -2 # and bounce Y velocity 231 else: # both spots are occupied 232 newx = g.x # Cancel X & Y motion 233 newy = g.y 234 g.vx //= -2 # Bounce X & Y velocity 235 g.vy //= -2 236 newidx = oldidx # Not moving 237 occupied_bits[oldidx] = False 238 occupied_bits[newidx] = True 239 g.x = newx 240 g.y = newy