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