This commit is contained in:
Scott Shawcroft 2025-04-04 16:14:54 -07:00
commit 365d3207dc
No known key found for this signature in database
37 changed files with 1213 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
dist

118
build.py Normal file
View file

@ -0,0 +1,118 @@
import os
import zipfile
import shutil
from pathlib import Path
def create_font_specific_zip(font_path: Path, src_dir: Path, learn_projects_dir: Path, output_dir: Path):
# Get font name without extension
font_name = font_path.stem
# Create output zip filename
output_zip = output_dir / f"fruit_jam_{font_name}.zip"
# Create a clean temporary directory for building the zip
temp_dir = output_dir / "temp"
if temp_dir.exists():
shutil.rmtree(temp_dir)
temp_dir.mkdir(parents=True)
try:
# Copy src contents
shutil.copytree(src_dir, temp_dir, dirs_exist_ok=True)
# Create fonts directory and copy the specific font
fonts_dir = temp_dir / "fonts"
fonts_dir.mkdir(parents=True, exist_ok=True)
shutil.copy2(font_path, fonts_dir / "terminal.lvfontbin")
# Extract learn-projects contents into apps directory
apps_dir = temp_dir / "apps"
apps_dir.mkdir(parents=True, exist_ok=True)
for zip_path in learn_projects_dir.glob("*.zip"):
# Create app-specific directory using zip name without extension
app_name = zip_path.stem
app_dir = apps_dir / app_name
app_dir.mkdir(parents=True, exist_ok=True)
# Extract zip contents and process them
with zipfile.ZipFile(zip_path, 'r') as zf:
# Find the directory containing code.py
code_dir = None
for path in zf.namelist():
if path.endswith('/code.py'):
code_dir = str(Path(path).parent) + '/'
break
if not code_dir:
print(f"Warning: No code.py found in {zip_path}")
continue
# Extract files from the code.py directory to app directory
for path in zf.namelist():
if path.startswith(code_dir):
# Skip the lib directory as we'll handle it separately
if 'lib/' in path:
continue
# Get the relative path from code_dir
rel_path = path[len(code_dir):]
if rel_path:
# Extract the file
source = zf.open(path)
target = app_dir / rel_path
target.parent.mkdir(parents=True, exist_ok=True)
with open(target, 'wb') as f:
f.write(source.read())
# Handle lib directory specially - move to root
for path in zf.namelist():
if '/lib/' in path:
# Get the part of the path after 'lib/'
lib_index = path.index('/lib/') + 5 # skip past '/lib/'
rel_path = path[lib_index:]
# Skip directory entries
if not rel_path or path.endswith('/'):
continue
# Extract the file to root lib directory
source = zf.open(path)
target = temp_dir / 'lib' / rel_path
# Ensure parent directory exists
target.parent.mkdir(parents=True, exist_ok=True)
# Write the file
with open(target, 'wb') as f:
f.write(source.read())
# Create the final zip file
with zipfile.ZipFile(output_zip, 'w', zipfile.ZIP_DEFLATED) as zf:
for file_path in temp_dir.rglob("*"):
if file_path.is_file():
arcname = file_path.relative_to(temp_dir)
zf.write(file_path, arcname)
print(f"Created {output_zip}")
finally:
# Clean up temporary directory
shutil.rmtree(temp_dir, ignore_errors=True)
def main():
# Get the project root directory
root_dir = Path(__file__).parent
# Set up paths
fonts_dir = root_dir / "fonts"
src_dir = root_dir / "src"
learn_projects_dir = root_dir / "learn-projects"
output_dir = root_dir / "dist"
# Create output directory
output_dir.mkdir(parents=True, exist_ok=True)
# Process each font
for font_path in fonts_dir.glob("*.lvfontbin"):
create_font_specific_zip(font_path, src_dir, learn_projects_dir, output_dir)
if __name__ == "__main__":
main()

BIN
fonts/ID.lvfontbin Normal file

Binary file not shown.

BIN
fonts/cs.lvfontbin Normal file

Binary file not shown.

BIN
fonts/de_DE.lvfontbin Normal file

Binary file not shown.

