Skip to content

Commit 8d8edc8

Browse files
Merge pull request #1896 from PaintYourDragon/main
Add EyeLights BMP animation (CircuitPython only)
2 parents c40eea5 + eea11cd commit 8d8edc8

File tree

4 files changed

+209
-0
lines changed

4 files changed

+209
-0
lines changed

EyeLights_BMP_Animation/code.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# SPDX-FileCopyrightText: 2021 Phil Burgess for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
"""
6+
EyeLightsAnim example for Adafruit EyeLights (LED Glasses + Driver).
7+
The accompanying eyelights_anim.py provides pre-drawn frame-by-frame
8+
animation from BMP images. Sort of a catch-all for modest projects that may
9+
want to implement some animation without having to express that animation
10+
entirely in code. The idea is based upon two prior projects:
11+
12+
https://learn.adafruit.com/32x32-square-pixel-display/overview
13+
learn.adafruit.com/circuit-playground-neoanim-using-bitmaps-to-animate-neopixels
14+
15+
The 18x5 matrix and the LED rings are regarded as distinct things, fed from
16+
two separate BMPs (or can use just one or the other). The former guide above
17+
uses the vertical axis for time (like a strip of movie film), while the
18+
latter uses the horizontal axis for time (as in audio or video editing).
19+
Despite this contrast, the same conventions are maintained here to avoid
20+
conflicting explanations...what worked in those guides is what works here,
21+
only the resolutions are different. See also the example BMPs.
22+
"""
23+
24+
import time
25+
import board
26+
from busio import I2C
27+
import adafruit_is31fl3741
28+
from adafruit_is31fl3741.adafruit_ledglasses import LED_Glasses
29+
from eyelights_anim import EyeLightsAnim
30+
31+
32+
# HARDWARE SETUP -----------------------
33+
34+
i2c = I2C(board.SCL, board.SDA, frequency=1000000)
35+
36+
# Initialize the IS31 LED driver, buffered for smoother animation
37+
glasses = LED_Glasses(i2c, allocate=adafruit_is31fl3741.MUST_BUFFER)
38+
glasses.show() # Clear any residue on startup
39+
glasses.global_current = 20 # Just middlin' bright, please
40+
41+
42+
# ANIMATION SETUP ----------------------
43+
44+
# Two indexed-color BMP filenames are specified: first is for the LED matrix
45+
# portion, second is for the LED rings -- or pass None for one or the other
46+
# if not animating that part. The two elements, matrix and rings, share a
47+
# few LEDs in common...by default the rings appear "on top" of the matrix,
48+
# or you can optionally pass a third argument of False to have the rings
49+
# underneath. There's that one odd unaligned pixel between the two though,
50+
# so this may only rarely be desirable.
51+
anim = EyeLightsAnim(glasses, "matrix.bmp", "rings.bmp")
52+
53+
54+
# MAIN LOOP ----------------------------
55+
56+
# This example just runs through a repeating cycle. If you need something
57+
# else, like ping-pong animation, or frames based on a specific time, the
58+
# anim.frame() function can optionally accept two arguments: an index for
59+
# the matrix animation, and an index for the rings.
60+
61+
while True:
62+
anim.frame() # Advance matrix and rings by 1 frame and wrap around
63+
glasses.show() # Update LED matrix
64+
time.sleep(0.02) # Pause briefly
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# SPDX-FileCopyrightText: 2021 Phil Burgess for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
"""
6+
EyeLightsAnim provides EyeLights LED glasses with pre-drawn frame-by-frame
7+
animation from BMP images. Sort of a catch-all for modest projects that may
8+
want to implement some animation without having to express that animation
9+
entirely in code. The idea is based upon two prior projects:
10+
11+
https://learn.adafruit.com/32x32-square-pixel-display/overview
12+
learn.adafruit.com/circuit-playground-neoanim-using-bitmaps-to-animate-neopixels
13+
14+
The 18x5 matrix and the LED rings are regarded as distinct things, fed from
15+
two separate BMPs (or can use just one or the other). The former guide above
16+
uses the vertical axis for time (like a strip of movie film), while the
17+
latter uses the horizontal axis for time (as in audio or video editing).
18+
Despite this contrast, the same conventions are maintained here to avoid
19+
conflicting explanations...what worked in those guides is what works here,
20+
only the resolutions are different."""
21+
22+
import displayio
23+
import adafruit_imageload
24+
25+
26+
def gamma_adjust(palette):
27+
"""Given a color palette that was returned by adafruit_imageload, apply
28+
gamma correction and place results back in original palette. This makes
29+
LED brightness and colors more perceptually linear, to better match how
30+
the source BMP might've appeared on screen."""
31+
32+
for index, entry in enumerate(palette):
33+
palette[index] = sum(
34+
[
35+
int(((((entry >> shift) & 0xFF) / 255) ** 2.6) * 255 + 0.5) << shift
36+
for shift in range(16, -1, -8)
37+
]
38+
)
39+
40+
41+
class EyeLightsAnim:
42+
"""Class encapsulating BMP image-based frame animation for the matrix
43+
and rings of an LED_Glasses object."""
44+
45+
def __init__(self, glasses, matrix_filename, ring_filename, rings_on_top=True):
46+
"""Constructor for EyeLightsAnim. Accepts an LED_Glasses object and
47+
filenames for two indexed-color BMP images: first is a "sprite
48+
sheet" for animating on the matrix portion of the glasses, second is
49+
a pixels-over-time graph for the rings portion. Either filename may
50+
be None if not used. Because the matrix and rings share some pixels
51+
in common, the last argument determines the "stacking order" - which
52+
of the two bitmaps is drawn later or "on top." Default of True
53+
places the rings over the matrix, False gives the matrix priority.
54+
It's possible to use transparent palette indices but that may be
55+
more trouble than it's worth."""
56+
57+
self.glasses = glasses
58+
self.matrix_bitmap = self.ring_bitmap = None
59+
self.rings_on_top = rings_on_top
60+
61+
if matrix_filename:
62+
self.matrix_bitmap, self.matrix_palette = adafruit_imageload.load(
63+
matrix_filename, bitmap=displayio.Bitmap, palette=displayio.Palette
64+
)
65+
if (self.matrix_bitmap.width < glasses.width) or (
66+
self.matrix_bitmap.height < glasses.height
67+
):
68+
raise ValueError("Matrix bitmap must be at least 18x5 pixels")
69+
gamma_adjust(self.matrix_palette)
70+
self.tiles_across = self.matrix_bitmap.width // glasses.width
71+
self.tiles_down = self.matrix_bitmap.height // glasses.height
72+
self.matrix_frames = self.tiles_across * self.tiles_down
73+
self.matrix_frame = self.matrix_frames - 1
74+
75+
if ring_filename:
76+
self.ring_bitmap, self.ring_palette = adafruit_imageload.load(
77+
ring_filename, bitmap=displayio.Bitmap, palette=displayio.Palette
78+
)
79+
if self.ring_bitmap.height < 48:
80+
raise ValueError("Ring bitmap must be at least 48 pixels tall")
81+
gamma_adjust(self.ring_palette)
82+
self.ring_frames = self.ring_bitmap.width
83+
self.ring_frame = self.ring_frames - 1
84+
85+
def draw_matrix(self, matrix_frame=None):
86+
"""Draw the matrix portion of EyeLights from one frame of the matrix
87+
bitmap "sprite sheet." Can either request a specific frame index
88+
(starting from 0), or pass None (or no arguments) to advance by one
89+
frame, "wrapping around" to beginning if needed. For internal use by
90+
library; user code should call frame(), not this function."""
91+
92+
if matrix_frame: # Go to specific frame
93+
self.matrix_frame = matrix_frame
94+
else: # Advance one frame forward
95+
self.matrix_frame += 1
96+
self.matrix_frame %= self.matrix_frames # Wrap to valid range
97+
98+
xoffset = self.matrix_frame % self.tiles_across * self.glasses.width
99+
yoffset = self.matrix_frame // self.tiles_across * self.glasses.height
100+
101+
for y in range(self.glasses.height):
102+
y1 = y + yoffset
103+
for x in range(self.glasses.width):
104+
idx = self.matrix_bitmap[x + xoffset, y1]
105+
if not self.matrix_palette.is_transparent(idx):
106+
self.glasses.pixel(x, y, self.matrix_palette[idx])
107+
108+
def draw_rings(self, ring_frame=None):
109+
"""Draw the rings portion of EyeLights from one frame of the rings
110+
bitmap graph. Can either request a specific frame index (starting
111+
from 0), or pass None (or no arguments) to advance by one frame,
112+
'wrapping around' to beginning if needed. For internal use by
113+
library; user code should call frame(), not this function."""
114+
115+
if ring_frame: # Go to specific frame
116+
self.ring_frame = ring_frame
117+
else: # Advance one frame forward
118+
self.ring_frame += 1
119+
self.ring_frame %= self.ring_frames # Wrap to valid range
120+
121+
for y in range(24):
122+
idx = self.ring_bitmap[self.ring_frame, y]
123+
if not self.ring_palette.is_transparent(idx):
124+
self.glasses.left_ring[y] = self.ring_palette[idx]
125+
idx = self.ring_bitmap[self.ring_frame, y + 24]
126+
if not self.ring_palette.is_transparent(idx):
127+
self.glasses.right_ring[y] = self.ring_palette[idx]
128+
129+
def frame(self, matrix_frame=None, ring_frame=None):
130+
"""Draw one frame of animation to the matrix and/or rings portions
131+
of EyeLights. Frame index (starting from 0) for matrix and rings
132+
respectively can be passed as arguments, or either/both may be None
133+
to advance by one frame, 'wrapping around' to beginning if needed.
134+
Because some pixels are shared in common between matrix and rings,
135+
the "stacking order" -- which of the two appears "on top", is
136+
specified as an argument to the constructor."""
137+
138+
if self.matrix_bitmap and self.rings_on_top:
139+
self.draw_matrix(matrix_frame)
140+
141+
if self.ring_bitmap:
142+
self.draw_rings(ring_frame)
143+
144+
if self.matrix_bitmap and not self.rings_on_top:
145+
self.draw_matrix(matrix_frame)

EyeLights_BMP_Animation/matrix.bmp

1.05 KB
Binary file not shown.

EyeLights_BMP_Animation/rings.bmp

2.38 KB
Binary file not shown.

0 commit comments

Comments
 (0)