Skip to content

Brush walker fx - a matrix-inspired colorful 2D effect#5452

Open
suromark wants to merge 13 commits intowled:mainfrom
suromark:BrushWalkerFX
Open

Brush walker fx - a matrix-inspired colorful 2D effect#5452
suromark wants to merge 13 commits intowled:mainfrom
suromark:BrushWalkerFX

Conversation

@suromark
Copy link
Copy Markdown

@suromark suromark commented Mar 26, 2026

The "Brush Walker" is a 2D effect that moves dots across the grid leaving a (fading, optional) trail. It is similar to the Matrix movie title / the matrix effect in WLED, but it picks directions randomly from four sides and uses the current palette to pick a random color, slowly changing the palette position of the next color as the dot moves. The rate/stepping of palette index change is configurable. The function requests storage for up to 32 dots from SEGENV, each dot having X/Y, dX/dY, active flag and a palette index.
The "Brush Walker AR" triggers new walkers by checking the data supplied by audioreactive peak detection, plus an adjustable "base noise" of walkers to keep the display from going completely blank.
I mainly ported this over from an old project I did in Arduino and ESP8266 in 2019, because I found no other effect with this look yet. I used VSC AI assistance and did some cleanup and added comments afterwards because AI isn't quite there yet :-)

Tested on ESP32 dev board and ESP32C3 Supermini board, with I2S microphone directly and audio over UDP, driving several small WS2812B 8x8 grids and a 16x36 SK6812 furniture installation.

Summary by CodeRabbit

  • New Features
    • Brush Walker: new 2D matrix effect with up to 32 persistent animated walkers that spawn at edges, paint single-pixel trails, advance palette colors per step, and fade over time with configurable speed, fade rate, palette step, intensity, spawn probability, and max walkers.
    • Brush Walker (Audio‑Reactive): audio-responsive variant that gates spawning on detected sound (with a non-audio fallback) and supports intensity/probability controls.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 26, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 43d05117-ca8d-4a9a-b62c-94d9bf9f3f1c

📥 Commits

Reviewing files that changed from the base of the PR and between 9ea1423 and 6d4a431.

📒 Files selected for processing (1)
  • usermods/user_fx/user_fx.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • usermods/user_fx/user_fx.cpp

Walkthrough

Adds a 2D "Brush Walker" matrix effect and an audio‑reactive variant, with per‑segment persistent Walker state (up to 32), spawn/conflict logic, timed stepping with fading/trails and palette stepping, and registers both effects with new PROGMEM metadata. (49 words)

Changes

Cohort / File(s) Summary
Brush Walker effect & registration
usermods/user_fx/user_fx.cpp
Adds mode_brushwalker and mode_brushwalker_ar; per‑segment Walker storage (max 32) and init; spawn logic (trySpawn) with conflict/gating; tick timing via segment speed, fading trails, palette-based coloring, deactivation on exit; audio‑reactive spawn path using UsermodManager::getUMData() (with simulateSound() fallback); registers both effects via strip.addEffect(...); adds PROGMEM metadata strings _data_FX_MODE_BRUSHWALKER and _data_FX_MODE_BRUSHWALKER_AR.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • DedeHai
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main feature being added: a new 2D visual effect called 'Brush Walker' with a Matrix-like aesthetic. The title directly matches the primary change in the changeset, which adds two new effect modes (mode_brushwalker and mode_brushwalker_ar) implementing this functionality.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

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

✨ 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.

Copy link
Copy Markdown
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: 1

🧹 Nitpick comments (1)
usermods/user_fx/user_fx.cpp (1)

1403-1491: Extract the shared walker loop before these modes drift further.

Both modes duplicate allocation, init, timing, fade, and render/update logic. The only real difference is the spawn predicate, and the bug above is already an example of the copies diverging.

Also applies to: 1504-1607

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

