145 lines
4.2 KiB
Python
145 lines
4.2 KiB
Python
"""Compatibility pattern helpers for NeoPixel demos.
|
|
|
|
This file mirrors `workspace/code/led_patterns.py` so imports like
|
|
`from led_patterns import ...` work even in older worker sessions that only
|
|
include `/workspace/lib` in `sys.path`.
|
|
"""
|
|
|
|
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:
|
|
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]:
|
|
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]:
|
|
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]:
|
|
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
|