From 3a843fa86e37eba0ef11f4bd6e7581f102a4ae0f Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Tue, 30 Dec 2025 21:10:21 -0700 Subject: [PATCH 01/12] Added Lava Lamp effect to user_fx usermod --- usermods/user_fx/user_fx.cpp | 242 +++++++++++++++++++++++++++++++++++ 1 file changed, 242 insertions(+) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index da6937c87d..3fd4efc07d 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -2,12 +2,17 @@ // for information how FX metadata strings work see https://kno.wled.ge/interfaces/json-api/#effect-metadata +// paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) +#define PALETTE_SOLID_WRAP (strip.paletteBlend == 1 || strip.paletteBlend == 3) +#define PALETTE_MOVING_WRAP !(strip.paletteBlend == 2 || (strip.paletteBlend == 0 && SEGMENT.speed == 0)) + // static effect, used if an effect fails to initialize static uint16_t mode_static(void) { SEGMENT.fill(SEGCOLOR(0)); return strip.isOffRefreshRequired() ? FRAMETIME : 350; } + ///////////////////////// // User FX functions // ///////////////////////// @@ -89,6 +94,242 @@ unsigned dataSize = cols * rows; // SEGLEN (virtual length) is equivalent to vW static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spark rate,Diffusion Speed,Turbulence,,Use palette;;Color;;2;pal=35"; +/* + * Lava Lamp 2D effect + * Uses particles to simulate rising blobs of "lava" + * Particles slowly rise, merge to create organic flowing shapes, and then fall to the bottom to start again + * Created by Bob Loeffler using claude.ai + */ +#define MAX_LAVA_PARTICLES 50 + +typedef struct LavaParticle { + float x, y; // Position + float vx, vy; // Velocity + float size; // Blob size + uint8_t hue; // Color + uint8_t life; // Lifetime/opacity + bool active; // will not be displayed if false +} LavaParticle; + +static LavaParticle lavaParticles[MAX_LAVA_PARTICLES]; + +uint16_t mode_2D_lavalamp(void) { + if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up + + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + + // Initialize particles on first call + if (SEGENV.call == 0) { + for (int i = 0; i < MAX_LAVA_PARTICLES; i++) { + lavaParticles[i].active = false; + } + } + + // Speed control (slower = more lava lamp like) + uint8_t speed = SEGMENT.speed >> 3; // 0-31 range + if (speed < 1) speed = 1; + + // Intensity controls number of active particles + uint8_t numParticles = (SEGMENT.intensity >> 3) + 3; // 3-34 particles (fewer blobs) + if (numParticles > MAX_LAVA_PARTICLES) numParticles = MAX_LAVA_PARTICLES; + + // Track size slider changes + static uint8_t lastSizeControl = 128; + uint8_t currentSizeControl = SEGMENT.custom1; + bool sizeChanged = (currentSizeControl != lastSizeControl); + + if (sizeChanged) { + // Recalculate size range based on new slider value + float minSize = cols * 0.15f; + float maxSize = cols * 0.4f; + float newRange = (maxSize - minSize) * (currentSizeControl / 255.0f); + + for (int i = 0; i < MAX_LAVA_PARTICLES; i++) { + if (lavaParticles[i].active) { + // Assign new random size within the new range + lavaParticles[i].size = minSize + random16((int)(newRange * 10)) / 10.0f; + // Ensure minimum size + if (lavaParticles[i].size < minSize) lavaParticles[i].size = minSize; + } + } + + lastSizeControl = currentSizeControl; + } + + // Spawn new particles at the bottom near the center + for (int i = 0; i < MAX_LAVA_PARTICLES; i++) { + if (!lavaParticles[i].active && random8() < 32) { // Always spawn when slot available + // Spawn in the middle 60% of the width + float centerStart = cols * 0.20f; + float centerWidth = cols * 0.60f; + lavaParticles[i].x = centerStart + (random16((int)(centerWidth * 10)) / 10.0f); + lavaParticles[i].y = rows - 1; + lavaParticles[i].vx = (random16(7) - 3) / 250.0f; + + // Speed slider controls vertical velocity (faster = more speed) + float speedFactor = (SEGMENT.speed + 30) / 100.0f; // 0.3 to 2.85 range + lavaParticles[i].vy = -(random16(20) + 10) / 100.0f * speedFactor; + + // Custom1 slider controls blob size (based on matrix width) + uint8_t sizeControl = SEGMENT.custom1; // 0-255 + float minSize = cols * 0.15f; // Minimum 15% of width + float maxSize = cols * 0.4f; // Maximum 40% of width + float sizeRange = (maxSize - minSize) * (sizeControl / 255.0f); + lavaParticles[i].size = minSize + random16((int)(sizeRange * 10)) / 10.0f; + + lavaParticles[i].hue = SEGMENT.check1 ? random8() : random16(256); + lavaParticles[i].life = 255; + lavaParticles[i].active = true; + break; + } + } + + // Fade background slightly for trailing effect + SEGMENT.fadeToBlackBy(40); + + // Update and draw particles + int activeCount = 0; + for (int i = 0; i < MAX_LAVA_PARTICLES; i++) { + if (!lavaParticles[i].active) continue; + activeCount++; + + // Keep particle count on target by deactivating excess particles + if (activeCount > numParticles) { + lavaParticles[i].active = false; + activeCount--; + continue; + } + + LavaParticle *p = &lavaParticles[i]; + + // Physics update + p->x += p->vx; + p->y += p->vy; + + // Optional blob attraction (enabled with check2) + if (SEGMENT.check2) { + for (int j = 0; j < MAX_LAVA_PARTICLES; j++) { + if (i == j || !lavaParticles[j].active) continue; + + LavaParticle *other = &lavaParticles[j]; + + // Only attract if moving in opposite vertical directions + if ((p->vy < 0 && other->vy < 0) || (p->vy > 0 && other->vy > 0)) continue; + + float dx = other->x - p->x; + float dy = other->y - p->y; + float dist = sqrt(dx*dx + dy*dy); + + // Apply weak horizontal attraction only + float attractRange = (p->size + other->size) * 1.0f; + if (dist > 0 && dist < attractRange) { + // Very weak horizontal-only attraction + float force = (1.0f - (dist / attractRange)) * 0.0001f; + p->vx += (dx / dist) * force; + } + } + } + + // Horizontal oscillation (makes it more organic) + p->vx += sin((millis() / 1000.0f + i) * 0.5f) * 0.002f; // Reduced oscillation + p->vx *= 0.92f; // Stronger damping for less drift + + // Bounce off sides (don't affect vertical velocity) + if (p->x <= 0) { + p->x = 1; + p->vx = abs(p->vx); // Just reverse horizontal, don't reduce + } + if (p->x >= cols) { + p->x = cols - 1; + p->vx = -abs(p->vx); // Just reverse horizontal, don't reduce + } + + // Boundary handling with proper reversal + // When reaching TOP (y=0 area), reverse to fall back down + if (p->y <= 0.5f * p->size) { + p->y = 0.5f * p->size; + if (p->vy < 0) { + p->vy = -p->vy * 0.5f; // Reverse to positive (fall down) at HALF speed + // Ensure minimum downward velocity + if (p->vy < 0.06f) p->vy = 0.06f; + } + } + + // When reaching BOTTOM (y=rows-1 area), reverse to rise back up + if (p->y >= rows - 0.5f * p->size) { + p->y = rows - 0.5f * p->size; + if (p->vy > 0) { + p->vy = -p->vy; // Reverse to negative (rise up) + // Add random speed boost when rising + p->vy -= random16(15) / 100.0f; // Subtract to make MORE negative (faster up) + // Ensure minimum upward velocity + if (p->vy > -0.10f) p->vy = -0.10f; + } + } + + // Keep blobs alive forever - no fading + p->life = 255; + + // Get color + uint32_t color; + if (SEGMENT.check1) { + color = SEGMENT.color_wheel(p->hue); // Random colors mode + } else { + color = SEGMENT.color_from_palette(p->hue, false, true, 0); // Palette mode + } + + // Extract RGB + uint8_t r = (color >> 16) & 0xFF; + uint8_t g = (color >> 8) & 0xFF; + uint8_t b = color & 0xFF; + + // Apply life/opacity + r = (r * p->life) >> 8; + g = (g * p->life) >> 8; + b = (b * p->life) >> 8; + + // Draw blob with soft edges (gaussian-like falloff) + for (int dy = -(int)p->size; dy <= (int)p->size; dy++) { + for (int dx = -(int)p->size; dx <= (int)p->size; dx++) { + int px = (int)(p->x + dx); + int py = (int)(p->y + dy); + + if (px >= 0 && px < cols && py >= 0 && py < rows) { + float dist = sqrt(dx*dx + dy*dy); + if (dist < p->size) { + // Soft falloff + float intensity = 1.0f - (dist / p->size); + intensity = intensity * intensity; // Square for smoother falloff + + uint8_t br = r * intensity; + uint8_t bg = g * intensity; + uint8_t bb = b * intensity; + + // Additive blending for organic merging + uint32_t existing = SEGMENT.getPixelColorXY(px, py); + uint8_t er = (existing >> 16) & 0xFF; + uint8_t eg = (existing >> 8) & 0xFF; + uint8_t eb = existing & 0xFF; + + er = qadd8(er, br); + eg = qadd8(eg, bg); + eb = qadd8(eb, bb); + + SEGMENT.setPixelColorXY(px, py, RGBW32(er, eg, eb, 0)); + } + } + } + } + } + + return FRAMETIME; +} +static const char _data_FX_MODE_2D_LAVALAMP[] PROGMEM = "Lava Lamp@Speed,# of blobs,Blob size,,,Color mode,Attract;;!;2;ix=64,o2=1,pal=47"; +#undef MAX_LAVA_PARTICLES + + + ///////////////////// // UserMod Class // ///////////////////// @@ -98,6 +339,7 @@ class UserFxUsermod : public Usermod { public: void setup() override { strip.addEffect(255, &mode_diffusionfire, _data_FX_MODE_DIFFUSIONFIRE); + strip.addEffect(255, &mode_2D_lavalamp, _data_FX_MODE_2D_LAVALAMP); //////////////////////////////////////// // add your effect function(s) here // From 401cafcb2685de0d2bd27638f0c64976fe4e633b Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Wed, 31 Dec 2025 01:16:59 -0700 Subject: [PATCH 02/12] changed random8() to hw_random8() and added some comments to beginning of effect code --- usermods/user_fx/user_fx.cpp | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 3fd4efc07d..17541fc6cc 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -95,11 +95,17 @@ static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spar /* - * Lava Lamp 2D effect - * Uses particles to simulate rising blobs of "lava" - * Particles slowly rise, merge to create organic flowing shapes, and then fall to the bottom to start again - * Created by Bob Loeffler using claude.ai - */ +/ Lava Lamp 2D effect +* Uses particles to simulate rising blobs of "lava" +* Particles slowly rise, merge to create organic flowing shapes, and then fall to the bottom to start again +* Created by Bob Loeffler using claude.ai +* The first slider sets the speed of the rising and falling blobs +* The second slider sets the number of active blobs +* The third slider sets the size range of the blobs +* The first checkbox sets the color mode (color wheel or palette) +* The second checkbox sets the attraction of blobs (checked will make the blobs attract other close blobs horizontally) +*/ + #define MAX_LAVA_PARTICLES 50 typedef struct LavaParticle { @@ -159,7 +165,7 @@ uint16_t mode_2D_lavalamp(void) { // Spawn new particles at the bottom near the center for (int i = 0; i < MAX_LAVA_PARTICLES; i++) { - if (!lavaParticles[i].active && random8() < 32) { // Always spawn when slot available + if (!lavaParticles[i].active && hw_random8() < 32) { // Always spawn when slot available // Spawn in the middle 60% of the width float centerStart = cols * 0.20f; float centerWidth = cols * 0.60f; @@ -178,7 +184,7 @@ uint16_t mode_2D_lavalamp(void) { float sizeRange = (maxSize - minSize) * (sizeControl / 255.0f); lavaParticles[i].size = minSize + random16((int)(sizeRange * 10)) / 10.0f; - lavaParticles[i].hue = SEGMENT.check1 ? random8() : random16(256); + lavaParticles[i].hue = SEGMENT.check1 ? hw_random8() : random16(256); lavaParticles[i].life = 255; lavaParticles[i].active = true; break; @@ -325,7 +331,7 @@ uint16_t mode_2D_lavalamp(void) { return FRAMETIME; } -static const char _data_FX_MODE_2D_LAVALAMP[] PROGMEM = "Lava Lamp@Speed,# of blobs,Blob size,,,Color mode,Attract;;!;2;ix=64,o2=1,pal=47"; +static const char _data_FX_MODE_2D_LAVALAMP[] PROGMEM = "Lava Lamp@Speed,# of blobs,Blob size,,,Color mode,Attract;;!;2;sx=64,ix=64,o2=1,pal=47"; #undef MAX_LAVA_PARTICLES From f51357a0f9138143b550955e6c150f0f5164ed47 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Thu, 1 Jan 2026 14:22:01 -0700 Subject: [PATCH 03/12] Allocate particle array in SEGENV.data using SEGENV.allocateData --- usermods/user_fx/user_fx.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 17541fc6cc..759920e9f8 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -106,8 +106,6 @@ static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spar * The second checkbox sets the attraction of blobs (checked will make the blobs attract other close blobs horizontally) */ -#define MAX_LAVA_PARTICLES 50 - typedef struct LavaParticle { float x, y; // Position float vx, vy; // Velocity @@ -117,14 +115,17 @@ typedef struct LavaParticle { bool active; // will not be displayed if false } LavaParticle; -static LavaParticle lavaParticles[MAX_LAVA_PARTICLES]; - uint16_t mode_2D_lavalamp(void) { if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up const uint16_t cols = SEGMENT.virtualWidth(); const uint16_t rows = SEGMENT.virtualHeight(); + // Allocate per-segment storage + constexpr size_t MAX_LAVA_PARTICLES = 50; + if (!SEGENV.allocateData(sizeof(LavaParticle) * MAX_LAVA_PARTICLES)) return mode_static(); + LavaParticle* lavaParticles = reinterpret_cast(SEGENV.data); + // Initialize particles on first call if (SEGENV.call == 0) { for (int i = 0; i < MAX_LAVA_PARTICLES; i++) { @@ -159,7 +160,6 @@ uint16_t mode_2D_lavalamp(void) { if (lavaParticles[i].size < minSize) lavaParticles[i].size = minSize; } } - lastSizeControl = currentSizeControl; } @@ -184,7 +184,7 @@ uint16_t mode_2D_lavalamp(void) { float sizeRange = (maxSize - minSize) * (sizeControl / 255.0f); lavaParticles[i].size = minSize + random16((int)(sizeRange * 10)) / 10.0f; - lavaParticles[i].hue = SEGMENT.check1 ? hw_random8() : random16(256); + lavaParticles[i].hue = hw_random8(); lavaParticles[i].life = 255; lavaParticles[i].active = true; break; @@ -332,7 +332,6 @@ uint16_t mode_2D_lavalamp(void) { return FRAMETIME; } static const char _data_FX_MODE_2D_LAVALAMP[] PROGMEM = "Lava Lamp@Speed,# of blobs,Blob size,,,Color mode,Attract;;!;2;sx=64,ix=64,o2=1,pal=47"; -#undef MAX_LAVA_PARTICLES From 899393a276ad1c314967bf37d12583bd57e4b2c5 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Thu, 1 Jan 2026 14:55:54 -0700 Subject: [PATCH 04/12] Now using SEGENV.aux0 to track the last size control value. --- usermods/user_fx/user_fx.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 759920e9f8..646ccc9eed 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -104,6 +104,7 @@ static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spar * The third slider sets the size range of the blobs * The first checkbox sets the color mode (color wheel or palette) * The second checkbox sets the attraction of blobs (checked will make the blobs attract other close blobs horizontally) +* aux0 keeps track of the blob size changes */ typedef struct LavaParticle { @@ -142,7 +143,7 @@ uint16_t mode_2D_lavalamp(void) { if (numParticles > MAX_LAVA_PARTICLES) numParticles = MAX_LAVA_PARTICLES; // Track size slider changes - static uint8_t lastSizeControl = 128; + uint8_t lastSizeControl = SEGENV.aux0; uint8_t currentSizeControl = SEGMENT.custom1; bool sizeChanged = (currentSizeControl != lastSizeControl); @@ -160,7 +161,7 @@ uint16_t mode_2D_lavalamp(void) { if (lavaParticles[i].size < minSize) lavaParticles[i].size = minSize; } } - lastSizeControl = currentSizeControl; + SEGENV.aux0 = currentSizeControl; } // Spawn new particles at the bottom near the center From 2b3a89f6b9d215b97e4b466d7ccd43c1ba0b8764 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Thu, 1 Jan 2026 16:22:30 -0700 Subject: [PATCH 05/12] Added color macros and white channel to RGB, but just temporarily. --- usermods/user_fx/user_fx.cpp | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 646ccc9eed..b643c68aa0 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -287,15 +287,17 @@ uint16_t mode_2D_lavalamp(void) { } // Extract RGB - uint8_t r = (color >> 16) & 0xFF; - uint8_t g = (color >> 8) & 0xFF; - uint8_t b = color & 0xFF; - + uint8_t w = W(color); + uint8_t r = R(color); + uint8_t g = G(color); + uint8_t b = B(color); + // Apply life/opacity + w = (w * p->life) >> 8; r = (r * p->life) >> 8; g = (g * p->life) >> 8; b = (b * p->life) >> 8; - + // Draw blob with soft edges (gaussian-like falloff) for (int dy = -(int)p->size; dy <= (int)p->size; dy++) { for (int dx = -(int)p->size; dx <= (int)p->size; dx++) { @@ -309,21 +311,24 @@ uint16_t mode_2D_lavalamp(void) { float intensity = 1.0f - (dist / p->size); intensity = intensity * intensity; // Square for smoother falloff + uint8_t bw = w * intensity; uint8_t br = r * intensity; uint8_t bg = g * intensity; uint8_t bb = b * intensity; - + // Additive blending for organic merging uint32_t existing = SEGMENT.getPixelColorXY(px, py); + uint8_t ew = (existing >> 24) & 0xFF; uint8_t er = (existing >> 16) & 0xFF; uint8_t eg = (existing >> 8) & 0xFF; uint8_t eb = existing & 0xFF; - + + ew = qadd8(ew, bw); er = qadd8(er, br); eg = qadd8(eg, bg); eb = qadd8(eb, bb); - SEGMENT.setPixelColorXY(px, py, RGBW32(er, eg, eb, 0)); + SEGMENT.setPixelColorXY(px, py, RGBW32(er, eg, eb, ew)); } } } From e54ff16abb789c40cdc2be2b22582cf6aae69315 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Thu, 1 Jan 2026 17:35:30 -0700 Subject: [PATCH 06/12] Replaced qadd8() with color_add() --- usermods/user_fx/user_fx.cpp | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index b643c68aa0..c4097a6226 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -275,7 +275,7 @@ uint16_t mode_2D_lavalamp(void) { } } - // Keep blobs alive forever - no fading + // Keep blobs alive forever (no fading) - maybe change in the future? p->life = 255; // Get color @@ -286,17 +286,11 @@ uint16_t mode_2D_lavalamp(void) { color = SEGMENT.color_from_palette(p->hue, false, true, 0); // Palette mode } - // Extract RGB - uint8_t w = W(color); - uint8_t r = R(color); - uint8_t g = G(color); - uint8_t b = B(color); - - // Apply life/opacity - w = (w * p->life) >> 8; - r = (r * p->life) >> 8; - g = (g * p->life) >> 8; - b = (b * p->life) >> 8; + // Extract RGB and apply life/opacity + uint8_t w = (W(color) * p->life) >> 8; + uint8_t r = (R(color) * p->life) >> 8; + uint8_t g = (G(color) * p->life) >> 8; + uint8_t b = (B(color) * p->life) >> 8; // Draw blob with soft edges (gaussian-like falloff) for (int dy = -(int)p->size; dy <= (int)p->size; dy++) { @@ -318,17 +312,9 @@ uint16_t mode_2D_lavalamp(void) { // Additive blending for organic merging uint32_t existing = SEGMENT.getPixelColorXY(px, py); - uint8_t ew = (existing >> 24) & 0xFF; - uint8_t er = (existing >> 16) & 0xFF; - uint8_t eg = (existing >> 8) & 0xFF; - uint8_t eb = existing & 0xFF; - - ew = qadd8(ew, bw); - er = qadd8(er, br); - eg = qadd8(eg, bg); - eb = qadd8(eb, bb); - - SEGMENT.setPixelColorXY(px, py, RGBW32(er, eg, eb, ew)); + uint32_t newColor = RGBW32(br, bg, bb, bw); + uint32_t blended = color_add(existing, newColor); + SEGMENT.setPixelColorXY(px, py, blended); } } } From b5a82b123f1e9ba4b09341dcfa6785594933d251 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Thu, 1 Jan 2026 22:54:29 -0700 Subject: [PATCH 07/12] optimized distance calculation and decreased sqrt() calls in inner rendering loop, as suggested by coderabbit. --- usermods/user_fx/user_fx.cpp | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index c4097a6226..d94c58175b 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -156,7 +156,7 @@ uint16_t mode_2D_lavalamp(void) { for (int i = 0; i < MAX_LAVA_PARTICLES; i++) { if (lavaParticles[i].active) { // Assign new random size within the new range - lavaParticles[i].size = minSize + random16((int)(newRange * 10)) / 10.0f; + lavaParticles[i].size = minSize + random16((int)(newRange)) / 1.0f; // Ensure minimum size if (lavaParticles[i].size < minSize) lavaParticles[i].size = minSize; } @@ -170,7 +170,7 @@ uint16_t mode_2D_lavalamp(void) { // Spawn in the middle 60% of the width float centerStart = cols * 0.20f; float centerWidth = cols * 0.60f; - lavaParticles[i].x = centerStart + (random16((int)(centerWidth * 10)) / 10.0f); + lavaParticles[i].x = centerStart + random16((int)(centerWidth)) / 1.0f; lavaParticles[i].y = rows - 1; lavaParticles[i].vx = (random16(7) - 3) / 250.0f; @@ -183,7 +183,7 @@ uint16_t mode_2D_lavalamp(void) { float minSize = cols * 0.15f; // Minimum 15% of width float maxSize = cols * 0.4f; // Maximum 40% of width float sizeRange = (maxSize - minSize) * (sizeControl / 255.0f); - lavaParticles[i].size = minSize + random16((int)(sizeRange * 10)) / 10.0f; + lavaParticles[i].size = minSize + random16((int)(sizeRange)) / 1.0f; lavaParticles[i].hue = hw_random8(); lavaParticles[i].life = 255; @@ -226,12 +226,13 @@ uint16_t mode_2D_lavalamp(void) { float dx = other->x - p->x; float dy = other->y - p->y; - float dist = sqrt(dx*dx + dy*dy); - + // Apply weak horizontal attraction only float attractRange = (p->size + other->size) * 1.0f; - if (dist > 0 && dist < attractRange) { - // Very weak horizontal-only attraction + float distSq = dx*dx + dy*dy; + float attractRangeSq = attractRange * attractRange; + if (distSq > 0 && distSq < attractRangeSq) { + float dist = sqrt(distSq); // Only compute sqrt when needed float force = (1.0f - (dist / attractRange)) * 0.0001f; p->vx += (dx / dist) * force; } @@ -293,16 +294,17 @@ uint16_t mode_2D_lavalamp(void) { uint8_t b = (B(color) * p->life) >> 8; // Draw blob with soft edges (gaussian-like falloff) + float sizeSq = p->size * p->size; for (int dy = -(int)p->size; dy <= (int)p->size; dy++) { for (int dx = -(int)p->size; dx <= (int)p->size; dx++) { int px = (int)(p->x + dx); int py = (int)(p->y + dy); if (px >= 0 && px < cols && py >= 0 && py < rows) { - float dist = sqrt(dx*dx + dy*dy); - if (dist < p->size) { + float distSq = dx*dx + dy*dy; + if (distSq < sizeSq) { // Soft falloff - float intensity = 1.0f - (dist / p->size); + float intensity = 1.0f - sqrt(distSq) / p->size; intensity = intensity * intensity; // Square for smoother falloff uint8_t bw = w * intensity; From cc686a689f263252b350c4f8d39823ba5eda6d27 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Fri, 2 Jan 2026 08:29:24 -0700 Subject: [PATCH 08/12] Moved millis() outside of particles loop --- usermods/user_fx/user_fx.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index d94c58175b..9440262b5d 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -2,10 +2,6 @@ // for information how FX metadata strings work see https://kno.wled.ge/interfaces/json-api/#effect-metadata -// paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) -#define PALETTE_SOLID_WRAP (strip.paletteBlend == 1 || strip.paletteBlend == 3) -#define PALETTE_MOVING_WRAP !(strip.paletteBlend == 2 || (strip.paletteBlend == 0 && SEGMENT.speed == 0)) - // static effect, used if an effect fails to initialize static uint16_t mode_static(void) { SEGMENT.fill(SEGCOLOR(0)); @@ -197,6 +193,7 @@ uint16_t mode_2D_lavalamp(void) { // Update and draw particles int activeCount = 0; + unsigned long currentMillis = millis(); for (int i = 0; i < MAX_LAVA_PARTICLES; i++) { if (!lavaParticles[i].active) continue; activeCount++; @@ -240,7 +237,7 @@ uint16_t mode_2D_lavalamp(void) { } // Horizontal oscillation (makes it more organic) - p->vx += sin((millis() / 1000.0f + i) * 0.5f) * 0.002f; // Reduced oscillation + p->vx += sin((currentMillis / 1000.0f + i) * 0.5f) * 0.002f; // Reduced oscillation p->vx *= 0.92f; // Stronger damping for less drift // Bounce off sides (don't affect vertical velocity) From 4fa11fc0993124781e6127ae132441c0a554bfa5 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Sat, 3 Jan 2026 00:01:49 -0700 Subject: [PATCH 09/12] Use an explicit cast to float for clarity for 3 variables. --- usermods/user_fx/user_fx.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 9440262b5d..20be0e61e2 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -152,7 +152,7 @@ uint16_t mode_2D_lavalamp(void) { for (int i = 0; i < MAX_LAVA_PARTICLES; i++) { if (lavaParticles[i].active) { // Assign new random size within the new range - lavaParticles[i].size = minSize + random16((int)(newRange)) / 1.0f; + lavaParticles[i].size = minSize + (float)random16((int)(newRange)); // Ensure minimum size if (lavaParticles[i].size < minSize) lavaParticles[i].size = minSize; } @@ -166,7 +166,7 @@ uint16_t mode_2D_lavalamp(void) { // Spawn in the middle 60% of the width float centerStart = cols * 0.20f; float centerWidth = cols * 0.60f; - lavaParticles[i].x = centerStart + random16((int)(centerWidth)) / 1.0f; + lavaParticles[i].x = centerStart + (float)random16((int)(centerWidth)); lavaParticles[i].y = rows - 1; lavaParticles[i].vx = (random16(7) - 3) / 250.0f; @@ -179,7 +179,7 @@ uint16_t mode_2D_lavalamp(void) { float minSize = cols * 0.15f; // Minimum 15% of width float maxSize = cols * 0.4f; // Maximum 40% of width float sizeRange = (maxSize - minSize) * (sizeControl / 255.0f); - lavaParticles[i].size = minSize + random16((int)(sizeRange)) / 1.0f; + lavaParticles[i].size = minSize + (float)random16((int)(sizeRange)); lavaParticles[i].hue = hw_random8(); lavaParticles[i].life = 255; From 3bb410e927269e18bd112c4ec851a6f3ff583bac Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Sat, 3 Jan 2026 00:35:00 -0700 Subject: [PATCH 10/12] Changed max particles to 35 and changed a couple things to be more efficient in calculations. --- usermods/user_fx/user_fx.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 20be0e61e2..fb006edf74 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -119,7 +119,7 @@ uint16_t mode_2D_lavalamp(void) { const uint16_t rows = SEGMENT.virtualHeight(); // Allocate per-segment storage - constexpr size_t MAX_LAVA_PARTICLES = 50; + constexpr size_t MAX_LAVA_PARTICLES = 35; if (!SEGENV.allocateData(sizeof(LavaParticle) * MAX_LAVA_PARTICLES)) return mode_static(); LavaParticle* lavaParticles = reinterpret_cast(SEGENV.data); @@ -225,7 +225,7 @@ uint16_t mode_2D_lavalamp(void) { float dy = other->y - p->y; // Apply weak horizontal attraction only - float attractRange = (p->size + other->size) * 1.0f; + float attractRange = p->size + other->size; float distSq = dx*dx + dy*dy; float attractRangeSq = attractRange * attractRange; if (distSq > 0 && distSq < attractRangeSq) { @@ -292,6 +292,7 @@ uint16_t mode_2D_lavalamp(void) { // Draw blob with soft edges (gaussian-like falloff) float sizeSq = p->size * p->size; + float invSize = 1.0f / p->size; for (int dy = -(int)p->size; dy <= (int)p->size; dy++) { for (int dx = -(int)p->size; dx <= (int)p->size; dx++) { int px = (int)(p->x + dx); @@ -301,7 +302,7 @@ uint16_t mode_2D_lavalamp(void) { float distSq = dx*dx + dy*dy; if (distSq < sizeSq) { // Soft falloff - float intensity = 1.0f - sqrt(distSq) / p->size; + float intensity = 1.0f - sqrt(distSq) * invSize; intensity = intensity * intensity; // Square for smoother falloff uint8_t bw = w * intensity; @@ -322,7 +323,7 @@ uint16_t mode_2D_lavalamp(void) { return FRAMETIME; } -static const char _data_FX_MODE_2D_LAVALAMP[] PROGMEM = "Lava Lamp@Speed,# of blobs,Blob size,,,Color mode,Attract;;!;2;sx=64,ix=64,o2=1,pal=47"; +static const char _data_FX_MODE_2D_LAVALAMP[] PROGMEM = "Lava Lamp@Speed,# of blobs,Blob size,,,Color mode,Attract;;!;2;ix=64,o2=1,pal=47"; From 4e66fe6552c5e78c40af9c75189f040bf9f42800 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Sat, 3 Jan 2026 13:49:31 -0700 Subject: [PATCH 11/12] A couple optimizations --- usermods/user_fx/user_fx.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index fb006edf74..661e631579 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -162,7 +162,7 @@ uint16_t mode_2D_lavalamp(void) { // Spawn new particles at the bottom near the center for (int i = 0; i < MAX_LAVA_PARTICLES; i++) { - if (!lavaParticles[i].active && hw_random8() < 32) { // Always spawn when slot available + if (!lavaParticles[i].active && hw_random8() < 32) { // sporadically spawn when slot available // Spawn in the middle 60% of the width float centerStart = cols * 0.20f; float centerWidth = cols * 0.60f; @@ -193,7 +193,7 @@ uint16_t mode_2D_lavalamp(void) { // Update and draw particles int activeCount = 0; - unsigned long currentMillis = millis(); + unsigned long currentMillis = strip.now; for (int i = 0; i < MAX_LAVA_PARTICLES; i++) { if (!lavaParticles[i].active) continue; activeCount++; @@ -292,7 +292,6 @@ uint16_t mode_2D_lavalamp(void) { // Draw blob with soft edges (gaussian-like falloff) float sizeSq = p->size * p->size; - float invSize = 1.0f / p->size; for (int dy = -(int)p->size; dy <= (int)p->size; dy++) { for (int dx = -(int)p->size; dx <= (int)p->size; dx++) { int px = (int)(p->x + dx); @@ -301,8 +300,8 @@ uint16_t mode_2D_lavalamp(void) { if (px >= 0 && px < cols && py >= 0 && py < rows) { float distSq = dx*dx + dy*dy; if (distSq < sizeSq) { - // Soft falloff - float intensity = 1.0f - sqrt(distSq) * invSize; + // Soft falloff using squared distance (faster) + float intensity = 1.0f - (distSq / sizeSq); intensity = intensity * intensity; // Square for smoother falloff uint8_t bw = w * intensity; From 98903a35562577ded3355d8df11e78a4ac1bfd82 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Sat, 3 Jan 2026 14:20:33 -0700 Subject: [PATCH 12/12] changed a comment regarding attraction --- 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 661e631579..4d64dd568e 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -218,7 +218,7 @@ uint16_t mode_2D_lavalamp(void) { LavaParticle *other = &lavaParticles[j]; - // Only attract if moving in opposite vertical directions + // Skip attraction if moving in same vertical direction (both up or both down) if ((p->vy < 0 && other->vy < 0) || (p->vy > 0 && other->vy > 0)) continue; float dx = other->x - p->x;