Skip to content

Conversation

@BobLoeffler68
Copy link
Contributor

@BobLoeffler68 BobLoeffler68 commented Jan 1, 2026

This PR will add the Lava Lamp effect into the new user_fx usermod instead of FX.cpp

  • Lava Lamp 2D by Bob Loeffler 2025
  • Created by Bob Loeffler using claude.ai Idea from Lava Lamp Effect Idea #1927
  • 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
  • First slider sets the speed of the rising and falling blobs
  • Second slider sets the number of active blobs
  • Third slider sets the size range of the blobs
  • Checkbox1 sets the color mode (color wheel or palette)
  • Checkbox2 sets the attraction of blobs (checked will make the blobs attract other close blobs horizontally)

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 1, 2026

Walkthrough

A new 2D Lava Lamp FX mode is added to the user effects module with particle system logic, palette wrapping helpers, and effect registration. The implementation includes particle state management, soft-edged additive blending, and blob attraction support.

Changes

Cohort / File(s) Summary
2D Lava Lamp FX Implementation
usermods/user_fx/user_fx.cpp
Adds mode_2D_lavalamp() function with LavaParticle data structures and particle array state management. Includes palette wrap helper functions (PALETTE_SOLID_WRAP, PALETTE_MOVING_WRAP), effect initialization/update logic with spawn/velocity mechanics, and soft-edged additive blending rendering. Registers effect via addEffect() with metadata descriptor specifying speed, blob count, size, color mode, and attract parameters.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • DedeHai
  • blazoncek

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Lava Lamp FX in the user_fx usermod' directly and clearly summarizes the main change: adding a Lava Lamp FX effect to the user_fx usermod, which matches the changeset content.
✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

5-8: Remove unused macro definitions.

These palette wrap macros are defined but never used in the file. Consider removing them unless they're intended for future use.

🔎 Proposed fix to remove unused macros
-// 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))
-

187-187: Use hw_random8() consistently for hue generation.

Both branches generate a random 0-255 value, making the conditional redundant. Since hw_random8() is already used elsewhere in WLED for better entropy, use it consistently for both color modes:

-      lavaParticles[i].hue = SEGMENT.check1 ? hw_random8() : random16(256);
+      lavaParticles[i].hue = hw_random8();
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 787d8a7 and 401cafc.

📒 Files selected for processing (1)
  • usermods/user_fx/user_fx.cpp
🧰 Additional context used
🧠 Learnings (10)
📓 Common learnings
Learnt from: DedeHai
Repo: wled/WLED PR: 4798
File: wled00/FX.cpp:7531-7533
Timestamp: 2025-08-26T11:51:21.817Z
Learning: In WLED PR #4798, DedeHai confirmed that certain gamma-related calls in FX.cpp/FX_fcn.cpp/particle systems are intentional for effect-level shaping (e.g., brightness curves, TV sim, Pride 2015 pre-mix), distinct from final output gamma. Do not flag or remove these in future reviews; add comments when feasible to clarify intent.
Learnt from: BobLoeffler68
Repo: wled/WLED PR: 5109
File: wled00/FX.cpp:3174-3343
Timestamp: 2025-11-27T06:33:11.436Z
Learning: WLED Ants effect (wled00/FX.cpp): The author prefers the current velocity initialization using hw_random16(1000, 5000)/5000.0f, resulting in an effective range of ~3.6–10.0 (with VELOCITY_MIN=2.0, VELOCITY_MAX=10.0), and wants the code kept as-is with comments updated to document this behavior. Avoid suggesting changes to span the full 2.0–10.0 range in future reviews.
📚 Learning: 2025-08-26T11:51:21.817Z
Learnt from: DedeHai
Repo: wled/WLED PR: 4798
File: wled00/FX.cpp:7531-7533
Timestamp: 2025-08-26T11:51:21.817Z
Learning: In WLED PR #4798, DedeHai confirmed that certain gamma-related calls in FX.cpp/FX_fcn.cpp/particle systems are intentional for effect-level shaping (e.g., brightness curves, TV sim, Pride 2015 pre-mix), distinct from final output gamma. Do not flag or remove these in future reviews; add comments when feasible to clarify intent.

Applied to files:

  • usermods/user_fx/user_fx.cpp
📚 Learning: 2025-06-15T09:59:52.720Z
Learnt from: netmindz
Repo: wled/WLED PR: 4728
File: wled00/FX.h:378-378
Timestamp: 2025-06-15T09:59:52.720Z
Learning: In WLED's FX.h, MODE_COUNT represents the highest FX_MODE_ ID + 1, not the total count of FX_MODE_ definitions. This is because some effects may have been removed but their IDs are not reused, creating gaps in the numbering sequence. MODE_COUNT is used for array bounds and range checking.

