From d42a0abda1f464c7e1e31e03af7c191314b27ba0 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 9 Jul 2026 03:54:11 +0200 Subject: [PATCH 1/4] properly apply opacity transitions in segment blending --- wled00/FX_fcn.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 34fde40058..5ad5aa1293 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1451,10 +1451,12 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { const size_t startIndx = XY(topSegment.start, topSegment.startY); const size_t stopIndx = startIndx + length; uint8_t opacity = topSegment.currentBri(); // returns transitioned opacity for style FADE + uint8_t opacityOld = opacity; // we set this to opacity of old segment in non-FADE transitions below uint8_t cct = topSegment.currentCCT(); if (gammaCorrectCol) opacity = gamma8inv(opacity); // use inverse gamma on brightness for correct color scaling after gamma correction (see #5343 for details) const Segment *segO = topSegment.getOldSegment(); + if (segO && blendingStyle != TRANSITION_FADE) opacityOld = gamma8inv(segO->currentBri()); // get old segment opacity note: can not use segO->opacity as that breaks off->on transition const bool hasGrouping = topSegment.groupLength() != 1; // fast path: handle the default case - no transitions, no grouping/spacing, no mirroring, no CCT @@ -1622,6 +1624,7 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { // we only traverse new segment, not old one for (int r = 0; r < nRows; r++) for (int c = 0; c < nCols; c++) { const bool clipped = topSegment.isPixelXYClipped(c, r); + uint8_t pixelOpacity = clipped ? opacityOld : opacity; // if segment is in transition and pixel is clipped take old segment's pixel and opacity const Segment *seg = clipped && segO ? segO : &topSegment; // pixel is never clipped for FADE int vCols = seg == segO ? oCols : nCols; // old segment may have different dimensions @@ -1654,7 +1657,7 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { } // expand pixel if (groupLen == 1) { - setMirroredPixel(x, y, c_a, opacity); + setMirroredPixel(x, y, c_a, pixelOpacity); } else { // handle grouping and spacing x *= groupLen; // expand to physical pixels @@ -1663,7 +1666,7 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { const int maxY = std::min(y + topSegment.grouping, height); while (y < maxY) { int _x = x; - while (_x < maxX) setMirroredPixel(_x++, y, c_a, opacity); + while (_x < maxX) setMirroredPixel(_x++, y, c_a, pixelOpacity); y++; } } @@ -1695,6 +1698,7 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { for (int k = 0; k < nLen; k++) { const bool clipped = topSegment.isPixelClipped(k); + uint8_t pixelOpacity = clipped ? opacityOld : opacity; // if segment is in transition and pixel is clipped take old segment's pixel and opacity const Segment *seg = clipped && segO ? segO : &topSegment; // pixel is never clipped for FADE const int vLen = seg == segO ? oLen : nLen; @@ -1723,7 +1727,7 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { i *= topSegment.groupLength(); // set all the pixels in the group const int maxI = std::min(i + topSegment.grouping, length); // make sure to not go beyond physical length - while (i < maxI) setMirroredPixel(i++, c_a, opacity); + while (i < maxI) setMirroredPixel(i++, c_a, pixelOpacity); } } From 72692e533bbeedf1ecbd8798c1fdea5ec18f0edd Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 9 Jul 2026 21:37:27 +0200 Subject: [PATCH 2/4] use gamma8inv conditionally --- wled00/FX_fcn.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 5ad5aa1293..67a136218d 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1453,10 +1453,12 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { uint8_t opacity = topSegment.currentBri(); // returns transitioned opacity for style FADE uint8_t opacityOld = opacity; // we set this to opacity of old segment in non-FADE transitions below uint8_t cct = topSegment.currentCCT(); - if (gammaCorrectCol) opacity = gamma8inv(opacity); // use inverse gamma on brightness for correct color scaling after gamma correction (see #5343 for details) - const Segment *segO = topSegment.getOldSegment(); - if (segO && blendingStyle != TRANSITION_FADE) opacityOld = gamma8inv(segO->currentBri()); // get old segment opacity note: can not use segO->opacity as that breaks off->on transition + if (segO && blendingStyle != TRANSITION_FADE) opacityOld = segO->currentBri(); // get old segment opacity note: can not use segO->opacity as that breaks off->on transition + if (gammaCorrectCol) { + opacity = gamma8inv(opacity); // use inverse gamma on brightness for correct color scaling after gamma correction (see #5343 for details) + opacityOld = gamma8inv(opacityOld); + } const bool hasGrouping = topSegment.groupLength() != 1; // fast path: handle the default case - no transitions, no grouping/spacing, no mirroring, no CCT From c7e9fbaaf6d31e992b589593579745b55a51fd43 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 20 Jul 2026 08:10:50 +0200 Subject: [PATCH 3/4] do not fade global brightness on non-fade on/off --- wled00/led.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/wled00/led.cpp b/wled00/led.cpp index 131ff95bab..17fede62ff 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -176,8 +176,13 @@ void handleTransitions() { return; } byte briTO = briT; - int deltaBri = (int)bri - (int)briOld; - briT = briOld + (deltaBri * ti / tr); + if ((bri == 0 || briOld == 0) && blendingStyle != TRANSITION_FADE) { + // On/Off change with non-FADE transition: segment transitions render the transition, do not fade global brightness in parallel + briT = (bri == 0) ? briOld : bri; + } else { + int deltaBri = (int)bri - (int)briOld; + briT = briOld + (deltaBri * ti / tr); + } if (briTO != briT) applyBri(); } } From 704de03b0c28c791bc357f8c5a3e0312a65807ba Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 20 Aug 2026 11:18:49 +0200 Subject: [PATCH 4/4] fix on/off transitions, properly apply transition to opacity/CCT change (or other fade transitions in progress) --- wled00/FX_fcn.cpp | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 67a136218d..5ed1ad9808 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -326,9 +326,10 @@ void Segment::startTransition(uint16_t dur, bool segmentCopy) { for (unsigned i = 0; i < NUM_COLORS; i++) _t->_colors[i] = color_blend16(_t->_colors[i], colors[i], _t->_progress); _t->_bri = currentBri(); // update "original" brightness note: _t->_progress is updated in updateTransitionProgress() so still valid here _t->_cct = currentCCT(); // update "original" CCT (reduces jump) - // restart transition timer only if a pure FADE transition, otherwise let the FX change or non-FADE transition finish + // restart transition timer only if a pure FADE transition or a transition without segment copy (opacity/CCT change), + // otherwise let the FX change or non-FADE transition finish // this avoids a re-start of the transition if color or brightness is changed during an ongoing FX or non-FADE transition - if (blendingStyle == TRANSITION_FADE) { + if (blendingStyle == TRANSITION_FADE || _t->_oldSegment == nullptr) { if (_t->_oldSegment != nullptr) { if (_t->_oldSegment->mode != mode) return; // do not reset transition if this is an FX change, note: the disadvantage is that colors still jump in that case @@ -382,7 +383,8 @@ void Segment::updateTransitionProgress() const { uint8_t Segment::currentCCT() const { unsigned prog = progress(); if (prog < 0xFFFFU) { - if (blendingStyle == TRANSITION_FADE) return (cct * prog + (_t->_cct * (0xFFFFU - prog))) / 0xFFFFU; + // fade if style is FADE or if the transition has no old segment (opacity or CCT transition) + if (blendingStyle == TRANSITION_FADE || _t->_oldSegment == nullptr) return (cct * prog + (_t->_cct * (0xFFFFU - prog))) / 0xFFFFU; //else return Segment::isPreviousMode() ? _t->_cct : cct; } return cct; @@ -394,8 +396,9 @@ uint8_t Segment::currentBri() const { unsigned curBri = on ? opacity : 0; if (prog < 0xFFFFU) { // this will blend opacity in new mode if style is FADE (single effect call) - if (blendingStyle == TRANSITION_FADE) curBri = (prog * curBri + _t->_bri * (0xFFFFU - prog)) / 0xFFFFU; - else curBri = Segment::isPreviousMode() ? _t->_bri : curBri; + // or if the transition has no old segment (opacity or CCT transition) + if (blendingStyle == TRANSITION_FADE || _t->_oldSegment == nullptr) curBri = (prog * curBri + _t->_bri * (0xFFFFU - prog)) / 0xFFFFU; + else curBri = Segment::isPreviousMode() ? _t->_bri : curBri; } return curBri; } @@ -569,7 +572,7 @@ Segment &Segment::setCCT(uint16_t k) { Segment &Segment::setOpacity(uint8_t o) { if (opacity != o) { //DEBUG_PRINTF_P(PSTR("- Starting opacity transition: %d\n"), o); - startTransition(strip.getTransition(), blendingStyle != TRANSITION_FADE); // start transition prior to change + startTransition(strip.getTransition(), false); // opacity change always fades (no segment copy needed) opacity = o; stateChanged = true; // send UDP/WS broadcast } @@ -1522,7 +1525,7 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { const unsigned dw = (blendingStyle==TRANSITION_OUTSIDE_IN ? progInv : progress) * width / 0xFFFFU + 1; const unsigned dh = (blendingStyle==TRANSITION_OUTSIDE_IN ? progInv : progress) * height / 0xFFFFU + 1; const unsigned orgBS = blendingStyle; - if (width*height == 1) blendingStyle = TRANSITION_FADE; // disable style for single pixel segments (use fade instead) + if (width*height == 1 || !segO) blendingStyle = TRANSITION_FADE; // single pixel segments or opacity/CCT transition: use fade switch (blendingStyle) { case TRANSITION_CIRCULAR_IN: // (must set entire segment, see isPixelXYClipped()) case TRANSITION_CIRCULAR_OUT:// (must set entire segment, see isPixelXYClipped()) @@ -1644,10 +1647,10 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { c_a = color_blend16(c_a, segO->getPixelColorRaw(x + y*oCols), progInv); } else if (blendingStyle != TRANSITION_FADE) { // if we have global brightness change (not On/Off change) we will ignore transition style and just fade brightness (see led.cpp) - // workaround for On/Off transition - // (bri != briT) && !bri => from On to Off - // (bri != briT) && bri => from Off to On - if ((briOld == 0 || bri == 0) && ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri))) c_a = BLACK; + // workaround for On/Off transition (applies while a global on/off transition is active) + // transitionActive && !bri => from On to Off + // transitionActive && bri => from Off to On + if ((briOld == 0 || bri == 0) && ((!clipped && transitionActive && !bri) || (clipped && transitionActive && bri))) c_a = BLACK; } // map it into frame buffer x = c; // restore coordiates if we were PUSHing @@ -1717,10 +1720,10 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { c_a = color_blend16(c_a, segO->getPixelColorRaw(i), progInv); } else if (blendingStyle != TRANSITION_FADE) { // if we have global brightness change (not On/Off change) we will ignore transition style and just fade brightness (see led.cpp) - // workaround for On/Off transition - // (bri != briT) && !bri => from On to Off - // (bri != briT) && bri => from Off to On - if ((briOld == 0 || bri == 0) && ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri))) c_a = BLACK; + // workaround for On/Off transition (applies while a global on/off transition is active) + // transitionActive && !bri => from On to Off + // transitionActive && bri => from Off to On + if ((briOld == 0 || bri == 0) && ((!clipped && transitionActive && !bri) || (clipped && transitionActive && bri))) c_a = BLACK; } // map into frame buffer i = k; // restore index if we were PUSHing @@ -1825,7 +1828,11 @@ void WS2812FX::restartRuntime() { void WS2812FX::setTransitionMode(bool t) { suspend(); waitForIt(); - for (Segment &seg : _segments) seg.startTransition(t ? _transitionDur : 0); + for (Segment &seg : _segments) { + // do not interrupt transitions without segment copy i.e. opacity/CCT change or FADE + if (t && seg.isInTransition() && !seg.getOldSegment()) continue; + seg.startTransition(t ? _transitionDur : 0); + } resume(); }