Lava Lamp FX in the user_fx usermod (#5253)

* Added Lava Lamp effect to user_fx usermod
This commit is contained in:
BobLoeffler68
2026-03-04 02:05:52 -07:00
committed by GitHub
parent 0e1da4f004
commit 278a1fb6c1

View File

@@ -96,6 +96,292 @@ 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" or wax
* 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 number of active blobs
* The second slider sets the size range of the blobs
* The third slider sets the damping value for horizontal blob movement
* The Attract checkbox sets the attraction of blobs (checked will make the blobs attract other close blobs horizontally)
* The Keep Color Ratio checkbox sets whether we preserve the color ratio when displaying pixels that are in 2 or more overlapping blobs
* aux0 keeps track of the blob size value
* aux1 keeps track of the number of blobs
*/
typedef struct LavaParticle {
float x, y; // Position
float vx, vy; // Velocity
float size; // Blob size
uint8_t hue; // Color
bool active; // will not be displayed if false
uint16_t delayTop; // number of frames to wait at top before falling again
bool idleTop; // sitting idle at the top
uint16_t delayBottom; // number of frames to wait at bottom before rising again
bool idleBottom; // sitting idle at the bottom
} LavaParticle;
static void mode_2D_lavalamp(void) {
if (!strip.isMatrix || !SEGMENT.is2D()) FX_FALLBACK_STATIC; // not a 2D set-up
const uint16_t cols = SEG_W;
const uint16_t rows = SEG_H;
constexpr float MAX_BLOB_RADIUS = 20.0f; // cap to prevent frame rate drops on large matrices
constexpr size_t MAX_LAVA_PARTICLES = 34; // increasing this value could cause slowness for large matrices
constexpr size_t MAX_TOP_FPS_DELAY = 900; // max delay when particles are at the top
constexpr size_t MAX_BOTTOM_FPS_DELAY = 1200; // max delay when particles are at the bottom
// Allocate per-segment storage
if (!SEGENV.allocateData(sizeof(LavaParticle) * MAX_LAVA_PARTICLES)) FX_FALLBACK_STATIC;
LavaParticle* lavaParticles = reinterpret_cast<LavaParticle*>(SEGENV.data);
// Initialize particles on first call
if (SEGENV.call == 0) {
for (int i = 0; i < MAX_LAVA_PARTICLES; i++) {
lavaParticles[i].active = false;
}
}
// Track particle size and particle count slider changes, re-initialize if either changes
uint8_t currentNumParticles = (SEGMENT.intensity >> 3) + 3;
uint8_t currentSize = SEGMENT.custom1;
if (currentNumParticles > MAX_LAVA_PARTICLES) currentNumParticles = MAX_LAVA_PARTICLES;
bool needsReinit = (currentSize != SEGENV.aux0) || (currentNumParticles != SEGENV.aux1);
if (needsReinit) {
for (int i = 0; i < MAX_LAVA_PARTICLES; i++) {
lavaParticles[i].active = false;
}
SEGENV.aux0 = currentSize;
SEGENV.aux1 = currentNumParticles;
}
uint8_t size = currentSize;
uint8_t numParticles = currentNumParticles;
// blob size based on matrix width
const float minSize = cols * 0.15f; // Minimum 15% of width
const float maxSize = cols * 0.4f; // Maximum 40% of width
float sizeRange = (maxSize - minSize) * (size / 255.0f);
int rangeInt = max(1, (int)(sizeRange));
// calculate the spawning area for the particles
const float spawnXStart = cols * 0.20f;
const float spawnXWidth = cols * 0.60f;
int spawnX = max(1, (int)(spawnXWidth));
bool preserveColorRatio = SEGMENT.check3;
// 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) { // spawn when slot available
// Spawn in the middle 60% of the matrix width
lavaParticles[i].x = spawnXStart + (float)hw_random16(spawnX);
lavaParticles[i].y = rows - 1;
lavaParticles[i].vx = (hw_random16(7) - 3) / 250.0f;
lavaParticles[i].vy = -(hw_random16(20) + 10) / 100.0f * 0.3f;
lavaParticles[i].size = minSize + (float)hw_random16(rangeInt);
if (lavaParticles[i].size > MAX_BLOB_RADIUS) lavaParticles[i].size = MAX_BLOB_RADIUS;
lavaParticles[i].hue = hw_random8();
lavaParticles[i].active = true;
// Set random delays when particles are at top and bottom
lavaParticles[i].delayTop = hw_random16(MAX_TOP_FPS_DELAY);
lavaParticles[i].delayBottom = hw_random16(MAX_BOTTOM_FPS_DELAY);
lavaParticles[i].idleBottom = true;
break;
}
}
// Fade background slightly for trailing effect
SEGMENT.fadeToBlackBy(40);
// Update and draw particles
int activeCount = 0;
unsigned long currentMillis = strip.now;
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 particle/blob attraction
if (SEGMENT.check2) {
for (int j = 0; j < MAX_LAVA_PARTICLES; j++) {
if (i == j || !lavaParticles[j].active) continue;
LavaParticle *other = &lavaParticles[j];
// 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;
float dy = other->y - p->y;
// Apply weak horizontal attraction only
float attractRange = p->size + other->size;
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;
}
}
}
// Horizontal oscillation (makes it more organic)
float damping= map(SEGMENT.custom2, 0, 255, 97, 87) / 100.0f;
p->vx += sin((currentMillis / 1000.0f + i) * 0.5f) * 0.002f; // Reduced oscillation
p->vx *= damping; // damping for more or less horizontal drift
// Bounce off sides (don't affect vertical velocity)
if (p->x < 0) {
p->x = 0;
p->vx = abs(p->vx); // reverse horizontal
}
if (p->x >= cols) {
p->x = cols - 1;
p->vx = -abs(p->vx); // reverse horizontal
}
// Adjust rise/fall velocity depending on approx distance from heat source (at bottom)
// In top 1/4th of rows...
if (p->y < rows * .25f) {
if (p->vy >= 0) { // if going down, delay the particles so they won't go down immediately
if (p->delayTop > 0 && p->idleTop) {
p->vy = 0.0f;
p->delayTop--;
p->idleTop = true;
} else {
p->vy = 0.01f;
p->delayTop = hw_random16(MAX_TOP_FPS_DELAY);
p->idleTop = false;
}
} else if (p->vy <= 0) { // if going up, slow down the rise rate
p->vy = -0.03f;
}
}
// In next 1/4th of rows...
if (p->y <= rows * .50f && p->y >= rows * .25f) {
if (p->vy > 0) { // if going down, speed up the fall rate
p->vy = 0.03f;
} else if (p->vy <= 0) { // if going up, speed up the rise rate a little more
p->vy = -0.05f;
}
}
// In next 1/4th of rows...
if (p->y <= rows * .75f && p->y >= rows * .50f) {
if (p->vy > 0) { // if going down, speed up the fall rate a little more
p->vy = 0.04f;
} else if (p->vy <= 0) { // if going up, speed up the rise rate
p->vy = -0.03f;
}
}
// In bottom 1/4th of rows...
if (p->y > rows * .75f) {
if (p->vy >= 0) { // if going down, slow down the fall rate
p->vy = 0.02f;
} else if (p->vy <= 0) { // if going up, delay the particles so they won't go up immediately
if (p->delayBottom > 0 && p->idleBottom) {
p->vy = 0.0f;
p->delayBottom--;
p->idleBottom = true;
} else {
p->vy = -0.01f;
p->delayBottom = hw_random16(MAX_BOTTOM_FPS_DELAY);
p->idleBottom = false;
}
}
}
// Boundary handling with reversal of direction
// When reaching TOP (y=0 area), reverse to fall back down, but need to delay first
if (p->y <= 0.5f * p->size) {
p->y = 0.5f * p->size;
if (p->vy < 0) {
p->vy = 0.005f; // set to a tiny positive value to start falling very slowly
p->idleTop = true;
}
}
// When reaching BOTTOM (y=rows-1 area), reverse to rise back up, but need to delay first
if (p->y >= rows - 0.5f * p->size) {
p->y = rows - 0.5f * p->size;
if (p->vy > 0) {
p->vy = -0.005f; // set to a tiny negative value to start rising very slowly
p->idleBottom = true;
}
}
// Get color
uint32_t color;
color = SEGMENT.color_from_palette(p->hue, true, PALETTE_SOLID_WRAP, 0);
// Extract RGB and apply life/opacity
uint8_t w = (W(color) * 255) >> 8;
uint8_t r = (R(color) * 255) >> 8;
uint8_t g = (G(color) * 255) >> 8;
uint8_t b = (B(color) * 255) >> 8;
// Draw blob with sub-pixel accuracy using bilinear distribution
float sizeSq = p->size * p->size;
// Get fractional offsets of particle center
float fracX = p->x - floorf(p->x);
float fracY = p->y - floorf(p->y);
int centerX = (int)floorf(p->x);
int centerY = (int)floorf(p->y);
for (int dy = -(int)p->size - 1; dy <= (int)p->size + 1; dy++) {
for (int dx = -(int)p->size - 1; dx <= (int)p->size + 1; dx++) {
int px = centerX + dx;
int py = centerY + dy;
if (px < 0 || px >= cols || py < 0 || py >= rows) continue;
// Sub-pixel distance: measure from true float center to pixel center
float subDx = dx - fracX; // distance from true center to this pixel's center
float subDy = dy - fracY;
float distSq = subDx * subDx + subDy * subDy;
if (distSq < sizeSq) {
float intensity = 1.0f - (distSq / sizeSq);
intensity = intensity * intensity; // smooth falloff
uint8_t bw = (uint8_t)(w * intensity);
uint8_t br = (uint8_t)(r * intensity);
uint8_t bg = (uint8_t)(g * intensity);
uint8_t bb = (uint8_t)(b * intensity);
uint32_t existing = SEGMENT.getPixelColorXY(px, py);
uint32_t newColor = RGBW32(br, bg, bb, bw);
SEGMENT.setPixelColorXY(px, py, color_add(existing, newColor, preserveColorRatio ? true : false));
}
}
}
}
}
static const char _data_FX_MODE_2D_LAVALAMP[] PROGMEM = "Lava Lamp@,# of blobs,Blob size,H. Damping,,,Attract,Keep Color Ratio;;!;2;ix=64,c2=192,o2=1,o3=1,pal=47";
/*
/ Magma effect
* 2D magma/lava animation
@@ -263,17 +549,17 @@ static void mode_2D_magma(void) {
float gravity = map(SEGMENT.custom2, 0, 255, 5, 20) / 100.0f;
// Number of particles (lava bombs)
uint8_t particleCount = map(SEGMENT.custom1, 0, 255, 2, MAGMA_MAX_PARTICLES);
particleCount = constrain(particleCount, 2, MAGMA_MAX_PARTICLES);
uint8_t particleCount = map(SEGMENT.custom1, 0, 255, 0, MAGMA_MAX_PARTICLES);
particleCount = constrain(particleCount, 0, MAGMA_MAX_PARTICLES);
// Draw lava bombs in front of magma (or behind it)
if (SEGMENT.check2) {
drawMagma(width, height, ff_y, ff_z, shiftHue);
SEGMENT.fadeToBlackBy(70); // Dim the entire display to create trailing effect
drawLavaBombs(width, height, particleData, gravity, particleCount);
if (particleCount > 0) drawLavaBombs(width, height, particleData, gravity, particleCount);
}
else {
drawLavaBombs(width, height, particleData, gravity, particleCount);
if (particleCount > 0) drawLavaBombs(width, height, particleData, gravity, particleCount);
SEGMENT.fadeToBlackBy(70); // Dim the entire display to create trailing effect
drawMagma(width, height, ff_y, ff_z, shiftHue);
}
@@ -737,6 +1023,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);
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);