Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
5f3fd4d
feat: add BrushWalker effect for dynamic color trails
suromark Mar 25, 2026
9cf7ed2
Reverted a mistaken edit in platformio.ini
suromark Mar 25, 2026
7309b3d
Added audio-reactive version of BrushWalker FX
suromark Mar 26, 2026
9436775
Undid auto-formatting affecting other parts of user_fx.cpp
suromark Mar 26, 2026
dd4e134
Some cleanup on BrushWalker FX
suromark Mar 26, 2026
3454cec
Added missing context in description
suromark Mar 26, 2026
acf489f
Refactored BrushWalker after suggestions and critique
suromark Mar 27, 2026
f1b7727
Cleaned up into a more self-contained OO style with Gemini
suromark Mar 27, 2026
8254352
Reworked brushwalker fx code styling, fixed mistaken edit of platform…
suromark Mar 27, 2026
53ee3b9
Fixed wrong index for reading from audioreactive; added peak-spawn in…
suromark Mar 27, 2026
b26d1a6
Fixed a logical error in the spawn logic
suromark Mar 27, 2026
9ea1423
Lowered the respawn cutoff
suromark Mar 27, 2026
6d4a431
Reworked storage for effect data
suromark Mar 28, 2026
6ac7f14
Merge branch 'wled:main' into BrushWalkerFX
suromark Apr 9, 2026
ce27414
Merge branch 'main' into pr/5452
softhack007 Apr 10, 2026
724cb8c
Implemented suggested code and style fixes from AI review, added comm…
suromark Apr 11, 2026
8145caf
Added comment reference to documentation for AI parser for clarificat…
suromark Apr 12, 2026
5102e06
Slider labels better description
suromark Apr 12, 2026
53f57db
Merge branch 'main' into BrushWalkerFX
softhack007 Jul 3, 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
3 changes: 2 additions & 1 deletion platformio.ini
Comment thread
softhack007 marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
# ------------------------------------------------------------------------------

# CI/release binaries
default_envs = nodemcuv2
default_envs =
nodemcuv2
esp8266_2m
esp01_1m_full
nodemcuv2_160
Expand Down
356 changes: 354 additions & 2 deletions usermods/user_fx/user_fx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1260,6 +1260,357 @@ static void mode_morsecode(void) {
static const char _data_FX_MODE_MORSECODE[] PROGMEM = "Morse Code@Speed,,,,Color mode,Color by Word,Punctuation,EndOfMessage;;!;1;sx=192,c3=8,o1=1,o2=1";



/**
* BrushWalker
* Uses palette for the trails and background color as fade target.
* Walkers spawn randomly from the edges and move in straight lines across the matrix, changing color as they go.
* leaving fading trails of "painted" color behind them.
* Tries to avoid spawning new walkers too close to existing ones to prevent overcrowding and create a more visually appealing distribution.
* Inspired by the concept of "Matrix", but with a more vivid, undirected and colorful twist.
* First implementation 2019 with FastLED, but without WLED framework.
* Redesigned and adapted for WLED in 2026, with support from claude.ai and chatGPT.
* Controls: Speed, Spawn Chance, Fade Rate, Palette Step, Max Walkers (up to 32)
*
* @author suromark 2019,2026
*
*/
struct BrushWalker
{
bool active;
int16_t x;
int16_t y;
int8_t dx;
int8_t dy;
uint8_t colorIndex;
};

// a helper function to count active walkers, to decide if spawning is permitted based on maxWalkers setting
static uint8_t BrushWalkerCountActive(const BrushWalker *walkers, uint8_t maxCount)
{
uint8_t n = 0;
for (uint8_t i = 0; i < maxCount; i++)
{
if (walkers[i].active)
n++;
}
return n;
}
// a helper function to create a start position for spawning a new walker, starting from a random edge and moving inward
static void BrushWalkerMakeCandidate(BrushWalker &w, uint16_t cols, uint16_t rows)
{
uint8_t side = hw_random8(4);

switch (side)
{
case 0: // top -> down
w.x = hw_random16(cols);
w.y = 0;
w.dx = 0;
w.dy = 1;
break;

case 1: // bottom -> up
w.x = hw_random16(cols);
w.y = rows - 1;
w.dx = 0;
w.dy = -1;
break;

case 2: // left -> right
w.x = 0;
w.y = hw_random16(rows);
w.dx = 1;
w.dy = 0;
break;

default: // right -> left
w.x = cols - 1;
w.y = hw_random16(rows);
w.dx = -1;
w.dy = 0;
break;
}

w.colorIndex = hw_random8();
w.active = true;
}

// a helper function to check if a candidate walker spawns too close to existing active walkers with the same direction, based on a minimum gap distance
static bool BrushWalkerConflicts(const BrushWalker *walkers, uint8_t maxCount, const BrushWalker &cand, uint8_t minGap)
{
for (uint8_t i = 0; i < maxCount; i++)
{
const BrushWalker &w = walkers[i];
if (!w.active)
continue;
if (w.dx != cand.dx || w.dy != cand.dy)
continue;

// horizontal: gleiche Zeile
if (cand.dy == 0 && w.y == cand.y)
{
if (abs(w.x - cand.x) < minGap)
return true;
}

// vertical: gleiche Spalte
if (cand.dx == 0 && w.x == cand.x)
{
if (abs(w.y - cand.y) < minGap)
return true;
}
}
return false;
}

static bool BrushWalkerTrySpawn(BrushWalker *walkers, uint8_t maxCount, uint16_t cols, uint16_t rows)
{
int freeSlot = -1;
for (uint8_t i = 0; i < maxCount; i++)
{
if (!walkers[i].active)
{
freeSlot = i;
break;
}
}
if (freeSlot < 0)
return false;

const uint8_t minGap = 8;
BrushWalker cand;

BrushWalkerMakeCandidate(cand, cols, rows);
if (!BrushWalkerConflicts(walkers, maxCount, cand, minGap))
{
walkers[freeSlot] = cand;
return true;
}

// One retry if collision happened before. If also not successful, just give up and skip this.
BrushWalkerMakeCandidate(cand, cols, rows);
if (!BrushWalkerConflicts(walkers, maxCount, cand, minGap))
{
walkers[freeSlot] = cand;
return true;
}

return false;
}

// the actual main loop for the brushwalker effect
static void mode_brushwalker(void)
{
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);

return;
}

