# -*- coding: utf-8 -*- """ Spezialeffekt-Animationen (CONTINUOUS = True). Enthaelt visuelle Effekte wie Regenbogen, Welle und Funkeln. """ from __future__ import annotations import colorsys import math import random from collections.abc import Iterator from trixy_core.led.animations import IAnimation, RGB from trixy_core.led.layouts import LEDLayout # Schwarz / aus OFF: RGB = (0, 0, 0) # Regenbogen: Schritte pro voller Rotation _RAINBOW_STEPS = 60 # Wellen-Breite (Anzahl LEDs die gleichzeitig leuchten, Gauss-Kern) _WAVE_WIDTH = 3 # Sparkle: Anzahl zufaellig leuchtender LEDs pro Frame _SPARKLE_MIN = 1 _SPARKLE_MAX = 2 class RainbowAnimation(IAnimation): """Regenbogen-Rotation ueber alle LEDs. Jede LED erhaelt einen anderen Farbton (Hue) aus dem vollen Spektrum. Der Regenbogen rotiert kontinuierlich. Der ``color`` Parameter wird ignoriert (volles Spektrum). Bei Jewel: Center-LED leuchtet gedimmt weiss. """ NAME = "rainbow" CONTINUOUS = True def frames( self, led_count: int, color: RGB, speed: int, layout: LEDLayout, **opts: object, ) -> Iterator[list[RGB]]: ring = layout.ring_indices ring_len = len(ring) while True: for offset in range(_RAINBOW_STEPS): frame = [OFF] * led_count # Jede Ring-LED bekommt eigenen Farbton for i, ring_idx in enumerate(ring): # Hue gleichmaessig verteilt + Offset fuer Rotation hue = (i / ring_len + offset / _RAINBOW_STEPS) % 1.0 frame[ring_idx] = _hsv_to_rgb(hue, 1.0, 1.0) # Jewel-Center: gedimmt weiss if layout.center_index is not None: frame[layout.center_index] = (64, 64, 64) yield frame class WaveAnimation(IAnimation): """Helligkeitswelle wandert hin und her (Knight Rider). Eine 3 LEDs breite Welle mit Gauss-Abfall bewegt sich ueber den Ring hin und zurueck. """ NAME = "wave" CONTINUOUS = True def frames( self, led_count: int, color: RGB, speed: int, layout: LEDLayout, **opts: object, ) -> Iterator[list[RGB]]: center_color: RGB | None = opts.get("center_color") # type: ignore[assignment] ring = layout.ring_indices ring_len = len(ring) if ring_len <= 1: # Fallback: einfaches Pulsieren fuer Single-LED yield from _single_fallback_pulse(led_count, color) return # Gauss-Standardabweichung fuer die Wellenbreite sigma = _WAVE_WIDTH / 2.0 while True: # Hinweg: Position 0 → ring_len-1 for pos in range(ring_len): yield _wave_frame( led_count, color, ring, ring_len, pos, sigma, layout, center_color, ) # Rueckweg: Position ring_len-2 → 1 (Endpunkte nicht doppeln) for pos in range(ring_len - 2, 0, -1): yield _wave_frame( led_count, color, ring, ring_len, pos, sigma, layout, center_color, ) class SparkleAnimation(IAnimation): """Zufaelliges Funkeln einzelner LEDs. Pro Frame leuchten 1-2 zufaellige LEDs in der angegebenen Farbe auf, der Rest bleibt dunkel. Erzeugt einen Sternen-/Funkel-Effekt. """ NAME = "sparkle" CONTINUOUS = True def frames( self, led_count: int, color: RGB, speed: int, layout: LEDLayout, **opts: object, ) -> Iterator[list[RGB]]: center_color: RGB | None = opts.get("center_color") # type: ignore[assignment] ring = layout.ring_indices # Dunkler Hintergrund (leichtes Glimmen) dim: RGB = _scale_color(color, 0.05) while True: frame = [dim] * led_count # Zufaellige Ring-LEDs zum Funkeln auswaehlen sparkle_count = random.randint(_SPARKLE_MIN, min(_SPARKLE_MAX, len(ring))) sparkle_indices = random.sample(ring, sparkle_count) for idx in sparkle_indices: frame[idx] = color # Jewel-Center: konstant bei center_color oder dunkel if layout.center_index is not None: if center_color is not None: frame[layout.center_index] = center_color else: frame[layout.center_index] = dim yield frame # ── Hilfsfunktionen ────────────────────────────────────────────────────── def _hsv_to_rgb(h: float, s: float, v: float) -> RGB: """Konvertiert HSV-Farbe (0.0-1.0) nach RGB (0-255). Args: h: Farbton (Hue) 0.0 - 1.0. s: Saettigung 0.0 - 1.0. v: Helligkeit (Value) 0.0 - 1.0. Returns: RGB-Tupel mit Werten 0-255. """ r, g, b = colorsys.hsv_to_rgb(h, s, v) return (int(r * 255), int(g * 255), int(b * 255)) def _scale_color(color: RGB, factor: float) -> RGB: """Skaliert eine RGB-Farbe um einen Faktor (0.0 - 1.0). Args: color: Ausgangsfarbe als (R, G, B). factor: Helligkeitsfaktor. Returns: Skalierte RGB-Farbe. """ return ( min(255, max(0, int(color[0] * factor))), min(255, max(0, int(color[1] * factor))), min(255, max(0, int(color[2] * factor))), ) def _wave_frame( led_count: int, color: RGB, ring: list[int], ring_len: int, pos: int, sigma: float, layout: LEDLayout, center_color: RGB | None, ) -> list[RGB]: """Erzeugt einen einzelnen Wellen-Frame. Berechnet fuer jede Ring-LED die Helligkeit anhand einer Gauss-Verteilung um die aktuelle Position. Args: led_count: Gesamtzahl LEDs. color: Wellenfarbe. ring: Ring-Indizes. ring_len: Anzahl Ring-LEDs. pos: Aktuelle Wellenposition (Index in ring). sigma: Gauss-Standardabweichung. layout: LED-Layout. center_color: Optionale Center-Farbe. Returns: Frame als Liste von RGB-Tupeln. """ frame: list[RGB] = [OFF] * led_count for i in range(ring_len): # Gauss-Abfall: Helligkeit nimmt mit Abstand vom Zentrum ab distance = abs(i - pos) brightness = math.exp(-(distance ** 2) / (2 * sigma ** 2)) if brightness > 0.01: frame[ring[i]] = _scale_color(color, brightness) # Jewel-Center if layout.center_index is not None and center_color is not None: frame[layout.center_index] = center_color return frame def _single_fallback_pulse( led_count: int, color: RGB, ) -> Iterator[list[RGB]]: """Fallback fuer Single-LED: einfaches Pulsieren. Args: led_count: Anzahl LEDs. color: Pulsfarbe. Yields: Frames mit variierender Helligkeit. """ steps = 30 while True: for step in range(steps): # Dreieckswelle half = steps // 2 if step < half: brightness = step / half else: brightness = (steps - step) / half yield [_scale_color(color, brightness)] * led_count def get_animations() -> list[type[IAnimation]]: """Gibt alle Spezialeffekt-Animationsklassen zurueck.""" return [ RainbowAnimation, WaveAnimation, SparkleAnimation, ]