From d42a0abda1f464c7e1e31e03af7c191314b27ba0 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 9 Jul 2026 03:54:11 +0200 Subject: [PATCH 01/19] 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 02/19] 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 03/19] 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 04/19] 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(); } From 23d758016d87a6dd2f8e447b02cbecac99ff4e21 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 20 Aug 2026 15:49:55 +0200 Subject: [PATCH 05/19] partial fix for on/off transitions --- wled00/FX_fcn.cpp | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 3a583a82b0..379764a579 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1513,7 +1513,8 @@ 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 || !segO) blendingStyle = TRANSITION_FADE; // single pixel segments or opacity/CCT transition: use fade + // single pixel segments or opacity/CCT transition and not on/off transition: use fade + if (width*height == 1 || (!segO && !(topSegment.isInTransition() && (briOld == 0 || bri == 0)))) blendingStyle = TRANSITION_FADE; switch (blendingStyle) { case TRANSITION_CIRCULAR_IN: // (must set entire segment, see isPixelXYClipped()) case TRANSITION_CIRCULAR_OUT:// (must set entire segment, see isPixelXYClipped()) @@ -1636,13 +1637,11 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { } 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 (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; + // (bri != briT) && !bri => from On to Off + // (bri != briT) && bri => from Off to On // note: only blank pixels once the segment transition has actually started; bri changes before // startTransition() is called (stateUpdated()) and a frame rendered in that window would blank the whole segment - //if (topSegment.isInTransition() && (briOld == 0 || bri == 0) && ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri))) c_a = BLACK; - //if (topSegment.isInTransition() && (briOld == 0 || bri == 0) && ((!clipped && transitionActive && !bri) || (clipped && transitionActive && bri))) c_a = BLACK; + if ((briOld == 0 || bri == 0) && ((!clipped && topSegment.isInTransition() && !bri) || (clipped && topSegment.isInTransition() && bri))) c_a = BLACK; } // map it into frame buffer x = c; // restore coordiates if we were PUSHing @@ -1715,11 +1714,9 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { // 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 && transitionActive && !bri) || (clipped && transitionActive && bri))) c_a = BLACK; // note: only blank pixels once the segment transition has actually started; bri changes before // startTransition() is called (stateUpdated()) and a frame rendered in that window would blank the whole segment - //if (topSegment.isInTransition() && (briOld == 0 || bri == 0) && ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri))) c_a = BLACK; - //if (topSegment.isInTransition() && (briOld == 0 || bri == 0) && ((!clipped && transitionActive && !bri) || (clipped && transitionActive && bri))) c_a = BLACK; + if ((briOld == 0 || bri == 0) && ((!clipped && topSegment.isInTransition() && !bri) || (clipped && topSegment.isInTransition() && bri))) c_a = BLACK; } // map into frame buffer i = k; // restore index if we were PUSHing From 174a6ebb2d98d308672a4de16cb574b19175e1d3 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 27 Aug 2026 20:00:53 +0200 Subject: [PATCH 06/19] WIP: rework on transitions using states, mostly working but on off is buggy --- wled00/FX.h | 58 +++++++--- wled00/FX_fcn.cpp | 266 ++++++++++++++++++++++++++++++---------------- wled00/led.cpp | 47 +++++--- 3 files changed, 250 insertions(+), 121 deletions(-) diff --git a/wled00/FX.h b/wled00/FX.h index c874c57209..d639b7488b 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -405,6 +405,17 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define TRANSITION_PUSH_MASK 0x10 #define TRANSITION_COUNT 18 +// transition kind: identifies which change triggered the transition (fade channel always runs, spatial channel only with segment copy) +#define TRANSITION_KIND_FADE 0 // attribute-only change (opacity, CCT): fade channel, never needs segment copy +#define TRANSITION_KIND_COLOR 1 // color or palette change +#define TRANSITION_KIND_EFFECT 2 // effect change +#define TRANSITION_KIND_POWER 3 // segment on/off change + +// transition flags (scope and target state of a power transition) +#define TRANSITION_FLAG_POWER 0x01 // power (on/off) transition +#define TRANSITION_FLAG_POWER_ON 0x02 // target state is "on" + + typedef enum mapping1D2D { M12_Pixels = 0, @@ -502,24 +513,34 @@ class Segment { // transition data, holds values during transition (76 bytes/28 bytes) struct Transition { Segment *_oldSegment; // previous segment environment (may be nullptr if effect did not change) - unsigned long _start; // must accommodate millis() - uint32_t _colors[NUM_COLORS]; // current colors + unsigned long _start; // spatial channel start, must accommodate millis() + unsigned long _fadeStart; // fade channel start + uint32_t _colors[NUM_COLORS]; // colors at the start of fade channel CRGBPalette16 _palT; // temporary palette (slowly being morphed from old to new) - uint16_t _dur; // duration of transition in ms - uint16_t _progress; // transition progress (0-65535); pre-calculated from _start & _dur in updateTransitionProgress() + uint16_t _dur; // duration of spatial channel in ms + uint16_t _fadeDur; // duration of fade channel in ms + uint16_t _progress; // spatial channel progress (0-65535); pre-calculated in updateTransitionProgress() + uint16_t _fadeProgress; // fade channel progress (0-65535) uint8_t _prevPaletteBlends; // number of previous palette blends (there are max 255 blends possible) - uint8_t _palette, _bri, _cct; // palette ID, brightness and CCT at the start of transition (brightness will be 0 if segment was off) + uint8_t _palette, _bri, _cct; // palette ID, brightness and CCT at the start of fade channel (brightness will be 0 if segment was off) + uint8_t _kind; // transition kind: Fade, Content, Effect (one of TRANSITION_KIND_*) TODO: this is now unused, remove it? + uint8_t _flags; // TRANSITION_FLAG_* power state Transition(uint16_t dur=750) : _oldSegment(nullptr) , _start(millis()) + , _fadeStart(_start) , _colors{0,0,0} - , _palT(CRGBPalette16()) + , _palT(CRGBPalette16()) // TODO: remove _palT as it is not necessary. , _dur(dur) + , _fadeDur(dur) , _progress(0) + , _fadeProgress(0) , _prevPaletteBlends(0) , _palette(0) , _bri(0) , _cct(0) + , _kind(0) + , _flags(0) {} ~Transition() { //DEBUGFX_PRINTF_P(PSTR("-- Destroying transition: %p\n"), this); @@ -546,11 +567,11 @@ class Segment { void updateTransitionProgress() const; // sets transition progress (0-65535) based on time passed since transition start inline void handleTransition() { updateTransitionProgress(); - if (isInTransition() && progress() == 0xFFFFU) stopTransition(); + if (isInTransition()) { + if (_t->_oldSegment && _t->_progress == 0xFFFFU) { delete _t->_oldSegment; _t->_oldSegment = nullptr; } // spatial channel completed, release segment copy + if (progress() == 0xFFFFU && fadeProgress() == 0xFFFFU) stopTransition(); + } } - inline uint16_t progress() const { return isInTransition() ? _t->_progress : 0xFFFFU; } // relies on handleTransition()/updateTransitionProgress() to update progression variable - inline Segment *getOldSegment() const { return isInTransition() ? _t->_oldSegment : nullptr; } - inline static void modeBlend(bool blend) { Segment::_modeBlend = blend; } // for isPreviousMode() inline static void setClippingRect(int startX, int stopX, int startY = 0, int stopY = 1) { _clipStart = startX; _clipStop = stopX; _clipStartY = startY; _clipStopY = stopY; }; inline static bool isPreviousMode() { return Segment::_modeBlend; } // needed for determining CCT/opacity during non-TRANSITION_FADE transition @@ -634,6 +655,14 @@ class Segment { inline bool getOption(uint8_t n) const { return ((options >> n) & 0x01); } inline bool isSelected() const { return selected; } inline bool isInTransition() const { return _t != nullptr; } + inline uint16_t progress() const { return isInTransition() && _t->_oldSegment ? _t->_progress : 0xFFFFU; } // spatial channel progress, relies on handleTransition()/updateTransitionProgress() + inline uint16_t fadeProgress() const { return isInTransition() ? _t->_fadeProgress : 0xFFFFU; } // fade channel progress, relies on handleTransition()/updateTransitionProgress() + inline unsigned long getTransitionStart() const { return isInTransition() ? _t->_start : 0; } // spatial channel start time + inline Segment *getOldSegment() const { return isInTransition() ? _t->_oldSegment : nullptr; } + // power transition helpers (only true while a spatial on/off transition is running) + inline bool isPowerTransition() const { return isInTransition() && (_t->_flags & TRANSITION_FLAG_POWER) && _t->_oldSegment != nullptr; } + inline bool isPowerOffTransition() const { return isPowerTransition() && !(_t->_flags & TRANSITION_FLAG_POWER_ON); } // spatial to off + inline bool isPowerOnTransition() const { return isPowerTransition() && (_t->_flags & TRANSITION_FLAG_POWER_ON); } // spatial to on inline bool isActive() const { return stop > start && pixels; } inline bool hasRGB() const { return _isRGB; } inline bool hasWhite() const { return _hasW; } @@ -679,7 +708,10 @@ class Segment { */ inline Segment &markForReset() { reset = true; return *this; } // setOption(SEG_OPTION_RESET, true) - void startTransition(uint16_t dur, bool segmentCopy = true); // transition has to start before actual segment values change + // transition has to start before actual segment values change + // powerTarget: target on-state for power transitions; -1 = derive from !on (segment setters call this before applying the change), + // 0/1 = explicit target (global on/off never toggles segment on state, only global brightness changes) + void startTransition(uint16_t dur, uint8_t kind = TRANSITION_KIND_COLOR, bool isPower = false, int8_t powerTarget = -1); uint8_t currentCCT() const; // current segment's CCT (blended while in transition) uint8_t currentBri() const; // current segment's opacity/brightness (blended while in transition) @@ -910,7 +942,7 @@ class WS2812FX { inline void resume() { _suspend = false; } // will resume strip.service() execution void restartRuntime(); - void setTransitionMode(bool t); + void setTransitionMode(bool start, bool powerOn = false); bool checkSegmentAlignment() const; bool hasRGBWBus() const; @@ -946,7 +978,7 @@ class WS2812FX { inline uint16_t getFrameTime() const { return _frametime; } // returns amount of time a frame should take (in ms) inline uint16_t getMinShowDelay() const { return MIN_FRAME_DELAY; } // returns minimum amount of time strip.service() can be delayed (constant) inline uint16_t getLength() const { return _length; } // returns actual amount of LEDs on a strip (2D matrix may have less LEDs than W*H) - inline uint16_t getTransition() const { return _transitionDur; } // returns currently set transition time (in ms) + inline uint16_t getTransitionDur() const { return _transitionDur; } // returns currently set transition duration time (in ms) inline uint16_t getMappedPixelIndex(uint16_t index) const { // convert logical address to physical if (index < customMappingSize && (realtimeMode == REALTIME_MODE_INACTIVE || realtimeRespectLedMaps)) index = customMappingTable[index]; return index; diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 379764a579..c4577a4193 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -289,54 +289,127 @@ void Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { // starting a transition has to occur before change so we get current values 1st // note: _t is the temporary segment that holds the values transitioned from (palette, colors, brightness,...) and the current segment holds the "to" values -// if this is a non FADE transition or an FX change, the _oldSegment is created which is a full copy of the segment before the change -void Segment::startTransition(uint16_t dur, bool segmentCopy) { - if (dur == 0 || !isActive()) { - if (isInTransition()) _t->_dur = 0; +// the transition has two independent channels: +// the fade channel (_fadeStart/_fadeDur/_fadeProgress) crossfades colors, palette, CCT and opacity and never needs a segment copy +// the spatial channel (_start/_dur/_progress and _oldSegment) renders wipe/push/etc. using a copy of the previous segment +// kind: TRANSITION_KIND_* identifying which change triggered the transition (determines whether a segment copy is needed) +// isPower: true for on/off transitions (segment or global); uses black clipping pixels in blendSegment() and transition time reversal when changing mid transition +// powerTarget: target on-state for global power transitions: 0 = off, 1 = on. Otherwise it is -1 -> use segment on state, i.e. if segment is off, the target is on) +void Segment::startTransition(uint16_t dur, uint8_t kind, bool isPower, int8_t powerTarget) { + // check if we even need to start a transition. No transitions if transitions disabled, not an active segment or not in an on state (unless this is a power-on request) + if (dur == 0 || !isActive() || (!isPower && (!on || bri == 0))) { + if (isInTransition()) _t->_dur = _t->_fadeDur = 0; // complete any running transition immediately return; } + // determine target "on" state for power transitions (if used), see powerTarget parameter description above + const bool targetOn = powerTarget >= 0 ? (powerTarget != 0) : !on; + // check if we need a copy of current segment: only effect transitions and transitions using a spatial (non-FADE) style + const bool segmentCopy = kind == TRANSITION_KIND_EFFECT || (kind != TRANSITION_KIND_FADE && blendingStyle != TRANSITION_FADE); + + // helper lambda function to capture current _bri/_cct and optionally _colors to the segments transitions (_t) state + const auto captureBlend = [&](bool rebaseColors, unsigned long fadeStart) { + if (rebaseColors) for (unsigned i = 0; i < NUM_COLORS; i++) _t->_colors[i] = color_blend16(_t->_colors[i], colors[i], _t->_fadeProgress); + _t->_bri = currentBri(); + _t->_cct = currentCCT(); + _t->_fadeDur = dur; + _t->_fadeStart = fadeStart; + _t->_prevPaletteBlends = 0; + }; + if (isInTransition()) { - if (segmentCopy && !_t->_oldSegment) { - // already in transition but segment copy requested and not yet created - _t->_oldSegment = new(std::nothrow) Segment(*this); // store/copy current segment settings - _t->_start = millis(); // restart transition timer - _t->_dur = dur; - _t->_prevPaletteBlends = 0; // reset palette blends - if (_t->_oldSegment) { - _t->_oldSegment->palette = _t->_palette; // restore original palette, colors, brightness and CCT (from start of transition) - for (unsigned i = 0; i < NUM_COLORS; i++) _t->_oldSegment->colors[i] = _t->_colors[i]; - _t->_oldSegment->opacity = _t->_bri; - _t->_oldSegment->cct = _t->_cct; - // if already partway through a FADE transition, set old segment's colors to current blend to avoid jumping back to original colors - if (_t->_progress > 0) { - // already in a transition, see comment below - for (unsigned i = 0; i < NUM_COLORS; i++) _t->_oldSegment->colors[i] = color_blend16(_t->_colors[i], colors[i], _t->_progress); - _t->_oldSegment->opacity = currentBri(); // update "original" brightness note: _t->_progress is updated in updateTransitionProgress() so still valid here - _t->_oldSegment->cct = currentCCT(); // update "original" CCT (reduces jump) + // re-targeting a running transition: fade restarts, starting from current blend, spatial continues but replaces the target (FX, color, palette) immediately + if (!isPower) { + // opacity/CCT/color/palette/FX change: rebase fades to the current visual blend and restart it (no jump) a running spatial transition continues + if (segmentCopy) { + // FX change: keep the existing old segment and its timeline, only the revealed (new) side switches FX + if (_t->_oldSegment == nullptr) { + // no old segment, meaning a fade transition is going on (color, palette, opacity, cct) + _t->_oldSegment = new(std::nothrow) Segment(*this); // store/copy current segment settings + if (_t->_oldSegment) { + // capture current state (colors, opacity, CCT) into the old segment (old segment does not fade, it is static) + for (unsigned i = 0; i < NUM_COLORS; i++) _t->_oldSegment->colors[i] = color_blend16(_t->_colors[i], colors[i], _t->_progress); + _t->_oldSegment->opacity = currentBri(); // captrue current opacity in case it was being faded + _t->_oldSegment->cct = currentCCT(); // capture current CCT in case it was being faded + // _t->_oldSegment->_currentPalette = _t->_palT; // capture current palette (might be partially faded) + _t->_start = millis(); // (re)start transition (but do not restart fade channel so fading continues on current segment) + _t->_dur = dur; + DEBUGFX_PRINTF_P(PSTR("-- Updated transition with segment copy: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); + //if (!_t->_oldSegment->isActive()) { stopTransition(); return; } // todo: remove? is now handled above before creating a old segment + } else { + // not enough RAM for segment copy: degrade to pure fade instead of dropping the transition + // captureBlend(false, millis()); TODO: use this? or not retarget the fade channel? + _t->_fadeStart = millis(); + _t->_fadeDur = dur; + _t->_prevPaletteBlends = 0; + } } - DEBUGFX_PRINTF_P(PSTR("-- Updated transition with segment copy: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); - if (!_t->_oldSegment->isActive()) stopTransition(); + // _t->_kind = TRANSITION_KIND_EFFECT; + } else if (_t->_oldSegment != nullptr && kind == TRANSITION_KIND_COLOR) { + // color/palette change during a spatial transition: do not fade colors but set them immediately as we are not in a fade transition, do not restart transition + //captureBlend(true, 0); + //_t->_fadeDur = 0; // fade completed immediately + //_t->_palette = palette; + //loadPalette(_t->_palT, palette); // load new palette into transition state (updates immediately) + // _t->_kind = TRANSITION_KIND_COLOR; + } + else { + // capture the current visual blend as the new "from" state so the incoming change does not cause a visible jump. + // _palT already holds the intermediate blended palette and will continue blending toward the new target (see beginDraw()). + captureBlend(true, millis()); // capture current colors, bri & CCT, restart transition + //_t->_prevPaletteBlends = 0; + // if (kind == TRANSITION_KIND_COLOR) _t->_kind = kind; // color/palette change upgrades kind (also from Effect so a later FX change can re-capture) } - } else if (_t->_progress > 0) { - // already in a transition: capture the current visual blend as the new "from" state so the incoming change does not cause a visible jump. - // _palT already holds the intermediate blended palette and will continue blending toward the new target (see beginDraw()), so no palette action needed. - // initial version by @blazoncek (https://github.com/blazoncek/WLED/commit/40d9812) - 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 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 || _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 - } - _t->_start = millis(); - _t->_dur = dur; - _t->_prevPaletteBlends = 0; + return; + } + // power (on/off) transition (per segment or global) + if (_t->_flags & TRANSITION_FLAG_POWER) { + // already in a power transition + if (targetOn == ((_t->_flags & TRANSITION_FLAG_POWER_ON) != 0)) return; // same target re-issued, let the running transition finish + // reverse in place: invert the spatial timeline (a 20%-completed swipe continues from 80%) + // keep the same style, no re-copy, no restart. progress is re-calculated from _start/_dur so backdate the start + unsigned long nowT = millis(); + _t->_dur = dur; + _t->_start = nowT - (((unsigned)(0xFFFFU - _t->_progress) * dur) / 0xFFFFU); + if (powerTarget < 0) { + // powerTarget == -1 means segment-level on/off: the fade channel target flips with the on state (on ? opacity : 0), so rebase the + // fade channel to the current visual blend and reverse it along with the spatial channel + // (also reverses a still-running fade after the spatial channel already completed, i.e. _oldSegment == nullptr) + // capture before inverting progress as currentBri()/currentCCT() rely on _fadeProgress + _t->_bri = opacity; + _t->_cct = cct; + //_t->_fadeDur = dur; + //_t->_fadeStart = nowT - (((unsigned)(0xFFFFU - _t->_fadeProgress) * dur) / 0xFFFFU); + if (_t->_oldSegment) _t->_oldSegment->on = !_t->_oldSegment->on; // invert old segment's on state } + // global on/off: the fade channel (colors/opacity/CCT) is independent of the power state, leave it running + _t->_prevPaletteBlends = 0; + _t->_flags ^= TRANSITION_FLAG_POWER_ON; // flip POWER_ON flag + return; } + // stop a spatial transition immediately: we only have one transition counter and need to restart for power off + if (_t->_oldSegment) { delete _t->_oldSegment; _t->_oldSegment = nullptr; } + // rebase fade channel to the current visual blend before starting the new power transition + for (unsigned i = 0; i < NUM_COLORS; i++) _t->_colors[i] = color_blend16(_t->_colors[i], colors[i], _t->_fadeProgress); + _t->_bri = currentBri(); + _t->_cct = currentCCT(); + _t->_fadeStart = millis(); + _t->_fadeDur = dur; + _t->_prevPaletteBlends = 0; + if (segmentCopy) _t->_oldSegment = new(std::nothrow) Segment(*this); // store/copy current segment settings + if (_t->_oldSegment) { + // freeze the old side at the current visual brightness: the copy picks up the final opacity (setters already + // applied it), which would otherwise make a still-running opacity fade jump to its final value right here + _t->_oldSegment->opacity = _t->_bri; + _t->_start = millis(); + _t->_dur = dur; + DEBUGFX_PRINTF_P(PSTR("-- Restarted power transition: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); + if (!_t->_oldSegment->isActive()) { stopTransition(); return; } + } else { + // not enough RAM for segment copy: degrade to pure fade instead of dropping the transition + _t->_start = millis(); + _t->_dur = 0; + } + _t->_flags = TRANSITION_FLAG_POWER | (targetOn ? TRANSITION_FLAG_POWER_ON : 0); return; } @@ -346,13 +419,16 @@ void Segment::startTransition(uint16_t dur, bool segmentCopy) { _t->_bri = on ? opacity : 0; _t->_cct = cct; _t->_palette = palette; - loadPalette(_t->_palT, palette); + // _t->_kind = kind; + _t->_flags = isPower ? TRANSITION_FLAG_POWER | (targetOn ? TRANSITION_FLAG_POWER_ON : 0) : 0; + // loadPalette(_t->_palT, palette); for (int i=0; i_colors[i] = colors[i]; if (segmentCopy) _t->_oldSegment = new(std::nothrow) Segment(*this); // store/copy current segment settings if (_t->_oldSegment) { DEBUGFX_PRINTF_P(PSTR("-- Started transition: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); if (!_t->_oldSegment->isActive()) stopTransition(); } else { + if (segmentCopy) _t->_dur = 0; // not enough RAM for segment copy: degrade spatial channel to pure fade instead of dropping the transition DEBUGFX_PRINTF_P(PSTR("-- Started transition without old segment: S=%p T(%p)\n"), this, _t); } }; @@ -365,60 +441,70 @@ void Segment::stopTransition() { _t = nullptr; } -// sets transition progress variable (0-65535) based on time passed since transition start +// sets transition progress variables (0-65535) based on time passed since transition start void Segment::updateTransitionProgress() const { if (isInTransition()) { - _t->_progress = 0xFFFF; + _t->_progress = _t->_fadeProgress = 0xFFFF; unsigned diff = millis() - _t->_start; if (_t->_dur > 0 && diff < _t->_dur) _t->_progress = diff * 0xFFFFU / _t->_dur; + diff = millis() - _t->_fadeStart; + if (_t->_fadeDur > 0 && diff < _t->_fadeDur) _t->_fadeProgress = diff * 0xFFFFU / _t->_fadeDur; } } // will return segment's CCT during a transition // isPreviousMode() is actually not implemented for CCT in strip.service() as WLED does not support per-pixel CCT uint8_t Segment::currentCCT() const { - unsigned prog = progress(); + unsigned prog = fadeProgress(); if (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; + // fade channel always crossfades CCT (never needs a segment copy) + return (cct * prog + (_t->_cct * (0xFFFFU - prog))) / 0xFFFFU; } return cct; } // will return segment's opacity during a transition (blending it with old in case of FADE transition) uint8_t Segment::currentBri() const { - unsigned prog = progress(); + unsigned prog = fadeProgress(); unsigned curBri = on ? opacity : 0; if (prog < 0xFFFFU) { - // this will blend opacity in new mode if style is FADE (single effect call) + // this will blend opacity in new mode if style or kind is FADE (single effect call) // 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; + //if (_t->_oldSegment == nullptr || blendingStyle == TRANSITION_FADE || _t->_kind == TRANSITION_KIND_FADE) curBri = (prog * curBri + _t->_bri * (0xFFFFU - prog)) / 0xFFFFU; + //else curBri = Segment::isPreviousMode() ? _t->_bri : curBri; + curBri = (prog * curBri + _t->_bri * (0xFFFFU - prog)) / 0xFFFFU; } return curBri; } // pre-calculate drawing parameters for faster access (based on the idea from @softhack007 from MM fork) // and blends colors and palettes if necessary -// prog is the progress of the transition (0-65535) and is passed to the function as it may be called in the context of old segment +// prog is the progress of the fade channel (0-65535) and is passed to the function as it may be called in the context of old segment // which does not have transition structure void Segment::beginDraw(uint16_t prog) { setDrawDimensions(); // load colors into _currentColors for (unsigned i = 0; i < NUM_COLORS; i++) _currentColors[i] = colors[i]; - // load palette into _currentPalette - loadPalette(Segment::_currentPalette, palette); - if (isInTransition() && prog < 0xFFFFU && blendingStyle == TRANSITION_FADE) { + + // color blending always uses fade channel progress (never spatial progress) + if (isInTransition() && prog < 0xFFFFU && (blendingStyle == TRANSITION_FADE || _t->_oldSegment == nullptr)) { + // Serial.println("Blending colors and palettes"); // blend colors for (unsigned i = 0; i < NUM_COLORS; i++) _currentColors[i] = color_blend16(_t->_colors[i], colors[i], prog); // blend palettes + // load target palette + CRGBPalette16 targetpalette; + loadPalette(targetpalette, palette); // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in _dur time) // minimum blend time is 100ms maximum is 65535ms unsigned noOfBlends = ((255U * prog) / 0xFFFFU) - _t->_prevPaletteBlends; if (noOfBlends > 255) noOfBlends = 255; // safety check - for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, Segment::_currentPalette, 48); - Segment::_currentPalette = _t->_palT; // copy transitioning/temporary palette + for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(Segment::_currentPalette, targetpalette, 48); + //Segment::_currentPalette = _t->_palT; // copy transitioning/temporary palette + } + else { + //Serial.println("Loading palette without blending"); + loadPalette(Segment::_currentPalette, palette); } } @@ -434,10 +520,10 @@ void Segment::handleRandomPalette() { Segment::_lastPaletteChange = now_s; Segment::_nextPaletteBlend = now; // starts blending immediately } - // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in strip.getTransition() time) - // if randomPaletteChangeTime is shorter than strip.getTransition() palette will never fully blend + // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in strip.getTransitionDur() time) + // if randomPaletteChangeTime is shorter than strip.getTransitionDur() palette will never fully blend unsigned frameTime = strip.getFrameTime(); // in ms [8-1000] - unsigned transitionTime = strip.getTransition(); // in ms [100-65535] + unsigned transitionTime = strip.getTransitionDur(); // in ms [100-65535] if ((uint16_t)now < Segment::_nextPaletteBlend || now > ((Segment::_lastPaletteChange*1000) + transitionTime + 2*frameTime)) return; // not yet time or past transition time, no need to blend unsigned transitionFrames = frameTime > transitionTime ? 1 : transitionTime / frameTime; // i.e. 700ms/23ms = 30 or 20000ms/8ms = 2500 or 100ms/1000ms = 0 -> 1 unsigned noOfBlends = transitionFrames > 255 ? 1 : (255 + (transitionFrames>>1)) / transitionFrames; // we do some rounding here @@ -536,7 +622,7 @@ Segment &Segment::setColor(uint8_t slot, uint32_t c) { if (slot == 1 && c != BLACK) return *this; // on/off segment cannot have secondary color non black } //DEBUG_PRINTF_P(PSTR("- Starting color transition: %d [0x%X]\n"), slot, c); - startTransition(strip.getTransition(), blendingStyle != TRANSITION_FADE); // start transition prior to change + startTransition(strip.getTransitionDur(), TRANSITION_KIND_COLOR); // start transition prior to change colors[slot] = c; stateChanged = true; // send UDP/WS broadcast return *this; @@ -550,7 +636,7 @@ Segment &Segment::setCCT(uint16_t k) { } if (cct != k) { //DEBUG_PRINTF_P(PSTR("- Starting CCT transition: %d\n"), k); - startTransition(strip.getTransition(), false); // start transition prior to change (no need to copy segment) + startTransition(strip.getTransitionDur(), TRANSITION_KIND_FADE); // start transition prior to change (no need to copy segment) cct = k; stateChanged = true; // send UDP/WS broadcast } @@ -560,7 +646,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(), false); // opacity change always fades (no segment copy needed) + startTransition(strip.getTransitionDur(), TRANSITION_KIND_FADE); // opacity change always fades (no segment copy needed) opacity = o; stateChanged = true; // send UDP/WS broadcast } @@ -571,7 +657,7 @@ Segment &Segment::setOption(uint8_t n, bool val) { bool prev = (options >> n) & 0x01; if (val == prev) return *this; //DEBUG_PRINTF_P(PSTR("- Starting option transition: %d\n"), n); - if (n == SEG_OPTION_ON) startTransition(strip.getTransition(), blendingStyle != TRANSITION_FADE); // start transition prior to change + if (n == SEG_OPTION_ON) startTransition(strip.getTransitionDur(), TRANSITION_KIND_COLOR, true); // on/off transitions need a copy (spatial) and enable reversal if (val) options |= 0x01 << n; else options &= ~(0x01 << n); stateChanged = true; // send UDP/WS broadcast @@ -584,7 +670,7 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) { if (fx >= strip.getModeCount()) fx = 0; // set solid mode // if we have a valid mode & is not reserved if (fx != mode) { - startTransition(strip.getTransition(), true); // set effect transitions (must create segment copy) + startTransition(strip.getTransitionDur(), TRANSITION_KIND_EFFECT); // set effect transitions (must create segment copy) mode = fx; int sOpt; // load default values from effect string @@ -624,7 +710,7 @@ Segment &Segment::setPalette(uint8_t pal) { } if (pal != palette) { //DEBUG_PRINTF_P(PSTR("- Starting palette transition: %d\n"), pal); - startTransition(strip.getTransition(), blendingStyle != TRANSITION_FADE); // start transition prior to change (no need to copy segment) + startTransition(strip.getTransitionDur(), TRANSITION_KIND_COLOR); // start transition prior to change palette = pal; stateChanged = true; // send UDP/WS broadcast } @@ -637,7 +723,7 @@ Segment &Segment::setName(const char *newName) { if (newLen) { if (name) p_free(name); // free old name name = static_cast(allocate_buffer(newLen+1, BFRALLOC_PREFER_PSRAM)); - if (mode == FX_MODE_2DSCROLLTEXT) startTransition(strip.getTransition(), true); // if the name changes in scrolling text mode, we need to copy the segment for blending + if (mode == FX_MODE_2DSCROLLTEXT) startTransition(strip.getTransitionDur(), TRANSITION_KIND_EFFECT); // if the name changes in scrolling text mode, we need to copy the segment for blending if (name) strlcpy(name, newName, newLen+1); return *this; } @@ -1334,8 +1420,8 @@ void WS2812FX::service() { doShow = true; if (!seg.freeze) { //only run effect function if not frozen // Effect blending - uint16_t prog = seg.progress(); - seg.beginDraw(prog); // set up parameters for get/setPixelColor() (will also blend colors and palette if blend style is FADE) + uint16_t prog = seg.fadeProgress(); // color blending uses fade channel progress + seg.beginDraw(prog); // set up parameters for get/setPixelColor() (will also blend colors and palette) _currentSegment = &seg; // set current segment for effect functions (SEGMENT & SEGENV) // workaround for on/off transition to respect blending style _mode[seg.mode](); // run new/current mode (needed for bri workaround) @@ -1346,7 +1432,7 @@ void WS2812FX::service() { if (segO && segO->isActive() && (seg.mode != segO->mode || blendingStyle != TRANSITION_FADE || (segO->name != seg.name && segO->name && seg.name && strncmp(segO->name, seg.name, WLED_MAX_SEGNAME_LEN) != 0))) { Segment::modeBlend(true); // set flag for beginDraw() to blend colors and palette - segO->beginDraw(prog); // set up palette & colors (also sets draw dimensions), parent segment has transition progress + segO->beginDraw(0xFFFFU); // old segment renders its captured state (no fade), parent segment holds transition progress _currentSegment = segO; // set current segment // workaround for on/off transition to respect blending style _mode[segO->mode](); // run old mode (needed for bri workaround; semaphore!!) @@ -1513,8 +1599,8 @@ 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; - // single pixel segments or opacity/CCT transition and not on/off transition: use fade - if (width*height == 1 || (!segO && !(topSegment.isInTransition() && (briOld == 0 || bri == 0)))) blendingStyle = TRANSITION_FADE; + // single pixel segments or transitions without a rendered old segment: use fade + if (width*height == 1 || !segO) blendingStyle = TRANSITION_FADE; switch (blendingStyle) { case TRANSITION_CIRCULAR_IN: // (must set entire segment, see isPixelXYClipped()) case TRANSITION_CIRCULAR_OUT:// (must set entire segment, see isPixelXYClipped()) @@ -1635,13 +1721,8 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { // we need to blend old segment using fade as pixels are not clipped 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 (applies while a global on/off transition is active) - // (bri != briT) && !bri => from On to Off - // (bri != briT) && bri => from Off to On - // note: only blank pixels once the segment transition has actually started; bri changes before - // startTransition() is called (stateUpdated()) and a frame rendered in that window would blank the whole segment - if ((briOld == 0 || bri == 0) && ((!clipped && topSegment.isInTransition() && !bri) || (clipped && topSegment.isInTransition() && bri))) c_a = BLACK; + // on/off transitions: pixels not yet revealed by a wipe-to-off are black, pixels still covered by a wipe-to-on are black + if ((topSegment.isPowerOffTransition() && !clipped) || (topSegment.isPowerOnTransition() && clipped)) c_a = BLACK; } // map it into frame buffer x = c; // restore coordiates if we were PUSHing @@ -1710,13 +1791,11 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { // we need to blend old segment using fade as pixels are not clipped 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 - // note: only blank pixels once the segment transition has actually started; bri changes before - // startTransition() is called (stateUpdated()) and a frame rendered in that window would blank the whole segment - if ((briOld == 0 || bri == 0) && ((!clipped && topSegment.isInTransition() && !bri) || (clipped && topSegment.isInTransition() && bri))) c_a = BLACK; + // blackout semantics for on/off transitions: pixels not yet revealed by a wipe-to-off are black, + // pixels still covered by a wipe-to-on are black (the wipe reveals from black) + // note: only blank pixels once the segment transition has actually started; the power change is applied + // before startTransition() is called (stateUpdated()) and a frame rendered in that window would blank the whole segment + if ((topSegment.isPowerOffTransition() && !clipped) || (topSegment.isPowerOnTransition() && clipped)) c_a = BLACK; } // map into frame buffer i = k; // restore index if we were PUSHing @@ -1818,13 +1897,11 @@ void WS2812FX::restartRuntime() { } // start or stop transition for all segments -void WS2812FX::setTransitionMode(bool t) { +void WS2812FX::setTransitionMode(bool start, bool powerOn) { suspend(); waitForIt(); 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); + seg.startTransition(start ? _transitionDur : 0, TRANSITION_KIND_COLOR, true, start ? (powerOn ? 1 : 0) : -1); } resume(); } @@ -1863,8 +1940,11 @@ void WS2812FX::setBrightness(uint8_t b, bool direct) { if (gammaCorrectBri) b = gamma8(b); if (_brightness == b) return; _brightness = b; - if (_brightness == 0) { //unfreeze all segments on power off - for (const Segment &seg : _segments) seg.freeze = false; // freeze is mutable + if (_brightness == 0) { // unfreeze all segments on power off and stop all ongoing segment transitions + for (Segment &seg : _segments) { + seg.freeze = false; // freeze is mutable + seg.stopTransition(); // stop transition, nothing to display anymore + } } BusManager::setBrightness(scaledBri(b)); if (!direct) { diff --git a/wled00/led.cpp b/wled00/led.cpp index 17fede62ff..41da5c119e 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -43,12 +43,13 @@ void toggleOnOff() { if (bri == 0) { + briOld = 0; // in case a transition is active, this ensures we can detect a "zero crossing" in stateUpdated() to handle the transition reversal correctly bri = briLast; strip.restartRuntime(); } else { briLast = bri; - bri = 0; + bri = 0; // note: no need to set briOld here, stateUpdated() will handle it correctly } stateChanged = true; } @@ -67,7 +68,7 @@ byte scaledBri(byte in) void applyBri() { if (realtimeOverride || !(realtimeMode && arlsForceMaxBri)) { - //DEBUG_PRINTF_P(PSTR("Applying strip brightness: %d (%d,%d)\n"), (int)briT, (int)bri, (int)briOld); + DEBUG_PRINTF_P(PSTR("Applying strip brightness: %d (%d,%d)\n"), (int)briT, (int)bri, (int)briOld); strip.setBrightness(briT); } } @@ -81,7 +82,6 @@ void applyFinalBri() { strip.trigger(); // force one last update } - //called after every state changes, schedules interface updates, handles brightness transition and nightlight activation //unlike colorUpdated(), does NOT apply any colors or FX to segments void stateUpdated(byte callMode) { @@ -122,17 +122,32 @@ void stateUpdated(byte callMode) { // notify usermods of state change UsermodManager::onStateChange(callMode); - if (strip.getTransition() == 0) { + // global brightness transition handling + DEBUG_PRINTF_P(PSTR("*****************state update: briT: %d bri: %d briOld: %d\n"), (int)briT, (int)bri, (int)briOld); + + if (strip.getTransitionDur() == 0) { jsonTransitionOnce = false; transitionActive = false; + Serial.println("strip.getTransitionDur() == 0"); applyFinalBri(); } else { + // check if this is global on or off (bri crosses 0) and if so, force all segments to transition mode if not using FADE (starts a spatial segment transition like swipe etc.) + bool isOnOffTransition = (bri == 0 || (bri > 0 && briOld == 0)) && blendingStyle != TRANSITION_FADE; + if ((bri == 0 || (bri > 0 && briOld == 0)) && blendingStyle != TRANSITION_FADE) { + strip.setTransitionMode(true, bri > 0); // force all segments to transition mode, also handles transition inversion (on during off or off during on) + } if (transitionActive) { - briOld = briT; - } else if (bri != briOld || stateChanged) - strip.setTransitionMode(true); // force all segments to transition mode - transitionActive = true; - transitionStartTime = now; + // record current transition brightness + Serial.println("state: transition active, briT: " + String(briT) + " bri: " + String(bri) + " briOld: " + String(briOld)); + briOld = briT; // update old value to current transition value + //transitionStartTime = now; // restart transition + } + + // if brightness changed, start a new global transition. note: fading is omitted if segments started a spatial transition (i.e. on/off with non FADE transition), see handleTransitions() + if (bri != briOld) { + transitionActive = true; + transitionStartTime = now; + } } stateChanged = false; } @@ -163,11 +178,13 @@ void handleTransitions() { //handle still pending interface update updateInterfaces(interfaceUpdateCallMode); - if (transitionActive && strip.getTransition() > 0) { + if (transitionActive && strip.getTransitionDur() > 0) { int ti = millis() - transitionStartTime; - int tr = strip.getTransition(); + int tr = strip.getTransitionDur(); + // finalize once the transition time has elapsed if (ti/tr) { - strip.setTransitionMode(false); // stop all transitions + //if (bri == 0) + strip.setTransitionMode(false); // stop all transitions TODO: this stops any transition after a global brightness transition, this is only needed for off transition!!! // restore (global) transition time if not called from UDP notifier or single/temporary transition from JSON (also playlist) if (jsonTransitionOnce) strip.setTransition(transitionDelay); transitionActive = false; @@ -178,10 +195,10 @@ void handleTransitions() { byte briTO = briT; 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; + briT = (bri == 0) ? briOld : bri; // TODO: this will snap brightness if a fade is going on. should this be omitted? } else { int deltaBri = (int)bri - (int)briOld; - briT = briOld + (deltaBri * ti / tr); + briT = briOld + (deltaBri * min(ti, tr) / tr); // clamp: the ramp must not overshoot to avoid glitches (interrupted transitions or global on/off) } if (briTO != briT) applyBri(); } @@ -236,7 +253,7 @@ void handleNightlight() { { for (unsigned i=0; i<4; i++) colPri[i] = colNlT[i]+ ((colSec[i] - colNlT[i])*nper); // fading from actual color to secondary color } - uint16_t transitionduration = strip.getTransition(); + uint16_t transitionduration = strip.getTransitionDur(); strip.setTransition(0); // temporary disable transition and set color & brightness directly, (hacky fix for #5620) colorUpdated(CALL_MODE_NO_NOTIFY); strip.setTransition(transitionduration); // restore transition time to previous value. Note: this needs proper fixing by disabling transitions completely in nightlight mode, reference implementation https://github.com/blazoncek/WLED/commit/c01a6b774969b652c30e383073958302042fd1f9 From 2992c585ffc6bee004508045919de63da3a427d1 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 5 Sep 2026 13:59:44 +0200 Subject: [PATCH 07/19] most special cases work now, needs more testing and cleanup --- wled00/FX.h | 53 ++++++----- wled00/FX_fcn.cpp | 230 ++++++++++++++++++++++++++++------------------ wled00/led.cpp | 100 ++++++++++++-------- 3 files changed, 231 insertions(+), 152 deletions(-) diff --git a/wled00/FX.h b/wled00/FX.h index d639b7488b..11d52e6073 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -405,11 +405,20 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define TRANSITION_PUSH_MASK 0x10 #define TRANSITION_COUNT 18 -// transition kind: identifies which change triggered the transition (fade channel always runs, spatial channel only with segment copy) -#define TRANSITION_KIND_FADE 0 // attribute-only change (opacity, CCT): fade channel, never needs segment copy -#define TRANSITION_KIND_COLOR 1 // color or palette change -#define TRANSITION_KIND_EFFECT 2 // effect change -#define TRANSITION_KIND_POWER 3 // segment on/off change +// transition kind (low nibble of startTransition() parameter): identifies which change triggered the transition (fade channel always runs, spatial channel only with segment copy) +#define TRANSITION_KIND_FADE 0x00 // attribute-only change (opacity, CCT): fade channel, never needs segment copy +#define TRANSITION_KIND_DEFAULT 0x01 // on/off or color/palette change (will use segment copy if spatial transition) +#define TRANSITION_KIND_EFFECT 0x02 // effect change +#define TRANSITION_KIND_MASK 0x0F + +// power transition flags (high nibble of startTransition() parameter) +#define TRANSITION_POWER_OFF 0x10 // global power transition with explicit target: off (only global brightness changes, segment on state is kept) +#define TRANSITION_POWER_ON 0x20 // global power transition with explicit target: on +#define TRANSITION_POWER_TOGGLE 0x30 // segment on/off, target is the inverted segment on state (segment setters call startTransition() before applying the change) +#define TRANSITION_POWER_TRIGGER 0x40 // set true if the transition was just triggered, used in stateUpdated() +#define TRANSITION_POWER_MASK 0x30 // mask for power transition flags, excluding the trigger flag + + // transition flags (scope and target state of a power transition) #define TRANSITION_FLAG_POWER 0x01 // power (on/off) transition @@ -523,14 +532,13 @@ class Segment { uint16_t _fadeProgress; // fade channel progress (0-65535) uint8_t _prevPaletteBlends; // number of previous palette blends (there are max 255 blends possible) uint8_t _palette, _bri, _cct; // palette ID, brightness and CCT at the start of fade channel (brightness will be 0 if segment was off) - uint8_t _kind; // transition kind: Fade, Content, Effect (one of TRANSITION_KIND_*) TODO: this is now unused, remove it? uint8_t _flags; // TRANSITION_FLAG_* power state Transition(uint16_t dur=750) : _oldSegment(nullptr) , _start(millis()) , _fadeStart(_start) , _colors{0,0,0} - , _palT(CRGBPalette16()) // TODO: remove _palT as it is not necessary. + , _palT(CRGBPalette16()) , _dur(dur) , _fadeDur(dur) , _progress(0) @@ -539,7 +547,6 @@ class Segment { , _palette(0) , _bri(0) , _cct(0) - , _kind(0) , _flags(0) {} ~Transition() { @@ -565,13 +572,7 @@ class Segment { // transition functions void stopTransition(); // ends transition mode by destroying transition structure (does nothing if not in transition) void updateTransitionProgress() const; // sets transition progress (0-65535) based on time passed since transition start - inline void handleTransition() { - updateTransitionProgress(); - if (isInTransition()) { - if (_t->_oldSegment && _t->_progress == 0xFFFFU) { delete _t->_oldSegment; _t->_oldSegment = nullptr; } // spatial channel completed, release segment copy - if (progress() == 0xFFFFU && fadeProgress() == 0xFFFFU) stopTransition(); - } - } + void handleTransition(); // handles transition progress and ends transitions when completed inline static void modeBlend(bool blend) { Segment::_modeBlend = blend; } // for isPreviousMode() inline static void setClippingRect(int startX, int stopX, int startY = 0, int stopY = 1) { _clipStart = startX; _clipStop = stopX; _clipStartY = startY; _clipStopY = stopY; }; inline static bool isPreviousMode() { return Segment::_modeBlend; } // needed for determining CCT/opacity during non-TRANSITION_FADE transition @@ -655,11 +656,12 @@ class Segment { inline bool getOption(uint8_t n) const { return ((options >> n) & 0x01); } inline bool isSelected() const { return selected; } inline bool isInTransition() const { return _t != nullptr; } - inline uint16_t progress() const { return isInTransition() && _t->_oldSegment ? _t->_progress : 0xFFFFU; } // spatial channel progress, relies on handleTransition()/updateTransitionProgress() + inline uint16_t progress() const { return isInTransition() ? _t->_progress : 0xFFFFU; } // spatial channel progress, relies on handleTransition()/updateTransitionProgress() inline uint16_t fadeProgress() const { return isInTransition() ? _t->_fadeProgress : 0xFFFFU; } // fade channel progress, relies on handleTransition()/updateTransitionProgress() inline unsigned long getTransitionStart() const { return isInTransition() ? _t->_start : 0; } // spatial channel start time inline Segment *getOldSegment() const { return isInTransition() ? _t->_oldSegment : nullptr; } - // power transition helpers (only true while a spatial on/off transition is running) + // power transition helpers (true while a spatial on/off transition is running; a power-off transition keeps its + // segment copy until the whole transition - incl. the fade channel - ends, see handleTransition()) inline bool isPowerTransition() const { return isInTransition() && (_t->_flags & TRANSITION_FLAG_POWER) && _t->_oldSegment != nullptr; } inline bool isPowerOffTransition() const { return isPowerTransition() && !(_t->_flags & TRANSITION_FLAG_POWER_ON); } // spatial to off inline bool isPowerOnTransition() const { return isPowerTransition() && (_t->_flags & TRANSITION_FLAG_POWER_ON); } // spatial to on @@ -709,9 +711,9 @@ class Segment { inline Segment &markForReset() { reset = true; return *this; } // setOption(SEG_OPTION_RESET, true) // transition has to start before actual segment values change - // powerTarget: target on-state for power transitions; -1 = derive from !on (segment setters call this before applying the change), - // 0/1 = explicit target (global on/off never toggles segment on state, only global brightness changes) - void startTransition(uint16_t dur, uint8_t kind = TRANSITION_KIND_COLOR, bool isPower = false, int8_t powerTarget = -1); + // kind: low nibble = TRANSITION_KIND_* (which change triggered the transition), high nibble = TRANSITION_POWER_* flags + // (POWER_ON/POWER_OFF = global on/off with explicit target, POWER_TOGGLE = segment on/off, target derived from !on) + void startTransition(uint16_t dur, uint8_t kind = TRANSITION_KIND_DEFAULT); uint8_t currentCCT() const; // current segment's CCT (blended while in transition) uint8_t currentBri() const; // current segment's opacity/brightness (blended while in transition) @@ -874,6 +876,7 @@ class WS2812FX { _frametime(FRAMETIME_FIXED), _cumulativeFps(WLED_FPS << FPS_CALC_SHIFT), _targetFps(WLED_FPS), + _poweringOnOff(0), _isServicing(false), _isOffRefreshRequired(false), _hasWhiteChannel(false), @@ -942,7 +945,7 @@ class WS2812FX { inline void resume() { _suspend = false; } // will resume strip.service() execution void restartRuntime(); - void setTransitionMode(bool start, bool powerOn = false); + void setTransitionMode(bool start); bool checkSegmentAlignment() const; bool hasRGBWBus() const; @@ -955,6 +958,11 @@ class WS2812FX { inline bool isOffRefreshRequired() const { return _isOffRefreshRequired; } // returns true if strip requires regular updates (i.e. TM1814 chipset) inline bool isSuspended() const { return _suspend; } // returns true if strip.service() execution is suspended inline bool needsUpdate() const { return _triggered; } // returns true if strip received a trigger() request + inline bool isPoweringOff() const { return _poweringOnOff & TRANSITION_POWER_OFF; } // returns true while a global power-off transition is running + inline bool isPoweringOn() const { return _poweringOnOff & TRANSITION_POWER_ON; } // returns true while a global power-on transition is running + inline bool isPowerTrigger() const { return _poweringOnOff & TRANSITION_POWER_TRIGGER; } // returns true if transition was triggered by toggleOnOff() + inline void setPowerFlag(uint8_t flag) { _poweringOnOff |= flag; } // set a global power transition flag + inline void clearPowerFlag(uint8_t flag) { _poweringOnOff &= ~flag; } // clear a global power transition flag // uint8_t paletteBlend; // obsolete - use global paletteBlend instead of strip.paletteBlend uint8_t getActiveSegmentsNum() const; @@ -978,7 +986,7 @@ class WS2812FX { inline uint16_t getFrameTime() const { return _frametime; } // returns amount of time a frame should take (in ms) inline uint16_t getMinShowDelay() const { return MIN_FRAME_DELAY; } // returns minimum amount of time strip.service() can be delayed (constant) inline uint16_t getLength() const { return _length; } // returns actual amount of LEDs on a strip (2D matrix may have less LEDs than W*H) - inline uint16_t getTransitionDur() const { return _transitionDur; } // returns currently set transition duration time (in ms) + inline uint16_t getTransition() const { return _transitionDur; } // returns currently set transition duration time (in ms) inline uint16_t getMappedPixelIndex(uint16_t index) const { // convert logical address to physical if (index < customMappingSize && (realtimeMode == REALTIME_MODE_INACTIVE || realtimeRespectLedMaps)) index = customMappingTable[index]; return index; @@ -1057,6 +1065,7 @@ class WS2812FX { uint16_t _frametime; uint16_t _cumulativeFps; uint8_t _targetFps; + uint8_t _poweringOnOff; // global power transition in progress: TRANSITION_POWER_ON/OFF, 0 = none (suppresses new segment transitions, see Segment::startTransition()) // will require only 1 byte struct { diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index c4577a4193..1aa4c694eb 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -287,26 +287,70 @@ void Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { } } +void Segment::handleTransition() { + updateTransitionProgress(); + if (isInTransition() && !strip.isPoweringOff()) { + // end transitions if completed but wait for a power-off transition to complete to avoid revealing pixels (see blendSegment() blanking) + // pixel blanking in blendSegment() rely on is PowerOffTransition() (which requires the copy) TODO: should we use the _t->_flags instead of isPoweringOff()? + if (_t->_oldSegment && _t->_progress == 0xFFFFU && !strip.isPoweringOff()) { + Serial.printf("handleTransition: deleting old segment copy, progress=%d\n", _t->_progress); + delete _t->_oldSegment; _t->_oldSegment = nullptr; + } + if (progress() == 0xFFFFU && fadeProgress() == 0xFFFFU) { + Serial.printf("handleTransition: transition completed, progress=%d fadeProgress=%d\n", _t->_progress, _t->_fadeProgress); + stopTransition(); // Transition frees a kept copy + } + } +} + +/* Note on how transitions work: + There are three transition channels: global on strip level that handles global brightness fading and triggering of segment spatial transitions (see led.cpp) + on segment level there are two independent channels: a fade channel that handles colors, palette, CCT and opacity/brightness and a spatial channel + that handles swipe/push/etc. using a copy of the previous segment (aka oldSegment). FX transitions always need the oldSegment but can be spatial or fade. + There are many "special rules" that apply to handle transition updates i.e. calling startTransition() while a transition is already running. + Here is a broad summary of the rules: + - Off transition takes priority, in general no other transitions are allowed to simplify the logic, "offMode" is set once the global off finishes + since this would require careful sync to the segment transition (for example using swipe) the segment is held in transition until global finishes (see handleTransition()) + - On strip level, there are flags to check for global on/off transitions which are set in toggleOnOff() + - A global transition is started in stateUpdated() and triggers segment transitions if needed for spatial transitions + - When a spatial on/off transition is triggered during an ongoing on/off transition, it is reversed (i.e. same number of LEDs are lit but reversed) + - Fade transitions continue from the current blend state if issued during a running transition + - A spatial transition never restarts but fade transitions can run in parallel (unless powering off globally or a segment) + - If a spatial transition is running, only brightness fading is allowed in parallel. Any other change applies immediately. + - For more details, see the comments throughout the code + In general the transition logic was chosen to avoid glitches or flashing while allowing segments to act as individual "lights" + Segment opacity is faded in currentBri() +*/ + // starting a transition has to occur before change so we get current values 1st // note: _t is the temporary segment that holds the values transitioned from (palette, colors, brightness,...) and the current segment holds the "to" values // the transition has two independent channels: // the fade channel (_fadeStart/_fadeDur/_fadeProgress) crossfades colors, palette, CCT and opacity and never needs a segment copy // the spatial channel (_start/_dur/_progress and _oldSegment) renders wipe/push/etc. using a copy of the previous segment -// kind: TRANSITION_KIND_* identifying which change triggered the transition (determines whether a segment copy is needed) -// isPower: true for on/off transitions (segment or global); uses black clipping pixels in blendSegment() and transition time reversal when changing mid transition -// powerTarget: target on-state for global power transitions: 0 = off, 1 = on. Otherwise it is -1 -> use segment on state, i.e. if segment is off, the target is on) -void Segment::startTransition(uint16_t dur, uint8_t kind, bool isPower, int8_t powerTarget) { +// kind: low nibble = TRANSITION_KIND_x identifying which change triggered the transition (determines whether a segment copy is needed) +// high nibble = TRANSITION_POWER_x flags: POWER_ON/POWER_OFF = global on/off, POWER_TOGGLE = segment on/off (both flags are set) + +void Segment::startTransition(uint16_t dur, uint8_t kind) { + const uint8_t power = kind & TRANSITION_POWER_MASK; // power flags (TRANSITION_POWER_*) + kind &= TRANSITION_KIND_MASK; // strip the power flags + const bool targetOn = power == TRANSITION_POWER_TOGGLE ? !on : power == TRANSITION_POWER_ON; // target on-state for power transitions + Serial.printf("startTransition: dur=%d kind=%d power=%d targetOn=%d\n", dur, kind, power, targetOn); + // do not interrupt a global power off transition (unless its a global reversal) otherwise we can turn segment on and it will jump to off once global completes TODO: this is broken again + // TODO: the issue is now this: if the next line is used, a segment on during global off will just switch it on as that segment is not in a transition. + // if the line is omitted, it will start an on transition but then switch fully off once global off completes. this may be a good compromise as a proper solution may need many more conditionals + //if (strip.isPoweringOff() && (power != TRANSITION_POWER_OFF && power != TRANSITION_POWER_ON)) return; + // check if we even need to start a transition. No transitions if transitions disabled, not an active segment or not in an on state (unless this is a power-on request) - if (dur == 0 || !isActive() || (!isPower && (!on || bri == 0))) { - if (isInTransition()) _t->_dur = _t->_fadeDur = 0; // complete any running transition immediately + // TODO: the simpler version starts a transition on off segments but they will not render as both old and new segment are off. when switching to on, it stops transitions so still works + // the only downside is that during a global off, it also fades, then a segment on starts rendering the new segment because it reverses. + //if (dur == 0 || !isActive()) || ((power != TRANSITION_POWER_TOGGLE) && !on)) { + if (dur == 0 || !isActive()) { return; } - // determine target "on" state for power transitions (if used), see powerTarget parameter description above - const bool targetOn = powerTarget >= 0 ? (powerTarget != 0) : !on; // check if we need a copy of current segment: only effect transitions and transitions using a spatial (non-FADE) style const bool segmentCopy = kind == TRANSITION_KIND_EFFECT || (kind != TRANSITION_KIND_FADE && blendingStyle != TRANSITION_FADE); - - // helper lambda function to capture current _bri/_cct and optionally _colors to the segments transitions (_t) state + Serial.printf("*****startTransition: dur=%d kind=%d power=%d targetOn=%d segmentCopy=%d seg is on=%d\n", dur, kind, power, targetOn, segmentCopy, on); + // helper lambda function to capture current _bri/_cct and optionally _colors to the segments transitions (_t) state TDODO: needs refinement const auto captureBlend = [&](bool rebaseColors, unsigned long fadeStart) { if (rebaseColors) for (unsigned i = 0; i < NUM_COLORS; i++) _t->_colors[i] = color_blend16(_t->_colors[i], colors[i], _t->_fadeProgress); _t->_bri = currentBri(); @@ -318,10 +362,10 @@ void Segment::startTransition(uint16_t dur, uint8_t kind, bool isPower, int8_t p if (isInTransition()) { // re-targeting a running transition: fade restarts, starting from current blend, spatial continues but replaces the target (FX, color, palette) immediately - if (!isPower) { - // opacity/CCT/color/palette/FX change: rebase fades to the current visual blend and restart it (no jump) a running spatial transition continues + if (!power) { + // opacity/CCT/color/palette/FX change: rebase fades to the current visual blend and restart it (no jump). A running spatial transition continues if (segmentCopy) { - // FX change: keep the existing old segment and its timeline, only the revealed (new) side switches FX + // spatial change: if already in a spatial transition, do not restart, new segment is updated immediately and transition continues if (_t->_oldSegment == nullptr) { // no old segment, meaning a fade transition is going on (color, palette, opacity, cct) _t->_oldSegment = new(std::nothrow) Segment(*this); // store/copy current segment settings @@ -330,9 +374,9 @@ void Segment::startTransition(uint16_t dur, uint8_t kind, bool isPower, int8_t p for (unsigned i = 0; i < NUM_COLORS; i++) _t->_oldSegment->colors[i] = color_blend16(_t->_colors[i], colors[i], _t->_progress); _t->_oldSegment->opacity = currentBri(); // captrue current opacity in case it was being faded _t->_oldSegment->cct = currentCCT(); // capture current CCT in case it was being faded - // _t->_oldSegment->_currentPalette = _t->_palT; // capture current palette (might be partially faded) _t->_start = millis(); // (re)start transition (but do not restart fade channel so fading continues on current segment) _t->_dur = dur; + // _t->_oldSegment->_currentPalette = _t->_palT; // capture current palette (might be partially faded) TODO: this is not working, _currentPalette is static, investigate why DEBUGFX_PRINTF_P(PSTR("-- Updated transition with segment copy: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); //if (!_t->_oldSegment->isActive()) { stopTransition(); return; } // todo: remove? is now handled above before creating a old segment } else { @@ -343,71 +387,85 @@ void Segment::startTransition(uint16_t dur, uint8_t kind, bool isPower, int8_t p _t->_prevPaletteBlends = 0; } } - // _t->_kind = TRANSITION_KIND_EFFECT; - } else if (_t->_oldSegment != nullptr && kind == TRANSITION_KIND_COLOR) { + } else if (_t->_oldSegment != nullptr && kind == TRANSITION_KIND_DEFAULT) { // color/palette change during a spatial transition: do not fade colors but set them immediately as we are not in a fade transition, do not restart transition + // TODO: actually would be better to fade instead of snap? any bad consequences from that? -> it can cause intermediate colors, when switching on/off but need to test //captureBlend(true, 0); //_t->_fadeDur = 0; // fade completed immediately //_t->_palette = palette; - //loadPalette(_t->_palT, palette); // load new palette into transition state (updates immediately) - // _t->_kind = TRANSITION_KIND_COLOR; - } + loadPalette(_t->_palT, palette); // load new palette into transition state (updates immediately) + } else { // capture the current visual blend as the new "from" state so the incoming change does not cause a visible jump. // _palT already holds the intermediate blended palette and will continue blending toward the new target (see beginDraw()). captureBlend(true, millis()); // capture current colors, bri & CCT, restart transition //_t->_prevPaletteBlends = 0; - // if (kind == TRANSITION_KIND_COLOR) _t->_kind = kind; // color/palette change upgrades kind (also from Effect so a later FX change can re-capture) } return; } - // power (on/off) transition (per segment or global) + Serial.println("***power transition"); + // power (on/off) transition (per segment or global) during an ongoing power transition if (_t->_flags & TRANSITION_FLAG_POWER) { - // already in a power transition + Serial.printf("***power transition: already in a power transition, dur=%d start=%d now=%d progress=%d\n", _t->_dur, _t->_start, millis(), _t->_progress); if (targetOn == ((_t->_flags & TRANSITION_FLAG_POWER_ON) != 0)) return; // same target re-issued, let the running transition finish - // reverse in place: invert the spatial timeline (a 20%-completed swipe continues from 80%) - // keep the same style, no re-copy, no restart. progress is re-calculated from _start/_dur so backdate the start - unsigned long nowT = millis(); - _t->_dur = dur; - _t->_start = nowT - (((unsigned)(0xFFFFU - _t->_progress) * dur) / 0xFFFFU); - if (powerTarget < 0) { - // powerTarget == -1 means segment-level on/off: the fade channel target flips with the on state (on ? opacity : 0), so rebase the - // fade channel to the current visual blend and reverse it along with the spatial channel - // (also reverses a still-running fade after the spatial channel already completed, i.e. _oldSegment == nullptr) - // capture before inverting progress as currentBri()/currentCCT() rely on _fadeProgress - _t->_bri = opacity; - _t->_cct = cct; - //_t->_fadeDur = dur; - //_t->_fadeStart = nowT - (((unsigned)(0xFFFFU - _t->_fadeProgress) * dur) / 0xFFFFU); - if (_t->_oldSegment) _t->_oldSegment->on = !_t->_oldSegment->on; // invert old segment's on state + + Serial.printf("***power transition: reversing ongoing power transition, dur=%d start=%d now=%d progress=%d\n", _t->_dur, _t->_start, millis(), _t->_progress); + if (blendingStyle != TRANSITION_FADE) { + // already in a power transition reverse in place: invert the spatial timeline (20%-completed swipe continues from 80%) + _t->_dur = dur; + _t->_start = millis() - (((unsigned)(0xFFFFU - _t->_progress) * dur) / 0xFFFFU); + //_t->_fadeDur = dur; + //_t->_fadeStart = _t->_start; // sync fade channel, we need them to complete at the same time (if turning off, segment will not turn off until fade is complete, causing a flash) + _t->_fadeDur = 0; // fade completed immediately + // TODO: when reversing a spatial transition, we should re-copy the current segment as it may have changed since the start + //if (_t->_oldSegment) { delete _t->_oldSegment; _t->_oldSegment = nullptr; } + //_t->_oldSegment = new(std::nothrow) Segment(*this); // create a fresh copy + } + if (power == TRANSITION_POWER_TOGGLE) { + // segment-level on/off + Serial.printf("***power toggle: reversing spatial timeline: dur=%d start=%d now=%d progress=%d\n", _t->_dur, _t->_start, millis(), _t->_progress); + if (_t->_oldSegment) { + if (strip.isPoweringOff()) _t->_flags ^= TRANSITION_FLAG_POWER_ON; // flip POWER_ON flag, it is flipped back below, we need it to stay off if a segment is turned on during global off + if (!strip.isPoweringOn()) _t->_oldSegment->on = !_t->_oldSegment->on; // invert old segment's on state (but do not turn old segment off so rendering continues) + _t->_oldSegment->opacity = opacity; + _t->_oldSegment->cct = cct; + } + else captureBlend(true, millis()); // capture current fade status when reversing + } + if (_t->_oldSegment == nullptr && segmentCopy) { + _t->_oldSegment = new(std::nothrow) Segment(*this); // create a copy if there is none + for (unsigned i = 0; i < NUM_COLORS; i++) _t->_oldSegment->colors[i] = color_blend16(_t->_colors[i], colors[i], _t->_progress); + _t->_oldSegment->opacity = currentBri(); // captrue current opacity in case it was being faded + _t->_oldSegment->cct = currentCCT(); // capture current CCT in case it was being faded } // global on/off: the fade channel (colors/opacity/CCT) is independent of the power state, leave it running - _t->_prevPaletteBlends = 0; + //_t->_prevPaletteBlends = 0; _t->_flags ^= TRANSITION_FLAG_POWER_ON; // flip POWER_ON flag return; } - // stop a spatial transition immediately: we only have one transition counter and need to restart for power off + Serial.printf("***power transition: starting new power transition, dur=%d start=%d now=%d progress=%d\n", _t->_dur, _t->_start, millis(), _t->_progress); + // global or segment on/off: stop ongoing segment transition immediately and start the power transition from scratch if (_t->_oldSegment) { delete _t->_oldSegment; _t->_oldSegment = nullptr; } // rebase fade channel to the current visual blend before starting the new power transition for (unsigned i = 0; i < NUM_COLORS; i++) _t->_colors[i] = color_blend16(_t->_colors[i], colors[i], _t->_fadeProgress); _t->_bri = currentBri(); _t->_cct = currentCCT(); - _t->_fadeStart = millis(); - _t->_fadeDur = dur; - _t->_prevPaletteBlends = 0; - if (segmentCopy) _t->_oldSegment = new(std::nothrow) Segment(*this); // store/copy current segment settings + if (segmentCopy) _t->_oldSegment = new(std::nothrow) Segment(*this); // spatial transition, need a fresh copy + else { + _t->_fadeStart = millis(); // fade transition, restart fading + _t->_fadeDur = dur; + _t->_prevPaletteBlends = 0; // rebase palette blending so it continues smoothly + } if (_t->_oldSegment) { - // freeze the old side at the current visual brightness: the copy picks up the final opacity (setters already - // applied it), which would otherwise make a still-running opacity fade jump to its final value right here + // set old side to the current transition brightness _t->_oldSegment->opacity = _t->_bri; _t->_start = millis(); _t->_dur = dur; DEBUGFX_PRINTF_P(PSTR("-- Restarted power transition: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); - if (!_t->_oldSegment->isActive()) { stopTransition(); return; } } else { // not enough RAM for segment copy: degrade to pure fade instead of dropping the transition _t->_start = millis(); - _t->_dur = 0; + _t->_dur = 0; // TODO: this means no transition. we need to set fade channel too } _t->_flags = TRANSITION_FLAG_POWER | (targetOn ? TRANSITION_FLAG_POWER_ON : 0); return; @@ -416,22 +474,22 @@ void Segment::startTransition(uint16_t dur, uint8_t kind, bool isPower, int8_t p // no previous transition running, start by allocating memory for segment copy _t = new(std::nothrow) Transition(dur); if (_t) { - _t->_bri = on ? opacity : 0; + if (on) _t->_bri = opacity; // if segment is on, start from current opacity instead of the default 0 for proper opacity fade + if (blendingStyle != TRANSITION_FADE && power) { + _t->_fadeDur = 0; // if not a fade power transition, do not fade anything but reveal the final state + } _t->_cct = cct; _t->_palette = palette; - // _t->_kind = kind; - _t->_flags = isPower ? TRANSITION_FLAG_POWER | (targetOn ? TRANSITION_FLAG_POWER_ON : 0) : 0; - // loadPalette(_t->_palT, palette); + _t->_flags = power ? TRANSITION_FLAG_POWER | (targetOn ? TRANSITION_FLAG_POWER_ON : 0) : 0; + loadPalette(_t->_palT, palette); // load target palette, will be blended in beginDraw() if FADE is used for (int i=0; i_colors[i] = colors[i]; - if (segmentCopy) _t->_oldSegment = new(std::nothrow) Segment(*this); // store/copy current segment settings + if (segmentCopy) _t->_oldSegment = new(std::nothrow) Segment(*this); // spatial transition, create copy of current segment (falls back to fade if this fails) if (_t->_oldSegment) { DEBUGFX_PRINTF_P(PSTR("-- Started transition: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); - if (!_t->_oldSegment->isActive()) stopTransition(); } else { - if (segmentCopy) _t->_dur = 0; // not enough RAM for segment copy: degrade spatial channel to pure fade instead of dropping the transition DEBUGFX_PRINTF_P(PSTR("-- Started transition without old segment: S=%p T(%p)\n"), this, _t); } - }; + } } void Segment::stopTransition() { @@ -468,10 +526,6 @@ uint8_t Segment::currentBri() const { unsigned prog = fadeProgress(); unsigned curBri = on ? opacity : 0; if (prog < 0xFFFFU) { - // this will blend opacity in new mode if style or kind is FADE (single effect call) - // or if the transition has no old segment (opacity or CCT transition) - //if (_t->_oldSegment == nullptr || blendingStyle == TRANSITION_FADE || _t->_kind == TRANSITION_KIND_FADE) curBri = (prog * curBri + _t->_bri * (0xFFFFU - prog)) / 0xFFFFU; - //else curBri = Segment::isPreviousMode() ? _t->_bri : curBri; curBri = (prog * curBri + _t->_bri * (0xFFFFU - prog)) / 0xFFFFU; } return curBri; @@ -485,26 +539,20 @@ void Segment::beginDraw(uint16_t prog) { setDrawDimensions(); // load colors into _currentColors for (unsigned i = 0; i < NUM_COLORS; i++) _currentColors[i] = colors[i]; + // load palette into _currentPalette + loadPalette(Segment::_currentPalette, palette); - // color blending always uses fade channel progress (never spatial progress) + // color&palette blending always use fade channel, in spatial transitions no fading is done (the old segment is static and the new segment is revealed over it) if (isInTransition() && prog < 0xFFFFU && (blendingStyle == TRANSITION_FADE || _t->_oldSegment == nullptr)) { - // Serial.println("Blending colors and palettes"); // blend colors for (unsigned i = 0; i < NUM_COLORS; i++) _currentColors[i] = color_blend16(_t->_colors[i], colors[i], prog); // blend palettes - // load target palette - CRGBPalette16 targetpalette; - loadPalette(targetpalette, palette); // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in _dur time) // minimum blend time is 100ms maximum is 65535ms unsigned noOfBlends = ((255U * prog) / 0xFFFFU) - _t->_prevPaletteBlends; if (noOfBlends > 255) noOfBlends = 255; // safety check - for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(Segment::_currentPalette, targetpalette, 48); - //Segment::_currentPalette = _t->_palT; // copy transitioning/temporary palette - } - else { - //Serial.println("Loading palette without blending"); - loadPalette(Segment::_currentPalette, palette); + for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, Segment::_currentPalette, 48); + Segment::_currentPalette = _t->_palT; // copy transitioning/temporary palette } } @@ -520,10 +568,10 @@ void Segment::handleRandomPalette() { Segment::_lastPaletteChange = now_s; Segment::_nextPaletteBlend = now; // starts blending immediately } - // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in strip.getTransitionDur() time) - // if randomPaletteChangeTime is shorter than strip.getTransitionDur() palette will never fully blend + // there are about 255 blend passes of 48 "blends" to completely blend two palettes (in strip.getTransition() time) + // if randomPaletteChangeTime is shorter than strip.getTransition() palette will never fully blend unsigned frameTime = strip.getFrameTime(); // in ms [8-1000] - unsigned transitionTime = strip.getTransitionDur(); // in ms [100-65535] + unsigned transitionTime = strip.getTransition(); // in ms [100-65535] if ((uint16_t)now < Segment::_nextPaletteBlend || now > ((Segment::_lastPaletteChange*1000) + transitionTime + 2*frameTime)) return; // not yet time or past transition time, no need to blend unsigned transitionFrames = frameTime > transitionTime ? 1 : transitionTime / frameTime; // i.e. 700ms/23ms = 30 or 20000ms/8ms = 2500 or 100ms/1000ms = 0 -> 1 unsigned noOfBlends = transitionFrames > 255 ? 1 : (255 + (transitionFrames>>1)) / transitionFrames; // we do some rounding here @@ -622,7 +670,7 @@ Segment &Segment::setColor(uint8_t slot, uint32_t c) { if (slot == 1 && c != BLACK) return *this; // on/off segment cannot have secondary color non black } //DEBUG_PRINTF_P(PSTR("- Starting color transition: %d [0x%X]\n"), slot, c); - startTransition(strip.getTransitionDur(), TRANSITION_KIND_COLOR); // start transition prior to change + startTransition(strip.getTransition(), TRANSITION_KIND_DEFAULT); // start transition prior to change colors[slot] = c; stateChanged = true; // send UDP/WS broadcast return *this; @@ -636,7 +684,7 @@ Segment &Segment::setCCT(uint16_t k) { } if (cct != k) { //DEBUG_PRINTF_P(PSTR("- Starting CCT transition: %d\n"), k); - startTransition(strip.getTransitionDur(), TRANSITION_KIND_FADE); // start transition prior to change (no need to copy segment) + startTransition(strip.getTransition(), TRANSITION_KIND_FADE); // start transition prior to change (no need to copy segment) cct = k; stateChanged = true; // send UDP/WS broadcast } @@ -645,8 +693,8 @@ 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.getTransitionDur(), TRANSITION_KIND_FADE); // opacity change always fades (no segment copy needed) + DEBUG_PRINTF_P(PSTR("- Starting opacity transition: %d\n"), o); + startTransition(strip.getTransition(), TRANSITION_KIND_FADE); // opacity change always fades (no segment copy needed) opacity = o; stateChanged = true; // send UDP/WS broadcast } @@ -657,7 +705,7 @@ Segment &Segment::setOption(uint8_t n, bool val) { bool prev = (options >> n) & 0x01; if (val == prev) return *this; //DEBUG_PRINTF_P(PSTR("- Starting option transition: %d\n"), n); - if (n == SEG_OPTION_ON) startTransition(strip.getTransitionDur(), TRANSITION_KIND_COLOR, true); // on/off transitions need a copy (spatial) and enable reversal + if (n == SEG_OPTION_ON) startTransition(strip.getTransition(), TRANSITION_KIND_DEFAULT | TRANSITION_POWER_TOGGLE); // on/off toggled, start transition if (val) options |= 0x01 << n; else options &= ~(0x01 << n); stateChanged = true; // send UDP/WS broadcast @@ -670,7 +718,7 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) { if (fx >= strip.getModeCount()) fx = 0; // set solid mode // if we have a valid mode & is not reserved if (fx != mode) { - startTransition(strip.getTransitionDur(), TRANSITION_KIND_EFFECT); // set effect transitions (must create segment copy) + startTransition(strip.getTransition(), TRANSITION_KIND_EFFECT); // set effect transitions (always needs a segment copy for blending) mode = fx; int sOpt; // load default values from effect string @@ -710,7 +758,7 @@ Segment &Segment::setPalette(uint8_t pal) { } if (pal != palette) { //DEBUG_PRINTF_P(PSTR("- Starting palette transition: %d\n"), pal); - startTransition(strip.getTransitionDur(), TRANSITION_KIND_COLOR); // start transition prior to change + startTransition(strip.getTransition(), TRANSITION_KIND_DEFAULT); // start transition prior to change palette = pal; stateChanged = true; // send UDP/WS broadcast } @@ -723,7 +771,7 @@ Segment &Segment::setName(const char *newName) { if (newLen) { if (name) p_free(name); // free old name name = static_cast(allocate_buffer(newLen+1, BFRALLOC_PREFER_PSRAM)); - if (mode == FX_MODE_2DSCROLLTEXT) startTransition(strip.getTransitionDur(), TRANSITION_KIND_EFFECT); // if the name changes in scrolling text mode, we need to copy the segment for blending + if (mode == FX_MODE_2DSCROLLTEXT) startTransition(strip.getTransition(), TRANSITION_KIND_EFFECT); // if the name changes in scrolling text mode, we need to copy the segment for blending if (name) strlcpy(name, newName, newLen+1); return *this; } @@ -1432,7 +1480,8 @@ void WS2812FX::service() { if (segO && segO->isActive() && (seg.mode != segO->mode || blendingStyle != TRANSITION_FADE || (segO->name != seg.name && segO->name && seg.name && strncmp(segO->name, seg.name, WLED_MAX_SEGNAME_LEN) != 0))) { Segment::modeBlend(true); // set flag for beginDraw() to blend colors and palette - segO->beginDraw(0xFFFFU); // old segment renders its captured state (no fade), parent segment holds transition progress + //segO->beginDraw(0xFFFFU); // old segment renders its captured state (no fade), parent segment holds transition progress + segO->beginDraw(prog); // set up palette & colors (also sets draw dimensions), parent segment has transition progress _currentSegment = segO; // set current segment // workaround for on/off transition to respect blending style _mode[segO->mode](); // run old mode (needed for bri workaround; semaphore!!) @@ -1530,7 +1579,7 @@ 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(); - const Segment *segO = topSegment.getOldSegment(); + const Segment *segO = topSegment.getOldSegment(); 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) @@ -1721,7 +1770,8 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { // we need to blend old segment using fade as pixels are not clipped c_a = color_blend16(c_a, segO->getPixelColorRaw(x + y*oCols), progInv); } else if (blendingStyle != TRANSITION_FADE) { - // on/off transitions: pixels not yet revealed by a wipe-to-off are black, pixels still covered by a wipe-to-on are black + // on/off transition workaround: pixels not yet revealed by a wipe-to-off are black, pixels still covered by a wipe-to-on are black + // TODO: test this in 2D if ((topSegment.isPowerOffTransition() && !clipped) || (topSegment.isPowerOnTransition() && clipped)) c_a = BLACK; } // map it into frame buffer @@ -1791,10 +1841,7 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { // we need to blend old segment using fade as pixels are not clipped c_a = color_blend16(c_a, segO->getPixelColorRaw(i), progInv); } else if (blendingStyle != TRANSITION_FADE) { - // blackout semantics for on/off transitions: pixels not yet revealed by a wipe-to-off are black, - // pixels still covered by a wipe-to-on are black (the wipe reveals from black) - // note: only blank pixels once the segment transition has actually started; the power change is applied - // before startTransition() is called (stateUpdated()) and a frame rendered in that window would blank the whole segment + // on/off transition workaround: pixels not yet revealed by a wipe-to-off are black, pixels still covered by a wipe-to-on are black if ((topSegment.isPowerOffTransition() && !clipped) || (topSegment.isPowerOnTransition() && clipped)) c_a = BLACK; } // map into frame buffer @@ -1896,12 +1943,13 @@ void WS2812FX::restartRuntime() { resume(); } -// start or stop transition for all segments -void WS2812FX::setTransitionMode(bool start, bool powerOn) { +// start global power on/off or stop transition for all segments +void WS2812FX::setTransitionMode(bool start) { suspend(); waitForIt(); for (Segment &seg : _segments) { - seg.startTransition(start ? _transitionDur : 0, TRANSITION_KIND_COLOR, true, start ? (powerOn ? 1 : 0) : -1); + if (start) seg.startTransition(_transitionDur, TRANSITION_KIND_DEFAULT | _poweringOnOff); // set color kind to let startTransition() determine if we need a segment copy or not + else seg.stopTransition(); } resume(); } diff --git a/wled00/led.cpp b/wled00/led.cpp index 41da5c119e..630c373eb2 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -41,17 +41,16 @@ void applyValuesToSelectedSegs() { void toggleOnOff() { - if (bri == 0) - { - briOld = 0; // in case a transition is active, this ensures we can detect a "zero crossing" in stateUpdated() to handle the transition reversal correctly + briOld = briT; // briT = 0 when off, briT = bri when on or in between while transitioning, store current value so brightness does not jump when toggling on/off during a transition + if (bri == 0) { bri = briLast; - strip.restartRuntime(); - } else - { + strip.setPowerFlag(TRANSITION_POWER_ON | TRANSITION_POWER_TRIGGER); + } else { briLast = bri; - bri = 0; // note: no need to set briOld here, stateUpdated() will handle it correctly + bri = 0; + strip.setPowerFlag(TRANSITION_POWER_OFF | TRANSITION_POWER_TRIGGER); } - stateChanged = true; + stateChanged = true; // note: if needed, stateUpdated() will start the global on/off transition } @@ -122,33 +121,56 @@ void stateUpdated(byte callMode) { // notify usermods of state change UsermodManager::onStateChange(callMode); - // global brightness transition handling - DEBUG_PRINTF_P(PSTR("*****************state update: briT: %d bri: %d briOld: %d\n"), (int)briT, (int)bri, (int)briOld); + // global brightness transition handling. Note: power flags are set in toggleOnOff() + DEBUG_PRINTF_P(PSTR("***********state update: briT: %d bri: %d briOld: %d, isPoweron: %d , isPoweroff %d, trigger: %d\n"), (int)briT, (int)bri, (int)briOld, (int)strip.isPoweringOn(), (int)strip.isPoweringOff(), (int)strip.isPowerTrigger()); - if (strip.getTransitionDur() == 0) { + if (strip.getTransition() == 0) { jsonTransitionOnce = false; transitionActive = false; - Serial.println("strip.getTransitionDur() == 0"); applyFinalBri(); } else { - // check if this is global on or off (bri crosses 0) and if so, force all segments to transition mode if not using FADE (starts a spatial segment transition like swipe etc.) - bool isOnOffTransition = (bri == 0 || (bri > 0 && briOld == 0)) && blendingStyle != TRANSITION_FADE; - if ((bri == 0 || (bri > 0 && briOld == 0)) && blendingStyle != TRANSITION_FADE) { - strip.setTransitionMode(true, bri > 0); // force all segments to transition mode, also handles transition inversion (on during off or off during on) + if (strip.isPoweringOff() && strip.isPoweringOn() && blendingStyle != TRANSITION_FADE) { + // if both flags are set, the power state was reversed during transition, invert the transition time to keep "overall brightness" i.e number of lit LEDs + // note: segments do the same, timing to finish the transition matches (more or less), segment blending is held in spatial transition until global transition finishes. + int progress = now - transitionStartTime; + int duration = strip.getTransition(); + transitionStartTime = now - (duration - progress); // invert transition progress + if (bri > 0) strip.clearPowerFlag(TRANSITION_POWER_OFF); + else strip.clearPowerFlag(TRANSITION_POWER_ON); } - if (transitionActive) { - // record current transition brightness - Serial.println("state: transition active, briT: " + String(briT) + " bri: " + String(bri) + " briOld: " + String(briOld)); - briOld = briT; // update old value to current transition value - //transitionStartTime = now; // restart transition + else if (strip.isPoweringOn() && strip.isPowerTrigger() || (bri > 0 && briOld == 0)) { + // global power on from off state either through power button or brightness change + strip.setPowerFlag(TRANSITION_POWER_ON | TRANSITION_POWER_TRIGGER); // if powering on by brightness change, set power flag to inite spatial transition (if set) + strip.setTransitionMode(false); // stop any transition that is going on while in off mode and start clean (a segment power on prior to global on will continue otherwise) + strip.restartRuntime(); // and restart any running effect when powering on + if (blendingStyle != TRANSITION_FADE) applyFinalBri();; // set brightness immediately, otherwise it will fade-in -> this does not yet work. need to set to bri old? or bri last? + } + //TODO: do we need to set briT = briOld when powering off? since we can now fade in parallel, just let it continue? + + if (strip.isPoweringOff() && bri > 0) { + // powering off but brightness was changed -> switch to powering on, update is handled below + strip.clearPowerFlag(TRANSITION_POWER_OFF); + strip.setPowerFlag(TRANSITION_POWER_ON | TRANSITION_POWER_TRIGGER); + Serial.println("state: brightness change during power off transition, toggling to on transition"); } - // if brightness changed, start a new global transition. note: fading is omitted if segments started a spatial transition (i.e. on/off with non FADE transition), see handleTransitions() - if (bri != briOld) { + // if brightness changed, start a new global transition but do not reset the timer if powering off (unless powering back on i.e. triggered) + // Note: fading is omitted if segments run a spatial power off transition, see handleTransitions() + if ((bri != briOld && !strip.isPoweringOff()) || strip.isPowerTrigger()) { + if (transitionActive) { + briOld = briT; // capture transition value: starts brightness fade from current value + } transitionActive = true; - transitionStartTime = now; + transitionStartTime = now; // note: this only affects brightness fade, spatial transition continues as it is handled on segment level + Serial.println("state: starting global transition, briT: " + String(briT) + " bri: " + String(bri) + " briOld: " + String(briOld)); + } + if (blendingStyle != TRANSITION_FADE && (strip.isPoweringOn() || strip.isPoweringOff()) && strip.isPowerTrigger()) { + Serial.println("state: global on/off transition detected, forcing all segments to transition mode"); + strip.setTransitionMode(true); // force all segments to a spatial on/off transition, segments handle transition inversion (on during off or off during on) } + strip.clearPowerFlag(TRANSITION_POWER_TRIGGER); } + stateChanged = false; } @@ -173,18 +195,19 @@ void updateInterfaces(uint8_t callMode) { #endif } - +// handle global transitions, for more details on transitions see Segment::startTransition() void handleTransitions() { //handle still pending interface update updateInterfaces(interfaceUpdateCallMode); - if (transitionActive && strip.getTransitionDur() > 0) { - int ti = millis() - transitionStartTime; - int tr = strip.getTransitionDur(); + // note: the !stateChanged is a workaround: bri is updated async, this code can run before stateUpdated() is called, causing a jump in the fade + if (transitionActive && strip.getTransition() > 0 && !stateChanged) { + //Serial.printf("GT: %d, bri: %d, briOld: %d\n", (int)briT, (int)bri, (int)briOld); + int progress = millis() - transitionStartTime; + int duration = strip.getTransition(); // finalize once the transition time has elapsed - if (ti/tr) { - //if (bri == 0) - strip.setTransitionMode(false); // stop all transitions TODO: this stops any transition after a global brightness transition, this is only needed for off transition!!! + if (progress >= duration) { + strip.clearPowerFlag(0xFF); // if transition ends, reset all global flags // restore (global) transition time if not called from UDP notifier or single/temporary transition from JSON (also playlist) if (jsonTransitionOnce) strip.setTransition(transitionDelay); transitionActive = false; @@ -192,15 +215,14 @@ void handleTransitions() { applyFinalBri(); return; } - byte briTO = briT; - 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; // TODO: this will snap brightness if a fade is going on. should this be omitted? - } else { + // fade global brightness from briOld to bri, skip if powering off using spatial transition (avoid fading in parallel) + // note: power on sets briOld = bri so it wont fade but still allows global brightness change during that transition + if (!strip.isPoweringOff() || blendingStyle == TRANSITION_FADE) { + byte briTO = briT; int deltaBri = (int)bri - (int)briOld; - briT = briOld + (deltaBri * min(ti, tr) / tr); // clamp: the ramp must not overshoot to avoid glitches (interrupted transitions or global on/off) + briT = briOld + (deltaBri * progress / duration); + if (briTO != briT) applyBri(); } - if (briTO != briT) applyBri(); } } @@ -253,7 +275,7 @@ void handleNightlight() { { for (unsigned i=0; i<4; i++) colPri[i] = colNlT[i]+ ((colSec[i] - colNlT[i])*nper); // fading from actual color to secondary color } - uint16_t transitionduration = strip.getTransitionDur(); + uint16_t transitionduration = strip.getTransition(); strip.setTransition(0); // temporary disable transition and set color & brightness directly, (hacky fix for #5620) colorUpdated(CALL_MODE_NO_NOTIFY); strip.setTransition(transitionduration); // restore transition time to previous value. Note: this needs proper fixing by disabling transitions completely in nightlight mode, reference implementation https://github.com/blazoncek/WLED/commit/c01a6b774969b652c30e383073958302042fd1f9 From 04362ce4289d85f55e3118918a0148904612da85 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 6 Sep 2026 12:56:47 +0200 Subject: [PATCH 08/19] bugfixes and defer to fading if spatial channel is used --- wled00/FX_fcn.cpp | 60 +++++++++++++++++++++++++++++------------------ wled00/led.cpp | 34 ++++++++++++++------------- 2 files changed, 55 insertions(+), 39 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 1aa4c694eb..3228eb6c4a 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -305,8 +305,9 @@ void Segment::handleTransition() { /* Note on how transitions work: There are three transition channels: global on strip level that handles global brightness fading and triggering of segment spatial transitions (see led.cpp) - on segment level there are two independent channels: a fade channel that handles colors, palette, CCT and opacity/brightness and a spatial channel - that handles swipe/push/etc. using a copy of the previous segment (aka oldSegment). FX transitions always need the oldSegment but can be spatial or fade. + on segment level there are two independent channels: a fade channel (_fadeProgress) that handles opacity/brightness (and colors & palette in FADE mode) + and a spatial channel (_progress) that handles FX blending and swipe/push/etc. using a copy of the previous segment (aka oldSegment). + FX transitions always need the oldSegment but can be spatial or fade and always use the spatial channel. There are many "special rules" that apply to handle transition updates i.e. calling startTransition() while a transition is already running. Here is a broad summary of the rules: - Off transition takes priority, in general no other transitions are allowed to simplify the logic, "offMode" is set once the global off finishes @@ -316,7 +317,8 @@ void Segment::handleTransition() { - When a spatial on/off transition is triggered during an ongoing on/off transition, it is reversed (i.e. same number of LEDs are lit but reversed) - Fade transitions continue from the current blend state if issued during a running transition - A spatial transition never restarts but fade transitions can run in parallel (unless powering off globally or a segment) - - If a spatial transition is running, only brightness fading is allowed in parallel. Any other change applies immediately. + - If a spatial transition is running it is never restarted; a subsequent change is deferred to the fade channel instead of applying immediately + - For more details, see the comments throughout the code In general the transition logic was chosen to avoid glitches or flashing while allowing segments to act as individual "lights" Segment opacity is faded in currentBri() @@ -355,17 +357,27 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { if (rebaseColors) for (unsigned i = 0; i < NUM_COLORS; i++) _t->_colors[i] = color_blend16(_t->_colors[i], colors[i], _t->_fadeProgress); _t->_bri = currentBri(); _t->_cct = currentCCT(); + _t->_prevPaletteBlends = 0; _t->_fadeDur = dur; _t->_fadeStart = fadeStart; - _t->_prevPaletteBlends = 0; + }; + // helper lambda: returns true while the fade channel is driving color/palette blending (mirrors the condition in beginDraw()); + // _fadeStart > _start marks a fade channel that was retargeted independently of the spatial channel (e.g. a change during a running spatial transition) + const auto isFadeBlending = [&]() { return _t->_fadeProgress < 0xFFFFU && (blendingStyle == TRANSITION_FADE || _t->_oldSegment == nullptr || _t->_fadeStart > _t->_start); }; + // helper lambda: capture current colors/palette as-is; needed when the fade channel was not blending them (spatial transition) so restarting it does not snap to stale values + const auto captureCurrent = [&]() { + for (unsigned i = 0; i < NUM_COLORS; i++) _t->_colors[i] = colors[i]; + loadPalette(_t->_palT, palette); }; if (isInTransition()) { - // re-targeting a running transition: fade restarts, starting from current blend, spatial continues but replaces the target (FX, color, palette) immediately + // re-targeting a running transition: fade restarts, starting from current blend; a running spatial transition continues to completion if (!power) { + Serial.printf("***re-targeting transition: dur=%d start=%d now=%d progress=%d fadeProgress=%d\n", _t->_dur, _t->_start, millis(), _t->_progress, _t->_fadeProgress); // opacity/CCT/color/palette/FX change: rebase fades to the current visual blend and restart it (no jump). A running spatial transition continues if (segmentCopy) { - // spatial change: if already in a spatial transition, do not restart, new segment is updated immediately and transition continues + // spatial change: upgrade a running fade transition to spatial; if a spatial transition is already running, do not restart it + // but smooth the change on the fade channel (see else branch below) if (_t->_oldSegment == nullptr) { // no old segment, meaning a fade transition is going on (color, palette, opacity, cct) _t->_oldSegment = new(std::nothrow) Segment(*this); // store/copy current segment settings @@ -378,7 +390,7 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { _t->_dur = dur; // _t->_oldSegment->_currentPalette = _t->_palT; // capture current palette (might be partially faded) TODO: this is not working, _currentPalette is static, investigate why DEBUGFX_PRINTF_P(PSTR("-- Updated transition with segment copy: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); - //if (!_t->_oldSegment->isActive()) { stopTransition(); return; } // todo: remove? is now handled above before creating a old segment + if (!_t->_oldSegment->isActive()) { stopTransition(); } // todo: this is to detect a failed oldsegment, we should not actually stop but degrade to fade } else { // not enough RAM for segment copy: degrade to pure fade instead of dropping the transition // captureBlend(false, millis()); TODO: use this? or not retarget the fade channel? @@ -386,21 +398,20 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { _t->_fadeDur = dur; _t->_prevPaletteBlends = 0; } + } else if (_t->_progress > 0) { + // spatial transition already running: defer color changes to fade channel instead (FX change will not defer but apply immediately) + if (!isFadeBlending()) captureCurrent(); // colors/palette were applied instantly: capture the current look as the fade start + captureBlend(true, millis()); // restart fade channel from the current visual state + // align fade time with ongoing spatial channel so they finish at the same time to be ready for the next spatial transition + _t->_fadeDur = (_t->_dur * _t->_progress) / 0xFFFFU; } - } else if (_t->_oldSegment != nullptr && kind == TRANSITION_KIND_DEFAULT) { - // color/palette change during a spatial transition: do not fade colors but set them immediately as we are not in a fade transition, do not restart transition - // TODO: actually would be better to fade instead of snap? any bad consequences from that? -> it can cause intermediate colors, when switching on/off but need to test - //captureBlend(true, 0); - //_t->_fadeDur = 0; // fade completed immediately - //_t->_palette = palette; - loadPalette(_t->_palT, palette); // load new palette into transition state (updates immediately) } - else { + else if (_t->_progress > 0) { + Serial.printf("***re-targeting fade channel transition, dur=%d start=%d now=%d progress=%d\n", _t->_dur, _t->_start, millis(), _t->_progress); // capture the current visual blend as the new "from" state so the incoming change does not cause a visible jump. - // _palT already holds the intermediate blended palette and will continue blending toward the new target (see beginDraw()). captureBlend(true, millis()); // capture current colors, bri & CCT, restart transition - //_t->_prevPaletteBlends = 0; } + Serial.printf("*returning from retargeting transition"); return; } Serial.println("***power transition"); @@ -416,11 +427,11 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { _t->_start = millis() - (((unsigned)(0xFFFFU - _t->_progress) * dur) / 0xFFFFU); //_t->_fadeDur = dur; //_t->_fadeStart = _t->_start; // sync fade channel, we need them to complete at the same time (if turning off, segment will not turn off until fade is complete, causing a flash) - _t->_fadeDur = 0; // fade completed immediately + _t->_fadeDur = 0; // fade completes immediately // TODO: when reversing a spatial transition, we should re-copy the current segment as it may have changed since the start //if (_t->_oldSegment) { delete _t->_oldSegment; _t->_oldSegment = nullptr; } //_t->_oldSegment = new(std::nothrow) Segment(*this); // create a fresh copy - } + } else captureBlend(true, millis()); // capture current fade status and restart fade when toggling if (power == TRANSITION_POWER_TOGGLE) { // segment-level on/off Serial.printf("***power toggle: reversing spatial timeline: dur=%d start=%d now=%d progress=%d\n", _t->_dur, _t->_start, millis(), _t->_progress); @@ -430,7 +441,6 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { _t->_oldSegment->opacity = opacity; _t->_oldSegment->cct = cct; } - else captureBlend(true, millis()); // capture current fade status when reversing } if (_t->_oldSegment == nullptr && segmentCopy) { _t->_oldSegment = new(std::nothrow) Segment(*this); // create a copy if there is none @@ -461,6 +471,7 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { _t->_oldSegment->opacity = _t->_bri; _t->_start = millis(); _t->_dur = dur; + if (!_t->_oldSegment->isActive()) stopTransition(); // old segment failed to allocate pixel buffer, stop transition DEBUGFX_PRINTF_P(PSTR("-- Restarted power transition: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); } else { // not enough RAM for segment copy: degrade to pure fade instead of dropping the transition @@ -476,7 +487,7 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { if (_t) { if (on) _t->_bri = opacity; // if segment is on, start from current opacity instead of the default 0 for proper opacity fade if (blendingStyle != TRANSITION_FADE && power) { - _t->_fadeDur = 0; // if not a fade power transition, do not fade anything but reveal the final state + _t->_fadeDur = 0; // if non-fade power transition, do not fade anything but reveal the final state } _t->_cct = cct; _t->_palette = palette; @@ -485,8 +496,10 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { for (int i=0; i_colors[i] = colors[i]; if (segmentCopy) _t->_oldSegment = new(std::nothrow) Segment(*this); // spatial transition, create copy of current segment (falls back to fade if this fails) if (_t->_oldSegment) { + if (!_t->_oldSegment->isActive()) stopTransition(); // old segment failed to allocate pixel buffer, stop transition DEBUGFX_PRINTF_P(PSTR("-- Started transition: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); } else { + _t->_start = 0; // disables the spatial channel and uses fade instead DEBUGFX_PRINTF_P(PSTR("-- Started transition without old segment: S=%p T(%p)\n"), this, _t); } } @@ -542,8 +555,9 @@ void Segment::beginDraw(uint16_t prog) { // load palette into _currentPalette loadPalette(Segment::_currentPalette, palette); - // color&palette blending always use fade channel, in spatial transitions no fading is done (the old segment is static and the new segment is revealed over it) - if (isInTransition() && prog < 0xFFFFU && (blendingStyle == TRANSITION_FADE || _t->_oldSegment == nullptr)) { + // color&palette blending can use fade channel: in spatial transitions (swpie etc.) _t->_fadeStart > _t->_start if false exept + // except if a change arrived during a running spatial transition: in that case colors&palette of new segment fade as the spatial transition continues + if (isInTransition() && prog < 0xFFFFU && _t->_fadeStart > _t->_start) {// && (blendingStyle == TRANSITION_FADE || _t->_oldSegment == nullptr || _t->_fadeStart > _t->_start)) { // blend colors for (unsigned i = 0; i < NUM_COLORS; i++) _currentColors[i] = color_blend16(_t->_colors[i], colors[i], prog); // blend palettes diff --git a/wled00/led.cpp b/wled00/led.cpp index 630c373eb2..44a7c0c32d 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -129,23 +129,25 @@ void stateUpdated(byte callMode) { transitionActive = false; applyFinalBri(); } else { - if (strip.isPoweringOff() && strip.isPoweringOn() && blendingStyle != TRANSITION_FADE) { - // if both flags are set, the power state was reversed during transition, invert the transition time to keep "overall brightness" i.e number of lit LEDs - // note: segments do the same, timing to finish the transition matches (more or less), segment blending is held in spatial transition until global transition finishes. - int progress = now - transitionStartTime; - int duration = strip.getTransition(); - transitionStartTime = now - (duration - progress); // invert transition progress - if (bri > 0) strip.clearPowerFlag(TRANSITION_POWER_OFF); - else strip.clearPowerFlag(TRANSITION_POWER_ON); - } - else if (strip.isPoweringOn() && strip.isPowerTrigger() || (bri > 0 && briOld == 0)) { - // global power on from off state either through power button or brightness change - strip.setPowerFlag(TRANSITION_POWER_ON | TRANSITION_POWER_TRIGGER); // if powering on by brightness change, set power flag to inite spatial transition (if set) - strip.setTransitionMode(false); // stop any transition that is going on while in off mode and start clean (a segment power on prior to global on will continue otherwise) - strip.restartRuntime(); // and restart any running effect when powering on - if (blendingStyle != TRANSITION_FADE) applyFinalBri();; // set brightness immediately, otherwise it will fade-in -> this does not yet work. need to set to bri old? or bri last? + if (blendingStyle != TRANSITION_FADE) { + if (strip.isPoweringOff() && strip.isPoweringOn()) { + // if both flags are set, the power state was reversed during transition, invert the transition time to keep "overall brightness" i.e number of lit LEDs + // note: segments do the same, timing to finish the transition matches (more or less), segment blending is held in spatial transition until global transition finishes. + int progress = now - transitionStartTime; + int duration = strip.getTransition(); + transitionStartTime = now - (duration - progress); // invert transition progress + if (bri > 0) strip.clearPowerFlag(TRANSITION_POWER_OFF); + else strip.clearPowerFlag(TRANSITION_POWER_ON); + } + else if (strip.isPoweringOn() && strip.isPowerTrigger() || (bri > 0 && briOld == 0)) { + // global power on from off state either through power button or brightness change + strip.setPowerFlag(TRANSITION_POWER_ON | TRANSITION_POWER_TRIGGER); // if powering on by brightness change, set power flag to inite spatial transition (if set) + strip.setTransitionMode(false); // stop any transition that is going on while in off mode and start clean (a segment power on prior to global on will continue otherwise) + strip.restartRuntime(); // and restart any running effect when powering on + if (blendingStyle != TRANSITION_FADE) applyFinalBri();; // set brightness immediately, otherwise it will fade-in -> this does not yet work. need to set to bri old? or bri last? + } } - //TODO: do we need to set briT = briOld when powering off? since we can now fade in parallel, just let it continue? + //TODO: do we need to set briT = briOld when powering off? since we can now fade in parallel, just let it continue? if (strip.isPoweringOff() && bri > 0) { // powering off but brightness was changed -> switch to powering on, update is handled below From aa1a535ad943fee91663a45935528062115f07be Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 7 Sep 2026 10:36:54 +0200 Subject: [PATCH 09/19] simplify logic somewhat, cleanup --- wled00/FX.h | 3 +- wled00/FX_fcn.cpp | 194 ++++++++++++++++++---------------------------- wled00/led.cpp | 94 +++++++++++----------- 3 files changed, 125 insertions(+), 166 deletions(-) diff --git a/wled00/FX.h b/wled00/FX.h index 11d52e6073..5b90107136 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -660,8 +660,7 @@ class Segment { inline uint16_t fadeProgress() const { return isInTransition() ? _t->_fadeProgress : 0xFFFFU; } // fade channel progress, relies on handleTransition()/updateTransitionProgress() inline unsigned long getTransitionStart() const { return isInTransition() ? _t->_start : 0; } // spatial channel start time inline Segment *getOldSegment() const { return isInTransition() ? _t->_oldSegment : nullptr; } - // power transition helpers (true while a spatial on/off transition is running; a power-off transition keeps its - // segment copy until the whole transition - incl. the fade channel - ends, see handleTransition()) + inline bool fadeTransitionActive() const { return isInTransition() && _t->_fadeStart > _t->_start; } // true if fading during a spatial transition inline bool isPowerTransition() const { return isInTransition() && (_t->_flags & TRANSITION_FLAG_POWER) && _t->_oldSegment != nullptr; } inline bool isPowerOffTransition() const { return isPowerTransition() && !(_t->_flags & TRANSITION_FLAG_POWER_ON); } // spatial to off inline bool isPowerOnTransition() const { return isPowerTransition() && (_t->_flags & TRANSITION_FLAG_POWER_ON); } // spatial to on diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 3228eb6c4a..1a0ccc7a87 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -290,8 +290,7 @@ void Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { void Segment::handleTransition() { updateTransitionProgress(); if (isInTransition() && !strip.isPoweringOff()) { - // end transitions if completed but wait for a power-off transition to complete to avoid revealing pixels (see blendSegment() blanking) - // pixel blanking in blendSegment() rely on is PowerOffTransition() (which requires the copy) TODO: should we use the _t->_flags instead of isPoweringOff()? + // end transitions if completed but wait for a global power-off transition to complete to avoid revealing pixels (see blendSegment() blanking) if (_t->_oldSegment && _t->_progress == 0xFFFFU && !strip.isPoweringOff()) { Serial.printf("handleTransition: deleting old segment copy, progress=%d\n", _t->_progress); delete _t->_oldSegment; _t->_oldSegment = nullptr; @@ -305,30 +304,28 @@ void Segment::handleTransition() { /* Note on how transitions work: There are three transition channels: global on strip level that handles global brightness fading and triggering of segment spatial transitions (see led.cpp) - on segment level there are two independent channels: a fade channel (_fadeProgress) that handles opacity/brightness (and colors & palette in FADE mode) + on segment level there are two independent channels: a fade channel (_fadeProgress) that handles opacity/brightness & CCT (and colors/palette if FADE or as a fallback) and a spatial channel (_progress) that handles FX blending and swipe/push/etc. using a copy of the previous segment (aka oldSegment). FX transitions always need the oldSegment but can be spatial or fade and always use the spatial channel. There are many "special rules" that apply to handle transition updates i.e. calling startTransition() while a transition is already running. - Here is a broad summary of the rules: + In general the transition logic was chosen to avoid glitches or flashing while allowing segments to act as individual "lights". + Here is a short summary of the rules: + - Segment opacity or global brightness always uses fade, they can run in parallel to any other transition (and even parallel to each other) - Off transition takes priority, in general no other transitions are allowed to simplify the logic, "offMode" is set once the global off finishes - since this would require careful sync to the segment transition (for example using swipe) the segment is held in transition until global finishes (see handleTransition()) + since this would require careful sync to spatial transition, the segment is held in transition until global finishes (see handleTransition() & blendSegment()) - On strip level, there are flags to check for global on/off transitions which are set in toggleOnOff() - A global transition is started in stateUpdated() and triggers segment transitions if needed for spatial transitions - - When a spatial on/off transition is triggered during an ongoing on/off transition, it is reversed (i.e. same number of LEDs are lit but reversed) + - When a spatial on/off transition is triggered during an ongoing on/off transition, it is reversed (i.e. same number of LEDs are lit but flip position) - Fade transitions continue from the current blend state if issued during a running transition - - A spatial transition never restarts but fade transitions can run in parallel (unless powering off globally or a segment) - - If a spatial transition is running it is never restarted; a subsequent change is deferred to the fade channel instead of applying immediately - + - If a spatial transition is running it is never restarted. A subsequent change is deferred to the fade channel instead - For more details, see the comments throughout the code - In general the transition logic was chosen to avoid glitches or flashing while allowing segments to act as individual "lights" - Segment opacity is faded in currentBri() */ -// starting a transition has to occur before change so we get current values 1st -// note: _t is the temporary segment that holds the values transitioned from (palette, colors, brightness,...) and the current segment holds the "to" values +// startTransition() is called before changing a sement parameter, it captures the current state into _t and/or _t->_oldSegment and starts/updates transition timers. +// note: _t has the temporary "from" segment value(s) and the current segment holds the "to" values which are set after the transition starts. // the transition has two independent channels: // the fade channel (_fadeStart/_fadeDur/_fadeProgress) crossfades colors, palette, CCT and opacity and never needs a segment copy -// the spatial channel (_start/_dur/_progress and _oldSegment) renders wipe/push/etc. using a copy of the previous segment +// the spatial channel (_start/_dur/_progress and _oldSegment) renders wipe/push/etc. using a copy of the current state (oldSegment) // kind: low nibble = TRANSITION_KIND_x identifying which change triggered the transition (determines whether a segment copy is needed) // high nibble = TRANSITION_POWER_x flags: POWER_ON/POWER_OFF = global on/off, POWER_TOGGLE = segment on/off (both flags are set) @@ -337,37 +334,37 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { kind &= TRANSITION_KIND_MASK; // strip the power flags const bool targetOn = power == TRANSITION_POWER_TOGGLE ? !on : power == TRANSITION_POWER_ON; // target on-state for power transitions Serial.printf("startTransition: dur=%d kind=%d power=%d targetOn=%d\n", dur, kind, power, targetOn); - // do not interrupt a global power off transition (unless its a global reversal) otherwise we can turn segment on and it will jump to off once global completes TODO: this is broken again - // TODO: the issue is now this: if the next line is used, a segment on during global off will just switch it on as that segment is not in a transition. - // if the line is omitted, it will start an on transition but then switch fully off once global off completes. this may be a good compromise as a proper solution may need many more conditionals - //if (strip.isPoweringOff() && (power != TRANSITION_POWER_OFF && power != TRANSITION_POWER_ON)) return; - - // check if we even need to start a transition. No transitions if transitions disabled, not an active segment or not in an on state (unless this is a power-on request) - // TODO: the simpler version starts a transition on off segments but they will not render as both old and new segment are off. when switching to on, it stops transitions so still works - // the only downside is that during a global off, it also fades, then a segment on starts rendering the new segment because it reverses. - //if (dur == 0 || !isActive()) || ((power != TRANSITION_POWER_TOGGLE) && !on)) { - if (dur == 0 || !isActive()) { + + // check if we even need to start a transition: abort if transitions disabled, not an active segment or not in an on state (unless this is a power-on request) + if (dur == 0 || !isActive() || ((power != TRANSITION_POWER_TOGGLE) && !on)) { return; } // check if we need a copy of current segment: only effect transitions and transitions using a spatial (non-FADE) style const bool segmentCopy = kind == TRANSITION_KIND_EFFECT || (kind != TRANSITION_KIND_FADE && blendingStyle != TRANSITION_FADE); Serial.printf("*****startTransition: dur=%d kind=%d power=%d targetOn=%d segmentCopy=%d seg is on=%d\n", dur, kind, power, targetOn, segmentCopy, on); // helper lambda function to capture current _bri/_cct and optionally _colors to the segments transitions (_t) state TDODO: needs refinement - const auto captureBlend = [&](bool rebaseColors, unsigned long fadeStart) { - if (rebaseColors) for (unsigned i = 0; i < NUM_COLORS; i++) _t->_colors[i] = color_blend16(_t->_colors[i], colors[i], _t->_fadeProgress); + const auto captureBlend = [&](unsigned long fadeStart) { + for (unsigned i = 0; i < NUM_COLORS; i++) _t->_colors[i] = color_blend16(_t->_colors[i], colors[i], _t->_fadeProgress); _t->_bri = currentBri(); _t->_cct = currentCCT(); _t->_prevPaletteBlends = 0; _t->_fadeDur = dur; _t->_fadeStart = fadeStart; }; - // helper lambda: returns true while the fade channel is driving color/palette blending (mirrors the condition in beginDraw()); - // _fadeStart > _start marks a fade channel that was retargeted independently of the spatial channel (e.g. a change during a running spatial transition) - const auto isFadeBlending = [&]() { return _t->_fadeProgress < 0xFFFFU && (blendingStyle == TRANSITION_FADE || _t->_oldSegment == nullptr || _t->_fadeStart > _t->_start); }; - // helper lambda: capture current colors/palette as-is; needed when the fade channel was not blending them (spatial transition) so restarting it does not snap to stale values - const auto captureCurrent = [&]() { - for (unsigned i = 0; i < NUM_COLORS; i++) _t->_colors[i] = colors[i]; - loadPalette(_t->_palT, palette); + // isFadeBlending returns true while the fade channel is driving color/palette blending +// const auto isFadeBlending = [&]() { return _t->_fadeProgress < 0xFFFFU && (blendingStyle == TRANSITION_FADE || _t->_oldSegment == nullptr || fadeTransitionActive()); }; + + // create a copy of the current segment to be used for spatial transitions (FX, palette, color, opacity, CCT) + const auto createOldSegment = [&](uint16_t colorProgress) { + if (_t->_oldSegment) { delete _t->_oldSegment; _t->_oldSegment = nullptr; } + _t->_oldSegment = new(std::nothrow) Segment(*this); // store/copy current segment settings + if (_t->_oldSegment) { + for (unsigned i = 0; i < NUM_COLORS; i++) _t->_oldSegment->colors[i] = color_blend16(_t->_colors[i], colors[i], colorProgress); + _t->_oldSegment->opacity = currentBri(); // capture current opacity in case it was being faded + _t->_oldSegment->cct = currentCCT(); // capture current CCT in case it was being faded + if (!_t->_oldSegment->isActive()) { delete _t->_oldSegment; _t->_oldSegment = nullptr; } // pixel buffer allocation failed, use fallback + } + return _t->_oldSegment != nullptr; }; if (isInTransition()) { @@ -375,48 +372,34 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { if (!power) { Serial.printf("***re-targeting transition: dur=%d start=%d now=%d progress=%d fadeProgress=%d\n", _t->_dur, _t->_start, millis(), _t->_progress, _t->_fadeProgress); // opacity/CCT/color/palette/FX change: rebase fades to the current visual blend and restart it (no jump). A running spatial transition continues - if (segmentCopy) { - // spatial change: upgrade a running fade transition to spatial; if a spatial transition is already running, do not restart it - // but smooth the change on the fade channel (see else branch below) - if (_t->_oldSegment == nullptr) { - // no old segment, meaning a fade transition is going on (color, palette, opacity, cct) - _t->_oldSegment = new(std::nothrow) Segment(*this); // store/copy current segment settings - if (_t->_oldSegment) { - // capture current state (colors, opacity, CCT) into the old segment (old segment does not fade, it is static) - for (unsigned i = 0; i < NUM_COLORS; i++) _t->_oldSegment->colors[i] = color_blend16(_t->_colors[i], colors[i], _t->_progress); - _t->_oldSegment->opacity = currentBri(); // captrue current opacity in case it was being faded - _t->_oldSegment->cct = currentCCT(); // capture current CCT in case it was being faded - _t->_start = millis(); // (re)start transition (but do not restart fade channel so fading continues on current segment) - _t->_dur = dur; - // _t->_oldSegment->_currentPalette = _t->_palT; // capture current palette (might be partially faded) TODO: this is not working, _currentPalette is static, investigate why - DEBUGFX_PRINTF_P(PSTR("-- Updated transition with segment copy: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); - if (!_t->_oldSegment->isActive()) { stopTransition(); } // todo: this is to detect a failed oldsegment, we should not actually stop but degrade to fade - } else { - // not enough RAM for segment copy: degrade to pure fade instead of dropping the transition - // captureBlend(false, millis()); TODO: use this? or not retarget the fade channel? - _t->_fadeStart = millis(); - _t->_fadeDur = dur; - _t->_prevPaletteBlends = 0; - } - } else if (_t->_progress > 0) { - // spatial transition already running: defer color changes to fade channel instead (FX change will not defer but apply immediately) - if (!isFadeBlending()) captureCurrent(); // colors/palette were applied instantly: capture the current look as the fade start - captureBlend(true, millis()); // restart fade channel from the current visual state - // align fade time with ongoing spatial channel so they finish at the same time to be ready for the next spatial transition - _t->_fadeDur = (_t->_dur * _t->_progress) / 0xFFFFU; + if (segmentCopy && _t->_oldSegment == nullptr) { + // no old segment means a fade transition is going on (color, palette, opacity, cct), capture current state into the old segment + if (createOldSegment(_t->_progress)) { + _t->_start = millis(); // start spatial transition (fading continues on current segment) + _t->_dur = dur; + DEBUGFX_PRINTF_P(PSTR("-- Updated transition with segment copy: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); + } else { + // not enough RAM for segment copy: degrade to pure fade instead of dropping the transition + captureBlend(millis()); // rebase fade channel to the current visual blend and restart it } } else if (_t->_progress > 0) { - Serial.printf("***re-targeting fade channel transition, dur=%d start=%d now=%d progress=%d\n", _t->_dur, _t->_start, millis(), _t->_progress); - // capture the current visual blend as the new "from" state so the incoming change does not cause a visible jump. - captureBlend(true, millis()); // capture current colors, bri & CCT, restart transition + // todo: isfadeblending is only used here, maybe remove it and make it explicit? + //if (!isFadeBlending() && _t->_oldSegment != nullptr) { + if (!fadeTransitionActive() && _t->_oldSegment != nullptr) { + // spatial transition with no fade running: enable fade and let the spatial transition continue. Need to capture the current "revealed" state i.e. copy segment colors to _t + for (unsigned i = 0; i < NUM_COLORS; i++) _t->_colors[i] = colors[i]; // rebase transition colors&palette from current final state + loadPalette(_t->_palT, palette); + } + captureBlend(millis()); // restart fade channel from the current visual state + if (segmentCopy) _t->_fadeDur = (_t->_dur * _t->_progress) / 0xFFFFU; // if this is a deferred spatial request align fade time with ongoing spatial channel } Serial.printf("*returning from retargeting transition"); return; } - Serial.println("***power transition"); - // power (on/off) transition (per segment or global) during an ongoing power transition + // power transition (on or off) request if (_t->_flags & TRANSITION_FLAG_POWER) { + // power transition request (per segment or global) during an ongoing power transition Serial.printf("***power transition: already in a power transition, dur=%d start=%d now=%d progress=%d\n", _t->_dur, _t->_start, millis(), _t->_progress); if (targetOn == ((_t->_flags & TRANSITION_FLAG_POWER_ON) != 0)) return; // same target re-issued, let the running transition finish @@ -425,13 +408,9 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { // already in a power transition reverse in place: invert the spatial timeline (20%-completed swipe continues from 80%) _t->_dur = dur; _t->_start = millis() - (((unsigned)(0xFFFFU - _t->_progress) * dur) / 0xFFFFU); - //_t->_fadeDur = dur; - //_t->_fadeStart = _t->_start; // sync fade channel, we need them to complete at the same time (if turning off, segment will not turn off until fade is complete, causing a flash) - _t->_fadeDur = 0; // fade completes immediately - // TODO: when reversing a spatial transition, we should re-copy the current segment as it may have changed since the start - //if (_t->_oldSegment) { delete _t->_oldSegment; _t->_oldSegment = nullptr; } - //_t->_oldSegment = new(std::nothrow) Segment(*this); // create a fresh copy - } else captureBlend(true, millis()); // capture current fade status and restart fade when toggling + _t->_fadeDur = 0; // disable fading (any ongoing fade completes immediately) + createOldSegment(0xFFFFU); // create a fresh copy from the final state which is currently displayed + } else captureBlend(millis()); // capture current fade status and restart fade when toggling if (power == TRANSITION_POWER_TOGGLE) { // segment-level on/off Serial.printf("***power toggle: reversing spatial timeline: dur=%d start=%d now=%d progress=%d\n", _t->_dur, _t->_start, millis(), _t->_progress); @@ -442,47 +421,31 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { _t->_oldSegment->cct = cct; } } - if (_t->_oldSegment == nullptr && segmentCopy) { - _t->_oldSegment = new(std::nothrow) Segment(*this); // create a copy if there is none - for (unsigned i = 0; i < NUM_COLORS; i++) _t->_oldSegment->colors[i] = color_blend16(_t->_colors[i], colors[i], _t->_progress); - _t->_oldSegment->opacity = currentBri(); // captrue current opacity in case it was being faded - _t->_oldSegment->cct = currentCCT(); // capture current CCT in case it was being faded - } - // global on/off: the fade channel (colors/opacity/CCT) is independent of the power state, leave it running - //_t->_prevPaletteBlends = 0; _t->_flags ^= TRANSITION_FLAG_POWER_ON; // flip POWER_ON flag - return; - } - Serial.printf("***power transition: starting new power transition, dur=%d start=%d now=%d progress=%d\n", _t->_dur, _t->_start, millis(), _t->_progress); - // global or segment on/off: stop ongoing segment transition immediately and start the power transition from scratch - if (_t->_oldSegment) { delete _t->_oldSegment; _t->_oldSegment = nullptr; } - // rebase fade channel to the current visual blend before starting the new power transition - for (unsigned i = 0; i < NUM_COLORS; i++) _t->_colors[i] = color_blend16(_t->_colors[i], colors[i], _t->_fadeProgress); - _t->_bri = currentBri(); - _t->_cct = currentCCT(); - if (segmentCopy) _t->_oldSegment = new(std::nothrow) Segment(*this); // spatial transition, need a fresh copy - else { - _t->_fadeStart = millis(); // fade transition, restart fading - _t->_fadeDur = dur; - _t->_prevPaletteBlends = 0; // rebase palette blending so it continues smoothly - } - if (_t->_oldSegment) { - // set old side to the current transition brightness - _t->_oldSegment->opacity = _t->_bri; - _t->_start = millis(); - _t->_dur = dur; - if (!_t->_oldSegment->isActive()) stopTransition(); // old segment failed to allocate pixel buffer, stop transition - DEBUGFX_PRINTF_P(PSTR("-- Restarted power transition: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); } else { - // not enough RAM for segment copy: degrade to pure fade instead of dropping the transition - _t->_start = millis(); - _t->_dur = 0; // TODO: this means no transition. we need to set fade channel too + Serial.printf("***power transition: starting new power transition, dur=%d start=%d now=%d progress=%d\n", _t->_dur, _t->_start, millis(), _t->_progress); + // global or segment on/off initiated: stop ongoing segment transition immediately, we do need the spatial channel and want to start a new transition + if (_t->_oldSegment) { delete _t->_oldSegment; _t->_oldSegment = nullptr; } + captureBlend(millis()); // rebase transition values to current visual blend before starting the new power transition + if (segmentCopy) { + if (createOldSegment(0xFFFFU)) { // spatial transition, need a fresh copy (colors as-is, old side captures the current transition brightness) + _t->_start = millis(); + _t->_dur = dur; + _t->_fadeDur = 0; // non-fade power transition, do not fade anything but reveal the final state (same as a fresh power start) + DEBUGFX_PRINTF_P(PSTR("-- Restarted power transition: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); + } else { + // not enough RAM for segment copy: degrade to pure fade (restarted above) instead of dropping the transition + _t->_start = 0; // disables the spatial channel and uses fade instead + } + } else { + // FADE blending: the fade channel (restarted above) carries the power transition + _t->_start = 0; // FADE blending: disables the spatial channel and uses fade instead + } + _t->_flags = TRANSITION_FLAG_POWER | (targetOn ? TRANSITION_FLAG_POWER_ON : 0); } - _t->_flags = TRANSITION_FLAG_POWER | (targetOn ? TRANSITION_FLAG_POWER_ON : 0); return; } - - // no previous transition running, start by allocating memory for segment copy + // no previous transition running, start by allocating memory for transition values _t = new(std::nothrow) Transition(dur); if (_t) { if (on) _t->_bri = opacity; // if segment is on, start from current opacity instead of the default 0 for proper opacity fade @@ -494,12 +457,11 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { _t->_flags = power ? TRANSITION_FLAG_POWER | (targetOn ? TRANSITION_FLAG_POWER_ON : 0) : 0; loadPalette(_t->_palT, palette); // load target palette, will be blended in beginDraw() if FADE is used for (int i=0; i_colors[i] = colors[i]; - if (segmentCopy) _t->_oldSegment = new(std::nothrow) Segment(*this); // spatial transition, create copy of current segment (falls back to fade if this fails) + if (segmentCopy) createOldSegment(0xFFFFU); // spatial transition, create copy of current segment (falls back to fade if this fails) if (_t->_oldSegment) { - if (!_t->_oldSegment->isActive()) stopTransition(); // old segment failed to allocate pixel buffer, stop transition DEBUGFX_PRINTF_P(PSTR("-- Started transition: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); } else { - _t->_start = 0; // disables the spatial channel and uses fade instead + _t->_start = 0; // disables the spatial channel and use fade i.e. enable fadeTransitionActive() DEBUGFX_PRINTF_P(PSTR("-- Started transition without old segment: S=%p T(%p)\n"), this, _t); } } @@ -555,9 +517,8 @@ void Segment::beginDraw(uint16_t prog) { // load palette into _currentPalette loadPalette(Segment::_currentPalette, palette); - // color&palette blending can use fade channel: in spatial transitions (swpie etc.) _t->_fadeStart > _t->_start if false exept - // except if a change arrived during a running spatial transition: in that case colors&palette of new segment fade as the spatial transition continues - if (isInTransition() && prog < 0xFFFFU && _t->_fadeStart > _t->_start) {// && (blendingStyle == TRANSITION_FADE || _t->_oldSegment == nullptr || _t->_fadeStart > _t->_start)) { + // color&palette fade blending: if using FADE or if changed during an ongoing spatial (swipe etc.) transition i.e. fadeTransitionActive() + if (isInTransition() && prog < 0xFFFFU && fadeTransitionActive()) { // blend colors for (unsigned i = 0; i < NUM_COLORS; i++) _currentColors[i] = color_blend16(_t->_colors[i], colors[i], prog); // blend palettes @@ -1785,7 +1746,6 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { c_a = color_blend16(c_a, segO->getPixelColorRaw(x + y*oCols), progInv); } else if (blendingStyle != TRANSITION_FADE) { // on/off transition workaround: pixels not yet revealed by a wipe-to-off are black, pixels still covered by a wipe-to-on are black - // TODO: test this in 2D if ((topSegment.isPowerOffTransition() && !clipped) || (topSegment.isPowerOnTransition() && clipped)) c_a = BLACK; } // map it into frame buffer diff --git a/wled00/led.cpp b/wled00/led.cpp index 44a7c0c32d..ad98073ccb 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -66,10 +66,7 @@ byte scaledBri(byte in) //applies global temporary brightness (briT) to strip void applyBri() { if (realtimeOverride || !(realtimeMode && arlsForceMaxBri)) - { - DEBUG_PRINTF_P(PSTR("Applying strip brightness: %d (%d,%d)\n"), (int)briT, (int)bri, (int)briOld); strip.setBrightness(briT); - } } @@ -81,54 +78,15 @@ void applyFinalBri() { strip.trigger(); // force one last update } -//called after every state changes, schedules interface updates, handles brightness transition and nightlight activation -//unlike colorUpdated(), does NOT apply any colors or FX to segments -void stateUpdated(byte callMode) { - //call for notifier -> 0: init 1: direct change 2: button 3: notification 4: nightlight 5: other (No notification) - // 6: fx changed 7: hue 8: preset cycle 9: blynk 10: alexa 11: ws send only 12: button preset - setValuesFromFirstSelectedSeg(); // a much better approach would be to use main segment: setValuesFromMainSeg() - - if (bri != briOld || stateChanged) { - if (stateChanged) currentPreset = 0; //something changed, so we are no longer in the preset - - if (callMode != CALL_MODE_NOTIFICATION && callMode != CALL_MODE_NO_NOTIFY) notify(callMode); - if (bri != briOld && nodeBroadcastEnabled) sendSysInfoUDP(); // update on state - - //set flag to update ws and mqtt - interfaceUpdateCallMode = callMode; - } else { - if (nightlightActive && !nightlightActiveOld && callMode != CALL_MODE_NOTIFICATION && callMode != CALL_MODE_NO_NOTIFY) { - notify(CALL_MODE_NIGHTLIGHT); - interfaceUpdateCallMode = CALL_MODE_NIGHTLIGHT; - } - } - - unsigned long now = millis(); - if (callMode != CALL_MODE_NO_NOTIFY && nightlightActive && (nightlightMode == NL_MODE_FADE || nightlightMode == NL_MODE_COLORFADE)) { - briNlT = bri; - nightlightDelayMs -= (now - nightlightStartTime); - nightlightStartTime = now; - } - if (briT == 0) { - if (callMode != CALL_MODE_NOTIFICATION) strip.resetTimebase(); //effect start from beginning - } - - if (bri > 0) briLast = bri; - - //deactivate nightlight if target brightness is reached - if (bri == nightlightTargetBri && callMode != CALL_MODE_NO_NOTIFY && nightlightMode != NL_MODE_SUN) nightlightActive = false; - - // notify usermods of state change - UsermodManager::onStateChange(callMode); - - // global brightness transition handling. Note: power flags are set in toggleOnOff() +// local function to handle global brightness transition, called from stateUpdated(). Note: power flags are set in toggleOnOff() +void handleBriChange() { DEBUG_PRINTF_P(PSTR("***********state update: briT: %d bri: %d briOld: %d, isPoweron: %d , isPoweroff %d, trigger: %d\n"), (int)briT, (int)bri, (int)briOld, (int)strip.isPoweringOn(), (int)strip.isPoweringOff(), (int)strip.isPowerTrigger()); - if (strip.getTransition() == 0) { jsonTransitionOnce = false; transitionActive = false; applyFinalBri(); } else { + uint32_t now = millis(); if (blendingStyle != TRANSITION_FADE) { if (strip.isPoweringOff() && strip.isPoweringOn()) { // if both flags are set, the power state was reversed during transition, invert the transition time to keep "overall brightness" i.e number of lit LEDs @@ -147,7 +105,6 @@ void stateUpdated(byte callMode) { if (blendingStyle != TRANSITION_FADE) applyFinalBri();; // set brightness immediately, otherwise it will fade-in -> this does not yet work. need to set to bri old? or bri last? } } - //TODO: do we need to set briT = briOld when powering off? since we can now fade in parallel, just let it continue? if (strip.isPoweringOff() && bri > 0) { // powering off but brightness was changed -> switch to powering on, update is handled below @@ -172,6 +129,49 @@ void stateUpdated(byte callMode) { } strip.clearPowerFlag(TRANSITION_POWER_TRIGGER); } +} + +//called after every state changes, schedules interface updates, handles brightness transition and nightlight activation +//unlike colorUpdated(), does NOT apply any colors or FX to segments +void stateUpdated(byte callMode) { + //call for notifier -> 0: init 1: direct change 2: button 3: notification 4: nightlight 5: other (No notification) + // 6: fx changed 7: hue 8: preset cycle 9: blynk 10: alexa 11: ws send only 12: button preset + setValuesFromFirstSelectedSeg(); // a much better approach would be to use main segment: setValuesFromMainSeg() + + if (bri != briOld || stateChanged) { + if (stateChanged) currentPreset = 0; //something changed, so we are no longer in the preset + + if (callMode != CALL_MODE_NOTIFICATION && callMode != CALL_MODE_NO_NOTIFY) notify(callMode); + if (bri != briOld && nodeBroadcastEnabled) sendSysInfoUDP(); // update on state + + //set flag to update ws and mqtt + interfaceUpdateCallMode = callMode; + } else { + if (nightlightActive && !nightlightActiveOld && callMode != CALL_MODE_NOTIFICATION && callMode != CALL_MODE_NO_NOTIFY) { + notify(CALL_MODE_NIGHTLIGHT); + interfaceUpdateCallMode = CALL_MODE_NIGHTLIGHT; + } + } + + unsigned long now = millis(); + if (callMode != CALL_MODE_NO_NOTIFY && nightlightActive && (nightlightMode == NL_MODE_FADE || nightlightMode == NL_MODE_COLORFADE)) { + briNlT = bri; + nightlightDelayMs -= (now - nightlightStartTime); + nightlightStartTime = now; + } + if (briT == 0) { + if (callMode != CALL_MODE_NOTIFICATION) strip.resetTimebase(); //effect start from beginning + } + + if (bri > 0) briLast = bri; + + //deactivate nightlight if target brightness is reached + if (bri == nightlightTargetBri && callMode != CALL_MODE_NO_NOTIFY && nightlightMode != NL_MODE_SUN) nightlightActive = false; + + // notify usermods of state change + UsermodManager::onStateChange(callMode); + + handleBriChange(); // check if a global brightness changed and start/update transition if needed stateChanged = false; } @@ -218,7 +218,7 @@ void handleTransitions() { return; } // fade global brightness from briOld to bri, skip if powering off using spatial transition (avoid fading in parallel) - // note: power on sets briOld = bri so it wont fade but still allows global brightness change during that transition + // note: power on sets briOld = bri so it wont fade but still allows global brightness change during that transition which then will fade if (!strip.isPoweringOff() || blendingStyle == TRANSITION_FADE) { byte briTO = briT; int deltaBri = (int)bri - (int)briOld; From 5ec1e88052871b0ba8ae67fb950000fe090d2115 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 7 Sep 2026 10:40:36 +0200 Subject: [PATCH 10/19] remove debug prints --- wled00/FX_fcn.cpp | 12 ------------ wled00/led.cpp | 6 +----- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 1a0ccc7a87..58d5cce09e 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -292,11 +292,9 @@ void Segment::handleTransition() { if (isInTransition() && !strip.isPoweringOff()) { // end transitions if completed but wait for a global power-off transition to complete to avoid revealing pixels (see blendSegment() blanking) if (_t->_oldSegment && _t->_progress == 0xFFFFU && !strip.isPoweringOff()) { - Serial.printf("handleTransition: deleting old segment copy, progress=%d\n", _t->_progress); delete _t->_oldSegment; _t->_oldSegment = nullptr; } if (progress() == 0xFFFFU && fadeProgress() == 0xFFFFU) { - Serial.printf("handleTransition: transition completed, progress=%d fadeProgress=%d\n", _t->_progress, _t->_fadeProgress); stopTransition(); // Transition frees a kept copy } } @@ -333,15 +331,12 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { const uint8_t power = kind & TRANSITION_POWER_MASK; // power flags (TRANSITION_POWER_*) kind &= TRANSITION_KIND_MASK; // strip the power flags const bool targetOn = power == TRANSITION_POWER_TOGGLE ? !on : power == TRANSITION_POWER_ON; // target on-state for power transitions - Serial.printf("startTransition: dur=%d kind=%d power=%d targetOn=%d\n", dur, kind, power, targetOn); - // check if we even need to start a transition: abort if transitions disabled, not an active segment or not in an on state (unless this is a power-on request) if (dur == 0 || !isActive() || ((power != TRANSITION_POWER_TOGGLE) && !on)) { return; } // check if we need a copy of current segment: only effect transitions and transitions using a spatial (non-FADE) style const bool segmentCopy = kind == TRANSITION_KIND_EFFECT || (kind != TRANSITION_KIND_FADE && blendingStyle != TRANSITION_FADE); - Serial.printf("*****startTransition: dur=%d kind=%d power=%d targetOn=%d segmentCopy=%d seg is on=%d\n", dur, kind, power, targetOn, segmentCopy, on); // helper lambda function to capture current _bri/_cct and optionally _colors to the segments transitions (_t) state TDODO: needs refinement const auto captureBlend = [&](unsigned long fadeStart) { for (unsigned i = 0; i < NUM_COLORS; i++) _t->_colors[i] = color_blend16(_t->_colors[i], colors[i], _t->_fadeProgress); @@ -370,7 +365,6 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { if (isInTransition()) { // re-targeting a running transition: fade restarts, starting from current blend; a running spatial transition continues to completion if (!power) { - Serial.printf("***re-targeting transition: dur=%d start=%d now=%d progress=%d fadeProgress=%d\n", _t->_dur, _t->_start, millis(), _t->_progress, _t->_fadeProgress); // opacity/CCT/color/palette/FX change: rebase fades to the current visual blend and restart it (no jump). A running spatial transition continues if (segmentCopy && _t->_oldSegment == nullptr) { // no old segment means a fade transition is going on (color, palette, opacity, cct), capture current state into the old segment @@ -394,16 +388,12 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { captureBlend(millis()); // restart fade channel from the current visual state if (segmentCopy) _t->_fadeDur = (_t->_dur * _t->_progress) / 0xFFFFU; // if this is a deferred spatial request align fade time with ongoing spatial channel } - Serial.printf("*returning from retargeting transition"); return; } // power transition (on or off) request if (_t->_flags & TRANSITION_FLAG_POWER) { // power transition request (per segment or global) during an ongoing power transition - Serial.printf("***power transition: already in a power transition, dur=%d start=%d now=%d progress=%d\n", _t->_dur, _t->_start, millis(), _t->_progress); if (targetOn == ((_t->_flags & TRANSITION_FLAG_POWER_ON) != 0)) return; // same target re-issued, let the running transition finish - - Serial.printf("***power transition: reversing ongoing power transition, dur=%d start=%d now=%d progress=%d\n", _t->_dur, _t->_start, millis(), _t->_progress); if (blendingStyle != TRANSITION_FADE) { // already in a power transition reverse in place: invert the spatial timeline (20%-completed swipe continues from 80%) _t->_dur = dur; @@ -413,7 +403,6 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { } else captureBlend(millis()); // capture current fade status and restart fade when toggling if (power == TRANSITION_POWER_TOGGLE) { // segment-level on/off - Serial.printf("***power toggle: reversing spatial timeline: dur=%d start=%d now=%d progress=%d\n", _t->_dur, _t->_start, millis(), _t->_progress); if (_t->_oldSegment) { if (strip.isPoweringOff()) _t->_flags ^= TRANSITION_FLAG_POWER_ON; // flip POWER_ON flag, it is flipped back below, we need it to stay off if a segment is turned on during global off if (!strip.isPoweringOn()) _t->_oldSegment->on = !_t->_oldSegment->on; // invert old segment's on state (but do not turn old segment off so rendering continues) @@ -423,7 +412,6 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { } _t->_flags ^= TRANSITION_FLAG_POWER_ON; // flip POWER_ON flag } else { - Serial.printf("***power transition: starting new power transition, dur=%d start=%d now=%d progress=%d\n", _t->_dur, _t->_start, millis(), _t->_progress); // global or segment on/off initiated: stop ongoing segment transition immediately, we do need the spatial channel and want to start a new transition if (_t->_oldSegment) { delete _t->_oldSegment; _t->_oldSegment = nullptr; } captureBlend(millis()); // rebase transition values to current visual blend before starting the new power transition diff --git a/wled00/led.cpp b/wled00/led.cpp index ad98073ccb..e92a17610f 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -80,7 +80,7 @@ void applyFinalBri() { // local function to handle global brightness transition, called from stateUpdated(). Note: power flags are set in toggleOnOff() void handleBriChange() { - DEBUG_PRINTF_P(PSTR("***********state update: briT: %d bri: %d briOld: %d, isPoweron: %d , isPoweroff %d, trigger: %d\n"), (int)briT, (int)bri, (int)briOld, (int)strip.isPoweringOn(), (int)strip.isPoweringOff(), (int)strip.isPowerTrigger()); + //DEBUG_PRINTF_P(PSTR("state update: briT: %d bri: %d briOld: %d, isPoweron: %d , isPoweroff %d, trigger: %d\n"), (int)briT, (int)bri, (int)briOld, (int)strip.isPoweringOn(), (int)strip.isPoweringOff(), (int)strip.isPowerTrigger()); if (strip.getTransition() == 0) { jsonTransitionOnce = false; transitionActive = false; @@ -110,7 +110,6 @@ void handleBriChange() { // powering off but brightness was changed -> switch to powering on, update is handled below strip.clearPowerFlag(TRANSITION_POWER_OFF); strip.setPowerFlag(TRANSITION_POWER_ON | TRANSITION_POWER_TRIGGER); - Serial.println("state: brightness change during power off transition, toggling to on transition"); } // if brightness changed, start a new global transition but do not reset the timer if powering off (unless powering back on i.e. triggered) @@ -121,10 +120,8 @@ void handleBriChange() { } transitionActive = true; transitionStartTime = now; // note: this only affects brightness fade, spatial transition continues as it is handled on segment level - Serial.println("state: starting global transition, briT: " + String(briT) + " bri: " + String(bri) + " briOld: " + String(briOld)); } if (blendingStyle != TRANSITION_FADE && (strip.isPoweringOn() || strip.isPoweringOff()) && strip.isPowerTrigger()) { - Serial.println("state: global on/off transition detected, forcing all segments to transition mode"); strip.setTransitionMode(true); // force all segments to a spatial on/off transition, segments handle transition inversion (on during off or off during on) } strip.clearPowerFlag(TRANSITION_POWER_TRIGGER); @@ -204,7 +201,6 @@ void handleTransitions() { // note: the !stateChanged is a workaround: bri is updated async, this code can run before stateUpdated() is called, causing a jump in the fade if (transitionActive && strip.getTransition() > 0 && !stateChanged) { - //Serial.printf("GT: %d, bri: %d, briOld: %d\n", (int)briT, (int)bri, (int)briOld); int progress = millis() - transitionStartTime; int duration = strip.getTransition(); // finalize once the transition time has elapsed From 3468367bb3a3b640a1ea413d026117e189c9922d Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 7 Sep 2026 11:27:11 +0200 Subject: [PATCH 11/19] bugfix --- wled00/FX_fcn.cpp | 2 +- wled00/led.cpp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index c7d5974fbf..e163cb6bf5 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -514,7 +514,7 @@ void Segment::beginDraw(uint16_t prog) { // minimum blend time is 100ms maximum is 65535ms unsigned noOfBlends = ((255U * prog) / 0xFFFFU) - _t->_prevPaletteBlends; if (noOfBlends > 255) noOfBlends = 255; // safety check - for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, Segment::_currentPalette, 48); + for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, Segment::_currentPalette, 48); Segment::_currentPalette = _t->_palT; // copy transitioning/temporary palette } } diff --git a/wled00/led.cpp b/wled00/led.cpp index e92a17610f..8eedfac4f7 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -84,6 +84,7 @@ void handleBriChange() { if (strip.getTransition() == 0) { jsonTransitionOnce = false; transitionActive = false; + strip.clearPowerFlag(0xFF); // clear all power flags applyFinalBri(); } else { uint32_t now = millis(); From ca6ee267d23b448e6b53d1340f864b56562debbf Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 26 Sep 2026 13:26:30 +0200 Subject: [PATCH 12/19] rename _start and _dur to _spatialX to make intent clearer --- wled00/FX.h | 14 +++++++------- wled00/FX_fcn.cpp | 24 ++++++++++++------------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/wled00/FX.h b/wled00/FX.h index 77e8547a6d..ce341d274f 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -522,11 +522,11 @@ class Segment { // transition data, holds values during transition (76 bytes/28 bytes) struct Transition { Segment *_oldSegment; // previous segment environment (may be nullptr if effect did not change) - unsigned long _start; // spatial channel start, must accommodate millis() + unsigned long _spatialStart; // spatial channel start, must accommodate millis() unsigned long _fadeStart; // fade channel start uint32_t _colors[NUM_COLORS]; // colors at the start of fade channel CRGBPalette16 _palT; // temporary palette (slowly being morphed from old to new) - uint16_t _dur; // duration of spatial channel in ms + uint16_t _spatialDur; // duration of spatial channel in ms uint16_t _fadeDur; // duration of fade channel in ms uint16_t _progress; // spatial channel progress (0-65535); pre-calculated in updateTransitionProgress() uint16_t _fadeProgress; // fade channel progress (0-65535) @@ -535,11 +535,11 @@ class Segment { uint8_t _flags; // TRANSITION_FLAG_* power state Transition(uint16_t dur=750) : _oldSegment(nullptr) - , _start(millis()) - , _fadeStart(_start) + , _spatialStart(millis()) + , _fadeStart(_spatialStart) , _colors{0,0,0} , _palT(CRGBPalette16()) - , _dur(dur) + , _spatialDur(dur) , _fadeDur(dur) , _progress(0) , _fadeProgress(0) @@ -657,9 +657,9 @@ class Segment { inline bool isInTransition() const { return _t != nullptr; } inline uint16_t progress() const { return isInTransition() ? _t->_progress : 0xFFFFU; } // spatial channel progress, relies on handleTransition()/updateTransitionProgress() inline uint16_t fadeProgress() const { return isInTransition() ? _t->_fadeProgress : 0xFFFFU; } // fade channel progress, relies on handleTransition()/updateTransitionProgress() - inline unsigned long getTransitionStart() const { return isInTransition() ? _t->_start : 0; } // spatial channel start time + inline unsigned long getTransitionStart() const { return isInTransition() ? _t->_spatialStart : 0; } // spatial channel start time inline Segment *getOldSegment() const { return isInTransition() ? _t->_oldSegment : nullptr; } - inline bool fadeTransitionActive() const { return isInTransition() && _t->_fadeStart > _t->_start; } // true if fading during a spatial transition + inline bool fadeTransitionActive() const { return isInTransition() && _t->_fadeStart > _t->_spatialStart; } // true if fading during a spatial transition inline bool isPowerTransition() const { return isInTransition() && (_t->_flags & TRANSITION_FLAG_POWER) && _t->_oldSegment != nullptr; } inline bool isPowerOffTransition() const { return isPowerTransition() && !(_t->_flags & TRANSITION_FLAG_POWER_ON); } // spatial to off inline bool isPowerOnTransition() const { return isPowerTransition() && (_t->_flags & TRANSITION_FLAG_POWER_ON); } // spatial to on diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index e163cb6bf5..e4dc93ba0c 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -369,8 +369,8 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { if (segmentCopy && _t->_oldSegment == nullptr) { // no old segment means a fade transition is going on (color, palette, opacity, cct), capture current state into the old segment if (createOldSegment(_t->_progress)) { - _t->_start = millis(); // start spatial transition (fading continues on current segment) - _t->_dur = dur; + _t->_spatialStart = millis(); // start spatial transition (fading continues on current segment) + _t->_spatialDur = dur; DEBUGFX_PRINTF_P(PSTR("-- Updated transition with segment copy: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); } else { // not enough RAM for segment copy: degrade to pure fade instead of dropping the transition @@ -386,7 +386,7 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { loadPalette(_t->_palT, palette); } captureBlend(millis()); // restart fade channel from the current visual state - if (segmentCopy) _t->_fadeDur = (_t->_dur * _t->_progress) / 0xFFFFU; // if this is a deferred spatial request align fade time with ongoing spatial channel + if (segmentCopy) _t->_fadeDur = (_t->_spatialDur * _t->_progress) / 0xFFFFU; // if this is a deferred spatial request align fade time with ongoing spatial channel } return; } @@ -396,8 +396,8 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { if (targetOn == ((_t->_flags & TRANSITION_FLAG_POWER_ON) != 0)) return; // same target re-issued, let the running transition finish if (blendingStyle != TRANSITION_FADE) { // already in a power transition reverse in place: invert the spatial timeline (20%-completed swipe continues from 80%) - _t->_dur = dur; - _t->_start = millis() - (((unsigned)(0xFFFFU - _t->_progress) * dur) / 0xFFFFU); + _t->_spatialDur = dur; + _t->_spatialStart = millis() - (((unsigned)(0xFFFFU - _t->_progress) * dur) / 0xFFFFU); _t->_fadeDur = 0; // disable fading (any ongoing fade completes immediately) createOldSegment(0xFFFFU); // create a fresh copy from the final state which is currently displayed } else captureBlend(millis()); // capture current fade status and restart fade when toggling @@ -417,17 +417,17 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { captureBlend(millis()); // rebase transition values to current visual blend before starting the new power transition if (segmentCopy) { if (createOldSegment(0xFFFFU)) { // spatial transition, need a fresh copy (colors as-is, old side captures the current transition brightness) - _t->_start = millis(); - _t->_dur = dur; + _t->_spatialStart = millis(); + _t->_spatialDur = dur; _t->_fadeDur = 0; // non-fade power transition, do not fade anything but reveal the final state (same as a fresh power start) DEBUGFX_PRINTF_P(PSTR("-- Restarted power transition: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); } else { // not enough RAM for segment copy: degrade to pure fade (restarted above) instead of dropping the transition - _t->_start = 0; // disables the spatial channel and uses fade instead + _t->_spatialStart = 0; // disables the spatial channel and uses fade instead } } else { // FADE blending: the fade channel (restarted above) carries the power transition - _t->_start = 0; // FADE blending: disables the spatial channel and uses fade instead + _t->_spatialStart = 0; // FADE blending: disables the spatial channel and uses fade instead } _t->_flags = TRANSITION_FLAG_POWER | (targetOn ? TRANSITION_FLAG_POWER_ON : 0); } @@ -449,7 +449,7 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { if (_t->_oldSegment) { DEBUGFX_PRINTF_P(PSTR("-- Started transition: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); } else { - _t->_start = 0; // disables the spatial channel and use fade i.e. enable fadeTransitionActive() + _t->_spatialStart = 0; // disables the spatial channel and use fade i.e. enable fadeTransitionActive() DEBUGFX_PRINTF_P(PSTR("-- Started transition without old segment: S=%p T(%p)\n"), this, _t); } } @@ -466,8 +466,8 @@ void Segment::stopTransition() { void Segment::updateTransitionProgress() const { if (isInTransition()) { _t->_progress = _t->_fadeProgress = 0xFFFF; - unsigned diff = millis() - _t->_start; - if (_t->_dur > 0 && diff < _t->_dur) _t->_progress = diff * 0xFFFFU / _t->_dur; + unsigned diff = millis() - _t->_spatialStart; + if (_t->_spatialDur > 0 && diff < _t->_spatialDur) _t->_progress = diff * 0xFFFFU / _t->_spatialDur; diff = millis() - _t->_fadeStart; if (_t->_fadeDur > 0 && diff < _t->_fadeDur) _t->_fadeProgress = diff * 0xFFFFU / _t->_fadeDur; } From aa544aa6f41929048e9faf7edaa84d6a4f4db5f9 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sat, 26 Sep 2026 15:28:11 +0200 Subject: [PATCH 13/19] invert on/off spatial animation instead of flipping it --- wled00/FX.h | 8 +++-- wled00/FX_2Dfcn.cpp | 15 +++++---- wled00/FX_fcn.cpp | 80 +++++++++++++++++++++++++++++++-------------- 3 files changed, 70 insertions(+), 33 deletions(-) diff --git a/wled00/FX.h b/wled00/FX.h index ce341d274f..57be636716 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -423,6 +423,7 @@ extern byte realtimeMode; // used in getMappedPixelIndex() // transition flags (scope and target state of a power transition) #define TRANSITION_FLAG_POWER 0x01 // power (on/off) transition #define TRANSITION_FLAG_POWER_ON 0x02 // target state is "on" +#define TRANSITION_FLAG_REVERSED 0x04 // use inverse spatial transition if set (e.g. swipe left -> swipe right) @@ -663,6 +664,7 @@ class Segment { inline bool isPowerTransition() const { return isInTransition() && (_t->_flags & TRANSITION_FLAG_POWER) && _t->_oldSegment != nullptr; } inline bool isPowerOffTransition() const { return isPowerTransition() && !(_t->_flags & TRANSITION_FLAG_POWER_ON); } // spatial to off inline bool isPowerOnTransition() const { return isPowerTransition() && (_t->_flags & TRANSITION_FLAG_POWER_ON); } // spatial to on + inline bool isTransitionReversed() const { return isInTransition() && (_t->_flags & TRANSITION_FLAG_REVERSED); } inline bool isActive() const { return stop > start && pixels; } inline bool hasRGB() const { return _isRGB; } inline bool hasWhite() const { return _hasW; } @@ -729,7 +731,7 @@ class Segment { inline void setPixelColor(float i, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0, bool aa = true) const { setPixelColor(i, RGBW32(r,g,b,w), aa); } inline void setPixelColor(float i, CRGB c, bool aa = true) const { setPixelColor(i, RGBW32(c.r,c.g,c.b,0), aa); } #endif - [[gnu::hot]] bool isPixelClipped(int i) const; + [[gnu::hot]] bool isPixelClipped(int i, uint8_t style) const; [[gnu::hot]] uint32_t getPixelColor(int i) const; // 1D support functions (some implement 2D as well) void blur(uint8_t, bool smear = false) const; @@ -776,7 +778,7 @@ class Segment { inline void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) const { setPixelColorXY(x, y, RGBW32(r,g,b,w), aa); } inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) const { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), aa); } #endif - [[gnu::hot]] bool isPixelXYClipped(int x, int y) const; + [[gnu::hot]] bool isPixelXYClipped(int x, int y, uint8_t style) const; [[gnu::hot]] uint32_t getPixelColorXY(int x, int y) const; // 2D support functions inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t color, uint8_t blend) const { setPixelColorXY(x, y, color_blend(getPixelColorXY(x,y), color, blend)); } @@ -813,7 +815,7 @@ class Segment { inline void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) { setPixelColor(x, RGBW32(r,g,b,w), aa); } inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) const { setPixelColor(x, RGBW32(c.r,c.g,c.b,0), aa); } #endif - inline bool isPixelXYClipped(int x, int y) { return isPixelClipped(x); } + inline bool isPixelXYClipped(int x, int y, uint8_t style) { return isPixelClipped(x, style); } inline uint32_t getPixelColorXY(int x, int y) { return getPixelColor(x); } inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t c, uint8_t blend) { blendPixelColor(x, c, blend); } inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) { blendPixelColor(x, RGBW32(c.r,c.g,c.b,0), blend); } diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index eb7748b5c7..1727479989 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -148,26 +148,29 @@ void WS2812FX::setUpMatrix() { #ifndef WLED_DISABLE_2D // pixel is clipped if it falls outside clipping range // if clipping start > stop the clipping range is inverted -bool Segment::isPixelXYClipped(int x, int y) const { - if (blendingStyle != TRANSITION_FADE && isInTransition() && _clipStart != _clipStop) { +bool Segment::isPixelXYClipped(int x, int y, uint8_t style) const { + if (style != TRANSITION_FADE && isInTransition() && _clipStart != _clipStop) { const bool invertX = _clipStart > _clipStop; const bool invertY = _clipStartY > _clipStopY; const int cStartX = invertX ? _clipStop : _clipStart; const int cStopX = invertX ? _clipStart : _clipStop; const int cStartY = invertY ? _clipStopY : _clipStartY; const int cStopY = invertY ? _clipStartY : _clipStopY; - if (blendingStyle == TRANSITION_FAIRY_DUST) { + if (style == TRANSITION_FAIRY_DUST) { const unsigned width = cStopX - cStartX; // assumes full segment width (faster than virtualWidth()) const unsigned len = width * (cStopY - cStartY); // assumes full segment height (faster than virtualHeight()) if (len < 2) return false; const unsigned shuffled = hashInt(x + y * width) % len; const unsigned pos = (shuffled * 0xFFFFU) / len; + if (isTransitionReversed()) { + return (0xFFFFU - progress()) > pos; // invert progress and invert mask -> plays animation in reverse + } return progress() <= pos; } - if (blendingStyle == TRANSITION_CIRCULAR_IN || blendingStyle == TRANSITION_CIRCULAR_OUT) { + if (style == TRANSITION_CIRCULAR_IN || style == TRANSITION_CIRCULAR_OUT) { const int cx = (cStopX-cStartX+1) / 2; const int cy = (cStopY-cStartY+1) / 2; - const bool out = (blendingStyle == TRANSITION_CIRCULAR_OUT); + const bool out = (style == TRANSITION_CIRCULAR_OUT); const unsigned prog = out ? progress() : 0xFFFFU - progress(); int radius2 = max(cx, cy) * prog / 0xFFFF; radius2 = 2 * radius2 * radius2; @@ -179,7 +182,7 @@ bool Segment::isPixelXYClipped(int x, int y) const { } bool xInside = (x >= cStartX && x < cStopX); if (invertX) xInside = !xInside; bool yInside = (y >= cStartY && y < cStopY); if (invertY) yInside = !yInside; - const bool clip = blendingStyle == TRANSITION_OUTSIDE_IN ? xInside || yInside : xInside && yInside; + const bool clip = style == TRANSITION_OUTSIDE_IN ? xInside || yInside : xInside && yInside; return !clip; } return false; diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index e4dc93ba0c..34711351db 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -313,7 +313,7 @@ void Segment::handleTransition() { since this would require careful sync to spatial transition, the segment is held in transition until global finishes (see handleTransition() & blendSegment()) - On strip level, there are flags to check for global on/off transitions which are set in toggleOnOff() - A global transition is started in stateUpdated() and triggers segment transitions if needed for spatial transitions - - When a spatial on/off transition is triggered during an ongoing on/off transition, it is reversed (i.e. same number of LEDs are lit but flip position) + - When a spatial on/off transition is triggered during an ongoing on/off transition, it is reversed by inverting the transition style (e.g. swipe-right becomes swipe-left) - Fade transitions continue from the current blend state if issued during a running transition - If a spatial transition is running it is never restarted. A subsequent change is deferred to the fade channel instead - For more details, see the comments throughout the code @@ -323,10 +323,37 @@ void Segment::handleTransition() { // note: _t has the temporary "from" segment value(s) and the current segment holds the "to" values which are set after the transition starts. // the transition has two independent channels: // the fade channel (_fadeStart/_fadeDur/_fadeProgress) crossfades colors, palette, CCT and opacity and never needs a segment copy -// the spatial channel (_start/_dur/_progress and _oldSegment) renders wipe/push/etc. using a copy of the current state (oldSegment) +// the spatial channel (_spatialStart/_spatialDur/_progress and _oldSegment) renders wipe/push/etc. using a copy of the current state (oldSegment) // kind: low nibble = TRANSITION_KIND_x identifying which change triggered the transition (determines whether a segment copy is needed) // high nibble = TRANSITION_POWER_x flags: POWER_ON/POWER_OFF = global on/off, POWER_TOGGLE = segment on/off (both flags are set) +// helper to return the opposite spatial transition style +static uint8_t reverseBlendingStyle(uint8_t bs) { + switch (bs) { + case TRANSITION_SWIPE_RIGHT: return TRANSITION_SWIPE_LEFT; + case TRANSITION_SWIPE_LEFT: return TRANSITION_SWIPE_RIGHT; + case TRANSITION_SWIPE_UP: return TRANSITION_SWIPE_DOWN; + case TRANSITION_SWIPE_DOWN: return TRANSITION_SWIPE_UP; + case TRANSITION_SWIPE_TL: return TRANSITION_SWIPE_BR; + case TRANSITION_SWIPE_BR: return TRANSITION_SWIPE_TL; + case TRANSITION_SWIPE_TR: return TRANSITION_SWIPE_BL; + case TRANSITION_SWIPE_BL: return TRANSITION_SWIPE_TR; + case TRANSITION_PUSH_RIGHT: return TRANSITION_PUSH_LEFT; + case TRANSITION_PUSH_LEFT: return TRANSITION_PUSH_RIGHT; + case TRANSITION_PUSH_UP: return TRANSITION_PUSH_DOWN; + case TRANSITION_PUSH_DOWN: return TRANSITION_PUSH_UP; + case TRANSITION_PUSH_TL: return TRANSITION_PUSH_BR; + case TRANSITION_PUSH_BR: return TRANSITION_PUSH_TL; + case TRANSITION_PUSH_TR: return TRANSITION_PUSH_BL; + case TRANSITION_PUSH_BL: return TRANSITION_PUSH_TR; + case TRANSITION_OUTSIDE_IN: return TRANSITION_INSIDE_OUT; + case TRANSITION_INSIDE_OUT: return TRANSITION_OUTSIDE_IN; + case TRANSITION_CIRCULAR_OUT: return TRANSITION_CIRCULAR_IN; + case TRANSITION_CIRCULAR_IN: return TRANSITION_CIRCULAR_OUT; + default: return bs; // OPEN_H and OPEN_V are symmetrical; FADE/FairyDust are unchanged + } +} + void Segment::startTransition(uint16_t dur, uint8_t kind) { const uint8_t power = kind & TRANSITION_POWER_MASK; // power flags (TRANSITION_POWER_*) kind &= TRANSITION_KIND_MASK; // strip the power flags @@ -395,7 +422,9 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { // power transition request (per segment or global) during an ongoing power transition if (targetOn == ((_t->_flags & TRANSITION_FLAG_POWER_ON) != 0)) return; // same target re-issued, let the running transition finish if (blendingStyle != TRANSITION_FADE) { - // already in a power transition reverse in place: invert the spatial timeline (20%-completed swipe continues from 80%) + // already in a power transition reverse the animation: setting the REVERSED flag inverts the transition style + // the timeline is flipped so it continues smoothly: e.g. 20%-on swipe-right becomes a 80% off swipe-left + _t->_flags ^= TRANSITION_FLAG_REVERSED; _t->_spatialDur = dur; _t->_spatialStart = millis() - (((unsigned)(0xFFFFU - _t->_progress) * dur) / 0xFFFFU); _t->_fadeDur = 0; // disable fading (any ongoing fade completes immediately) @@ -857,16 +886,19 @@ uint16_t Segment::maxMappingLength() const { #endif // pixel is clipped if it falls outside clipping range // if clipping start > stop the clipping range is inverted -bool Segment::isPixelClipped(int i) const { - if (blendingStyle != TRANSITION_FADE && isInTransition() && _clipStart != _clipStop) { +bool Segment::isPixelClipped(int i, uint8_t style) const { + if (style != TRANSITION_FADE && isInTransition() && _clipStart != _clipStop) { bool invert = _clipStart > _clipStop; // ineverted start & stop int start = invert ? _clipStop : _clipStart; int stop = invert ? _clipStart : _clipStop; - if (blendingStyle == TRANSITION_FAIRY_DUST) { + if (style == TRANSITION_FAIRY_DUST) { unsigned len = stop - start; if (len < 2) return false; unsigned shuffled = hashInt(i) % len; unsigned pos = (shuffled * 0xFFFFU) / len; + if (isTransitionReversed()) { + return (0xFFFFU - progress()) > pos; // invert progress and invert mask -> plays animation in reverse + } return progress() <= pos; } const bool iInside = (i >= start && i < stop); @@ -1576,7 +1608,9 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { uint8_t opacityOld = opacity; // we set this to opacity of old segment in non-FADE transitions below uint8_t cct = topSegment.currentCCT(); const Segment *segO = topSegment.getOldSegment(); - if (segO && blendingStyle != TRANSITION_FADE) opacityOld = segO->currentBri(); // get old segment opacity note: can not use segO->opacity as that breaks off->on transition + uint8_t style = blendingStyle; + if (topSegment.isTransitionReversed()) style = reverseBlendingStyle(style); + if (segO && style != 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); @@ -1584,7 +1618,7 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { const bool hasGrouping = topSegment.groupLength() != 1; // fast path: handle the default case - no transitions, no grouping/spacing, no mirroring, no CCT - if (!segO && blendingStyle == TRANSITION_FADE && !hasGrouping && !topSegment.mirror && !topSegment.mirror_y) { + if (!segO && style == TRANSITION_FADE && !hasGrouping && !topSegment.mirror && !topSegment.mirror_y) { if (isMatrix && stopIndx <= matrixSize && !_pixelCCT) { #ifndef WLED_DISABLE_2D // Calculate pointer steps to avoid 'if' and 'XY()' inside loops @@ -1641,12 +1675,11 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { Segment::setClippingRect(0, 0); // disable clipping by default const unsigned progress = topSegment.progress(); const unsigned progInv = 0xFFFFU - progress; - 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; + const unsigned dw = (style==TRANSITION_OUTSIDE_IN ? progInv : progress) * width / 0xFFFFU + 1; + const unsigned dh = (style==TRANSITION_OUTSIDE_IN ? progInv : progress) * height / 0xFFFFU + 1; // single pixel segments or transitions without a rendered old segment: use fade - if (width*height == 1 || !segO) blendingStyle = TRANSITION_FADE; - switch (blendingStyle) { + if (width*height == 1 || !segO) style = TRANSITION_FADE; + switch (style) { case TRANSITION_CIRCULAR_IN: // (must set entire segment, see isPixelXYClipped()) case TRANSITION_CIRCULAR_OUT:// (must set entire segment, see isPixelXYClipped()) case TRANSITION_FAIRY_DUST: // fairy dust (must set entire segment, see isPixelXYClipped()) @@ -1730,13 +1763,13 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { }; // if we blend using "push" style we need to "shift" canvas to left/right/up/down - unsigned offsetX = (blendingStyle == TRANSITION_PUSH_UP || blendingStyle == TRANSITION_PUSH_DOWN) ? 0 : progInv * nCols / 0xFFFFU; - unsigned offsetY = (blendingStyle == TRANSITION_PUSH_LEFT || blendingStyle == TRANSITION_PUSH_RIGHT) ? 0 : progInv * nRows / 0xFFFFU; + unsigned offsetX = (style == TRANSITION_PUSH_UP || style == TRANSITION_PUSH_DOWN) ? 0 : progInv * nCols / 0xFFFFU; + unsigned offsetY = (style == TRANSITION_PUSH_LEFT || style == TRANSITION_PUSH_RIGHT) ? 0 : progInv * nRows / 0xFFFFU; const unsigned groupLen = topSegment.groupLength(); bool applyReverse = topSegment.reverse || topSegment.reverse_y || topSegment.transpose; int pushOffsetX = 0, pushOffsetY = 0; // if we blend using "push" style we need to "shift" canvas to left/right/up/down - switch (blendingStyle) { + switch (style) { case TRANSITION_PUSH_RIGHT: pushOffsetX = offsetX; break; case TRANSITION_PUSH_LEFT: pushOffsetX = -offsetX + nCols; break; case TRANSITION_PUSH_DOWN: pushOffsetY = offsetY; break; @@ -1748,7 +1781,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); + bool clipped = topSegment.isPixelXYClipped(c, r, style); 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 @@ -1760,12 +1793,12 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { if (pushOffsetY != 0) y = (y + pushOffsetY) % nRows; uint32_t c_a = BLACK; if (x < vCols && y < vRows) c_a = seg->getPixelColorRaw(x + y*vCols); // will get clipped pixel from old segment or unclipped pixel from new segment - if (segO && blendingStyle == TRANSITION_FADE + if (segO && style == TRANSITION_FADE && (topSegment.mode != segO->mode || (segO->name != topSegment.name && segO->name && topSegment.name && strncmp(segO->name, topSegment.name, WLED_MAX_SEGNAME_LEN) != 0)) && x < oCols && y < oRows) { // we need to blend old segment using fade as pixels are not clipped c_a = color_blend16(c_a, segO->getPixelColorRaw(x + y*oCols), progInv); - } else if (blendingStyle != TRANSITION_FADE) { + } else if (style != TRANSITION_FADE) { // on/off transition workaround: pixels not yet revealed by a wipe-to-off are black, pixels still covered by a wipe-to-on are black if ((topSegment.isPowerOffTransition() && !clipped) || (topSegment.isPowerOnTransition() && clipped)) c_a = BLACK; } @@ -1819,23 +1852,23 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { unsigned offsetI = progInv * nLen / 0xFFFFU; for (int k = 0; k < nLen; k++) { - const bool clipped = topSegment.isPixelClipped(k); + bool clipped = topSegment.isPixelClipped(k, style); 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; int i = k; // if we blend using "push" style we need to "shift" canvas to left or right - switch (blendingStyle) { + switch (style) { case TRANSITION_PUSH_RIGHT: i = (i + offsetI) % nLen; break; case TRANSITION_PUSH_LEFT: i = (i - offsetI + nLen) % nLen; break; } uint32_t c_a = BLACK; if (i < vLen) c_a = seg->getPixelColorRaw(i); // will get clipped pixel from old segment or unclipped pixel from new segment - if (segO && blendingStyle == TRANSITION_FADE && topSegment.mode != segO->mode && i < oLen) { + if (segO && style == TRANSITION_FADE && topSegment.mode != segO->mode && i < oLen) { // we need to blend old segment using fade as pixels are not clipped c_a = color_blend16(c_a, segO->getPixelColorRaw(i), progInv); - } else if (blendingStyle != TRANSITION_FADE) { + } else if (style != TRANSITION_FADE) { // on/off transition workaround: pixels not yet revealed by a wipe-to-off are black, pixels still covered by a wipe-to-on are black if ((topSegment.isPowerOffTransition() && !clipped) || (topSegment.isPowerOnTransition() && clipped)) c_a = BLACK; } @@ -1850,7 +1883,6 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { } } - blendingStyle = orgBS; Segment::setClippingRect(0, 0); // disable clipping for overlays } From 399d1bccc23d45882e3c4f3211677b9e28c681ff Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 27 Sep 2026 15:37:56 +0200 Subject: [PATCH 14/19] Fix reversal of 2D animations, fully take care of reversal in clipping functions --- wled00/FX_2Dfcn.cpp | 12 +++++++----- wled00/FX_fcn.cpp | 42 +++++++++--------------------------------- 2 files changed, 16 insertions(+), 38 deletions(-) diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index 1727479989..8013951d2f 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -171,19 +171,21 @@ bool Segment::isPixelXYClipped(int x, int y, uint8_t style) const { const int cx = (cStopX-cStartX+1) / 2; const int cy = (cStopY-cStartY+1) / 2; const bool out = (style == TRANSITION_CIRCULAR_OUT); - const unsigned prog = out ? progress() : 0xFFFFU - progress(); + unsigned prog = out ? progress() : 0xFFFFU - progress(); + if (isTransitionReversed()) prog = 0xFFFFU - prog; // invert progress and invert mask -> plays animation in reverse int radius2 = max(cx, cy) * prog / 0xFFFF; radius2 = 2 * radius2 * radius2; - if (radius2 == 0) return out; + if (radius2 == 0) return isTransitionReversed() ? !out : out; const int dx = x - cx; const int dy = y - cy; const bool outside = dx * dx + dy * dy > radius2; - return out ? outside : !outside; + const bool clip = out ? outside : !outside; + return isTransitionReversed() ? !clip : clip; } bool xInside = (x >= cStartX && x < cStopX); if (invertX) xInside = !xInside; bool yInside = (y >= cStartY && y < cStopY); if (invertY) yInside = !yInside; - const bool clip = style == TRANSITION_OUTSIDE_IN ? xInside || yInside : xInside && yInside; - return !clip; + const bool notclipped = style == TRANSITION_OUTSIDE_IN ? xInside || yInside : xInside && yInside; + return isTransitionReversed() ? notclipped : !notclipped; } return false; } diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 34711351db..9fbb582ae7 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -327,33 +327,6 @@ void Segment::handleTransition() { // kind: low nibble = TRANSITION_KIND_x identifying which change triggered the transition (determines whether a segment copy is needed) // high nibble = TRANSITION_POWER_x flags: POWER_ON/POWER_OFF = global on/off, POWER_TOGGLE = segment on/off (both flags are set) -// helper to return the opposite spatial transition style -static uint8_t reverseBlendingStyle(uint8_t bs) { - switch (bs) { - case TRANSITION_SWIPE_RIGHT: return TRANSITION_SWIPE_LEFT; - case TRANSITION_SWIPE_LEFT: return TRANSITION_SWIPE_RIGHT; - case TRANSITION_SWIPE_UP: return TRANSITION_SWIPE_DOWN; - case TRANSITION_SWIPE_DOWN: return TRANSITION_SWIPE_UP; - case TRANSITION_SWIPE_TL: return TRANSITION_SWIPE_BR; - case TRANSITION_SWIPE_BR: return TRANSITION_SWIPE_TL; - case TRANSITION_SWIPE_TR: return TRANSITION_SWIPE_BL; - case TRANSITION_SWIPE_BL: return TRANSITION_SWIPE_TR; - case TRANSITION_PUSH_RIGHT: return TRANSITION_PUSH_LEFT; - case TRANSITION_PUSH_LEFT: return TRANSITION_PUSH_RIGHT; - case TRANSITION_PUSH_UP: return TRANSITION_PUSH_DOWN; - case TRANSITION_PUSH_DOWN: return TRANSITION_PUSH_UP; - case TRANSITION_PUSH_TL: return TRANSITION_PUSH_BR; - case TRANSITION_PUSH_BR: return TRANSITION_PUSH_TL; - case TRANSITION_PUSH_TR: return TRANSITION_PUSH_BL; - case TRANSITION_PUSH_BL: return TRANSITION_PUSH_TR; - case TRANSITION_OUTSIDE_IN: return TRANSITION_INSIDE_OUT; - case TRANSITION_INSIDE_OUT: return TRANSITION_OUTSIDE_IN; - case TRANSITION_CIRCULAR_OUT: return TRANSITION_CIRCULAR_IN; - case TRANSITION_CIRCULAR_IN: return TRANSITION_CIRCULAR_OUT; - default: return bs; // OPEN_H and OPEN_V are symmetrical; FADE/FairyDust are unchanged - } -} - void Segment::startTransition(uint16_t dur, uint8_t kind) { const uint8_t power = kind & TRANSITION_POWER_MASK; // power flags (TRANSITION_POWER_*) kind &= TRANSITION_KIND_MASK; // strip the power flags @@ -902,7 +875,9 @@ bool Segment::isPixelClipped(int i, uint8_t style) const { return progress() <= pos; } const bool iInside = (i >= start && i < stop); - return !iInside ^ invert; // thanks @willmmiles (https://github.com/wled/WLED/pull/3877#discussion_r1554633876) + bool isClipped = !iInside ^ invert; // thanks @willmmiles (https://github.com/wled/WLED/pull/3877#discussion_r1554633876) + if (isTransitionReversed()) isClipped = !isClipped; + return isClipped; } return false; } @@ -1608,8 +1583,7 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { uint8_t opacityOld = opacity; // we set this to opacity of old segment in non-FADE transitions below uint8_t cct = topSegment.currentCCT(); const Segment *segO = topSegment.getOldSegment(); - uint8_t style = blendingStyle; - if (topSegment.isTransitionReversed()) style = reverseBlendingStyle(style); + uint8_t style = blendingStyle; // need a copy as the function may modify it to FADE if (segO && style != 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) @@ -1673,10 +1647,12 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { // slow path: handle transitions, grouping/spacing, segments with clipping and CCT pixels Segment::setClippingRect(0, 0); // disable clipping by default - const unsigned progress = topSegment.progress(); + unsigned progress = (style==TRANSITION_OUTSIDE_IN ? 0xFFFFU - topSegment.progress() : topSegment.progress()); + // play animation in reverse if requrested, clipping is inverted too (see isPixelClipped() functions), startTransition needs to takes care of flipping the timing + if (topSegment.isTransitionReversed()) progress = 0xFFFFU - progress; const unsigned progInv = 0xFFFFU - progress; - const unsigned dw = (style==TRANSITION_OUTSIDE_IN ? progInv : progress) * width / 0xFFFFU + 1; - const unsigned dh = (style==TRANSITION_OUTSIDE_IN ? progInv : progress) * height / 0xFFFFU + 1; + const unsigned dw = (progress * width) / 0xFFFFU + 1; + const unsigned dh = (progress * height) / 0xFFFFU + 1; // single pixel segments or transitions without a rendered old segment: use fade if (width*height == 1 || !segO) style = TRANSITION_FADE; switch (style) { From f1920ee1975c13196bb4606108b717e4280b0283 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 27 Sep 2026 16:03:45 +0200 Subject: [PATCH 15/19] remove unused commented code, fix rabbit findings --- wled00/FX_fcn.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 9fbb582ae7..eeabc07e95 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -346,8 +346,6 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { _t->_fadeDur = dur; _t->_fadeStart = fadeStart; }; - // isFadeBlending returns true while the fade channel is driving color/palette blending -// const auto isFadeBlending = [&]() { return _t->_fadeProgress < 0xFFFFU && (blendingStyle == TRANSITION_FADE || _t->_oldSegment == nullptr || fadeTransitionActive()); }; // create a copy of the current segment to be used for spatial transitions (FX, palette, color, opacity, CCT) const auto createOldSegment = [&](uint16_t colorProgress) { @@ -368,7 +366,7 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { // opacity/CCT/color/palette/FX change: rebase fades to the current visual blend and restart it (no jump). A running spatial transition continues if (segmentCopy && _t->_oldSegment == nullptr) { // no old segment means a fade transition is going on (color, palette, opacity, cct), capture current state into the old segment - if (createOldSegment(_t->_progress)) { + if (createOldSegment(_t->_fadeProgress)) { _t->_spatialStart = millis(); // start spatial transition (fading continues on current segment) _t->_spatialDur = dur; DEBUGFX_PRINTF_P(PSTR("-- Updated transition with segment copy: S=%p T(%p) O[%p] OP[%p]\n"), this, _t, _t->_oldSegment, _t->_oldSegment->pixels); @@ -378,15 +376,13 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { } } else if (_t->_progress > 0) { - // todo: isfadeblending is only used here, maybe remove it and make it explicit? - //if (!isFadeBlending() && _t->_oldSegment != nullptr) { if (!fadeTransitionActive() && _t->_oldSegment != nullptr) { // spatial transition with no fade running: enable fade and let the spatial transition continue. Need to capture the current "revealed" state i.e. copy segment colors to _t for (unsigned i = 0; i < NUM_COLORS; i++) _t->_colors[i] = colors[i]; // rebase transition colors&palette from current final state loadPalette(_t->_palT, palette); } captureBlend(millis()); // restart fade channel from the current visual state - if (segmentCopy) _t->_fadeDur = (_t->_spatialDur * _t->_progress) / 0xFFFFU; // if this is a deferred spatial request align fade time with ongoing spatial channel + if (segmentCopy) _t->_fadeDur = ((uint32_t)_t->_spatialDur * (0xFFFFU - _t->_progress)) / 0xFFFFU; // if this is a deferred spatial request align fade duration with ongoing spatial channel } return; } From c78e77217778a630b002ac86af694546e3603261 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Mon, 28 Sep 2026 03:20:47 +0000 Subject: [PATCH 16/19] Simplify transition inversion - Apply inverted progress calculation up front - Apply inverted clipping at point of use - Simplify some clipping function cases - Clarify 'spatialProgress()' --- wled00/FX.h | 7 ++++--- wled00/FX_2Dfcn.cpp | 14 +++++--------- wled00/FX_fcn.cpp | 44 ++++++++++++++++++++++++-------------------- 3 files changed, 33 insertions(+), 32 deletions(-) diff --git a/wled00/FX.h b/wled00/FX.h index 57be636716..8e93ff2109 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -529,7 +529,7 @@ class Segment { CRGBPalette16 _palT; // temporary palette (slowly being morphed from old to new) uint16_t _spatialDur; // duration of spatial channel in ms uint16_t _fadeDur; // duration of fade channel in ms - uint16_t _progress; // spatial channel progress (0-65535); pre-calculated in updateTransitionProgress() + uint16_t _spatialProgress; // spatial channel progress (0-65535); pre-calculated in updateTransitionProgress() uint16_t _fadeProgress; // fade channel progress (0-65535) uint8_t _prevPaletteBlends; // number of previous palette blends (there are max 255 blends possible) uint8_t _palette, _bri, _cct; // palette ID, brightness and CCT at the start of fade channel (brightness will be 0 if segment was off) @@ -542,7 +542,7 @@ class Segment { , _palT(CRGBPalette16()) , _spatialDur(dur) , _fadeDur(dur) - , _progress(0) + , _spatialProgress(0) , _fadeProgress(0) , _prevPaletteBlends(0) , _palette(0) @@ -573,6 +573,7 @@ class Segment { void stopTransition(); // ends transition mode by destroying transition structure (does nothing if not in transition) void updateTransitionProgress() const; // sets transition progress (0-65535) based on time passed since transition start void handleTransition(); // handles transition progress and ends transitions when completed + inline static uint16_t invertProgress(uint16_t prog) { return 0xFFFFU - prog; }; inline static void modeBlend(bool blend) { Segment::_modeBlend = blend; } // for isPreviousMode() inline static void setClippingRect(int startX, int stopX, int startY = 0, int stopY = 1) { _clipStart = startX; _clipStop = stopX; _clipStartY = startY; _clipStopY = stopY; }; inline static bool isPreviousMode() { return Segment::_modeBlend; } // needed for determining CCT/opacity during non-TRANSITION_FADE transition @@ -656,7 +657,7 @@ class Segment { inline bool getOption(uint8_t n) const { return ((options >> n) & 0x01); } inline bool isSelected() const { return selected; } inline bool isInTransition() const { return _t != nullptr; } - inline uint16_t progress() const { return isInTransition() ? _t->_progress : 0xFFFFU; } // spatial channel progress, relies on handleTransition()/updateTransitionProgress() + inline uint16_t spatialProgress() const { return isInTransition() ? _t->_spatialProgress : 0xFFFFU; } // spatial channel progress, relies on handleTransition()/updateTransitionProgress() inline uint16_t fadeProgress() const { return isInTransition() ? _t->_fadeProgress : 0xFFFFU; } // fade channel progress, relies on handleTransition()/updateTransitionProgress() inline unsigned long getTransitionStart() const { return isInTransition() ? _t->_spatialStart : 0; } // spatial channel start time inline Segment *getOldSegment() const { return isInTransition() ? _t->_oldSegment : nullptr; } diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index 8013951d2f..5766424588 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -162,30 +162,26 @@ bool Segment::isPixelXYClipped(int x, int y, uint8_t style) const { if (len < 2) return false; const unsigned shuffled = hashInt(x + y * width) % len; const unsigned pos = (shuffled * 0xFFFFU) / len; - if (isTransitionReversed()) { - return (0xFFFFU - progress()) > pos; // invert progress and invert mask -> plays animation in reverse - } - return progress() <= pos; + return spatialProgress() <= pos; } if (style == TRANSITION_CIRCULAR_IN || style == TRANSITION_CIRCULAR_OUT) { const int cx = (cStopX-cStartX+1) / 2; const int cy = (cStopY-cStartY+1) / 2; const bool out = (style == TRANSITION_CIRCULAR_OUT); - unsigned prog = out ? progress() : 0xFFFFU - progress(); - if (isTransitionReversed()) prog = 0xFFFFU - prog; // invert progress and invert mask -> plays animation in reverse + unsigned prog = out ? spatialProgress() : invertProgress(spatialProgress()); int radius2 = max(cx, cy) * prog / 0xFFFF; radius2 = 2 * radius2 * radius2; - if (radius2 == 0) return isTransitionReversed() ? !out : out; + if (radius2 == 0) return out; const int dx = x - cx; const int dy = y - cy; const bool outside = dx * dx + dy * dy > radius2; const bool clip = out ? outside : !outside; - return isTransitionReversed() ? !clip : clip; + return clip; } bool xInside = (x >= cStartX && x < cStopX); if (invertX) xInside = !xInside; bool yInside = (y >= cStartY && y < cStopY); if (invertY) yInside = !yInside; const bool notclipped = style == TRANSITION_OUTSIDE_IN ? xInside || yInside : xInside && yInside; - return isTransitionReversed() ? notclipped : !notclipped; + return !notclipped; } return false; } diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index eeabc07e95..bcf50374b6 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -291,10 +291,11 @@ void Segment::handleTransition() { updateTransitionProgress(); if (isInTransition() && !strip.isPoweringOff()) { // end transitions if completed but wait for a global power-off transition to complete to avoid revealing pixels (see blendSegment() blanking) - if (_t->_oldSegment && _t->_progress == 0xFFFFU && !strip.isPoweringOff()) { + const bool spatialDone = spatialProgress() == (_t->_flags & TRANSITION_FLAG_REVERSED ? 0 : 0xFFFFU); + if (_t->_oldSegment && spatialDone) { delete _t->_oldSegment; _t->_oldSegment = nullptr; } - if (progress() == 0xFFFFU && fadeProgress() == 0xFFFFU) { + if (spatialDone && fadeProgress() == 0xFFFFU) { stopTransition(); // Transition frees a kept copy } } @@ -375,14 +376,18 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { captureBlend(millis()); // rebase fade channel to the current visual blend and restart it } } - else if (_t->_progress > 0) { + else if (_t->_spatialProgress > 0 || _t->_fadeProgress < 0xFFFFU) { if (!fadeTransitionActive() && _t->_oldSegment != nullptr) { // spatial transition with no fade running: enable fade and let the spatial transition continue. Need to capture the current "revealed" state i.e. copy segment colors to _t for (unsigned i = 0; i < NUM_COLORS; i++) _t->_colors[i] = colors[i]; // rebase transition colors&palette from current final state loadPalette(_t->_palT, palette); } captureBlend(millis()); // restart fade channel from the current visual state - if (segmentCopy) _t->_fadeDur = ((uint32_t)_t->_spatialDur * (0xFFFFU - _t->_progress)) / 0xFFFFU; // if this is a deferred spatial request align fade duration with ongoing spatial channel + if (segmentCopy) { + // if this is a deferred spatial request align fade duration with ongoing spatial channel + const unsigned remainingProgress = (_t->_flags & TRANSITION_FLAG_REVERSED) ? _t->_spatialProgress : invertProgress(_t->_spatialProgress); + _t->_fadeDur = ((uint32_t)_t->_spatialDur * remainingProgress) / 0xFFFFU; + } } return; } @@ -394,8 +399,9 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { // already in a power transition reverse the animation: setting the REVERSED flag inverts the transition style // the timeline is flipped so it continues smoothly: e.g. 20%-on swipe-right becomes a 80% off swipe-left _t->_flags ^= TRANSITION_FLAG_REVERSED; - _t->_spatialDur = dur; - _t->_spatialStart = millis() - (((unsigned)(0xFFFFU - _t->_progress) * dur) / 0xFFFFU); + unsigned prog = (_t->_flags & TRANSITION_FLAG_REVERSED) ? invertProgress(_t->_spatialProgress) : _t->_spatialProgress; // "used time" + _t->_spatialDur = dur; + _t->_spatialStart = millis() - ((prog * dur) / 0xFFFFU); _t->_fadeDur = 0; // disable fading (any ongoing fade completes immediately) createOldSegment(0xFFFFU); // create a fresh copy from the final state which is currently displayed } else captureBlend(millis()); // capture current fade status and restart fade when toggling @@ -463,11 +469,13 @@ void Segment::stopTransition() { // sets transition progress variables (0-65535) based on time passed since transition start void Segment::updateTransitionProgress() const { if (isInTransition()) { - _t->_progress = _t->_fadeProgress = 0xFFFF; + _t->_spatialProgress = _t->_fadeProgress = 0xFFFFU; unsigned diff = millis() - _t->_spatialStart; - if (_t->_spatialDur > 0 && diff < _t->_spatialDur) _t->_progress = diff * 0xFFFFU / _t->_spatialDur; + if (_t->_spatialDur > 0 && diff < _t->_spatialDur) _t->_spatialProgress = diff * 0xFFFFU / _t->_spatialDur; + if (_t->_flags & TRANSITION_FLAG_REVERSED) _t->_spatialProgress = invertProgress(_t->_spatialProgress); diff = millis() - _t->_fadeStart; if (_t->_fadeDur > 0 && diff < _t->_fadeDur) _t->_fadeProgress = diff * 0xFFFFU / _t->_fadeDur; + // Fade transitions are never calculated reversed: instead we adjust the target and timing in startTranstion(.) } } @@ -865,14 +873,10 @@ bool Segment::isPixelClipped(int i, uint8_t style) const { if (len < 2) return false; unsigned shuffled = hashInt(i) % len; unsigned pos = (shuffled * 0xFFFFU) / len; - if (isTransitionReversed()) { - return (0xFFFFU - progress()) > pos; // invert progress and invert mask -> plays animation in reverse - } - return progress() <= pos; + return (spatialProgress() <= pos); } const bool iInside = (i >= start && i < stop); bool isClipped = !iInside ^ invert; // thanks @willmmiles (https://github.com/wled/WLED/pull/3877#discussion_r1554633876) - if (isTransitionReversed()) isClipped = !isClipped; return isClipped; } return false; @@ -1643,12 +1647,12 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { // slow path: handle transitions, grouping/spacing, segments with clipping and CCT pixels Segment::setClippingRect(0, 0); // disable clipping by default - unsigned progress = (style==TRANSITION_OUTSIDE_IN ? 0xFFFFU - topSegment.progress() : topSegment.progress()); - // play animation in reverse if requrested, clipping is inverted too (see isPixelClipped() functions), startTransition needs to takes care of flipping the timing - if (topSegment.isTransitionReversed()) progress = 0xFFFFU - progress; - const unsigned progInv = 0xFFFFU - progress; + unsigned progress = (style==TRANSITION_OUTSIDE_IN) ? Segment::invertProgress(topSegment.spatialProgress()) : topSegment.spatialProgress(); + unsigned progInv = Segment::invertProgress(progress); const unsigned dw = (progress * width) / 0xFFFFU + 1; const unsigned dh = (progress * height) / 0xFFFFU + 1; + const bool invertClipping = (style != TRANSITION_FADE) && topSegment.isTransitionReversed(); + // single pixel segments or transitions without a rendered old segment: use fade if (width*height == 1 || !segO) style = TRANSITION_FADE; switch (style) { @@ -1666,7 +1670,7 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { Segment::setClippingRect(width - dw, width, 0, height); break; case TRANSITION_OUTSIDE_IN: // corners - Segment::setClippingRect((width + dw)/2, (width - dw)/2, (height + dh)/2, (height - dh)/2); // inverted!! + Segment::setClippingRect((width + dw)/2, (width - dw)/2, (height + dh)/2, (height - dh)/2); // inverted! break; case TRANSITION_INSIDE_OUT: // outward Segment::setClippingRect((width - dw)/2, (width + dw)/2, (height - dh)/2, (height + dh)/2); @@ -1753,7 +1757,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++) { - bool clipped = topSegment.isPixelXYClipped(c, r, style); + bool clipped = topSegment.isPixelXYClipped(c, r, style) ^ invertClipping; 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 @@ -1824,7 +1828,7 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { unsigned offsetI = progInv * nLen / 0xFFFFU; for (int k = 0; k < nLen; k++) { - bool clipped = topSegment.isPixelClipped(k, style); + bool clipped = topSegment.isPixelClipped(k, style) ^ invertClipping; 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 From ef2d000593bfe8951630fffeb0a4347453f900f8 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Tue, 29 Sep 2026 01:34:01 +0000 Subject: [PATCH 17/19] Fix power-off timing Reorganize the brightness transition handling during power reversals to ensure the transition timing remains consistent. Avoid restarting the transition timer during fades, and ensure it's coterminous with any spatial transitions so the last pixel doesn't linger. H/T Claude for pointing out the corner case with changing duration during a reversal. Co-Authored-By: Claude Opus 5.5 --- wled00/led.cpp | 78 +++++++++++++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 29 deletions(-) diff --git a/wled00/led.cpp b/wled00/led.cpp index 8eedfac4f7..be3e3d492a 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -88,44 +88,64 @@ void handleBriChange() { applyFinalBri(); } else { uint32_t now = millis(); - if (blendingStyle != TRANSITION_FADE) { + + if ((!strip.isPoweringOn() && bri > 0 && (briOld == 0 || strip.isPoweringOff()))) { + // Brightness was increased from zero; treat as power on if we're not already + strip.setPowerFlag(TRANSITION_POWER_ON | TRANSITION_POWER_TRIGGER); + } else if (!strip.isPoweringOff() && bri == 0 && (briOld > 0 || strip.isPoweringOn())) { + // Brightness was decreased to zero; treat as power off if we're not already + strip.setPowerFlag(TRANSITION_POWER_OFF | TRANSITION_POWER_TRIGGER); + } + + // Was there a power state change? + if (strip.isPowerTrigger()) { if (strip.isPoweringOff() && strip.isPoweringOn()) { - // if both flags are set, the power state was reversed during transition, invert the transition time to keep "overall brightness" i.e number of lit LEDs + // if both flags are set, the power state was reversed during transition. We invert the transition. // note: segments do the same, timing to finish the transition matches (more or less), segment blending is held in spatial transition until global transition finishes. - int progress = now - transitionStartTime; int duration = strip.getTransition(); - transitionStartTime = now - (duration - progress); // invert transition progress - if (bri > 0) strip.clearPowerFlag(TRANSITION_POWER_OFF); - else strip.clearPowerFlag(TRANSITION_POWER_ON); + int elapsed = constrain((int)(now - transitionStartTime), 0, duration); // duration may have changed mid-flight + int progress = duration - elapsed; + int delta = (bri > 0 || blendingStyle == TRANSITION_FADE) ? (int)briT - (int)bri : 0; // spatial power-off doesn't fade + if (delta) { + int room = (delta > 0) ? 255 - bri : bri; // headroom for the fade start on briT's side of bri + elapsed = max(elapsed, (duration * abs(delta) + room - 1) / room); // target moved: stretch so the start stays in range + } + if (elapsed) briOld = bri + delta * duration / elapsed; // extrapolate from bri back through briT + transitionStartTime = now - (duration - elapsed); // invert progress + strip.clearPowerFlag((bri > 0) ? TRANSITION_POWER_OFF : TRANSITION_POWER_ON); // bri carries the target state + } + else + { + // Starting a power transition + if (strip.isPoweringOn()) { + // global power on + strip.setTransitionMode(false); // stop any transition that is going on while in off mode and start clean (a segment power on prior to global on will continue otherwise) + strip.restartRuntime(); // and restart any running effect when powering on + if (blendingStyle != TRANSITION_FADE) applyFinalBri(); // set brightness immediately, otherwise it will fade-in -> this does not yet work. need to set to bri old? or bri last? + } + briOld = briT; // Adopt the current state of any existing fade transition + transitionStartTime = now; // (Re)start global transition timer } - else if (strip.isPoweringOn() && strip.isPowerTrigger() || (bri > 0 && briOld == 0)) { - // global power on from off state either through power button or brightness change - strip.setPowerFlag(TRANSITION_POWER_ON | TRANSITION_POWER_TRIGGER); // if powering on by brightness change, set power flag to inite spatial transition (if set) - strip.setTransitionMode(false); // stop any transition that is going on while in off mode and start clean (a segment power on prior to global on will continue otherwise) - strip.restartRuntime(); // and restart any running effect when powering on - if (blendingStyle != TRANSITION_FADE) applyFinalBri();; // set brightness immediately, otherwise it will fade-in -> this does not yet work. need to set to bri old? or bri last? + + if (blendingStyle != TRANSITION_FADE) { // Note: fading is omitted if segments run a spatial power off transition, see handleTransitions() + strip.setTransitionMode(true); // force all segments to a spatial on/off transition, segments handle transition inversion (on during off or off during on) } - } - if (strip.isPoweringOff() && bri > 0) { - // powering off but brightness was changed -> switch to powering on, update is handled below - strip.clearPowerFlag(TRANSITION_POWER_OFF); - strip.setPowerFlag(TRANSITION_POWER_ON | TRANSITION_POWER_TRIGGER); - } + strip.clearPowerFlag(TRANSITION_POWER_TRIGGER); + } else { + if (bri == briOld) return; // no change found - // if brightness changed, start a new global transition but do not reset the timer if powering off (unless powering back on i.e. triggered) - // Note: fading is omitted if segments run a spatial power off transition, see handleTransitions() - if ((bri != briOld && !strip.isPoweringOff()) || strip.isPowerTrigger()) { - if (transitionActive) { - briOld = briT; // capture transition value: starts brightness fade from current value + // Purely a brightness change. + // Strictly speaking someone might have set brightness 0->0 so we have to ignore that case during power off. + if (!strip.isPoweringOff()) { + // Any existing transition re-starts from where we are + briOld = briT; + transitionStartTime = now; } - transitionActive = true; - transitionStartTime = now; // note: this only affects brightness fade, spatial transition continues as it is handled on segment level - } - if (blendingStyle != TRANSITION_FADE && (strip.isPoweringOn() || strip.isPoweringOff()) && strip.isPowerTrigger()) { - strip.setTransitionMode(true); // force all segments to a spatial on/off transition, segments handle transition inversion (on during off or off during on) } - strip.clearPowerFlag(TRANSITION_POWER_TRIGGER); + + // In all cases that arrive here, there is an active transition now + transitionActive = true; } } From 933bcedec5a24122dd676a0a82c22a8dc1bc465f Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 7 Oct 2026 13:19:17 +0200 Subject: [PATCH 18/19] bugfix: do not invert oldsegment on state anymore (no longer needed, generates immediate jump) --- wled00/FX_fcn.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index bcf50374b6..412f8856bf 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -409,7 +409,6 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { // segment-level on/off if (_t->_oldSegment) { if (strip.isPoweringOff()) _t->_flags ^= TRANSITION_FLAG_POWER_ON; // flip POWER_ON flag, it is flipped back below, we need it to stay off if a segment is turned on during global off - if (!strip.isPoweringOn()) _t->_oldSegment->on = !_t->_oldSegment->on; // invert old segment's on state (but do not turn old segment off so rendering continues) _t->_oldSegment->opacity = opacity; _t->_oldSegment->cct = cct; } From 78cbcafbb385298ad831ac4c88c1ddf9a07235aa Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 7 Oct 2026 16:24:38 +0200 Subject: [PATCH 19/19] fix parallel fading if multiple parameters change in a spatial "start transiton" --- wled00/FX_fcn.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 412f8856bf..8d4d4308c9 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -376,7 +376,8 @@ void Segment::startTransition(uint16_t dur, uint8_t kind) { captureBlend(millis()); // rebase fade channel to the current visual blend and restart it } } - else if (_t->_spatialProgress > 0 || _t->_fadeProgress < 0xFFFFU) { + else if (_t->_spatialProgress > 0 || _t->_oldSegment == nullptr) { + // note: if spatialProgress == 0, do not capture captureBlend(): for example an FX change also changes palette, we do not want that to fade in prallel if (!fadeTransitionActive() && _t->_oldSegment != nullptr) { // spatial transition with no fade running: enable fade and let the spatial transition continue. Need to capture the current "revealed" state i.e. copy segment colors to _t for (unsigned i = 0; i < NUM_COLORS; i++) _t->_colors[i] = colors[i]; // rebase transition colors&palette from current final state