211 lines
6.4 KiB
Python
211 lines
6.4 KiB
Python
import random
|
||
import utime
|
||
|
||
# Default warm palette: ember → orange → yellow → pale hot (RGB)
|
||
_DEFAULT_PALETTE = (
|
||
(90, 8, 8),
|
||
(200, 40, 12),
|
||
(255, 120, 30),
|
||
(255, 220, 140),
|
||
)
|
||
|
||
|
||
def _clamp(x, lo, hi):
|
||
if x < lo:
|
||
return lo
|
||
if x > hi:
|
||
return hi
|
||
return x
|
||
|
||
|
||
def _lerp_chan(a, b, t):
|
||
return a + ((b - a) * t >> 8)
|
||
|
||
|
||
def _lerp_rgb(c0, c1, t):
|
||
return (
|
||
_lerp_chan(c0[0], c1[0], t),
|
||
_lerp_chan(c0[1], c1[1], t),
|
||
_lerp_chan(c0[2], c1[2], t),
|
||
)
|
||
|
||
|
||
def _palette_sample(palette, pos256):
|
||
n = len(palette)
|
||
if n == 0:
|
||
return (255, 160, 60)
|
||
if n == 1:
|
||
return palette[0]
|
||
span = (n - 1) * pos256
|
||
seg = span >> 8
|
||
if seg >= n - 1:
|
||
return palette[n - 1]
|
||
frac = span & 0xFF
|
||
return _lerp_rgb(palette[seg], palette[seg + 1], frac)
|
||
|
||
|
||
def _triangle_255(elapsed_ms, period_ms):
|
||
period_ms = max(period_ms, 400)
|
||
p = elapsed_ms % period_ms
|
||
half = period_ms >> 1
|
||
if half <= 0:
|
||
return 128
|
||
if p < half:
|
||
return (p * 255) // half
|
||
return ((period_ms - p) * 255) // (period_ms - half)
|
||
|
||
|
||
class Flame:
|
||
def __init__(self, driver):
|
||
self.driver = driver
|
||
|
||
def _build_palette(self, preset):
|
||
colors = preset.c
|
||
if not colors:
|
||
return list(_DEFAULT_PALETTE)
|
||
out = []
|
||
for c in colors:
|
||
if isinstance(c, (list, tuple)) and len(c) == 3:
|
||
out.append(
|
||
(
|
||
_clamp(int(c[0]), 0, 255),
|
||
_clamp(int(c[1]), 0, 255),
|
||
_clamp(int(c[2]), 0, 255),
|
||
)
|
||
)
|
||
return out if out else list(_DEFAULT_PALETTE)
|
||
|
||
def _draw_frame(self, preset, palette, ticks_now, breath_el_ms, rise, cluster_jit, breath_ms, lo, hi, spark_state):
|
||
"""spark_state: (active: bool, start_ticks, duration_ms). ticks_now for sparks; breath_el_ms for slow wave."""
|
||
num = self.driver.num_leds
|
||
denom = num - 1 if num > 1 else 1
|
||
|
||
breathe = _triangle_255(breath_el_ms, breath_ms)
|
||
base_level = lo + (((hi - lo) * breathe) >> 8)
|
||
micro = 232 + random.randint(0, 35)
|
||
level = (base_level * micro) >> 8
|
||
level = _clamp(level, lo, hi)
|
||
|
||
spark_boost = 0
|
||
spark_white = (0, 0, 0)
|
||
active, s0, dur = spark_state
|
||
if active and dur > 0:
|
||
el = utime.ticks_diff(ticks_now, s0)
|
||
if el < 0:
|
||
el = 0
|
||
if el >= dur:
|
||
spark_boost = 0
|
||
else:
|
||
env = 255 - ((el * 255) // dur)
|
||
spark_boost = (env * 90) >> 8
|
||
spark_white = ((env * 55) >> 8, (env * 50) >> 8, (env * 40) >> 8)
|
||
|
||
for i in range(num):
|
||
h = (i * 256) // denom
|
||
flow = (h + rise + ((i // max(1, num >> 3)) * 17)) & 255
|
||
pos = (flow + cluster_jit[(i >> 2) & 7]) & 255
|
||
rgb = _palette_sample(palette, pos)
|
||
if spark_boost:
|
||
rgb = (
|
||
_clamp(rgb[0] + spark_white[0] + (spark_boost * 3 >> 2), 0, 255),
|
||
_clamp(rgb[1] + spark_white[1] + (spark_boost >> 1), 0, 255),
|
||
_clamp(rgb[2] + spark_white[2] + (spark_boost >> 2), 0, 255),
|
||
)
|
||
self.driver.n[i] = self.driver.apply_brightness(rgb, level)
|
||
|
||
self.driver.n.write()
|
||
|
||
def run(self, preset):
|
||
"""Salt-lamp / hearth-style flame: warm gradient, breathing, jitter, drift, rare sparks."""
|
||
palette = self._build_palette(preset)
|
||
lo = max(0, min(255, int(preset.n1)))
|
||
hi = max(0, min(255, int(preset.b)))
|
||
if lo > hi:
|
||
lo, hi = hi, lo
|
||
|
||
bp = int(preset.n2)
|
||
breath_ms = max(800, bp if bp > 0 else 2500)
|
||
|
||
gap_lo = int(preset.n3)
|
||
gap_hi = int(preset.n4)
|
||
# n3 < 0 disables sparks; n3=n4=0 uses ~10–30 s gaps (hearth pops).
|
||
if gap_lo < 0:
|
||
sparks_on = False
|
||
else:
|
||
sparks_on = True
|
||
if gap_lo == 0 and gap_hi == 0:
|
||
gap_lo, gap_hi = 10000, 30000
|
||
else:
|
||
gap_lo = max(gap_lo, 500)
|
||
if gap_hi < gap_lo:
|
||
gap_hi = gap_lo
|
||
|
||
delay_ms = max(16, int(preset.d))
|
||
rise = random.randint(0, 255)
|
||
cluster_jit = [random.randint(-18, 18) for _ in range(8)]
|
||
last_draw = utime.ticks_ms()
|
||
breath_origin = last_draw
|
||
last_cluster = last_draw
|
||
spark_active = False
|
||
spark_start = 0
|
||
spark_dur = 0
|
||
next_spark = utime.ticks_add(last_draw, random.randint(gap_lo, gap_hi)) if sparks_on else 0
|
||
|
||
if not preset.a:
|
||
now = utime.ticks_ms()
|
||
self._draw_frame(
|
||
preset,
|
||
palette,
|
||
now,
|
||
utime.ticks_diff(now, breath_origin),
|
||
rise,
|
||
cluster_jit,
|
||
breath_ms,
|
||
lo,
|
||
hi,
|
||
(False, 0, 0),
|
||
)
|
||
yield
|
||
return
|
||
|
||
while True:
|
||
now = utime.ticks_ms()
|
||
if utime.ticks_diff(now, last_draw) < delay_ms:
|
||
yield
|
||
continue
|
||
last_draw = utime.ticks_add(last_draw, delay_ms)
|
||
|
||
rise = (rise + random.randint(-10, 12)) & 255
|
||
|
||
if utime.ticks_diff(now, last_cluster) >= (delay_ms * 4):
|
||
last_cluster = now
|
||
cluster_jit = [random.randint(-18, 18) for _ in range(8)]
|
||
|
||
spark_state = (spark_active, spark_start, spark_dur)
|
||
if sparks_on:
|
||
if spark_active:
|
||
if utime.ticks_diff(now, spark_start) >= spark_dur:
|
||
spark_active = False
|
||
next_spark = utime.ticks_add(
|
||
now,
|
||
random.randint(gap_lo, gap_hi),
|
||
)
|
||
elif utime.ticks_diff(now, next_spark) >= 0:
|
||
spark_active = True
|
||
spark_start = now
|
||
spark_dur = random.randint(180, 360)
|
||
|
||
self._draw_frame(
|
||
preset,
|
||
palette,
|
||
now,
|
||
utime.ticks_diff(now, breath_origin),
|
||
rise,
|
||
cluster_jit,
|
||
breath_ms,
|
||
lo,
|
||
hi,
|
||
(spark_active, spark_start, spark_dur),
|
||
)
|
||
yield
|