# -*- coding: utf-8 -*- """ Kontinuierliche LED-Animationen (Endlosschleife, CONTINUOUS = True). Enthaelt Blink-, Atmen- und Puls-Animationen die endlos wiederholt werden. """ from __future__ import annotations import math 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) # Schritte pro Zyklus BREATHE_STEPS = 60 PULSE_STEPS = 30 class BlinkAnimation(IAnimation): """Endloses Blinken: an/aus im Wechsel. Bei Jewel: Center-LED bleibt bei center_color (wenn angegeben), Ring-LEDs blinken normal. """ NAME = "blink" 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] on_frame = [color] * led_count if layout.center_index is not None and center_color is not None: on_frame[layout.center_index] = center_color # Aus-Frame: Center behalt center_color wenn gesetzt off_frame = [OFF] * led_count if layout.center_index is not None and center_color is not None: off_frame[layout.center_index] = center_color while True: yield list(on_frame) yield list(off_frame) class BreatheAnimation(IAnimation): """Sanftes Atmen mit Sinuskurve. Helligkeit folgt einer Sinuswelle ueber 60 Frames pro Zyklus (0% → 100% → 0%). Bei Jewel: Center atmet mit halber Intensitaet. """ NAME = "breathe" 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] while True: for step in range(BREATHE_STEPS): # Sinuskurve: 0 → 1 → 0 ueber einen Zyklus brightness = (math.sin(2 * math.pi * step / BREATHE_STEPS - math.pi / 2) + 1) / 2 frame = [_scale_color(color, brightness)] * led_count # Jewel-Center: halbe Intensitaet if layout.center_index is not None: base = center_color if center_color is not None else color frame[layout.center_index] = _scale_color(base, brightness * 0.5) yield frame class PulseAnimation(IAnimation): """Schneller Puls mit Dreieckswelle. Abrupter als Breathe, verwendet eine lineare Dreieckswelle statt einer Sinuskurve. 30 Frames pro Zyklus. """ NAME = "pulse" 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] while True: for step in range(PULSE_STEPS): # Dreieckswelle: 0 → 1 (erste Haelfte), 1 → 0 (zweite Haelfte) half = PULSE_STEPS // 2 if step < half: brightness = step / half else: brightness = (PULSE_STEPS - step) / half frame = [_scale_color(color, brightness)] * led_count # Jewel-Center: gleiche Dreieckswelle, halbe Intensitaet if layout.center_index is not None: base = center_color if center_color is not None else color frame[layout.center_index] = _scale_color(base, brightness * 0.5) yield frame # ── Hilfsfunktionen ────────────────────────────────────────────────────── 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 zwischen 0.0 und 1.0. Returns: Skalierte RGB-Farbe mit geclampten Werten. """ 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 get_animations() -> list[type[IAnimation]]: """Gibt alle kontinuierlichen Animationsklassen zurueck.""" return [ BlinkAnimation, BreatheAnimation, PulseAnimation, ]