Toggle Navigation
Hatchery
Eggs
Mod Music Player
graphics.py
Users
Badges
Login
Register
MCH2022 badge?
go to mch2022.badge.team
graphics.py
raw
Content
import gc import random import sys from math import floor, ceil if not hasattr(sys, "_called_from_test"): from .settings import KEYS from .rotated_list import forward, backward import display else: from settings import KEYS from rotated_list import forward, backward # ttb, rtl, btt -> left to right, top to bottom, right to left. perhaps later diagonal. DIRECTION_LTR = 0 DIRECTION_RTL = 1 DIRECTION_TTB = 2 DIRECTION_BTT = 3 RED = 0xFF0000 GREEN = 0x00FF00 BLUE = 0x0000FF WHITE = 0xFFFFFF BLACK = 0x000000 width = height = [0, 1, 2, 3] state = { "frame": 0, "current_animation": None, "sweep": {"position": 0, "direction": DIRECTION_LTR, "limit_frames": 2}, "flash": {"limit_frames": 8}, "debug": {"limit_frames": 100, "limit_subframes": 10}, "angle": {"direction": 0}, "shapes": {"limit_frames": 5} } # can only write the the first level of a dictionary. # sounds crazy, but it seems to be the case. smooth_color_transition_directions = {"r": 1, "g": 1, "b": 1} sweep_current_color = {'color': WHITE} opposites = {3: 0, 2: 1, 1: 2, 0: 3} happy_colors = [0xFF6E6E, 0xFFA36E, 0xFFC06E, 0x59FFAC, 0x59A9FF, 0x8E59FF, 0xF530FF] # This is the first color of the display, and it will be continuously adjusted. smooth_transition_current_color = {'color': 0xFF6E6E} state["current_animation"] = default_animation = "smooth_color_transition" def darken_color_part(hex_color, color_name): bits = 16 if color_name == "r" else 8 if color_name == "g" else 0 current_color = hex_color >> bits & 0b11111111 new_color = ceil(current_color * 0.9) # don't go completely dark, so the hex value still has something to << return 17 if new_color < 1 else new_color def lighten_color_part(hex_color, color_name): bits = 16 if color_name == "r" else 8 if color_name == "g" else 0 current_color = hex_color >> bits & 0b11111111 new_color = ceil(current_color * 1.1) # don't go completely dark, so the hex value still has something to << return 255 if new_color > 255 else new_color def make_new_color_part(hex_color, color_name: str): global smooth_color_transition_directions bits = 16 if color_name == "r" else 8 if color_name == "g" else 0 # https://stackoverflow.com/questions/45220959/python-how-do-i-extract-specific-bits-from-a-byte current_color = hex_color >> bits & 0b11111111 # 255 == max if current_color > 249: smooth_color_transition_directions[color_name] = 0 # don't make it too dark if current_color < 100: smooth_color_transition_directions[color_name] = 1 return ( current_color + random.randint(0, 2) if smooth_color_transition_directions[color_name] else current_color - random.randint(0, 2) ) def smooth_transition_color(hex_color): """ # slowly adjust the color based on random values. This will slowly change the color of the display. # the base color can be changed by hitting a button. How can we do some bit magic that changes one # of the colors slightly? This can be done per pixel even, which is nicer. But let's start with one color and # fading. # Colors are always represented in 24-bit from within Python, in the 0xRRGGBB format. # This matches HTML/CSS colors which are #RRGGBB as well. 6 bits. :param hex_color: :return: """ return ( make_new_color_part(hex_color, "r") << 16 | make_new_color_part(hex_color, "g") << 8 | make_new_color_part(hex_color, "b") ) def darken_color(hex_color): darker = ( darken_color_part(hex_color, "r") << 16 | darken_color_part(hex_color, "g") << 8 | darken_color_part(hex_color, "b") ) return darker def lighten_color(hex_color): lighter = ( lighten_color_part(hex_color, "r") << 16 | lighten_color_part(hex_color, "g") << 8 | lighten_color_part(hex_color, "b") ) return lighter def get_random_color(): global happy_colors return happy_colors[random.randint(0, len(happy_colors) - 1)] def full_sceen_feedback(): global state, smooth_transition_current_color new_color = get_random_color() # x, y, width, height, filled, color display.drawFill(new_color) # as an extra, alter the smooth color transition, so during this animation the animation # continues from this random color, which looks more cool smooth_transition_current_color['color'] = new_color def bootscreen(): display.drawFill(WHITE) display.flush() def animation_wifi_searching(): # waiting for wifi is a thing. The 'wifi' samples are probably not available. So visualize without deps. # draw wifi signal thingy, the top left corner is a status color, that changes color every connection attempt. display.drawFill(WHITE) display.drawPixel(0, 2, 0x333333) display.drawPixel(1, 1, 0x333333) display.drawPixel(2, 1, 0x333333) display.drawPixel(3, 2, 0x333333) display.flush() def animation_wifi_searching_progress(): # used as a progress bar indicator, also helps with feedback when crashing during (WIFI) setup. display.drawPixel(0, 0, get_random_color()) display.flush() def draw_buttons(): # previous and next buttons for music and animations, lighten_button(KEYS["previous_track"]) darken_button(KEYS["previous_animation"]) darken_button(KEYS["next_animation"]) lighten_button(KEYS["next_track"]) def key_to_xy(key_number): return key_number % 4, floor(key_number / 4) def lighten_button(key_number): x, y = key_to_xy(key_number) display.drawPixel(x, y, lighten_color(swap_rb(display.getPixel(x, y)))) def darken_button(key_number): x, y = key_to_xy(key_number) display.drawPixel(x, y, darken_color(swap_rb(display.getPixel(x, y)))) def sweep_frame(): global state, opposites, sweep_current_color """ Draws a line in a random direction: left to right, up to down etc. Together with fade_out this will create an interesting loading effect. todo: i can just reorient the frame buffer... which is much easier. But this will not allow two animations at the same time in different directions. So no. Or then again re-pan? :return: """ angles = { DIRECTION_LTR: 0, DIRECTION_RTL: 180, DIRECTION_TTB: 90, DIRECTION_BTT: 270, } display.orientation(angles[state["sweep"]["direction"]]) display.drawLine(state["sweep"]["position"], 0, state["sweep"]["position"], 3, sweep_current_color['color']) display.orientation(-angles[state["sweep"]["direction"]]) # prepare for the next sweep animation step # It turns out, that for whatever reason, you can't write to global integer directly, but you can to a dict. if state["sweep"]["position"] > 2: state["sweep"]["position"] = 0 # next sweep direction state["sweep"]["direction"] = random.choice( [DIRECTION_LTR, DIRECTION_TTB, DIRECTION_RTL, DIRECTION_BTT] ) # next sweep color: sweep_current_color['color'] = get_random_color() else: state["sweep"]["position"] += 1 def get_frame(): frame = [] for x in width: for y in height: frame += [display.getPixel(x, y)] return frame matrix_drops = [] matrix_blinks = [] def animate_matrix(): # Creates a path and walks through it. global state, matrix_drops, matrix_blinks # animate drops new_drops = [] for drop in matrix_drops: x, y = drop # disappearing drop if y == 4: continue if y < 3: display.drawPixel(x, y + 1, GREEN) new_drops.append((x, y + 1)) matrix_drops = new_drops # add new drops: if random.randint(0, 10) < 5: matrix_drops.append([random.randint(0, 3), random.randint(-1, 2)]) # animate blinks new_blinks = [] for blink in matrix_blinks: x, y = blink if random.randint(0, 100) > 90: # blink disappears continue display.drawPixel(x, y, GREEN) if random.randint(0, 100) > 90: # blink disappears display.drawPixel(x, y, darken_color(GREEN)) if random.randint(0, 100) > 90: # blink disappears display.drawPixel(x, y, lighten_color(GREEN)) new_blinks.append((x, y)) matrix_blinks = new_blinks if random.randint(0, 10) < 4: matrix_blinks.append([random.randint(0, 3), random.randint(0, 3)]) fade_out_entire_display() def animate_shapes(): shapes = [ # dash [1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1], # circle [0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0], # 4x line [0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1], # sideways line [0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0], # checkerboard [1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1], # arrow [0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0], # big line [0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0], # small circle [0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0], # large circle [1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1], # side bar [1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], # blinds [1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0], ] # randomize direction, color and inversion orientation = random.choice([0, 90, 180, 270]) shape = random.choice(shapes) inverse = random.choice([0, 1]) color = get_random_color() if inverse: shape = [0x000000 if thing else color for thing in shape] else: shape = [color if thing else 0x000000 for thing in shape] if state["frame"] % state["shapes"]["limit_frames"] == 0: display.orientation(orientation) for i in range(0, 16): display.drawPixel(i // 4, i % 4, shape[i]) display.orientation(-orientation) else: fade_out_entire_display() def fade_out_entire_display(): """ Fades the entire screen with one step. This is used to create ghosting effects. Reads all pixels, and writes them with a darker variant. Probably also possible with a single mask operation... but oh welll.. :return: """ # makes it looks like the fading comes from the center, which is smoother for x in [1, 2, 3, 0]: for y in [1, 2, 3, 0]: display.drawPixel(x, y, swap_rb(darken_color(display.getPixel(x, y)))) def animate_sweep(): if state["frame"] % state["sweep"]["limit_frames"] == 0: sweep_frame() fade_out_entire_display() def animate_smooth_color_transition(): # noqa not using time because it's not needed. Provided for when you do. global smooth_transition_current_color smooth_transition_current_color['color'] = smooth_transition_color( smooth_transition_current_color['color'] ) display.drawFill(smooth_transition_current_color['color']) def animate_sparkle(): # a random pixel somewhere, which fades out. display.drawPixel( random.choice([0, 1, 2, 3]), random.choice([0, 1, 2, 3]), get_random_color() ) fade_out_entire_display() def animate_amateurstrobe(): frames = { 0: BLACK, 1: WHITE, } display.drawFill(frames[state["frame"] % len(frames)]) def animate_professionalstrobe(): frames = { 0: RED, 1: GREEN, 2: BLUE, } display.drawFill(frames[state["frame"] % len(frames)]) def animate_chadstrobe(): # only for chads frames = { 0: WHITE, 1: BLACK, 2: RED, 3: BLACK, 4: WHITE, 5: BLACK, 6: GREEN, 7: BLACK, 8: WHITE, 9: BLACK, 10: BLUE, 11: BLACK } display.drawFill(frames[state["frame"] % len(frames)]) def swap_rb(hex_color): """ Swap R and B color, as an attempt to see if it's possible to work with the badget display code instead of my own python library (which is ofc much slower). Of course this is also a slow routine... :param hex_color: :return: """ r = hex_color >> 16 & 0b11111111 g = hex_color >> 8 & 0b11111111 b = hex_color >> 0 & 0b11111111 return b << 16 | g << 8 | r def animate_debug(): # fill the entire screen, which fades out, every 5 frames # the getPixel retrieves a sampled down value, and then writes that again, this causes colors to get lost. global state if state["frame"] % state["debug"]["limit_frames"] == 0: new_color = get_random_color() display.drawFill(new_color) print("New color: " + bin(new_color)) if state["frame"] % state["debug"]["limit_subframes"] == 0: # Because getpixel does not return the value that has been drawn # the screen will do crazy stuff when writing the current value to it read = display.getPixel(0, 0) read = swap_rb(read) display.drawFill(read) print("The display is now: " + bin(read)) animations = { "smooth_color_transition": animate_smooth_color_transition, "swipe": animate_sweep, "sparkle": animate_sparkle, "matrix": animate_matrix, "shapes": animate_shapes, "debug": animate_debug, "amateur_strobe": animate_amateurstrobe, "professional_strobe": animate_professionalstrobe, "chad_strobe": animate_chadstrobe, } available_animations = ['smooth_color_transition', "shapes", 'swipe', 'sparkle','matrix', 'amateur_strobe', 'professional_strobe', 'chad_strobe'] # dict.keys() didn't work... def get_previous_animation(): global available_animations, state return backward(available_animations, state["current_animation"]) def get_next_animation(): global available_animations, state return forward(available_animations, state["current_animation"]) def set_animation(animation_name): global state print("Setting animation to: " + animation_name) state["current_animation"] = animation_name def progress_animation( mytimer, ): # noqa not using time because it's not needed. Provided for when you do. """ This is called with a timer, each call is a frame. :param mytimer: :return: """ global animations, state # make sure it doesn't 'overflow' state["frame"] += 1 if state["frame"] > 10000: state["frame"] = 0 animation_frame = animations[state["current_animation"]] animation_frame() # possibly that the memory gets filled with above animation calling. make sure to clear some memory # otherwise the player will crash after a minute or so... This will impact the framerate a bit, but that's # better than a hard crash :) del animation_frame if state["frame"] % 100 == 0: show_memory_usage() gc.collect() draw_buttons() display.flush() # the badge hangs when animations are used, probably because of memory issues # it's probably leaking a lot def show_memory_usage(): # https://forum.micropython.org/viewtopic.php?t=3499 import gc import os def df(): s = os.statvfs('//') return '{0} MB'.format((s[0] * s[3]) / 1048576) def free(): F = gc.mem_free() A = gc.mem_alloc() T = F + A P = '{0:.2f}%'.format(F / T * 100) return 'Total:{0} Free:{1} ({2})'.format(T, F, P) print(df()) print(free())