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:
@@ -26,7 +26,7 @@ from wled_controller.core.capture.calibration import (
|
||||
)
|
||||
from wled_controller.core.capture.screen_capture import get_available_displays
|
||||
from wled_controller.core.processing.processor_manager import ProcessorManager
|
||||
from wled_controller.storage.color_strip_source import GradientColorStripSource, PictureColorStripSource, StaticColorStripSource
|
||||
from wled_controller.storage.color_strip_source import ColorCycleColorStripSource, GradientColorStripSource, PictureColorStripSource, StaticColorStripSource
|
||||
from wled_controller.storage.color_strip_store import ColorStripStore
|
||||
from wled_controller.storage.picture_source import ProcessedPictureSource, ScreenCapturePictureSource
|
||||
from wled_controller.storage.picture_source_store import PictureSourceStore
|
||||
@@ -69,8 +69,11 @@ def _css_to_response(source, overlay_active: bool = False) -> ColorStripSourceRe
|
||||
calibration=calibration,
|
||||
color=getattr(source, "color", None),
|
||||
stops=stops,
|
||||
colors=getattr(source, "colors", None),
|
||||
cycle_speed=getattr(source, "cycle_speed", None),
|
||||
description=source.description,
|
||||
frame_interpolation=getattr(source, "frame_interpolation", None),
|
||||
animation=getattr(source, "animation", None),
|
||||
overlay_active=overlay_active,
|
||||
created_at=source.created_at,
|
||||
updated_at=source.updated_at,
|
||||
@@ -136,6 +139,9 @@ async def create_color_strip_source(
|
||||
stops=stops,
|
||||
description=data.description,
|
||||
frame_interpolation=data.frame_interpolation,
|
||||
animation=data.animation.model_dump() if data.animation else None,
|
||||
colors=data.colors,
|
||||
cycle_speed=data.cycle_speed,
|
||||
)
|
||||
return _css_to_response(source)
|
||||
|
||||
@@ -193,6 +199,9 @@ async def update_color_strip_source(
|
||||
stops=stops,
|
||||
description=data.description,
|
||||
frame_interpolation=data.frame_interpolation,
|
||||
animation=data.animation.model_dump() if data.animation else None,
|
||||
colors=data.colors,
|
||||
cycle_speed=data.cycle_speed,
|
||||
)
|
||||
|
||||
# Hot-reload running stream (no restart needed for in-place param changes)
|
||||
@@ -278,7 +287,7 @@ async def test_css_calibration(
|
||||
if body.edges:
|
||||
try:
|
||||
source = store.get_source(source_id)
|
||||
if isinstance(source, (StaticColorStripSource, GradientColorStripSource)):
|
||||
if isinstance(source, (StaticColorStripSource, GradientColorStripSource, ColorCycleColorStripSource)):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Calibration test is not applicable for this color strip source type",
|
||||
@@ -324,7 +333,7 @@ async def start_css_overlay(
|
||||
"""Start screen overlay visualization for a color strip source."""
|
||||
try:
|
||||
source = store.get_source(source_id)
|
||||
if isinstance(source, (StaticColorStripSource, GradientColorStripSource)):
|
||||
if isinstance(source, (StaticColorStripSource, GradientColorStripSource, ColorCycleColorStripSource)):
|
||||
raise HTTPException(status_code=400, detail="Overlay is not supported for this color strip source type")
|
||||
if not isinstance(source, PictureColorStripSource):
|
||||
raise HTTPException(status_code=400, detail="Overlay only supported for picture color strip sources")
|
||||
|
||||
@@ -8,6 +8,14 @@ from pydantic import BaseModel, Field
|
||||
from wled_controller.api.schemas.devices import Calibration
|
||||
|
||||
|
||||
class AnimationConfig(BaseModel):
|
||||
"""Procedural animation configuration for static/gradient color strip sources."""
|
||||
|
||||
enabled: bool = True
|
||||
type: str = "breathing" # breathing | color_cycle | gradient_shift | wave
|
||||
speed: float = Field(1.0, ge=0.1, le=10.0, description="Speed multiplier (0.1–10.0)")
|
||||
|
||||
|
||||
class ColorStop(BaseModel):
|
||||
"""A single color stop in a gradient."""
|
||||
|
||||
@@ -23,7 +31,7 @@ class ColorStripSourceCreate(BaseModel):
|
||||
"""Request to create a color strip source."""
|
||||
|
||||
name: str = Field(description="Source name", min_length=1, max_length=100)
|
||||
source_type: Literal["picture", "static", "gradient"] = Field(default="picture", description="Source type")
|
||||
source_type: Literal["picture", "static", "gradient", "color_cycle"] = Field(default="picture", description="Source type")
|
||||
# picture-type fields
|
||||
picture_source_id: str = Field(default="", description="Picture source ID (for picture type)")
|
||||
fps: int = Field(default=30, description="Target frames per second", ge=10, le=90)
|
||||
@@ -37,10 +45,14 @@ class ColorStripSourceCreate(BaseModel):
|
||||
color: Optional[List[int]] = Field(None, description="Static RGB color [R, G, B] (0-255 each, for static type)")
|
||||
# gradient-type fields
|
||||
stops: Optional[List[ColorStop]] = Field(None, description="Color stops for gradient type")
|
||||
# color_cycle-type fields
|
||||
colors: Optional[List[List[int]]] = Field(None, description="List of [R,G,B] colors to cycle (color_cycle type)")
|
||||
cycle_speed: Optional[float] = Field(None, description="Cycle speed multiplier 0.1–10.0 (color_cycle type)", ge=0.1, le=10.0)
|
||||
# shared
|
||||
led_count: int = Field(default=0, description="Total LED count (0 = auto from calibration / device)", ge=0)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
frame_interpolation: bool = Field(default=False, description="Blend between consecutive captured frames for smoother output")
|
||||
animation: Optional[AnimationConfig] = Field(None, description="Procedural animation config (static/gradient only)")
|
||||
|
||||
|
||||
class ColorStripSourceUpdate(BaseModel):
|
||||
@@ -60,10 +72,14 @@ class ColorStripSourceUpdate(BaseModel):
|
||||
color: Optional[List[int]] = Field(None, description="Static RGB color [R, G, B] (0-255 each, for static type)")
|
||||
# gradient-type fields
|
||||
stops: Optional[List[ColorStop]] = Field(None, description="Color stops for gradient type")
|
||||
# color_cycle-type fields
|
||||
colors: Optional[List[List[int]]] = Field(None, description="List of [R,G,B] colors to cycle (color_cycle type)")
|
||||
cycle_speed: Optional[float] = Field(None, description="Cycle speed multiplier 0.1–10.0 (color_cycle type)", ge=0.1, le=10.0)
|
||||
# shared
|
||||
led_count: Optional[int] = Field(None, description="Total LED count (0 = auto from calibration / device)", ge=0)
|
||||
description: Optional[str] = Field(None, description="Optional description", max_length=500)
|
||||
frame_interpolation: Optional[bool] = Field(None, description="Blend between consecutive captured frames")
|
||||
animation: Optional[AnimationConfig] = Field(None, description="Procedural animation config (static/gradient only)")
|
||||
|
||||
|
||||
class ColorStripSourceResponse(BaseModel):
|
||||
@@ -85,10 +101,14 @@ class ColorStripSourceResponse(BaseModel):
|
||||
color: Optional[List[int]] = Field(None, description="Static RGB color [R, G, B]")
|
||||
# gradient-type fields
|
||||
stops: Optional[List[ColorStop]] = Field(None, description="Color stops for gradient type")
|
||||
# color_cycle-type fields
|
||||
colors: Optional[List[List[int]]] = Field(None, description="List of [R,G,B] colors to cycle (color_cycle type)")
|
||||
cycle_speed: Optional[float] = Field(None, description="Cycle speed multiplier (color_cycle type)")
|
||||
# shared
|
||||
led_count: int = Field(0, description="Total LED count (0 = auto from calibration / device)")
|
||||
description: Optional[str] = Field(None, description="Description")
|
||||
frame_interpolation: Optional[bool] = Field(None, description="Blend between consecutive captured frames")
|
||||
animation: Optional[AnimationConfig] = Field(None, description="Procedural animation config (static/gradient only)")
|
||||
overlay_active: bool = Field(False, description="Whether the screen overlay is currently active")
|
||||
created_at: datetime = Field(description="Creation timestamp")
|
||||
updated_at: datetime = Field(description="Last update timestamp")
|
||||
|
||||
Reference in New Issue
Block a user