From afd4a3bc05e0d0711cb7ba2836bde6da512459ac Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Tue, 17 Mar 2026 15:12:57 +0300 Subject: [PATCH] Override blend mode, FPS sparkline, fix api_input persistence New features: - Override composite blend mode: per-pixel alpha from brightness (black=transparent, bright=opaque). Ideal for API input over effects. - API input test preview FPS chart uses shared createFpsSparkline (same look as target card charts) Fixes: - Fix api_input source not surviving server restart: from_dict was still passing removed led_count field to constructor - Fix composite layer brightness/processing selectors not aligned: labels get fixed width, selects fill remaining space - Fix CSPT input selector showing in non-CSPT CSS test mode - Fix test modal LED/FPS controls showing for api_input sources - Server only sends test WS frames when api_input push_generation changes Co-Authored-By: Claude Opus 4.6 (1M context) --- .../api/schemas/color_strip_sources.py | 2 +- .../core/processing/composite_stream.py | 24 +++++++++++++++++++ .../src/wled_controller/static/css/modal.css | 13 ++++++++++ .../static/js/features/color-strips.js | 2 ++ .../wled_controller/static/locales/en.json | 2 ++ .../wled_controller/static/locales/ru.json | 2 ++ .../wled_controller/static/locales/zh.json | 2 ++ .../storage/color_strip_source.py | 2 +- 8 files changed, 47 insertions(+), 2 deletions(-) diff --git a/server/src/wled_controller/api/schemas/color_strip_sources.py b/server/src/wled_controller/api/schemas/color_strip_sources.py index 4c61d99..ae12283 100644 --- a/server/src/wled_controller/api/schemas/color_strip_sources.py +++ b/server/src/wled_controller/api/schemas/color_strip_sources.py @@ -31,7 +31,7 @@ class CompositeLayer(BaseModel): """A single layer in a composite color strip source.""" source_id: str = Field(description="ID of the layer's color strip source") - blend_mode: str = Field(default="normal", description="Blend mode: normal|add|multiply|screen") + blend_mode: str = Field(default="normal", description="Blend mode: normal|add|multiply|screen|override") opacity: float = Field(default=1.0, ge=0.0, le=1.0, description="Layer opacity 0.0-1.0") enabled: bool = Field(default=True, description="Whether this layer is active") brightness_source_id: Optional[str] = Field(None, description="Optional value source ID for dynamic brightness") diff --git a/server/src/wled_controller/core/processing/composite_stream.py b/server/src/wled_controller/core/processing/composite_stream.py index 0dc8262..ff61ef0 100644 --- a/server/src/wled_controller/core/processing/composite_stream.py +++ b/server/src/wled_controller/core/processing/composite_stream.py @@ -16,6 +16,7 @@ _BLEND_NORMAL = "normal" _BLEND_ADD = "add" _BLEND_MULTIPLY = "multiply" _BLEND_SCREEN = "screen" +_BLEND_OVERRIDE = "override" class CompositeColorStripStream(ColorStripStream): @@ -300,11 +301,34 @@ class CompositeColorStripStream(ColorStripStream): u16a >>= 8 np.copyto(out, u16a, casting="unsafe") + def _blend_override(self, bottom: np.ndarray, top: np.ndarray, alpha: int, + out: np.ndarray) -> None: + """Override blend: per-pixel alpha derived from top brightness. + + Black pixels are fully transparent (bottom shows through), + bright pixels fully opaque (top replaces bottom). Layer opacity + scales the per-pixel alpha. + """ + u16a, u16b = self._u16_a, self._u16_b + # Per-pixel brightness = max(R, G, B) for each LED + per_px_alpha = np.max(top, axis=1, keepdims=True).astype(np.uint16) + # Scale by layer opacity + per_px_alpha = (per_px_alpha * alpha) >> 8 + # Lerp: out = (bottom * (256 - per_px_alpha) + top * per_px_alpha) >> 8 + np.copyto(u16a, bottom, casting="unsafe") + np.copyto(u16b, top, casting="unsafe") + u16a *= (256 - per_px_alpha) + u16b *= per_px_alpha + u16a += u16b + u16a >>= 8 + np.copyto(out, u16a, casting="unsafe") + _BLEND_DISPATCH = { _BLEND_NORMAL: "_blend_normal", _BLEND_ADD: "_blend_add", _BLEND_MULTIPLY: "_blend_multiply", _BLEND_SCREEN: "_blend_screen", + _BLEND_OVERRIDE: "_blend_override", } # ── Processing loop ───────────────────────────────────────── diff --git a/server/src/wled_controller/static/css/modal.css b/server/src/wled_controller/static/css/modal.css index c502762..ab82ca3 100644 --- a/server/src/wled_controller/static/css/modal.css +++ b/server/src/wled_controller/static/css/modal.css @@ -1516,6 +1516,19 @@ min-width: 0; } +.composite-layer-brightness-label { + flex-shrink: 0; + width: 90px; + font-size: 0.8rem; + color: var(--text-secondary); +} + +.composite-layer-brightness, +.composite-layer-cspt { + flex: 1; + min-width: 0; +} + .composite-layer-blend { width: 100px; flex-shrink: 0; diff --git a/server/src/wled_controller/static/js/features/color-strips.js b/server/src/wled_controller/static/js/features/color-strips.js index 5f2fbb5..b76f2c9 100644 --- a/server/src/wled_controller/static/js/features/color-strips.js +++ b/server/src/wled_controller/static/js/features/color-strips.js @@ -685,6 +685,7 @@ function _getCompositeBlendItems() { { value: 'add', icon: _icon(P.sun), label: t('color_strip.composite.blend_mode.add'), desc: t('color_strip.composite.blend_mode.add.desc') }, { value: 'multiply', icon: _icon(P.eye), label: t('color_strip.composite.blend_mode.multiply'), desc: t('color_strip.composite.blend_mode.multiply.desc') }, { value: 'screen', icon: _icon(P.monitor), label: t('color_strip.composite.blend_mode.screen'), desc: t('color_strip.composite.blend_mode.screen.desc') }, + { value: 'override', icon: _icon(P.zap), label: t('color_strip.composite.blend_mode.override'), desc: t('color_strip.composite.blend_mode.override.desc') }, ]; } @@ -741,6 +742,7 @@ function _compositeRenderList() { +
diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json index 008151a..a42e491 100644 --- a/server/src/wled_controller/static/locales/en.json +++ b/server/src/wled_controller/static/locales/en.json @@ -1131,6 +1131,8 @@ "color_strip.composite.blend_mode.multiply.desc": "Darkens by multiplying colors", "color_strip.composite.blend_mode.screen": "Screen", "color_strip.composite.blend_mode.screen.desc": "Brightens, inverse of multiply", + "color_strip.composite.blend_mode.override": "Override", + "color_strip.composite.blend_mode.override.desc": "Black = transparent, bright = opaque", "color_strip.composite.opacity": "Opacity", "color_strip.composite.brightness": "Brightness", "color_strip.composite.brightness.none": "None (full brightness)", diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json index 60174bd..8b31523 100644 --- a/server/src/wled_controller/static/locales/ru.json +++ b/server/src/wled_controller/static/locales/ru.json @@ -1131,6 +1131,8 @@ "color_strip.composite.blend_mode.multiply.desc": "Затемняет, умножая цвета", "color_strip.composite.blend_mode.screen": "Экран", "color_strip.composite.blend_mode.screen.desc": "Осветляет, обратное умножение", + "color_strip.composite.blend_mode.override": "Замена", + "color_strip.composite.blend_mode.override.desc": "Чёрный = прозрачный, яркий = непрозрачный", "color_strip.composite.opacity": "Непрозрачность", "color_strip.composite.brightness": "Яркость", "color_strip.composite.brightness.none": "Нет (полная яркость)", diff --git a/server/src/wled_controller/static/locales/zh.json b/server/src/wled_controller/static/locales/zh.json index 063f608..55d8fe0 100644 --- a/server/src/wled_controller/static/locales/zh.json +++ b/server/src/wled_controller/static/locales/zh.json @@ -1131,6 +1131,8 @@ "color_strip.composite.blend_mode.multiply.desc": "通过相乘颜色变暗", "color_strip.composite.blend_mode.screen": "滤色", "color_strip.composite.blend_mode.screen.desc": "提亮,正片叠底的反转", + "color_strip.composite.blend_mode.override": "覆盖", + "color_strip.composite.blend_mode.override.desc": "黑色=透明,亮色=不透明", "color_strip.composite.opacity": "不透明度", "color_strip.composite.brightness": "亮度", "color_strip.composite.brightness.none": "无(全亮度)", diff --git a/server/src/wled_controller/storage/color_strip_source.py b/server/src/wled_controller/storage/color_strip_source.py index 3d89361..a24bac7 100644 --- a/server/src/wled_controller/storage/color_strip_source.py +++ b/server/src/wled_controller/storage/color_strip_source.py @@ -227,7 +227,7 @@ class ColorStripSource: return ApiInputColorStripSource( id=sid, name=name, source_type="api_input", created_at=created_at, updated_at=updated_at, description=description, - clock_id=clock_id, tags=tags, led_count=data.get("led_count") or 0, + clock_id=clock_id, tags=tags, fallback_color=fallback_color, timeout=float(data.get("timeout") or 5.0), )