diff --git a/wled00/FX.h b/wled00/FX.h index d545609aa7..8e93ff2109 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -405,6 +405,27 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define TRANSITION_PUSH_MASK 0x10 #define TRANSITION_COUNT 18 +// 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 +#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) + + typedef enum mapping1D2D { M12_Pixels = 0, @@ -502,24 +523,32 @@ 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 _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 transition in ms - uint16_t _progress; // transition progress (0-65535); pre-calculated from _start & _dur in updateTransitionProgress() + uint16_t _spatialDur; // duration of spatial channel in ms + uint16_t _fadeDur; // duration of fade channel in ms + 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 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 _flags; // TRANSITION_FLAG_* power state Transition(uint16_t dur=750) : _oldSegment(nullptr) - , _start(millis()) + , _spatialStart(millis()) + , _fadeStart(_spatialStart) , _colors{0,0,0} , _palT(CRGBPalette16()) - , _dur(dur) - , _progress(0) + , _spatialDur(dur) + , _fadeDur(dur) + , _spatialProgress(0) + , _fadeProgress(0) , _prevPaletteBlends(0) , _palette(0) , _bri(0) , _cct(0) + , _flags(0) {} ~Transition() { //DEBUGFX_PRINTF_P(PSTR("-- Destroying transition: %p\n"), this); @@ -543,13 +572,8 @@ 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() && progress() == 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; } - + 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 @@ -633,6 +657,15 @@ 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 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; } + 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 + 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; } @@ -679,7 +712,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 + // 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) @@ -696,7 +732,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; @@ -743,7 +779,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)); } @@ -780,7 +816,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); } @@ -843,6 +879,7 @@ class WS2812FX { _frametime(FRAMETIME_FIXED), _cumulativeFps(WLED_FPS << FPS_CALC_SHIFT), _targetFps(WLED_FPS), + _poweringOnOff(0), _isServicing(false), _isOffRefreshRequired(false), _hasWhiteChannel(false), @@ -911,7 +948,7 @@ class WS2812FX { inline void resume() { _suspend = false; } // will resume strip.service() execution void restartRuntime(); - void setTransitionMode(bool t); + void setTransitionMode(bool start); bool checkSegmentAlignment() const; bool hasRGBWBus() const; @@ -924,6 +961,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; @@ -947,7 +989,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 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; @@ -1024,6 +1066,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()) bool _isServicing; bool _isOffRefreshRequired; //periodic refresh is required for the strip to remain off. diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index eb7748b5c7..5766424588 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -148,39 +148,40 @@ 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; - return progress() <= pos; + return spatialProgress() <= 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 unsigned prog = out ? progress() : 0xFFFFU - progress(); + const bool out = (style == TRANSITION_CIRCULAR_OUT); + unsigned prog = out ? spatialProgress() : invertProgress(spatialProgress()); int radius2 = max(cx, cy) * prog / 0xFFFF; radius2 = 2 * radius2 * radius2; if (radius2 == 0) return 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 clip; } 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; - return !clip; + const bool notclipped = style == TRANSITION_OUTSIDE_IN ? xInside || yInside : xInside && yInside; + return !notclipped; } return false; } diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index cfbc09216e..8d4d4308c9 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -287,74 +287,176 @@ 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; +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) + const bool spatialDone = spatialProgress() == (_t->_flags & TRANSITION_FLAG_REVERSED ? 0 : 0xFFFFU); + if (_t->_oldSegment && spatialDone) { + delete _t->_oldSegment; _t->_oldSegment = nullptr; + } + if (spatialDone && fadeProgress() == 0xFFFFU) { + 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 (_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. + 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 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 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 +*/ + +// 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 (_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) + +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 + // 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); + // 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); + _t->_bri = currentBri(); + _t->_cct = currentCCT(); + _t->_prevPaletteBlends = 0; + _t->_fadeDur = dur; + _t->_fadeStart = fadeStart; + }; + + // 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()) { - 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; a running spatial transition continues to completion + 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 && _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->_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); + } 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->_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 + loadPalette(_t->_palT, palette); + } + captureBlend(millis()); // restart fade channel from the current visual state + 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; } - 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(); } - } 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, 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 (_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 + 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 + 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 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; + 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 + if (power == TRANSITION_POWER_TOGGLE) { + // 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 + _t->_oldSegment->opacity = opacity; + _t->_oldSegment->cct = cct; + } + } + _t->_flags ^= TRANSITION_FLAG_POWER_ON; // flip POWER_ON flag + } else { + // 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->_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->_spatialStart = 0; // disables the spatial channel and uses fade instead } - _t->_start = millis(); - _t->_dur = dur; - _t->_prevPaletteBlends = 0; + } else { + // FADE blending: the fade channel (restarted above) carries the power transition + _t->_spatialStart = 0; // FADE blending: disables the spatial channel and uses fade instead } + _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) { - _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 non-fade power transition, do not fade anything but reveal the final state + } _t->_cct = cct; _t->_palette = palette; - 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) createOldSegment(0xFFFFU); // 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 { + _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); } - }; + } } void Segment::stopTransition() { @@ -364,41 +466,43 @@ 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; - unsigned diff = millis() - _t->_start; - if (_t->_dur > 0 && diff < _t->_dur) _t->_progress = diff * 0xFFFFU / _t->_dur; + _t->_spatialProgress = _t->_fadeProgress = 0xFFFFU; + unsigned diff = millis() - _t->_spatialStart; + 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(.) } } // 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) { - if (blendingStyle == TRANSITION_FADE) 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) - if (blendingStyle == TRANSITION_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(); @@ -406,7 +510,9 @@ void Segment::beginDraw(uint16_t prog) { 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&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 @@ -562,7 +668,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.getTransition(), TRANSITION_KIND_DEFAULT); // start transition prior to change colors[slot] = c; stateChanged = true; // send UDP/WS broadcast return *this; @@ -576,7 +682,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.getTransition(), TRANSITION_KIND_FADE); // start transition prior to change (no need to copy segment) cct = k; stateChanged = true; // send UDP/WS broadcast } @@ -585,8 +691,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.getTransition(), blendingStyle != TRANSITION_FADE); // start transition prior to change + 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 } @@ -597,7 +703,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.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 @@ -610,7 +716,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.getTransition(), TRANSITION_KIND_EFFECT); // set effect transitions (always needs a segment copy for blending) mode = fx; int sOpt; // load default values from effect string @@ -650,7 +756,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.getTransition(), TRANSITION_KIND_DEFAULT); // start transition prior to change palette = pal; stateChanged = true; // send UDP/WS broadcast } @@ -664,7 +770,7 @@ Segment &Segment::setName(const char *newName) { char *newBuf = static_cast(allocate_buffer(newLen+1, BFRALLOC_PREFER_PSRAM)); if (newBuf) { strlcpy(newBuf, newName, newLen+1); - 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.getTransition(), TRANSITION_KIND_EFFECT); // if the name changes in scrolling text mode, we need to copy the segment for blending char *oldName = name; name = newBuf; if (oldName) p_free(oldName); @@ -757,20 +863,21 @@ 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; - return progress() <= pos; + return (spatialProgress() <= 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) + return isClipped; } return false; } @@ -1364,8 +1471,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) @@ -1376,6 +1483,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(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 @@ -1472,14 +1580,19 @@ 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(); + const Segment *segO = topSegment.getOldSegment(); + 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) + opacityOld = gamma8inv(opacityOld); + } 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 @@ -1534,13 +1647,15 @@ 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(); - 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; - if (width*height == 1) blendingStyle = TRANSITION_FADE; // disable style for single pixel segments (use fade instead) - switch (blendingStyle) { + 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) { 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()) @@ -1555,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); @@ -1624,13 +1739,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; @@ -1642,7 +1757,8 @@ 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) ^ 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 int vCols = seg == segO ? oCols : nCols; // old segment may have different dimensions @@ -1653,19 +1769,14 @@ 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) { - // 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 (topSegment.isInTransition() && (briOld == 0 || bri == 0) && ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri))) c_a = BLACK; + } 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; } // map it into frame buffer x = c; // restore coordiates if we were PUSHing @@ -1677,7 +1788,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 @@ -1686,7 +1797,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++; } } @@ -1717,29 +1828,25 @@ 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) ^ 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 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) { - // 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 (topSegment.isInTransition() && (briOld == 0 || bri == 0) && ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri))) c_a = BLACK; + } 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; } // map into frame buffer i = k; // restore index if we were PUSHing @@ -1748,11 +1855,10 @@ 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); } } - blendingStyle = orgBS; Segment::setClippingRect(0, 0); // disable clipping for overlays } @@ -1840,11 +1946,14 @@ void WS2812FX::restartRuntime() { resume(); } -// start or stop transition for all segments -void WS2812FX::setTransitionMode(bool t) { +// start global power on/off or stop transition for all segments +void WS2812FX::setTransitionMode(bool start) { suspend(); waitForIt(); - for (Segment &seg : _segments) seg.startTransition(t ? _transitionDur : 0); + for (Segment &seg : _segments) { + 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(); } @@ -1882,8 +1991,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 131ff95bab..be3e3d492a 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -41,16 +41,16 @@ void applyValuesToSelectedSegs() { void toggleOnOff() { - if (bri == 0) - { + 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; + strip.setPowerFlag(TRANSITION_POWER_OFF | TRANSITION_POWER_TRIGGER); } - stateChanged = true; + stateChanged = true; // note: if needed, stateUpdated() will start the global on/off transition } @@ -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,6 +78,76 @@ void applyFinalBri() { strip.trigger(); // force one last update } +// 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; + strip.clearPowerFlag(0xFF); // clear all power flags + applyFinalBri(); + } else { + uint32_t now = millis(); + + 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. 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 duration = strip.getTransition(); + 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 + } + + 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) + } + + strip.clearPowerFlag(TRANSITION_POWER_TRIGGER); + } else { + if (bri == briOld) return; // no change found + + // 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; + } + } + + // In all cases that arrive here, there is an active transition now + transitionActive = true; + } +} //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 @@ -122,18 +189,8 @@ void stateUpdated(byte callMode) { // notify usermods of state change UsermodManager::onStateChange(callMode); - if (strip.getTransition() == 0) { - jsonTransitionOnce = false; - transitionActive = false; - applyFinalBri(); - } else { - if (transitionActive) { - briOld = briT; - } else if (bri != briOld || stateChanged) - strip.setTransitionMode(true); // force all segments to transition mode - transitionActive = true; - transitionStartTime = now; - } + handleBriChange(); // check if a global brightness changed and start/update transition if needed + stateChanged = false; } @@ -158,16 +215,18 @@ 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.getTransition() > 0) { - int ti = millis() - transitionStartTime; - int tr = strip.getTransition(); - if (ti/tr) { - strip.setTransitionMode(false); // stop all transitions + // 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) { + int progress = millis() - transitionStartTime; + int duration = strip.getTransition(); + // finalize once the transition time has elapsed + 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; @@ -175,10 +234,14 @@ void handleTransitions() { applyFinalBri(); return; } - byte briTO = briT; - int deltaBri = (int)bri - (int)briOld; - briT = briOld + (deltaBri * ti / tr); - if (briTO != briT) applyBri(); + // 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 which then will fade + if (!strip.isPoweringOff() || blendingStyle == TRANSITION_FADE) { + byte briTO = briT; + int deltaBri = (int)bri - (int)briOld; + briT = briOld + (deltaBri * progress / duration); + if (briTO != briT) applyBri(); + } } }