BIN
fonts/el.lvfontbin Normal file

Binary file not shown.

BIN
fonts/en_GB.lvfontbin Normal file

Binary file not shown.

BIN
fonts/en_US.lvfontbin Normal file

Binary file not shown.

BIN
fonts/en_x_pirate.lvfontbin Normal file

Binary file not shown.

BIN
fonts/es.lvfontbin Normal file

Binary file not shown.

BIN
fonts/fil.lvfontbin Normal file

Binary file not shown.

BIN
fonts/fr.lvfontbin Normal file

Binary file not shown.

BIN
fonts/hi.lvfontbin Normal file

Binary file not shown.

BIN
fonts/it_IT.lvfontbin Normal file

Binary file not shown.

BIN
fonts/ja.lvfontbin Normal file

Binary file not shown.

BIN
fonts/ko.lvfontbin Normal file

Binary file not shown.

BIN
fonts/nl.lvfontbin Normal file

Binary file not shown.

BIN
fonts/pl.lvfontbin Normal file

Binary file not shown.

BIN
fonts/pt_BR.lvfontbin Normal file

Binary file not shown.

BIN
fonts/ru.lvfontbin Normal file

Binary file not shown.

BIN
fonts/sv.lvfontbin Normal file

Binary file not shown.

BIN
fonts/tr.lvfontbin Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

778
src/boot.py Normal file
View file

