Skip to content

Add slow transition FX#5379

Open
DedeHai wants to merge 3 commits intowled:mainfrom
DedeHai:slowTransitionFX
Open

Add slow transition FX#5379
DedeHai wants to merge 3 commits intowled:mainfrom
DedeHai:slowTransitionFX

Conversation

@DedeHai
Copy link
Collaborator

@DedeHai DedeHai commented Feb 18, 2026

A new effect that is purely made for very slow transitions, up to 255 minutes.
It also allows to call a preset after the transition is done by setting intensity slider to the preset number to be applied after the transition has finished.
Using segment layering, this also allows it to be a "mask" although that takes a bit of tinkering with the preset json.

One scenario possible with this effect would be this:

  • start with a preset using this FX, say fully black, set the next preset to one with the same FX but that one at full brightness at a warm white with a transition time of 60 minutes
  • the LEDs will transition from black to warm white over 60 mintues, at the end it sets another preset "cold white"
  • the "cold white" preset transitions to full bright daylight white in another 60 minutes

The FX only transitions palettes (colors if palette 0 or any color derived palette is used), brightness slider transitions normally i.e. fast. If a color/palette is changed mid transition, it starts a new transition towards that color with the current state as a starting point so transitions never jump.

p.s.
I also remvoed the return value from Segment::loadPalette()` as the target palette is passed by reference and the return value is never even used anymore.

Fixes #5375

Summary by CodeRabbit

  • New Features
    • Added "Slow Transition" LED effect for smooth palette blending and gradual transitions
    • Supports sweep mode and automatic preset application configurable via intensity settings
    • Includes adjustable transition speed controls

@netmindz
Copy link
Member

Not tried it, but sounds exactly what one of my customers wanted from my lamps

@DedeHai
Copy link
Collaborator Author

DedeHai commented Feb 20, 2026

it also was requested somewhere before, I saw it more than once but could not find the references.
Its not the perfect solution and the "call preset after fade" is a bit of a hack or at least not strictly future proof as an FX basically has no business calling presets but I am not aware of anything breaking if it does it.
this probably needs some more testing on systems that use mqtt or sync or other means of external communication. I tested only on a confined device and there it works quite well.

In a nutshell this is the "sunrise FX" but instead of a fixed sunrise pattern, it allows to use any palette/color as a starting point and as a destination. I am not sure it is very easy to use though: if you just call it once it does nothing but initialize, so you have to use it either in a playlist or in a "chain of presets"

@softhack007 softhack007 modified the milestone: 16.0.0 beta Mar 9, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 20, 2026

Walkthrough

This PR introduces a new "Slow Transition" effect that enables gradual palette transitions over extended durations with configurable timing. It also modifies the Segment::loadPalette() method signature to return void instead of CRGBPalette16&.

Changes

Cohort / File(s) Summary
Slow Transition Effect Implementation
wled00/FX.cpp
Added mode_slow_transition() function that manages palette transitions via persistent per-segment data (SlowTransitionData), tracks elapsed time and progress, interpolates between palettes, and optionally applies a preset upon completion.
Effect Registration & Constants
wled00/FX.h
Added new effect constant FX_MODE_SLOW_TRANSITION (219) and incremented MODE_COUNT to 220; changed Segment::loadPalette() return type from CRGBPalette16& to void.
Method Signature Update
wled00/FX_fcn.cpp
Updated Segment::loadPalette() implementation to return void instead of CRGBPalette16&; fixed inline comment typo from "_deault_palette" to "_default_palette".

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • PR #4506 — Also modifies FX-related symbols and Segment palette loading logic, directly intersecting with the loadPalette signature change in this PR.

Suggested reviewers

  • DedeHai
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add slow transition FX' accurately describes the main change: introduction of a new effect called 'Slow Transition' for performing very slow palette/color transitions.
Linked Issues check ✅ Passed The PR fully implements the requirements from #5375: speed slider controls transition duration in minutes, palette/color blending is supported, states update while rendering, mid-transition changes reset the baseline, and preset selection on completion is included.
Out of Scope Changes check ✅ Passed The PR contains a minimal change to Segment::loadPalette() return type (unused return value removed); this refactoring is a necessary supporting change and not out of scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📋 Issue Planner

Built with CodeRabbit's Coding Plans for faster development and fewer bugs.

View plan used: #5375

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can make CodeRabbit's review stricter and more nitpicky using the `assertive` profile, if that's what you prefer.

Change the reviews.profile setting to assertive to make CodeRabbit's nitpick more issues in your PRs.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wled00/FX.cpp`:
- Around line 10830-10836: The transition uses the sync-adjusted clock
(strip.now) causing jumps; change all uses of strip.now in this sunrise-like
effect to the real-time millis() clock: when initializing the timer set
*startTime = millis(); compute current = millis(); then use elapsed = current -
*startTime (with uint32_t arithmetic) to calculate expectedSteps (leave
totalSteps and duration logic unchanged). Replace references to strip.now in the
startTime assignment and the elapsed calculation so this mode mirrors
mode_sunrise() behavior and is not affected by strip time sync adjustments.
- Around line 10797-10801: The preset handoff currently repeatedly calls
applyPreset() each render after the fade completes; modify the handoff code that
uses slow_transition_data (SlowTransitionData / slow_transition_data) so it sets
a one-shot latch (e.g., a boolean flag) when the transition finishes, invokes
applyPreset() only once, and immediately return from the render path after
performing that single handoff to avoid continued rendering with stale data;
apply the same one-shot latch + early-return pattern to the other two handoff
blocks referenced (the similar blocks around the other locations).
- Around line 10839-10849: The sweep branch (when SEGMENT.check2 is true) jumps
*stepsDone to expectedSteps and only applies the final microstep, losing
intermediate palette-entry updates; change the logic in the block using
*stepsDone, expectedSteps, data->currentPalette, data->startPalette,
data->endPalette, CRGB/CRGBW and color_blend so that you advance step-by-step
for any skipped microsteps: iterate from the previous stepsDone+1 up to
expectedSteps and for each step compute i = step % 16 and blendAmount = step /
16 and update data->currentPalette[i] accordingly (instead of applying only the
final step), then set *stepsDone = expectedSteps after the loop.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c313c770-8556-4149-b475-3255b2f43376

📥 Commits

Reviewing files that changed from the base of the PR and between a721264 and de05335.

📒 Files selected for processing (3)
  • wled00/FX.cpp
  • wled00/FX.h
  • wled00/FX_fcn.cpp

Comment on lines +10797 to +10801
typedef struct SlowTransitionData {
CRGBPalette16 startPalette; // initial palette
CRGBPalette16 currentPalette; // blended palette for current frame, need permanent storage so we can start from this if target changes mid transition
CRGBPalette16 endPalette; // target palette
} slow_transition_data;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Make the preset handoff one-shot and stop rendering after it.

Once the fade completes, this path can invoke applyPreset() on every render until something else resets the effect, and it then keeps drawing with the pre-preset data snapshot after mutating segment state. Add a latch and return immediately after the handoff.

Suggested fix
 typedef struct SlowTransitionData {
   CRGBPalette16 startPalette;        // initial palette
   CRGBPalette16 currentPalette;      // blended palette for current frame, need permanent storage so we can start from this if target changes mid transition
   CRGBPalette16 endPalette;          // target palette
+  bool presetApplied;                // ensure applyPreset() only runs once per completed transition
 } slow_transition_data;
@@
   if (changed || SEGMENT.call == 0) {
     if (SEGMENT.call == 0) {
       //data->presetapplied = false;
       data->startPalette = SEGPALETTE;
       data->currentPalette = SEGPALETTE;
       data->endPalette = SEGPALETTE;
       *stepsDone = 0xFFFF; // set to max, fading will start once a change is detected
     }
     else {
       data->startPalette = data->currentPalette;
       data->endPalette = SEGPALETTE;
       *stepsDone = 0; // reset counter
     }
+    data->presetApplied = false;
     *startSpeed = SEGMENT.speed;
     *startTime = strip.now; // set start time
   }
@@
     if (*stepsDone >= totalSteps) {
       // transition complete, apply preset set by intensity slider (if any)
       data->currentPalette = data->endPalette; // set to end palette (sweep may not have set all entries)
-      if (SEGMENT.intensity > 0) {
+      if (SEGMENT.intensity > 0 && !data->presetApplied) {
         uint8_t targetPreset = SEGMENT.intensity;
+        data->presetApplied = true;
         applyPreset(targetPreset, CALL_MODE_DIRECT_CHANGE); // apply preset (if it exists)
+        return;
       }
     }
   }

