-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
properly apply opacity transitions in segment blending #5729
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d42a0ab
72692e5
c7e9fba
704de03
3a68818
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -324,9 +324,10 @@ void Segment::startTransition(uint16_t dur, bool segmentCopy) { | |
| for (unsigned i = 0; i < NUM_COLORS; i++) _t->_colors[i] = color_blend16(_t->_colors[i], colors[i], _t->_progress); | ||
| _t->_bri = currentBri(); // update "original" brightness note: _t->_progress is updated in updateTransitionProgress() so still valid here | ||
| _t->_cct = currentCCT(); // update "original" CCT (reduces jump) | ||
| // restart transition timer only if a pure FADE transition, otherwise let the FX change or non-FADE transition finish | ||
| // restart transition timer only if a pure FADE transition or a transition without segment copy (opacity/CCT change), | ||
| // otherwise let the FX change or non-FADE transition finish | ||
| // this avoids a re-start of the transition if color or brightness is changed during an ongoing FX or non-FADE transition | ||
| if (blendingStyle == TRANSITION_FADE) { | ||
| if (blendingStyle == TRANSITION_FADE || _t->_oldSegment == nullptr) { | ||
| if (_t->_oldSegment != nullptr) { | ||
| if (_t->_oldSegment->mode != mode) | ||
| return; // do not reset transition if this is an FX change, note: the disadvantage is that colors still jump in that case | ||
|
|
@@ -378,7 +379,8 @@ void Segment::updateTransitionProgress() const { | |
| uint8_t Segment::currentCCT() const { | ||
| unsigned prog = progress(); | ||
| if (prog < 0xFFFFU) { | ||
| if (blendingStyle == TRANSITION_FADE) return (cct * prog + (_t->_cct * (0xFFFFU - prog))) / 0xFFFFU; | ||
| // fade if style is FADE or if the transition has no old segment (opacity or CCT transition) | ||
| if (blendingStyle == TRANSITION_FADE || _t->_oldSegment == nullptr) return (cct * prog + (_t->_cct * (0xFFFFU - prog))) / 0xFFFFU; | ||
| //else return Segment::isPreviousMode() ? _t->_cct : cct; | ||
| } | ||
| return cct; | ||
|
|
@@ -390,8 +392,9 @@ uint8_t Segment::currentBri() const { | |
| unsigned curBri = on ? opacity : 0; | ||
| if (prog < 0xFFFFU) { | ||
| // this will blend opacity in new mode if style is FADE (single effect call) | ||
| if (blendingStyle == TRANSITION_FADE) curBri = (prog * curBri + _t->_bri * (0xFFFFU - prog)) / 0xFFFFU; | ||
| else curBri = Segment::isPreviousMode() ? _t->_bri : curBri; | ||
| // or if the transition has no old segment (opacity or CCT transition) | ||
| if (blendingStyle == TRANSITION_FADE || _t->_oldSegment == nullptr) curBri = (prog * curBri + _t->_bri * (0xFFFFU - prog)) / 0xFFFFU; | ||
| else curBri = Segment::isPreviousMode() ? _t->_bri : curBri; | ||
| } | ||
| return curBri; | ||
| } | ||
|
|
@@ -557,7 +560,7 @@ Segment &Segment::setCCT(uint16_t k) { | |
| Segment &Segment::setOpacity(uint8_t o) { | ||
| if (opacity != o) { | ||
| //DEBUG_PRINTF_P(PSTR("- Starting opacity transition: %d\n"), o); | ||
| startTransition(strip.getTransition(), blendingStyle != TRANSITION_FADE); // start transition prior to change | ||
| startTransition(strip.getTransition(), false); // opacity change always fades (no segment copy needed) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Keep global non-FADE on/off rendering out of the forced-FADE path. When a segment is already in a no-copy opacity or CCT transition, Line 1833 leaves Preserve the selected non-FADE mode for the global on/off condition, or add the blacking path before the Suggested guard- if (width*height == 1 || !segO) blendingStyle = TRANSITION_FADE;
+ if (width*height == 1 ||
+ (!segO && !(transitionActive && (briOld == 0 || bri == 0)))) {
+ blendingStyle = TRANSITION_FADE;
+ }Also applies to: 1528-1528, 1831-1835 🤖 Prompt for AI Agents |
||
| opacity = o; | ||
| stateChanged = true; // send UDP/WS broadcast | ||
| } | ||
|
|
@@ -1439,10 +1442,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(); | ||
| 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 | ||
|
|
@@ -1506,7 +1513,7 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { | |
| const unsigned dw = (blendingStyle==TRANSITION_OUTSIDE_IN ? progInv : progress) * width / 0xFFFFU + 1; | ||
| const unsigned dh = (blendingStyle==TRANSITION_OUTSIDE_IN ? progInv : progress) * height / 0xFFFFU + 1; | ||
| const unsigned orgBS = blendingStyle; | ||
| if (width*height == 1) blendingStyle = TRANSITION_FADE; // disable style for single pixel segments (use fade instead) | ||
| if (width*height == 1 || !segO) blendingStyle = TRANSITION_FADE; // single pixel segments or opacity/CCT transition: use fade | ||
| switch (blendingStyle) { | ||
| case TRANSITION_CIRCULAR_IN: // (must set entire segment, see isPixelXYClipped()) | ||
| case TRANSITION_CIRCULAR_OUT:// (must set entire segment, see isPixelXYClipped()) | ||
|
|
@@ -1610,6 +1617,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 | ||
|
|
@@ -1627,12 +1635,14 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { | |
| c_a = color_blend16(c_a, segO->getPixelColorRaw(x + y*oCols), progInv); | ||
| } else if (blendingStyle != TRANSITION_FADE) { | ||
| // if we have global brightness change (not On/Off change) we will ignore transition style and just fade brightness (see led.cpp) | ||
| // workaround for On/Off transition | ||
| // (bri != briT) && !bri => from On to Off | ||
| // (bri != briT) && bri => from Off to On | ||
| // workaround for On/Off transition (applies while a global on/off transition is active) | ||
| // transitionActive && !bri => from On to Off | ||
| // transitionActive && bri => from Off to On | ||
| if ((briOld == 0 || bri == 0) && ((!clipped && transitionActive && !bri) || (clipped && transitionActive && bri))) c_a = BLACK; | ||
| // note: only blank pixels once the segment transition has actually started; bri changes before | ||
| // startTransition() is called (stateUpdated()) and a frame rendered in that window would blank the whole segment | ||
| if (topSegment.isInTransition() && (briOld == 0 || bri == 0) && ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri))) c_a = BLACK; | ||
| //if (topSegment.isInTransition() && (briOld == 0 || bri == 0) && ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri))) c_a = BLACK; | ||
| //if (topSegment.isInTransition() && (briOld == 0 || bri == 0) && ((!clipped && transitionActive && !bri) || (clipped && transitionActive && bri))) c_a = BLACK; | ||
| } | ||
| // map it into frame buffer | ||
| x = c; // restore coordiates if we were PUSHing | ||
|
|
@@ -1644,7 +1654,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 | ||
|
|
@@ -1653,7 +1663,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++; | ||
| } | ||
| } | ||
|
|
@@ -1685,6 +1695,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; | ||
|
|
@@ -1704,9 +1715,11 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { | |
| // workaround for On/Off transition | ||
| // (bri != briT) && !bri => from On to Off | ||
| // (bri != briT) && bri => from Off to On | ||
| if ((briOld == 0 || bri == 0) && ((!clipped && transitionActive && !bri) || (clipped && transitionActive && bri))) c_a = BLACK; | ||
| // note: only blank pixels once the segment transition has actually started; bri changes before | ||
| // startTransition() is called (stateUpdated()) and a frame rendered in that window would blank the whole segment | ||
| if (topSegment.isInTransition() && (briOld == 0 || bri == 0) && ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri))) c_a = BLACK; | ||
| //if (topSegment.isInTransition() && (briOld == 0 || bri == 0) && ((!clipped && (bri != briT) && !bri) || (clipped && (bri != briT) && bri))) c_a = BLACK; | ||
| //if (topSegment.isInTransition() && (briOld == 0 || bri == 0) && ((!clipped && transitionActive && !bri) || (clipped && transitionActive && bri))) c_a = BLACK; | ||
| } | ||
| // map into frame buffer | ||
| i = k; // restore index if we were PUSHing | ||
|
|
@@ -1715,7 +1728,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); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -1811,7 +1824,11 @@ void WS2812FX::restartRuntime() { | |
| void WS2812FX::setTransitionMode(bool t) { | ||
| suspend(); | ||
| waitForIt(); | ||
| for (Segment &seg : _segments) seg.startTransition(t ? _transitionDur : 0); | ||
| for (Segment &seg : _segments) { | ||
| // do not interrupt transitions without segment copy i.e. opacity/CCT change or FADE | ||
| if (t && seg.isInTransition() && !seg.getOldSegment()) continue; | ||
| seg.startTransition(t ? _transitionDur : 0); | ||
| } | ||
| resume(); | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Reset transition timing for rapid on/off retriggers.
When
_t->_oldSegmentexists andblendingStyle != TRANSITION_FADE, this condition is false. A retrigger can update_t->_bribut leave_t->_startand_t->_durfrom the first transition. If the elapsed time already exceeds_dur, the next service tick completes the transition immediately instead of rendering the new transition. The_progress == 0path skips the refresh entirely.Reset
_t->_start,_t->_dur, and the current visible brightness for on/off retriggers. Preserve the no-restart behavior only for changes that must let an existing effect transition finish.Based on learnings: “when
isInTransition()is true and_oldSegmentalready exists, reset_t->_start,_t->_dur, and_t->_brifor rapid successive toggles.”🤖 Prompt for AI Agents
Source: Learnings
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in what exact scenario would this pose an issue, what are the consequences and how would I test that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@DedeHai, the exact case is a segment on/off transition with a non-FADE transition mode.The second change updates the target state, but it does not restart the copied non-FADE transition timer. Therefore, the second transition uses only the remaining time from the first transition. If the first timer has effectively expired before the next render, the segment can change to the new state on that render without a visible full-duration transition.
You can test the pre-existing behavior with a 5-second Wipe or Push transition. Turn a segment off, wait about 4 seconds, then turn it on. The return transition should complete in about 1 second instead of taking 5 seconds.
However, this behavior is not caused by this diff. Copied non-FADE transitions have
_t->_oldSegment != nullptr, so the new condition remains false for them. This change only restarts transitions without an old segment, which covers opacity and CCT transitions. The original finding incorrectly treated the existing copied-transition policy as a regression from this change.🐇🔎
✅ Review thread resolved.
You are interacting with an AI system.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what fix do you propose?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@DedeHai, I do not propose a code change for this PR.A timer-only reset is not safe. During a copied non-FADE transition,
_oldSegmentand the current segment represent the original transition endpoints. Resetting_t->_startand_t->_durwould restart clipping against those endpoints. It would not use the partially rendered output as the new source state. This can cause a visible jump.A correct change would require separate transition semantics for rapid on/off retriggers. It must capture the current composited segment output, then create a new transition from that output. The existing
Segmentcopy does not contain this composited state for Wipe, Push, and similar modes.The behavior is pre-existing and is outside this PR’s opacity-transition change. The original finding should be withdrawn.
🧠 Learnings used
✅ Review thread resolved.
You are interacting with an AI system.