@ -0,0 +1,778 @@
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import gc
import displayio
import supervisor
from displayio import OnDiskBitmap, TileGrid, Group
import adafruit_imageload
import time
import math
display = supervisor.runtime.display
display.auto_refresh = False
def make_alternate_platte(input_palette, target_color):
new_palette = displayio.Palette(len(input_palette))
for i in range(len(input_palette)):
new_palette[i] = input_palette[i] & target_color
new_palette.make_transparent(0)
return new_palette
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:
# print(f"cur idx: {self.cur_sprite_index}")
self.element[0] = self.cur_sprite_index
self.last_sprite_frame_time = cur_time
# display.refresh()
self.cur_sprite_index += 1
# print(f"cur idx: {self.cur_sprite_index} > {self.sprite_anim_to_index}")
if self.cur_sprite_index > self.sprite_anim_to_index:
# print("returning false from sprite_anim_tick")
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
# display.refresh()
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)
# print(f"cur: {self.element.x}, {self.element.y}")
# display.refresh(target_frames_per_second=30)
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
# 8172592
# apple_sprites, apple_sprites_palette = adafruit_imageload.load("apple_spritesheet.bmp")
# 8342576
def color_sweep(color, palette, delay=0, start_at_index=1):
for i in range(start_at_index, len(palette)):
palette[i] = color
if delay > 0:
time.sleep(delay)
apple_sprites, apple_sprites_palette = adafruit_imageload.load("/boot_animation/apple_spritesheet_lined_6_indexed.bmp")
#print(f"free: {gc.mem_free()}")
# apple_sprites_palette.make_transparent(0)
color_sweep(0xffffff, apple_sprites_palette)
f_sprites, f_sprites_palette = adafruit_imageload.load("/boot_animation/f_spritesheet_lined_6_indexed.bmp")
color_sweep(0xffffff, f_sprites_palette)
# f_sprites_palette.make_transparent(0)
r_sprites, r_sprites_palette = adafruit_imageload.load("/boot_animation/r_spritesheet_lined_6_indexed.bmp")
color_sweep(0xffffff, r_sprites_palette)
# r_sprites_palette.make_transparent(0)
u_sprites, u_sprites_palette = adafruit_imageload.load("/boot_animation/u_spritesheet_lined_6_indexed.bmp")
color_sweep(0xffffff, u_sprites_palette)
# u_sprites_palette.make_transparent(0)
i_sprites, i_sprites_palette = adafruit_imageload.load("/boot_animation/i_spritesheet_lined_6_indexed.bmp")
color_sweep(0xffffff, i_sprites_palette)
# i_sprites_palette.make_transparent(0)
t_sprites, t_sprites_palette = adafruit_imageload.load("/boot_animation/t_spritesheet_lined_6_indexed.bmp")
color_sweep(0xffffff, t_sprites_palette)
# t_sprites_palette.make_transparent(0)
j_sprites, j_sprites_palette = adafruit_imageload.load("/boot_animation/j_spritesheet_lined_6_indexed.bmp")
j_sprites_palette.make_transparent(0)
color_sweep(0xffffff, j_sprites_palette, start_at_index=2)
a_sprites, a_sprites_palette = adafruit_imageload.load("/boot_animation/a_spritesheet_lined_6_indexed.bmp")
a_sprites_palette.make_transparent(0)
color_sweep(0xffffff, a_sprites_palette, start_at_index=2)
m_sprites, m_sprites_palette = adafruit_imageload.load("/boot_animation/m_spritesheet_lined_6_indexed.bmp")
m_sprites_palette.make_transparent(0)
color_sweep(0xffffff, m_sprites_palette, start_at_index=2)
def color_sweep_all(color, delay=0.05, refresh_during=True):
_start = 4
for i in range(1, len(apple_sprites_palette)):
apple_sprites_palette[i] = color
if _start <= i < _start+7:
f_sprites_palette[i-_start+1] = color
r_sprites_palette[i-_start+1] = color
u_sprites_palette[i-_start+1] = color
i_sprites_palette[i-_start+1] = color
t_sprites_palette[i-_start+1] = color
# print(f"i: {i} >= {len(apple_sprites_palette) -7}")
if i >= len(apple_sprites_palette) - 7:
_adjusted_idx = i - (len(apple_sprites_palette) - 7) + 2
# print(f"adj idx: {_adjusted_idx}")
j_sprites_palette[_adjusted_idx] = color
a_sprites_palette[_adjusted_idx] = color
m_sprites_palette[_adjusted_idx] = color
if i % 6 == 0 and refresh_during:
display.refresh()
if delay > 0:
time.sleep(delay)
display.refresh()
# apple_sprites = OnDiskBitmap("apple_spritesheet.bmp")
# apple_sprites_palette = apple_sprites.pixel_shader
# apple_sprites_palette.make_transparent(0)
#
# f_sprites = OnDiskBitmap("f_spritesheet.bmp")
# f_sprites_palette = f_sprites.pixel_shader
# f_sprites_palette.make_transparent(0)
# r_sprites = OnDiskBitmap("r_spritesheet.bmp")
# r_sprites_palette = r_sprites.pixel_shader
# r_sprites_palette.make_transparent(0)
# u_sprites = OnDiskBitmap("u_spritesheet.bmp")
# u_sprites_palette = u_sprites.pixel_shader
# u_sprites_palette.make_transparent(0)
# i_sprites = OnDiskBitmap("i_spritesheet.bmp")
# i_sprites_palette = i_sprites.pixel_shader
# i_sprites_palette.make_transparent(0)
# t_sprites = OnDiskBitmap("t_spritesheet.bmp")
# t_sprites_palette = t_sprites.pixel_shader
# t_sprites_palette.make_transparent(0)
# j_sprites = OnDiskBitmap("j_spritesheet.bmp")
# j_sprites_palette = j_sprites.pixel_shader
# j_sprites_palette.make_transparent(0)
# a_sprites = OnDiskBitmap("a_spritesheet.bmp")
# a_sprites_palette = a_sprites.pixel_shader
# a_sprites_palette.make_transparent(0)
# m_sprites = OnDiskBitmap("m_spritesheet.bmp")
# m_sprites_palette = m_sprites.pixel_shader
# m_sprites_palette.make_transparent(0)
default_sprite_delay = 1 / 35
main_group = Group()
main_group.x = display.width // 4
main_group.y = display.height // 4
sliding_group = Group()
main_group.append(sliding_group)
letters_x_start = 83
letters_y_start = display.height
coordinator = {
"steps": [
# Apple fly on
{
"type": "animation_step",
"tilegrid": TileGrid(apple_sprites, pixel_shader=apple_sprites_palette,
tile_width=73, tile_height=107, width=1, height=1),
"offscreen_loc": (0, -107),
"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": TileGrid(f_sprites, pixel_shader=f_sprites_palette,
tile_width=32, tile_height=39, width=1, height=1),
"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": TileGrid(r_sprites, pixel_shader=r_sprites_palette,
tile_width=32, tile_height=39, width=1, height=1),
"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": TileGrid(u_sprites, pixel_shader=u_sprites_palette,
tile_width=32, tile_height=39, width=1, height=1),
"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": TileGrid(i_sprites, pixel_shader=i_sprites_palette,
tile_width=16, tile_height=39, width=1, height=1),
"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": TileGrid(t_sprites, pixel_shader=t_sprites_palette,
tile_width=32, tile_height=39, width=1, height=1),
"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": TileGrid(j_sprites, pixel_shader=j_sprites_palette,
tile_width=32, tile_height=39, width=1, height=1),
"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": TileGrid(a_sprites, pixel_shader=a_sprites_palette,
tile_width=32, tile_height=39, width=1, height=1),
"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": TileGrid(m_sprites, pixel_shader=m_sprites_palette,
tile_width=43, tile_height=39, width=1, height=1),
"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["red_palette"] = make_alternate_platte(step["default_palette"], 0xff0000)
# step["yellow_palette"] = make_alternate_platte(step["default_palette"], 0xffff00)
# step["teal_palette"] = make_alternate_platte(step["default_palette"], 0x00ffff)
# step["pink_palette"] = make_alternate_platte(step["default_palette"], 0xff00ff)
# step["blue_palette"] = make_alternate_platte(step["default_palette"], 0x0000ff)
# step["green_palette"] = make_alternate_platte(step["default_palette"], 0x00ff00)
# step["tilegrid"].pixel_shader = step["red_palette"]
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,
"move_duration": 0.3,
"overshoot_pixels": 22,
"eased_value": None,
"sprite_anim_range": (19, 27),
"sprite_delay": 1 / 22,
# "start_time": 4.08,
# "start_time": 3.047,
"start_time": 3.0,
# "sprite_anim_start": 0.15,
"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": 4.78,
# "start_time": 3.497,
"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": 5.48,
# "start_time": 3.947,
"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": 5.58,
# "start_time": 4.047,
"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": 5.68,
# "start_time": 4.147,
"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, -107),
"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": 5.68,
"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, -107),
"onscreen_loc": (0, 21),
"move_duration": 0.01,
"overshoot_pixels": 0,
"eased_value": None,
"sprite_anim_range": (13, 18),
"sprite_delay": 1 / 32,
# "start_time": 5.68,
"start_time": 8.75,
"sprite_anim_start": 0.0,
"started": False,
}
)
display.root_group = main_group
# hrule = displayio.Bitmap(200, 1, 1)
# hrule_palette = displayio.Palette(1)
# hrule_palette[0] = 0xff0000
# hrule_tg = TileGrid(bitmap=hrule, pixel_shader=hrule_palette)
# main_group.append(hrule_tg)
# hrule_tg.y = 59
# hrule_tg.x = 40
start_time = time.monotonic()
while True:
now = time.monotonic()
still_going = True
for i, step in enumerate(coordinator["steps"]):
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)
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()
if not still_going:
for step in coordinator["steps"]:
if step["type"] == "animation_step":
step["tilegrid"].x = step["offscreen_loc"][0]
step["tilegrid"].y = step["offscreen_loc"][1]
step["started"] = False
# reset the apple to no eyes sprite
coordinator["steps"][0]["tilegrid"][0] = 0
time.sleep(2)
color_sweep_all(0xffffff, delay=0, refresh_during=False)
display.refresh()
start_time = time.monotonic()

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

