Skip to content

Commit 631a11e

Browse files
committed
Port from C and optimize
1 parent dcc85e6 commit 631a11e

File tree

1 file changed

+174
-0
lines changed

1 file changed

+174
-0
lines changed

examples/gravity/main.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import board
2+
import busio
3+
import math
4+
import random
5+
6+
import adafruit_lsm303
7+
import adafruit_dotstar
8+
9+
N_GRAINS = 10 # Number of grains of sand
10+
WIDTH = 12 # Display width in pixels
11+
HEIGHT = 6 # Display height in pixels
12+
NUMBER_PIXELS = WIDTH * HEIGHT
13+
MAX_FPS = 45 # Maximum redraw rate, frames/second
14+
GRAIN_COLOR = (64, 64, 64)
15+
MAX_X = WIDTH * 256 - 1
16+
MAX_Y = HEIGHT * 256 - 1
17+
18+
class Grain:
19+
def __init__(self):
20+
x = 0
21+
y = 0
22+
vx = 0
23+
vy = 0
24+
25+
26+
grains = [Grain() for _ in range(N_GRAINS)]
27+
i2c = busio.I2C(board.SCL, board.SDA)
28+
sensor = adafruit_lsm303.LSM303(i2c)
29+
wing = adafruit_dotstar.DotStar(board.D13, board.D11, WIDTH * HEIGHT, 1.0, False)
30+
31+
oldidx = 0
32+
newidx = 0
33+
delta = 0
34+
newx = 0
35+
newy = 0
36+
37+
occupied_bits = [False for _ in range(WIDTH * HEIGHT)]
38+
39+
def index_of_xy(x, y):
40+
return (y >> 8) * WIDTH + (x >> 8)
41+
42+
def already_present(i, x, y):
43+
for j in range(i):
44+
if x == grains[j].x or y == grains[j].y:
45+
return True
46+
return False
47+
48+
for g in grains:
49+
placed = False
50+
while not placed:
51+
g.x = random.randint(0, WIDTH * 256 - 1)
52+
g.y = random.randint(0, HEIGHT * 256 - 1)
53+
placed = not occupied_bits[index_of_xy(g.x, g.y)]
54+
occupied_bits[index_of_xy(g.x, g.y)] = True
55+
g.vx = 0
56+
g.vy = 0
57+
58+
while True:
59+
# Display frame rendered on prior pass. It's done immediately after the
60+
# FPS sync (rather than after rendering) for consistent animation timing.
61+
62+
for i in range(NUMBER_PIXELS):
63+
wing[i] = GRAIN_COLOR if occupied_bits[i] else (0,0,0)
64+
wing.show()
65+
66+
# Read accelerometer...
67+
f_x, f_y, f_z = sensor.raw_accelerometer
68+
ax = f_x >> 8 # Transform accelerometer axes
69+
ay = f_y >> 8 # to grain coordinate space
70+
az = abs(f_z) >> 11 # Random motion factor
71+
az = 1 if (az >= 3) else (4 - az) # Clip & invert
72+
ax -= az # Subtract motion factor from X, Y
73+
ay -= az
74+
az2 = (az << 1) + 1 # Range of random motion to add back in
75+
76+
# ...and apply 2D accel vector to grain velocities...
77+
v2 = 0 # Velocity squared
78+
v = 0.0 # Absolute velociy
79+
for g in grains:
80+
g.vx += ax + random.randint(0, az2) # A little randomness makes
81+
g.vy += ay + random.randint(0, az2) # tall stacks topple better!
82+
83+
# Terminal velocity (in any direction) is 256 units -- equal to
84+
# 1 pixel -- which keeps moving grains from passing through each other
85+
# and other such mayhem. Though it takes some extra math, velocity is
86+
# clipped as a 2D vector (not separately-limited X & Y) so that
87+
# diagonal movement isn't faster
88+
89+
v2 = g.vx * g.vx + g.vy * g.vy
90+
if v2 > 65536: # If v^2 > 65536, then v > 256
91+
v = math.floor(math.sqrt(v2)) # Velocity vector magnitude
92+
g.vx = (g.vx // v) << 8 # Maintain heading
93+
g.vy = (g.vy // v) << 8 # Limit magnitude
94+
95+
# ...then update position of each grain, one at a time, checking for
96+
# collisions and having them react. This really seems like it shouldn't
97+
# work, as only one grain is considered at a time while the rest are
98+
# regarded as stationary. Yet this naive algorithm, taking many not-
99+
# technically-quite-correct steps, and repeated quickly enough,
100+
# visually integrates into something that somewhat resembles physics.
101+
# (I'd initially tried implementing this as a bunch of concurrent and
102+
# "realistic" elastic collisions among circular grains, but the
103+
# calculations and volument of code quickly got out of hand for both
104+
# the tiny 8-bit AVR microcontroller and my tiny dinosaur brain.)
105+
106+
for g in grains:
107+
newx = g.x + g.vx # New position in grain space
108+
newy = g.y + g.vy
109+
if newx > MAX_X: # If grain would go out of bounds
110+
newx = MAX_X # keep it inside, and
111+
g.vx //= -2 # give a slight bounce off the wall
112+
elif newx < 0:
113+
newx = 0
114+
g.vx //= -2
115+
if newy > MAX_Y:
116+
newy = MAX_Y
117+
g.vy //= -2
118+
elif newy < 0:
119+
newy = 0
120+
g.vy //= -2
121+
122+
oldidx = index_of_xy(g.x, g.y) # prior pixel
123+
newidx = index_of_xy(newx, newy) # new pixel
124+
if oldidx != newidx and occupied_bits[newidx]: # If grain is moving to a new pixel... but if that pixel is already occupied...
125+
delta = abs(newidx - oldidx) # What direction when blocked?
126+
if delta == 1: # 1 pixel left or right
127+
newx = g.x # cancel x motion
128+
g.vx //= -2 # and bounce X velocity (Y is ok)
129+
newidx = oldidx # no pixel change
130+
elif delta == WIDTH: # 1 pixel up or down
131+
newy = g.y # cancel Y motion
132+
g.vy //= -2 # and bounce Y velocity (X is ok)
133+
newidx = oldidx # no pixel change
134+
else: # Diagonal intersection is more tricky...
135+
# Try skidding along just one axis of motion if possible (start w/
136+
# faster axis). Because we've already established that diagonal
137+
# (both-axis) motion is occurring, moving on either axis alone WILL
138+
# change the pixel index, no need to check that again.
139+
if abs(g.vx) > abs(g.vy): # x axis is faster
140+
newidx = index_of_xy(newx, g.y)
141+
if not occupied_bits[newidx]: # that pixel is free, take it! But...
142+
newy = g.y # cancel Y motion
143+
g.vy //= -2 # and bounce Y velocity
144+
else: # X pixel is taken, so try Y...
145+
newidx = index_of_xy(g.x, newy)
146+
if not occupied_bits[newidx]: # Pixel is free, take it, but first...
147+
newx = g.x # Cancel X motion
148+
g.vx //= -2 # Bounce X velocity
149+
else: # both spots are occupied
150+
newx = g.x # Cancel X & Y motion
151+
newy = g.y
152+
g.vx //= -2 # Bounce X & Y velocity
153+
g.vy //= -2
154+
newidx = oldidx # Not moving
155+
else: # y axis is faster. start there
156+
newidx = index_of_xy(g.x, newy)
157+
if not occupied_bits[newidx]: # Pixel's free! Take it! But...
158+
newx = g.x # Cancel X motion
159+
g.vx //= -2 # Bounce X velocity
160+
else: # Y pixel is taken, so try X...
161+
newidx = index_of_xy(newx, g.y)
162+
if not occupied_bits[newidx]: # Pixel is free, take it, but first...
163+
newy = g.y # cancel Y motion
164+
g.vy //= -2 # and bounce Y velocity
165+
else: # both spots are occupied
166+
newx = g.x # Cancel X & Y motion
167+
newy = g.y
168+
g.vx //= -2 # Bounce X & Y velocity
169+
g.vy //= -2
170+
newidx = oldidx # Not moving
171+
occupied_bits[oldidx] = False
172+
occupied_bits[newidx] = True
173+
g.x = newx
174+
g.y = newy

0 commit comments

Comments
 (0)