feat(patterns): add twinkle pattern defaults
Made-with: Cursor
This commit is contained in:
227
src/patterns/twinkle.py
Normal file
227
src/patterns/twinkle.py
Normal file
@@ -0,0 +1,227 @@
|
||||
import random
|
||||
import utime
|
||||
|
||||
# Default cool palette (icy blues, violet, mint) when preset has no colours.
|
||||
# When `driver.debug` is True, print stats every N twinkle ticks (serial can be slow).
|
||||
_TWINKLE_DBG_INTERVAL = 40
|
||||
|
||||
_DEFAULT_COOL = (
|
||||
(120, 200, 255),
|
||||
(80, 140, 255),
|
||||
(180, 120, 255),
|
||||
(100, 220, 240),
|
||||
(160, 200, 255),
|
||||
(90, 180, 220),
|
||||
)
|
||||
|
||||
|
||||
class Twinkle:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def _palette(self, preset):
|
||||
colors = preset.c
|
||||
if not colors:
|
||||
return list(_DEFAULT_COOL)
|
||||
out = []
|
||||
for c in colors:
|
||||
if isinstance(c, (list, tuple)) and len(c) == 3:
|
||||
out.append(
|
||||
(
|
||||
max(0, min(255, int(c[0]))),
|
||||
max(0, min(255, int(c[1]))),
|
||||
max(0, min(255, int(c[2]))),
|
||||
)
|
||||
)
|
||||
return out if out else list(_DEFAULT_COOL)
|
||||
|
||||
def run(self, preset):
|
||||
"""Twinkle: n1 activity, n2 density; n3/n4 min/max length of adjacent on/off runs."""
|
||||
palette = self._palette(preset)
|
||||
num = self.driver.num_leds
|
||||
if num <= 0:
|
||||
while True:
|
||||
yield
|
||||
return
|
||||
|
||||
def activity_rate():
|
||||
r = int(preset.n1)
|
||||
if r <= 0:
|
||||
r = 48
|
||||
return max(1, min(255, r))
|
||||
|
||||
def density255():
|
||||
"""Higher → more LEDs lit on average when a twinkle step fires (0 = default mid)."""
|
||||
d = int(preset.n2)
|
||||
if d <= 0:
|
||||
d = 128
|
||||
return max(0, min(255, d))
|
||||
|
||||
def cluster_len_bounds():
|
||||
"""n3 = min adjacent LEDs per twinkle, n4 = max (both 0 → 1..4)."""
|
||||
lo = int(preset.n3)
|
||||
hi = int(preset.n4)
|
||||
if lo <= 0 and hi <= 0:
|
||||
lo, hi = 1, min(4, num)
|
||||
else:
|
||||
if lo <= 0:
|
||||
lo = 1
|
||||
if hi <= 0:
|
||||
hi = lo
|
||||
if hi < lo:
|
||||
lo, hi = hi, lo
|
||||
lo = max(1, min(lo, num))
|
||||
hi = max(lo, min(hi, num))
|
||||
return lo, hi
|
||||
|
||||
def random_cluster_len():
|
||||
lo, hi = cluster_len_bounds()
|
||||
# When min and max match, every lit/dim run is exactly that many LEDs (still capped by strip length).
|
||||
if lo == hi:
|
||||
return lo
|
||||
return random.randint(lo, hi)
|
||||
|
||||
def cluster_base_index(start, k):
|
||||
"""Shift run left so a length-k segment fits; keeps full k when num >= k."""
|
||||
k = min(max(0, int(k)), num)
|
||||
if k <= 0:
|
||||
return 0
|
||||
return max(0, min(int(start), num - k))
|
||||
|
||||
dens = density255()
|
||||
on = [random.randint(0, 255) < dens for _ in range(num)]
|
||||
colour_i = [random.randint(0, len(palette) - 1) for _ in range(num)]
|
||||
last_update = utime.ticks_ms()
|
||||
dbg_tick = 0
|
||||
dbg_banner = False
|
||||
|
||||
def on_run_min_max(bits):
|
||||
"""Smallest and largest contiguous run of True in bits (0,0 if all off)."""
|
||||
best_min = num + 1
|
||||
best_max = 0
|
||||
cur = 0
|
||||
for j in range(num):
|
||||
if bits[j]:
|
||||
cur += 1
|
||||
else:
|
||||
if cur:
|
||||
if cur < best_min:
|
||||
best_min = cur
|
||||
if cur > best_max:
|
||||
best_max = cur
|
||||
cur = 0
|
||||
if cur:
|
||||
if cur < best_min:
|
||||
best_min = cur
|
||||
if cur > best_max:
|
||||
best_max = cur
|
||||
if best_min == num + 1:
|
||||
return 0, 0
|
||||
return best_min, best_max
|
||||
|
||||
if not preset.a:
|
||||
for i in range(num):
|
||||
if on[i]:
|
||||
base = palette[colour_i[i] % len(palette)]
|
||||
self.driver.n[i] = self.driver.apply_brightness(base, preset.b)
|
||||
else:
|
||||
self.driver.n[i] = (0, 0, 0)
|
||||
self.driver.n.write()
|
||||
yield
|
||||
return
|
||||
|
||||
while True:
|
||||
now = utime.ticks_ms()
|
||||
delay_ms = max(1, int(preset.d))
|
||||
if utime.ticks_diff(now, last_update) >= delay_ms:
|
||||
rate = activity_rate()
|
||||
dens = density255()
|
||||
dbg = bool(getattr(self.driver, "debug", False))
|
||||
dbg_tick += 1
|
||||
# Snapshot for decisions; apply all darks then all lights so
|
||||
# overlaps in the same tick favour lit runs (lights win).
|
||||
prev_on = on[:]
|
||||
prev_ci = colour_i[:]
|
||||
next_on = list(prev_on)
|
||||
next_ci = list(prev_ci)
|
||||
dbg_ops = {"L": 0, "D": 0}
|
||||
|
||||
light_i = []
|
||||
dark_i = []
|
||||
for i in range(num):
|
||||
if random.randint(0, 255) < rate:
|
||||
r = random.randint(0, 255)
|
||||
if not prev_on[i]:
|
||||
if r < dens:
|
||||
light_i.append(i)
|
||||
else:
|
||||
if r < (255 - dens):
|
||||
dark_i.append(i)
|
||||
|
||||
def light_adjacent(start):
|
||||
dbg_ops["L"] += 1
|
||||
k = random_cluster_len()
|
||||
b = cluster_base_index(start, k)
|
||||
for dj in range(k):
|
||||
idx = b + dj
|
||||
next_on[idx] = True
|
||||
next_ci[idx] = random.randint(0, len(palette) - 1)
|
||||
|
||||
def dark_adjacent(start):
|
||||
dbg_ops["D"] += 1
|
||||
k = random_cluster_len()
|
||||
b = cluster_base_index(start, k)
|
||||
for dj in range(k):
|
||||
idx = b + dj
|
||||
next_on[idx] = False
|
||||
|
||||
for i in dark_i:
|
||||
dark_adjacent(i)
|
||||
for i in light_i:
|
||||
light_adjacent(i)
|
||||
|
||||
for i in range(num):
|
||||
if next_on[i]:
|
||||
base = palette[next_ci[i] % len(palette)]
|
||||
self.driver.n[i] = self.driver.apply_brightness(base, preset.b)
|
||||
else:
|
||||
self.driver.n[i] = (0, 0, 0)
|
||||
self.driver.n.write()
|
||||
on = next_on
|
||||
colour_i = next_ci
|
||||
last_update = utime.ticks_add(last_update, delay_ms)
|
||||
|
||||
if dbg:
|
||||
lo, hi = cluster_len_bounds()
|
||||
if not dbg_banner:
|
||||
dbg_banner = True
|
||||
print(
|
||||
"[twinkle] debug on: n1=%s n2=%s n3=%s n4=%s d=%s -> lo=%d hi=%d num=%d"
|
||||
% (
|
||||
preset.n1,
|
||||
preset.n2,
|
||||
preset.n3,
|
||||
preset.n4,
|
||||
preset.d,
|
||||
lo,
|
||||
hi,
|
||||
num,
|
||||
)
|
||||
)
|
||||
rmin, rmax = on_run_min_max(on)
|
||||
bad = lo > 0 and rmin > 0 and rmin < lo and num >= lo
|
||||
if bad or (dbg_tick % _TWINKLE_DBG_INTERVAL == 0):
|
||||
print(
|
||||
"[twinkle] tick=%d rate=%d dens=%d L=%d D=%d on_runs min=%d max=%d%s"
|
||||
% (
|
||||
dbg_tick,
|
||||
rate,
|
||||
dens,
|
||||
dbg_ops["L"],
|
||||
dbg_ops["D"],
|
||||
rmin,
|
||||
rmax,
|
||||
" **run<lo**" if bad else "",
|
||||
)
|
||||
)
|
||||
yield
|
||||
Reference in New Issue
Block a user