fix(patterns): use preset background fallback across animations

Align pattern background rendering to use preset.background_or(...) and update pulse/radiate single-step behaviour to preserve visible frames and step progression.
This commit is contained in:
2026-05-09 14:28:05 +12:00
parent fbebe9f4f9
commit 4879fcfe90
20 changed files with 32 additions and 33 deletions

View File

@@ -173,13 +173,6 @@ async def presets_loop():
while True: while True:
presets.tick() presets.tick()
wdt.feed() wdt.feed()
if bool(getattr(presets, "debug", False)):
now = utime.ticks_ms()
if utime.ticks_diff(now, last_mem_log) >= 5000:
gc.collect()
print("mem runtime:", {"free": gc.mem_free(), "alloc": gc.mem_alloc()})
last_mem_log = now
# tick() does not await; yield so UDP hello and HTTP/WebSocket can run.
await asyncio.sleep(0) await asyncio.sleep(0)

View File

@@ -16,7 +16,7 @@ class BarGraph:
target = (self.driver.num_leds * level) // 100 target = (self.driver.num_leds * level) // 100
lit = self.driver.apply_brightness(colors[0], preset.b) lit = self.driver.apply_brightness(colors[0], preset.b)
unlit = self.driver.apply_brightness( unlit = self.driver.apply_brightness(
colors[-1], preset.background_or(colors),
preset.b, preset.b,
) )
for i in range(self.driver.num_leds): for i in range(self.driver.num_leds):

View File

@@ -9,6 +9,7 @@ class Blink:
"""Blink pattern: toggles LEDs on/off using preset delay, cycling through colors.""" """Blink pattern: toggles LEDs on/off using preset delay, cycling through colors."""
# Use provided colors, or default to white if none # Use provided colors, or default to white if none
colors = preset.c if preset.c else [(255, 255, 255)] colors = preset.c if preset.c else [(255, 255, 255)]
bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b)
color_index = 0 color_index = 0
state = True # True = on, False = off state = True # True = on, False = off
last_update = utime.ticks_ms() last_update = utime.ticks_ms()
@@ -25,8 +26,8 @@ class Blink:
# Advance to next color for the next "on" phase # Advance to next color for the next "on" phase
color_index += 1 color_index += 1
else: else:
# "Off" phase should actually be off. # Inactive phase uses the preset background color.
self.driver.fill((0, 0, 0)) self.driver.fill(bg_color)
state = not state state = not state
last_update = utime.ticks_add(last_update, delay_ms) last_update = utime.ticks_add(last_update, delay_ms)
# Yield once per tick so other logic can run # Yield once per tick so other logic can run

View File

@@ -26,7 +26,7 @@ class Chase:
color0 = self.driver.apply_brightness(color0, preset.b) color0 = self.driver.apply_brightness(color0, preset.b)
color1 = self.driver.apply_brightness(color1, preset.b) color1 = self.driver.apply_brightness(color1, preset.b)
bg_color = self.driver.apply_brightness(colors[-1], preset.b) bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b)
n1 = max(1, int(preset.n1)) # LEDs of color 0 n1 = max(1, int(preset.n1)) # LEDs of color 0
n2 = max(1, int(preset.n2)) # LEDs of color 1 n2 = max(1, int(preset.n2)) # LEDs of color 1

View File

@@ -31,10 +31,10 @@ class Circle:
base0 = base1 = (255, 255, 255) base0 = base1 = (255, 255, 255)
elif len(colors) == 1: elif len(colors) == 1:
base0 = colors[0] base0 = colors[0]
base1 = colors[-1] base1 = preset.background_or(colors)
else: else:
base0 = colors[0] base0 = colors[0]
base1 = colors[-1] base1 = preset.background_or(colors)
color0 = self.driver.apply_brightness(base0, preset.b) color0 = self.driver.apply_brightness(base0, preset.b)
color1 = self.driver.apply_brightness(base1, preset.b) color1 = self.driver.apply_brightness(base1, preset.b)

View File

@@ -15,7 +15,7 @@ class ClockSweep:
d = max(1, int(preset.d)) d = max(1, int(preset.d))
now = utime.ticks_ms() now = utime.ticks_ms()
if utime.ticks_diff(now, last) >= d: if utime.ticks_diff(now, last) >= d:
bg = self.driver.apply_brightness(colors[-1], preset.b) bg = self.driver.apply_brightness(preset.background_or(colors), preset.b)
fg = self.driver.apply_brightness(colors[0], preset.b) fg = self.driver.apply_brightness(colors[0], preset.b)
for i in range(self.driver.num_leds): for i in range(self.driver.num_leds):
self.driver.n[i] = bg self.driver.n[i] = bg

View File

