| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268 |
- # -*- coding: utf-8 -*-
- """
- Kreisfoermige LED-Animationen (CONTINUOUS = True).
- Animationen die LEDs um den Ring herum bewegen. Fuer Single-LEDs
- wird auf einfaches Blinken/Pulsieren zurueckgefallen.
- """
- from __future__ import annotations
- 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)
- # Standard-Fade-Stufen fuer den Trail (prozentual)
- _DEFAULT_TRAIL_FACTORS = [0.6, 0.3, 0.1]
- # Chase-Gruppengroesse (Anzahl gleichzeitig leuchtender LEDs)
- _CHASE_WIDTH = 3
- class CirculateAnimation(IAnimation):
- """Eine leuchtende LED wandert um den Ring.
- Bei Jewel: Center-LED bleibt konstant (center_color), nur
- Ring-LEDs animieren. Bei Single-LED: einfaches Blinken.
- """
- NAME = "circulate"
- 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
- # Fallback fuer Single-LED: Blinken
- if len(ring) <= 1:
- yield from _single_fallback_blink(led_count, color)
- return
- while True:
- for pos in range(len(ring)):
- frame = [OFF] * led_count
- # Aktive Ring-LED setzen
- frame[ring[pos]] = color
- # Jewel-Center konstant
- if layout.center_index is not None and center_color is not None:
- frame[layout.center_index] = center_color
- yield frame
- class CircularFadeAnimation(IAnimation):
- """Kreisbewegung mit nachleuchtender Spur.
- Wie Circulate, aber mit einem Schweif hinter der Kopf-LED.
- Die Laenge des Schweifs wird ueber den Parameter ``tail``
- gesteuert (Standard: 3).
- Trail-Helligkeiten: 60%, 30%, 10% (abnehmend).
- """
- NAME = "circular-fade"
- 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]
- tail: int = opts.get("tail", 3) # type: ignore[assignment]
- ring = layout.ring_indices
- ring_len = len(ring)
- # Fallback fuer Single-LED: Blinken
- if ring_len <= 1:
- yield from _single_fallback_blink(led_count, color)
- return
- # Trail-Faktoren: so viele wie tail, ggf. aus Standard erweitern
- trail_factors = _DEFAULT_TRAIL_FACTORS[:tail]
- # Falls tail groesser als Standardliste, mit kleinen Werten auffuellen
- while len(trail_factors) < tail:
- trail_factors.append(0.05)
- while True:
- for pos in range(ring_len):
- frame = [OFF] * led_count
- # Kopf-LED volle Helligkeit
- frame[ring[pos]] = color
- # Schweif-LEDs mit abnehmender Helligkeit
- for t_idx, factor in enumerate(trail_factors):
- trail_pos = (pos - t_idx - 1) % ring_len
- frame[ring[trail_pos]] = _scale_color(color, factor)
- # Jewel-Center konstant
- if layout.center_index is not None and center_color is not None:
- frame[layout.center_index] = center_color
- yield frame
- class ChaseAnimation(IAnimation):
- """Gruppe von LEDs bewegt sich um den Ring.
- Wie Circulate, aber mit 2-3 gleichzeitig leuchtenden LEDs
- die als Gruppe wandern.
- """
- NAME = "chase"
- 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)
- # Fallback fuer Single-LED: Blinken
- if ring_len <= 1:
- yield from _single_fallback_blink(led_count, color)
- return
- # Gruppenbreite an Ringgroesse anpassen (min 2, max _CHASE_WIDTH)
- width = min(_CHASE_WIDTH, max(2, ring_len // 3))
- while True:
- for pos in range(ring_len):
- frame = [OFF] * led_count
- # Alle LEDs der Gruppe einschalten
- for w in range(width):
- idx = (pos + w) % ring_len
- frame[ring[idx]] = color
- # Jewel-Center konstant
- if layout.center_index is not None and center_color is not None:
- frame[layout.center_index] = center_color
- yield frame
- class RadarAnimation(IAnimation):
- """Radar-Sweep mit langem Schweif.
- Wie CircularFade, aber der Schweif erstreckt sich ueber die
- halbe Ringlaenge mit langsam abnehmender Helligkeit.
- Erzeugt einen Radar-Scan-Effekt.
- """
- NAME = "radar"
- 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)
- # Fallback fuer Single-LED: Blinken
- if ring_len <= 1:
- yield from _single_fallback_blink(led_count, color)
- return
- # Schweiflaenge: halber Ring
- tail_length = ring_len // 2
- while True:
- for pos in range(ring_len):
- frame = [OFF] * led_count
- # Kopf-LED volle Helligkeit
- frame[ring[pos]] = color
- # Langer Schweif mit linearem Fade
- for t_idx in range(tail_length):
- # Linearer Abfall von 0.8 bis nahe 0
- factor = 0.8 * (1.0 - (t_idx + 1) / tail_length)
- trail_pos = (pos - t_idx - 1) % ring_len
- frame[ring[trail_pos]] = _scale_color(color, factor)
- # Jewel-Center konstant
- if layout.center_index is not None and center_color is not None:
- frame[layout.center_index] = center_color
- 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.
- 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 _single_fallback_blink(
- led_count: int,
- color: RGB,
- ) -> Iterator[list[RGB]]:
- """Fallback fuer Single-LED: einfaches Endlos-Blinken.
- Args:
- led_count: Anzahl LEDs (sollte 1 sein).
- color: Blinkfarbe.
- Yields:
- Abwechselnd an- und aus-Frames.
- """
- on_frame = [color] * led_count
- off_frame = [OFF] * led_count
- while True:
- yield on_frame
- yield off_frame
- def get_animations() -> list[type[IAnimation]]:
- """Gibt alle kreisfoermigen Animationsklassen zurueck."""
- return [
- CirculateAnimation,
- CircularFadeAnimation,
- ChaseAnimation,
- RadarAnimation,
- ]
|