"""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