@@ -17,7 +17,7 @@ class CometDual:
d = max(1, int(preset.d)) d = max(1, int(preset.d))
now = utime.ticks_ms() now = utime.ticks_ms()
if utime.ticks_diff(now, last) >= d: if utime.ticks_diff(now, last) >= d:
bg_color = self.driver.apply_brightness(colors[-1], preset.b) bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b)
for i in range(self.driver.num_leds): for i in range(self.driver.num_leds):
self.driver.n[i] = bg_color self.driver.n[i] = bg_color
c1 = self.driver.apply_brightness(colors[0 % len(colors)], preset.b) c1 = self.driver.apply_brightness(colors[0 % len(colors)], preset.b)

View File

@@ -16,7 +16,7 @@ class Fireflies:
d = max(1, int(preset.d)) d = max(1, int(preset.d))
now = utime.ticks_ms() now = utime.ticks_ms()
if utime.ticks_diff(now, last) >= d: if utime.ticks_diff(now, last) >= d:
bg_color = self.driver.apply_brightness(colors[-1], preset.b) bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b)
for i in range(self.driver.num_leds): for i in range(self.driver.num_leds):
self.driver.n[i] = bg_color self.driver.n[i] = bg_color
for b in bugs: for b in bugs:

View File

@@ -17,7 +17,7 @@ class Heartbeat:
beat_gap = max(20, int(preset.d)) beat_gap = max(20, int(preset.d))
colors = preset.c if preset.c else [(255, 0, 40)] colors = preset.c if preset.c else [(255, 0, 40)]
lit_color = self.driver.apply_brightness(colors[0], preset.b) lit_color = self.driver.apply_brightness(colors[0], preset.b)
bg_color = self.driver.apply_brightness(colors[-1], preset.b) bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b)
phase_durations = (p1, beat_gap, p2, pause) phase_durations = (p1, beat_gap, p2, pause)
phase_colors = (lit_color, bg_color, lit_color, bg_color) phase_colors = (lit_color, bg_color, lit_color, bg_color)

View File

@@ -17,7 +17,7 @@ class Marquee:
now = utime.ticks_ms() now = utime.ticks_ms()
if utime.ticks_diff(now, last) >= d: if utime.ticks_diff(now, last) >= d:
c = self.driver.apply_brightness(colors[0], preset.b) c = self.driver.apply_brightness(colors[0], preset.b)
bg_color = self.driver.apply_brightness(colors[-1], preset.b) bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b)
for i in range(self.driver.num_leds): for i in range(self.driver.num_leds):
m = (i + phase) % (on_len + off_len) m = (i + phase) % (on_len + off_len)
self.driver.n[i] = c if m < on_len else bg_color self.driver.n[i] = c if m < on_len else bg_color

View File

@@ -15,7 +15,7 @@ class Orbit:
d = max(1, int(preset.d)) d = max(1, int(preset.d))
now = utime.ticks_ms() now = utime.ticks_ms()
if utime.ticks_diff(now, last) >= d: if utime.ticks_diff(now, last) >= d:
bg_color = self.driver.apply_brightness(colors[-1], preset.b) bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b)
for i in range(self.driver.num_leds): for i in range(self.driver.num_leds):
self.driver.n[i] = bg_color self.driver.n[i] = bg_color
for k in range(orbits): for k in range(orbits):

View File

@@ -6,19 +6,19 @@ class Pulse:
self.driver = driver self.driver = driver
def run(self, preset): def run(self, preset):
self.driver.off()
# Get colors from preset # Get colors from preset
colors = preset.c colors = preset.c
if not colors: if not colors:
colors = [(255, 255, 255)] colors = [(255, 255, 255)]
bg_base = preset.background_or(colors)
self.driver.fill(self.driver.apply_brightness(bg_base, preset.b))
color_index = 0 color_index = self.driver.step % max(1, len(colors))
cycle_start = utime.ticks_ms() cycle_start = utime.ticks_ms()
# State machine based pulse using a single generator loop # State machine based pulse using a single generator loop
while True: while True:
bg_color = self.driver.apply_brightness(colors[-1], preset.b) bg_color = self.driver.apply_brightness(bg_base, preset.b)
# Read current timing parameters from preset # Read current timing parameters from preset
attack_ms = max(0, int(preset.n1)) # Attack time in ms attack_ms = max(0, int(preset.n1)) # Attack time in ms
hold_ms = max(0, int(preset.n2)) # Hold time in ms hold_ms = max(0, int(preset.n2)) # Hold time in ms
@@ -53,7 +53,8 @@ class Pulse:
self.driver.fill(bg_color) self.driver.fill(bg_color)
else: else:
# End of cycle, move to next color and restart timing # End of cycle, move to next color and restart timing
color_index += 1 color_index = (color_index + 1) % max(1, len(colors))
self.driver.step = color_index
cycle_start = now cycle_start = now
if not preset.a: if not preset.a:
break break

View File

