From 73ca612cbbe11a4d34dcd7f14191ee946790300b Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Fri, 3 Apr 2026 01:11:53 -0700 Subject: [PATCH 1/9] Minor changes to bring it up to the latest version on github --- usermods/user_fx/user_fx.cpp | 38 +++++++++++++++++------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 2258b8ad4f..719bd433dc 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -117,7 +117,7 @@ static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spar static void mode_spinning_wheel(void) { if (SEGLEN < 1) FX_FALLBACK_STATIC; - + unsigned strips = SEGMENT.nrOfVStrips(); if (strips == 0) FX_FALLBACK_STATIC; @@ -148,7 +148,6 @@ static void mode_spinning_wheel(void) { // Handle random seeding globally (outside the virtual strip) if (SEGENV.call == 0) { - random16_set_seed(hw_random16()); SEGENV.aux1 = (255 << 8) / SEGLEN; // Cache the color scaling } @@ -156,7 +155,6 @@ static void mode_spinning_wheel(void) { uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom3 + SEGMENT.check1 + SEGMENT.check3; bool settingsChanged = (SEGENV.aux0 != settingssum); if (settingsChanged) { - random16_add_entropy(hw_random16()); SEGENV.aux0 = settingssum; } @@ -166,7 +164,7 @@ static void mode_spinning_wheel(void) { uint8_t spinnerSize = map(SEGMENT.custom1, 0, 255, 1, 10); uint16_t spin_delay = map(SEGMENT.custom3, 0, 31, 2000, 15000); uint32_t now = strip.now; - + for (unsigned stripNr = 0; stripNr < strips; stripNr += spinnerSize) { uint32_t* stripState = &state[stripNr * stateVarsPerStrip]; // Check if this spinner is stopped AND has waited its delay @@ -181,7 +179,7 @@ static void mode_spinning_wheel(void) { } } } - + struct virtualStrip { static void runStrip(uint16_t stripNr, uint32_t* state, bool settingsChanged, bool allReadyToRestart, unsigned strips) { uint8_t phase = state[PHASE_IDX]; @@ -211,23 +209,23 @@ static void mode_spinning_wheel(void) { // Initialize or restart if (needsReset && SEGMENT.check1) { // spin the spinner(s) only if the "Spin me!" checkbox is enabled state[CUR_POS_IDX] = 0; - + // Set velocity uint16_t speed = map(SEGMENT.speed, 0, 255, 300, 800); if (speed == 300) { // random speed (user selected 0 on speed slider) - state[VELOCITY_IDX] = random16(200, 900) * 655; // fixed-point velocity scaling (approx. 65536/100) + state[VELOCITY_IDX] = hw_random16(200, 900) * 655; // fixed-point velocity scaling (approx. 65536/100) } else { - state[VELOCITY_IDX] = random16(speed - 100, speed + 100) * 655; + state[VELOCITY_IDX] = hw_random16(speed - 100, speed + 100) * 655; } - + // Set slowdown start time uint16_t slowdown = map(SEGMENT.intensity, 0, 255, 3000, 5000); if (slowdown == 3000) { // random slowdown start time (user selected 0 on intensity slider) - state[SLOWDOWN_TIME_IDX] = now + random16(2000, 6000); + state[SLOWDOWN_TIME_IDX] = now + hw_random16(2000, 6000); } else { - state[SLOWDOWN_TIME_IDX] = now + random16(slowdown - 1000, slowdown + 1000); + state[SLOWDOWN_TIME_IDX] = now + hw_random16(slowdown - 1000, slowdown + 1000); } - + state[PHASE_IDX] = 0; state[STOP_TIME_IDX] = 0; state[WOBBLE_STEP_IDX] = 0; @@ -238,7 +236,7 @@ static void mode_spinning_wheel(void) { uint32_t pos_fixed = state[CUR_POS_IDX]; uint32_t velocity = state[VELOCITY_IDX]; - + // Phase management if (phase == 0) { // Fast spinning phase @@ -250,10 +248,10 @@ static void mode_spinning_wheel(void) { // Slowing phase - apply deceleration uint32_t decel = velocity / 80; if (decel < 100) decel = 100; - + velocity = (velocity > decel) ? velocity - decel : 0; state[VELOCITY_IDX] = velocity; - + // Check if stopped if (velocity < 2000) { velocity = 0; @@ -270,7 +268,7 @@ static void mode_spinning_wheel(void) { uint32_t wobble_step = state[WOBBLE_STEP_IDX]; uint16_t stop_pos = state[STOP_POS_IDX]; uint32_t elapsed = now - state[WOBBLE_TIME_IDX]; - + if (wobble_step == 0 && elapsed >= 200) { // Move back one LED from stop position uint16_t back_pos = (stop_pos == 0) ? SEGLEN - 1 : stop_pos - 1; @@ -291,13 +289,13 @@ static void mode_spinning_wheel(void) { state[STOP_TIME_IDX] = now; } } - + // Update position (phases 0 and 1 only) if (phase == 0 || phase == 1) { pos_fixed += velocity; state[CUR_POS_IDX] = pos_fixed; } - + // Draw LED for all phases uint16_t pos = (pos_fixed >> 16) % SEGLEN; @@ -314,14 +312,14 @@ static void mode_spinning_wheel(void) { hue = (SEGENV.aux1 * pos) >> 8; } - uint32_t color = ColorFromPaletteWLED(SEGPALETTE, hue, 255, LINEARBLEND); + uint32_t color = ColorFromPalette(SEGPALETTE, hue, 255, LINEARBLEND); // Draw the spinner with configurable size (1-10 LEDs) for (int8_t x = 0; x < spinnerSize; x++) { for (uint8_t y = 0; y < spinnerSize; y++) { uint16_t drawPos = (pos + y) % SEGLEN; int16_t drawStrip = stripNr + x; - + // Wrap horizontally if needed, or skip if out of bounds if (drawStrip >= 0 && drawStrip < strips) { SEGMENT.setPixelColor(indexToVStrip(drawPos, drawStrip), color); From e76d4af66b8ad8ddd0fe2262cb6ca2742e41b4fb Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Sat, 25 Apr 2026 14:47:35 -0700 Subject: [PATCH 2/9] Adding white space back in to match WLED main branch --- usermods/user_fx/user_fx.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 9827fc4116..cf7a651db6 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -179,7 +179,7 @@ static void mode_spinning_wheel(void) { } } } - + struct virtualStrip { static void runStrip(uint16_t stripNr, uint32_t* state, bool settingsChanged, bool allReadyToRestart, unsigned strips) { uint8_t phase = state[PHASE_IDX]; @@ -236,7 +236,7 @@ static void mode_spinning_wheel(void) { uint32_t pos_fixed = state[CUR_POS_IDX]; uint32_t velocity = state[VELOCITY_IDX]; - + // Phase management if (phase == 0) { // Fast spinning phase From 8d8e107c4a8c30f06603bd1619ab2bc9c376640a Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Sun, 10 May 2026 10:01:24 -0700 Subject: [PATCH 3/9] Added zooming/scaling as suggested by blazoncek --- usermods/user_fx/user_fx.cpp | 98 ++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index cf7a651db6..0e1d3c99a9 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -1258,6 +1258,103 @@ 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"; +/* +/ Perlinscape effect - a Perlin noise Landscape +* Created by stepko as part of Stepko Land on soulmatelights.com +* Adapted to WLED by Bob Loeffler with additional features (and help from Claude) +* First slider is for speed +* Second slider is for zooming in/out (Perlin scaling) +* Third slider is the X multiplier +* Fourth slider is the Y multiplier +* First checkbox will use the selected palette +* Second checkbox will rotate the image +* Third checkbox will randomize the horizonal and vertical directions +*/ +static void mode_2D_perlinscape(void) { + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up + const uint16_t width = SEG_W; + const uint16_t height = SEG_H; + if (!SEGENV.allocateData(5 * sizeof(float) + width * height)) FX_FALLBACK_STATIC; + + uint32_t speedDiv = map(SEGMENT.speed, 0, 255, 20, 1); + uint32_t t = strip.now / speedDiv; + uint8_t Xmult = map(SEGMENT.custom1, 0, 255, 0, 64); + uint8_t Ymult = map(SEGMENT.custom2, 0, 255, 0, 64); + + float *offX = reinterpret_cast(SEGENV.data) + 0; + float *offY = reinterpret_cast(SEGENV.data) + 1; + float *stepX = reinterpret_cast(SEGENV.data) + 2; + float *stepY = reinterpret_cast(SEGENV.data) + 3; + float *prevT = reinterpret_cast(SEGENV.data) + 4; + + if (SEGENV.call == 0) { + SEGENV.aux0 = hw_random16(5000, 10000); + SEGENV.aux1 = 0b00; + *offX = 0.0f; + *offY = 0.0f; + *stepX = 1.0f; + *stepY = 1.0f; + *prevT = (float)t; + } + + if (SEGMENT.check3 && (strip.now - SEGENV.step > SEGENV.aux0)) { + SEGENV.aux0 = hw_random16(5000, 10000); + SEGENV.aux1 = hw_random8(4); + SEGENV.step = strip.now; + } + + bool flipX = SEGMENT.check3 ? (SEGENV.aux1 & 0x01) : false; + bool flipY = SEGMENT.check3 ? (SEGENV.aux1 & 0x02) : false; + + float targetX = flipX ? -1.0f : 1.0f; + float targetY = flipY ? -1.0f : 1.0f; + *stepX += (targetX - *stepX) * 0.05f; + *stepY += (targetY - *stepY) * 0.05f; + + float dt = (float)t - *prevT; + *offX += *stepX * dt; + *offY += *stepY * dt; + *prevT = (float)t; + + int32_t tX = (int32_t)*offX; + int32_t tY = (int32_t)*offY; + + // Rotation + float cosA = 1.0f, sinA = 0.0f; + float cx = width * 0.5f; + float cy = height * 0.5f; + + if (SEGMENT.check2) { + float angle = strip.now / 5000.0f; + cosA = cosf(angle); + sinA = sinf(angle); + } + + float scale = map(SEGMENT.intensity, 0, 255, 10, 200) / 100.0f; // range 0.1 to 2.0 + + for (byte x = 0; x < width; x++) { + for (byte y = 0; y < height; y++) { + float rx = cosA * (x - cx) - sinA * (y - cy) + cx; + float ry = sinA * (x - cx) + cosA * (y - cy) + cy; + + float scaled_x = rx * Xmult * scale; + float scaled_y = ry * Ymult * scale; + + if (SEGMENT.check1) { + // Palette mode + uint8_t paletteIndex = perlin8(scaled_x, scaled_y, t); + uint8_t brightness = perlin8(scaled_x + tX, scaled_y + tY); + SEGMENT.setPixelColorXY(x, y, SEGMENT.color_from_palette(paletteIndex, false, PALETTE_SOLID_WRAP, brightness)); + } else { + // Raw RGB mode + SEGMENT.setPixelColorXY(x, y, perlin8(scaled_x, scaled_y, t), perlin8(scaled_x, scaled_y + tY), perlin8(scaled_x + tX, scaled_y)); + } + } + } +} +static const char _data_FX_MODE_2D_PERLINSCAPE[] PROGMEM = "Perlinscape@!,Zoom (+/-),X multiplier,Y multiplier,,Palettes,Rotation,Random direction;;!;2;"; + + ///////////////////// // UserMod Class // ///////////////////// @@ -1272,6 +1369,7 @@ 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); + strip.addEffect(255, &mode_2D_perlinscape, _data_FX_MODE_2D_PERLINSCAPE); //////////////////////////////////////// // add your effect function(s) here // From 0dcdbaa3411fae13008f34300de67a70a863df74 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Sun, 10 May 2026 11:30:34 -0700 Subject: [PATCH 4/9] Fixed two issues mentioned by the rabbit: - fixed memory allocation. - use cos_approx() and sin_approx() instead of cosf and sinf --- usermods/user_fx/user_fx.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 0e1d3c99a9..5e79c2b25c 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -1274,8 +1274,8 @@ static void mode_2D_perlinscape(void) { if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const uint16_t width = SEG_W; const uint16_t height = SEG_H; - if (!SEGENV.allocateData(5 * sizeof(float) + width * height)) FX_FALLBACK_STATIC; - +// if (!SEGENV.allocateData(5 * sizeof(float) + width * height)) FX_FALLBACK_STATIC; + if (!SEGENV.allocateData(5 * sizeof(float))) FX_FALLBACK_STATIC; uint32_t speedDiv = map(SEGMENT.speed, 0, 255, 20, 1); uint32_t t = strip.now / speedDiv; uint8_t Xmult = map(SEGMENT.custom1, 0, 255, 0, 64); @@ -1326,8 +1326,10 @@ static void mode_2D_perlinscape(void) { if (SEGMENT.check2) { float angle = strip.now / 5000.0f; - cosA = cosf(angle); - sinA = sinf(angle); +// cosA = cosf(angle); +// sinA = sinf(angle); + cosA = cos_approx(angle); + sinA = sin_approx(angle); } float scale = map(SEGMENT.intensity, 0, 255, 10, 200) / 100.0f; // range 0.1 to 2.0 From 4d1d8181b2133133e240a5c5d97c5cf5c7152691 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Sat, 16 May 2026 00:33:44 -0700 Subject: [PATCH 5/9] Perlinscape: Will now use Perlin color if Default palette is used but otherwise use selected palette. Also, I expanded the speed range towards the slower side. Both requested by blazoncek. --- usermods/user_fx/user_fx.cpp | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 5e79c2b25c..b6e8496db4 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -1266,17 +1266,15 @@ static const char _data_FX_MODE_MORSECODE[] PROGMEM = "Morse Code@Speed,,,,Color * Second slider is for zooming in/out (Perlin scaling) * Third slider is the X multiplier * Fourth slider is the Y multiplier -* First checkbox will use the selected palette -* Second checkbox will rotate the image -* Third checkbox will randomize the horizonal and vertical directions +* First checkbox will rotate the image +* Second checkbox will randomize the horizonal and vertical directions */ static void mode_2D_perlinscape(void) { if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up const uint16_t width = SEG_W; const uint16_t height = SEG_H; -// if (!SEGENV.allocateData(5 * sizeof(float) + width * height)) FX_FALLBACK_STATIC; if (!SEGENV.allocateData(5 * sizeof(float))) FX_FALLBACK_STATIC; - uint32_t speedDiv = map(SEGMENT.speed, 0, 255, 20, 1); + uint32_t speedDiv = map(SEGMENT.speed, 0, 255, 30, 1); uint32_t t = strip.now / speedDiv; uint8_t Xmult = map(SEGMENT.custom1, 0, 255, 0, 64); uint8_t Ymult = map(SEGMENT.custom2, 0, 255, 0, 64); @@ -1326,8 +1324,6 @@ static void mode_2D_perlinscape(void) { if (SEGMENT.check2) { float angle = strip.now / 5000.0f; -// cosA = cosf(angle); -// sinA = sinf(angle); cosA = cos_approx(angle); sinA = sin_approx(angle); } @@ -1342,19 +1338,19 @@ static void mode_2D_perlinscape(void) { float scaled_x = rx * Xmult * scale; float scaled_y = ry * Ymult * scale; - if (SEGMENT.check1) { - // Palette mode + if (SEGMENT.palette == 0) { + // Raw RGB mode (original perlin noise colors) + SEGMENT.setPixelColorXY(x, y, perlin8(scaled_x, scaled_y, t), perlin8(scaled_x, scaled_y + tY), perlin8(scaled_x + tX, scaled_y)); + } else { + // Use the selected palette's colors uint8_t paletteIndex = perlin8(scaled_x, scaled_y, t); uint8_t brightness = perlin8(scaled_x + tX, scaled_y + tY); SEGMENT.setPixelColorXY(x, y, SEGMENT.color_from_palette(paletteIndex, false, PALETTE_SOLID_WRAP, brightness)); - } else { - // Raw RGB mode - SEGMENT.setPixelColorXY(x, y, perlin8(scaled_x, scaled_y, t), perlin8(scaled_x, scaled_y + tY), perlin8(scaled_x + tX, scaled_y)); } } } } -static const char _data_FX_MODE_2D_PERLINSCAPE[] PROGMEM = "Perlinscape@!,Zoom (+/-),X multiplier,Y multiplier,,Palettes,Rotation,Random direction;;!;2;"; +static const char _data_FX_MODE_2D_PERLINSCAPE[] PROGMEM = "Perlinscape@!,Zoom (+/-),X multiplier,Y multiplier,,,Rotation,Random direction;;!;2;"; ///////////////////// From b41d994f546c99ac0a9ccf29d3bbe147d524b174 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Sat, 16 May 2026 00:43:07 -0700 Subject: [PATCH 6/9] Comment change --- usermods/user_fx/user_fx.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index b6e8496db4..b8c21c5339 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -1266,7 +1266,7 @@ static const char _data_FX_MODE_MORSECODE[] PROGMEM = "Morse Code@Speed,,,,Color * Second slider is for zooming in/out (Perlin scaling) * Third slider is the X multiplier * Fourth slider is the Y multiplier -* First checkbox will rotate the image +* First checkbox will rotate the animation * Second checkbox will randomize the horizonal and vertical directions */ static void mode_2D_perlinscape(void) { From 967cea0bcb030aa3b0dedc632d65c8e1e0e80d24 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Sun, 17 May 2026 15:22:08 -0700 Subject: [PATCH 7/9] * Converted all float math to integer math. * Switched from using pointers to using references. * Added a slider for the rotation speed (0 = no rotation), so I removed the rotation checkbox. * Fixed a bug during rotation where the animation would "jump" because it would overflow * Changed byte to uint16_t in for loops. --- usermods/user_fx/user_fx.cpp | 139 ++++++++++++++++++++--------------- 1 file changed, 81 insertions(+), 58 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index b8c21c5339..f16364ff8a 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -1262,95 +1262,118 @@ static const char _data_FX_MODE_MORSECODE[] PROGMEM = "Morse Code@Speed,,,,Color / Perlinscape effect - a Perlin noise Landscape * Created by stepko as part of Stepko Land on soulmatelights.com * Adapted to WLED by Bob Loeffler with additional features (and help from Claude) -* First slider is for speed +* First slider is for speed/movement * Second slider is for zooming in/out (Perlin scaling) * Third slider is the X multiplier * Fourth slider is the Y multiplier -* First checkbox will rotate the animation -* Second checkbox will randomize the horizonal and vertical directions +* Fifth slider is the rotation speed (0 = do not rotate) +* Checkbox will randomize the horizontal and vertical directions */ static void mode_2D_perlinscape(void) { - if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up + if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; const uint16_t width = SEG_W; const uint16_t height = SEG_H; - if (!SEGENV.allocateData(5 * sizeof(float))) FX_FALLBACK_STATIC; + if (!SEGENV.allocateData(5 * sizeof(int32_t))) FX_FALLBACK_STATIC; + uint32_t speedDiv = map(SEGMENT.speed, 0, 255, 30, 1); - uint32_t t = strip.now / speedDiv; - uint8_t Xmult = map(SEGMENT.custom1, 0, 255, 0, 64); - uint8_t Ymult = map(SEGMENT.custom2, 0, 255, 0, 64); + uint32_t t = strip.now / speedDiv; + uint8_t Xmult = map(SEGMENT.custom1, 0, 255, 0, 64); + uint8_t Ymult = map(SEGMENT.custom2, 0, 255, 0, 64); - float *offX = reinterpret_cast(SEGENV.data) + 0; - float *offY = reinterpret_cast(SEGENV.data) + 1; - float *stepX = reinterpret_cast(SEGENV.data) + 2; - float *stepY = reinterpret_cast(SEGENV.data) + 3; - float *prevT = reinterpret_cast(SEGENV.data) + 4; + int32_t &offX = *(reinterpret_cast(SEGENV.data) + 0); + int32_t &offY = *(reinterpret_cast(SEGENV.data) + 1); + int32_t &stepX = *(reinterpret_cast(SEGENV.data) + 2); + int32_t &stepY = *(reinterpret_cast(SEGENV.data) + 3); + int32_t &prevT = *(reinterpret_cast(SEGENV.data) + 4); if (SEGENV.call == 0) { SEGENV.aux0 = hw_random16(5000, 10000); SEGENV.aux1 = 0b00; - *offX = 0.0f; - *offY = 0.0f; - *stepX = 1.0f; - *stepY = 1.0f; - *prevT = (float)t; + offX = 0; + offY = 0; + stepX = 256; // 1.0 in Q8 + stepY = 256; // 1.0 in Q8 + prevT = (int32_t)t; } - if (SEGMENT.check3 && (strip.now - SEGENV.step > SEGENV.aux0)) { + if (SEGMENT.check1 && (strip.now - SEGENV.step > SEGENV.aux0)) { SEGENV.aux0 = hw_random16(5000, 10000); SEGENV.aux1 = hw_random8(4); SEGENV.step = strip.now; } - bool flipX = SEGMENT.check3 ? (SEGENV.aux1 & 0x01) : false; - bool flipY = SEGMENT.check3 ? (SEGENV.aux1 & 0x02) : false; - - float targetX = flipX ? -1.0f : 1.0f; - float targetY = flipY ? -1.0f : 1.0f; - *stepX += (targetX - *stepX) * 0.05f; - *stepY += (targetY - *stepY) * 0.05f; - - float dt = (float)t - *prevT; - *offX += *stepX * dt; - *offY += *stepY * dt; - *prevT = (float)t; - - int32_t tX = (int32_t)*offX; - int32_t tY = (int32_t)*offY; - - // Rotation - float cosA = 1.0f, sinA = 0.0f; - float cx = width * 0.5f; - float cy = height * 0.5f; - - if (SEGMENT.check2) { - float angle = strip.now / 5000.0f; - cosA = cos_approx(angle); - sinA = sin_approx(angle); + bool flipX = SEGMENT.check1 ? (SEGENV.aux1 & 0x01) : false; + bool flipY = SEGMENT.check1 ? (SEGENV.aux1 & 0x02) : false; + + // targetX/Y: +256 or -256 in Q8 + int32_t targetX = flipX ? -256 : 256; + int32_t targetY = flipY ? -256 : 256; + + stepX += ((targetX - stepX) * 13) >> 8; + stepY += ((targetY - stepY) * 13) >> 8; + + // dt in milliseconds; offX/offY accumulate in Q8 + int32_t dt = (int32_t)t - prevT; + if (dt < 0 || dt > 1000) dt = 0; + offX += (stepX * dt) >> 8; + offY += (stepY * dt) >> 8; + prevT = (int32_t)t; // store t, not strip.now + + // Integer pixel offsets — gradual drift motion (Q8 >> 8 = integer) + int32_t tX = offX >> 2; + int32_t tY = offY >> 2; + + // Fixed offsets to spread the three Perlin calls into different color regions (300 just looks good) + constexpr uint16_t colorOffX = 300; + constexpr uint16_t colorOffY = 300; + + // Rotation — cos16/sin16 return Q15 (-32768..32767 = -1.0..1.0) + int32_t cosA = 1024; // Q10: 1.0 = 1024 + int32_t sinA = 0; + + // Center in Q8 (avoids 0.5 fractions) + int32_t cx256 = (int32_t)width * 128; // width/2 * 256 + int32_t cy256 = (int32_t)height * 128; // height/2 * 256 + + // rotate if rotation speed is not 0 + if (SEGMENT.custom3 > 0) { + uint32_t rotatePeriod = map(SEGMENT.custom3, 1, 31, 30000, 1000); + uint16_t angle = (uint64_t)strip.now * 65536u / rotatePeriod; // uint64_t needed to prevent overflowing which causes a jump in the animation + cosA = cos16_t(angle) >> 5; + sinA = sin16_t(angle) >> 5; } - float scale = map(SEGMENT.intensity, 0, 255, 10, 200) / 100.0f; // range 0.1 to 2.0 + // scale: map intensity 0-255 -> 10-200, then store as Q8 (divide by 100 baked in) + // scale_q8 = map(...) * 256 / 100 + int32_t scale_q8 = (int32_t)map(SEGMENT.intensity, 0, 255, 10, 200) * 256 / 100; - for (byte x = 0; x < width; x++) { - for (byte y = 0; y < height; y++) { - float rx = cosA * (x - cx) - sinA * (y - cy) + cx; - float ry = sinA * (x - cx) + cosA * (y - cy) + cy; + for (uint16_t x = 0; x < width; x++) { + for (uint16_t y = 0; y < height; y++) { + // (x - cx) and (y - cy) in Q8 + int32_t dx256 = (int32_t)x * 256 - cx256; + int32_t dy256 = (int32_t)y * 256 - cy256; - float scaled_x = rx * Xmult * scale; - float scaled_y = ry * Ymult * scale; + // Rotation in Q8: cosA/sinA are Q10, dx/dy are Q8 + // cosA*dx >> 10 = Q8 result; add cx256 to re-center + int32_t rx256 = ((cosA * dx256 - sinA * dy256) >> 10) + cx256; + int32_t ry256 = ((sinA * dx256 + cosA * dy256) >> 10) + cy256; + + // scaled_x = rx * Xmult * scale + // rx256 is Q8, scale_q8 is Q8 => product is Q16, >> 16 gives integer + int32_t scaled_x = (rx256 * Xmult * scale_q8) >> 16; + int32_t scaled_y = (ry256 * Ymult * scale_q8) >> 16; if (SEGMENT.palette == 0) { - // Raw RGB mode (original perlin noise colors) - SEGMENT.setPixelColorXY(x, y, perlin8(scaled_x, scaled_y, t), perlin8(scaled_x, scaled_y + tY), perlin8(scaled_x + tX, scaled_y)); + SEGMENT.setPixelColorXY(x, y, perlin8(scaled_x + tX, scaled_y + tY, t), perlin8(scaled_x + tX, scaled_y + tY + colorOffY), perlin8(scaled_x + tX + colorOffX, scaled_y + tY)); } else { - // Use the selected palette's colors - uint8_t paletteIndex = perlin8(scaled_x, scaled_y, t); - uint8_t brightness = perlin8(scaled_x + tX, scaled_y + tY); + uint8_t paletteIndex = perlin8(scaled_x + tX, scaled_y + tY, t); + uint8_t brightness = perlin8(scaled_x + tX + colorOffX, scaled_y + tY + colorOffY); SEGMENT.setPixelColorXY(x, y, SEGMENT.color_from_palette(paletteIndex, false, PALETTE_SOLID_WRAP, brightness)); } } } -} -static const char _data_FX_MODE_2D_PERLINSCAPE[] PROGMEM = "Perlinscape@!,Zoom (+/-),X multiplier,Y multiplier,,,Rotation,Random direction;;!;2;"; +} +static const char _data_FX_MODE_2D_PERLINSCAPE[] PROGMEM = "Perlinscape@!,Zoom (In/Out),X multiplier,Y multiplier,Rotation speed,Random direction;;!;2;"; ///////////////////// From 7050e7273e8d53d0b21bab56ea937fe27dad236b Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Mon, 18 May 2026 00:58:20 -0700 Subject: [PATCH 8/9] * Changed a couple things requested by the rabbit. * Fixed the speed slider and rotation slider because their middle values were pretty much the same as their lower values. --- usermods/user_fx/user_fx.cpp | 44 +++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index f16364ff8a..edff13e022 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -1273,10 +1273,10 @@ static void mode_2D_perlinscape(void) { if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; const uint16_t width = SEG_W; const uint16_t height = SEG_H; - if (!SEGENV.allocateData(5 * sizeof(int32_t))) FX_FALLBACK_STATIC; + if (!SEGENV.allocateData(5 * sizeof(int32_t) + sizeof(uint32_t))) FX_FALLBACK_STATIC; - uint32_t speedDiv = map(SEGMENT.speed, 0, 255, 30, 1); - uint32_t t = strip.now / speedDiv; + uint32_t speedMult = map(SEGMENT.speed, 0, 255, 1, 30); + uint32_t t = strip.now / 10; uint8_t Xmult = map(SEGMENT.custom1, 0, 255, 0, 64); uint8_t Ymult = map(SEGMENT.custom2, 0, 255, 0, 64); @@ -1284,7 +1284,8 @@ static void mode_2D_perlinscape(void) { int32_t &offY = *(reinterpret_cast(SEGENV.data) + 1); int32_t &stepX = *(reinterpret_cast(SEGENV.data) + 2); int32_t &stepY = *(reinterpret_cast(SEGENV.data) + 3); - int32_t &prevT = *(reinterpret_cast(SEGENV.data) + 4); + int32_t &angle = *(reinterpret_cast(SEGENV.data) + 4); + uint32_t &prevT = *(reinterpret_cast(reinterpret_cast(SEGENV.data) + 5)); if (SEGENV.call == 0) { SEGENV.aux0 = hw_random16(5000, 10000); @@ -1293,7 +1294,8 @@ static void mode_2D_perlinscape(void) { offY = 0; stepX = 256; // 1.0 in Q8 stepY = 256; // 1.0 in Q8 - prevT = (int32_t)t; + angle = 0; + prevT = t; } if (SEGMENT.check1 && (strip.now - SEGENV.step > SEGENV.aux0)) { @@ -1313,15 +1315,15 @@ static void mode_2D_perlinscape(void) { stepY += ((targetY - stepY) * 13) >> 8; // dt in milliseconds; offX/offY accumulate in Q8 - int32_t dt = (int32_t)t - prevT; - if (dt < 0 || dt > 1000) dt = 0; - offX += (stepX * dt) >> 8; - offY += (stepY * dt) >> 8; - prevT = (int32_t)t; // store t, not strip.now + uint32_t udt = strip.now - prevT; + int32_t dt = (udt > 500) ? 0 : (int32_t)udt; + offX += (stepX * dt * speedMult) >> 13; + offY += (stepY * dt * speedMult) >> 13; + prevT = strip.now; // Integer pixel offsets — gradual drift motion (Q8 >> 8 = integer) - int32_t tX = offX >> 2; - int32_t tY = offY >> 2; + int32_t tX = offX << 1; + int32_t tY = offY << 1; // Fixed offsets to spread the three Perlin calls into different color regions (300 just looks good) constexpr uint16_t colorOffX = 300; @@ -1332,15 +1334,15 @@ static void mode_2D_perlinscape(void) { int32_t sinA = 0; // Center in Q8 (avoids 0.5 fractions) - int32_t cx256 = (int32_t)width * 128; // width/2 * 256 - int32_t cy256 = (int32_t)height * 128; // height/2 * 256 + int32_t cx256 = (int32_t)width * 128; + int32_t cy256 = (int32_t)height * 128; // rotate if rotation speed is not 0 if (SEGMENT.custom3 > 0) { - uint32_t rotatePeriod = map(SEGMENT.custom3, 1, 31, 30000, 1000); - uint16_t angle = (uint64_t)strip.now * 65536u / rotatePeriod; // uint64_t needed to prevent overflowing which causes a jump in the animation - cosA = cos16_t(angle) >> 5; - sinA = sin16_t(angle) >> 5; + angle += (int32_t)SEGMENT.custom3 * dt * 2; + angle &= 0xFFFF; // wrap to 16-bit + cosA = cos16_t((uint16_t)angle) >> 5; + sinA = sin16_t((uint16_t)angle) >> 5; } // scale: map intensity 0-255 -> 10-200, then store as Q8 (divide by 100 baked in) @@ -1360,8 +1362,8 @@ static void mode_2D_perlinscape(void) { // scaled_x = rx * Xmult * scale // rx256 is Q8, scale_q8 is Q8 => product is Q16, >> 16 gives integer - int32_t scaled_x = (rx256 * Xmult * scale_q8) >> 16; - int32_t scaled_y = (ry256 * Ymult * scale_q8) >> 16; + int32_t scaled_x = int32_t((int64_t(rx256) * Xmult * scale_q8) >> 16); + int32_t scaled_y = int32_t((int64_t(ry256) * Ymult * scale_q8) >> 16); if (SEGMENT.palette == 0) { SEGMENT.setPixelColorXY(x, y, perlin8(scaled_x + tX, scaled_y + tY, t), perlin8(scaled_x + tX, scaled_y + tY + colorOffY), perlin8(scaled_x + tX + colorOffX, scaled_y + tY)); @@ -1373,7 +1375,7 @@ static void mode_2D_perlinscape(void) { } } } -static const char _data_FX_MODE_2D_PERLINSCAPE[] PROGMEM = "Perlinscape@!,Zoom (In/Out),X multiplier,Y multiplier,Rotation speed,Random direction;;!;2;"; +static const char _data_FX_MODE_2D_PERLINSCAPE[] PROGMEM = "Perlinscape@!,Zoom (In/Out),X multiplier,Y multiplier,Rotation speed,Random direction;;!;2;sx=64,c3=0,o1=1"; ///////////////////// From 1f335a9ea5a0a5d56ce46956c174b136cc462213 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Mon, 18 May 2026 01:26:03 -0700 Subject: [PATCH 9/9] Recommendation from Claude to prevent a possible overflow with angle during rotation --- usermods/user_fx/user_fx.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index edff13e022..8971f32fde 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -1314,7 +1314,7 @@ static void mode_2D_perlinscape(void) { stepX += ((targetX - stepX) * 13) >> 8; stepY += ((targetY - stepY) * 13) >> 8; - // dt in milliseconds; offX/offY accumulate in Q8 + // dt in raw milliseconds; offX/offY accumulate scaled by speedMult uint32_t udt = strip.now - prevT; int32_t dt = (udt > 500) ? 0 : (int32_t)udt; offX += (stepX * dt * speedMult) >> 13; @@ -1339,8 +1339,7 @@ static void mode_2D_perlinscape(void) { // rotate if rotation speed is not 0 if (SEGMENT.custom3 > 0) { - angle += (int32_t)SEGMENT.custom3 * dt * 2; - angle &= 0xFFFF; // wrap to 16-bit + angle = ((angle + (int32_t)SEGMENT.custom3 * dt * 2) & 0xFFFF); cosA = cos16_t((uint16_t)angle) >> 5; sinA = sin16_t((uint16_t)angle) >> 5; }