266
src/code.py Normal file
View file

@ -0,0 +1,266 @@
# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries
# SPDX-License-Identifier: MIT
"""
This example uses adafruit_display_text.label to display text using a custom font
loaded by adafruit_bitmap_font
"""
import array
import board
import displayio
import pathlib
import supervisor
import sys
import usb
from adafruit_display_text import label
from adafruit_bitmap_font import bitmap_font
from adafruit_display_text.text_box import TextBox
from adafruit_display_text.bitmap_label import Label
from adafruit_displayio_layout.layouts.grid_layout import GridLayout
from adafruit_anchored_tilegrid import AnchoredTileGrid
import adafruit_imageload
import adafruit_usb_host_descriptors
from sized_group import SizedGroup
display = supervisor.runtime.display
font_file = "/fonts/terminal.lvfontbin"
font = bitmap_font.load_font(font_file)
main_group = displayio.Group()
display.root_group = main_group
background_bmp = displayio.Bitmap(display.width, display.height, 1)
bg_palette = displayio.Palette(1)
bg_palette[0] = 0x222222
bg_tg = displayio.TileGrid(bitmap=background_bmp, pixel_shader=bg_palette)
main_group.append(bg_tg)
# load the mouse cursor bitmap
mouse_bmp = displayio.OnDiskBitmap("mouse_cursor.bmp")
# make the background pink pixels transparent
mouse_bmp.pixel_shader.make_transparent(0)
# create a TileGrid for the mouse, using its bitmap and pixel_shader
mouse_tg = displayio.TileGrid(mouse_bmp, pixel_shader=mouse_bmp.pixel_shader)
# move it to the center of the display
mouse_tg.x = display.width // 2
mouse_tg.y = display.height // 2
#046d:c52f
#mouse = usb.core.find(idVendor=0x046d, idProduct=0xc52f)
DIR_IN = 0x80
mouse_interface_index, mouse_endpoint_address = None, None
mouse = None
#scan for connected USB device and loop over any found
print("scanning usb")
for device in usb.core.find(find_all=True):
# print device info
print(f"{device.idVendor:04x}:{device.idProduct:04x}")
print(device.manufacturer, device.product)
print()
config_descriptor = adafruit_usb_host_descriptors.get_configuration_descriptor(
device, 0
)
print(config_descriptor)
#
# i = 0
# while i < len(config_descriptor):
# descriptor_len = config_descriptor[i]
# descriptor_type = config_descriptor[i + 1]
# if descriptor_type == adafruit_usb_host_descriptors.DESC_CONFIGURATION:
# config_value = config_descriptor[i + 5]
# print(f" value {config_value:d}")
# elif descriptor_type == adafruit_usb_host_descriptors.DESC_INTERFACE:
# interface_number = config_descriptor[i + 2]
# interface_class = config_descriptor[i + 5]
# interface_subclass = config_descriptor[i + 6]
# interface_protocol = config_descriptor[i + 7]
# print(f" interface[{interface_number:d}]")
# print(
# f" class {interface_class:02x} subclass {interface_subclass:02x}"
# )
# print(f"protocol: {interface_protocol}")
# elif descriptor_type == adafruit_usb_host_descriptors.DESC_ENDPOINT:
# endpoint_address = config_descriptor[i + 2]
# if endpoint_address & DIR_IN:
# print(f" IN {endpoint_address:02x}")
# else:
# print(f" OUT {endpoint_address:02x}")
# i += descriptor_len
# print()
#
# # 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:
mouse = device
print(f"mouse interface: {mouse_interface_index} endpoint_address: {hex(mouse_endpoint_address)}")
if mouse is not None:
# detach the kernel driver if needed
if mouse.is_kernel_driver_active(0):
mouse.detach_kernel_driver(0)
# set configuration on the mouse so we can use it
mouse.set_configuration()
mouse_buf = array.array("b", [0] * 8)
WIDTH = 300
HEIGHT = 192
config = {
"menu_title": "Launcher Menu",
"width": 3,
"height": 2,
"apps": [
{
"title": "🐍Snake🐍",
"icon": "icon_snake.bmp",
"file": "code_snake_game.py"
},
{
"title": "Nyan😺Flap",
"icon": "icon_flappynyan.bmp",
"file": "code_flappy_nyan.py"
},
{
"title": "Memory🧠",
"icon": "icon_memory.bmp",
"file": "code_memory.py"
},
{
"title": "Matrix",
"icon": "/apps/matrix/icon.bmp",
"file": "/apps/matrix/code.py"
},
{
"title": "Breakout",
"icon": "icon_breakout.bmp",
"file": "code_breakout.py"
},
{
"title": "Paint🖌",
"icon": "icon_paint.bmp",
}
]
}
cell_width = WIDTH // config["width"]
default_icon_bmp, default_icon_palette = adafruit_imageload.load("default_icon_64.bmp")
default_icon_palette.make_transparent(0)
menu_grid = GridLayout(x=10, y=26, width=WIDTH, height=HEIGHT, grid_size=(config["width"], config["height"]),
divider_lines=False)
main_group.append(menu_grid)
menu_title_txt = Label(font, text=config["menu_title"])
menu_title_txt.anchor_point = (0.5, 0.5)
menu_title_txt.anchored_position = (display.width//2, 2)
main_group.append(menu_title_txt)
app_titles = []
apps = []
app_path = pathlib.Path("/apps")
i = 0
for path in app_path.iterdir():
print(path)
code_file = path / "code.py"
if not code_file.exists():
continue
cell_group = SizedGroup()
icon_file = path / "icon.bmp"
if not icon_file.exists():
icon_file = None
apps.append({
"title": path.name,
"icon": str(icon_file.absolute()),
"file": str(code_file.absolute())
})
if apps[-1]["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_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, 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
left_bmp, left_palette = adafruit_imageload.load("arrow_left.bmp")
left_palette.make_transparent(0)
right_bmp, right_palette = adafruit_imageload.load("arrow_right.bmp")
right_palette.make_transparent(0)
left_tg = AnchoredTileGrid(bitmap=left_bmp, pixel_shader=left_palette)
left_tg.anchor_point = (0, 1.0)
left_tg.anchored_position = (10, display.height - 2)
right_tg = AnchoredTileGrid(bitmap=right_bmp, pixel_shader=right_palette)
right_tg.anchor_point = (1.0, 1.0)
right_tg.anchored_position = (display.width - 10, display.height - 2)
main_group.append(left_tg)
main_group.append(right_tg)
if mouse:
main_group.append(mouse_tg)
selected = 0
while True:
index = None
available = supervisor.runtime.serial_bytes_available
if available:
c = sys.stdin.read(available)
print(repr(c))
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!")
print("selected", selected)
app_titles[selected].background_color = 0x008800
if mouse:
try:
# attempt to read data from the mouse
# 10ms timeout, so we don't block long if there
# is no data
count = mouse.read(mouse_endpoint_address, mouse_buf, timeout=10)
except usb.core.USBTimeoutError:
# skip the rest of the loop if there is no data
count = 0
# update the mouse tilegrid x and y coordinates
# based on the delta values read from the mouse
if count > 0:
mouse_tg.x = max(0, min(display.width - 1, mouse_tg.x + mouse_buf[1]))
mouse_tg.y = max(0, min(display.height - 1, mouse_tg.y + mouse_buf[2]))
if mouse_buf[0] & (1 << 0) != 0:
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:
supervisor.set_next_code_file(config["apps"][index]["file"], sticky_on_reload=True, reload_on_error=True, working_directory="/apps/matrix")
if mouse and not mouse.is_kernel_driver_active(0):
mouse.attach_kernel_driver(0)
supervisor.reload()

49
src/sized_group.py Normal file
View file

@ -0,0 +1,49 @@
import displayio
class SizedGroup(displayio.Group):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@property
def size(self):
min_x = 0
min_y = 0
max_x = 0
max_y = 0
for element in self:
# print(type(element))
if type(element) == displayio.TileGrid:
if element.x < min_x:
min_x = element.x
if element.y < min_y:
min_y = element.y
_element_max_x = element.x + (element.width * element.tile_width)
_element_max_y = element.y + (element.height * element.tile_height)
if _element_max_x > max_x:
max_x = _element_max_x
if _element_max_y > max_y:
max_y = _element_max_y
else:
if element.x < min_x:
min_x = element.x
if element.y < min_y:
min_y = element.y
_element_max_x = element.x + (element.width * element.scale)
_element_max_y = element.y + (element.height * element.scale)
if _element_max_x > max_x:
max_x = _element_max_x
if _element_max_y > max_y:
max_y = _element_max_y
return max_x - min_x, max_y - min_y
@property
def width(self):
return self.size[0]
@property
def height(self):
return self.size[1]