Applied to files:

  • usermods/user_fx/user_fx.cpp
📚 Learning: 2025-09-02T19:42:57.475Z
Learnt from: freakintoddles2
Repo: wled/WLED PR: 4904
File: wled00/FX.cpp:3915-3930
Timestamp: 2025-09-02T19:42:57.475Z
Learning: WLED wled00/FX.cpp::mode_percent — For time-based options (% of Minute/Hour/Day), behavior is clock-like: at rollover (elapsed==0) intentionally show SEGLEN lit for one tick to represent completion, then move to 1 LED at the next second. This is by design (not a progress bar) and should not be “fixed”.

Applied to files:

  • usermods/user_fx/user_fx.cpp
📚 Learning: 2025-10-20T09:41:50.374Z
Learnt from: blazoncek
Repo: wled/WLED PR: 4995
File: wled00/FX.cpp:5209-5212
Timestamp: 2025-10-20T09:41:50.374Z
Learning: In WLED 2D effects, SEGMENT.length() returns the physical pixel count (upper bound), while SEG_W * SEG_H can be smaller depending on mapping (mirroring/grouping). For per-cell state buffers (e.g., mode_2Dgameoflife in wled00/FX.cpp), allocate using SEGMENT.length() to ensure sufficient capacity and avoid reallocations, while bounding indexing by cols*rows.

Applied to files:

  • usermods/user_fx/user_fx.cpp
📚 Learning: 2025-12-28T14:06:48.764Z
Learnt from: softhack007
Repo: wled/WLED PR: 0
File: :0-0
Timestamp: 2025-12-28T14:06:48.764Z
Learning: In WLED post-commit ee9ac947, the rendering pipeline uses per-segment buffers and per-pixel bus updates. Unmapped (0xFFFF) mapping entries are now skipped in WS2812FX::show() (no “clear to black”), which can leave physical gap LEDs with stale/random colors unless they are explicitly cleared. This is a behavior change from pre-0.16 where a full physical buffer was effectively refreshed each frame.

Applied to files:

  • usermods/user_fx/user_fx.cpp
📚 Learning: 2025-11-16T19:40:46.260Z
Learnt from: DedeHai
Repo: wled/WLED PR: 4926
File: wled00/FX.cpp:4727-4730
Timestamp: 2025-11-16T19:40:46.260Z
Learning: WLED AuroraWave (wled00/FX.cpp): wave_start and wave_end intentionally use int16_t; segments longer than 32k LEDs are not supported (bounded by MAX_LEDS), so widening to 32-bit is unnecessary.

Applied to files:

  • usermods/user_fx/user_fx.cpp
📚 Learning: 2025-06-07T15:58:42.579Z
Learnt from: willmmiles
Repo: wled/WLED PR: 4712
File: wled00/FX_fcn.cpp:992-993
Timestamp: 2025-06-07T15:58:42.579Z
Learning: In WLED segments, the backing pixel buffer is always allocated to the full physical segment size, but effect rendering only uses a subset when mirror or grouping are enabled. For 2D segments, this subset is vWidth() * vHeight(), and for 1D segments it's vLength(). Color processing operations like fade_out should iterate over this rendering subset, not the full allocated buffer.

Applied to files:

  • usermods/user_fx/user_fx.cpp
📚 Learning: 2025-11-22T12:12:53.486Z
Learnt from: DedeHai
Repo: wled/WLED PR: 5105
File: wled00/FX.h:968-968
Timestamp: 2025-11-22T12:12:53.486Z
Learning: In WLED's WS2812FX class (wled00/FX.h), the _pixels array stores LED colors indexed by unmapped (logical) LED positions, not physical positions. The getPixelColor(n) function intentionally checks if the logical LED at index n maps to a valid physical position via getMappedPixelIndex(n), then accesses _pixels[n] with the unmapped index. If the mapping is invalid (e.g., -1 for gaps in LED maps, or out of bounds), it returns 0 (black). This allows peek preview to correctly show gaps in LED mappings as black pixels. The mapping from logical to physical positions is only applied when writing to the hardware bus, not when reading from the pixel buffer.
<!-- [add_learning]

Applied to files:

  • usermods/user_fx/user_fx.cpp
📚 Learning: 2025-10-20T09:38:51.997Z
Learnt from: blazoncek
Repo: wled/WLED PR: 4995
File: wled00/FX.cpp:5223-5226
Timestamp: 2025-10-20T09:38:51.997Z
Learning: WLED matrices: each dimension (SEG_W, SEG_H) is limited to ≤255; 256 or larger per side is not supported/feasible on ESP32, so effects should assume per-dimension max 255.

Applied to files:

  • usermods/user_fx/user_fx.cpp
🔇 Additional comments (4)
usermods/user_fx/user_fx.cpp (4)

