feat(patterns): add twinkle pattern defaults
Made-with: Cursor
This commit is contained in:
1
presets.json
Normal file
1
presets.json
Normal file
@@ -0,0 +1 @@
|
||||
{"15": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[255, 0, 0], [0, 255, 0]], "b": 255, "n2": 0, "n1": 0, "p": "blink", "n3": 0, "d": 500}, "40": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[255, 255, 0]], "b": 255, "n2": 2600, "n1": 35, "p": "flame", "n3": 0, "d": 50}, "41": {"n5": 0, "n4": 5, "a": true, "n6": 0, "c": [[120, 200, 255], [80, 140, 255], [180, 120, 255], [100, 220, 232], [160, 200, 255]], "b": 255, "n2": 10, "n1": 72, "p": "twinkle", "n3": 5, "d": 500}, "42": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[166, 0, 255], [0, 10, 10]], "b": 255, "n2": 900, "n1": 30, "p": "radiate", "n3": 4000, "d": 5000}, "6": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[0, 255, 0]], "b": 255, "n2": 500, "n1": 1000, "p": "pulse", "n3": 1000, "d": 500}, "10": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[230, 242, 255]], "b": 200, "n2": 0, "n1": 0, "p": "on", "n3": 0, "d": 100}, "13": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[255, 255, 255]], "b": 255, "n2": 0, "n1": 1, "p": "rainbow", "n3": 0, "d": 150}, "3": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[255, 255, 255]], "b": 255, "n2": 0, "n1": 2, "p": "rainbow", "n3": 0, "d": 100}, "2": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[255, 255, 255]], "b": 0, "n2": 0, "n1": 0, "p": "off", "n3": 0, "d": 100}, "38": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[255, 0, 0], [0, 0, 255]], "b": 255, "n2": 0, "n1": 1, "p": "colour_cycle", "n3": 0, "d": 100}, "11": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[255, 0, 0]], "b": 255, "n2": 0, "n1": 0, "p": "on", "n3": 0, "d": 100}, "12": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[0, 0, 255]], "b": 255, "n2": 0, "n1": 0, "p": "on", "n3": 0, "d": 100}, "1": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[255, 255, 255]], "b": 255, "n2": 0, "n1": 0, "p": "on", "n3": 0, "d": 100}, "9": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[255, 245, 230]], "b": 200, "n2": 0, "n1": 0, "p": "on", "n3": 0, "d": 100}, "8": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[255, 0, 0], [0, 255, 0], [0, 0, 255], [255, 255, 0]], "b": 255, "n2": 0, "n1": 0, "p": "blink", "n3": 0, "d": 1000}, "39": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[255, 184, 77]], "b": 255, "n2": 0, "n1": 30, "p": "flicker", "n3": 0, "d": 80}, "14": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[255, 102, 0]], "b": 255, "n2": 1000, "n1": 2000, "p": "pulse", "n3": 2000, "d": 800}, "5": {"n5": 0, "n4": 1, "a": true, "n6": 0, "c": [[255, 0, 0], [0, 0, 255]], "b": 255, "n2": 5, "n1": 5, "p": "chase", "n3": 1, "d": 200}, "4": {"n5": 0, "n4": 0, "a": true, "n6": 0, "c": [[255, 0, 0], [0, 255, 0], [0, 0, 255], [255, 255, 255], [0, 0, 255], [255, 255, 0]], "b": 255, "n2": 0, "n1": 0, "p": "transition", "n3": 0, "d": 5000}, "7": {"n5": 0, "n4": 5, "a": true, "n6": 0, "c": [[255, 165, 0], [128, 0, 128]], "b": 255, "n2": 10, "n1": 2, "p": "circle", "n3": 2, "d": 200}}
|
||||
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