feat(patterns): add icicles blizzard and rime winter effects
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
65
src/patterns/blizzard.py
Normal file
65
src/patterns/blizzard.py
Normal 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 (0–255 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
62
src/patterns/icicles.py
Normal 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
72
src/patterns/rime.py
Normal 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
|
||||||
43
tests/patterns/blizzard.py
Normal file
43
tests/patterns/blizzard.py
Normal 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
43
tests/patterns/icicles.py
Normal 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
43
tests/patterns/rime.py
Normal 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()
|
||||||
Reference in New Issue
Block a user