continuous.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. # -*- coding: utf-8 -*-
  2. """
  3. Kontinuierliche LED-Animationen (Endlosschleife, CONTINUOUS = True).
  4. Enthaelt Blink-, Atmen- und Puls-Animationen die endlos wiederholt werden.
  5. """
  6. from __future__ import annotations
  7. import math
  8. from collections.abc import Iterator
  9. from trixy_core.led.animations import IAnimation, RGB
  10. from trixy_core.led.layouts import LEDLayout
  11. # Schwarz / aus
  12. OFF: RGB = (0, 0, 0)
  13. # Schritte pro Zyklus
  14. BREATHE_STEPS = 60
  15. PULSE_STEPS = 30
  16. class BlinkAnimation(IAnimation):
  17. """Endloses Blinken: an/aus im Wechsel.
  18. Bei Jewel: Center-LED bleibt bei center_color (wenn angegeben),
  19. Ring-LEDs blinken normal.
  20. """
  21. NAME = "blink"
  22. CONTINUOUS = True
  23. def frames(
  24. self,
  25. led_count: int,
  26. color: RGB,
  27. speed: int,
  28. layout: LEDLayout,
  29. **opts: object,
  30. ) -> Iterator[list[RGB]]:
  31. center_color: RGB | None = opts.get("center_color") # type: ignore[assignment]
  32. on_frame = [color] * led_count
  33. if layout.center_index is not None and center_color is not None:
  34. on_frame[layout.center_index] = center_color
  35. # Aus-Frame: Center behalt center_color wenn gesetzt
  36. off_frame = [OFF] * led_count
  37. if layout.center_index is not None and center_color is not None:
  38. off_frame[layout.center_index] = center_color
  39. while True:
  40. yield list(on_frame)
  41. yield list(off_frame)
  42. class BreatheAnimation(IAnimation):
  43. """Sanftes Atmen mit Sinuskurve.
  44. Helligkeit folgt einer Sinuswelle ueber 60 Frames pro Zyklus
  45. (0% → 100% → 0%). Bei Jewel: Center atmet mit halber Intensitaet.
  46. """
  47. NAME = "breathe"
  48. CONTINUOUS = True
  49. def frames(
  50. self,
  51. led_count: int,
  52. color: RGB,
  53. speed: int,
  54. layout: LEDLayout,
  55. **opts: object,
  56. ) -> Iterator[list[RGB]]:
  57. center_color: RGB | None = opts.get("center_color") # type: ignore[assignment]
  58. while True:
  59. for step in range(BREATHE_STEPS):
  60. # Sinuskurve: 0 → 1 → 0 ueber einen Zyklus
  61. brightness = (math.sin(2 * math.pi * step / BREATHE_STEPS - math.pi / 2) + 1) / 2
  62. frame = [_scale_color(color, brightness)] * led_count
  63. # Jewel-Center: halbe Intensitaet
  64. if layout.center_index is not None:
  65. base = center_color if center_color is not None else color
  66. frame[layout.center_index] = _scale_color(base, brightness * 0.5)
  67. yield frame
  68. class PulseAnimation(IAnimation):
  69. """Schneller Puls mit Dreieckswelle.
  70. Abrupter als Breathe, verwendet eine lineare Dreieckswelle
  71. statt einer Sinuskurve. 30 Frames pro Zyklus.
  72. """
  73. NAME = "pulse"
  74. CONTINUOUS = True
  75. def frames(
  76. self,
  77. led_count: int,
  78. color: RGB,
  79. speed: int,
  80. layout: LEDLayout,
  81. **opts: object,
  82. ) -> Iterator[list[RGB]]:
  83. center_color: RGB | None = opts.get("center_color") # type: ignore[assignment]
  84. while True:
  85. for step in range(PULSE_STEPS):
  86. # Dreieckswelle: 0 → 1 (erste Haelfte), 1 → 0 (zweite Haelfte)
  87. half = PULSE_STEPS // 2
  88. if step < half:
  89. brightness = step / half
  90. else:
  91. brightness = (PULSE_STEPS - step) / half
  92. frame = [_scale_color(color, brightness)] * led_count
  93. # Jewel-Center: gleiche Dreieckswelle, halbe Intensitaet
  94. if layout.center_index is not None:
  95. base = center_color if center_color is not None else color
  96. frame[layout.center_index] = _scale_color(base, brightness * 0.5)
  97. yield frame
  98. # ── Hilfsfunktionen ──────────────────────────────────────────────────────
  99. def _scale_color(color: RGB, factor: float) -> RGB:
  100. """Skaliert eine RGB-Farbe um einen Faktor (0.0 - 1.0).
  101. Args:
  102. color: Ausgangsfarbe als (R, G, B).
  103. factor: Helligkeitsfaktor zwischen 0.0 und 1.0.
  104. Returns:
  105. Skalierte RGB-Farbe mit geclampten Werten.
  106. """
  107. return (
  108. min(255, max(0, int(color[0] * factor))),
  109. min(255, max(0, int(color[1] * factor))),
  110. min(255, max(0, int(color[2] * factor))),
  111. )
  112. def get_animations() -> list[type[IAnimation]]:
  113. """Gibt alle kontinuierlichen Animationsklassen zurueck."""
  114. return [
  115. BlinkAnimation,
  116. BreatheAnimation,
  117. PulseAnimation,
  118. ]