Add color_cycle as standalone source type; UI polish
- color_cycle is now a top-level source type (alongside picture/static/gradient) with a configurable color list and cycle_speed; defaults to full rainbow spectrum - ColorCycleColorStripSource + ColorCycleColorStripStream: smooth 30 fps interpolation between user-defined colors, one full cycle every 20s at speed=1.0 - Removed color_cycle animation sub-type from StaticColorStripStream - Color cycle editor: compact horizontal swatch layout, proper module-scope fix (colorCycleAdd/Remove now exposed on window, DOM-synced before mutations) - Animation enabled + Frame interpolation checkboxes use toggle-switch style - Removed Potential FPS metric from targets and KC targets metric grids Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,9 +5,10 @@ from some input, encapsulating everything needed to drive a physical LED strip:
|
||||
calibration, color correction, smoothing, and FPS.
|
||||
|
||||
Current types:
|
||||
PictureColorStripSource — derives LED colors from a PictureSource (screen capture)
|
||||
StaticColorStripSource — constant solid color fills all LEDs
|
||||
GradientColorStripSource — linear gradient across all LEDs from user-defined color stops
|
||||
PictureColorStripSource — derives LED colors from a PictureSource (screen capture)
|
||||
StaticColorStripSource — constant solid color fills all LEDs
|
||||
GradientColorStripSource — linear gradient across all LEDs from user-defined color stops
|
||||
ColorCycleColorStripSource — smoothly cycles through a user-defined list of colors
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
@@ -53,6 +54,9 @@ class ColorStripSource:
|
||||
"led_count": None,
|
||||
"color": None,
|
||||
"stops": None,
|
||||
"animation": None,
|
||||
"colors": None,
|
||||
"cycle_speed": None,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -96,6 +100,7 @@ class ColorStripSource:
|
||||
created_at=created_at, updated_at=updated_at, description=description,
|
||||
color=color,
|
||||
led_count=data.get("led_count") or 0,
|
||||
animation=data.get("animation"),
|
||||
)
|
||||
|
||||
if source_type == "gradient":
|
||||
@@ -106,6 +111,18 @@ class ColorStripSource:
|
||||
created_at=created_at, updated_at=updated_at, description=description,
|
||||
stops=stops,
|
||||
led_count=data.get("led_count") or 0,
|
||||
animation=data.get("animation"),
|
||||
)
|
||||
|
||||
if source_type == "color_cycle":
|
||||
raw_colors = data.get("colors")
|
||||
colors = raw_colors if isinstance(raw_colors, list) else []
|
||||
return ColorCycleColorStripSource(
|
||||
id=sid, name=name, source_type="color_cycle",
|
||||
created_at=created_at, updated_at=updated_at, description=description,
|
||||
colors=colors,
|
||||
cycle_speed=float(data.get("cycle_speed") or 1.0),
|
||||
led_count=data.get("led_count") or 0,
|
||||
)
|
||||
|
||||
# Default: "picture" type
|
||||
@@ -172,11 +189,13 @@ class StaticColorStripSource(ColorStripSource):
|
||||
|
||||
color: list = field(default_factory=lambda: [255, 255, 255]) # [R, G, B]
|
||||
led_count: int = 0 # 0 = use device LED count
|
||||
animation: Optional[dict] = None # {"enabled": bool, "type": str, "speed": float} or None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
d = super().to_dict()
|
||||
d["color"] = list(self.color)
|
||||
d["led_count"] = self.led_count
|
||||
d["animation"] = self.animation
|
||||
return d
|
||||
|
||||
|
||||
@@ -197,9 +216,35 @@ class GradientColorStripSource(ColorStripSource):
|
||||
{"position": 1.0, "color": [0, 0, 255]},
|
||||
])
|
||||
led_count: int = 0 # 0 = use device LED count
|
||||
animation: Optional[dict] = None # {"enabled": bool, "type": str, "speed": float} or None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
d = super().to_dict()
|
||||
d["stops"] = [dict(s) for s in self.stops]
|
||||
d["led_count"] = self.led_count
|
||||
d["animation"] = self.animation
|
||||
return d
|
||||
|
||||
|
||||
@dataclass
|
||||
class ColorCycleColorStripSource(ColorStripSource):
|
||||
"""Color strip source that smoothly cycles through a user-defined list of colors.
|
||||
|
||||
All LEDs receive the same solid color at any point in time, smoothly
|
||||
interpolating between the configured color stops in a continuous loop.
|
||||
LED count auto-sizes from the connected device when led_count == 0.
|
||||
"""
|
||||
|
||||
colors: list = field(default_factory=lambda: [
|
||||
[255, 0, 0], [255, 255, 0], [0, 255, 0],
|
||||
[0, 255, 255], [0, 0, 255], [255, 0, 255],
|
||||
])
|
||||
cycle_speed: float = 1.0 # speed multiplier; 1.0 ≈ one full cycle every 20 seconds
|
||||
led_count: int = 0 # 0 = use device LED count
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
d = super().to_dict()
|
||||
d["colors"] = [list(c) for c in self.colors]
|
||||
d["cycle_speed"] = self.cycle_speed
|
||||
d["led_count"] = self.led_count
|
||||
return d
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import Dict, List, Optional
|
||||
|
||||
from wled_controller.core.capture.calibration import CalibrationConfig, calibration_to_dict
|
||||
from wled_controller.storage.color_strip_source import (
|
||||
ColorCycleColorStripSource,
|
||||
ColorStripSource,
|
||||
GradientColorStripSource,
|
||||
PictureColorStripSource,
|
||||
@@ -105,6 +106,9 @@ class ColorStripStore:
|
||||
stops: Optional[list] = None,
|
||||
description: Optional[str] = None,
|
||||
frame_interpolation: bool = False,
|
||||
animation: Optional[dict] = None,
|
||||
colors: Optional[list] = None,
|
||||
cycle_speed: float = 1.0,
|
||||
) -> ColorStripSource:
|
||||
"""Create a new color strip source.
|
||||
|
||||
@@ -132,6 +136,7 @@ class ColorStripStore:
|
||||
description=description,
|
||||
color=rgb,
|
||||
led_count=led_count,
|
||||
animation=animation,
|
||||
)
|
||||
elif source_type == "gradient":
|
||||
source = GradientColorStripSource(
|
||||
@@ -146,6 +151,23 @@ class ColorStripStore:
|
||||
{"position": 1.0, "color": [0, 0, 255]},
|
||||
],
|
||||
led_count=led_count,
|
||||
animation=animation,
|
||||
)
|
||||
elif source_type == "color_cycle":
|
||||
default_colors = [
|
||||
[255, 0, 0], [255, 255, 0], [0, 255, 0],
|
||||
[0, 255, 255], [0, 0, 255], [255, 0, 255],
|
||||
]
|
||||
source = ColorCycleColorStripSource(
|
||||
id=source_id,
|
||||
name=name,
|
||||
source_type="color_cycle",
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
description=description,
|
||||
colors=colors if isinstance(colors, list) and len(colors) >= 2 else default_colors,
|
||||
cycle_speed=float(cycle_speed) if cycle_speed else 1.0,
|
||||
led_count=led_count,
|
||||
)
|
||||
else:
|
||||
if calibration is None:
|
||||
@@ -192,6 +214,9 @@ class ColorStripStore:
|
||||
stops: Optional[list] = None,
|
||||
description: Optional[str] = None,
|
||||
frame_interpolation: Optional[bool] = None,
|
||||
animation: Optional[dict] = None,
|
||||
colors: Optional[list] = None,
|
||||
cycle_speed: Optional[float] = None,
|
||||
) -> ColorStripSource:
|
||||
"""Update an existing color strip source.
|
||||
|
||||
@@ -239,11 +264,22 @@ class ColorStripStore:
|
||||
source.color = color
|
||||
if led_count is not None:
|
||||
source.led_count = led_count
|
||||
if animation is not None:
|
||||
source.animation = animation
|
||||
elif isinstance(source, GradientColorStripSource):
|
||||
if stops is not None and isinstance(stops, list):
|
||||
source.stops = stops
|
||||
if led_count is not None:
|
||||
source.led_count = led_count
|
||||
if animation is not None:
|
||||
source.animation = animation
|
||||
elif isinstance(source, ColorCycleColorStripSource):
|
||||
if colors is not None and isinstance(colors, list) and len(colors) >= 2:
|
||||
source.colors = colors
|
||||
if cycle_speed is not None:
|
||||
source.cycle_speed = float(cycle_speed)
|
||||
if led_count is not None:
|
||||
source.led_count = led_count
|
||||
|
||||
source.updated_at = datetime.utcnow()
|
||||
self._save()
|
||||
|
||||
Reference in New Issue
Block a user