Add RLCD animations and screensavers

This commit is contained in:
aeroreyna
2026-06-21 21:19:51 -04:00
parent edf3da1a30
commit 8dbc5f4c7b
24 changed files with 3097 additions and 0 deletions
@@ -0,0 +1,143 @@
# Hermes ESP32 voice-state animations for MicroPython screen boards.
# Uploaded by the API server; can also be run manually with:
# exec(open('hermes_voice_animations.py').read()); run_state('ack')
import time
try:
import board_config
except Exception:
board_config = None
def _display():
return getattr(board_config, 'display_instance', None) if board_config else None
def _sleep(ms):
try:
time.sleep_ms(ms)
except AttributeError:
time.sleep(ms / 1000)
def _text(d, msg, x, y, scale=1, color=1):
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale=scale, color=color)
return
except TypeError:
pass
d.text(msg, x, y, color)
def _center_text(d, msg, y, scale=1, color=1):
width = getattr(d, 'width', 320)
char_w = 8 * scale
x = max(0, (width - len(msg) * char_w) // 2)
_text(d, msg, x, y, scale=scale, color=color)
def _wrap(text, width_chars=34, max_lines=4):
words = str(text or '').replace('\n', ' ').split()
lines, line = [], ''
for w in words:
candidate = (line + ' ' + w).strip()
if len(candidate) > width_chars and line:
lines.append(line)
line = w
if len(lines) >= max_lines:
break
else:
line = candidate
if line and len(lines) < max_lines:
lines.append(line)
return lines
def _spinner(d, title, subtitle='', frames=24, delay=70):
width, height = getattr(d, 'width', 320), getattr(d, 'height', 240)
cx, cy = width // 2, height // 2 - 8
spokes = [(-22,0),(-16,-16),(0,-22),(16,-16),(22,0),(16,16),(0,22),(-16,16)]
for frame in range(frames):
d.clear(0)
_center_text(d, title, 18, scale=2)
for i, (dx, dy) in enumerate(spokes):
color = 1 if (i + frame) % len(spokes) in (0, 1, 2, 3) else 0
size = 4 if i == frame % len(spokes) else 2
d.fill_rect(cx + dx - size // 2, cy + dy - size // 2, size, size, color)
d.rect(8, height - 34, width - 16, 18, 1)
d.fill_rect(10, height - 32, ((width - 20) * ((frame % 16) + 1)) // 16, 14, 1)
if subtitle:
_center_text(d, subtitle[:32], height - 56, scale=1)
d.show()
_sleep(delay)
def ack(frames=18):
d = _display()
if not d:
return
width, height = d.width, d.height
for i in range(frames):
d.clear(0)
_center_text(d, 'GOT IT', 18, scale=2)
# expanding check pulse
box = 48 + (i % 6) * 8
x, y = width // 2 - box // 2, height // 2 - box // 2
d.rect(x, y, box, box, 1)
d.line(width//2 - 24, height//2, width//2 - 6, height//2 + 18, 1)
d.line(width//2 - 6, height//2 + 18, width//2 + 30, height//2 - 22, 1)
_center_text(d, 'Listening captured', height - 36, scale=1)
d.show()
_sleep(55)
def captioning(frames=36):
d = _display()
if not d:
return
width, height = d.width, d.height
for i in range(frames):
d.clear(0)
_center_text(d, 'CAPTIONING', 16, scale=2)
for bar in range(9):
h = 8 + ((i * 5 + bar * 13) % 42)
x = width // 2 - 72 + bar * 18
d.fill_rect(x, height // 2 - h // 2, 10, h, 1)
dots = '.' * ((i // 5) % 4)
_center_text(d, 'Transcribing' + dots, height - 42, scale=1)
d.show()
_sleep(65)
def waiting(text='', frames=44):
d = _display()
if not d:
return
width, height = d.width, d.height
for i in range(frames):
d.clear(0)
_center_text(d, 'THINKING', 12, scale=2)
r = 18 + (i % 10) * 2
cx, cy = width // 2, height // 2 - 10
d.rect(cx - r, cy - r, r * 2, r * 2, 1)
d.rect(cx - r//2, cy - r//2, r, r, 1)
if text:
d.text('Heard:', 10, height - 68, 1)
for idx, line in enumerate(_wrap(text, 36, 3)):
d.text(line[:38], 10, height - 54 + idx * 12, 1)
else:
_center_text(d, 'Waiting for reply...', height - 42, scale=1)
d.show()
_sleep(75)
def run_state(state='ack', text='', frames=None):
state = str(state or 'ack').lower()
if state in ('ack', 'listening', 'got_it'):
ack(frames or 18)
elif state in ('caption', 'captioning', 'transcribing'):
captioning(frames or 36)
elif state in ('wait', 'waiting', 'thinking', 'reply'):
waiting(text, frames or 44)
else:
_spinner(_display(), 'HERMES', state[:28], frames or 20)
+48
View File
@@ -0,0 +1,48 @@
# Retro Video Game Screensavers for Reyna little32 ESP32-S3
A collection of 5 standalone, retro-game-inspired MicroPython screensavers designed for high-contrast, low-resolution reflective LCD screens (specifically the Waveshare ESP32-S3-RLCD-4.2).
## Screensavers List
1. **Maze Chomper (`maze_chomper.py`)**
* **Inspired By:** *maze-chase arcade classic*
* **Description:** A wedge-shaped mouth chomps its way through a symmetric maze grid of dots. A ghost follows and chases the chomper. If the ghost catches the chomper, a collision is registered, and the board resets.
2. **Block Puzzle (`block_puzzle.py`)**
* **Inspired By:** *falling-block puzzle classic*
* **Description:** Tetromino pieces fall from the top of the board. A simple self-playing heuristic automatically shifts and rotates each piece to target a column. When a horizontal row is fully occupied, it is cleared. The game resets once blocks reach the ceiling.
3. **Alien Invaders (`alien_invaders.py`)**
* **Inspired By:** *alien-wave shooter classic*
* **Description:** Crawling leg-animated invaders march side-to-side and descend down the screen. An automated defender ship moves underneath to aim and destroy invaders. Both invaders and ship shoot bullets/bombs, causing pixel-spark explosions.
4. **Asteroid Drift (`asteroid_drift.py`)**
* **Inspired By:** *vector-rock space classic*
* **Description:** A wireframe, vector-style ship sits in the center of the screen, automatically aiming its nose towards the closest drifting space rock. The ship fires lasers to blow up and split larger rocks into medium and small fragments.
5. **Paddle Bounce (`paddle_bounce.py`)**
* **Inspired By:** *paddle-ball arcade classic*
* **Description:** Two vertical paddles play tennis against each other. The ball bounces off the top/bottom boundaries and the paddles, increasing speed with each paddle contact. Simulated reaction times and position offsets introduce realistic mistakes, allowing scores to accrue.
## How It Works
* **Standalone Execution:** Each script is designed to run completely standalone and can be executed via REPL or boot routines.
* **Hardware Fallback:** If `board_config` or `display_instance` is not available (such as running under test environments or without initialized hardware), the script gracefully prints a warning message and exits without throwing a crash exception.
* **Touch Stop Detection:** Every script contains a robust `check_touch` function that intercepts touch interrupts/coordinates across various formats (`board_config.touch_instance`, `board_config.touch`, `display.get_touch()`, or `display.touch()`). If a touch is registered, the loop terminates immediately.
* **Frame Control:** Every script exposes a `run(text="", frames=None)` function. If `frames` is set to `1` (useful for syntax checks or single-frame rendering), the screensaver renders exactly one frame and exits. If `frames` is `None` or `<=0`, it runs indefinitely.
## Example Execution Commands
Upload the desired screensaver to your board using `mpremote`:
```bash
mpremote connect /dev/cu.usbmodem101 cp screensavers/maze_chomper.py :
```
Run the screensaver directly from the MicroPython REPL:
```python
# Execute the entire file to run automatically
exec(open("maze_chomper.py").read())
```
Or import the module and invoke with custom parameters:
```python
# Import and run with frame limits (e.g. exit after 300 frames)
import maze_chomper
maze_chomper.run(text="BREAKROOM SCREEN", frames=300)
```
+322
View File
@@ -0,0 +1,322 @@
# Standalone MicroPython Screensaver: Alien Invaders
# Inspired by alien-wave shooter classic. Exposes run(text="", frames=None).
import time
import math
import random
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def sleep_ms(ms):
try:
time.sleep_ms(ms)
except AttributeError:
time.sleep(ms / 1000.0)
def check_touch(d, bc):
"""Robust check for screen touch to stop the screensaver."""
if bc is not None:
touch_inst = getattr(bc, 'touch_instance', None)
if touch_inst is not None:
if hasattr(touch_inst, 'is_touched'):
try:
if touch_inst.is_touched():
return True
except Exception:
pass
for meth in ('get_touch', 'read_touch'):
if hasattr(touch_inst, meth):
try:
res = getattr(touch_inst, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
touch_val = getattr(bc, 'touch', None)
if touch_val is not None:
if callable(touch_val):
try:
res = touch_val()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
else:
if hasattr(touch_val, 'is_touched'):
try:
if touch_val.is_touched():
return True
except Exception:
pass
for meth in ('get_touch', 'read_touch'):
if hasattr(touch_val, meth):
try:
res = getattr(touch_val, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
if d is not None:
for meth in ('get_touch', 'touch'):
if hasattr(d, meth):
try:
res = getattr(d, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
return False
def draw_invader(d, ix, iy, frame_state):
# Alternating animation frames for crawling legs
d.fill_rect(ix - 7, iy - 4, 14, 3, 1)
d.fill_rect(ix - 5, iy - 1, 10, 3, 1)
if frame_state:
# Legs out
d.fill_rect(ix - 7, iy + 2, 2, 3, 1)
d.fill_rect(ix + 5, iy + 2, 2, 3, 1)
else:
# Legs in
d.fill_rect(ix - 4, iy + 2, 2, 3, 1)
d.fill_rect(ix + 2, iy + 2, 2, 3, 1)
# Eyes
d.fill_rect(ix - 3, iy - 3, 2, 2, 0)
d.fill_rect(ix + 1, iy - 3, 2, 2, 0)
def draw_ship(d, sx, sy):
d.fill_rect(sx - 9, sy - 4, 18, 5, 1)
d.fill_rect(sx - 3, sy - 8, 6, 4, 1)
d.fill_rect(sx - 1, sy - 12, 2, 4, 1)
def run(text="", frames=None):
if not display:
print("board_config.display_instance is not available. Skipping screensaver.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
# Game parameters
invader_w, invader_h = 14, 9
col_spacing = 28
row_spacing = 18
grid_cols = 6
grid_rows = 3
# Initialize game objects
invaders = [] # entries: [col, row, alive_bool]
def reset_invaders():
invaders.clear()
for r in range(grid_rows):
for c in range(grid_cols):
invaders.append([c, r, True])
reset_invaders()
inv_grid_x = (width - (grid_cols * col_spacing)) // 2
inv_grid_y = 40
inv_speed = 1.5
inv_dir = 1
ship_x = width // 2
ship_y = height - 15
ship_speed = 2.0
bullets = [] # player bullets: [x, y]
bombs = [] # alien bombs: [x, y]
explosions = [] # particle list: [x, y, vx, vy, life]
score = 0
frame = 0
cooldown = 0
death_timer = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
if check_touch(display, board_config):
print("Screensaver stopped by screen touch.")
break
# If player was hit, run a death screen-shake / reset sequence
if death_timer > 0:
death_timer -= 1
display.clear(0)
# Simple screen shake by offsetting lines slightly
shake = random.randint(-4, 4)
display.text("SYSTEM ERROR", width // 2 - 48 + shake, height // 2 - 4 + shake, 1)
display.show()
if death_timer == 0:
reset_invaders()
bullets.clear()
bombs.clear()
explosions.clear()
inv_grid_x = (width - (grid_cols * col_spacing)) // 2
inv_grid_y = 40
ship_x = width // 2
frame += 1
sleep_ms(30)
continue
# AI: Move player ship toward closest invader horizontally
alive_invaders = [inv for inv in invaders if inv[2]]
if not alive_invaders:
score += 500
reset_invaders()
inv_grid_y = 40
bullets.clear()
bombs.clear()
continue
# Find the lowest/closest invader to follow
target_inv = None
max_r = -1
for inv in alive_invaders:
if inv[1] > max_r:
max_r = inv[1]
target_inv = inv
if target_inv:
inv_col, inv_row = target_inv[0], target_inv[1]
t_x = inv_grid_x + inv_col * col_spacing + col_spacing // 2
if ship_x < t_x:
ship_x = min(width - 15, ship_x + ship_speed)
elif ship_x > t_x:
ship_x = max(15, ship_x - ship_speed)
# Auto shooting
if cooldown > 0:
cooldown -= 1
elif random.random() < 0.15:
bullets.append([ship_x, ship_y - 12])
cooldown = 12
# Invader movements
# Calculate grid bounds based on active invaders
left_col = min(inv[0] for inv in alive_invaders)
right_col = max(inv[0] for inv in alive_invaders)
grid_left = inv_grid_x + left_col * col_spacing
grid_right = inv_grid_x + right_col * col_spacing + col_spacing
# Check boundary collision
if inv_dir == 1 and grid_right >= width - 15:
inv_dir = -1
inv_grid_y += row_spacing
elif inv_dir == -1 and grid_left <= 15:
inv_dir = 1
inv_grid_y += row_spacing
else:
inv_grid_x += inv_dir * inv_speed
# Invader shooting (bombs)
if random.random() < 0.03:
shooter = random.choice(alive_invaders)
bx = inv_grid_x + shooter[0] * col_spacing + col_spacing // 2
by = inv_grid_y + shooter[1] * row_spacing + row_spacing
bombs.append([bx, by])
# Update bullets (going up)
next_bullets = []
for b in bullets:
b[1] -= 4
hit = False
# Collision check with invaders
for inv in invaders:
if inv[2]:
ix = inv_grid_x + inv[0] * col_spacing + col_spacing // 2
iy = inv_grid_y + inv[1] * row_spacing + row_spacing // 2
if abs(b[0] - ix) < inv_col * 0 + 8 and abs(b[1] - iy) < 6:
inv[2] = False
hit = True
score += 30
# Spawn explosion particles
for _ in range(8):
explosions.append([ix, iy, random.uniform(-2, 2), random.uniform(-2, 2), 10])
break
if not hit and b[1] > 28:
next_bullets.append(b)
bullets = next_bullets
# Update bombs (going down)
next_bombs = []
for b in bombs:
b[1] += 3
# Collision check with player ship
if abs(b[0] - ship_x) < 10 and abs(b[1] - ship_y) < 6:
death_timer = 50 # Start system error state
# Large explosion
for _ in range(20):
explosions.append([ship_x, ship_y, random.uniform(-4, 4), random.uniform(-4, 4), 20])
break
elif b[1] < height:
next_bombs.append(b)
bombs = next_bombs
# Update explosions
next_explosions = []
for p in explosions:
p[0] += p[2]
p[1] += p[3]
p[4] -= 1
if p[4] > 0:
next_explosions.append(p)
explosions = next_explosions
# Check game over by invaders reaching bottom
max_y = max(inv_grid_y + inv[1] * row_spacing for inv in alive_invaders)
if max_y >= ship_y - 8:
death_timer = 50
for _ in range(20):
explosions.append([ship_x, ship_y, random.uniform(-4, 4), random.uniform(-4, 4), 20])
continue
# Render game objects
display.clear(0)
# Draw header
display.text(text if text else "INVADER DRIFT", 10, 10, 1)
display.text(f"SCORE: {score:05d}", width - 120, 10, 1)
display.line(0, 26, width, 26, 1)
# Draw invaders
inv_anim_state = (frame // 10) % 2 == 0
for inv in invaders:
if inv[2]:
ix = inv_grid_x + inv[0] * col_spacing + col_spacing // 2
iy = inv_grid_y + inv[1] * row_spacing + row_spacing // 2
draw_invader(display, ix, iy, inv_anim_state)
# Draw player ship
draw_ship(display, ship_x, ship_y)
# Draw player bullets
for b in bullets:
display.fill_rect(b[0] - 1, b[1] - 3, 2, 6, 1)
# Draw alien bombs
for b in bombs:
display.line(b[0], b[1] - 3, b[0], b[1] + 3, 1)
display.line(b[0] - 1, b[1], b[0] + 1, b[1], 1)
# Draw explosion particles
for p in explosions:
display.fill_rect(int(p[0]), int(p[1]), 2, 2, 1)
display.show()
frame += 1
sleep_ms(30)
except KeyboardInterrupt:
print("Screensaver stopped by user interrupt.")
if __name__ in ('__main__', '__name__'):
run()
+319
View File
@@ -0,0 +1,319 @@
# Standalone MicroPython Screensaver: Asteroid Drift
# Inspired by vector-rock space classic. Exposes run(text="", frames=None).
import time
import math
import random
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def sleep_ms(ms):
try:
time.sleep_ms(ms)
except AttributeError:
time.sleep(ms / 1000.0)
def check_touch(d, bc):
"""Robust check for screen touch to stop the screensaver."""
if bc is not None:
touch_inst = getattr(bc, 'touch_instance', None)
if touch_inst is not None:
if hasattr(touch_inst, 'is_touched'):
try:
if touch_inst.is_touched():
return True
except Exception:
pass
for meth in ('get_touch', 'read_touch'):
if hasattr(touch_inst, meth):
try:
res = getattr(touch_inst, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
touch_val = getattr(bc, 'touch', None)
if touch_val is not None:
if callable(touch_val):
try:
res = touch_val()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
else:
if hasattr(touch_val, 'is_touched'):
try:
if touch_val.is_touched():
return True
except Exception:
pass
for meth in ('get_touch', 'read_touch'):
if hasattr(touch_val, meth):
try:
res = getattr(touch_val, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
if d is not None:
for meth in ('get_touch', 'touch'):
if hasattr(d, meth):
try:
res = getattr(d, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
return False
def make_rock(x, y, radius):
num_verts = 8
vertices = []
for i in range(num_verts):
angle = i * (2 * math.pi / num_verts)
vert_r = radius + random.uniform(-radius / 3.0, radius / 3.0)
dx = vert_r * math.cos(angle)
dy = vert_r * math.sin(angle)
vertices.append((dx, dy))
# Velocity
speed = random.uniform(0.5, 1.5)
v_angle = random.uniform(0, 2 * math.pi)
vx = speed * math.cos(v_angle)
vy = speed * math.sin(v_angle)
return {
'x': x,
'y': y,
'vx': vx,
'vy': vy,
'radius': radius,
'vertices': vertices
}
def draw_rock(d, rock):
rx, ry = rock['x'], rock['y']
verts = rock['vertices']
n = len(verts)
for i in range(n):
x1, y1 = rx + verts[i][0], ry + verts[i][1]
x2, y2 = rx + verts[(i+1)%n][0], ry + verts[(i+1)%n][1]
d.line(int(x1), int(y1), int(x2), int(y2), 1)
def run(text="", frames=None):
if not display:
print("board_config.display_instance is not available. Skipping screensaver.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
px, py = width // 2, height // 2
theta = 0.0
bullets = [] # elements: {'x', 'y', 'vx', 'vy', 'life'}
rocks = [] # elements: rock dicts
explosions = [] # elements: {'x', 'y', 'vx', 'vy', 'life'}
score = 0
cooldown = 0
frame = 0
# Spawn initial rocks
def spawn_initial_rocks():
rocks.clear()
for _ in range(4):
# Spawn rocks away from center
rx = random.choice([random.uniform(20, px - 50), random.uniform(px + 50, width - 20)])
ry = random.choice([random.uniform(20, py - 50), random.uniform(py + 50, height - 20)])
rocks.append(make_rock(rx, ry, 24))
spawn_initial_rocks()
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
if check_touch(display, board_config):
print("Screensaver stopped by screen touch.")
break
# If no rocks are left, spawn more
if not rocks:
score += 1000
spawn_initial_rocks()
bullets.clear()
continue
# AI: Target the nearest rock
nearest_rock = None
min_dist = 999999
for r in rocks:
dx = r['x'] - px
dy = r['y'] - py
dist = dx*dx + dy*dy
if dist < min_dist:
min_dist = dist
nearest_rock = r
if nearest_rock:
tx = nearest_rock['x']
ty = nearest_rock['y']
target_theta = math.atan2(ty - py, tx - px)
# Rotate toward target
angle_diff = target_theta - theta
# Normalize angle to -pi..pi
while angle_diff > math.pi:
angle_diff -= 2 * math.pi
while angle_diff < -math.pi:
angle_diff += 2 * math.pi
if abs(angle_diff) < 0.08:
theta = target_theta
else:
theta += 0.08 if angle_diff > 0 else -0.08
# Shoot if aligned and off cooldown
if abs(angle_diff) < 0.25:
if cooldown == 0:
bx = px + 12 * math.cos(theta)
by = py + 12 * math.sin(theta)
bvx = 5.0 * math.cos(theta)
bvy = 5.0 * math.sin(theta)
bullets.append({'x': bx, 'y': by, 'vx': bvx, 'vy': bvy, 'life': 45})
cooldown = 15
if cooldown > 0:
cooldown -= 1
# Update rocks
for r in rocks:
r['x'] += r['vx']
r['y'] += r['vy']
# Wrap boundaries
rad = r['radius']
if r['x'] < -rad:
r['x'] = width + rad
elif r['x'] > width + rad:
r['x'] = -rad
if r['y'] < -rad:
r['y'] = height + rad
elif r['y'] > height + rad:
r['y'] = -rad
# Update bullets
next_bullets = []
for b in bullets:
b['x'] += b['vx']
b['y'] += b['vy']
b['life'] -= 1
# Wrap
if b['x'] < 0:
b['x'] = width
elif b['x'] > width:
b['x'] = 0
if b['y'] < 0:
b['y'] = height
elif b['y'] > height:
b['y'] = 0
# Collision with rocks
hit = False
for r in rocks:
rdx = b['x'] - r['x']
rdy = b['y'] - r['y']
if rdx*rdx + rdy*rdy < r['radius']*r['radius']:
hit = True
rocks.remove(r)
score += int(100 / r['radius'] * 10)
# Split rock
if r['radius'] > 8:
new_r = r['radius'] // 2
rocks.append(make_rock(r['x'], r['y'], new_r))
rocks.append(make_rock(r['x'], r['y'], new_r))
# Spawn particle sparks
for _ in range(6):
explosions.append({
'x': r['x'],
'y': r['y'],
'vx': random.uniform(-2, 2),
'vy': random.uniform(-2, 2),
'life': 12
})
break
if not hit and b['life'] > 0:
next_bullets.append(b)
bullets = next_bullets
# Update explosions
next_explosions = []
for p in explosions:
p['x'] += p['vx']
p['y'] += p['vy']
p['life'] -= 1
if p['life'] > 0:
next_explosions.append(p)
explosions = next_explosions
# Render frame
display.clear(0)
# Draw header
display.text(text if text else "ASTEROID DRIFT", 10, 10, 1)
display.text(f"SCORE: {score:05d}", width - 120, 10, 1)
display.line(0, 26, width, 26, 1)
# Draw ship (triangle)
fx = px + 12 * math.cos(theta)
fy = py + 12 * math.sin(theta)
blx = px + 8 * math.cos(theta + 2.5)
bly = py + 8 * math.sin(theta + 2.5)
brx = px + 8 * math.cos(theta - 2.5)
bry = py + 8 * math.sin(theta - 2.5)
display.line(int(fx), int(fy), int(blx), int(bly), 1)
display.line(int(blx), int(bly), int(brx), int(bry), 1)
display.line(int(brx), int(bry), int(fx), int(fy), 1)
# Draw small engine flame when firing
if cooldown > 10:
flame_tip_x = px - 12 * math.cos(theta) + random.uniform(-2, 2)
flame_tip_y = py - 12 * math.sin(theta) + random.uniform(-2, 2)
display.line(int(blx), int(bly), int(flame_tip_x), int(flame_tip_y), 1)
display.line(int(brx), int(bry), int(flame_tip_x), int(flame_tip_y), 1)
# Draw rocks
for r in rocks:
draw_rock(display, r)
# Draw bullets
for b in bullets:
display.fill_rect(int(b['x']) - 1, int(b['y']) - 1, 2, 2, 1)
# Draw particles
for p in explosions:
display.pixel(int(p['x']), int(p['y']), 1) if hasattr(display, 'pixel') else display.fill_rect(int(p['x']), int(p['y']), 1, 1, 1)
display.show()
frame += 1
sleep_ms(30)
except KeyboardInterrupt:
print("Screensaver stopped by user interrupt.")
if __name__ in ('__main__', '__name__'):
run()
+297
View File
@@ -0,0 +1,297 @@
# Standalone MicroPython Screensaver: Block Puzzle
# Inspired by falling-block puzzle classic. Exposes run(text="", frames=None).
import time
import math
import random
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def sleep_ms(ms):
try:
time.sleep_ms(ms)
except AttributeError:
time.sleep(ms / 1000.0)
def check_touch(d, bc):
"""Robust check for screen touch to stop the screensaver."""
if bc is not None:
touch_inst = getattr(bc, 'touch_instance', None)
if touch_inst is not None:
if hasattr(touch_inst, 'is_touched'):
try:
if touch_inst.is_touched():
return True
except Exception:
pass
for meth in ('get_touch', 'read_touch'):
if hasattr(touch_inst, meth):
try:
res = getattr(touch_inst, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
touch_val = getattr(bc, 'touch', None)
if touch_val is not None:
if callable(touch_val):
try:
res = touch_val()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
else:
if hasattr(touch_val, 'is_touched'):
try:
if touch_val.is_touched():
return True
except Exception:
pass
for meth in ('get_touch', 'read_touch'):
if hasattr(touch_val, meth):
try:
res = getattr(touch_val, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
if d is not None:
for meth in ('get_touch', 'touch'):
if hasattr(d, meth):
try:
res = getattr(d, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
return False
# Shapes relative coordinates
SHAPES = [
[(0, 0), (1, 0), (2, 0), (3, 0)], # I
[(0, 0), (1, 0), (0, 1), (1, 1)], # O
[(1, 0), (0, 1), (1, 1), (2, 1)], # T
[(0, 0), (0, 1), (1, 1), (2, 1)], # L
[(2, 0), (0, 1), (1, 1), (2, 1)], # J
[(1, 0), (2, 0), (0, 1), (1, 1)], # S
[(0, 0), (1, 0), (1, 1), (2, 1)] # Z
]
def rotate_coords(coords, rotation):
# Rotate coords clockwise
res = coords
for _ in range(rotation % 4):
res = [(-y, x) for x, y in res]
# Normalize coordinates so min x and min y are at least 0 (easier bounding)
min_x = min(x for x, y in res)
min_y = min(y for x, y in res)
return [(x - min_x, y - min_y) for x, y in res]
def get_piece_dims(coords):
w = max(x for x, y in coords) - min(x for x, y in coords) + 1
h = max(y for x, y in coords) - min(y for x, y in coords) + 1
return w, h
def run(text="", frames=None):
if not display:
print("board_config.display_instance is not available. Skipping screensaver.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
# Calculate grid sizing
cell_size = (height - 40) // 20
board_w = 10 * cell_size
board_h = 20 * cell_size
board_x = (width - board_w) // 2
board_y = (height - board_h) // 2
grid = [[0 for _ in range(10)] for _ in range(20)]
score = 0
lines_cleared = 0
# Game states
current_shape_idx = random.randint(0, len(SHAPES) - 1)
next_shape_idx = random.randint(0, len(SHAPES) - 1)
# Piece rotation & coords
piece_rot = 0
piece_coords = rotate_coords(SHAPES[current_shape_idx], piece_rot)
# Initial piece placement
pw, ph = get_piece_dims(piece_coords)
px = (10 - pw) // 2
py = 0
# Self-playing AI target
target_x = random.randint(0, 10 - pw)
target_rot = random.randint(0, 3)
frame = 0
game_over_delay = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
if check_touch(display, board_config):
print("Screensaver stopped by screen touch.")
break
# If in game over state, delay and restart
if game_over_delay > 0:
game_over_delay -= 1
if game_over_delay == 0:
grid = [[0 for _ in range(10)] for _ in range(20)]
score = 0
lines_cleared = 0
display.clear(0)
display.text("GAME OVER", width // 2 - 36, height // 2 - 4, 1)
display.show()
frame += 1
sleep_ms(30)
continue
# AI logic: adjust rotation and column position as block falls
# Try to rotate toward target rotation
if piece_rot != target_rot:
new_rot = (piece_rot + 1) % 4
new_coords = rotate_coords(SHAPES[current_shape_idx], new_rot)
npw, nph = get_piece_dims(new_coords)
# Keep position valid when rotating
npx = max(0, min(px, 10 - npw))
# Validate
valid = True
for dx, dy in new_coords:
tx, ty = npx + dx, py + dy
if ty >= 20 or tx < 0 or tx >= 10 or grid[ty][tx]:
valid = False
break
if valid:
piece_rot = new_rot
piece_coords = new_coords
px = npx
# Try to move toward target column
if px != target_x:
dx_step = 1 if target_x > px else -1
npx = px + dx_step
valid = True
for dx, dy in piece_coords:
tx, ty = npx + dx, py + dy
if ty >= 20 or tx < 0 or tx >= 10 or grid[ty][tx]:
valid = False
break
if valid:
px = npx
# Apply gravity every 4 frames (speeds up self-play nicely)
if frame % 4 == 0:
npy = py + 1
can_drop = True
for dx, dy in piece_coords:
tx, ty = px + dx, npy + dy
if ty >= 20 or grid[ty][tx]:
can_drop = False
break
if can_drop:
py = npy
else:
# Lock piece
for dx, dy in piece_coords:
grid[py + dy][px + dx] = 1
# Check lines
lines_this_turn = 0
for r in range(20):
if all(grid[r]):
del grid[r]
grid.insert(0, [0 for _ in range(10)])
lines_this_turn += 1
if lines_this_turn > 0:
lines_cleared += lines_this_turn
score += [0, 40, 100, 300, 1200][min(lines_this_turn, 4)]
# Spawn next piece
current_shape_idx = next_shape_idx
next_shape_idx = random.randint(0, len(SHAPES) - 1)
piece_rot = 0
piece_coords = rotate_coords(SHAPES[current_shape_idx], piece_rot)
pw, ph = get_piece_dims(piece_coords)
px = (10 - pw) // 2
py = 0
# Set next targets
target_x = random.randint(0, 10 - pw)
target_rot = random.randint(0, 3)
# Check game over (spawning block collides immediately)
for dx, dy in piece_coords:
if grid[py + dy][px + dx]:
game_over_delay = 60 # 2 seconds of game over display
break
# Draw game state
display.clear(0)
# Draw board border
display.rect(board_x - 2, board_y - 2, board_w + 4, board_h + 4, 1)
# Draw grid contents
for r in range(20):
for c in range(10):
cx_px = board_x + c * cell_size
cy_px = board_y + r * cell_size
if grid[r][c] == 1:
display.fill_rect(cx_px + 1, cy_px + 1, cell_size - 1, cell_size - 1, 1)
# Draw active piece
for dx, dy in piece_coords:
cx_px = board_x + (px + dx) * cell_size
cy_px = board_y + (py + dy) * cell_size
if 0 <= py + dy < 20:
display.fill_rect(cx_px + 1, cy_px + 1, cell_size - 1, cell_size - 1, 1)
# Add a small white dot in falling piece for visual difference
display.fill_rect(cx_px + cell_size//2, cy_px + cell_size//2, 2, 2, 0)
# Side panel (Left) - Title & Info
display.text(text if text else "FALLING", 10, 20, 1)
display.text("PUZZLE", 10, 32, 1)
display.text(f"SCORE:", 10, 80, 1)
display.text(f"{score:05d}", 10, 92, 1)
display.text(f"LINES:", 10, 130, 1)
display.text(f"{lines_cleared}", 10, 142, 1)
# Side panel (Right) - Next piece preview
preview_x = board_x + board_w + 20
display.text("NEXT:", preview_x, 20, 1)
display.rect(preview_x, 35, 50, 50, 1)
next_coords = rotate_coords(SHAPES[next_shape_idx], 0)
npw, nph = get_piece_dims(next_coords)
off_x = preview_x + (50 - npw * cell_size) // 2
off_y = 35 + (50 - nph * cell_size) // 2
for dx, dy in next_coords:
display.fill_rect(off_x + dx * cell_size + 1, off_y + dy * cell_size + 1, cell_size - 1, cell_size - 1, 1)
display.show()
frame += 1
sleep_ms(30)
except KeyboardInterrupt:
print("Screensaver stopped by user interrupt.")
if __name__ in ('__main__', '__name__'):
run()
+29
View File
@@ -0,0 +1,29 @@
{
"screensavers": [
{
"filename": "maze_chomper.py",
"inspired_by_style": "maze-chase arcade classic",
"purpose": "A wedge-shaped chomper navigates a grid maze eating dots while being chased by a ghost. If they collide, they reset after showing a collision warning."
},
{
"filename": "block_puzzle.py",
"inspired_by_style": "falling-block puzzle classic",
"purpose": "Falling tetromino blocks are auto-arranged by a self-playing heuristic, aligning and locking into a grid. Full lines are cleared, and a game over triggers a reset when the grid fills to the top."
},
{
"filename": "alien_invaders.py",
"inspired_by_style": "alien-wave shooter classic",
"purpose": "A grid of crawling invaders crawls across and down the screen. An automated player tank tracks the closest invaders and shoots them down, dodging falling enemy bombs."
},
{
"filename": "asteroid_drift.py",
"inspired_by_style": "vector-rock space classic",
"purpose": "A central vector-style triangular ship rotates to automatically track and shoot laser bullets at drifting wireframe rocks. Shot rocks split into smaller rocks and wrap around borders."
},
{
"filename": "paddle_bounce.py",
"inspired_by_style": "paddle-ball arcade classic",
"purpose": "Two automated paddles play table tennis with a bouncing ball that speeds up with each hit. Paddles track the ball with minor errors to allow points to be scored."
}
]
}
+289
View File
@@ -0,0 +1,289 @@
# Standalone MicroPython Screensaver: Maze Chomper
# Inspired by maze-chase arcade classic. Exposes run(text="", frames=None).
import time
import math
import random
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def sleep_ms(ms):
try:
time.sleep_ms(ms)
except AttributeError:
time.sleep(ms / 1000.0)
def check_touch(d, bc):
"""Robust check for screen touch to stop the screensaver."""
if bc is not None:
touch_inst = getattr(bc, 'touch_instance', None)
if touch_inst is not None:
if hasattr(touch_inst, 'is_touched'):
try:
if touch_inst.is_touched():
return True
except Exception:
pass
for meth in ('get_touch', 'read_touch'):
if hasattr(touch_inst, meth):
try:
res = getattr(touch_inst, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
touch_val = getattr(bc, 'touch', None)
if touch_val is not None:
if callable(touch_val):
try:
res = touch_val()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
else:
if hasattr(touch_val, 'is_touched'):
try:
if touch_val.is_touched():
return True
except Exception:
pass
for meth in ('get_touch', 'read_touch'):
if hasattr(touch_val, meth):
try:
res = getattr(touch_val, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
if d is not None:
for meth in ('get_touch', 'touch'):
if hasattr(d, meth):
try:
res = getattr(d, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
return False
def random_choice(seq):
if not seq:
return None
return seq[random.randint(0, len(seq) - 1)]
def draw_chomper(d, cx, cy, rad, direction, mouth_open):
# Draw body
for dy in range(-rad, rad + 1):
dx = int(math.sqrt(rad*rad - dy*dy))
d.line(cx - dx, cy + dy, cx + dx, cy + dy, 1)
# Draw open mouth
if mouth_open:
dx_dir, dy_dir = direction
if dx_dir == 0 and dy_dir == 0:
dx_dir = 1
if dx_dir > 0:
for dy in range(-rad//2, rad//2 + 1):
mx = rad - abs(dy) * 2
d.line(cx, cy + dy, cx + mx, cy + dy, 0)
elif dx_dir < 0:
for dy in range(-rad//2, rad//2 + 1):
mx = rad - abs(dy) * 2
d.line(cx - mx, cy + dy, cx, cy + dy, 0)
elif dy_dir > 0:
for dx in range(-rad//2, rad//2 + 1):
my = rad - abs(dx) * 2
d.line(cx + dx, cy, cx + dx, cy + my, 0)
elif dy_dir < 0:
for dx in range(-rad//2, rad//2 + 1):
my = rad - abs(dx) * 2
d.line(cx + dx, cy - my, cx + dx, cy, 0)
def draw_ghost(d, gcx, gcy, rad):
for dy in range(-rad, 0):
dx = int(math.sqrt(rad*rad - dy*dy))
d.line(gcx - dx, gcy + dy, gcx + dx, gcy + dy, 1)
d.fill_rect(gcx - rad, gcy, rad * 2 + 1, rad, 1)
eye_offset = rad // 3
d.fill_rect(gcx - eye_offset - 1, gcy - eye_offset, 2, 2, 0)
d.fill_rect(gcx + eye_offset - 1, gcy - eye_offset, 2, 2, 0)
for i in range(gcx - rad, gcx + rad + 1, 4):
d.fill_rect(i, gcy + rad - 2, 2, 3, 0)
def run(text="", frames=None):
if not display:
print("board_config.display_instance is not available. Skipping screensaver.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
header_h = 30
grid_y_offset = header_h
cell_size = 40
cols = width // cell_size
rows = (height - header_h) // cell_size
grid = []
for r in range(rows):
row_data = []
for c in range(cols):
is_wall = False
if r == 0 or r == rows - 1 or c == 0 or c == cols - 1:
is_wall = True
elif (r == 2 or r == rows - 3) and (2 <= c <= 3 or cols - 4 <= c <= cols - 3):
is_wall = True
elif r == rows // 2 and (cols // 2 - 1 <= c <= cols // 2 + 1):
is_wall = True
row_data.append(2 if is_wall else 1)
grid.append(row_data)
score = 0
chomper_x, chomper_y = cell_size, grid_y_offset + cell_size
chomper_dx, chomper_dy = 1, 0
ghost_x, ghost_y = (cols - 2) * cell_size, grid_y_offset + (rows - 2) * cell_size
ghost_dx, ghost_dy = -1, 0
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
if check_touch(display, board_config):
print("Screensaver stopped by screen touch.")
break
chomper_rel_y = chomper_y - grid_y_offset
if chomper_x % cell_size == 0 and chomper_rel_y % cell_size == 0:
cx, cy = chomper_x // cell_size, chomper_rel_y // cell_size
if grid[cy][cx] == 1:
grid[cy][cx] = 0
score += 10
has_dots = False
for r_data in grid:
if 1 in r_data:
has_dots = True
break
if not has_dots:
for r in range(rows):
for c in range(cols):
if grid[r][c] == 0:
grid[r][c] = 1
valid_dirs = []
dot_dirs = []
for ndx, ndy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
nx, ny = cx + ndx, cy + ndy
if 0 <= nx < cols and 0 <= ny < rows:
if grid[ny][nx] != 2:
valid_dirs.append((ndx, ndy))
if grid[ny][nx] == 1:
dot_dirs.append((ndx, ndy))
if dot_dirs:
if (chomper_dx, chomper_dy) in dot_dirs:
pass
else:
chomper_dx, chomper_dy = random_choice(dot_dirs)
elif valid_dirs:
if (chomper_dx, chomper_dy) in valid_dirs and random.random() > 0.3:
pass
else:
chomper_dx, chomper_dy = random_choice(valid_dirs)
else:
chomper_dx, chomper_dy = 0, 0
ghost_rel_y = ghost_y - grid_y_offset
if ghost_x % cell_size == 0 and ghost_rel_y % cell_size == 0:
gcx, gcy = ghost_x // cell_size, ghost_rel_y // cell_size
valid_gdirs = []
for ndx, ndy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
if ndx == -ghost_dx and ndy == -ghost_dy:
continue
nx, ny = gcx + ndx, gcy + ndy
if 0 <= nx < cols and 0 <= ny < rows:
if grid[ny][nx] != 2:
valid_gdirs.append((ndx, ndy))
if not valid_gdirs:
valid_gdirs = [(-ghost_dx, -ghost_dy)]
best_dir = valid_gdirs[0]
min_dist = 999999
ccx, ccy = chomper_x // cell_size, chomper_rel_y // cell_size
for ndx, ndy in valid_gdirs:
nx, ny = gcx + ndx, gcy + ndy
dist = (nx - ccx)**2 + (ny - ccy)**2
if dist < min_dist:
min_dist = dist
best_dir = (ndx, ndy)
if random.random() < 0.25:
ghost_dx, ghost_dy = random_choice(valid_gdirs)
else:
ghost_dx, ghost_dy = best_dir
chomper_x += chomper_dx * 2
chomper_y += chomper_dy * 2
ghost_x += ghost_dx * 2
ghost_y += ghost_dy * 2
dist_sq = (chomper_x - ghost_x)**2 + (chomper_y - ghost_y)**2
if dist_sq < 256:
display.clear(0)
display.text("COLLISION!", width // 2 - 40, height // 2 - 10, 1)
display.show()
sleep_ms(800)
chomper_x, chomper_y = cell_size, grid_y_offset + cell_size
chomper_dx, chomper_dy = 1, 0
ghost_x, ghost_y = (cols - 2) * cell_size, grid_y_offset + (rows - 2) * cell_size
ghost_dx, ghost_dy = -1, 0
frame += 1
continue
display.clear(0)
# Header
display.text(text if text else "MAZE CHOMPER", 10, 10, 1)
display.text(f"SCORE: {score:05d}", width - 120, 10, 1)
display.line(0, 28, width, 28, 1)
# Grid / Walls / Dots
for r in range(rows):
for c in range(cols):
cell_val = grid[r][c]
cx_px = c * cell_size
cy_px = grid_y_offset + r * cell_size
if cell_val == 2:
display.fill_rect(cx_px + 2, cy_px + 2, cell_size - 4, cell_size - 4, 1)
elif cell_val == 1:
display.fill_rect(cx_px + cell_size//2 - 1, cy_px + cell_size//2 - 1, 3, 3, 1)
# Sprites
mouth_open = (frame % 4) < 2
draw_chomper(display, chomper_x + cell_size//2, chomper_y + cell_size//2, 12, (chomper_dx, chomper_dy), mouth_open)
draw_ghost(display, ghost_x + cell_size//2, ghost_y + cell_size//2, 12)
display.show()
frame += 1
sleep_ms(30)
except KeyboardInterrupt:
print("Screensaver stopped by user interrupt.")
if __name__ in ('__main__', '__name__'):
run()
+223
View File
@@ -0,0 +1,223 @@
# Standalone MicroPython Screensaver: Paddle Bounce
# Inspired by paddle-ball arcade classic. Exposes run(text="", frames=None).
import time
import math
import random
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def sleep_ms(ms):
try:
time.sleep_ms(ms)
except AttributeError:
time.sleep(ms / 1000.0)
def check_touch(d, bc):
"""Robust check for screen touch to stop the screensaver."""
if bc is not None:
touch_inst = getattr(bc, 'touch_instance', None)
if touch_inst is not None:
if hasattr(touch_inst, 'is_touched'):
try:
if touch_inst.is_touched():
return True
except Exception:
pass
for meth in ('get_touch', 'read_touch'):
if hasattr(touch_inst, meth):
try:
res = getattr(touch_inst, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
touch_val = getattr(bc, 'touch', None)
if touch_val is not None:
if callable(touch_val):
try:
res = touch_val()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
else:
if hasattr(touch_val, 'is_touched'):
try:
if touch_val.is_touched():
return True
except Exception:
pass
for meth in ('get_touch', 'read_touch'):
if hasattr(touch_val, meth):
try:
res = getattr(touch_val, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
if d is not None:
for meth in ('get_touch', 'touch'):
if hasattr(d, meth):
try:
res = getattr(d, meth)()
if res is not None and res is not False and res != 0:
return True
except Exception:
pass
return False
def run(text="", frames=None):
if not display:
print("board_config.display_instance is not available. Skipping screensaver.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
# Paddle parameters
pad_w = 6
pad_h = 36
left_x = 15
right_x = width - 15 - pad_w
# Initial states
ly = (height - pad_h) // 2
ry = (height - pad_h) // 2
bx, by = width // 2, height // 2
vx, vy = 3.5, 1.8
score_l = 0
score_r = 0
paddle_speed = 3.0
# Simulated tracking error offset
target_offset_l = 0
target_offset_r = 0
def reset_ball(serve_to_left):
nonlocal bx, by, vx, vy, target_offset_l, target_offset_r
bx = width // 2
by = height // 2
vx = -3.5 if serve_to_left else 3.5
vy = random.choice([-2.0, -1.0, 1.0, 2.0])
target_offset_l = random.randint(-12, 12)
target_offset_r = random.randint(-12, 12)
frame = 0
serve_delay = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
if check_touch(display, board_config):
print("Screensaver stopped by screen touch.")
break
if serve_delay > 0:
serve_delay -= 1
else:
# Move ball
bx += vx
by += vy
# Ceiling/floor bounce
if by <= 32:
by = 32
vy = -vy
elif by >= height - 8:
by = height - 8
vy = -vy
# AI tracking
if vx < 0: # Ball heading Left
# Track ball center with error offset
target_y = by + 3 - (pad_h // 2) + target_offset_l
if ly < target_y:
ly = min(height - pad_h - 6, ly + paddle_speed)
elif ly > target_y:
ly = max(32, ly - paddle_speed)
else: # Ball heading Right
target_y = by + 3 - (pad_h // 2) + target_offset_r
if ry < target_y:
ry = min(height - pad_h - 6, ry + paddle_speed)
elif ry > target_y:
ry = max(32, ry - paddle_speed)
# Check Left boundary
if bx <= left_x + pad_w:
if left_x <= bx:
# Ball within x-range of paddle, check y-collision
if ly - 3 <= by <= ly + pad_h + 3:
bx = left_x + pad_w
# Bounce & increase speed
vx = -vx * 1.05
# Cap horizontal speed to prevent teleporting
if abs(vx) > 8.0:
vx = 8.0 if vx > 0 else -8.0
vy = vy * 1.05 + random.uniform(-0.6, 0.6)
# Assign new tracking error for right paddle
target_offset_r = random.randint(-15, 15)
else:
# Miss! Point for Right
score_r += 1
reset_ball(serve_to_left=False)
serve_delay = 40
# Check Right boundary
if bx + 6 >= right_x:
if bx <= right_x + pad_w:
# Ball within x-range of paddle, check y-collision
if ry - 3 <= by <= ry + pad_h + 3:
bx = right_x - 6
# Bounce & increase speed
vx = -vx * 1.05
if abs(vx) > 8.0:
vx = 8.0 if vx > 0 else -8.0
vy = vy * 1.05 + random.uniform(-0.6, 0.6)
# Assign new tracking error for left paddle
target_offset_l = random.randint(-15, 15)
else:
# Miss! Point for Left
score_l += 1
reset_ball(serve_to_left=True)
serve_delay = 40
# Draw frame
display.clear(0)
# Header score line
display.text(text if text else "PADDLE BOUNCE", 10, 10, 1)
display.text(f"{score_l:02d} {score_r:02d}", width // 2 - 24, 10, 1)
display.line(0, 26, width, 26, 1)
# Draw center dotted line
for y in range(30, height, 16):
display.fill_rect(width // 2 - 1, y, 2, 8, 1)
# Draw paddles
display.fill_rect(left_x, int(ly), pad_w, pad_h, 1)
display.fill_rect(right_x, int(ry), pad_w, pad_h, 1)
# Draw ball (6x6 square)
if serve_delay == 0 or (serve_delay // 4) % 2 == 0:
display.fill_rect(int(bx), int(by), 6, 6, 1)
display.show()
frame += 1
sleep_ms(30)
except KeyboardInterrupt:
print("Screensaver stopped by user interrupt.")
if __name__ in ('__main__', '__name__'):
run()
+70
View File
@@ -0,0 +1,70 @@
# Reyna little32 ESP32-S3 Voice State Animations
A collection of 14 standalone MicroPython animation scripts designed for the Reyna little32 ESP32-S3 reflective screen board. These animations support voice gateway visual states instead of a spoken initial acknowledgement (ACK).
## Hardware & API Assumptions
- **Display Size:** Auto-scaling (reads `display.width` and `display.height`; defaults to 400x300 for RLCD, but supports others).
- **Colors:** Designed for monochrome-ish 0 (White/Background) and 1 (Black/Foreground) high-contrast visuals.
- **Methods Used:** `clear(c)`, `fill_rect(x,y,w,h,c)`, `rect(x,y,w,h,c)`, `line(x1,y1,x2,y2,c)`, `text(msg,x,y,c)`, `show()`.
- **Standalone:** Each script is entirely self-contained. It can be uploaded individually to board flash and requires no helper modules.
- **Robustness:** Gracefully falls back and prints a warning if `board_config` or `board_config.display_instance` is missing.
---
## Animation List & State Mappings
| Filename | Voice States / Purposes | Visual Behavior |
| :--- | :--- | :--- |
| **[ack_pulse.py](file:///home/adolforeyna/Projects/Screen/voice_animations/ack_pulse.py)** | `ack`, `received`, `got_it` | Bold checkmark with expanding concentric radar pulses. |
| **[listening_wave.py](file:///home/adolforeyna/Projects/Screen/voice_animations/listening_wave.py)** | `listening`, `audio_input` | A decorative mic icon at the top and dual active overlapping sine waves. |
| **[caption_scan.py](file:///home/adolforeyna/Projects/Screen/voice_animations/caption_scan.py)** | `captioning`, `transcribing` | Simulated typing log with scanner sweeps and blinking typewriter cursors. |
| **[waiting_orbit.py](file:///home/adolforeyna/Projects/Screen/voice_animations/waiting_orbit.py)** | `waiting`, `thinking`, `processing` | 4 circular ring nodes orbiting a central pulsing energy reactor core. |
| **[working_gears.py](file:///home/adolforeyna/Projects/Screen/voice_animations/working_gears.py)** | `working`, `tool_use`, `executing` | Large and small mechanical gears interlocking and rotating in opposite directions. |
| **[speaking_mouth.py](file:///home/adolforeyna/Projects/Screen/voice_animations/speaking_mouth.py)** | `speaking`, `audio_output` | Equalizer voiceprint spectrum bars bouncing symmetrically from a center axis. |
| **[success_spark.py](file:///home/adolforeyna/Projects/Screen/voice_animations/success_spark.py)** | `success`, `done`, `completed` | Bold checkmark inside a blooming radial starburst explosion. |
| **[error_alert.py](file:///home/adolforeyna/Projects/Screen/voice_animations/error_alert.py)** | `error`, `failure`, `alert` | Flashing bold exclamation triangle with diagonal warning hazard stripes. |
| **[network_retry.py](file:///home/adolforeyna/Projects/Screen/voice_animations/network_retry.py)** | `network_retry`, `reconnecting` | Wi-Fi waves lighting up sequentially inside looping dashed reload arrows. |
| **[battery_low.py](file:///home/adolforeyna/Projects/Screen/voice_animations/battery_low.py)** | `battery_low`, `low_power` | Battery outline with tips, flashing empty charge block, and warning triangle. |
| **[update_progress.py](file:///home/adolforeyna/Projects/Screen/voice_animations/update_progress.py)** | `update`, `uploading`, `downloading` | Sliding download arrow dropping into a tray above a percentage progress bar. |
| **[breathing_idle.py](file:///home/adolforeyna/Projects/Screen/voice_animations/breathing_idle.py)** | `idle`, `ready`, `breathing` | Concentric squares and accent dots scaling slowly in a calm breathing rhythm. |
| **[wake_attention.py](file:///home/adolforeyna/Projects/Screen/voice_animations/wake_attention.py)** | `wake`, `attention`, `active` | Stylized robot eyes blinking and shifting gaze left and right to look around. |
| **[kid_companion.py](file:///home/adolforeyna/Projects/Screen/voice_animations/kid_companion.py)** | `kid_friendly`, `companion` | Friendly robot head with wiggling ears, a winking eye, and a warm smile. |
---
## Execution Guide
### 1. Uploading to Flash
You can upload the desired animation files using tools like `ampy`, `rshell`, `mpremote`, or via the board's web console. E.g.
```bash
mpremote cp voice_animations/*.py :
```
### 2. Execution from REPL (Script Mode)
Run any script automatically with default parameters using `exec`:
```python
# Runs for 100 frames (approx. 5 seconds) and exits
exec(open("ack_pulse.py").read())
```
### 3. Importing and Running (Module Mode)
You can import the module and call its `run` method directly. The `run` method supports text customization and infinite looping:
```python
import listening_wave
# Run with custom caption text for 50 frames
listening_wave.run(text="HOTWORD CAPTURED", frames=50)
# Run indefinitely (blocking loop, break with Ctrl+C)
listening_wave.run(text="READY TO HEAR", frames=-1)
```
---
## Technical Details
### Customizing Loop Duration
In the `run()` function:
- Set `frames > 0` to run for a finite duration (useful for transition ACK animations).
- Set `frames=None` or `frames=-1` (or omit) to run indefinitely inside a `while True` loop.
- All scripts catch `KeyboardInterrupt` to exit gracefully, leaving the screen in a clean state if interrupted.
+68
View File
@@ -0,0 +1,68 @@
# Standalone MicroPython Voice Animation: ACK / Received
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def draw_center_text(d, msg, y, w, scale=1, c=1):
char_w = 8 * scale
x = max(0, (w - len(msg) * char_w) // 2)
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale, c)
return
except Exception:
pass
d.text(msg, x, y, c)
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
label = text if text else "GOT IT"
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Draw a thick checkmark in the center
display.line(cx - 24, cy + 4, cx - 6, cy + 22, 1)
display.line(cx - 23, cy + 4, cx - 5, cy + 22, 1)
display.line(cx - 6, cy + 22, cx + 30, cy - 14, 1)
display.line(cx - 5, cy + 22, cx + 31, cy - 14, 1)
# Concentric pulsing squares radiating outwards
for p in range(3):
# Scale pulse radius based on frame count
r = 30 + ((frame * 4 + p * 30) % 90)
display.rect(cx - r, cy - r, r * 2, r * 2, 1)
# Draw centered text label at the bottom
draw_center_text(display, label, height - 30, width, scale=1, c=1)
display.show()
frame += 1
try:
time.sleep_ms(50)
except AttributeError:
time.sleep(0.05)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()
+91
View File
@@ -0,0 +1,91 @@
# Standalone MicroPython Voice Animation: Battery Low
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def draw_center_text(d, msg, y, w, scale=1, c=1):
char_w = 8 * scale
x = max(0, (w - len(msg) * char_w) // 2)
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale, c)
return
except Exception:
pass
d.text(msg, x, y, c)
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
label = text if text else "LOW BATTERY!"
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Center coordinates adjusted for warning triangle on the left side
# Battery offset to the right
bat_x = cx + 20
bat_y = cy - 10
# Draw battery container outline
display.rect(bat_x - 65, bat_y - 30, 115, 60, 1)
display.rect(bat_x - 64, bat_y - 29, 113, 58, 1) # thicker
# Battery cap tip
display.fill_rect(bat_x + 50, bat_y - 12, 10, 24, 1)
# Low charge blinking segment on the left
blink_on = (frame // 4) % 2 == 0
if blink_on:
# Draw small low charge bar
display.fill_rect(bat_x - 58, bat_y - 23, 25, 46, 1)
# Draw a warning triangle on the left
warn_x = cx - 110
warn_y = cy - 10
# Warning triangle outline
display.line(warn_x, warn_y - 25, warn_x - 25, warn_y + 20, 1)
display.line(warn_x, warn_y - 25, warn_x + 25, warn_y + 20, 1)
display.line(warn_x - 25, warn_y + 20, warn_x + 25, warn_y + 20, 1)
# Warning exclamation mark
display.fill_rect(warn_x - 2, warn_y - 8, 4, 16, 1)
display.fill_rect(warn_x - 2, warn_y + 11, 4, 4, 1)
# Draw diagonal warning grid lines in battery background if empty to look stylish
if not blink_on:
for diag in range(-20, 40, 10):
display.line(bat_x - 58, bat_y - 23 + diag, bat_x - 38, bat_y + 23 + diag, 1)
# Draw centered text label at the bottom
draw_center_text(display, label, height - 30, width, scale=1, c=1)
display.show()
frame += 1
try:
time.sleep_ms(60)
except AttributeError:
time.sleep(0.06)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()
+80
View File
@@ -0,0 +1,80 @@
# Standalone MicroPython Voice Animation: Breathing Idle
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
import math
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def draw_center_text(d, msg, y, w, scale=1, c=1):
char_w = 8 * scale
x = max(0, (w - len(msg) * char_w) // 2)
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale, c)
return
except Exception:
pass
d.text(msg, x, y, c)
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
label = text if text else "READY"
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Cosine-based breathing curve: slow and smooth, scaling from 0 to 1 and back
breath_scale = 0.5 + 0.5 * math.cos(frame * 0.07)
# Draw three concentric breathing boxes
# Base radii are scaled dynamically
r1 = int(24 + 32 * breath_scale)
r2 = int(14 + 18 * breath_scale)
r3 = int(6 + 8 * breath_scale)
# Outer box
display.rect(cx - r1, cy - r1, r1 * 2, r1 * 2, 1)
# Middle box
display.rect(cx - r2, cy - r2, r2 * 2, r2 * 2, 1)
# Inner solid core
display.fill_rect(cx - r3, cy - r3, r3 * 2, r3 * 2, 1)
# Draw small accent dots at the corners of the outer breathing box
accent = r1 + 8
display.fill_rect(cx - accent - 2, cy - accent - 2, 4, 4, 1)
display.fill_rect(cx + accent - 2, cy - accent - 2, 4, 4, 1)
display.fill_rect(cx - accent - 2, cy + accent - 2, 4, 4, 1)
display.fill_rect(cx + accent - 2, cy + accent - 2, 4, 4, 1)
# Draw centered text label at the bottom
draw_center_text(display, label, height - 30, width, scale=1, c=1)
display.show()
frame += 1
try:
time.sleep_ms(65)
except AttributeError:
time.sleep(0.065)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()
+111
View File
@@ -0,0 +1,111 @@
# Standalone MicroPython Voice Animation: Caption Scan
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def wrap_text(msg, max_chars=36):
words = msg.split()
lines = []
curr = ""
for w in words:
if len(curr) + len(w) + 1 > max_chars:
lines.append(curr)
curr = w
else:
curr = (curr + " " + w).strip()
if curr:
lines.append(curr)
return lines
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
# Simulated text sequence if no real-time caption text is supplied
default_script = [
"RECEIVING VOICE PACKETS...",
"DECODING AUDIO STREAM...",
"MATCHING PHONEME VECTORS...",
"EXTRACTING INTENT AND CONTEXT...",
"STT METRICS: LATENCY 42MS",
"TRANSCRIPTION STABILIZED.",
"AWAITING PIPELINE RESPONSE..."
]
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Draw outer scan framing
display.rect(15, 15, width - 30, height - 50, 1)
# Draw moving scanner bar line
scan_y = 20 + ((frame * 6) % (height - 60))
display.line(20, scan_y, width - 20, scan_y, 1)
# Draw static top status line
display.text("SPEECH-TO-TEXT DECODER", 25, 25, 1)
display.line(25, 37, width - 25, 37, 1)
# Determine what text to write based on typewriter frame counter
if text:
lines = wrap_text(text, max_chars=(width - 50) // 8)
char_limit = int(frame * 2.5)
chars_drawn = 0
for idx, line in enumerate(lines):
if chars_drawn >= char_limit:
break
line_to_draw = line[:char_limit - chars_drawn]
display.text(line_to_draw, 25, 55 + idx * 18, 1)
chars_drawn += len(line)
else:
# Typewriter animation of simulated lines
active_line_idx = (frame // 15) % len(default_script)
char_limit = int((frame % 15) * 3)
# Show previously printed lines in full
start_line = max(0, active_line_idx - 5)
for idx in range(start_line, active_line_idx):
display.text(default_script[idx], 25, 55 + (idx - start_line) * 18, 1)
# Typewriter effect on the current line
curr_line_text = default_script[active_line_idx][:char_limit]
display.text(curr_line_text, 25, 55 + (active_line_idx - start_line) * 18, 1)
# Flash a cursor at the end of the typing line
if (frame // 3) % 2 == 0:
cursor_x = 25 + len(curr_line_text) * 8
cursor_y = 55 + (active_line_idx - start_line) * 18
display.fill_rect(cursor_x, cursor_y, 8, 8, 1)
# Draw bottom transcription indicator
display.text("TRANSCRIBING...", 25, height - 25, 1)
display.show()
frame += 1
try:
time.sleep_ms(60)
except AttributeError:
time.sleep(0.06)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()
+95
View File
@@ -0,0 +1,95 @@
# Standalone MicroPython Voice Animation: Error Alert
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def draw_center_text(d, msg, y, w, scale=1, c=1):
char_w = 8 * scale
x = max(0, (w - len(msg) * char_w) // 2)
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale, c)
return
except Exception:
pass
d.text(msg, x, y, c)
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
label = text if text else "ERROR OCCURRED"
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Blink animation state
# Flashes between standard warning symbol and high-contrast double border
blink_state = (frame // 4) % 2
# Base warning triangle dimensions
ty_top = cy - 40
ty_bottom = cy + 30
tx_left = cx - 45
tx_right = cx + 45
if blink_state == 0:
# Draw single bold warning triangle
display.line(cx, ty_top, tx_left, ty_bottom, 1)
display.line(cx, ty_top, tx_right, ty_bottom, 1)
display.line(tx_left, ty_bottom, tx_right, ty_bottom, 1)
else:
# Draw double thick warning triangle for "flashing" impact
display.line(cx, ty_top - 3, tx_left - 4, ty_bottom + 2, 1)
display.line(cx, ty_top - 3, tx_right + 4, ty_bottom + 2, 1)
display.line(tx_left - 4, ty_bottom + 2, tx_right + 4, ty_bottom + 2, 1)
display.line(cx, ty_top, tx_left, ty_bottom, 1)
display.line(cx, ty_top, tx_right, ty_bottom, 1)
display.line(tx_left, ty_bottom, tx_right, ty_bottom, 1)
# Draw Exclamation mark inside the triangle
# Exclamation bar
display.fill_rect(cx - 3, cy - 15, 6, 22, 1)
# Exclamation dot
display.fill_rect(cx - 3, cy + 13, 6, 6, 1)
# Draw warning stripes on the left and right edges of the screen
stripe_y = (frame * 3) % 20
for sy in range(-20, height + 20, 20):
# Left stripe
display.line(5, sy + stripe_y, 15, sy + stripe_y + 10, 1)
# Right stripe
display.line(width - 15, sy + stripe_y, width - 5, sy + stripe_y + 10, 1)
# Draw centered text label at the bottom
draw_center_text(display, label, height - 30, width, scale=1, c=1)
display.show()
frame += 1
try:
time.sleep_ms(60)
except AttributeError:
time.sleep(0.06)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()
+109
View File
@@ -0,0 +1,109 @@
# Standalone MicroPython Voice Animation: Kid Companion Robot
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
import math
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def draw_center_text(d, msg, y, w, scale=1, c=1):
char_w = 8 * scale
x = max(0, (w - len(msg) * char_w) // 2)
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale, c)
return
except Exception:
pass
d.text(msg, x, y, c)
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
label = text if text else "HELLO BUDDY!"
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Robot head main box
head_y = cy - 15
display.rect(cx - 50, head_y - 40, 100, 80, 1)
display.rect(cx - 49, head_y - 39, 98, 78, 1) # thicker
# Neck
display.fill_rect(cx - 10, head_y + 40, 20, 12, 1)
# Ears wiggling dynamically using sin waves
ear_offset = int(5 * math.sin(frame * 0.25))
# Left ear
display.rect(cx - 62, head_y - 15 + ear_offset, 12, 30, 1)
display.line(cx - 62, head_y + ear_offset, cx - 50, head_y, 1)
# Right ear
display.rect(cx + 50, head_y - 15 - ear_offset, 12, 30, 1)
display.line(cx + 62, head_y - ear_offset, cx + 50, head_y, 1)
# Antenna extending from head top
display.line(cx, head_y - 40, cx, head_y - 58, 1)
display.line(cx - 1, head_y - 40, cx - 1, head_y - 58, 1)
# Antenna bulb winks (flashes)
bulb_on = (frame // 4) % 2 == 0
if bulb_on:
display.fill_rect(cx - 6, head_y - 69, 12, 12, 1)
else:
display.rect(cx - 6, head_y - 69, 12, 12, 1)
# Eyes
eye_y = head_y - 12
# Left Eye (always open, cute square with highlight)
display.rect(cx - 32, eye_y - 8, 16, 16, 1)
display.fill_rect(cx - 28, eye_y - 4, 8, 8, 1)
display.fill_rect(cx - 26, eye_y - 2, 3, 3, 0) # highlight
# Right Eye winks every 12 frames
is_winking = (frame // 10) % 2 == 1
if is_winking:
# Wink line (happy curved line)
display.line(cx + 12, eye_y, cx + 20, eye_y + 4, 1)
display.line(cx + 20, eye_y + 4, cx + 28, eye_y, 1)
else:
# Open right eye
display.rect(cx + 16, eye_y - 8, 16, 16, 1)
display.fill_rect(cx + 20, eye_y - 4, 8, 8, 1)
display.fill_rect(cx + 22, eye_y - 2, 3, 3, 0) # highlight
# Cute happy smile mouth
display.line(cx - 16, head_y + 16, cx + 16, head_y + 16, 1)
display.line(cx - 16, head_y + 16, cx - 20, head_y + 10, 1)
display.line(cx + 16, head_y + 16, cx + 20, head_y + 10, 1)
# Draw centered text label at the bottom
draw_center_text(display, label, height - 30, width, scale=1, c=1)
display.show()
frame += 1
try:
time.sleep_ms(60)
except AttributeError:
time.sleep(0.06)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()
+88
View File
@@ -0,0 +1,88 @@
# Standalone MicroPython Voice Animation: Listening Wave
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
import math
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def draw_center_text(d, msg, y, w, scale=1, c=1):
char_w = 8 * scale
x = max(0, (w - len(msg) * char_w) // 2)
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale, c)
return
except Exception:
pass
d.text(msg, x, y, c)
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
label = text if text else "LISTENING"
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Draw a microphone icon at the top-center
mic_y = 35
display.rect(cx - 8, mic_y, 16, 22, 1) # Capsule
display.line(cx - 12, mic_y + 10, cx - 12, mic_y + 18, 1) # Cup left
display.line(cx - 12, mic_y + 18, cx + 12, mic_y + 18, 1) # Cup bottom
display.line(cx + 12, mic_y + 18, cx + 12, mic_y + 10, 1) # Cup right
display.line(cx, mic_y + 22, cx, mic_y + 28, 1) # Stand stem
display.line(cx - 10, mic_y + 28, cx + 10, mic_y + 28, 1) # Stand base
# Draw overlapping sine waves simulating microphone input
# Adjust wave amplitude with a breathing factor
breath = 0.7 + 0.3 * math.sin(frame * 0.15)
# Draw Wave 1 (larger, main wave)
prev_y1 = cy
for x in range(30, width - 30, 4):
val = math.sin((x + frame * 10) * 0.035) * math.sin(frame * 0.08)
y1 = cy + int(breath * 35 * val)
if x > 30:
display.line(x - 4, prev_y1, x, y1, 1)
prev_y1 = y1
# Draw Wave 2 (smaller, high frequency wave)
prev_y2 = cy
for x in range(30, width - 30, 4):
val = math.sin((x - frame * 14) * 0.07) * math.cos(frame * 0.1)
y2 = cy + int(breath * 18 * val)
if x > 30:
display.line(x - 4, prev_y2, x, y2, 1)
prev_y2 = y2
# Draw centered text label at the bottom
draw_center_text(display, label, height - 30, width, scale=1, c=1)
display.show()
frame += 1
try:
time.sleep_ms(50)
except AttributeError:
time.sleep(0.05)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()
+74
View File
@@ -0,0 +1,74 @@
{
"voice_animations": [
{
"filename": "ack_pulse.py",
"states": ["ack", "received", "got_it"],
"description": "Expanding concentric pulse squares around a central bold checkmark, representing an initial command receipt ACK."
},
{
"filename": "listening_wave.py",
"states": ["listening", "audio_input"],
"description": "An active dual-sinusoid voice waveform and microphone outline indicating active microphone capture."
},
{
"filename": "caption_scan.py",
"states": ["captioning", "transcribing", "speech_to_text"],
"description": "Real-time horizontal text line simulator with a typewriter character printer and vertical scanner sweep."
},
{
"filename": "waiting_orbit.py",
"states": ["waiting", "thinking", "processing"],
"description": "Orbital loading nodes revolving around a central pulsing engine core, indicating wait/server processing state."
},
{
"filename": "working_gears.py",
"states": ["working", "tool_use", "executing"],
"description": "Interlocking gears rotating in opposite directions, indicating background computation or external tool invocation."
},
{
"filename": "speaking_mouth.py",
"states": ["speaking", "audio_output", "text_to_speech"],
"description": "Dynamic equalizer audio spectrum bars bouncing from a center axis, representing voice response playback."
},
{
"filename": "success_spark.py",
"states": ["success", "done", "completed"],
"description": "A blooming starburst sparkle expansion and central success checkmark, representing task success."
},
{
"filename": "error_alert.py",
"states": ["error", "failure", "alert"],
"description": "A bold warning exclamation triangle flashing alongside warning stripes, representing pipeline or connection error."
},
{
"filename": "network_retry.py",
"states": ["network_retry", "reconnecting", "searching"],
"description": "Wi-Fi signal waves lighting up sequentially enclosed in rotating dash arrows, indicating connection retry."
},
{
"filename": "battery_low.py",
"states": ["battery_low", "low_power"],
"description": "A horizontal battery cell with a blinking single charge bar and alert triangle, showing low power warning."
},
{
"filename": "update_progress.py",
"states": ["update", "uploading", "downloading"],
"description": "A sliding download arrow dropping into a tray with a growing horizontal progress bar (0% - 100%)."
},
{
"filename": "breathing_idle.py",
"states": ["idle", "ready", "breathing"],
"description": "Smoothly scaling concentric squares simulating a calm breathing rhythm, representing an idle/ready state."
},
{
"filename": "wake_attention.py",
"states": ["wake", "attention", "active"],
"description": "Expressive robot eyes that look left, center, right, and blink periodically to display device wake attention."
},
{
"filename": "kid_companion.py",
"states": ["kid_friendly", "companion", "child_mode"],
"description": "A cute robot face companion with wiggling ears, a happy smile, blinking antenna, and a winking eye."
}
]
}
+101
View File
@@ -0,0 +1,101 @@
# Standalone MicroPython Voice Animation: Network Retry
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
import math
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def draw_center_text(d, msg, y, w, scale=1, c=1):
char_w = 8 * scale
x = max(0, (w - len(msg) * char_w) // 2)
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale, c)
return
except Exception:
pass
d.text(msg, x, y, c)
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
label = text if text else "CONNECTING..."
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Wi-Fi Dot base at bottom of logo
dot_y = cy + 20
display.fill_rect(cx - 4, dot_y - 4, 8, 8, 1)
# Determine Wi-Fi wave stage (0 to 3) for sequential scanning
stage = (frame // 3) % 4
# Wave 1 (inner arc)
if stage >= 1:
display.line(cx - 15, dot_y - 12, cx, dot_y - 20, 1)
display.line(cx, dot_y - 20, cx + 15, dot_y - 12, 1)
# Wave 2 (middle arc)
if stage >= 2:
display.line(cx - 30, dot_y - 25, cx, dot_y - 36, 1)
display.line(cx, dot_y - 36, cx + 30, dot_y - 25, 1)
# Wave 3 (outer arc)
if stage >= 3:
display.line(cx - 45, dot_y - 38, cx, dot_y - 52, 1)
display.line(cx, dot_y - 52, cx + 45, dot_y - 38, 1)
# Draw a rotating retry circle (dashed arrow indicator) around the Wi-Fi icon
r = 68
# Compute 2 rotating arrow segments based on frame
rot_offset = frame * 0.12
for side in (0, math.pi):
for a_deg in range(0, 120, 15):
angle = side + rot_offset + math.radians(a_deg)
px = cx + int(r * math.cos(angle))
py = cy - 10 + int(r * math.sin(angle))
display.fill_rect(px - 2, py - 2, 4, 4, 1)
# Draw arrowheads at the leading edge of each segment
lead_angle = side + rot_offset + math.radians(120)
lx = cx + int(r * math.cos(lead_angle))
ly = cy - 10 + int(r * math.sin(lead_angle))
# Arrowhead wings
w1 = lead_angle - math.pi * 0.75
w2 = lead_angle + math.pi * 0.75
display.line(lx, ly, lx + int(8 * math.cos(w1)), ly + int(8 * math.sin(w1)), 1)
display.line(lx, ly, lx + int(8 * math.cos(w2)), ly + int(8 * math.sin(w2)), 1)
# Draw centered text label at the bottom
draw_center_text(display, label, height - 30, width, scale=1, c=1)
display.show()
frame += 1
try:
time.sleep_ms(55)
except AttributeError:
time.sleep(0.055)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()
+88
View File
@@ -0,0 +1,88 @@
# Standalone MicroPython Voice Animation: Speaking Mouth (Audio Spectrum)
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
import math
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def draw_center_text(d, msg, y, w, scale=1, c=1):
char_w = 8 * scale
x = max(0, (w - len(msg) * char_w) // 2)
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale, c)
return
except Exception:
pass
d.text(msg, x, y, c)
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
label = text if text else "SPEAKING..."
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Draw top and bottom boundary lines for the sound waves
display.line(cx - 100, cy - 45, cx + 100, cy - 45, 1)
display.line(cx - 100, cy + 45, cx + 100, cy + 45, 1)
# Draw a series of vertical equalizer bars modulated to simulate speech patterns
# An envelope simulates words (peaks and silences)
word_envelope = abs(math.sin(frame * 0.05))
num_bars = 15
bar_w = 8
gap = 4
total_w = num_bars * bar_w + (num_bars - 1) * gap
start_x = cx - total_w // 2
for i in range(num_bars):
bx = start_x + i * (bar_w + gap)
# Modulate individual bar heights using frame phase offsets
phase = frame * 0.28 + i * 0.45
noise_val = abs(math.sin(phase)) * 0.7 + abs(math.cos(phase * 1.5)) * 0.3
# Height peaks towards the center bars (bell shape)
center_factor = math.cos(((i - num_bars // 2) / (num_bars // 2)) * (math.pi / 2.2))
h = int(6 + 72 * word_envelope * noise_val * center_factor)
# Keep height within boundaries
h = min(80, h)
# Draw vertical bar centered at cy
display.fill_rect(bx, cy - h // 2, bar_w, h, 1)
# Draw centered text label at the bottom
draw_center_text(display, label, height - 30, width, scale=1, c=1)
display.show()
frame += 1
try:
time.sleep_ms(50)
except AttributeError:
time.sleep(0.05)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()
+84
View File
@@ -0,0 +1,84 @@
# Standalone MicroPython Voice Animation: Success Spark
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
import math
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def draw_center_text(d, msg, y, w, scale=1, c=1):
char_w = 8 * scale
x = max(0, (w - len(msg) * char_w) // 2)
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale, c)
return
except Exception:
pass
d.text(msg, x, y, c)
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
label = text if text else "SUCCESS!"
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Draw a thick success checkmark
display.line(cx - 20, cy + 5, cx - 5, cy + 20, 1)
display.line(cx - 19, cy + 5, cx - 4, cy + 20, 1)
display.line(cx - 5, cy + 20, cx + 25, cy - 10, 1)
display.line(cx - 4, cy + 20, cx + 26, cy - 10, 1)
# Sparkles/rays radiating outwards
# The rays expand from r=30 to r=90 and loop
dist = (frame * 6) % 80
# Draw rays only if within reasonable expansion bounds
if dist < 65:
for angle_deg in range(0, 360, 45):
angle = math.radians(angle_deg)
# Outer point of sparkle
sx1 = cx + int(dist * math.cos(angle))
sy1 = cy + int(dist * math.sin(angle))
# Inner point of sparkle
sx2 = cx + int((dist + 12) * math.cos(angle))
sy2 = cy + int((dist + 12) * math.sin(angle))
display.line(sx1, sy1, sx2, sy2, 1)
# Add side sparkles for variety on cross angles (45, 135, etc.)
if angle_deg % 90 != 0:
display.fill_rect(sx2 - 2, sy2 - 2, 4, 4, 1)
# Draw centered text label at the bottom
draw_center_text(display, label, height - 30, width, scale=1, c=1)
display.show()
frame += 1
try:
time.sleep_ms(45)
except AttributeError:
time.sleep(0.045)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()
+89
View File
@@ -0,0 +1,89 @@
# Standalone MicroPython Voice Animation: Update Progress
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def draw_center_text(d, msg, y, w, scale=1, c=1):
char_w = 8 * scale
x = max(0, (w - len(msg) * char_w) // 2)
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale, c)
return
except Exception:
pass
d.text(msg, x, y, c)
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Draw a sliding downward-pointing arrow in the top half
# Offset slides down and wraps around to create motion
arrow_slide = (frame * 3) % 24
ay = cy - 45 + arrow_slide
# Arrow Shaft
display.fill_rect(cx - 6, ay - 24, 12, 24, 1)
# Arrow Head (triangle)
display.line(cx - 18, ay, cx, ay + 15, 1)
display.line(cx + 18, ay, cx, ay + 15, 1)
display.line(cx - 18, ay, cx + 18, ay, 1)
# Fill the head slightly
display.fill_rect(cx - 6, ay, 12, 8, 1)
# Draw static receiver container (horizontal tray) below the arrow path
tray_y = cy + 12
display.line(cx - 30, tray_y, cx - 30, tray_y + 10, 1)
display.line(cx - 30, tray_y + 10, cx + 30, tray_y + 10, 1)
display.line(cx + 30, tray_y + 10, cx + 30, tray_y, 1)
# Calculate update progress percentage
# (Cycles 0 to 100 over 25 frames, or follows frame count)
percent = (frame * 4) % 101
# Progress bar outline
bar_y = cy + 45
display.rect(cx - 90, bar_y, 180, 16, 1)
# Progress bar fill
fill_width = int(176 * percent / 100)
if fill_width > 0:
display.fill_rect(cx - 88, bar_y + 2, fill_width, 12, 1)
# Draw label + percentage text
label = text if text else "UPDATING SYSTEM..."
pct_label = f"{label} {percent}%"
draw_center_text(display, pct_label, height - 30, width, scale=1, c=1)
display.show()
frame += 1
try:
time.sleep_ms(55)
except AttributeError:
time.sleep(0.055)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()
+81
View File
@@ -0,0 +1,81 @@
# Standalone MicroPython Voice Animation: Waiting Orbit
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
import math
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def draw_center_text(d, msg, y, w, scale=1, c=1):
char_w = 8 * scale
x = max(0, (w - len(msg) * char_w) // 2)
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale, c)
return
except Exception:
pass
d.text(msg, x, y, c)
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
label = text if text else "THINKING..."
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Draw central pulsing core
# Uses sin wave to expand/contract
pulse = int(14 + 4 * math.sin(frame * 0.18))
display.rect(cx - pulse, cy - pulse, pulse * 2, pulse * 2, 1)
display.rect(cx - pulse + 3, cy - pulse + 3, (pulse - 3) * 2, (pulse - 3) * 2, 1)
display.fill_rect(cx - 3, cy - 3, 6, 6, 1)
# Draw a dotted circular path of radius 60
for a_deg in range(0, 360, 15):
angle = math.radians(a_deg)
px = cx + int(60 * math.cos(angle))
py = cy + int(60 * math.sin(angle))
display.fill_rect(px, py, 2, 2, 1)
# Draw 4 orbiting ring nodes
for node_idx in range(4):
angle = (frame * 0.08) + (node_idx * math.pi / 2)
nx = cx + int(60 * math.cos(angle))
ny = cy + int(60 * math.sin(angle))
# Draw square ring nodes
display.fill_rect(nx - 6, ny - 6, 12, 12, 1)
display.fill_rect(nx - 4, ny - 4, 8, 8, 0)
# Draw centered text label at the bottom
draw_center_text(display, label, height - 35, width, scale=1, c=1)
display.show()
frame += 1
try:
time.sleep_ms(50)
except AttributeError:
time.sleep(0.05)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()
+96
View File
@@ -0,0 +1,96 @@
# Standalone MicroPython Voice Animation: Wake Attention
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
import math
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def draw_center_text(d, msg, y, w, scale=1, c=1):
char_w = 8 * scale
x = max(0, (w - len(msg) * char_w) // 2)
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale, c)
return
except Exception:
pass
d.text(msg, x, y, c)
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
label = text if text else "HI, I'M LISTENING"
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Left and Right eye center X coordinates
left_cx = cx - 55
right_cx = cx + 55
eye_y = cy - 10
# State cycle to control pupil gaze offset (look center, look left, look right)
gaze_state = (frame // 16) % 4
gaze_offset = 0
if gaze_state == 1:
gaze_offset = -12 # Look left
elif gaze_state == 3:
gaze_offset = 12 # Look right
# Blink cycle: close eyes for a few frames periodically
is_blinking = (frame % 22) in (0, 1, 2)
if is_blinking:
# Closed eyes: draw simple horizontal lines across the eye spaces
display.line(left_cx - 28, eye_y, left_cx + 28, eye_y, 1)
display.line(left_cx - 28, eye_y + 1, left_cx + 28, eye_y + 1, 1) # thicker
display.line(right_cx - 28, eye_y, right_cx + 28, eye_y, 1)
display.line(right_cx - 28, eye_y + 1, right_cx + 28, eye_y + 1, 1) # thicker
else:
# Open eyes: draw outer rounded rectangular bounding frames
# Left eye border
display.rect(left_cx - 32, eye_y - 22, 64, 44, 1)
display.rect(left_cx - 31, eye_y - 21, 62, 42, 1)
# Right eye border
display.rect(right_cx - 32, eye_y - 22, 64, 44, 1)
display.rect(right_cx - 31, eye_y - 21, 62, 42, 1)
# Draw pupils inside, shifted by the gaze offset
display.fill_rect(left_cx - 10 + gaze_offset, eye_y - 10, 20, 20, 1)
display.fill_rect(right_cx - 10 + gaze_offset, eye_y - 10, 20, 20, 1)
# Draw small cute highlights inside pupils
display.fill_rect(left_cx - 6 + gaze_offset, eye_y - 6, 6, 6, 0)
display.fill_rect(right_cx - 6 + gaze_offset, eye_y - 6, 6, 6, 0)
# Draw centered text label at the bottom
draw_center_text(display, label, height - 30, width, scale=1, c=1)
display.show()
frame += 1
try:
time.sleep_ms(60)
except AttributeError:
time.sleep(0.06)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()
+102
View File
@@ -0,0 +1,102 @@
# Standalone MicroPython Voice Animation: Working Gears
# Exposes run(text="", frames=...) and runs automatically if executed directly.
import time
import math
try:
import board_config
display = getattr(board_config, 'display_instance', None)
except Exception:
board_config = None
display = None
def draw_center_text(d, msg, y, w, scale=1, c=1):
char_w = 8 * scale
x = max(0, (w - len(msg) * char_w) // 2)
if hasattr(d, 'text_large') and scale > 1:
try:
d.text_large(msg, x, y, scale, c)
return
except Exception:
pass
d.text(msg, x, y, c)
def draw_gear(d, gcx, gcy, r, teeth_count, rotation, c=1):
# Draw gear boundary
d.rect(gcx - r, gcy - r, r * 2, r * 2, c)
# Inner shaft hole
d.fill_rect(gcx - 5, gcy - 5, 10, 10, c)
d.fill_rect(gcx - 2, gcy - 2, 4, 4, 0)
# Draw gear teeth extending outwards
for i in range(teeth_count):
angle = rotation + i * (2 * math.pi / teeth_count)
# Inner teeth start
x1 = gcx + int((r - 2) * math.cos(angle))
y1 = gcy + int((r - 2) * math.sin(angle))
# Outer teeth end
x2 = gcx + int((r + 10) * math.cos(angle))
y2 = gcy + int((r + 10) * math.sin(angle))
d.line(x1, y1, x2, y2, c)
# Add tiny horizontal heads to teeth for thickness
perp_angle = angle + math.pi / 2
tx1 = x2 - int(3 * math.cos(perp_angle))
ty1 = y2 - int(3 * math.sin(perp_angle))
tx2 = x2 + int(3 * math.cos(perp_angle))
ty2 = y2 + int(3 * math.sin(perp_angle))
d.line(tx1, ty1, tx2, ty2, c)
def run(text="", frames=100):
if not display:
print("board_config.display_instance is not available. Skipping animation.")
return
width = getattr(display, 'width', 400)
height = getattr(display, 'height', 300)
cx, cy = width // 2, height // 2
label = text if text else "WORKING..."
frame = 0
try:
while True:
if frames is not None and frames > 0 and frame >= frames:
break
display.clear(0)
# Draw two rotating, interlocking gears
# Large Gear (Left & slightly up)
rot1 = frame * 0.08
draw_gear(display, cx - 45, cy - 10, r=32, teeth_count=8, rotation=rot1, c=1)
# Small Gear (Right & slightly down)
# Rotates counter-clockwise, offset to interlock teeth
rot2 = -frame * 0.106 + (math.pi / 8)
draw_gear(display, cx + 45, cy + 20, r=24, teeth_count=6, rotation=rot2, c=1)
# Little decorative background computer dots
dot_phase = (frame // 4) % 4
for i in range(4):
fill_color = 1 if i == dot_phase else 0
display.rect(cx - 80 + i * 16, cy + 70, 8, 8, 1)
if fill_color:
display.fill_rect(cx - 78 + i * 16, cy + 72, 4, 4, 1)
# Draw centered text label at the bottom
draw_center_text(display, label, height - 30, width, scale=1, c=1)
display.show()
frame += 1
try:
time.sleep_ms(50)
except AttributeError:
time.sleep(0.05)
except KeyboardInterrupt:
print("Animation interrupted by user.")
if __name__ in ('__main__', '__name__'):
run()