In `@usermods/user_fx/user_fx.cpp` around lines 1403 - 1491, The brush walker
logic in mode_brushwalker duplicates allocation, initialization, timing, fade,
spawn and update/render loops (see BrushWalker, SEGENV, SEGMENT,
BrushWalkerCountActive, BrushWalkerTrySpawn), causing drift/bugs between modes;
extract shared behavior into a small helper API: create
AllocateOrInitWalkers(SEGENV, BrushWalker*, count) to handle SEGENV.allocateData
and zero/init walkers, a ShouldStep(SEGENV, interval) timing helper to manage
SEGENV.step, a CommonFade(SEGMENT, fadeRate) call, and a
WalkersStepRender(BrushWalker*, count, cols, rows, palStep, SEGMENT, SEGCOLOR)
that does palette/primary color selection, setPixelColorXY, movement, colorIndex
increment and deactivation; modify mode_brushwalker and the other duplicated
mode to call these helpers and only supply the spawn predicate (e.g., use
BrushWalkerTrySpawn or alternate spawn logic) and per-mode parameters like
spawnChance, maxWalkers, fadeRate, palStep and interval.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@usermods/user_fx/user_fx.cpp`:
- Around line 1569-1570: The code reads the peak flag from the wrong u_data
index; change the access in user_fx.cpp where isPeak is set (uint8_t isPeak =
*(uint8_t *)um_data->u_data[2];) to read from u_data[3] instead so the AR
variant uses the actual samplePeak flag (i.e., use um_data->u_data[3] when
dereferencing the peak byte).

---

Nitpick comments:
In `@usermods/user_fx/user_fx.cpp`:
- Around line 1403-1491: The brush walker logic in mode_brushwalker duplicates
allocation, initialization, timing, fade, spawn and update/render loops (see
BrushWalker, SEGENV, SEGMENT, BrushWalkerCountActive, BrushWalkerTrySpawn),
causing drift/bugs between modes; extract shared behavior into a small helper
API: create AllocateOrInitWalkers(SEGENV, BrushWalker*, count) to handle
SEGENV.allocateData and zero/init walkers, a ShouldStep(SEGENV, interval) timing
helper to manage SEGENV.step, a CommonFade(SEGMENT, fadeRate) call, and a
WalkersStepRender(BrushWalker*, count, cols, rows, palStep, SEGMENT, SEGCOLOR)
that does palette/primary color selection, setPixelColorXY, movement, colorIndex
increment and deactivation; modify mode_brushwalker and the other duplicated
mode to call these helpers and only supply the spawn predicate (e.g., use
BrushWalkerTrySpawn or alternate spawn logic) and per-mode parameters like
spawnChance, maxWalkers, fadeRate, palStep and interval.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a5def1e0-168b-4cc8-b4d1-cf91716aa8e0

📥 Commits

Reviewing files that changed from the base of the PR and between 78ecd38 and 3454cec.

📒 Files selected for processing (2)
  • platformio.ini
  • usermods/user_fx/user_fx.cpp

@softhack007 softhack007 added effect usermod usermod related labels Mar 26, 2026
Copy link
Copy Markdown
Member

@softhack007 softhack007 Mar 26, 2026

Choose a reason for hiding this comment

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

please undo change in platformio.ini, these are not related to your PR.

strip.addEffect(255, &mode_ants, _data_FX_MODE_ANTS);
strip.addEffect(255, &mode_morsecode, _data_FX_MODE_MORSECODE);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this whitespace change should be reverted.

{
if (!strip.isMatrix || !SEGMENT.is2D())
{
SEGMENT.fill(SEGCOLOR(0));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

please use FX_FALLBACK_STATIC in case of errors.

#define FX_FALLBACK_STATIC { mode_static(); return; }

example:

WLED/wled00/FX.cpp

Lines 5572 to 5578 in 78ecd38

if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up
const int cols = SEG_W;
const int rows = SEG_H;
if (!SEGENV.allocateData(sizeof(julia))) FX_FALLBACK_STATIC;
Julia* julias = reinterpret_cast<Julia*>(SEGENV.data);

@softhack007
Copy link
Copy Markdown
Member

softhack007 commented Mar 26, 2026

For code readability, please adjust your coding style for opening/closing brackets.
We usually have the opening bracket on the same line as the if or for, and single statements (often) don't need brackets at all.

for example, use

    if (SEGMENT.palette > 0) {
       c = SEGMENT.color_from_palette(w.colorIndex, false, PALETTE_SOLID_WRAP, 0);
    } else {
      c = SEGCOLOR(0);
    }

or even simpler

    if (SEGMENT.palette > 0) c = SEGMENT.color_from_palette(w.colorIndex, false, PALETTE_SOLID_WRAP, 0);
    else c = SEGCOLOR(0);

https://github.com/wled/WLED/tree/main?tab=contributing-ov-file#code-style

@suromark
Copy link
Copy Markdown
Author

Thanks for the quick replies! I've already made some fixes, and can hopefully convince VSC to not mess up the styles again while I work my way down the todo list.

@DedeHai
Copy link
Copy Markdown
Collaborator

DedeHai commented Mar 27, 2026

I ran you FX quickly yesterday, looks very much like the "matrix" FX to me but I only did a quick test.
if you want to make it more versatile or unique you can explore on how to do it using my particle system, the same effect can be done using it but it will take like 20lines of code, not 400 ;)

Copy link
Copy Markdown
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: 1

♻️ Duplicate comments (2)
usermods/user_fx/user_fx.cpp (2)

1405-1411: ⚠️ Potential issue | 🟠 Major

Read samplePeak from u_data[3].

The AR spawn gate still dereferences u_data[2], so it is reacting to the wrong usermod payload instead of the peak flag.

🔧 Minimal fix
-    if (um_data &&
-        (*(uint8_t*)um_data->u_data[2] || hw_random8() < sensitivity))
+    if (um_data &&
+        (((*(uint8_t*)um_data->u_data[3]) != 0) || hw_random8() < sensitivity))
       shouldSpawn = true;

Based on learnings, samplePeak should be read as (*(uint8_t*)um_data->u_data[3]) != 0 to stay compatible with AudioReactive's untyped um_data payload.

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

In `@usermods/user_fx/user_fx.cpp` around lines 1405 - 1411, The spawn gate is
reading the wrong payload byte (u_data[2]) from the AudioReactive usermod;
change the condition that checks the peak flag to read samplePeak from
(*(uint8_t*)um_data->u_data[3]) != 0 instead of using u_data[2] (keep the
existing cast to uint8_t and the same null-check flow around
UsermodManager::getUMData, simulateSound, and shouldSpawn so the only change is
replacing the u_data index to 3 when evaluating the peak).

1376-1379: ⚠️ Potential issue | 🟡 Minor

Use FX_FALLBACK_STATIC on the error exits.

The unsupported-2D branch currently renders SEGCOLOR(1), and the OOM path just returns. That makes Brush Walker behave differently from the other user FX and can leave stale pixels on allocation failure.

🔁 Minimal fix
-  if (!strip.isMatrix || !SEGMENT.is2D()) {
-    SEGMENT.fill(SEGCOLOR(1));
-    return;
-  }
+  if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC;
...
-    if (!SEGENV.allocateData(sizeof(Walker) * absoluteMaxWalkers)) return;
+    if (!SEGENV.allocateData(sizeof(Walker) * absoluteMaxWalkers)) FX_FALLBACK_STATIC;

Also applies to: 1386-1386

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

In `@usermods/user_fx/user_fx.cpp` around lines 1376 - 1379, The unsupported-2D
and OOM error exits in Brush Walker currently render SEGCOLOR(1) or simply
return, which can leave stale pixels; replace both early-exit paths to use
FX_FALLBACK_STATIC so they match other user FX behavior: when !strip.isMatrix or
!SEGMENT.is2D() and on any allocation/oom failure in the Brush Walker code
paths, invoke FX_FALLBACK_STATIC (rather than SEGCOLOR/return) to perform the
standardized fallback cleanup and exit; update the branches around the checks
that reference strip.isMatrix, SEGMENT.is2D(), and any allocation failure
returns to call FX_FALLBACK_STATIC.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@usermods/user_fx/user_fx.cpp`:
- Around line 1383-1387: You take a pointer to SEGENV.data in walkers before
ensuring the buffer exists or is reallocated; move the
reinterpret_cast<Walker*>(SEGENV.data) to after the allocation check so that
walkers is obtained only once allocateData(sizeof(Walker) * absoluteMaxWalkers)
has succeeded (and handle the failure by returning). Specifically, call
SEGENV.allocateData(...) first, then set Walker* walkers =
reinterpret_cast<Walker*>(SEGENV.data), and only then run the reset loop over
absoluteMaxWalkers to avoid dereferencing a stale or null pointer.