122-133: Initialization logic is correct assuming the static array issue is resolved.

The 2D validation and particle initialization on first call follows standard WLED patterns.


217-238: Blob attraction logic is correct.

The condition at line 224 correctly implements "attract only when moving in opposite vertical directions" by skipping pairs where both particles move in the same direction.


240-330: LGTM: Rendering implementation is solid.

The physics simulation, boundary handling, and soft-edged additive rendering create the intended lava lamp effect. The use of qadd8() for additive blending and distance-based intensity falloff are appropriate techniques for organic blob merging.


334-334: LGTM: Effect metadata and registration follow WLED conventions.

The metadata string properly defines sliders, checkboxes, 2D flag, and defaults. Registration with id=255 is correct for user effects.

Also applies to: 348-348

bool active; // will not be displayed if false
} LavaParticle;

static LavaParticle lavaParticles[MAX_LAVA_PARTICLES];
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Static global array breaks multi-segment support.

The static global lavaParticles array is shared across all segments. If multiple segments run this effect simultaneously, they will corrupt each other's particle state, causing visual artifacts and incorrect behavior.

WLED effects should store per-segment state using SEGENV.allocateData() or manage buffers in SEGENV.data. This is consistent with the Diffusion Fire effect above (lines 36-37) which properly allocates per-segment storage.

🔎 Recommended approach

Allocate particle array in SEGENV.data using SEGENV.allocateData(sizeof(LavaParticle) * MAX_LAVA_PARTICLES) and cast the pointer:

LavaParticle* lavaParticles = reinterpret_cast<LavaParticle*>(SEGENV.data);

Initialize on first call or when allocation succeeds. This ensures each segment has its own independent particle state.

🤖 Prompt for AI Agents
In usermods/user_fx/user_fx.cpp around line 120, remove the static global
lavaParticles array and instead allocate per-segment storage via
SEGENV.allocateData(sizeof(LavaParticle) * MAX_LAVA_PARTICLES); cast SEGENV.data
to LavaParticle* (e.g., LavaParticle* lavaParticles =
reinterpret_cast<LavaParticle*>(SEGENV.data)); ensure allocation is done once
per-segment (check return of allocateData or a per-seg init flag) and initialize
the particles when allocation succeeds; replace all uses of the former global
with the per-segment pointer so each segment has independent particle state.

Comment on lines +144 to +164
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;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Static variable breaks per-segment size tracking.

The static lastSizeControl variable is shared across all segments. When multiple segments run this effect with different custom1 values, they will incorrectly detect size changes and interfere with each other's size calculations.

Use segment-specific storage such as SEGENV.aux0 to track the last size control value:

uint8_t lastSizeControl = SEGENV.aux0;
uint8_t currentSizeControl = SEGMENT.custom1;
bool sizeChanged = (currentSizeControl != lastSizeControl);

Then update at line 163:

SEGENV.aux0 = currentSizeControl;
🤖 Prompt for AI Agents
In usermods/user_fx/user_fx.cpp around lines 144 to 164, the static variable
lastSizeControl is shared across all segments causing cross-segment
interference; replace the static with segment-local storage by reading
lastSizeControl from SEGENV.aux0, compare it to SEGMENT.custom1, and after
applying size changes write the new currentSizeControl back to SEGENV.aux0 so
each segment tracks its own last size control value.

// Extract RGB
uint8_t r = (color >> 16) & 0xFF;
uint8_t g = (color >> 8) & 0xFF;
uint8_t b = color & 0xFF;
Copy link
Member

Choose a reason for hiding this comment

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

you can use the R() G() B() and W() macros here, they do the same thing

Copy link
Collaborator

Choose a reason for hiding this comment

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

better yet: use the methods from colors.cpp for scaling and blending, no need to implement them here again.

// Apply life/opacity
r = (r * p->life) >> 8;
g = (g * p->life) >> 8;
b = (b * p->life) >> 8;
Copy link
Member

Choose a reason for hiding this comment

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

did you intend to omit the "white" channel?

uint32_t existing = SEGMENT.getPixelColorXY(px, py);
uint8_t er = (existing >> 16) & 0xFF;
uint8_t eg = (existing >> 8) & 0xFF;
uint8_t eb = existing & 0xFF;
Copy link
Member

Choose a reason for hiding this comment

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

same as above - recommended to use R() G() B() and W() macros


er = qadd8(er, br);
eg = qadd8(eg, bg);
eb = qadd8(eb, bb);
Copy link
Member

Choose a reason for hiding this comment

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

the faster alternative here would be to use color_add() or color_blend(). These functions handle all color components at once.

Copy link
Collaborator

Choose a reason for hiding this comment

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

exactly, see my comment above.

@softhack007 softhack007 linked an issue Jan 1, 2026 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Lava Lamp Effect Idea

3 participants