-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Brush walker fx - a matrix-inspired colorful 2D effect #5452
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 6 commits
5f3fd4d
9cf7ed2
7309b3d
9436775
dd4e134
3454cec
acf489f
f1b7727
8254352
53ee3b9
b26d1a6
9ea1423
6d4a431
6ac7f14
ce27414
724cb8c
8145caf
5102e06
53f57db
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 | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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)); | ||||||||||||||||||
|
Member
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. please use Line 17 in 78ecd38
example: Lines 5572 to 5578 in 78ecd38
|
||||||||||||||||||
| 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); | ||||||||||||||||||
| } | ||||||||||||||||||
|
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]; | ||||||||||||||||||
|
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 // | ||||||||||||||||||
| ///////////////////// | ||||||||||||||||||
|
|
@@ -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); | ||||||||||||||||||
|
|
||||||||||||||||||
|
Member
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. 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); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.