diff --git a/wled00/FX.h b/wled00/FX.h index 90e5d98189..7a51eee762 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -405,6 +405,26 @@ 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" + + typedef enum mapping1D2D { M12_Pixels = 0, @@ -502,24 +522,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 _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 _flags; // TRANSITION_FLAG_* power state Transition(uint16_t dur=750) : _oldSegment(nullptr) , _start(millis()) + , _fadeStart(_start) , _colors{0,0,0} , _palT(CRGBPalette16()) , _dur(dur) + , _fadeDur(dur) , _progress(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 +571,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() && 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 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 +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->_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; } + 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 inline bool isActive() const { return stop > start && pixels; } inline bool hasRGB() const { return _isRGB; } inline bool hasWhite() const { return _hasW; } @@ -679,7 +709,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) @@ -842,6 +875,7 @@ class WS2812FX { _frametime(FRAMETIME_FIXED), _cumulativeFps(WLED_FPS << FPS_CALC_SHIFT), _targetFps(WLED_FPS), + _poweringOnOff(0), _isServicing(false), _isOffRefreshRequired(false), _hasWhiteChannel(false), @@ -910,7 +944,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; @@ -923,6 +957,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; @@ -946,7 +985,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; @@ -1025,6 +1064,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 cfbc09216e..c7d5974fbf 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -287,74 +287,172 @@ 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) + if (_t->_oldSegment && _t->_progress == 0xFFFFU && !strip.isPoweringOff()) { + delete _t->_oldSegment; _t->_oldSegment = nullptr; + } + if (progress() == 0xFFFFU && 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 (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 + - 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 (_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) + +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; + }; + // 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()) { - 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->_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 } - 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 + 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); } - _t->_start = millis(); - _t->_dur = dur; - _t->_prevPaletteBlends = 0; + 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 } + 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 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 = 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 + 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; + } + } + _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->_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); } 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->_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); } - }; + } } void Segment::stopTransition() { @@ -364,41 +462,41 @@ 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) { - 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 +504,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 @@ -414,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 } } @@ -562,7 +662,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 +676,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 +685,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 +697,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 +710,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 +750,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 +764,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); @@ -1364,8 +1464,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 +1476,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,10 +1573,14 @@ 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(); + 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 @@ -1539,7 +1644,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) blendingStyle = TRANSITION_FADE; // disable style for single pixel segments (use fade instead) + // 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()) @@ -1643,6 +1749,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 @@ -1659,13 +1766,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 - // (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; + // 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 +1779,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 +1788,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++; } } @@ -1718,6 +1820,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; @@ -1733,13 +1836,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(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; + // 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,7 +1846,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); } } @@ -1840,11 +1938,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 +1983,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..e92a17610f 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,55 @@ 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; + 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 + // 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 (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); + } + + // 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; // 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); + } +} //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 +168,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 +194,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 +213,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(); + } } }