Toggle Navigation
Hatchery
Eggs
Mod Music Player
graphics.py
Users
Badges
Login
Register
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())