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() { +