diff --git a/modules/main.py b/modules/main.py index 0f77f04..24ad461 100644 --- a/modules/main.py +++ b/modules/main.py @@ -9,9 +9,12 @@ from system.launcher.app import Launcher from system.power.handler import PowerEventHandler from system.power.app import PowerManager +from system.boopscreen.app import BoopSpinner from frontboards.twentyfour import TwentyTwentyFour +# Start the spinning-tilde boop animation +scheduler.start_app(BoopSpinner(), always_on_top=True) # Start front-board interface scheduler.start_app(TwentyTwentyFour()) diff --git a/modules/system/boopscreen/app.py b/modules/system/boopscreen/app.py new file mode 100644 index 0000000..d09c6ed --- /dev/null +++ b/modules/system/boopscreen/app.py @@ -0,0 +1,78 @@ +from random import random + +from events.input import BUTTON_TYPES, Buttons +from system.eventbus import eventbus +from system.patterndisplay.events import PatternDisable, PatternEnable + +import app + +from .base.background import Background +from .base.conf import conf +from .base.terminate import terminate +from .common.colour_tools import rgb_from_hue +from .common.led_lighter import LEDLighter +from .boopscreen.logo import Logo + + +class BoopSpinner(app.App): + """Spinning Boop Screen.""" + + def __init__(self): + """Construct.""" + eventbus.emit(PatternDisable()) + self.button_states = Buttons(self) + self.hue = random() + self.rotation = conf["rotation"]["start"] + self.fading = False + self.logo = Logo() + self.logo.scale = 0 + self.logo.opacity = 1 + + self.leds = LEDLighter(brightness=conf["leds"]["start"]) + + def update(self, _): + """Update.""" + self.scan_buttons() + colour = rgb_from_hue(self.hue) + + self.logo.grow() + self.logo.colour = colour + + self.rotation += conf["rotation"]["rate"] + self.hue += conf["hue-increment"] + + if self.logo.full_grown(): + self.fading = True + + if self.fading: + self.logo.fade() + + elif self.leds.brightness < conf["leds"]["max"]: + self.leds.brightness += conf["leds"]["increment"] + + self.leds.from_rgb([int(x * 255) for x in colour]) + + if self.logo.faded(): + eventbus.emit(PatternEnable()) + terminate(self) + + def draw(self, ctx): + """Draw.""" + ctx.rotate(self.rotation) + self.overlays = [] + self.overlays.append( + Background(colour=(0, 0, 0), opacity=conf["background-opacity"]) + ) + + self.overlays.append(self.logo) + + self.draw_overlays(ctx) + + def scan_buttons(self): + """Buttons.""" + if self.button_states.get(BUTTON_TYPES["CANCEL"]): + self.button_states.clear() + terminate(self) + + +__app_export__ = BoopSpinner diff --git a/modules/system/boopscreen/base/background.py b/modules/system/boopscreen/base/background.py new file mode 100644 index 0000000..f1a272a --- /dev/null +++ b/modules/system/boopscreen/base/background.py @@ -0,0 +1,14 @@ +class Background: + """Background.""" + + def __init__( + self, + colour=(0, 0, 0), + opacity=1, + ): + """Construct.""" + self.colour = list(colour) + [opacity] + + def draw(self, ctx): + """Draw ourself.""" + ctx.rgba(*self.colour).rectangle(-120, -120, 240, 240).fill() diff --git a/modules/system/boopscreen/base/conf.py b/modules/system/boopscreen/base/conf.py new file mode 100644 index 0000000..a6ac39d --- /dev/null +++ b/modules/system/boopscreen/base/conf.py @@ -0,0 +1,17 @@ +conf = { + "background-opacity": 1, + "bars": {"height": 1.25, "offset": 0.893, "width": 0.223}, + "circle": {"radius": 1.0, "ring-width": 0.179}, + "fade-rate": 0.1, + "growth-rate": 10, + "hue-increment": 0.03, + "leds": {"increment": 0.02, "start": 0.1, "max": 1.0}, + "max-scale": 120, + "rotation": {"rate": 0.3, "start": 2}, + "tilde": { + "bottom-curve": [-0.321, -0.125, -0.446, 0.179], + "mid-line": [-0.089, 0.089], + "start": [-0.625, 0.054], + "top-curve": [-0.446, -0.571, 0.089, -0.089], + }, +} diff --git a/modules/system/boopscreen/base/terminate.py b/modules/system/boopscreen/base/terminate.py new file mode 100644 index 0000000..8e30705 --- /dev/null +++ b/modules/system/boopscreen/base/terminate.py @@ -0,0 +1,7 @@ +from system.eventbus import eventbus +from system.scheduler.events import RequestStopAppEvent + + +def terminate(app): + """Quit the app.""" + eventbus.emit(RequestStopAppEvent(app)) diff --git a/modules/system/boopscreen/boopscreen/logo.py b/modules/system/boopscreen/boopscreen/logo.py new file mode 100644 index 0000000..9011ee7 --- /dev/null +++ b/modules/system/boopscreen/boopscreen/logo.py @@ -0,0 +1,87 @@ +from math import pi + +from ..base.conf import conf + + +class Logo: + """The logo.""" + + def __init__(self, scale=1, colour=None, opacity=1, centre=None): + """Construct.""" + self.scale = scale + self.colour = colour or [1, 1, 1] + self.opacity = opacity + self.centre = centre or (0, 0) + + def draw(self, ctx): + """Draw.""" + self.rgba = self.colour + [self.opacity] + ctx.rgba(*self.rgba) + + ctx.translate(*self.centre) + + for _ in range(2): + self.circle(ctx) + self.bars(ctx) + self.tilde(ctx) + ctx.rotate(pi) + + ctx.fill() + + def circle(self, ctx): + """Draw the circle.""" + ctx.rgba(*self.rgba) + ctx.arc(0, 0, conf["circle"]["radius"] * self.scale, pi, 0, True) + + ctx.arc( + 0, + 0, + (conf["circle"]["radius"] - conf["circle"]["ring-width"]) * self.scale, + 0, + pi, + False, + ) + + ctx.close_path() + + def bars(self, ctx): + """Draw the bars.""" + horizontal_offset = (conf["bars"]["width"] / 2) * self.scale + vertical_offset = conf["bars"]["offset"] * self.scale + height = (conf["bars"]["height"] + conf["bars"]["offset"]) * self.scale + + ctx.move_to(-horizontal_offset, -height) + ctx.line_to(-horizontal_offset, -vertical_offset) + ctx.line_to(horizontal_offset, -vertical_offset) + ctx.line_to(horizontal_offset, -height) + ctx.close_path() + + def tilde(self, ctx): + """Draw the ~.""" + ctx.move_to(*scale_list(conf["tilde"]["start"], self.scale)) + ctx.quad_to(*scale_list(conf["tilde"]["top-curve"], self.scale)) + ctx.line_to(*scale_list(conf["tilde"]["mid-line"], self.scale)) + ctx.quad_to(*scale_list(conf["tilde"]["bottom-curve"], self.scale)) + + ctx.close_path() + + def fade(self, amount=conf["fade-rate"]): + """Fade.""" + self.opacity -= amount + + def faded(self, limit=0): + """Are we faded-out?""" + return self.opacity <= limit + + def grow(self, amount=conf["growth-rate"]): + """Grow.""" + self.scale += amount + + def full_grown(self, limit=conf["max-scale"]): + """Are we big?""" + return self.scale > limit + + +def scale_list(items, scale): + """Scale some numbers.""" + return [x * scale for x in items] diff --git a/modules/system/boopscreen/common/colour_tools.py b/modules/system/boopscreen/common/colour_tools.py new file mode 100644 index 0000000..5c58b21 --- /dev/null +++ b/modules/system/boopscreen/common/colour_tools.py @@ -0,0 +1,44 @@ +from math import floor + + +def _get_segments(): + """Get the segments.""" + pattern = [1, None, 0, 0, None, 1] + offsets = {"red": 0, "blue": 2, "green": 4} + + segments = [] + for i in range(6): + segments.append({"offset": i * 60}) + for component, offset in offsets.items(): + index = (i + offset) % len( + pattern + ) # because `deque` isn't the same in micropython + if pattern[index] is not None: + segments[-1][component] = pattern[index] + + return segments + + +_segments = _get_segments() + + +def _get_sector(degrees): + """Determine which sector we're in.""" + return floor(degrees / 60) + + +def _rgb_from_degrees(degrees): + """Get RGB from degrees of rotation.""" + sector = _get_sector(degrees) + segment = _segments[sector] + offset = (1 / 60) * (degrees - segment["offset"]) + + if sector % 2 == 1: + offset = 1 - offset + + return [segment.get(x, offset) for x in ["red", "green", "blue"]] + + +def rgb_from_hue(decimal): + """Get RGB from hue value (0.0 - 1.0).""" + return _rgb_from_degrees((decimal * 360) % 360) diff --git a/modules/system/boopscreen/common/led_lighter.py b/modules/system/boopscreen/common/led_lighter.py new file mode 100644 index 0000000..b041780 --- /dev/null +++ b/modules/system/boopscreen/common/led_lighter.py @@ -0,0 +1,55 @@ +from tildagonos import tildagonos + +from .colour_tools import rgb_from_hue + + +class LEDLighter: + """Light some LEDs.""" + + def __init__(self, brightness): + """Construct.""" + self.brightness = brightness + + def light(self, hue, secondary_hue=None): + """Light lights.""" + colour = rgb_from_hue(hue) + for i in range(18): + if secondary_hue and i > 11: + colour = rgb_from_hue(secondary_hue) + tildagonos.leds[i + 1] = [ + gamma_corrections[int(i * 255 * self.brightness)] for i in colour + ] + + tildagonos.leds.write() + + def from_rgb(self, rgb, secondary_rgb=None): + """Light lights from an RGB.""" + colour = rgb + for i in range(12): + tildagonos.leds[i + 1] = [ + gamma_corrections[int(i * self.brightness)] for i in colour + ] + + tildagonos.leds.write() + + +# fmt: off +gamma_corrections = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, + 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 5, + 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, + 10, 10, 11, 11, 11, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16, + 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 24, 24, 25, + 25, 26, 27, 27, 28, 29, 29, 30, 31, 32, 32, 33, 34, 35, 35, 36, + 37, 38, 39, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 50, + 51, 52, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 66, 67, 68, + 69, 70, 72, 73, 74, 75, 77, 78, 79, 81, 82, 83, 85, 86, 87, 89, + 90, 92, 93, 95, 96, 98, 99, 101, 102, 104, 105, 107, 109, 110, 112, 114, + 115, 117, 119, 120, 122, 124, 126, 127, 129, 131, 133, 135, 137, 138, 140, 142, + 144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164, 167, 169, 171, 173, 175, + 177, 180, 182, 184, 186, 189, 191, 193, 196, 198, 200, 203, 205, 208, 210, 213, + 215, 218, 220, 223, 225, 228, 231, 233, 236, 239, 241, 244, 247, 249, 252, 255 +] +# fmt: on