|
| 1 | +"""Gravity Pulls Pixel |
| 2 | +
|
| 3 | +This program uses the Circuit Playground Express’s accelerometer to position |
| 4 | +a white pixel as if gravity were pulling it. |
| 5 | +
|
| 6 | +Flip the switch left (toward the notes) to turn on debugging messages and |
| 7 | +slow down the action. See a code walkthrough here: https://youtu.be/sZ4tNOUKRpw |
| 8 | +""" |
| 9 | +import time |
| 10 | +import math |
| 11 | +from adafruit_circuitplayground.express import cpx |
| 12 | + |
| 13 | +PIXEL_SPACING_ANGLE = 30 |
| 14 | +STANDARD_GRAVITY = 9.81 |
| 15 | + |
| 16 | +BACKGROUND_COLOR = 0, 0, 64 |
| 17 | +MIN_BRIGHTNESS = 15 # Minimum brightness for anti-aliasing |
| 18 | +LIGHTING_ARC_LENGTH = 45 |
| 19 | + |
| 20 | + |
| 21 | +def compute_pixel_angles(): |
| 22 | + """Return a list of rotation angles of the ten NeoPixels. |
| 23 | +
|
| 24 | + On the CPX there are ten pixels arranged like the 12 hours of a clock, except that positions |
| 25 | + 6 and 12 are empty. The numbers in the list are the angles from the (x, y) accelerometer |
| 26 | + values for each pixel when the CPX is rotated with that pixel at the bottom. For example, |
| 27 | + with pixel 0 at the bottom (and pixel 5 at the top), the accelerometer’s (x, y) values |
| 28 | + give an angle of 300°. Rotated clockwise 1/12 turn (30°), so that pixel 1 is at the bottom, |
| 29 | + the angle is 330°. |
| 30 | + """ |
| 31 | + return [(300 + PIXEL_SPACING_ANGLE * n) % 360 for n in range(12) if n not in (5, 11)] |
| 32 | + |
| 33 | + |
| 34 | +def degrees_between(a1, a2): |
| 35 | + smaller = min(a1, a2) |
| 36 | + larger = max(a1, a2) |
| 37 | + return min(larger - smaller, 360 + smaller - larger) |
| 38 | + |
| 39 | + |
| 40 | +def pixel_brightness(distance_from_down, accel_magnitude): |
| 41 | + """Return the a brightness for a pixel, or None if the pixel is not in the lighting arc""" |
| 42 | + half_lighting_arc_length = LIGHTING_ARC_LENGTH / 2 |
| 43 | + |
| 44 | + if accel_magnitude < 0.1 or distance_from_down > half_lighting_arc_length: |
| 45 | + return None |
| 46 | + |
| 47 | + normalized_nearness = 1 - distance_from_down / half_lighting_arc_length |
| 48 | + scale_factor = (255 - MIN_BRIGHTNESS) * accel_magnitude |
| 49 | + color_part = MIN_BRIGHTNESS + round(normalized_nearness * scale_factor) |
| 50 | + return color_part |
| 51 | + |
| 52 | + |
| 53 | +def angle_in_degrees(x, y): |
| 54 | + """Return the angle of the point (x, y), in degrees from -180 to 180""" |
| 55 | + return math.atan2(y, x) / math.pi * 180 |
| 56 | + |
| 57 | + |
| 58 | +def positive_degrees(angle): |
| 59 | + """Convert -180 through 180 to 0 through 360""" |
| 60 | + return (angle + 360) % 360 |
| 61 | + |
| 62 | + |
| 63 | +cpx.pixels.brightness = 0.1 # Adjust overall brightness as desired, between 0 and 1 |
| 64 | +pixel_positions = compute_pixel_angles() |
| 65 | + |
| 66 | +while True: |
| 67 | + debug = cpx.switch # True is toward the left |
| 68 | + accel_x, accel_y = cpx.acceleration[:2] # Ignore z |
| 69 | + down_angle = positive_degrees(angle_in_degrees(accel_x, accel_y)) |
| 70 | + magnitude_limit = STANDARD_GRAVITY |
| 71 | + normalized_magnitude = min(math.sqrt(accel_x * accel_x + accel_y * accel_y), |
| 72 | + magnitude_limit) / magnitude_limit |
| 73 | + |
| 74 | + pixels_lit = [] |
| 75 | + for i, pixel_position in enumerate(pixel_positions): |
| 76 | + pe = pixel_brightness(degrees_between(pixel_position, down_angle), normalized_magnitude) |
| 77 | + cpx.pixels[i] = (pe, pe, pe) if pe else BACKGROUND_COLOR |
| 78 | + if pe: |
| 79 | + pixels_lit.append((i, pe)) |
| 80 | + |
| 81 | + if debug: |
| 82 | + lit_formatted = ', '.join(('{}: {:>3d}'.format(p, i) for p, i in pixels_lit)) |
| 83 | + print('x: {:>6.2f}, y: {:>6.2f}, angle: {:>6.2f}, mag: {:>3.2f}, pixels: [{}]'.format( |
| 84 | + accel_x, accel_y, down_angle, normalized_magnitude, lit_formatted)) |
| 85 | + time.sleep(0.5) |
0 commit comments