diff --git a/python_payload/main.py b/python_payload/main.py index ff80588fea09617c2f9025c0c20ea39a74ed1ba3..872d8aadf1fd8c872623c92711bb45607522d20d 100644 --- a/python_payload/main.py +++ b/python_payload/main.py @@ -1,24 +1,32 @@ -import gc, time +import time, gc + +ts_start = time.time() + from st3m import logging log = logging.Log(__name__, level=logging.INFO) log.info(f"starting main") log.info(f"free memory: {gc.mem_free()}") -from st3m import control, application, ui, menu - -log = logging.Log(__name__, level=logging.INFO) +import st4m -ts_start = time.time() +from st4m.goose import Optional, List, ABCBase, abstractmethod +from st4m.ui.view import View, ViewManager, ViewTransitionBlend +from st4m.ui.menu import ( + MenuItem, + MenuController, + MenuItemBack, + MenuItemForeground, + MenuItemNoop, +) -from st3m import * +from st4m import Responder, InputState, Ctx -from apps import flow3r +# from apps import flow3r log.info("import apps done") log.info(f"free memory: {gc.mem_free()}") ts_end = time.time() - log.info(f"boot took {ts_end-ts_start} seconds") # TODO persistent settings @@ -28,7 +36,284 @@ log.info("calibrating captouch, reset volume") captouch.calibration_request() audio.set_volume_dB(0) -# Start default app -default_app = flow3r.app -log.info(f"running default app '{default_app.title}'") -default_app.run() +import math, random +from st3m.ui import xy_from_polar + +WIDTH = 240 +HEIGHT = 240 + +# Define a few RGB (0.0 to 1.0) colors +BLACK = (0, 0, 0) +RED = (1, 0, 0) +GREEN = (0, 1, 0) +BLUE = (0, 0, 1) +WHITE = (1, 1, 1) +GREY = (0.5, 0.5, 0.5) +GO_GREEN = (63 / 255, 255 / 255, 33 / 53) +PUSH_RED = (251 / 255, 72 / 255, 196 / 255) + +tau = 2 * math.pi + +vm = ViewManager(ViewTransitionBlend()) + + +def lerp(a: float, b: float, v: float) -> float: + if v <= 0: + return a + if v >= 1.0: + return b + return a + (b - a) * v + + +class GroupRing(Responder): + def __init__(self, r=100, x=0, y=0): + self.r = r + self.x = x + self.y = y + self.items_ring = [] + self.item_center = None + self.angle_offset = 0 + self.ts = 0.0 + + def think(self, ins: InputState, delta_ms: int) -> None: + self.ts += delta_ms + self.item_center.think(ins, delta_ms) + for item in self.items_ring: + item.think(ins, delta_ms) + + def draw(self, ctx: Ctx) -> None: + if self.item_center: + self.item_center.has_highlight = False + self.item_center.draw(ctx) + + for index, item in enumerate(self.items_ring): + if item is None: + continue + angle = tau / len(self.items_ring) * index + self.angle_offset + (x, y) = xy_from_polar(self.r, angle) + ctx.save() + ctx.translate(self.x + x, self.y + y) + item.draw(ctx) + ctx.restore() + + +class FlowerIcon(Responder): + """ + A flower icon + """ + + def __init__(self, label="?") -> None: + self.x = 0.0 + self.y = 0.0 + self.size = 50.0 + self.ts = 1.0 + self.bg = (random.random(), random.random(), random.random()) + self.label = label + + self.highlighted = False + self.rotation_time = 0.0 + + self.petal_count = random.randint(2, 3) + self.petal_color = (random.random(), random.random(), random.random()) + self.phi_offset = random.random() + self.size_offset = random.randint(0, 20) + + def think(self, ins: InputState, delta_ms: int) -> None: + self.ts += delta_ms + pass + + def draw(self, ctx: Ctx) -> None: + x = self.x + y = self.y + petal_size = 0 + if self.petal_count: + petal_size = 2.3 * self.size / self.petal_count + self.size_offset + + hs = 5 + # print(self.ts) + ctx.save() + ctx.move_to(x, y) + ctx.text_align = ctx.CENTER + ctx.text_baseline = ctx.MIDDLE + ctx.font_size = self.size / 3 + ctx.line_width = 10 + if self.rotation_time: + phi_rotate = tau * ((self.ts % self.rotation_time) / self.rotation_time) + else: + phi_rotate = 0 + for i in range(self.petal_count): + ctx.save() + + phi = (tau / self.petal_count * i + self.phi_offset + phi_rotate) % tau + r = self.size / 2 + (x_, y_) = xy_from_polar(r, phi) + + size_offset = abs(math.pi - (phi + math.pi) % tau) * 5 + ctx.move_to(x + x_ + petal_size / 2 + size_offset + 5, y + y_) + if self.highlighted: + # ctx.move_to(x + x_ - petal_size / 2 - size_offset - 5, y + y_) + ctx.arc(x + x_, y + y_, petal_size / 2 + size_offset + 1, 0, tau, 0) + ctx.rgb(*GO_GREEN).stroke() + + ctx.arc(x + x_, y + y_, petal_size / 2 + size_offset, 0, tau, 0) + ctx.rgb(*self.petal_color).fill() + ctx.restore() + if self.highlighted: + ctx.arc(x, y, self.size / 2 + 5, 0, tau, 1) + ctx.rgb(*GO_GREEN).stroke() + + ctx.arc(x, y, self.size / 2, 0, tau, 0) + ctx.rgb(*self.bg).fill() + + # label + # y += self.size / 3 + w = max(self.size, ctx.text_width(self.label) + 10) + h = self.size / 3 + 8 + if False and self.highlighted: + ctx.rgb(*BLACK).move_to(x, y - height / 2).round_rectangle( + x - width / 2, y - height / 2, width, height, width // 2 + ).fill() + ctx.rgb(*GO_GREEN).move_to(x, y).text(self.label) + else: + ctx.save() + ctx.translate(0, self.size / 3) + ctx.rgb(*PUSH_RED).round_rectangle( + x - w / 2, y - h / 2, w, h, w // 2 + ).fill() + + ctx.rgb(*BLACK).move_to(x, y).text(self.label) + ctx.restore() + + ctx.restore() + + +class FlowerMenu(MenuController): + """ + A circular menu with flowers. + """ + + __slots__ = ( + "_ts", + "_sun", + ) + + def __init__(self, items: List[MenuItem], vm: ViewManager, name="flow3r") -> None: + self._ts = 0 + self.name = name + self.ui = GroupRing(r=80) + for item in items: + self.ui.items_ring.append(FlowerIcon(label=item.label())) + super().__init__(items, vm) + self._scroll_controller.wrap = True + + self.icon = FlowerIcon(label=self.name) + self.icon.rotation_time = -5000 + self.ui.item_center = self.icon + + self.angle = 0 + self.angle_step = 0.2 + + def think(self, ins: InputState, delta_ms: int) -> None: + super().think(ins, delta_ms) + self.ui.think(ins, delta_ms) + self._ts += delta_ms + + def draw(self, ctx: Ctx) -> None: + ctx.gray(0) + ctx.rectangle(-120, -120, 240, 240).fill() + for item in self.ui.items_ring: + item.highlighted = False + item.rotation_time = 0 + current = self._scroll_controller.current_position() + self.ui.items_ring[int(current)].highlighted = True + self.ui.items_ring[int(current)].rotation_time = 3000 + self.ui.angle_offset = math.pi - (tau * current / len(self.ui.items_ring)) + + self.ui.draw(ctx) + # print("here") + # ctx.font_size = 40 + # ctx.text_align = ctx.CENTER + # ctx.text_baseline = ctx.MIDDLE + + # angle_per_item = 0.4 + + # current = self._scroll_controller.current_position() + + # for ix, item in enumerate(self._items): + # rot = (ix - current) * angle_per_item + # self._draw_text_angled(ctx, item.label(), rot, 1 - abs(rot)) + + def _draw_text_angled( + self, ctx: Ctx, text: str, angle: float, activity: float + ) -> None: + size = lerp(20, 40, activity) + color = lerp(0, 1, activity) + if color < 0.01: + return + + ctx.save() + ctx.translate(-120, 0).rotate(angle).translate(140, 0) + ctx.font_size = size + ctx.rgba(1.0, 1.0, 1.0, color).move_to(0, 0).text(text) + ctx.restore() + + +class SimpleMenu(MenuController): + """ + A simple line-by-line menu. + """ + + def draw(self, ctx: Ctx) -> None: + ctx.gray(0) + ctx.rectangle(-120, -120, 240, 240).fill() + + ctx.text_align = ctx.CENTER + ctx.text_baseline = ctx.MIDDLE + + current = self._scroll_controller.current_position() + + ctx.gray(1) + for ix, item in enumerate(self._items): + offs = (ix - current) * 30 + size = lerp(30, 20, abs(offs / 20)) + ctx.font_size = size + ctx.move_to(0, offs).text(item.label()) + + +menu_music = SimpleMenu( + [ + MenuItemBack(), + MenuItemNoop("Harmonic"), + MenuItemNoop("Melodic"), + MenuItemNoop("TinySynth"), + MenuItemNoop("CrazySynth"), + MenuItemNoop("Sequencer"), + ], + vm, +) + +menu_apps = SimpleMenu( + [ + MenuItemBack(), + MenuItemNoop("captouch"), + MenuItemNoop("worms"), + ], + vm, +) + + +menu_main = FlowerMenu( + [ + MenuItemForeground("MUsic", menu_music), + MenuItemForeground("Apps", menu_apps), + MenuItemNoop("Settings"), + ], + vm, +) + + +vm.push(menu_main) + +reactor = st4m.Reactor() +reactor.set_top(vm) +reactor.run() diff --git a/python_payload/st4m/ui/interactions.py b/python_payload/st4m/ui/interactions.py index f8eb2f17a863eb437813ab224a25afb25cc47aab..1d8a54e9dae97e171731566ff6f3e47985667ded 100644 --- a/python_payload/st4m/ui/interactions.py +++ b/python_payload/st4m/ui/interactions.py @@ -31,11 +31,12 @@ class ScrollController(st4m.Responder): "_velocity", ) - def __init__(self) -> None: + def __init__(self, wrap=False) -> None: self._nitems = 0 self._target_position = 0 self._current_position = 0.0 self._velocity: float = 0.0 + self.wrap = wrap def set_item_count(self, count: int) -> None: """ @@ -72,10 +73,13 @@ class ScrollController(st4m.Responder): self._velocity = 0 return - if self._target_position < 0: - self._target_position = 0 - if self._target_position >= self._nitems: - self._target_position = self._nitems - 1 + if self.wrap: + self._target_position = self._target_position % self._nitems + else: + if self._target_position < 0: + self._target_position = 0 + if self._target_position >= self._nitems: + self._target_position = self._nitems - 1 self._physics_step(delta_ms / 1000.0) @@ -118,11 +122,19 @@ class ScrollController(st4m.Responder): return self._target_position >= self._nitems - 1 def _physics_step(self, delta: float) -> None: - diff = float(self._target_position) - self._current_position + diff = self._nitems + for i in [0, 1, -1]: + d = ( + float(self._target_position) + self._nitems * i + ) - self._current_position + if abs(d) < abs(diff): + diff = d + # diff = + # print(diff) max_velocity = 500 velocity = self._velocity - if abs(diff) > 0.1: + if abs(diff) > 0.2: # Apply force to reach target position. if diff > 0: velocity += 80 * delta @@ -139,7 +151,7 @@ class ScrollController(st4m.Responder): # Try to snap to target position. pos = self._velocity > 0 and diff > 0 neg = self._velocity < 0 and diff < 0 - if pos or neg: + if self.wrap or pos or neg: self._current_position = self._target_position self._velocity = 0 @@ -147,4 +159,6 @@ class ScrollController(st4m.Responder): def _physics_integrate(self, delta: float) -> None: self._velocity -= self._velocity * delta * 10 - self._current_position += self._velocity * delta + self._current_position = ( + self._current_position + self._velocity * delta + ) % self._nitems