Slow mood animations 2x, symmetric jaw VU bar with peak+decay
Mood: halve all pulse speeds, slow wave/larson/sparkle proportionally. Jaw: center-out symmetric bar (LED 7 = center, expands both sides), instant attack with smooth decay falloff, color shifts from cyan (low) through white (mid) to warm (high amplitude). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
117
firmware/main.py
117
firmware/main.py
@@ -105,17 +105,24 @@ MOOD_COLORS = {
|
||||
"sleep": (20, 22, 35),
|
||||
}
|
||||
|
||||
JAW_COLOR = (0, 200, 255)
|
||||
# Jaw color breakpoints for amplitude-based color shift
|
||||
# (threshold 0-100, r, g, b)
|
||||
JAW_COLOR_LO = (0, 200, 255) # Cyan at low amplitude
|
||||
JAW_COLOR_MID = (200, 240, 255) # White-ish at mid amplitude
|
||||
JAW_COLOR_HI = (255, 220, 180) # Warm at high amplitude
|
||||
JAW_CENTER = JAW_OFFSET + 6 # Center LED index in strip (index 7)
|
||||
|
||||
|
||||
# === Animation state ===
|
||||
mood_mode = "idle"
|
||||
jaw_mode = "off"
|
||||
jaw_amplitude = 0 # 0-100, set by bare integer commands
|
||||
jaw_peak = 0 # Current display level with peak+decay (0-100)
|
||||
|
||||
# Per-animation persistent state
|
||||
larson_pos = 0
|
||||
larson_dir = 1
|
||||
larson_tick = 0
|
||||
wave_offset = 0.0
|
||||
# Sparkle buffer: store per-LED brightness as fixed-point (0-255)
|
||||
sparkle_buf = array.array("B", [0] * MOOD_LEDS)
|
||||
@@ -179,12 +186,12 @@ def tick_wave(color):
|
||||
brightness = 51 + ((204 * raw) >> 8)
|
||||
ar[i] = scale_color_grb(r, g, b, brightness)
|
||||
mood.show()
|
||||
wave_offset += 0.5
|
||||
wave_offset += 0.25
|
||||
|
||||
|
||||
def tick_larson(color):
|
||||
"""Larson scanner (bouncing dot with trail)."""
|
||||
global larson_pos, larson_dir
|
||||
"""Larson scanner (bouncing dot with trail). Advances every 2nd frame."""
|
||||
global larson_pos, larson_dir, larson_tick
|
||||
r, g, b = color
|
||||
ar = mood.ar
|
||||
# Clear
|
||||
@@ -201,13 +208,17 @@ def tick_larson(color):
|
||||
fade = _sine_table[(SINE_TABLE_SIZE // 4 - (t * SINE_TABLE_SIZE // 32)) % SINE_TABLE_SIZE]
|
||||
ar[idx] = scale_color_grb(r, g, b, fade)
|
||||
mood.show()
|
||||
larson_pos += larson_dir
|
||||
if larson_pos >= MOOD_LEDS - 1:
|
||||
larson_pos = MOOD_LEDS - 1
|
||||
larson_dir = -1
|
||||
elif larson_pos <= 0:
|
||||
larson_pos = 0
|
||||
larson_dir = 1
|
||||
# Advance position every 2nd frame
|
||||
larson_tick += 1
|
||||
if larson_tick >= 2:
|
||||
larson_tick = 0
|
||||
larson_pos += larson_dir
|
||||
if larson_pos >= MOOD_LEDS - 1:
|
||||
larson_pos = MOOD_LEDS - 1
|
||||
larson_dir = -1
|
||||
elif larson_pos <= 0:
|
||||
larson_pos = 0
|
||||
larson_dir = 1
|
||||
|
||||
|
||||
def tick_sparkle(color):
|
||||
@@ -216,10 +227,10 @@ def tick_sparkle(color):
|
||||
ar = mood.ar
|
||||
buf = sparkle_buf
|
||||
for i in range(MOOD_LEDS):
|
||||
# Decay existing brightness by ~15%: multiply by 217/256
|
||||
v = (buf[i] * 217) >> 8
|
||||
# Random spawn: ~15% chance = getrandbits check
|
||||
if getrandbits(3) == 0: # 1/8 = 12.5%, close enough to 15%
|
||||
# Decay existing brightness by ~10%: multiply by 230/256
|
||||
v = (buf[i] * 230) >> 8
|
||||
# Random spawn: ~6% chance
|
||||
if getrandbits(4) == 0: # 1/16 = 6.25%
|
||||
v = 255
|
||||
buf[i] = v
|
||||
ar[i] = scale_color_grb(r, g, b, v)
|
||||
@@ -237,37 +248,84 @@ def tick_mood(now_ms):
|
||||
return
|
||||
|
||||
if mode == "idle":
|
||||
tick_pulse(now_ms, color, 500, 51, 204) # 0.5 Hz, 20-80%
|
||||
tick_pulse(now_ms, color, 250, 51, 204) # 0.25 Hz, 20-80%
|
||||
elif mode == "listening":
|
||||
tick_pulse(now_ms, color, 2000, 153, 255) # 2.0 Hz, 60-100%
|
||||
tick_pulse(now_ms, color, 1000, 153, 255) # 1.0 Hz, 60-100%
|
||||
elif mode == "responding":
|
||||
tick_wave(color)
|
||||
elif mode == "pleasure":
|
||||
tick_pulse(now_ms, color, 800, 77, 230) # 0.8 Hz, 30-90%
|
||||
tick_pulse(now_ms, color, 400, 77, 230) # 0.4 Hz, 30-90%
|
||||
elif mode == "thinking":
|
||||
tick_larson(color)
|
||||
elif mode == "playful":
|
||||
tick_sparkle(color)
|
||||
elif mode == "commanding":
|
||||
tick_pulse(now_ms, color, 3000, 128, 255) # 3.0 Hz, 50-100%
|
||||
tick_pulse(now_ms, color, 1500, 128, 255) # 1.5 Hz, 50-100%
|
||||
elif mode == "love":
|
||||
tick_pulse(now_ms, color, 600, 102, 230) # 0.6 Hz, 40-90%
|
||||
tick_pulse(now_ms, color, 300, 102, 230) # 0.3 Hz, 40-90%
|
||||
elif mode == "sleep":
|
||||
tick_pulse(now_ms, color, 200, 26, 77) # 0.2 Hz, 10-30%
|
||||
tick_pulse(now_ms, color, 100, 26, 77) # 0.1 Hz, 10-30%
|
||||
|
||||
|
||||
# === Jaw animation ticks ===
|
||||
|
||||
def _jaw_color_for_amp(amp):
|
||||
"""Interpolate jaw color based on amplitude 0-100. Returns (r, g, b)."""
|
||||
if amp <= 33:
|
||||
# Blend lo -> mid over 0-33
|
||||
t = (amp * 256) // 33 # 0-256
|
||||
r = JAW_COLOR_LO[0] + ((JAW_COLOR_MID[0] - JAW_COLOR_LO[0]) * t >> 8)
|
||||
g = JAW_COLOR_LO[1] + ((JAW_COLOR_MID[1] - JAW_COLOR_LO[1]) * t >> 8)
|
||||
b = JAW_COLOR_LO[2] + ((JAW_COLOR_MID[2] - JAW_COLOR_LO[2]) * t >> 8)
|
||||
else:
|
||||
# Blend mid -> hi over 34-100
|
||||
t = ((amp - 34) * 256) // 66 # 0-256
|
||||
r = JAW_COLOR_MID[0] + ((JAW_COLOR_HI[0] - JAW_COLOR_MID[0]) * t >> 8)
|
||||
g = JAW_COLOR_MID[1] + ((JAW_COLOR_HI[1] - JAW_COLOR_MID[1]) * t >> 8)
|
||||
b = JAW_COLOR_MID[2] + ((JAW_COLOR_HI[2] - JAW_COLOR_MID[2]) * t >> 8)
|
||||
return (r, g, b)
|
||||
|
||||
|
||||
def jaw_set_amplitude(amp):
|
||||
"""Set jaw LEDs based on amplitude 0-100."""
|
||||
num_lit = (amp * JAW_USABLE) // 100
|
||||
r, g, b = JAW_COLOR
|
||||
on = pack_grb(r, g, b)
|
||||
"""Symmetric VU bar from center with peak+decay and color shift."""
|
||||
global jaw_peak
|
||||
# Peak+decay: snap up, smooth decay down
|
||||
if amp >= jaw_peak:
|
||||
jaw_peak = amp
|
||||
else:
|
||||
jaw_peak = jaw_peak - (jaw_peak - amp) * 0.3
|
||||
if jaw_peak < 0.5:
|
||||
jaw_peak = 0
|
||||
|
||||
display = int(jaw_peak)
|
||||
ar = jaw.ar
|
||||
ar[0] = 0 # Damaged LED always off
|
||||
|
||||
if display == 0:
|
||||
for i in range(JAW_OFFSET, JAW_LEDS):
|
||||
ar[i] = 0
|
||||
jaw.show()
|
||||
return
|
||||
|
||||
r, g, b = _jaw_color_for_amp(display)
|
||||
on = pack_grb(r, g, b)
|
||||
|
||||
# How many LEDs per side (0-6), plus center = 1 + 2*num_side
|
||||
# At amp 100: num_side = 6 (all 13 lit). At amp ~8: num_side = 0 (center only)
|
||||
num_side_256 = (display * 6 * 256) // 100 # fixed-point 0..6*256
|
||||
num_side_full = num_side_256 >> 8 # whole LEDs per side
|
||||
edge_brightness = num_side_256 & 0xFF # fractional part for edge LED
|
||||
|
||||
for i in range(JAW_OFFSET, JAW_LEDS):
|
||||
if i - JAW_OFFSET < num_lit:
|
||||
dist = abs(i - JAW_CENTER) # distance from center (0-6)
|
||||
if dist == 0:
|
||||
# Center always on when amp > 0
|
||||
ar[i] = on
|
||||
elif dist <= num_side_full:
|
||||
ar[i] = on
|
||||
elif dist == num_side_full + 1 and edge_brightness > 0:
|
||||
# Edge LED with fractional brightness
|
||||
ar[i] = scale_color_grb(r, g, b, edge_brightness)
|
||||
else:
|
||||
ar[i] = 0
|
||||
jaw.show()
|
||||
@@ -293,10 +351,10 @@ def tick_jaw(now_ms):
|
||||
amp = (val * 100) >> 8 # Scale to 0-100
|
||||
jaw_set_amplitude(amp)
|
||||
elif mode == "idle":
|
||||
# Subtle dim glow
|
||||
# Subtle dim glow from center
|
||||
ar = jaw.ar
|
||||
ar[0] = 0
|
||||
dim = scale_color_grb(JAW_COLOR[0], JAW_COLOR[1], JAW_COLOR[2], 15)
|
||||
dim = scale_color_grb(JAW_COLOR_LO[0], JAW_COLOR_LO[1], JAW_COLOR_LO[2], 15)
|
||||
for i in range(JAW_OFFSET, JAW_LEDS):
|
||||
ar[i] = dim
|
||||
jaw.show()
|
||||
@@ -358,9 +416,10 @@ def process_command(line):
|
||||
|
||||
def _reset_mood_state():
|
||||
"""Reset per-animation state on mode switch."""
|
||||
global larson_pos, larson_dir, wave_offset
|
||||
global larson_pos, larson_dir, larson_tick, wave_offset
|
||||
larson_pos = 0
|
||||
larson_dir = 1
|
||||
larson_tick = 0
|
||||
wave_offset = 0.0
|
||||
for i in range(MOOD_LEDS):
|
||||
sparkle_buf[i] = 0
|
||||
|
||||
Reference in New Issue
Block a user