Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d42a0ab
properly apply opacity transitions in segment blending
DedeHai Jul 9, 2026
72692e5
use gamma8inv conditionally
DedeHai Jul 9, 2026
c7e9fba
do not fade global brightness on non-fade on/off
DedeHai Jul 20, 2026
704de03
fix on/off transitions, properly apply transition to opacity/CCT chan…
DedeHai Aug 20, 2026
3a68818
Merge branch 'main' into opacity_SegBlending_fix
DedeHai Aug 20, 2026
23d7580
partial fix for on/off transitions
DedeHai Aug 20, 2026
174a6eb
WIP: rework on transitions using states, mostly working but on off is…
DedeHai Aug 27, 2026
2992c58
most special cases work now, needs more testing and cleanup
DedeHai Sep 5, 2026
04362ce
bugfixes and defer to fading if spatial channel is used
DedeHai Sep 6, 2026
aa1a535
simplify logic somewhat, cleanup
DedeHai Sep 7, 2026
5ec1e88
remove debug prints
DedeHai Sep 7, 2026
c8c0673
Merge branch 'main' into opacity_SegBlending_fix
DedeHai Sep 7, 2026
3468367
bugfix
DedeHai Sep 7, 2026
b9213eb
Merge branch 'main' into opacity_SegBlending_fix
DedeHai Sep 26, 2026
ca6ee26
rename _start and _dur to _spatialX to make intent clearer
DedeHai Sep 26, 2026
aa544aa
invert on/off spatial animation instead of flipping it
DedeHai Sep 26, 2026
399d1bc
Fix reversal of 2D animations, fully take care of reversal in clippin…
DedeHai Sep 27, 2026
f1920ee
remove unused commented code, fix rabbit findings
DedeHai Sep 27, 2026
c78e772
Simplify transition inversion
willmmiles Sep 28, 2026
ef2d000
Fix power-off timing
willmmiles Sep 29, 2026
933bced
bugfix: do not invert oldsegment on state anymore (no longer needed, …
DedeHai Oct 7, 2026
78cbcaf
fix parallel fading if multiple parameters change in a spatial "start…
DedeHai Oct 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 64 additions & 21 deletions wled00/FX.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -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; }
Expand Down Expand Up @@ -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)

Expand All @@ -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;
Expand Down Expand Up @@ -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)); }
Expand Down Expand Up @@ -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); }
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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.
Expand Down
21 changes: 11 additions & 10 deletions wled00/FX_2Dfcn.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Loading
Loading