Files
python-editor/workspace/code/led_patterns.py

144 lines
4.2 KiB
Python

"""LED pattern helpers inspired by embedded NeoPixel drivers."""
from __future__ import annotations
import random
Color = tuple[int, int, int]
def _clamp(channel: int) -> int:
return max(0, min(255, int(channel)))
def wheel(pos: int) -> Color:
"""Return a rainbow color for position 0-255."""
pos = 255 - (pos & 255)
if pos < 85:
return (_clamp(255 - pos * 3), 0, _clamp(pos * 3))
if pos < 170:
pos -= 85
return (0, _clamp(pos * 3), _clamp(255 - pos * 3))
pos -= 170
return (_clamp(pos * 3), _clamp(255 - pos * 3), 0)
def rainbow_frame(led_count: int, frame: int, step: int = 4) -> list[Color]:
"""Generate one rainbow frame across all LEDs."""
if led_count <= 0:
return []
return [wheel((i * 256 // led_count + frame * step) & 255) for i in range(led_count)]
def chase_frame(
led_count: int,
frame: int,
color: Color = (255, 120, 0),
tail: Color = (16, 0, 0),
) -> list[Color]:
"""Generate a two-pixel chase pattern."""
if led_count <= 0:
return []
out: list[Color] = [(0, 0, 0) for _ in range(led_count)]
head = frame % led_count
trail = (head - 1) % led_count
out[trail] = tuple(_clamp(v) for v in tail) # type: ignore[assignment]
out[head] = tuple(_clamp(v) for v in color) # type: ignore[assignment]
return out
def _bounce_head_index(led_count: int, frame: int) -> int:
"""Map frame to a triangular index sweep 0..N-1..0 (Ping-Pong position)."""
if led_count <= 1:
return 0
span = led_count - 1
cycle = span * 2
if cycle <= 0:
return 0
t = frame % cycle
return t if t <= span else 2 * span - t
def _bounce_phase_tail_direction(led_count: int, frame: int) -> int:
"""Extend tail opposite motion: -1 fades toward lower indices, +1 toward higher."""
if led_count <= 1:
return -1
span = led_count - 1
cycle = span * 2
if cycle <= 0:
return -1
t = frame % cycle
if t <= span:
return -1
return 1
def knight_rider_scanner_frame(
led_count: int,
frame: int,
head_color: Color = (220, 0, 28),
tail_len: int = 8,
falloff_gamma: float = 2.6,
) -> list[Color]:
"""KITT-style bouncing scanner: saturated head with exponential tail fading to off."""
if led_count <= 0:
return []
out: list[Color] = [(0, 0, 0) for _ in range(led_count)]
tl = max(1, tail_len)
head = _bounce_head_index(led_count, frame)
direc = _bounce_phase_tail_direction(led_count, frame)
gamma = max(1.05, falloff_gamma)
for rk in reversed(range(tl)):
idx = head + direc * rk
if idx < 0 or idx >= led_count:
continue
w = max(0.0, float(tl - rk) / float(tl))
strength = w**gamma
out[idx] = tuple(_clamp(int(head_color[ch] * strength)) for ch in range(3))
return out
def scanner_bounce_frame(
led_count: int,
frame: int,
head_color: Color = (0, 220, 255),
tail_color: Color = (0, 40, 90),
tail_len: int = 5,
) -> list[Color]:
"""Ping-pong scanner: head reverses at both ends with a directional fading tail."""
if led_count <= 0:
return []
out: list[Color] = [(0, 0, 0) for _ in range(led_count)]
tl = max(1, tail_len)
for rk in reversed(range(tl)):
past = frame - rk
if past < 0:
continue
idx = _bounce_head_index(led_count, past)
strength = max(0.0, float(tl - rk) / float(tl))
if rk == 0:
out[idx] = tuple(_clamp(int(c)) for c in head_color)
else:
out[idx] = tuple(_clamp(int(tail_color[i] * strength)) for i in range(3))
return out
def twinkle_frame(
led_count: int,
frame: int,
base: Color = (0, 0, 8),
sparkle: Color = (255, 255, 180),
sparkles: int = 3,
seed: int = 1337,
) -> list[Color]:
"""Generate deterministic twinkle frames for testing/replay."""
if led_count <= 0:
return []
out: list[Color] = [tuple(_clamp(v) for v in base) for _ in range(led_count)] # type: ignore[list-item]
rng = random.Random(seed + frame)
for _ in range(min(max(0, sparkles), led_count)):
idx = rng.randrange(led_count)
out[idx] = tuple(_clamp(v) for v in sparkle) # type: ignore[assignment]
return out