1 Commits

Author SHA1 Message Date
8f8bc894a9 feat(patterns): add icicles blizzard and rime winter effects
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-16 15:09:59 +12:00
6 changed files with 328 additions and 0 deletions

65
src/patterns/blizzard.py Normal file
View File

@@ -0,0 +1,65 @@
import random
import utime
class Blizzard:
"""Dense falling flakes with sideways drift (compare `snowfall` for gentler flakes)."""
def __init__(self, driver):
self.driver = driver
def run(self, preset):
colors = preset.c if preset.c else [(255, 255, 255), (200, 230, 255), (180, 210, 255)]
# Higher n1 → more spawns (0255 threshold vs random)
density = max(1, int(preset.n1) if int(preset.n1) > 0 else 90)
speed = max(1, int(preset.n2) if int(preset.n2) > 0 else 2)
# n3: 128 = no bias; <128 drift one way, >128 the other (scaled to small steps)
wraw = int(preset.n3)
if wraw <= 0:
wind = 0
else:
wind = max(-4, min(4, (wraw - 128) // 20))
flakes = []
last = utime.ticks_ms()
while True:
d_ms = max(1, int(preset.d))
now = utime.ticks_ms()
if utime.ticks_diff(now, last) >= d_ms:
nled = self.driver.num_leds
bg = self.driver.apply_brightness(preset.background_or(colors), preset.b)
for i in range(nled):
self.driver.n[i] = bg
if random.randint(0, 255) < density:
flakes.append(
[
nled - 1,
random.randint(0, len(colors) - 1),
0 if wind == 0 else random.randint(-1, 1),
]
)
nf = []
for pos, ci, wj in flakes:
p = pos
lateral = wind + (wj if wj else 0)
p -= speed
p += lateral
if p < -2 or p >= nled + 2:
continue
pi = max(0, min(nled - 1, int(p)))
self.driver.n[pi] = self.driver.apply_brightness(colors[ci], preset.b)
nf.append([p, ci, wj])
flakes = nf
self.driver.n.write()
last = utime.ticks_add(last, d_ms)
if not preset.a:
yield
return
yield

62
src/patterns/icicles.py Normal file
View File

@@ -0,0 +1,62 @@
import utime
class Icicles:
"""Icicles hanging from anchor points; tips brighten toward max length then shrink."""
def __init__(self, driver):
self.driver = driver
def run(self, preset):
colors = preset.c if preset.c else [(240, 248, 255), (160, 210, 255), (255, 255, 255)]
spacing = max(1, int(preset.n1) if int(preset.n1) > 0 else 12)
nled = self.driver.num_leds
max_len = max(
2,
min(
int(preset.n2) if int(preset.n2) > 0 else min(14, max(3, nled // 4)),
max(2, nled),
),
)
span = max_len * 2
phase_step = max(1, int(preset.n3) if int(preset.n3) > 0 else 1)
phase = 0
last = utime.ticks_ms()
while True:
d_ms = max(1, int(preset.d))
now = utime.ticks_ms()
if utime.ticks_diff(now, last) >= d_ms:
bg_rgb = preset.background_or(colors)
bg = self.driver.apply_brightness(bg_rgb, preset.b)
for i in range(nled):
self.driver.n[i] = bg
aidx = 0
for anchor in range(0, nled, spacing):
tri_i = (phase + aidx * 5) % span
ic_len = tri_i if tri_i <= max_len else span - tri_i
tip_c = colors[aidx % len(colors)]
tip = self.driver.apply_brightness(tip_c, preset.b)
for k in range(ic_len):
idx = anchor + k
if idx >= nled:
break
br = ((k + 1) * 255) // max(1, ic_len)
self.driver.n[idx] = (
(tip[0] * br + bg[0] * (255 - br)) // 255,
(tip[1] * br + bg[1] * (255 - br)) // 255,
(tip[2] * br + bg[2] * (255 - br)) // 255,
)
aidx += 1
self.driver.n.write()
phase = (phase + phase_step) % span
last = utime.ticks_add(last, d_ms)
if not preset.a:
yield
return
yield

72
src/patterns/rime.py Normal file
View File

@@ -0,0 +1,72 @@
import random
import utime
class Rime:
"""Slow frost build-up on a chilly background — gentle random brightening then decay."""
def __init__(self, driver):
self.driver = driver
def run(self, preset):
colors = preset.c if preset.c else [(220, 235, 255), (255, 255, 255), (185, 220, 255)]
num = self.driver.num_leds
if num <= 0:
while True:
yield
return
# n1: spawn tendency (like twinkle upper range)
chill = max(1, min(255, int(preset.n1) if int(preset.n1) > 0 else 36))
# n2: decay per refresh (subtract from glow buffer)
melt = max(1, min(255, int(preset.n2) if int(preset.n2) > 0 else 12))
# n3: how many LEDs can flash brighter per refresh (cap)
spark_cap = max(1, min(num, int(preset.n3) if int(preset.n3) > 0 else 3))
glow = [0] * num
last = utime.ticks_ms()
while True:
d_ms = max(1, int(preset.d))
now = utime.ticks_ms()
if utime.ticks_diff(now, last) >= d_ms:
base_bg = preset.background_or(colors)
bg = self.driver.apply_brightness(base_bg, preset.b)
for i in range(num):
if glow[i] > melt:
glow[i] -= melt
else:
glow[i] = 0
spawned = 0
tries = spark_cap + num // 8
for _ in range(tries):
if spawned >= spark_cap:
break
if random.randint(0, 255) >= chill:
continue
j = random.randint(0, num - 1)
glow[j] = min(255, glow[j] + random.randint(80, 200))
spawned += 1
palette = colors
for i in range(num):
g = glow[i]
fg = palette[i % len(palette)]
hi = self.driver.apply_brightness(fg, preset.b)
mix = max(0, min(255, g))
self.driver.n[i] = (
(hi[0] * mix + bg[0] * (255 - mix)) // 255,
(hi[1] * mix + bg[1] * (255 - mix)) // 255,
(hi[2] * mix + bg[2] * (255 - mix)) // 255,
)
self.driver.n.write()
last = utime.ticks_add(last, d_ms)
if not preset.a:
yield
return
yield

View File

@@ -0,0 +1,43 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets
def run_for(p, wdt, ms):
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
p.tick()
utime.sleep_ms(10)
def main():
s = Settings()
p = Presets(pin=s.get("led_pin", 10), num_leds=s.get("num_leds", 48))
wdt = WDT(timeout=10000)
p.edit(
"test_blizzard",
{
"p": "blizzard",
"b": 220,
"d": 40,
"c": [(255, 255, 255), (200, 220, 255)],
"n1": 100,
"n2": 2,
"n3": 150,
"a": True,
},
)
p.select("test_blizzard")
run_for(p, wdt, 2500)
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 80)
if __name__ == "__main__":
main()

43
tests/patterns/icicles.py Normal file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets
def run_for(p, wdt, ms):
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
p.tick()
utime.sleep_ms(10)
def main():
s = Settings()
p = Presets(pin=s.get("led_pin", 10), num_leds=s.get("num_leds", 40))
wdt = WDT(timeout=10000)
p.edit(
"test_icicles",
{
"p": "icicles",
"b": 220,
"d": 50,
"c": [(200, 230, 255), (255, 255, 255)],
"n1": 10,
"n2": 8,
"n3": 1,
"a": True,
},
)
p.select("test_icicles")
run_for(p, wdt, 2500)
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 80)
if __name__ == "__main__":
main()

43
tests/patterns/rime.py Normal file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env python3
import utime
from machine import WDT
from settings import Settings
from presets import Presets
def run_for(p, wdt, ms):
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < ms:
wdt.feed()
p.tick()
utime.sleep_ms(10)
def main():
s = Settings()
p = Presets(pin=s.get("led_pin", 10), num_leds=s.get("num_leds", 40))
wdt = WDT(timeout=10000)
p.edit(
"test_rime",
{
"p": "rime",
"b": 200,
"d": 80,
"c": [(240, 248, 255), (255, 255, 255)],
"n1": 48,
"n2": 14,
"n3": 4,
"a": True,
},
)
p.select("test_rime")
run_for(p, wdt, 2800)
p.edit("cleanup_off", {"p": "off"})
p.select("cleanup_off")
run_for(p, wdt, 80)
if __name__ == "__main__":
main()