---

Duplicate comments:
In `@usermods/user_fx/user_fx.cpp`:
- Around line 1405-1411: The spawn gate is reading the wrong payload byte
(u_data[2]) from the AudioReactive usermod; change the condition that checks the
peak flag to read samplePeak from (*(uint8_t*)um_data->u_data[3]) != 0 instead
of using u_data[2] (keep the existing cast to uint8_t and the same null-check
flow around UsermodManager::getUMData, simulateSound, and shouldSpawn so the
only change is replacing the u_data index to 3 when evaluating the peak).
- Around line 1376-1379: The unsupported-2D and OOM error exits in Brush Walker
currently render SEGCOLOR(1) or simply return, which can leave stale pixels;
replace both early-exit paths to use FX_FALLBACK_STATIC so they match other user
FX behavior: when !strip.isMatrix or !SEGMENT.is2D() and on any allocation/oom
failure in the Brush Walker code paths, invoke FX_FALLBACK_STATIC (rather than
SEGCOLOR/return) to perform the standardized fallback cleanup and exit; update
the branches around the checks that reference strip.isMatrix, SEGMENT.is2D(),
and any allocation failure returns to call FX_FALLBACK_STATIC.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4d6d0c21-f0ab-4f07-afad-af46c07f89e1

📥 Commits

