From 04f9f2a448c25cb01f63cd0563f67e7ec7d42f91 Mon Sep 17 00:00:00 2001 From: Phillip Burgess Date: Sun, 17 Oct 2021 10:40:19 -0700 Subject: [PATCH] Add EyeLights BMP animation (CircuitPython only) --- EyeLights_BMP_Animation/code.py | 64 ++++++++++ EyeLights_BMP_Animation/eyelights_anim.py | 148 ++++++++++++++++++++++ EyeLights_BMP_Animation/matrix.bmp | Bin 0 -> 1076 bytes EyeLights_BMP_Animation/rings.bmp | Bin 0 -> 2436 bytes 4 files changed, 212 insertions(+) create mode 100755 EyeLights_BMP_Animation/code.py create mode 100755 EyeLights_BMP_Animation/eyelights_anim.py create mode 100755 EyeLights_BMP_Animation/matrix.bmp create mode 100755 EyeLights_BMP_Animation/rings.bmp diff --git a/EyeLights_BMP_Animation/code.py b/EyeLights_BMP_Animation/code.py new file mode 100755 index 000000000..f49412eca --- /dev/null +++ b/EyeLights_BMP_Animation/code.py @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: 2021 Phil Burgess for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +EyeLightsAnim example for Adafruit EyeLights (LED Glasses + Driver). +The accompanying eyelights_anim.py provides pre-drawn frame-by-frame +animation from BMP images. Sort of a catch-all for modest projects that may +want to implement some animation without having to express that animation +entirely in code. The idea is based upon two prior projects: + +https://learn.adafruit.com/32x32-square-pixel-display/overview +learn.adafruit.com/circuit-playground-neoanim-using-bitmaps-to-animate-neopixels + +The 18x5 matrix and the LED rings are regarded as distinct things, fed from +two separate BMPs (or can use just one or the other). The former guide above +uses the vertical axis for time (like a strip of movie film), while the +latter uses the horizontal axis for time (as in audio or video editing). +Despite this contrast, the same conventions are maintained here to avoid +conflicting explanations...what worked in those guides is what works here, +only the resolutions are different. See also the example BMPs. +""" + +import time +import board +from busio import I2C +import adafruit_is31fl3741 +from adafruit_is31fl3741.adafruit_ledglasses import LED_Glasses +from eyelights_anim import EyeLightsAnim + + +# HARDWARE SETUP ----------------------- + +i2c = I2C(board.SCL, board.SDA, frequency=1000000) + +# Initialize the IS31 LED driver, buffered for smoother animation +glasses = LED_Glasses(i2c, allocate=adafruit_is31fl3741.MUST_BUFFER) +glasses.show() # Clear any residue on startup +glasses.global_current = 20 # Just middlin' bright, please + + +# ANIMATION SETUP ---------------------- + +# Two indexed-color BMP filenames are specified: first is for the LED matrix +# portion, second is for the LED rings -- or pass None for one or the other +# if not animating that part. The two elements, matrix and rings, share a +# few LEDs in common...by default the rings appear "on top" of the matrix, +# or you can optionally pass a third argument of False to have the rings +# underneath. There's that one odd unaligned pixel between the two though, +# so this may only rarely be desirable. +anim = EyeLightsAnim(glasses, "matrix.bmp", "rings.bmp") + + +# MAIN LOOP ---------------------------- + +# This example just runs through a repeating cycle. If you need something +# else, like ping-pong animation, or frames based on a specific time, the +# anim.frame() function can optionally accept two arguments: an index for +# the matrix animation, and an index for the rings. + +while True: + anim.frame() # Advance matrix and rings by 1 frame and wrap around + glasses.show() # Update LED matrix + time.sleep(0.02) # Pause briefly diff --git a/EyeLights_BMP_Animation/eyelights_anim.py b/EyeLights_BMP_Animation/eyelights_anim.py new file mode 100755 index 000000000..78ba3e438 --- /dev/null +++ b/EyeLights_BMP_Animation/eyelights_anim.py @@ -0,0 +1,148 @@ +# SPDX-FileCopyrightText: 2021 Phil Burgess for Adafruit Industries +# +# SPDX-License-Identifier: MIT + +""" +EyeLightsAnim provides EyeLights LED glasses with pre-drawn frame-by-frame +animation from BMP images. Sort of a catch-all for modest projects that may +want to implement some animation without having to express that animation +entirely in code. The idea is based upon two prior projects: + +https://learn.adafruit.com/32x32-square-pixel-display/overview +learn.adafruit.com/circuit-playground-neoanim-using-bitmaps-to-animate-neopixels + +The 18x5 matrix and the LED rings are regarded as distinct things, fed from +two separate BMPs (or can use just one or the other). The former guide above +uses the vertical axis for time (like a strip of movie film), while the +latter uses the horizontal axis for time (as in audio or video editing). +Despite this contrast, the same conventions are maintained here to avoid +conflicting explanations...what worked in those guides is what works here, +only the resolutions are different.""" + +import displayio +import adafruit_imageload + + +def gamma_adjust(palette): + """Given a color palette that was returned by adafruit_imageload, apply + gamma correction and place results back in original palette. This makes + LED brightness and colors more perceptually linear, to better match how + the source BMP might've appeared on screen.""" + + for index, entry in enumerate(palette): + palette[index] = sum( + [ + int(((((entry >> shift) & 0xFF) / 255) ** 2.6) * 255 + 0.5) << shift + for shift in range(16, -1, -8) + ] + ) + + +class EyeLightsAnim: + """Class encapsulating BMP image-based frame animation for the matrix + and rings of an LED_Glasses object.""" + + def __init__(self, glasses, matrix_filename, ring_filename, rings_on_top=True): + """Constructor for EyeLightsAnim. Accepts an LED_Glasses object and + filenames for two indexed-color BMP images: first is a "sprite + sheet" for animating on the matrix portion of the glasses, second is + a pixels-over-time graph for the rings portion. Either filename may + be None if not used. Because the matrix and rings share some pixels + in common, the last argument determines the "stacking order" - which + of the two bitmaps is drawn later or "on top." Default of True + places the rings over the matrix, False gives the matrix priority. + It's possible to use transparent palette indices but that may be + more trouble than it's worth.""" + + self.glasses = glasses + self.matrix_bitmap = self.ring_bitmap = None + self.rings_on_top = rings_on_top + + if matrix_filename: + self.matrix_bitmap, self.matrix_palette = adafruit_imageload.load( + matrix_filename, bitmap=displayio.Bitmap, palette=displayio.Palette + ) + # pylint and black can't agree on formatting this statement, so... + # pylint: disable=bad-continuation + if (self.matrix_bitmap.width < glasses.width) or ( + self.matrix_bitmap.height < glasses.height + ): + raise ValueError("Matrix bitmap must be at least 18x5 pixels") + # pylint: enable=bad-continuation + gamma_adjust(self.matrix_palette) + self.tiles_across = self.matrix_bitmap.width // glasses.width + self.tiles_down = self.matrix_bitmap.height // glasses.height + self.matrix_frames = self.tiles_across * self.tiles_down + self.matrix_frame = self.matrix_frames - 1 + + if ring_filename: + self.ring_bitmap, self.ring_palette = adafruit_imageload.load( + ring_filename, bitmap=displayio.Bitmap, palette=displayio.Palette + ) + if self.ring_bitmap.height < 48: + raise ValueError("Ring bitmap must be at least 48 pixels tall") + gamma_adjust(self.ring_palette) + self.ring_frames = self.ring_bitmap.width + self.ring_frame = self.ring_frames - 1 + + def draw_matrix(self, matrix_frame=None): + """Draw the matrix portion of EyeLights from one frame of the matrix + bitmap "sprite sheet." Can either request a specific frame index + (starting from 0), or pass None (or no arguments) to advance by one + frame, "wrapping around" to beginning if needed. For internal use by + library; user code should call frame(), not this function.""" + + if matrix_frame: # Go to specific frame + self.matrix_frame = matrix_frame + else: # Advance one frame forward + self.matrix_frame += 1 + self.matrix_frame %= self.matrix_frames # Wrap to valid range + + xoffset = self.matrix_frame % self.tiles_across * self.glasses.width + yoffset = self.matrix_frame // self.tiles_across * self.glasses.height + + for y in range(self.glasses.height): + y1 = y + yoffset + for x in range(self.glasses.width): + idx = self.matrix_bitmap[x + xoffset, y1] + if not self.matrix_palette.is_transparent(idx): + self.glasses.pixel(x, y, self.matrix_palette[idx]) + + def draw_rings(self, ring_frame=None): + """Draw the rings portion of EyeLights from one frame of the rings + bitmap graph. Can either request a specific frame index (starting + from 0), or pass None (or no arguments) to advance by one frame, + 'wrapping around' to beginning if needed. For internal use by + library; user code should call frame(), not this function.""" + + if ring_frame: # Go to specific frame + self.ring_frame = ring_frame + else: # Advance one frame forward + self.ring_frame += 1 + self.ring_frame %= self.ring_frames # Wrap to valid range + + for y in range(24): + idx = self.ring_bitmap[self.ring_frame, y] + if not self.ring_palette.is_transparent(idx): + self.glasses.left_ring[y] = self.ring_palette[idx] + idx = self.ring_bitmap[self.ring_frame, y + 24] + if not self.ring_palette.is_transparent(idx): + self.glasses.right_ring[y] = self.ring_palette[idx] + + def frame(self, matrix_frame=None, ring_frame=None): + """Draw one frame of animation to the matrix and/or rings portions + of EyeLights. Frame index (starting from 0) for matrix and rings + respectively can be passed as arguments, or either/both may be None + to advance by one frame, 'wrapping around' to beginning if needed. + Because some pixels are shared in common between matrix and rings, + the "stacking order" -- which of the two appears "on top", is + specified as an argument to the constructor.""" + + if self.matrix_bitmap and self.rings_on_top: + self.draw_matrix(matrix_frame) + + if self.ring_bitmap: + self.draw_rings(ring_frame) + + if self.matrix_bitmap and not self.rings_on_top: + self.draw_matrix(matrix_frame) diff --git a/EyeLights_BMP_Animation/matrix.bmp b/EyeLights_BMP_Animation/matrix.bmp new file mode 100755 index 0000000000000000000000000000000000000000..d497da88f72de8020b7c7f8f54a6e8c8db6f8625 GIT binary patch literal 1076 zcmbtTK@P$&40BnKIP(QAoH!uvlQ>S|O?%q@!zYHDtxQc)X^2XxGRv`(wtK!F0$l_5 zE91<#GZIeRfp-U7Zc7~bonxM7Ow)vM9MSiErs0qvy1J4P+;sXzOObdOIS2bK~$GSRYg?RL`4JR9o(=r;0kJR0Tnoc z66`<$5?~yN6qfHzT*usa_is7~!Z2Y=;?@qLABBS~GrlKGr9Y(H>t4Y6>5#_$`CP;y zR=15T`k+~MowF>_XXdBPj*r#xl71TT5Uc{twWIne|8ON#9P literal 0 HcmV?d00001