@@ -17,7 +17,7 @@ class Radiate:
""" """
colors = preset.c if preset.c else [(255, 255, 255)] colors = preset.c if preset.c else [(255, 255, 255)]
base_on = colors[0] base_on = colors[0]
base_off = colors[-1] base_off = preset.background_or(colors)
spacing = max(1, int(preset.n1)) spacing = max(1, int(preset.n1))
outward_ms = max(1, int(preset.n2)) outward_ms = max(1, int(preset.n2))
@@ -34,8 +34,9 @@ class Radiate:
dbg_banner = False dbg_banner = False
if not preset.a: if not preset.a:
# Single-step render uses only the first instant pulse. # Single-shot mode exits after one rendered frame. Seed the pulse
active_pulses = [utime.ticks_ms()] # slightly in the past so this frame is visible before returning.
active_pulses = [utime.ticks_add(utime.ticks_ms(), -1)]
while True: while True:
now = utime.ticks_ms() now = utime.ticks_ms()

View File

@@ -16,7 +16,7 @@ class RainDrops:
d = max(1, int(preset.d)) d = max(1, int(preset.d))
now = utime.ticks_ms() now = utime.ticks_ms()
if utime.ticks_diff(now, last) >= d: if utime.ticks_diff(now, last) >= d:
bg_color = self.driver.apply_brightness(colors[-1], preset.b) bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b)
for i in range(self.driver.num_leds): for i in range(self.driver.num_leds):
self.driver.n[i] = bg_color self.driver.n[i] = bg_color
if random.randint(0, 255) < rate: if random.randint(0, 255) < rate:

View File

@@ -27,7 +27,7 @@ class Scanner:
if utime.ticks_diff(now, last_update) >= delay_ms: if utime.ticks_diff(now, last_update) >= delay_ms:
base = colors[color_index % len(colors)] base = colors[color_index % len(colors)]
base = self.driver.apply_brightness(base, preset.b) base = self.driver.apply_brightness(base, preset.b)
bg_color = self.driver.apply_brightness(colors[-1], preset.b) bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b)
for i in range(self.driver.num_leds): for i in range(self.driver.num_leds):
dist = i - center dist = i - center
if dist < 0: if dist < 0:

View File

@@ -24,7 +24,7 @@ class SegmentChase:
d = max(1, int(preset.d)) d = max(1, int(preset.d))
now = utime.ticks_ms() now = utime.ticks_ms()
if utime.ticks_diff(now, last) >= d: if utime.ticks_diff(now, last) >= d:
bg_color = self.driver.apply_brightness(colors[-1], preset.b) bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b)
for i in range(self.driver.num_leds): for i in range(self.driver.num_leds):
seg_idx = i // seg seg_idx = i // seg
in_seg = i % seg in_seg = i % seg

View File

@@ -16,7 +16,7 @@ class Snowfall:
d = max(1, int(preset.d)) d = max(1, int(preset.d))
now = utime.ticks_ms() now = utime.ticks_ms()
if utime.ticks_diff(now, last) >= d: if utime.ticks_diff(now, last) >= d:
bg_color = self.driver.apply_brightness(colors[-1], preset.b) bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b)
if random.randint(0, 255) < density: if random.randint(0, 255) < density:
flakes.append([self.driver.num_leds - 1, random.randint(0, len(colors)-1)]) flakes.append([self.driver.num_leds - 1, random.randint(0, len(colors)-1)])
for i in range(self.driver.num_leds): for i in range(self.driver.num_leds):

View File

@@ -16,7 +16,7 @@ class StrobeBurst:
cooldown = max(1, int(preset.n3) if int(preset.n3) > 0 else 400) cooldown = max(1, int(preset.n3) if int(preset.n3) > 0 else 400)
on_ms = max(1, int(preset.d) // 2) on_ms = max(1, int(preset.d) // 2)
c = self.driver.apply_brightness(colors[0], preset.b) c = self.driver.apply_brightness(colors[0], preset.b)
bg_color = self.driver.apply_brightness(colors[-1], preset.b) bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b)
now = utime.ticks_ms() now = utime.ticks_ms()
if state == "flash_on": if state == "flash_on":

View File

@@ -39,7 +39,7 @@ class Twinkle:
"""Twinkle: n1 activity, n2 density; n3/n4 min/max length of adjacent on/off runs.""" """Twinkle: n1 activity, n2 density; n3/n4 min/max length of adjacent on/off runs."""
palette = self._palette(preset) palette = self._palette(preset)
num = self.driver.num_leds num = self.driver.num_leds
bg_color = self.driver.apply_brightness(palette[-1], preset.b) bg_color = self.driver.apply_brightness(preset.background_or(palette), preset.b)
if num <= 0: if num <= 0:
while True: while True:
yield yield

View File

@@ -100,6 +100,9 @@ class Preset:
def auto(self, value): def auto(self, value):
self.a = value self.a = value
def background_or(self, colors=None, default=(0, 0, 0)):
return default
def to_dict(self): def to_dict(self):
return { return {
"p": self.p, "p": self.p,