Reviewing files that changed from the base of the PR and between 3454cec and 8254352.

📒 Files selected for processing (1)
  • usermods/user_fx/user_fx.cpp

Copy link
Copy Markdown
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 `@usermods/user_fx/user_fx.cpp`:
- Line 1379: The local uint32_t triggerGate is reset each frame, so replace the
local variable with a persistent slot (e.g., read/write SEGENV.aux0 or
SEGENV.aux1) to hold the "next allowed spawn time"; change occurrences that
compare SEGENV.step > triggerGate to read uint32_t triggerGate =
(uint32_t)SEGENV.aux0 (or aux1), and when allowing a spawn update SEGENV.aux0 =
(double)(SEGENV.step + spawnInterval) (or store the timestamp you need) so the
rate-limiting persists across frames; apply this same change to the other
instance noted (around the second occurrence referenced in the comment).
- Around line 1367-1372: The retry loop that calls
walkers[freeSlot].makeCandidate(...) can overwrite a previously successful
activation because makeCandidate resets active and a later retry may set active
back to false; fix this in the for (uint8_t retry = 0; retry < 2; retry++) loop
by exiting the loop immediately after a successful activation (i.e., after
walkers[freeSlot].active = true) so subsequent makeCandidate calls cannot
overwrite the valid spawn—add a break from the retry loop when hasConflict(...)
returns false and active is set.
- Around line 1382-1386: The allocation of segment data is currently gated by
SEGENV.call == 0 which makes future calls crash if SEGENV.data was freed; call
SEGENV.allocateData(sizeof(Walker) * absoluteMaxWalkers) unconditionally before
using SEGENV.data so allocateData can no-op when already sized or reallocate
when needed, then reinterpret_cast SEGENV.data to Walker* (Walker* walkers) and
handle a failed allocation via FX_FALLBACK_STATIC as before (keep the
FX_FALLBACK_STATIC path if allocateData returns false).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2c21c6a8-f5f0-4581-bfb5-36013f41b6da

📥 Commits

Reviewing files that changed from the base of the PR and between b26d1a6 and 9ea1423.

📒 Files selected for processing (1)
  • usermods/user_fx/user_fx.cpp

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

Labels

effect usermod usermod related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants