Add 5 procedural LED effects, gradient presets, auto-crop min aspect ratio, static source polling optimization

New features:
- Procedural effect source type with fire, meteor, plasma, noise, and aurora algorithms
  using palette LUT system and 1D value noise generator
- 12 predefined gradient presets (rainbow, sunset, ocean, forest, fire, lava, aurora,
  ice, warm, cool, neon, pastel) selectable from a dropdown in the gradient editor
- Auto-crop filter: min aspect ratio parameter to prevent false-positive cropping
  in dark scenes on ultrawide displays

Optimization:
- Static/gradient sources without animation: stream thread sleeps 0.25s instead of
  frame_time; processor repolls at frame_time instead of 5ms (~40x fewer iterations)
- Inverted isinstance checks in routes to test for PictureColorStripSource only

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-23 01:03:16 +03:00
parent 9392741f08
commit a4083764fb
14 changed files with 1033 additions and 19 deletions

View File

@@ -57,6 +57,11 @@ class ColorStripSource:
"animation": None,
"colors": None,
"cycle_speed": None,
"effect_type": None,
"palette": None,
"intensity": None,
"scale": None,
"mirror": None,
}
@staticmethod
@@ -125,6 +130,25 @@ class ColorStripSource:
led_count=data.get("led_count") or 0,
)
if source_type == "effect":
raw_color = data.get("color")
color = (
raw_color if isinstance(raw_color, list) and len(raw_color) == 3
else [255, 80, 0]
)
return EffectColorStripSource(
id=sid, name=name, source_type="effect",
created_at=created_at, updated_at=updated_at, description=description,
effect_type=data.get("effect_type") or "fire",
speed=float(data.get("speed") or 1.0),
led_count=data.get("led_count") or 0,
palette=data.get("palette") or "fire",
color=color,
intensity=float(data.get("intensity") or 1.0),
scale=float(data.get("scale") or 1.0),
mirror=bool(data.get("mirror", False)),
)
# Default: "picture" type
return PictureColorStripSource(
id=sid, name=name, source_type=source_type,
@@ -248,3 +272,34 @@ class ColorCycleColorStripSource(ColorStripSource):
d["cycle_speed"] = self.cycle_speed
d["led_count"] = self.led_count
return d
@dataclass
class EffectColorStripSource(ColorStripSource):
"""Color strip source that runs a procedural LED effect.
The effect_type field selects which algorithm to use:
fire, meteor, plasma, noise, aurora.
LED count auto-sizes from the connected device when led_count == 0.
"""
effect_type: str = "fire" # fire | meteor | plasma | noise | aurora
speed: float = 1.0 # animation speed multiplier (0.110.0)
led_count: int = 0 # 0 = use device LED count
palette: str = "fire" # named color palette
color: list = field(default_factory=lambda: [255, 80, 0]) # [R,G,B] for meteor head
intensity: float = 1.0 # effect-specific intensity (0.12.0)
scale: float = 1.0 # spatial scale / zoom (0.55.0)
mirror: bool = False # bounce mode (meteor)
def to_dict(self) -> dict:
d = super().to_dict()
d["effect_type"] = self.effect_type
d["speed"] = self.speed
d["led_count"] = self.led_count
d["palette"] = self.palette
d["color"] = list(self.color)
d["intensity"] = self.intensity
d["scale"] = self.scale
d["mirror"] = self.mirror
return d

View File

@@ -10,6 +10,7 @@ from wled_controller.core.capture.calibration import CalibrationConfig, calibrat
from wled_controller.storage.color_strip_source import (
ColorCycleColorStripSource,
ColorStripSource,
EffectColorStripSource,
GradientColorStripSource,
PictureColorStripSource,
StaticColorStripSource,
@@ -109,6 +110,12 @@ class ColorStripStore:
animation: Optional[dict] = None,
colors: Optional[list] = None,
cycle_speed: float = 1.0,
effect_type: str = "fire",
speed: float = 1.0,
palette: str = "fire",
intensity: float = 1.0,
scale: float = 1.0,
mirror: bool = False,
) -> ColorStripSource:
"""Create a new color strip source.
@@ -169,6 +176,24 @@ class ColorStripStore:
cycle_speed=float(cycle_speed) if cycle_speed else 1.0,
led_count=led_count,
)
elif source_type == "effect":
rgb = color if isinstance(color, list) and len(color) == 3 else [255, 80, 0]
source = EffectColorStripSource(
id=source_id,
name=name,
source_type="effect",
created_at=now,
updated_at=now,
description=description,
effect_type=effect_type or "fire",
speed=float(speed) if speed else 1.0,
led_count=led_count,
palette=palette or "fire",
color=rgb,
intensity=float(intensity) if intensity else 1.0,
scale=float(scale) if scale else 1.0,
mirror=bool(mirror),
)
else:
if calibration is None:
calibration = CalibrationConfig(layout="clockwise", start_position="bottom_left")
@@ -217,6 +242,12 @@ class ColorStripStore:
animation: Optional[dict] = None,
colors: Optional[list] = None,
cycle_speed: Optional[float] = None,
effect_type: Optional[str] = None,
speed: Optional[float] = None,
palette: Optional[str] = None,
intensity: Optional[float] = None,
scale: Optional[float] = None,
mirror: Optional[bool] = None,
) -> ColorStripSource:
"""Update an existing color strip source.
@@ -280,6 +311,23 @@ class ColorStripStore:
source.cycle_speed = float(cycle_speed)
if led_count is not None:
source.led_count = led_count
elif isinstance(source, EffectColorStripSource):
if effect_type is not None:
source.effect_type = effect_type
if speed is not None:
source.speed = float(speed)
if led_count is not None:
source.led_count = led_count
if palette is not None:
source.palette = palette
if color is not None and isinstance(color, list) and len(color) == 3:
source.color = color
if intensity is not None:
source.intensity = float(intensity)
if scale is not None:
source.scale = float(scale)
if mirror is not None:
source.mirror = bool(mirror)
source.updated_at = datetime.utcnow()
self._save()