Also applies to: 10816-10829, 10856-10869

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/FX.cpp` around lines 10797 - 10801, The preset handoff currently
repeatedly calls applyPreset() each render after the fade completes; modify the
handoff code that uses slow_transition_data (SlowTransitionData /
slow_transition_data) so it sets a one-shot latch (e.g., a boolean flag) when
the transition finishes, invokes applyPreset() only once, and immediately return
from the render path after performing that single handoff to avoid continued
rendering with stale data; apply the same one-shot latch + early-return pattern
to the other two handoff blocks referenced (the similar blocks around the other
locations).

Comment on lines +10830 to +10836
*startTime = strip.now; // set start time
}

uint32_t totalSteps = SEGMENT.check2 ? 16 * 255 : 255;
uint32_t duration = (SEGMENT.speed == 0) ? 10000 : (uint32_t)SEGMENT.speed * 60000; // 10s if zero (good for testing), otherwise map 1-255 to 1-255 minutes
uint32_t elapsed = strip.now - *startTime; // note: will overflow after ~50 days if just left alone (edge case unhandled)
uint32_t expectedSteps = (uint64_t)elapsed * totalSteps / duration;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use millis() for the transition timer.

strip.now is the sync-adjusted animation clock in WLED. That is fine for frame-synced FX, but this mode measures real minutes, so a sync jump can fast-forward or rewind the transition. mode_sunrise() already avoids that in this file for the same reason.

Suggested fix
-    *startTime = strip.now; // set start time
+    *startTime = millis(); // keep minute-scale timing independent from FX sync
   }
 
   uint32_t totalSteps = SEGMENT.check2 ? 16 * 255 : 255;
   uint32_t duration = (SEGMENT.speed == 0) ? 10000 : (uint32_t)SEGMENT.speed * 60000; // 10s if zero (good for testing), otherwise map 1-255 to 1-255 minutes
-  uint32_t elapsed = strip.now - *startTime; // note: will overflow after ~50 days if just left alone (edge case unhandled)
+  uint32_t elapsed = millis() - *startTime;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/FX.cpp` around lines 10830 - 10836, The transition uses the
sync-adjusted clock (strip.now) causing jumps; change all uses of strip.now in
this sunrise-like effect to the real-time millis() clock: when initializing the
timer set *startTime = millis(); compute current = millis(); then use elapsed =
current - *startTime (with uint32_t arithmetic) to calculate expectedSteps
(leave totalSteps and duration logic unchanged). Replace references to strip.now
in the startTime assignment and the elapsed calculation so this mode mirrors
mode_sunrise() behavior and is not affected by strip time sync adjustments.

Comment on lines +10839 to +10849
if (*stepsDone > expectedSteps)
*stepsDone = expectedSteps;// in case sweep was disabled mid transition

if (*stepsDone < expectedSteps) {
*stepsDone = expectedSteps; // jump to expected steps to make sure timing is correct (need up to 4080 frames, at 20fps that is ~200 seconds)
if (SEGMENT.check2) {
// sweep: one palette entry at a time
uint8_t i = *stepsDone % 16;
uint8_t blendAmount = *stepsDone / 16;
data->currentPalette[i] = CRGB(color_blend(CRGBW(data->startPalette[i]), CRGBW(data->endPalette[i]), blendAmount));
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Sweep mode drops skipped microsteps.

When SEGMENT.check2 is enabled, the transition has 4080 microsteps. expectedSteps can advance by more than one between renders, but this branch jumps straight to the final value and applies only the last microstep, so the skipped palette-entry updates are lost and the sweep no longer matches elapsed time.

Suggested fix
-  if (*stepsDone > expectedSteps)
-    *stepsDone = expectedSteps;// in case sweep was disabled mid transition
-
-  if (*stepsDone < expectedSteps) {
-    *stepsDone = expectedSteps; // jump to expected steps to make sure timing is correct (need up to 4080 frames, at 20fps that is ~200 seconds)
+  if (*stepsDone != expectedSteps) {
     if (SEGMENT.check2) {
-        // sweep: one palette entry at a time
-        uint8_t i = *stepsDone % 16;
-        uint8_t blendAmount  = *stepsDone / 16;
-        data->currentPalette[i] = CRGB(color_blend(CRGBW(data->startPalette[i]), CRGBW(data->endPalette[i]), blendAmount));
+      while (*stepsDone < expectedSteps) {
+        ++(*stepsDone);
+        uint8_t i = *stepsDone % 16;
+        uint8_t blendAmount = *stepsDone / 16;
+        data->currentPalette[i] = CRGB(color_blend(CRGBW(data->startPalette[i]), CRGBW(data->endPalette[i]), blendAmount));
+      }
     } else {
+      *stepsDone = expectedSteps;
       // full palette at once
       uint8_t blendAmount = (uint8_t)*stepsDone;
       for (uint8_t i = 0; i < 16; i++) {
         data->currentPalette[i] = CRGB(color_blend(CRGBW(data->startPalette[i]), CRGBW(data->endPalette[i]), blendAmount));
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wled00/FX.cpp` around lines 10839 - 10849, The sweep branch (when
SEGMENT.check2 is true) jumps *stepsDone to expectedSteps and only applies the
final microstep, losing intermediate palette-entry updates; change the logic in
the block using *stepsDone, expectedSteps, data->currentPalette,
data->startPalette, data->endPalette, CRGB/CRGBW and color_blend so that you
advance step-by-step for any skipped microsteps: iterate from the previous
stepsDone+1 up to expectedSteps and for each step compute i = step % 16 and
blendAmount = step / 16 and update data->currentPalette[i] accordingly (instead
of applying only the final step), then set *stepsDone = expectedSteps after the
loop.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Long Transitions are currently not possible

3 participants