From a22702df4dd1e201f013b52ad699c148e5a5c9d8 Mon Sep 17 00:00:00 2001 From: Jimmy Date: Mon, 20 Apr 2026 23:37:43 +1200 Subject: [PATCH] feat(patterns): add radiate animation --- src/patterns/radiate.py | 89 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/patterns/radiate.py diff --git a/src/patterns/radiate.py b/src/patterns/radiate.py new file mode 100644 index 0000000..f3ad4b6 --- /dev/null +++ b/src/patterns/radiate.py @@ -0,0 +1,89 @@ +import utime + + +class Radiate: + def __init__(self, driver): + self.driver = driver + + def run(self, preset): + """Radiate from nodes every n1 LEDs, retriggering every delay (d). + + - n1: node spacing in LEDs + - n2: outbound travel time in ms + - n3: return travel time in ms + - d: retrigger interval in ms + """ + colors = preset.c if preset.c else [(255, 255, 255)] + base_on = colors[0] + base_off = colors[1] if len(colors) > 1 else (0, 0, 0) + + spacing = max(1, int(preset.n1)) + outward_ms = max(1, int(preset.n2)) + return_ms = max(1, int(preset.n3)) + max_dist = spacing // 2 + + lit_color = self.driver.apply_brightness(base_on, preset.b) + off_color = self.driver.apply_brightness(base_off, preset.b) + + now = utime.ticks_ms() + last_trigger = now + active_pulses = [now] + + if not preset.a: + # Single-step render uses only the first instant pulse. + active_pulses = [utime.ticks_ms()] + + while True: + now = utime.ticks_ms() + delay_ms = max(1, int(preset.d)) + spacing = max(1, int(preset.n1)) + outward_ms = max(1, int(preset.n2)) + return_ms = max(1, int(preset.n3)) + max_dist = spacing // 2 + lit_color = self.driver.apply_brightness(base_on, preset.b) + off_color = self.driver.apply_brightness(base_off, preset.b) + + if preset.a and utime.ticks_diff(now, last_trigger) >= delay_ms: + active_pulses.append(now) + last_trigger = utime.ticks_add(last_trigger, delay_ms) + + # Drop pulses once their out-and-back lifetime ends. + pulse_lifetime = outward_ms + return_ms + kept = [] + for start in active_pulses: + age = utime.ticks_diff(now, start) + if age <= pulse_lifetime: + kept.append(start) + active_pulses = kept + + for i in range(self.driver.num_leds): + # Nearest node distance for a repeating node grid every `spacing` LEDs. + offset = i % spacing + dist = min(offset, spacing - offset) + + lit = False + for start in active_pulses: + age = utime.ticks_diff(now, start) + if age < 0: + continue + if age <= outward_ms: + front = (age * max_dist) / outward_ms + elif age <= outward_ms + return_ms: + back_age = age - outward_ms + front = ((return_ms - back_age) * max_dist) / return_ms + else: + continue + + if dist <= front: + lit = True + break + + self.driver.n[i] = lit_color if lit else off_color + + self.driver.n.write() + + if not preset.a: + yield + return + + yield