const uint16_t cols = SEG_W;
const uint16_t rows = SEG_H;
if (cols < 2 || rows < 2)
{
SEGMENT.fill(SEGCOLOR(0));
return;
}

// Allocate per-segment storage for walkers
if (!SEGENV.allocateData(sizeof(BrushWalker) * 32))
{
SEGMENT.fill(SEGCOLOR(0));
return;
}
BrushWalker *walkers = reinterpret_cast<BrushWalker *>(SEGENV.data);

const uint8_t spawnChance = SEGMENT.intensity;
const uint8_t fadeRate = SEGMENT.custom1 >> 1; // 0-63 based on slider, higher is faster fading
const uint8_t palStep = SEGMENT.custom2 >> 4; //
const uint8_t maxWalkers = 1 + SEGMENT.custom3; // Custom3 slider ranges from 0-31 only

const uint16_t interval = 8 + ( ( 255 - SEGMENT.speed ) >> 1); // base 8ms interval + up to 127ms based on speed slider

if (SEGENV.call == 0)
{
for (uint8_t i = 0; i < 32; i++)
{
walkers[i].active = false;
walkers[i].x = 0;
walkers[i].y = 0;
walkers[i].dx = 0;
walkers[i].dy = 0;
walkers[i].colorIndex = 0;
}
SEGMENT.fill(SEGCOLOR(1));
SEGENV.step = strip.now;
}

if ((strip.now - SEGENV.step) < interval)
return;
SEGENV.step = strip.now;

// Fade using the background color (secondary color)
SEGMENT.fadeToSecondaryBy(fadeRate);

if (BrushWalkerCountActive(walkers, 32) < maxWalkers)
{
if (hw_random8() < spawnChance)
{
BrushWalkerTrySpawn(walkers, 32, cols, rows);
}
}

for (uint8_t i = 0; i < 32; i++)
{
BrushWalker &w = walkers[i];
if (!w.active)
continue;

// Use WLED's palette mechanism if a palette is selected, otherwise use primary color
uint32_t c;
if (SEGMENT.palette > 0)
{
c = SEGMENT.color_from_palette(w.colorIndex, false, PALETTE_SOLID_WRAP, 0);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
else
{
c = SEGCOLOR(0);
}
SEGMENT.setPixelColorXY(w.x, w.y, c);

w.x += w.dx;
w.y += w.dy;
w.colorIndex += palStep;

if (! (w.x >= 0 && w.y >= 0 && w.x < (int16_t)cols && w.y < (int16_t)rows ))
{
w.active = false;
}
}
}

// The metadata string consists of up to five sections, separated by semicolons:
// <Effect parameters>;<Colors>;<Palette>;<Flags>;<Defaults>
static const char _data_FX_MODE_BRUSHWALKER[] PROGMEM =
"Brush Walker@!,Spawn,Fade,Palette Step,Max Walkers;,!;!;2;pal=11,sx=200,ix=64,c1=48,c2=24,c3=16";

