CSS: add GradientColorStripSource with visual editor
- Backend: GradientColorStripSource storage model, GradientColorStripStream with numpy interpolation (bidirectional stops, auto-size from device LED count), ColorStop Pydantic schema, API create/update/guard routes - Frontend: gradient editor modal (canvas preview, draggable markers, stop rows), CSS hard-edge card swatch, locale keys (en + ru) - Fixes: stop row mousedown no longer rebuilds DOM (buttons now clickable), position input max-width, bidir/remove button static width Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,9 +7,7 @@ calibration, color correction, smoothing, and FPS.
|
||||
Current types:
|
||||
PictureColorStripSource — derives LED colors from a PictureSource (screen capture)
|
||||
StaticColorStripSource — constant solid color fills all LEDs
|
||||
|
||||
Future types (not yet implemented):
|
||||
GradientColorStripSource — animated gradient
|
||||
GradientColorStripSource — linear gradient across all LEDs from user-defined color stops
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
@@ -54,6 +52,7 @@ class ColorStripSource:
|
||||
"calibration": None,
|
||||
"led_count": None,
|
||||
"color": None,
|
||||
"stops": None,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@@ -99,6 +98,16 @@ class ColorStripSource:
|
||||
led_count=data.get("led_count") or 0,
|
||||
)
|
||||
|
||||
if source_type == "gradient":
|
||||
raw_stops = data.get("stops")
|
||||
stops = raw_stops if isinstance(raw_stops, list) else []
|
||||
return GradientColorStripSource(
|
||||
id=sid, name=name, source_type="gradient",
|
||||
created_at=created_at, updated_at=updated_at, description=description,
|
||||
stops=stops,
|
||||
led_count=data.get("led_count") or 0,
|
||||
)
|
||||
|
||||
# Default: "picture" type
|
||||
return PictureColorStripSource(
|
||||
id=sid, name=name, source_type=source_type,
|
||||
@@ -166,3 +175,28 @@ class StaticColorStripSource(ColorStripSource):
|
||||
d["color"] = list(self.color)
|
||||
d["led_count"] = self.led_count
|
||||
return d
|
||||
|
||||
|
||||
@dataclass
|
||||
class GradientColorStripSource(ColorStripSource):
|
||||
"""Color strip source that produces a linear gradient across all LEDs.
|
||||
|
||||
The gradient is defined by color stops at relative positions (0.0–1.0).
|
||||
Each stop has a primary color; optionally a second "right" color to create
|
||||
a hard discontinuity (bidirectional stop) at that position.
|
||||
|
||||
LED count auto-sizes from the connected device when led_count == 0.
|
||||
"""
|
||||
|
||||
# Each stop: {"position": float, "color": [R,G,B], "color_right": [R,G,B] | null}
|
||||
stops: list = field(default_factory=lambda: [
|
||||
{"position": 0.0, "color": [255, 0, 0]},
|
||||
{"position": 1.0, "color": [0, 0, 255]},
|
||||
])
|
||||
led_count: int = 0 # 0 = use device LED count
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
d = super().to_dict()
|
||||
d["stops"] = [dict(s) for s in self.stops]
|
||||
d["led_count"] = self.led_count
|
||||
return d
|
||||
|
||||
@@ -9,6 +9,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 (
|
||||
ColorStripSource,
|
||||
GradientColorStripSource,
|
||||
PictureColorStripSource,
|
||||
StaticColorStripSource,
|
||||
)
|
||||
@@ -101,6 +102,7 @@ class ColorStripStore:
|
||||
calibration=None,
|
||||
led_count: int = 0,
|
||||
color: Optional[list] = None,
|
||||
stops: Optional[list] = None,
|
||||
description: Optional[str] = None,
|
||||
) -> ColorStripSource:
|
||||
"""Create a new color strip source.
|
||||
@@ -130,6 +132,20 @@ class ColorStripStore:
|
||||
color=rgb,
|
||||
led_count=led_count,
|
||||
)
|
||||
elif source_type == "gradient":
|
||||
source = GradientColorStripSource(
|
||||
id=source_id,
|
||||
name=name,
|
||||
source_type="gradient",
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
description=description,
|
||||
stops=stops if isinstance(stops, list) else [
|
||||
{"position": 0.0, "color": [255, 0, 0]},
|
||||
{"position": 1.0, "color": [0, 0, 255]},
|
||||
],
|
||||
led_count=led_count,
|
||||
)
|
||||
else:
|
||||
if calibration is None:
|
||||
calibration = CalibrationConfig(layout="clockwise", start_position="bottom_left")
|
||||
@@ -171,6 +187,7 @@ class ColorStripStore:
|
||||
calibration=None,
|
||||
led_count: Optional[int] = None,
|
||||
color: Optional[list] = None,
|
||||
stops: Optional[list] = None,
|
||||
description: Optional[str] = None,
|
||||
) -> ColorStripSource:
|
||||
"""Update an existing color strip source.
|
||||
@@ -217,6 +234,11 @@ class ColorStripStore:
|
||||
source.color = color
|
||||
if led_count is not None:
|
||||
source.led_count = led_count
|
||||
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
|
||||
|
||||
source.updated_at = datetime.utcnow()
|
||||
self._save()
|
||||
|
||||
Reference in New Issue
Block a user