| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274 |
- # -*- 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,
- ]
|