/*
*
*
* Audio-reactive version of the brushwalker main loop
*
*/
static void mode_brushwalker_ar(void)
{
if (!strip.isMatrix || !SEGMENT.is2D())
{
SEGMENT.fill(SEGCOLOR(0));
return;
}

const uint16_t cols = SEG_W;
const uint16_t rows = SEG_H;
if (cols < 2 || rows < 2)
{
SEGMENT.fill(SEGCOLOR(0));
return;
}

// Allocate per-segment storage for walkers
if (!SEGENV.allocateData(sizeof(BrushWalker) * 32))
{
SEGMENT.fill(SEGCOLOR(0));
return;
}
BrushWalker *walkers = reinterpret_cast<BrushWalker *>(SEGENV.data);

const uint8_t sensitivity = SEGMENT.intensity;
const uint8_t fadeRate = SEGMENT.custom1 >> 1; // 0-63 based on slider, higher is faster fading
const uint8_t palStep = SEGMENT.custom2 >> 4; //
const uint8_t maxWalkers = 1 + SEGMENT.custom3; // Custom3 slider ranges from 0-31 only

const uint16_t interval = 8 + ( ( 255 - SEGMENT.speed ) >> 1); // base 8ms interval + up to 127ms based on speed slider

if (SEGENV.call == 0)
{
for (uint8_t i = 0; i < 32; i++)
{
walkers[i].active = false;
walkers[i].x = 0;
walkers[i].y = 0;
walkers[i].dx = 0;
walkers[i].dy = 0;
walkers[i].colorIndex = 0;
}
SEGMENT.fill(SEGCOLOR(1));
SEGENV.step = strip.now;
}

if ((strip.now - SEGENV.step) < interval)
return;
SEGENV.step = strip.now;

// Fade using the background color (secondary color)
SEGMENT.fadeToSecondaryBy(fadeRate);

// Audio-reactive evaluation

um_data_t *um_data;
if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE))
{
// add support for no audio
um_data = simulateSound(SEGMENT.soundSim);
}
/*
// Access smoothed volume (index 0)
float volumeSmth = *(float*)um_data->u_data[0];
*/
// Access the pre-calculated peak trigger (index 2)
uint8_t isPeak = *(uint8_t *)um_data->u_data[2];
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

if (isPeak || hw_random8() < sensitivity)
{
if (BrushWalkerCountActive(walkers, 32) < maxWalkers)
{
BrushWalkerTrySpawn(walkers, 32, cols, rows);
}
}

for (uint8_t i = 0; i < 32; i++)
{
BrushWalker &w = walkers[i];
if (!w.active)
continue;

// Use WLED's palette mechanism if a palette is selected, otherwise use primary color
uint32_t c;
if (SEGMENT.palette > 0)
{
c = SEGMENT.color_from_palette(w.colorIndex, false, PALETTE_SOLID_WRAP, 0);
}
else
{
c = SEGCOLOR(0);
}
SEGMENT.setPixelColorXY(w.x, w.y, c);

w.x += w.dx;
w.y += w.dy;
w.colorIndex += palStep;

if (! (w.x >= 0 && w.y >= 0 && w.x < (int16_t)cols && w.y < (int16_t)rows ))
{
w.active = false;
}
}
}

// The metadata string consists of up to five sections, separated by semicolons:
// <Effect parameters>;<Colors>;<Palette>;<Flags>;<Defaults>
static const char _data_FX_MODE_BRUSHWALKER_AR[] PROGMEM =
"Brush Walker AR@!,Sensitivity,Fade,Palette Step,Max Walkers;,!;!;2v;pal=11,sx=200,ix=64,c1=48,c2=24,c3=16";

/////////////////////
// UserMod Class //
/////////////////////
Expand All @@ -1274,14 +1625,15 @@ class UserFxUsermod : public Usermod {
strip.addEffect(255, &mode_2D_magma, _data_FX_MODE_2D_MAGMA);
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.

////////////////////////////////////////
// add your effect function(s) here //
////////////////////////////////////////

// use id=255 for all custom user FX (the final id is assigned when adding the effect)

// strip.addEffect(255, &mode_your_effect, _data_FX_MODE_YOUR_EFFECT);
strip.addEffect(255, &mode_brushwalker, _data_FX_MODE_BRUSHWALKER);
strip.addEffect(255, &mode_brushwalker_ar, _data_FX_MODE_BRUSHWALKER_AR);
// strip.addEffect(255, &mode_your_effect2, _data_FX_MODE_YOUR_EFFECT2);
// strip.addEffect(255, &mode_your_effect3, _data_FX_MODE_YOUR_EFFECT3);
}
Expand Down
Loading