diff --git a/python_payload/apps/demo_worms4.py b/python_payload/apps/demo_worms4.py new file mode 100644 index 0000000000000000000000000000000000000000..dfba5f02059185145b6597c19d0c8dc4aaf491d7 --- /dev/null +++ b/python_payload/apps/demo_worms4.py @@ -0,0 +1,139 @@ +# python imports +import random +import time +import math + +# flow3r imports +from st3m import event, ui +from st4m import application +from st4m import Ctx, InputState + + +tau = 2 * math.pi + + +# Subclass Application +class AppWorms(application.Application): + def __init__(self, name): + super().__init__(name) + + # HACK: we work against double buffering by keeping note of how many + # times on_draw got called. + # + # We know there's two buffers, so if we render the same state twice in a + # row we'll be effectively able to keep a persistent framebuffer, like + # with the old API. + # + # When bufn is in [0, 1] we render the background image. + # When bufn is in [2, ...) we render the worms. + # When bufn is > 3, we enable updating the worm state. + # + # TODO(q3k): allow apps to request single-buffered graphics for + # persistent framebuffers. + self.bufn = 0 + + self.worms = [] + for i in range(0): + self.worms.append(Worm()) + + self.just_shown = True + + def on_enter(self): + # print("on foreground") + self.just_shown = True + + def draw(self, ctx): + if self.bufn == 0 or self.bufn == 1: + ctx.rgb(*ui.BLUE).rectangle( + -ui.WIDTH / 2, -ui.HEIGHT / 2, ui.WIDTH, ui.HEIGHT + ).fill() + ctx.text_align = ctx.CENTER + ctx.text_baseline = ctx.MIDDLE + ctx.move_to(0, 0).rgb(*ui.WHITE).text("touch me :)") + self.bufn += 1 + + return + + for w in self.worms: + w.draw(ctx) + self.bufn += 1 + + def think(self, ins: InputState, delta_ms: int) -> None: + super().think(ins, delta_ms) + + # Simulation is currently locked to FPS. + if self.bufn > 3: + for w in self.worms: + w.move() + self.bufn = 2 + for index, petal in enumerate(self.input.captouch.petals): + if petal.pressed or petal.repeated: + self.worms.append(Worm(tau * index / 10 + math.pi)) + + def handle_input(self, data): + worms = self.worms + worms.append(Worm(data.get("index", 0) * 2 * math.pi / 10 + math.pi)) + if len(worms) > 10: + worms.pop(0) + + +class Worm: + def __init__(self, direction=None): + self.color = ui.randrgb() + + if direction: + self.direction = direction + else: + self.direction = random.random() * math.pi * 2 + + self.size = 50 + self.speed = self.size / 5 + (x, y) = ui.xy_from_polar(100, self.direction) + self.x = x + self.y = y + # (self.dx,self.dy) = xy_from_polar(1,self.direction) + self._lastdist = 0.0 + + def draw(self, ctx): + ctx.rgb(*self.color) + ctx.round_rectangle( + self.x - self.size / 2, + self.y - self.size / 2, + self.size, + self.size, + self.size // 2, + ).fill() + + def mutate(self): + self.color = [ + max(0, min(1, x + ((random.random() - 0.5) * 0.3))) for x in self.color + ] + + def move(self): + dist = math.sqrt(self.x**2 + self.y**2) + target_size = (130 - dist) / 3 + + if self.size > target_size: + self.size -= 1 + + if self.size < target_size: + self.size += 1 + + self.speed = self.size / 5 + + self.direction += (random.random() - 0.5) * math.pi / 4 + + (dx, dy) = ui.xy_from_polar(self.speed, self.direction) + self.x += dx + self.y += dy + + if dist > 120 - self.size / 2 and dist > self._lastdist: + polar_position = math.atan2(self.y, self.x) + dx = dx * -abs(math.cos(polar_position)) + dy = dy * -abs(math.sin(polar_position)) + self.direction = -math.atan2(dy, dx) + self.mutate() + self._lastdist = dist + + +app = AppWorms("worms") diff --git a/python_payload/main.py b/python_payload/main.py index 16867279cb732621f936554a8260e47ead0a1c3d..fbbd8df4c814ce08dbecc74f00ce1fe17ae8da5c 100644 --- a/python_payload/main.py +++ b/python_payload/main.py @@ -46,11 +46,14 @@ menu_music = SimpleMenu( vm, ) +from apps.demo_worms4 import app as worms + +worms._view_manager = vm menu_apps = SimpleMenu( [ MenuItemBack(), MenuItemNoop("captouch"), - MenuItemNoop("worms"), + MenuItemForeground("worms", worms), ], vm, ) @@ -58,7 +61,7 @@ menu_apps = SimpleMenu( menu_main = FlowerMenu( [ - MenuItemForeground("MUsic", menu_music), + MenuItemForeground("Music", menu_music), MenuItemForeground("Apps", menu_apps), MenuItemNoop("Settings"), ], diff --git a/python_payload/st4m/application.py b/python_payload/st4m/application.py new file mode 100644 index 0000000000000000000000000000000000000000..dcb44fcadfb13036113c545a558718e5684c972b --- /dev/null +++ b/python_payload/st4m/application.py @@ -0,0 +1,15 @@ +from st4m.ui.view import ViewWithInputState, ViewTransitionSwipeRight + + +class Application(ViewWithInputState): + def __init__(self, name: str = __name__): + self._name = name + self._view_manager = None + super().__init__() + + def think(self, ins: InputState, delta_ms: int) -> None: + super().think(ins, delta_ms) + + if self.input.left_shoulder.middle.pressed: + if self._view_manager is not None: + self._view_manager.pop(ViewTransitionSwipeRight()) diff --git a/python_payload/st4m/input.py b/python_payload/st4m/input.py index 576bb906cbd084f911d5e46c1361169c05b51262..cdeacc05ba85e4e9efed3729426449ab96af8ecf 100644 --- a/python_payload/st4m/input.py +++ b/python_payload/st4m/input.py @@ -8,24 +8,24 @@ class InputState: """ Current state of inputs from badge user. Passed via think() to every Responder. - + If you want to detect edges, use the stateful InputController. """ - def __init__(self, petal_pressed: List[bool], left_button: int, right_button: int) -> None: + + def __init__( + self, petal_pressed: List[bool], left_button: int, right_button: int + ) -> None: self.petal_pressed = petal_pressed self.left_button = left_button self.right_button = right_button - + @classmethod - def gather(cls) -> 'InputState': + def gather(cls) -> "InputState": """ Build InputState from current hardware state. Should only be used by the Reactor. """ - petal_pressed = [ - hardware.get_captouch(i) - for i in range(10) - ] + petal_pressed = [hardware.get_captouch(i) for i in range(10)] left_button = hardware.left_button_get() right_button = hardware.right_button_get() @@ -48,11 +48,11 @@ class Pressable: button repeating. """ - PRESSED = 'pressed' - REPEATED = 'repeated' - RELEASED = 'released' - DOWN = 'down' - UP = 'up' + PRESSED = "pressed" + REPEATED = "repeated" + RELEASED = "released" + DOWN = "down" + UP = "up" def __init__(self, state: bool) -> None: self._state = state @@ -93,7 +93,6 @@ class Pressable: self._prev_state = False self._pressed_at = ts self._repeated = True - @property def state(self) -> str: @@ -162,10 +161,10 @@ class Pressable: self._ignoring = 2 self._repeating = False self._repeated = False - + def __repr__(self) -> str: - return '<Pressable: ' + self.state + '>' - + return "<Pressable: " + self.state + ">" + class PetalState(Pressable): def __init__(self, ix: int) -> None: @@ -180,18 +179,16 @@ class CaptouchState: The petals are indexed from 0 to 9 (inclusive). Petal 0 is above the USB-C socket, then the numbering continues counter-clockwise. """ - __slots__ = ('petals') + + __slots__ = "petals" def __init__(self) -> None: - self.petals = [ - PetalState(i) - for i in range(10) - ] - + self.petals = [PetalState(i) for i in range(10)] + def _update(self, ts: int, hr: InputState) -> None: for i, petal in enumerate(self.petals): petal._update(ts, hr.petal_pressed[i]) - + def _ignore_pressed(self) -> None: for petal in self.petals: petal._ignore_pressed() @@ -201,15 +198,17 @@ class TriSwitchHandedness(Enum): """ Left or right shoulder button. """ - left = 'left' - right = 'right' + + left = "left" + right = "right" class TriSwitchState: """ State of a tri-stat shoulder button """ - __slots__ = ('left', 'middle', 'right', 'handedness') + + __slots__ = ("left", "middle", "right", "handedness") def __init__(self, h: TriSwitchHandedness) -> None: self.handedness = h @@ -219,7 +218,11 @@ class TriSwitchState: self.right = Pressable(False) def _update(self, ts: int, hr: InputState) -> None: - st = hr.left_button if self.handedness == TriSwitchHandedness.left else hr.right_button + st = ( + hr.left_button + if self.handedness == TriSwitchHandedness.left + else hr.right_button + ) self.left._update(ts, st == -1) self.middle._update(ts, st == 2) self.right._update(ts, st == 1) @@ -228,7 +231,7 @@ class TriSwitchState: self.left._ignore_pressed() self.middle._ignore_pressed() self.right._ignore_pressed() - + class InputController: """ @@ -241,12 +244,14 @@ class InputController: Then, access the captouch/left_shoulder/right_shoulder fields. """ + __slots__ = ( - 'captouch', - 'left_shoulder', - 'right_shoulder', - '_ts', + "captouch", + "left_shoulder", + "right_shoulder", + "_ts", ) + def __init__(self) -> None: self.captouch = CaptouchState() self.left_shoulder = TriSwitchState(TriSwitchHandedness.left) @@ -268,4 +273,3 @@ class InputController: self.captouch._ignore_pressed() self.left_shoulder._ignore_pressed() self.right_shoulder._ignore_pressed() - diff --git a/python_payload/st4m/ui/elements/visuals.py b/python_payload/st4m/ui/elements/visuals.py index 244ae3036fdef667b65cd4cd0e5e04a2ec847b6d..43236a4411a94076ba4b53100bdcb2f009f13cef 100644 --- a/python_payload/st4m/ui/elements/visuals.py +++ b/python_payload/st4m/ui/elements/visuals.py @@ -84,7 +84,6 @@ class GroupRing(Responder): ctx.rotate(-angle).translate(0, self.r).rotate(math.pi) item.draw(ctx) ctx.restore() - # ctx.restore() class FlowerIcon(Responder): @@ -103,7 +102,7 @@ class FlowerIcon(Responder): self.highlighted = False self.rotation_time = 0.0 - self.petal_count = random.randint(2, 3) + self.petal_count = random.randint(3, 5) self.petal_color = (random.random(), random.random(), random.random()) self.phi_offset = random.random() self.size_offset = random.randint(0, 20) @@ -127,11 +126,13 @@ class FlowerIcon(Responder): ctx.text_baseline = ctx.MIDDLE ctx.font_size = self.size / 3 ctx.line_width = 5 + ctx.font = ctx.get_font_name(6) 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): + # continue # ctx.save() phi = (tau / self.petal_count * i + self.phi_offset + phi_rotate) % tau @@ -158,21 +159,20 @@ class FlowerIcon(Responder): # 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) + # ctx.save() + ctx.translate(0, self.size / 3) + + if self.highlighted: + bg = BLACK + fg = GO_GREEN 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() + bg = PUSH_RED + fg = BLACK - ctx.rgb(*BLACK).move_to(x, y).text(self.label) - ctx.restore() + ctx.rgb(*bg).round_rectangle(x - w / 2, y - h / 2, w, h, w // 2).fill() + ctx.rgb(*fg).move_to(x, y).text(self.label) ctx.restore() diff --git a/python_payload/st4m/ui/view.py b/python_payload/st4m/ui/view.py index a66d9194f357d2854ae3333791ac216e2477dbfe..da1948874465ab3cded8816ebe7f9582b0cd50d6 100644 --- a/python_payload/st4m/ui/view.py +++ b/python_payload/st4m/ui/view.py @@ -30,9 +30,8 @@ class ViewWithInputState(View): Remember to call super().think() in think()! """ - __slots__ = ( - 'input', - ) + + __slots__ = ("input",) def __init__(self) -> None: self.input = InputController() @@ -50,8 +49,11 @@ class ViewTransition(ABCBase): Can be implemented by the user to provide transition animations. """ + @abstractmethod - def draw(self, ctx: Ctx, transition: float, incoming: Responder, outgoing: Responder) -> None: + def draw( + self, ctx: Ctx, transition: float, incoming: Responder, outgoing: Responder + ) -> None: """ Called when the ViewManager performs a transition from the outgoing responder to the incoming responder. The implementer should draw both @@ -67,7 +69,10 @@ class ViewTransitionBlend(ViewTransition): """ Transition from one view to another by opacity blending. """ - def draw(self, ctx: Ctx, transition: float, incoming: Responder, outgoing: Responder) -> None: + + def draw( + self, ctx: Ctx, transition: float, incoming: Responder, outgoing: Responder + ) -> None: ctx.start_group() outgoing.draw(ctx) ctx.end_group() @@ -82,7 +87,10 @@ class ViewTransitionSwipeLeft(ViewTransition): """ Swipe the outoing view to the left and replace it with the incoming view. """ - def draw(self, ctx: Ctx, transition: float, incoming: Responder, outgoing: Responder) -> None: + + def draw( + self, ctx: Ctx, transition: float, incoming: Responder, outgoing: Responder + ) -> None: ctx.save() ctx.translate(transition * -240, 0) outgoing.draw(ctx) @@ -98,7 +106,10 @@ class ViewTransitionSwipeRight(ViewTransition): """ Swipe the outoing view to the right and replace it with the incoming view. """ - def draw(self, ctx: Ctx, transition: float, incoming: Responder, outgoing: Responder) -> None: + + def draw( + self, ctx: Ctx, transition: float, incoming: Responder, outgoing: Responder + ) -> None: ctx.save() ctx.translate(transition * 240, 0) outgoing.draw(ctx) @@ -117,6 +128,7 @@ class ViewManager(Responder): It manages a history of Views, to which new Views can be pushed and then popped. """ + def __init__(self, vt: ViewTransition) -> None: """ Create a new ViewManager with a default ViewTransition. @@ -124,7 +136,7 @@ class ViewManager(Responder): self._incoming: Optional[View] = None self._outgoing: Optional[View] = None - # Transition time. + # Transition time. self._time_ms = 150 self._default_vt = vt @@ -140,9 +152,9 @@ class ViewManager(Responder): if self._transition >= 1.0: self._transition = 0 self._transitioning = False - + self._outgoing = None - + if self._outgoing is not None: self._outgoing.think(ins, delta_ms) if self._incoming is not None: @@ -195,4 +207,4 @@ class ViewManager(Responder): animation. """ r = self._history.pop() - self.replace(r, override_vt) \ No newline at end of file + self.replace(r, override_vt)