Add RLCD animations and screensavers
This commit is contained in:
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user