diff --git a/build.py b/build.py index fac5cf3..17784f8 100644 --- a/build.py +++ b/build.py @@ -12,7 +12,7 @@ LEARN_PROJECT_URLS = [ "https://cdn-learn.adafruit.com/downloads/zip/3194658/Metro/Metro_RP2350_FlappyNyanCat.zip?timestamp={}", "https://cdn-learn.adafruit.com/downloads/zip/3196927/Metro/Metro_RP2350_Match3/match3_game.zip?timestamp={}", "https://cdn-learn.adafruit.com/downloads/zip/3194422/Metro/Metro_RP2350_Breakout.zip?timestamp={}", - # "", + "https://cdn-learn.adafruit.com/downloads/zip/3196755/Metro/Metro_RP2350_Chips_Challenge.zip?timestamp=1746112286", ] def create_font_specific_zip(font_path: Path, src_dir: Path, learn_projects_dir: Path, output_dir: Path): diff --git a/src/boot.py b/src/boot.py index 0e45b51..5757f83 100644 --- a/src/boot.py +++ b/src/boot.py @@ -1,673 +1,54 @@ -# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries -# -# SPDX-License-Identifier: MIT -import gc +import os -import board -import displayio import supervisor -from displayio import OnDiskBitmap, TileGrid, Group -import adafruit_imageload -import time -import math -import adafruit_tlv320 -from audiocore import WaveFile -import audiobusio +from argv_file_helper import argv_filename +import json +import storage -BOX_SIZE = (235, 107) -TARGET_FPS = 70 +supervisor.runtime.autoreload = False -display = supervisor.runtime.display -display.auto_refresh = False +""" +boot.py arguments -i2c = board.I2C() -dac = adafruit_tlv320.TLV320DAC3100(i2c) + 0: storage readonly flag, False means writable to CircuitPython, True means read-only to CircuitPython + 1: next code files +2-N: args to pass to next code file -# set sample rate & bit depth -dac.configure_clocks(sample_rate=11030, bit_depth=16) +""" +try: + arg_file = argv_filename(__file__) + print(f"arg files: {arg_file}") + with open(arg_file, "r") as f: + args = json.load(f) -# use headphones -dac.headphone_output = True -dac.headphone_volume = -15 # dB + print("args file found and loaded") + os.remove(arg_file) + print("args file removed") -wave_file = open("/boot_animation/ada_fruitjam_boot_jingle.wav", "rb") -wave = WaveFile(wave_file) -audio = audiobusio.I2SOut(board.I2S_BCLK, board.I2S_WS, board.I2S_DIN) + readonly = args[0] + next_code_file = None + remaining_args = None + + if len(args) >= 1: + next_code_file = args[1] + if len(args) >= 2: + remaining_args = args[2:] + + if remaining_args is not None: + next_code_argv_filename = argv_filename(next_code_file) + with open(next_code_argv_filename, "w") as f: + f.write(json.dumps(remaining_args)) + print("next code args written") + + print(f"setting storage readonly to: {readonly}") + storage.remount("/", readonly=readonly) + + next_code_file = next_code_file + supervisor.set_next_code_file(next_code_file) + print(f"launching: {next_code_file}") + # os.rename("/saves/.boot_py_argv", "/saves/.not_boot_py_argv") -class OvershootAnimator: - """ - A non-blocking animator that moves an element to a target with overshoot effect. - - Instead of blocking with sleep(), this class provides a tick() method that - should be called repeatedly by an external loop (e.g., game loop, UI event loop). - """ - - def __init__(self, element): - """ - Initialize the animator with an element to animate. - - Parameters: - - element: An object with x and y properties that will be animated - """ - self.element = element - self.pos_animating = False - self.start_time = 0 - self.start_x = 0 - self.start_y = 0 - self.target_x = 0 - self.target_y = 0 - self.overshoot_x = 0 - self.overshoot_y = 0 - self.duration = 0 - self.overshoot_pixels = 0 - self.eased_value = None - - self.cur_sprite_index = None - self.last_sprite_frame_time = -1 - self.sprite_anim_start_time = -1 - self.sprite_anim_from_index = None - self.sprite_anim_to_index = None - self.sprite_anim_delay = None - - def animate_to(self, target_x, target_y, duration=1.0, overshoot_pixels=20, - start_sprite_anim_at=None, sprite_delay=1 / 60, - sprite_from_index=None, sprite_to_index=None, eased_value=None): - - """ - Start a new animation to the specified target. - - Parameters: - - target_x, target_y: The final target coordinates - - duration: Total animation time in seconds - - overshoot_pixels: How many pixels to overshoot beyond the target - (use 0 for no overshoot) - """ - _now = time.monotonic() - - # Record starting position and time - self.start_x = self.element.x - self.start_y = self.element.y - self.start_time = _now - if start_sprite_anim_at is not None: - self.sprite_anim_start_time = _now + start_sprite_anim_at - self.sprite_anim_to_index = sprite_to_index - self.sprite_anim_from_index = sprite_from_index - self.cur_sprite_index = self.sprite_anim_from_index - self.sprite_anim_delay = sprite_delay - - # Store target position and parameters - self.target_x = target_x - self.target_y = target_y - self.duration = duration - self.overshoot_pixels = overshoot_pixels - - # Calculate distance to target - dx = target_x - self.start_x - dy = target_y - self.start_y - - # Calculate the direction vector (normalized) - distance = math.sqrt(dx * dx + dy * dy) - if distance <= 0: - # Already at target - return False - - dir_x = dx / distance - dir_y = dy / distance - - # Calculate overshoot position - self.overshoot_x = target_x + dir_x * overshoot_pixels - self.overshoot_y = target_y + dir_y * overshoot_pixels - - self.eased_value = eased_value - - # Start the animation - self.pos_animating = True - return True - - def sprite_anim_tick(self, cur_time): - if cur_time >= self.last_sprite_frame_time + self.sprite_anim_delay: - self.element[0] = self.cur_sprite_index - self.last_sprite_frame_time = cur_time - self.cur_sprite_index += 1 - - if self.cur_sprite_index > self.sprite_anim_to_index: - self.cur_sprite_index = None - self.sprite_anim_from_index = None - self.sprite_anim_to_index = None - self.sprite_anim_delay = None - self.last_sprite_frame_time = -1 - self.sprite_anim_start_time = -1 - return False - - return True - - def tick(self): - """ - Update the animation based on the current time. - - This method should be called repeatedly until it returns False. - - Returns: - - True if the animation is still in progress - - False if the animation has completed - """ - still_sprite_animating = False - _now = time.monotonic() - if self.cur_sprite_index is not None: - if _now >= self.sprite_anim_start_time: - still_sprite_animating = self.sprite_anim_tick(_now) - # print("sprite_still_animating", still_sprite_animating) - if not still_sprite_animating: - return False - else: - if not self.pos_animating: - # print("returning false cur_sprite_index was None and pos_animating False") - return False - - # Calculate elapsed time and progress - elapsed = _now - self.start_time - progress = elapsed / self.duration - - # Check if animation is complete - if progress >= 1.0: - # Ensure we end exactly at the target - if self.element.x != self.target_x or self.element.y != self.target_y: - self.element.x = self.target_x - self.element.y = self.target_y - - self.pos_animating = False - if still_sprite_animating: - return True - else: - return False - - # Calculate the current position based on progress - if self.overshoot_pixels > 0: - # Two-phase animation with overshoot - if progress < 0.7: # Move smoothly toward overshoot position - # Use a single smooth curve to the overshoot point - eased = progress / 0.7 # Linear acceleration toward overshoot - # Apply slight ease-in to make it accelerate through the target point - eased = eased ** 1.2 - current_x = self.start_x + (self.overshoot_x - self.start_x) * eased - current_y = self.start_y + (self.overshoot_y - self.start_y) * eased - else: # Return from overshoot to target - sub_progress = (progress - 0.7) / 0.3 - # Decelerate toward final target - eased = 1 - (1 - sub_progress) ** 2 # ease-out quad - current_x = self.overshoot_x + (self.target_x - self.overshoot_x) * eased - current_y = self.overshoot_y + (self.target_y - self.overshoot_y) * eased - else: - # Simple ease-out when no overshoot is desired - if self.eased_value is None: - eased = 1 - (1 - progress) ** 4 - else: - eased = progress / self.eased_value - current_x = self.start_x + (self.target_x - self.start_x) * eased - current_y = self.start_y + (self.target_y - self.start_y) * eased - - # Update element position - self.element.x = int(current_x) - self.element.y = int(current_y) - - return True - - def is_animating(self): - """Check if an animation is currently in progress.""" - return self.pos_animating - - def cancel(self): - """Cancel the current animation.""" - self.pos_animating = False - - -apple_sprites, apple_sprites_palette = adafruit_imageload.load("/boot_animation/apple_spritesheet.bmp") -f_sprites, f_sprites_palette = adafruit_imageload.load("/boot_animation/f_spritesheet.bmp") -r_sprites, r_sprites_palette = adafruit_imageload.load("/boot_animation/r_spritesheet.bmp") -u_sprites, u_sprites_palette = adafruit_imageload.load("/boot_animation/u_spritesheet.bmp") -i_sprites, i_sprites_palette = adafruit_imageload.load("/boot_animation/i_spritesheet.bmp") -t_sprites, t_sprites_palette = adafruit_imageload.load("/boot_animation/t_spritesheet.bmp") -j_sprites, j_sprites_palette = adafruit_imageload.load("/boot_animation/j_spritesheet.bmp") -j_sprites_palette.make_transparent(0) -a_sprites, a_sprites_palette = adafruit_imageload.load("/boot_animation/a_spritesheet.bmp") -a_sprites_palette.make_transparent(0) -m_sprites, m_sprites_palette = adafruit_imageload.load("/boot_animation/m_spritesheet.bmp") -m_sprites_palette.make_transparent(0) - -default_sprite_delay = 1 / 35 - -main_group = Group() -main_group.x = display.width // 2 - BOX_SIZE[0] // 2 - 30 -main_group.y = display.height // 2 - BOX_SIZE[1] // 2 - 31 - -sliding_group = Group() -main_group.append(sliding_group) - -letters_x_start = 83 -letters_y_start = display.height - -apple_tilegrid = TileGrid(apple_sprites, pixel_shader=apple_sprites_palette, - tile_width=73, tile_height=107, width=1, height=1) -f_tilegrid = TileGrid(f_sprites, pixel_shader=f_sprites_palette, - tile_width=32, tile_height=39, width=1, height=1) -r_tilegrid = TileGrid(r_sprites, pixel_shader=r_sprites_palette, - tile_width=32, tile_height=39, width=1, height=1) -u_tilegrid = TileGrid(u_sprites, pixel_shader=u_sprites_palette, - tile_width=32, tile_height=39, width=1, height=1) -i_tilegrid = TileGrid(i_sprites, pixel_shader=i_sprites_palette, - tile_width=16, tile_height=39, width=1, height=1) -t_tilegrid = TileGrid(t_sprites, pixel_shader=t_sprites_palette, - tile_width=32, tile_height=39, width=1, height=1) -j_tilegrid = TileGrid(j_sprites, pixel_shader=j_sprites_palette, - tile_width=32, tile_height=39, width=1, height=1) -a_tilegrid = TileGrid(a_sprites, pixel_shader=a_sprites_palette, - tile_width=32, tile_height=39, width=1, height=1) -m_tilegrid = TileGrid(m_sprites, pixel_shader=m_sprites_palette, - tile_width=43, tile_height=39, width=1, height=1) - -coordinator = { - "steps": [ - # Apple fly on - { - "type": "animation_step", - "tilegrid": apple_tilegrid, - "offscreen_loc": (0, -207), - "onscreen_loc": (0, 21), - "move_duration": 0.45, - "overshoot_pixels": 1, - "eased_value": None, - "sprite_anim_range": (0, 11), - "sprite_delay": 1 / 42, - "start_time": 0.0, - "sprite_anim_start": 0.347, - "started": False, - }, - # F fly on - { - "type": "animation_step", - "tilegrid": f_tilegrid, - "offscreen_loc": (letters_x_start, letters_y_start), - "onscreen_loc": (letters_x_start, 67), - "move_duration": 0.45, - "overshoot_pixels": 20, - "eased_value": None, - "sprite_anim_range": (0, 15), - "sprite_delay": default_sprite_delay, - "start_time": 0.45, - "sprite_anim_start": 0.347, - "started": False, - - }, - # R fly on - { - "type": "animation_step", - "tilegrid": r_tilegrid, - "offscreen_loc": (letters_x_start + 32 + 3 - 1, letters_y_start), - "onscreen_loc": (letters_x_start + 32 + 3 - 1, 67), - "move_duration": 0.45, - "overshoot_pixels": 20, - "eased_value": None, - "sprite_anim_range": (0, 15), - "sprite_delay": default_sprite_delay, - "start_time": 0.9, - "sprite_anim_start": 0.347, - "started": False, - }, - # Left slide everything - { - "type": "animation_step", - "tilegrid": sliding_group, - "offscreen_loc": (100, 0), - "onscreen_loc": (30, 0), - "move_duration": 1.75, - "overshoot_pixels": 0, - "eased_value": 1, - "sprite_anim_range": None, - "sprite_delay": None, - "start_time": 0.9, - "sprite_anim_start": None, - "started": False, - }, - # U fly on - { - "type": "animation_step", - "tilegrid": u_tilegrid, - "offscreen_loc": (letters_x_start + (32 + 3) * 2 - 2, letters_y_start), - "onscreen_loc": (letters_x_start + (32 + 3) * 2 - 2, 67), - "move_duration": 0.45, - "overshoot_pixels": 20, - "eased_value": None, - "sprite_anim_range": (0, 15), - "sprite_delay": default_sprite_delay, - "start_time": 1.35, - "sprite_anim_start": 0.347, - "started": False, - }, - # I fly on - { - "type": "animation_step", - "tilegrid": i_tilegrid, - "offscreen_loc": (letters_x_start + (32 + 3) * 3 - 3, letters_y_start), - "onscreen_loc": (letters_x_start + (32 + 3) * 3 - 3, 67), - "move_duration": 0.45, - "overshoot_pixels": 20, - "eased_value": None, - "sprite_anim_range": (0, 15), - "sprite_delay": default_sprite_delay, - "start_time": 1.8, - "sprite_anim_start": 0.347, - "started": False, - }, - # T fly on - { - "type": "animation_step", - "tilegrid": t_tilegrid, - "offscreen_loc": (letters_x_start + (32 + 3) * 3 + 16 + 3 - 4, letters_y_start), - "onscreen_loc": (letters_x_start + (32 + 3) * 3 + 16 + 3 - 4, 67), - "move_duration": 0.45, - "overshoot_pixels": 20, - "eased_value": None, - "sprite_anim_range": (0, 15), - "sprite_delay": default_sprite_delay, - "start_time": 2.25, - "sprite_anim_start": 0.347, - "started": False, - }, - # J fly on - { - "type": "animation_step", - "tilegrid": j_tilegrid, - "offscreen_loc": (letters_x_start, letters_y_start), - "onscreen_loc": (letters_x_start, 50 + 39), - "move_duration": 0.45, - "overshoot_pixels": 4, - "eased_value": None, - "sprite_anim_range": (0, 15), - "sprite_delay": default_sprite_delay, - "start_time": 2.7, - # "sprite_anim_start": 0.347, - "sprite_anim_start": 0.4, - "started": False, - }, - # A fly on - { - "type": "animation_step", - "tilegrid": a_tilegrid, - "offscreen_loc": (letters_x_start + 32 + 3 - 1, letters_y_start), - "onscreen_loc": (letters_x_start + 32 + 3 - 1, 50 + 39), - "move_duration": 0.45, - "overshoot_pixels": 4, - "eased_value": None, - "sprite_anim_range": (0, 15), - "sprite_delay": default_sprite_delay, - "start_time": 3.15, - "sprite_anim_start": 0.4, - "started": False, - }, - # M fly on - { - "type": "animation_step", - "tilegrid": m_tilegrid, - "offscreen_loc": (letters_x_start + 32 + 3 + 32 + 2 - 1, letters_y_start), - "onscreen_loc": (letters_x_start + 32 + 3 + 32 + 2 - 1, 50 + 39), - "move_duration": 0.45, - "overshoot_pixels": 4, - "eased_value": None, - "sprite_anim_range": (0, 15), - "sprite_delay": default_sprite_delay, - "start_time": 3.6, - "sprite_anim_start": 0.4, - "started": False, - } - ] -} - -for step in coordinator["steps"]: - if isinstance(step["tilegrid"], TileGrid): - sliding_group.append(step["tilegrid"]) - step["default_palette"] = step["tilegrid"].pixel_shader - step["tilegrid"].x = step["offscreen_loc"][0] - step["tilegrid"].y = step["offscreen_loc"][1] - step["animator"] = OvershootAnimator(step["tilegrid"]) - -# F bounce up from J impact -coordinator["steps"].insert(8, - { - "type": "animation_step", - "tilegrid": coordinator["steps"][1]["tilegrid"], - "animator": coordinator["steps"][1]["animator"], - "offscreen_loc": (letters_x_start, letters_y_start), - "onscreen_loc": (letters_x_start, 52), - "move_duration": 0.3, - "overshoot_pixels": 22, - "eased_value": None, - "sprite_anim_range": (19, 27), - "sprite_delay": 1 / 22, - "start_time": 3.0, - "sprite_anim_start": 0.15, - "started": False, - }, - ) -# R bounce up from A impact -coordinator["steps"].insert(10, - { - "type": "animation_step", - "tilegrid": coordinator["steps"][2]["tilegrid"], - "animator": coordinator["steps"][2]["animator"], - "offscreen_loc": (letters_x_start + 32 + 3 - 1, letters_y_start), - "onscreen_loc": (letters_x_start + 32 + 3 - 1, 52), - "move_duration": 0.3, - "overshoot_pixels": 22, - "eased_value": None, - "sprite_anim_range": (19, 27), - "sprite_delay": 1 / 22, - "start_time": 3.45, - "sprite_anim_start": 0.15, - "started": False, - }, - ) -# U bounce up from M impact -coordinator["steps"].append( - { - "type": "animation_step", - "tilegrid": coordinator["steps"][4]["tilegrid"], - "animator": coordinator["steps"][4]["animator"], - "offscreen_loc": (letters_x_start + (32 + 3) * 2 - 2, letters_y_start), - "onscreen_loc": (letters_x_start + (32 + 3) * 2 - 2, 52), - "move_duration": 0.3, - "overshoot_pixels": 22, - "eased_value": None, - "sprite_anim_range": (19, 27), - "sprite_delay": 1 / 22, - "start_time": 3.9, - "sprite_anim_start": 0.15, - "started": False, - }, -) -# I bounce up from M impact -coordinator["steps"].append( - { - "type": "animation_step", - "tilegrid": coordinator["steps"][5]["tilegrid"], - "animator": coordinator["steps"][5]["animator"], - "offscreen_loc": (letters_x_start + (32 + 3) * 3 - 3, letters_y_start), - "onscreen_loc": (letters_x_start + (32 + 3) * 3 - 3, 52), - "move_duration": 0.3, - "overshoot_pixels": 22, - "eased_value": None, - "sprite_anim_range": (19, 27), - "sprite_delay": 1 / 22, - "start_time": 4.00, - "sprite_anim_start": 0.15, - "started": False, - }, -) -# T bounce up from M impact -coordinator["steps"].append( - { - "type": "animation_step", - "tilegrid": coordinator["steps"][6]["tilegrid"], - "animator": coordinator["steps"][6]["animator"], - "offscreen_loc": (letters_x_start + (32 + 3) * 3 + 16 + 3 - 4, letters_y_start), - "onscreen_loc": (letters_x_start + (32 + 3) * 3 + 16 + 3 - 4, 52), - "move_duration": 0.3, - "overshoot_pixels": 22, - "eased_value": None, - "sprite_anim_range": (19, 27), - "sprite_delay": 1 / 22, - "start_time": 4.1, - "sprite_anim_start": 0.15, - "started": False, - }, -) -# color red -coordinator["steps"].append( - { - "start_time": 4.75, - "type": "change_palette", - "new_palette": "red_palette", - "color": 0xff0000, - "started": False, - } -) -# color yellow -coordinator["steps"].append( - { - "start_time": 5, - "type": "change_palette", - "new_palette": "yellow_palette", - "color": 0xffff00, - "started": False, - } -) -# color teal -coordinator["steps"].append( - { - "start_time": 5.25, - "type": "change_palette", - "new_palette": "teal_palette", - "color": 0x00ffff, - "started": False, - } -) -# color pink -coordinator["steps"].append( - { - "start_time": 5.5, - "type": "change_palette", - "new_palette": "pink_palette", - "color": 0xff00ff, - "started": False, - } -) -# color blue -coordinator["steps"].append( - { - "start_time": 5.75, - "type": "change_palette", - "new_palette": "blue_palette", - "color": 0x0000ff, - "started": False, - } -) -# color green -coordinator["steps"].append( - { - "start_time": 6.00, - "type": "change_palette", - "new_palette": "green_palette", - "color": 0x00ff00, - "started": False, - } -) -# Apple eyes blink -coordinator["steps"].append( - { - "type": "animation_step", - "tilegrid": coordinator["steps"][0]["tilegrid"], - "animator": coordinator["steps"][0]["animator"], - "offscreen_loc": (0, -207), - "onscreen_loc": (0, 21), - "move_duration": 0.01, - "overshoot_pixels": 0, - "eased_value": None, - "sprite_anim_range": (12, 27), - "sprite_delay": 1 / 32, - "start_time": 6.65, - "sprite_anim_start": 0.0, - "started": False, - } -) -# Apple eyes blink again -coordinator["steps"].append( - { - "type": "animation_step", - "tilegrid": coordinator["steps"][0]["tilegrid"], - "animator": coordinator["steps"][0]["animator"], - "offscreen_loc": (0, -207), - "onscreen_loc": (0, 21), - "move_duration": 0.01, - "overshoot_pixels": 0, - "eased_value": None, - "sprite_anim_range": (12, 18), - "sprite_delay": 1 / 32, - "start_time": 8.75, - "sprite_anim_start": 0.0, - "started": False, - } -) - -display.root_group = main_group - -start_time = time.monotonic() - -audio.play(wave) - -while True: - now = time.monotonic() - still_going = True - - for i in range(len(coordinator["steps"])): - step = coordinator["steps"][i] - if now - start_time >= step["start_time"]: - if not step["started"]: - step["started"] = True - if step["type"] == "animation_step": - if step["sprite_anim_range"] is not None: - step["animator"].animate_to( - *step["onscreen_loc"], - duration=step["move_duration"], overshoot_pixels=step["overshoot_pixels"], - start_sprite_anim_at=step["sprite_anim_start"], - sprite_from_index=step["sprite_anim_range"][0], - sprite_to_index=step["sprite_anim_range"][1], - sprite_delay=step["sprite_delay"], eased_value=step["eased_value"], - ) - else: - step["animator"].animate_to( - *step["onscreen_loc"], - duration=step["move_duration"], overshoot_pixels=step["overshoot_pixels"], - eased_value=step["eased_value"] - ) - elif step["type"] == "change_palette": - # color_sweep_all(step["color"], delay=0) - for _cur_step in coordinator["steps"]: - if "tilegrid" in _cur_step and isinstance(_cur_step["tilegrid"], TileGrid): - _cur_step["tilegrid"].pixel_shader[1] = step["color"] - - if "animator" in step: - if i == len(coordinator["steps"]) - 1: - still_going = step["animator"].tick() - else: - step["animator"].tick() - else: - if i == len(coordinator["steps"]) - 1: - still_going = False - # display.refresh(target_frames_per_second=TARGET_FPS) - display.refresh() - - if not still_going: - break - -while audio.playing: - pass +except OSError: + print("launching boot animation") + supervisor.set_next_code_file("boot_animation.py") \ No newline at end of file diff --git a/src/boot_animation.py b/src/boot_animation.py new file mode 100644 index 0000000..363a195 --- /dev/null +++ b/src/boot_animation.py @@ -0,0 +1,676 @@ +# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries +# +# SPDX-License-Identifier: MIT +import gc + +import board +import displayio +import supervisor +from displayio import OnDiskBitmap, TileGrid, Group +import adafruit_imageload +import time +import math +import adafruit_tlv320 +from audiocore import WaveFile +import audiobusio + +BOX_SIZE = (235, 107) +TARGET_FPS = 70 + +display = supervisor.runtime.display +display.auto_refresh = False + +i2c = board.I2C() +dac = adafruit_tlv320.TLV320DAC3100(i2c) + +# set sample rate & bit depth +dac.configure_clocks(sample_rate=11030, bit_depth=16) + +# use headphones +dac.headphone_output = True +dac.headphone_volume = -15 # dB + +wave_file = open("/boot_animation/ada_fruitjam_boot_jingle.wav", "rb") +wave = WaveFile(wave_file) +audio = audiobusio.I2SOut(board.I2S_BCLK, board.I2S_WS, board.I2S_DIN) + + +class OvershootAnimator: + """ + A non-blocking animator that moves an element to a target with overshoot effect. + + Instead of blocking with sleep(), this class provides a tick() method that + should be called repeatedly by an external loop (e.g., game loop, UI event loop). + """ + + def __init__(self, element): + """ + Initialize the animator with an element to animate. + + Parameters: + - element: An object with x and y properties that will be animated + """ + self.element = element + self.pos_animating = False + self.start_time = 0 + self.start_x = 0 + self.start_y = 0 + self.target_x = 0 + self.target_y = 0 + self.overshoot_x = 0 + self.overshoot_y = 0 + self.duration = 0 + self.overshoot_pixels = 0 + self.eased_value = None + + self.cur_sprite_index = None + self.last_sprite_frame_time = -1 + self.sprite_anim_start_time = -1 + self.sprite_anim_from_index = None + self.sprite_anim_to_index = None + self.sprite_anim_delay = None + + def animate_to(self, target_x, target_y, duration=1.0, overshoot_pixels=20, + start_sprite_anim_at=None, sprite_delay=1 / 60, + sprite_from_index=None, sprite_to_index=None, eased_value=None): + + """ + Start a new animation to the specified target. + + Parameters: + - target_x, target_y: The final target coordinates + - duration: Total animation time in seconds + - overshoot_pixels: How many pixels to overshoot beyond the target + (use 0 for no overshoot) + """ + _now = time.monotonic() + + # Record starting position and time + self.start_x = self.element.x + self.start_y = self.element.y + self.start_time = _now + if start_sprite_anim_at is not None: + self.sprite_anim_start_time = _now + start_sprite_anim_at + self.sprite_anim_to_index = sprite_to_index + self.sprite_anim_from_index = sprite_from_index + self.cur_sprite_index = self.sprite_anim_from_index + self.sprite_anim_delay = sprite_delay + + # Store target position and parameters + self.target_x = target_x + self.target_y = target_y + self.duration = duration + self.overshoot_pixels = overshoot_pixels + + # Calculate distance to target + dx = target_x - self.start_x + dy = target_y - self.start_y + + # Calculate the direction vector (normalized) + distance = math.sqrt(dx * dx + dy * dy) + if distance <= 0: + # Already at target + return False + + dir_x = dx / distance + dir_y = dy / distance + + # Calculate overshoot position + self.overshoot_x = target_x + dir_x * overshoot_pixels + self.overshoot_y = target_y + dir_y * overshoot_pixels + + self.eased_value = eased_value + + # Start the animation + self.pos_animating = True + return True + + def sprite_anim_tick(self, cur_time): + if cur_time >= self.last_sprite_frame_time + self.sprite_anim_delay: + self.element[0] = self.cur_sprite_index + self.last_sprite_frame_time = cur_time + self.cur_sprite_index += 1 + + if self.cur_sprite_index > self.sprite_anim_to_index: + self.cur_sprite_index = None + self.sprite_anim_from_index = None + self.sprite_anim_to_index = None + self.sprite_anim_delay = None + self.last_sprite_frame_time = -1 + self.sprite_anim_start_time = -1 + return False + + return True + + def tick(self): + """ + Update the animation based on the current time. + + This method should be called repeatedly until it returns False. + + Returns: + - True if the animation is still in progress + - False if the animation has completed + """ + still_sprite_animating = False + _now = time.monotonic() + if self.cur_sprite_index is not None: + if _now >= self.sprite_anim_start_time: + still_sprite_animating = self.sprite_anim_tick(_now) + # print("sprite_still_animating", still_sprite_animating) + if not still_sprite_animating: + return False + else: + if not self.pos_animating: + # print("returning false cur_sprite_index was None and pos_animating False") + return False + + # Calculate elapsed time and progress + elapsed = _now - self.start_time + progress = elapsed / self.duration + + # Check if animation is complete + if progress >= 1.0: + # Ensure we end exactly at the target + if self.element.x != self.target_x or self.element.y != self.target_y: + self.element.x = self.target_x + self.element.y = self.target_y + + self.pos_animating = False + if still_sprite_animating: + return True + else: + return False + + # Calculate the current position based on progress + if self.overshoot_pixels > 0: + # Two-phase animation with overshoot + if progress < 0.7: # Move smoothly toward overshoot position + # Use a single smooth curve to the overshoot point + eased = progress / 0.7 # Linear acceleration toward overshoot + # Apply slight ease-in to make it accelerate through the target point + eased = eased ** 1.2 + current_x = self.start_x + (self.overshoot_x - self.start_x) * eased + current_y = self.start_y + (self.overshoot_y - self.start_y) * eased + else: # Return from overshoot to target + sub_progress = (progress - 0.7) / 0.3 + # Decelerate toward final target + eased = 1 - (1 - sub_progress) ** 2 # ease-out quad + current_x = self.overshoot_x + (self.target_x - self.overshoot_x) * eased + current_y = self.overshoot_y + (self.target_y - self.overshoot_y) * eased + else: + # Simple ease-out when no overshoot is desired + if self.eased_value is None: + eased = 1 - (1 - progress) ** 4 + else: + eased = progress / self.eased_value + current_x = self.start_x + (self.target_x - self.start_x) * eased + current_y = self.start_y + (self.target_y - self.start_y) * eased + + # Update element position + self.element.x = int(current_x) + self.element.y = int(current_y) + + return True + + def is_animating(self): + """Check if an animation is currently in progress.""" + return self.pos_animating + + def cancel(self): + """Cancel the current animation.""" + self.pos_animating = False + + +apple_sprites, apple_sprites_palette = adafruit_imageload.load("/boot_animation/apple_spritesheet.bmp") +f_sprites, f_sprites_palette = adafruit_imageload.load("/boot_animation/f_spritesheet.bmp") +r_sprites, r_sprites_palette = adafruit_imageload.load("/boot_animation/r_spritesheet.bmp") +u_sprites, u_sprites_palette = adafruit_imageload.load("/boot_animation/u_spritesheet.bmp") +i_sprites, i_sprites_palette = adafruit_imageload.load("/boot_animation/i_spritesheet.bmp") +t_sprites, t_sprites_palette = adafruit_imageload.load("/boot_animation/t_spritesheet.bmp") +j_sprites, j_sprites_palette = adafruit_imageload.load("/boot_animation/j_spritesheet.bmp") +j_sprites_palette.make_transparent(0) +a_sprites, a_sprites_palette = adafruit_imageload.load("/boot_animation/a_spritesheet.bmp") +a_sprites_palette.make_transparent(0) +m_sprites, m_sprites_palette = adafruit_imageload.load("/boot_animation/m_spritesheet.bmp") +m_sprites_palette.make_transparent(0) + +default_sprite_delay = 1 / 35 + +main_group = Group() +main_group.x = display.width // 2 - BOX_SIZE[0] // 2 - 30 +main_group.y = display.height // 2 - BOX_SIZE[1] // 2 - 31 + +sliding_group = Group() +main_group.append(sliding_group) + +letters_x_start = 83 +letters_y_start = display.height + +apple_tilegrid = TileGrid(apple_sprites, pixel_shader=apple_sprites_palette, + tile_width=73, tile_height=107, width=1, height=1) +f_tilegrid = TileGrid(f_sprites, pixel_shader=f_sprites_palette, + tile_width=32, tile_height=39, width=1, height=1) +r_tilegrid = TileGrid(r_sprites, pixel_shader=r_sprites_palette, + tile_width=32, tile_height=39, width=1, height=1) +u_tilegrid = TileGrid(u_sprites, pixel_shader=u_sprites_palette, + tile_width=32, tile_height=39, width=1, height=1) +i_tilegrid = TileGrid(i_sprites, pixel_shader=i_sprites_palette, + tile_width=16, tile_height=39, width=1, height=1) +t_tilegrid = TileGrid(t_sprites, pixel_shader=t_sprites_palette, + tile_width=32, tile_height=39, width=1, height=1) +j_tilegrid = TileGrid(j_sprites, pixel_shader=j_sprites_palette, + tile_width=32, tile_height=39, width=1, height=1) +a_tilegrid = TileGrid(a_sprites, pixel_shader=a_sprites_palette, + tile_width=32, tile_height=39, width=1, height=1) +m_tilegrid = TileGrid(m_sprites, pixel_shader=m_sprites_palette, + tile_width=43, tile_height=39, width=1, height=1) + +coordinator = { + "steps": [ + # Apple fly on + { + "type": "animation_step", + "tilegrid": apple_tilegrid, + "offscreen_loc": (0, -207), + "onscreen_loc": (0, 21), + "move_duration": 0.45, + "overshoot_pixels": 1, + "eased_value": None, + "sprite_anim_range": (0, 11), + "sprite_delay": 1 / 42, + "start_time": 0.0, + "sprite_anim_start": 0.347, + "started": False, + }, + # F fly on + { + "type": "animation_step", + "tilegrid": f_tilegrid, + "offscreen_loc": (letters_x_start, letters_y_start), + "onscreen_loc": (letters_x_start, 67), + "move_duration": 0.45, + "overshoot_pixels": 20, + "eased_value": None, + "sprite_anim_range": (0, 15), + "sprite_delay": default_sprite_delay, + "start_time": 0.45, + "sprite_anim_start": 0.347, + "started": False, + + }, + # R fly on + { + "type": "animation_step", + "tilegrid": r_tilegrid, + "offscreen_loc": (letters_x_start + 32 + 3 - 1, letters_y_start), + "onscreen_loc": (letters_x_start + 32 + 3 - 1, 67), + "move_duration": 0.45, + "overshoot_pixels": 20, + "eased_value": None, + "sprite_anim_range": (0, 15), + "sprite_delay": default_sprite_delay, + "start_time": 0.9, + "sprite_anim_start": 0.347, + "started": False, + }, + # Left slide everything + { + "type": "animation_step", + "tilegrid": sliding_group, + "offscreen_loc": (100, 0), + "onscreen_loc": (30, 0), + "move_duration": 1.75, + "overshoot_pixels": 0, + "eased_value": 1, + "sprite_anim_range": None, + "sprite_delay": None, + "start_time": 0.9, + "sprite_anim_start": None, + "started": False, + }, + # U fly on + { + "type": "animation_step", + "tilegrid": u_tilegrid, + "offscreen_loc": (letters_x_start + (32 + 3) * 2 - 2, letters_y_start), + "onscreen_loc": (letters_x_start + (32 + 3) * 2 - 2, 67), + "move_duration": 0.45, + "overshoot_pixels": 20, + "eased_value": None, + "sprite_anim_range": (0, 15), + "sprite_delay": default_sprite_delay, + "start_time": 1.35, + "sprite_anim_start": 0.347, + "started": False, + }, + # I fly on + { + "type": "animation_step", + "tilegrid": i_tilegrid, + "offscreen_loc": (letters_x_start + (32 + 3) * 3 - 3, letters_y_start), + "onscreen_loc": (letters_x_start + (32 + 3) * 3 - 3, 67), + "move_duration": 0.45, + "overshoot_pixels": 20, + "eased_value": None, + "sprite_anim_range": (0, 15), + "sprite_delay": default_sprite_delay, + "start_time": 1.8, + "sprite_anim_start": 0.347, + "started": False, + }, + # T fly on + { + "type": "animation_step", + "tilegrid": t_tilegrid, + "offscreen_loc": (letters_x_start + (32 + 3) * 3 + 16 + 3 - 4, letters_y_start), + "onscreen_loc": (letters_x_start + (32 + 3) * 3 + 16 + 3 - 4, 67), + "move_duration": 0.45, + "overshoot_pixels": 20, + "eased_value": None, + "sprite_anim_range": (0, 15), + "sprite_delay": default_sprite_delay, + "start_time": 2.25, + "sprite_anim_start": 0.347, + "started": False, + }, + # J fly on + { + "type": "animation_step", + "tilegrid": j_tilegrid, + "offscreen_loc": (letters_x_start, letters_y_start), + "onscreen_loc": (letters_x_start, 50 + 39), + "move_duration": 0.45, + "overshoot_pixels": 4, + "eased_value": None, + "sprite_anim_range": (0, 15), + "sprite_delay": default_sprite_delay, + "start_time": 2.7, + # "sprite_anim_start": 0.347, + "sprite_anim_start": 0.4, + "started": False, + }, + # A fly on + { + "type": "animation_step", + "tilegrid": a_tilegrid, + "offscreen_loc": (letters_x_start + 32 + 3 - 1, letters_y_start), + "onscreen_loc": (letters_x_start + 32 + 3 - 1, 50 + 39), + "move_duration": 0.45, + "overshoot_pixels": 4, + "eased_value": None, + "sprite_anim_range": (0, 15), + "sprite_delay": default_sprite_delay, + "start_time": 3.15, + "sprite_anim_start": 0.4, + "started": False, + }, + # M fly on + { + "type": "animation_step", + "tilegrid": m_tilegrid, + "offscreen_loc": (letters_x_start + 32 + 3 + 32 + 2 - 1, letters_y_start), + "onscreen_loc": (letters_x_start + 32 + 3 + 32 + 2 - 1, 50 + 39), + "move_duration": 0.45, + "overshoot_pixels": 4, + "eased_value": None, + "sprite_anim_range": (0, 15), + "sprite_delay": default_sprite_delay, + "start_time": 3.6, + "sprite_anim_start": 0.4, + "started": False, + } + ] +} + +for step in coordinator["steps"]: + if isinstance(step["tilegrid"], TileGrid): + sliding_group.append(step["tilegrid"]) + step["default_palette"] = step["tilegrid"].pixel_shader + step["tilegrid"].x = step["offscreen_loc"][0] + step["tilegrid"].y = step["offscreen_loc"][1] + step["animator"] = OvershootAnimator(step["tilegrid"]) + +# F bounce up from J impact +coordinator["steps"].insert(8, + { + "type": "animation_step", + "tilegrid": coordinator["steps"][1]["tilegrid"], + "animator": coordinator["steps"][1]["animator"], + "offscreen_loc": (letters_x_start, letters_y_start), + "onscreen_loc": (letters_x_start, 52), + "move_duration": 0.3, + "overshoot_pixels": 22, + "eased_value": None, + "sprite_anim_range": (19, 27), + "sprite_delay": 1 / 22, + "start_time": 3.0, + "sprite_anim_start": 0.15, + "started": False, + }, + ) +# R bounce up from A impact +coordinator["steps"].insert(10, + { + "type": "animation_step", + "tilegrid": coordinator["steps"][2]["tilegrid"], + "animator": coordinator["steps"][2]["animator"], + "offscreen_loc": (letters_x_start + 32 + 3 - 1, letters_y_start), + "onscreen_loc": (letters_x_start + 32 + 3 - 1, 52), + "move_duration": 0.3, + "overshoot_pixels": 22, + "eased_value": None, + "sprite_anim_range": (19, 27), + "sprite_delay": 1 / 22, + "start_time": 3.45, + "sprite_anim_start": 0.15, + "started": False, + }, + ) +# U bounce up from M impact +coordinator["steps"].append( + { + "type": "animation_step", + "tilegrid": coordinator["steps"][4]["tilegrid"], + "animator": coordinator["steps"][4]["animator"], + "offscreen_loc": (letters_x_start + (32 + 3) * 2 - 2, letters_y_start), + "onscreen_loc": (letters_x_start + (32 + 3) * 2 - 2, 52), + "move_duration": 0.3, + "overshoot_pixels": 22, + "eased_value": None, + "sprite_anim_range": (19, 27), + "sprite_delay": 1 / 22, + "start_time": 3.9, + "sprite_anim_start": 0.15, + "started": False, + }, +) +# I bounce up from M impact +coordinator["steps"].append( + { + "type": "animation_step", + "tilegrid": coordinator["steps"][5]["tilegrid"], + "animator": coordinator["steps"][5]["animator"], + "offscreen_loc": (letters_x_start + (32 + 3) * 3 - 3, letters_y_start), + "onscreen_loc": (letters_x_start + (32 + 3) * 3 - 3, 52), + "move_duration": 0.3, + "overshoot_pixels": 22, + "eased_value": None, + "sprite_anim_range": (19, 27), + "sprite_delay": 1 / 22, + "start_time": 4.00, + "sprite_anim_start": 0.15, + "started": False, + }, +) +# T bounce up from M impact +coordinator["steps"].append( + { + "type": "animation_step", + "tilegrid": coordinator["steps"][6]["tilegrid"], + "animator": coordinator["steps"][6]["animator"], + "offscreen_loc": (letters_x_start + (32 + 3) * 3 + 16 + 3 - 4, letters_y_start), + "onscreen_loc": (letters_x_start + (32 + 3) * 3 + 16 + 3 - 4, 52), + "move_duration": 0.3, + "overshoot_pixels": 22, + "eased_value": None, + "sprite_anim_range": (19, 27), + "sprite_delay": 1 / 22, + "start_time": 4.1, + "sprite_anim_start": 0.15, + "started": False, + }, +) +# color red +coordinator["steps"].append( + { + "start_time": 4.75, + "type": "change_palette", + "new_palette": "red_palette", + "color": 0xff0000, + "started": False, + } +) +# color yellow +coordinator["steps"].append( + { + "start_time": 5, + "type": "change_palette", + "new_palette": "yellow_palette", + "color": 0xffff00, + "started": False, + } +) +# color teal +coordinator["steps"].append( + { + "start_time": 5.25, + "type": "change_palette", + "new_palette": "teal_palette", + "color": 0x00ffff, + "started": False, + } +) +# color pink +coordinator["steps"].append( + { + "start_time": 5.5, + "type": "change_palette", + "new_palette": "pink_palette", + "color": 0xff00ff, + "started": False, + } +) +# color blue +coordinator["steps"].append( + { + "start_time": 5.75, + "type": "change_palette", + "new_palette": "blue_palette", + "color": 0x0000ff, + "started": False, + } +) +# color green +coordinator["steps"].append( + { + "start_time": 6.00, + "type": "change_palette", + "new_palette": "green_palette", + "color": 0x00ff00, + "started": False, + } +) +# Apple eyes blink +coordinator["steps"].append( + { + "type": "animation_step", + "tilegrid": coordinator["steps"][0]["tilegrid"], + "animator": coordinator["steps"][0]["animator"], + "offscreen_loc": (0, -207), + "onscreen_loc": (0, 21), + "move_duration": 0.01, + "overshoot_pixels": 0, + "eased_value": None, + "sprite_anim_range": (12, 27), + "sprite_delay": 1 / 32, + "start_time": 6.65, + "sprite_anim_start": 0.0, + "started": False, + } +) +# Apple eyes blink again +coordinator["steps"].append( + { + "type": "animation_step", + "tilegrid": coordinator["steps"][0]["tilegrid"], + "animator": coordinator["steps"][0]["animator"], + "offscreen_loc": (0, -207), + "onscreen_loc": (0, 21), + "move_duration": 0.01, + "overshoot_pixels": 0, + "eased_value": None, + "sprite_anim_range": (12, 18), + "sprite_delay": 1 / 32, + "start_time": 8.75, + "sprite_anim_start": 0.0, + "started": False, + } +) + +display.root_group = main_group + +start_time = time.monotonic() + +audio.play(wave) + +while True: + now = time.monotonic() + still_going = True + + for i in range(len(coordinator["steps"])): + step = coordinator["steps"][i] + if now - start_time >= step["start_time"]: + if not step["started"]: + step["started"] = True + if step["type"] == "animation_step": + if step["sprite_anim_range"] is not None: + step["animator"].animate_to( + *step["onscreen_loc"], + duration=step["move_duration"], overshoot_pixels=step["overshoot_pixels"], + start_sprite_anim_at=step["sprite_anim_start"], + sprite_from_index=step["sprite_anim_range"][0], + sprite_to_index=step["sprite_anim_range"][1], + sprite_delay=step["sprite_delay"], eased_value=step["eased_value"], + ) + else: + step["animator"].animate_to( + *step["onscreen_loc"], + duration=step["move_duration"], overshoot_pixels=step["overshoot_pixels"], + eased_value=step["eased_value"] + ) + elif step["type"] == "change_palette": + # color_sweep_all(step["color"], delay=0) + for _cur_step in coordinator["steps"]: + if "tilegrid" in _cur_step and isinstance(_cur_step["tilegrid"], TileGrid): + _cur_step["tilegrid"].pixel_shader[1] = step["color"] + + if "animator" in step: + if i == len(coordinator["steps"]) - 1: + still_going = step["animator"].tick() + else: + step["animator"].tick() + else: + if i == len(coordinator["steps"]) - 1: + still_going = False + # display.refresh(target_frames_per_second=TARGET_FPS) + display.refresh() + + if not still_going: + break + +while audio.playing: + pass + +supervisor.set_next_code_file("code.py") +supervisor.reload() \ No newline at end of file diff --git a/src/code.py b/src/code.py index 099d566..d347ddf 100644 --- a/src/code.py +++ b/src/code.py @@ -8,6 +8,8 @@ loaded by adafruit_bitmap_font import array import atexit import json +import math +import os import displayio @@ -23,7 +25,46 @@ from adafruit_anchored_tilegrid import AnchoredTileGrid import adafruit_imageload import adafruit_usb_host_descriptors from adafruit_anchored_group import AnchoredGroup +from adafruit_fruitjam.peripherals import request_display_config +from argv_file_helper import argv_filename +""" +desktop launcher code.py arguments + + 0: next code files +1-N: args to pass to next code file + +""" +try: + arg_file = argv_filename(__file__) + print(f"arg files: {arg_file}") + with open(arg_file, "r") as f: + args = json.load(f) + os.remove(arg_file) + next_code_file = None + remaining_args = None + + if len(args) > 0: + next_code_file = args[0] + if len(args) > 1: + remaining_args = args[1:] + + if remaining_args is not None: + next_code_argv_filename = argv_filename(next_code_file) + with open(next_code_argv_filename, "w") as f: + f.write(json.dumps(remaining_args)) + + next_code_file = next_code_file + supervisor.set_next_code_file(next_code_file) + print(f"launching: {next_code_file}") + supervisor.reload() + # os.rename("/saves/.boot_py_argv", "/saves/.not_boot_py_argv") + +except OSError: + # no args, just launch desktop + pass + +request_display_config(720, 400) display = supervisor.runtime.display scale = 1 @@ -101,9 +142,11 @@ for device in usb.core.find(find_all=True): # # # assume the device is the mouse # mouse = device - mouse_interface_index, mouse_endpoint_address = adafruit_usb_host_descriptors.find_boot_mouse_endpoint(device) - if mouse_interface_index is not None and mouse_endpoint_address is not None: + _possible_interface_index, _possible_endpoint_address = adafruit_usb_host_descriptors.find_boot_mouse_endpoint(device) + if _possible_interface_index is not None and _possible_endpoint_address is not None: mouse = device + mouse_interface_index = _possible_interface_index + mouse_endpoint_address = _possible_endpoint_address print(f"mouse interface: {mouse_interface_index} endpoint_address: {hex(mouse_endpoint_address)}") mouse_was_attached = None @@ -168,7 +211,7 @@ menu_grid = GridLayout(x=40, y=16, width=WIDTH, height=HEIGHT, grid_size=(config divider_lines=False) main_group.append(menu_grid) -menu_title_txt = Label(font, text=config["menu_title"]) +menu_title_txt = Label(font, text="Fruit Jam OS") menu_title_txt.anchor_point = (0.5, 0.5) menu_title_txt.anchored_position = (display.width // (2 * scale), 2) main_group.append(menu_title_txt) @@ -181,10 +224,9 @@ i = 0 pages = [{}] cur_file_index = 0 -cur_page = 0 + for path in app_path.iterdir(): print(path) - cell_group = AnchoredGroup() code_file = path / "code.py" if not code_file.exists(): @@ -216,23 +258,97 @@ for path in app_path.iterdir(): "icon": str(icon_file.absolute()) if icon_file is not None else None, "file": str(code_file.absolute()) }) - if apps[-1]["icon"] is None: + i += 1 +def reuse_cell(grid_coords): + try: + cell_group = menu_grid.get_content(grid_coords) + return cell_group + except KeyError: + return None + + +def _create_cell_group(app): + cell_group = AnchoredGroup() + + if app["icon"] is None: icon_tg = displayio.TileGrid(bitmap=default_icon_bmp, pixel_shader=default_icon_palette) cell_group.append(icon_tg) else: - icon_bmp, icon_palette = adafruit_imageload.load(apps[-1]["icon"]) + icon_bmp, icon_palette = adafruit_imageload.load(app["icon"]) icon_tg = displayio.TileGrid(bitmap=icon_bmp, pixel_shader=icon_palette) cell_group.append(icon_tg) icon_tg.x = cell_width // 2 - icon_tg.tile_width // 2 - title_txt = TextBox(font, text=apps[-1]["title"], width=WIDTH // config["width"], height=18, + title_txt = TextBox(font, text=app["title"], width=WIDTH // config["width"], height=18, align=TextBox.ALIGN_CENTER) cell_group.append(title_txt) title_txt.anchor_point = (0, 0) title_txt.anchored_position = (0, icon_tg.y + icon_tg.tile_height) - app_titles.append(title_txt) - menu_grid.add_content(cell_group, grid_position=(i % config["width"], i // config["width"]), cell_size=(1, 1)) - i += 1 + return cell_group + + +def _reuse_cell_group(app, cell_group): + _unhide_cell_group(cell_group) + if app["icon"] is None: + icon_tg = cell_group[0] + icon_tg.bitmap = default_icon_bmp + icon_tg.pixel_shader = default_icon_palette + else: + icon_bmp, icon_palette = adafruit_imageload.load(app["icon"]) + icon_tg = cell_group[0] + icon_tg.bitmap = icon_bmp + icon_tg.pixel_shader = icon_palette + + icon_tg.x = cell_width // 2 - icon_tg.tile_width // 2 + # title_txt = TextBox(font, text=app["title"], width=WIDTH // config["width"], height=18, + # align=TextBox.ALIGN_CENTER) + # cell_group.append(title_txt) + title_txt = cell_group[1] + title_txt.text = app["title"] + # title_txt.anchor_point = (0, 0) + # title_txt.anchored_position = (0, icon_tg.y + icon_tg.tile_height) + + +def _hide_cell_group(cell_group): + # hide the tilegrid + cell_group[0].hidden = True + # set the title to blank space + cell_group[1].text = " " + + +def _unhide_cell_group(cell_group): + # show tilegrid + cell_group[0].hidden = False + + +def display_page(page_index): + for grid_index in range(6): + grid_pos = (grid_index % config["width"], grid_index // config["width"]) + try: + cur_app = apps[grid_index + (page_index * 6)] + except IndexError: + try: + cell_group = menu_grid.get_content(grid_pos) + _hide_cell_group(cell_group) + except KeyError: + pass + + # skip to the next for loop iteration + continue + + try: + cell_group = menu_grid.get_content(grid_pos) + _reuse_cell_group(cur_app, cell_group) + except KeyError: + cell_group = _create_cell_group(cur_app) + menu_grid.add_content(cell_group, grid_position=grid_pos, cell_size=(1, 1)) + + # app_titles.append(title_txt) + print(f"{grid_index} | {grid_index % config["width"], grid_index // config["width"]}") + + +cur_page = 0 +display_page(cur_page) left_bmp, left_palette = adafruit_imageload.load("launcher_assets/arrow_left.bmp") left_palette.make_transparent(0) @@ -246,15 +362,18 @@ left_tg.anchored_position = (4, (display.height // 2 // scale) - 2) right_tg = AnchoredTileGrid(bitmap=right_bmp, pixel_shader=right_palette) right_tg.anchor_point = (1.0, 0.5) right_tg.anchored_position = ((display.width // scale) - 4, (display.height // 2 // scale) - 2) +original_arrow_btn_color = left_palette[2] main_group.append(left_tg) main_group.append(right_tg) +if len(apps) <= 6: + right_tg.hidden = True + left_tg.hidden = True + if mouse: main_group.append(mouse_tg) -selected = 0 - def atexit_callback(): """ @@ -265,27 +384,127 @@ def atexit_callback(): if mouse_was_attached and not mouse.is_kernel_driver_active(0): mouse.attach_kernel_driver(0) + atexit.register(atexit_callback) -# print(f"apps: {apps}") +selected = None + + +def change_selected(new_selected): + global selected + # tuple means an item in the grid is selected + if isinstance(selected, tuple): + menu_grid.get_content(selected)[1].background_color = None + + # TileGrid means arrow is selected + elif isinstance(selected, AnchoredTileGrid): + selected.pixel_shader[2] = original_arrow_btn_color + + # tuple means an item in the grid is selected + if isinstance(new_selected, tuple): + menu_grid.get_content(new_selected)[1].background_color = 0x008800 + # TileGrid means arrow is selected + elif isinstance(new_selected, AnchoredTileGrid): + new_selected.pixel_shader[2] = 0x008800 + selected = new_selected + + +change_selected((0, 0)) + + +def handle_key_press(key): + global index, editor_index, cur_page + # print(key) + # up key + if key == "\x1b[A": + if isinstance(selected, tuple): + change_selected((selected[0], (selected[1] - 1) % 2)) + elif selected is left_tg: + change_selected((0, 0)) + elif selected is right_tg: + change_selected((2, 0)) + + + # down key + elif key == "\x1b[B": + if isinstance(selected, tuple): + change_selected((selected[0], (selected[1] + 1) % 2)) + elif selected is left_tg: + change_selected((0, 1)) + elif selected is right_tg: + change_selected((2, 1)) + # selected = min(len(config["apps"]) - 1, selected + 1) + + # left key + elif key == "\x1b[D": + if isinstance(selected, tuple): + if selected[0] >= 1: + change_selected((selected[0] - 1, selected[1])) + elif not left_tg.hidden: + change_selected(left_tg) + else: + change_selected(((selected[0] - 1) % 3, selected[1])) + elif selected is left_tg: + change_selected(right_tg) + elif selected is right_tg: + change_selected((2, 0)) + + # right key + elif key == "\x1b[C": + if isinstance(selected, tuple): + if selected[0] <= 1: + change_selected((selected[0] + 1, selected[1])) + elif not right_tg.hidden: + change_selected(right_tg) + else: + change_selected(((selected[0] + 1) % 3, selected[1])) + elif selected is left_tg: + change_selected((0, 0)) + elif selected is right_tg: + change_selected(left_tg) + + elif key == "\n": + if isinstance(selected, tuple): + index = (selected[1] * 3 + selected[0]) + (cur_page * 6) + if index >= len(apps): + index = None + print("go!") + elif selected is left_tg: + if cur_page > 0: + cur_page -= 1 + display_page(cur_page) + + elif selected is right_tg: + if cur_page < math.ceil(len(apps) / 6) - 1: + cur_page += 1 + display_page(cur_page) + + elif key == "e": + if isinstance(selected, tuple): + editor_index = (selected[1] * 3 + selected[0]) + (cur_page * 6) + if editor_index >= len(apps): + editor_index = None + + print("go!") + else: + print(f"unhandled key: {repr(key)}") + + +print(f"apps: {apps}") +print(mouse_interface_index, mouse_endpoint_address) while True: index = None + editor_index = None available = supervisor.runtime.serial_bytes_available if available: c = sys.stdin.read(available) print(repr(c)) - app_titles[selected].background_color = None + # app_titles[selected].background_color = None - if c == "\x1b[A": - selected = max(0, selected - 1) - elif c == "\x1b[B": - selected = min(len(config["apps"]) - 1, selected + 1) - elif c == "\n": - index = selected - print("go!") + handle_key_press(c) print("selected", selected) - app_titles[selected].background_color = 0x008800 + # app_titles[selected].background_color = 0x008800 if mouse: try: @@ -304,17 +523,27 @@ while True: mouse_tg.y = max(0, min((display.height // scale) - 1, mouse_tg.y + mouse_buf[2])) if mouse_buf[0] & (1 << 0) != 0: + print("left click") clicked_cell = menu_grid.which_cell_contains((mouse_tg.x, mouse_tg.y)) if clicked_cell is not None: index = clicked_cell[1] * config["width"] + clicked_cell[0] if index is not None: - # print("index", index) - # print(f"selected: {apps[index]}") + print("index", index) + print(f"selected: {apps[index]}") launch_file = apps[index]["file"] - supervisor.set_next_code_file(launch_file, sticky_on_reload=True, reload_on_error=True, + supervisor.set_next_code_file(launch_file, sticky_on_reload=False, reload_on_error=True, + working_directory="/".join(launch_file.split("/")[:-1])) + supervisor.reload() + if editor_index is not None: + print("editor_index", editor_index) + print(f"editor selected: {apps[editor_index]}") + edit_file = apps[editor_index]["file"] + + launch_file = "/code_editor.py" + with open(argv_filename(launch_file), "w") as f: + f.write(json.dumps([apps[editor_index]["file"]])) + + supervisor.set_next_code_file(launch_file, sticky_on_reload=False, reload_on_error=True, working_directory="/".join(launch_file.split("/")[:-1])) - - if mouse and not mouse.is_kernel_driver_active(0): - mouse.attach_kernel_driver(0) supervisor.reload() diff --git a/src/code_editor.py b/src/code_editor.py new file mode 100644 index 0000000..92ff787 --- /dev/null +++ b/src/code_editor.py @@ -0,0 +1,62 @@ +import os + +import supervisor +from displayio import Group, Palette, TileGrid +import terminalio +from lvfontio import OnDiskFont +from adafruit_display_text.bitmap_label import Label +from adafruit_bitmap_font import bitmap_font +from adafruit_editor import editor, picker +from tilepalettemapper import TilePaletteMapper +import json +from argv_file_helper import argv_filename +from adafruit_fruitjam.peripherals import request_display_config + +request_display_config(720, 400) +display = supervisor.runtime.display +display.auto_refresh = True + +main_group = Group() + +display.root_group = main_group + +font_palette = Palette(2) +font_palette[0] = 0x000000 +font_palette[1] = 0xFFFFFF + + +font = terminalio.FONT +char_size = font.get_bounding_box() +screen_size = (display.width // char_size[0], display.height // char_size[1]) +print(screen_size) + +terminal_area = TileGrid(bitmap=font.bitmap, pixel_shader=font_palette, width=screen_size[0], height=screen_size[1], + tile_width=char_size[0], tile_height=char_size[1]) + +main_group.append(terminal_area) + +terminal = terminalio.Terminal(terminal_area, font) + +visible_cursor = Label(terminalio.FONT, text="", + color=0x000000, background_color=0xeeeeee, padding_left=1) +visible_cursor.hidden = False +visible_cursor.anchor_point = (0, 0) +visible_cursor.anchored_position = (0, 0) +main_group.append(visible_cursor) + + +file = None +try: + editor_argv_file = argv_filename(__file__) + with open(editor_argv_file, "r") as f: + argv_data = json.load(f) + file = argv_data[0] + os.remove(editor_argv_file) +except OSError: + file = "boot_out.txt" + +print(f"opening {file}") +editor.edit(file, terminal, visible_cursor) +print("after edit") +# while True: +# pass