From 991f9b04da4ffad268596eb17158fe676a1dfe1a Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Sun, 21 Dec 2025 16:51:59 -0700 Subject: [PATCH 1/8] Added the Ants effect to the user_fx usermod --- usermods/user_fx/user_fx.cpp | 188 +++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index da6937c87d..3905c5cd0c 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -89,6 +89,193 @@ 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"; +/* +/ Ants (created by making modifications to the Rolling Balls code) - Bob Loeffler 2025 +* First slider is for the ants' speed. +* Second slider is for the # of ants. +* Third slider is for the Ants' size. +* Fourth slider (custom2) is for blurring the LEDs in the segment. +* Checkbox1 is for Gathering food (enabled if you want the ants to gather food, disabled if they are just walking). +* We will switch directions when they get to the beginning or end of the segment when gathering food. +* When gathering food, the Pass By option will automatically be enabled so they can drop off their food easier (and look for more food). +* Checkbox2 is for Overlay mode (enabled is Overlay, disabled is no overlay) +* Checkbox3 is for whether the ants will bump into each other (disabled) or just pass by each other (enabled) +*/ +// Ant structure representing each ant's state +struct Ant { + unsigned long lastBumpUpdate; // the last time the ant bumped into another ant + bool hasFood; + float velocity; + float position; // (0.0 to 1.0 range) +}; + +constexpr unsigned MAX_ANTS = 32; +constexpr float MIN_COLLISION_TIME_MS = 2.0f; +constexpr float VELOCITY_MIN = 2.0f; +constexpr float VELOCITY_MAX = 10.0f; + +// Helper function to get food pixel color based on ant and background colors +static uint32_t getFoodColor(uint32_t antColor, uint32_t backgroundColor) { + if (antColor == WHITE) + return (backgroundColor == YELLOW) ? GRAY : YELLOW; + return (backgroundColor == WHITE) ? YELLOW : WHITE; +} + +// Helper function to handle ant boundary wrapping or bouncing +static void handleBoundary(Ant& ant, float& position, bool gatherFood, bool atStart, unsigned long currentTime) { + if (gatherFood) { + // Bounce mode: reverse direction and update food status + position = atStart ? 0.0f : 1.0f; + ant.velocity = -ant.velocity; + ant.lastBumpUpdate = currentTime; + ant.position = position; + ant.hasFood = atStart; // Has food when leaving start, drops it at end + } else { + // Wrap mode: teleport to opposite end + position = atStart ? 1.0f : 0.0f; + ant.lastBumpUpdate = currentTime; + ant.position = position; + } +} + +// Helper function to calculate ant color +static uint32_t getAntColor(int antIndex, int numAnts, bool usePalette) { + if (usePalette) + return SEGMENT.color_from_palette(antIndex * 255 / numAnts, false, (strip.paletteBlend == 1 || strip.paletteBlend == 3), 255); + // Alternate between two colors for default palette + return (antIndex % 3 == 1) ? SEGCOLOR(0) : SEGCOLOR(2); +} + +// Helper function to render a single ant pixel with food handling +static void renderAntPixel(int pixelIndex, int pixelOffset, int antSize, const Ant& ant, uint32_t antColor, uint32_t backgroundColor, bool gatherFood) { + bool isMovingBackward = (ant.velocity < 0); + bool isFoodPixel = gatherFood && ant.hasFood && ((isMovingBackward && pixelOffset == 0) || (!isMovingBackward && pixelOffset == antSize - 1)); + if (isFoodPixel) { + SEGMENT.setPixelColor(pixelIndex, getFoodColor(antColor, backgroundColor)); + } else { + SEGMENT.setPixelColor(pixelIndex, antColor); + } +} + +static uint16_t mode_ants(void) { + if (SEGLEN <= 1) return mode_static(); + + // Allocate memory for ant data + uint32_t backgroundColor = SEGCOLOR(1); + unsigned dataSize = sizeof(Ant) * MAX_ANTS; + if (!SEGENV.allocateData(dataSize)) return mode_static(); // Allocation failed + + Ant* ants = reinterpret_cast(SEGENV.data); + + // Extract configuration from segment settings + unsigned numAnts = min(1 + (SEGLEN * SEGMENT.intensity >> 12), MAX_ANTS); + bool gatherFood = SEGMENT.check1; + bool overlayMode = SEGMENT.check2; + bool passBy = SEGMENT.check3 || gatherFood; // global no‑collision when gathering food is enabled + unsigned antSize = map(SEGMENT.custom1, 0, 255, 1, 20) + (gatherFood ? 1 : 0); + + // Initialize ants on first call + if (SEGENV.call == 0) { + int confusedAntIndex = hw_random(0, numAnts); // the first random ant to go backwards + + for (int i = 0; i < MAX_ANTS; i++) { + ants[i].lastBumpUpdate = strip.now; + + // Random velocity + float velocity = VELOCITY_MIN + (VELOCITY_MAX - VELOCITY_MIN) * hw_random16(1000, 5000) / 5000.0f; + // One random ant moves in opposite direction + ants[i].velocity = (i == confusedAntIndex) ? -velocity : velocity; + // Random starting position (0.0 to 1.0) + ants[i].position = hw_random16(0, 10000) / 10000.0f; + // Ants don't have food yet + ants[i].hasFood = false; + } + } + + // Calculate time conversion factor based on speed slider + float timeConversionFactor = float(scale8(8, 255 - SEGMENT.speed) + 1) * 20000.0f; + + // Clear background if not in overlay mode + if (!overlayMode) SEGMENT.fill(backgroundColor); + + // Update and render each ant + for (int i = 0; i < numAnts; i++) { + float timeSinceLastUpdate = float(strip.now - ants[i].lastBumpUpdate) / timeConversionFactor; + float newPosition = ants[i].position + ants[i].velocity * timeSinceLastUpdate; + + // Reset ants that wandered too far off-track (e.g., after intensity change) + if (newPosition < -0.5f || newPosition > 1.5f) { + newPosition = ants[i].position = hw_random16(0, 10000) / 10000.0f; + ants[i].lastBumpUpdate = strip.now; + } + + // Handle boundary conditions (bounce or wrap) + if (newPosition <= 0.0f && ants[i].velocity < 0.0f) { + handleBoundary(ants[i], newPosition, gatherFood, true, strip.now); + } else if (newPosition >= 1.0f && ants[i].velocity > 0.0f) { + handleBoundary(ants[i], newPosition, gatherFood, false, strip.now); + } + + // Handle collisions between ants (if not passing by) + if (!passBy) { + for (int j = i + 1; j < numAnts; j++) { + if (fabsf(ants[j].velocity - ants[i].velocity) < 0.001f) continue; // Moving in same direction at same speed; avoids tiny denominators + + // Calculate collision time using physics - collisionTime formula adapted from rolling_balls + float timeOffset = float(ants[j].lastBumpUpdate - ants[i].lastBumpUpdate); + float collisionTime = (timeConversionFactor * (ants[i].position - ants[j].position) + ants[i].velocity * timeOffset) / (ants[j].velocity - ants[i].velocity); + + // Check if collision occurred in valid time window + float timeSinceJ = float(strip.now - ants[j].lastBumpUpdate); + if (collisionTime > MIN_COLLISION_TIME_MS && collisionTime < timeSinceJ) { + // Update positions to collision point + float adjustedTime = (collisionTime + float(ants[j].lastBumpUpdate - ants[i].lastBumpUpdate)) / timeConversionFactor; + ants[i].position += ants[i].velocity * adjustedTime; + ants[j].position = ants[i].position; + + // Update collision time + unsigned long collisionMoment = static_cast(collisionTime + 0.5f) + ants[j].lastBumpUpdate; + ants[i].lastBumpUpdate = collisionMoment; + ants[j].lastBumpUpdate = collisionMoment; + + // Reverse the ant with greater speed magnitude + if (fabsf(ants[i].velocity) > fabsf(ants[j].velocity)) { + ants[i].velocity = -ants[i].velocity; + } else { + ants[j].velocity = -ants[j].velocity; + } + + // Recalculate position after collision + newPosition = ants[i].position + ants[i].velocity * (strip.now - ants[i].lastBumpUpdate) / timeConversionFactor; + } + } + } + + // Clamp position to valid range + newPosition = constrain(newPosition, 0.0f, 1.0f); + unsigned pixelPosition = roundf(newPosition * (SEGLEN - 1)); + + // Determine ant color + uint32_t antColor = getAntColor(i, numAnts, SEGMENT.palette != 0); + + // Render ant pixels + for (int pixelOffset = 0; pixelOffset < antSize; pixelOffset++) { + unsigned currentPixel = pixelPosition + pixelOffset; + if (currentPixel >= SEGLEN) break; + renderAntPixel(currentPixel, pixelOffset, antSize, ants[i], antColor, backgroundColor, gatherFood); + } + + // Update ant state + ants[i].lastBumpUpdate = strip.now; + ants[i].position = newPosition; + } + + SEGMENT.blur(SEGMENT.custom2>>1); + return FRAMETIME; +} +static const char _data_FX_MODE_ANTS[] PROGMEM = "Ants@Ant speed,# of ants,Ant size,Blur,,Gathering food,Overlay,Pass by;!,!,!;!;1;sx=192,ix=255,c1=32,c2=0,o1=1,o3=1"; + + ///////////////////// // UserMod Class // ///////////////////// @@ -98,6 +285,7 @@ class UserFxUsermod : public Usermod { public: void setup() override { strip.addEffect(255, &mode_diffusionfire, _data_FX_MODE_DIFFUSIONFIRE); + strip.addEffect(255, &mode_ants, _data_FX_MODE_ANTS); //////////////////////////////////////// // add your effect function(s) here // From 4aec306ddaaaf0f64cb57895077e0d41e438f9bc Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Sun, 21 Dec 2025 17:52:22 -0700 Subject: [PATCH 2/8] Renamed the Overlay option to Smear --- usermods/user_fx/user_fx.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 3905c5cd0c..15c1a9eb13 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -170,7 +170,7 @@ static uint16_t mode_ants(void) { // Extract configuration from segment settings unsigned numAnts = min(1 + (SEGLEN * SEGMENT.intensity >> 12), MAX_ANTS); bool gatherFood = SEGMENT.check1; - bool overlayMode = SEGMENT.check2; + bool SmearMode = SEGMENT.check2; bool passBy = SEGMENT.check3 || gatherFood; // global no‑collision when gathering food is enabled unsigned antSize = map(SEGMENT.custom1, 0, 255, 1, 20) + (gatherFood ? 1 : 0); @@ -195,8 +195,8 @@ static uint16_t mode_ants(void) { // Calculate time conversion factor based on speed slider float timeConversionFactor = float(scale8(8, 255 - SEGMENT.speed) + 1) * 20000.0f; - // Clear background if not in overlay mode - if (!overlayMode) SEGMENT.fill(backgroundColor); + // Clear background if not in Smear mode + if (!SmearMode) SEGMENT.fill(backgroundColor); // Update and render each ant for (int i = 0; i < numAnts; i++) { @@ -273,7 +273,7 @@ static uint16_t mode_ants(void) { SEGMENT.blur(SEGMENT.custom2>>1); return FRAMETIME; } -static const char _data_FX_MODE_ANTS[] PROGMEM = "Ants@Ant speed,# of ants,Ant size,Blur,,Gathering food,Overlay,Pass by;!,!,!;!;1;sx=192,ix=255,c1=32,c2=0,o1=1,o3=1"; +static const char _data_FX_MODE_ANTS[] PROGMEM = "Ants@Ant speed,# of ants,Ant size,Blur,,Gathering food,Smear,Pass by;!,!,!;!;1;sx=192,ix=255,c1=32,c2=0,o1=1,o3=1"; ///////////////////// From 9743e076b13bffe67a8ba1e8d04e5f6cd7546669 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Wed, 31 Dec 2025 01:03:09 -0700 Subject: [PATCH 3/8] cosmetic changes --- usermods/user_fx/user_fx.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 15c1a9eb13..c6255e0afd 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -101,6 +101,7 @@ static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spar * Checkbox2 is for Overlay mode (enabled is Overlay, disabled is no overlay) * Checkbox3 is for whether the ants will bump into each other (disabled) or just pass by each other (enabled) */ + // Ant structure representing each ant's state struct Ant { unsigned long lastBumpUpdate; // the last time the ant bumped into another ant @@ -276,6 +277,7 @@ static uint16_t mode_ants(void) { static const char _data_FX_MODE_ANTS[] PROGMEM = "Ants@Ant speed,# of ants,Ant size,Blur,,Gathering food,Smear,Pass by;!,!,!;!;1;sx=192,ix=255,c1=32,c2=0,o1=1,o3=1"; + ///////////////////// // UserMod Class // ///////////////////// From 5182b91d4f9115181dbee0e0b2e93fc4e5ffa73c Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Thu, 1 Jan 2026 09:13:15 -0700 Subject: [PATCH 4/8] casted the lastBumpUpdate difference to int before casting to float as suggested by softhack007 --- 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 c6255e0afd..9c7b57ce20 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -223,7 +223,7 @@ static uint16_t mode_ants(void) { if (fabsf(ants[j].velocity - ants[i].velocity) < 0.001f) continue; // Moving in same direction at same speed; avoids tiny denominators // Calculate collision time using physics - collisionTime formula adapted from rolling_balls - float timeOffset = float(ants[j].lastBumpUpdate - ants[i].lastBumpUpdate); + float timeOffset = float(int(ants[j].lastBumpUpdate - ants[i].lastBumpUpdate)); float collisionTime = (timeConversionFactor * (ants[i].position - ants[j].position) + ants[i].velocity * timeOffset) / (ants[j].velocity - ants[i].velocity); // Check if collision occurred in valid time window From 4231f8b5981d64e9d6851e5f0fac8e65275302a5 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Thu, 1 Jan 2026 11:28:34 -0700 Subject: [PATCH 5/8] Applied timestamp casting to int and then to float for adjustedTime --- 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 9c7b57ce20..e9931b52d5 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -230,7 +230,7 @@ static uint16_t mode_ants(void) { float timeSinceJ = float(strip.now - ants[j].lastBumpUpdate); if (collisionTime > MIN_COLLISION_TIME_MS && collisionTime < timeSinceJ) { // Update positions to collision point - float adjustedTime = (collisionTime + float(ants[j].lastBumpUpdate - ants[i].lastBumpUpdate)) / timeConversionFactor; + float adjustedTime = (collisionTime + float(int(ants[j].lastBumpUpdate - ants[i].lastBumpUpdate))) / timeConversionFactor; ants[i].position += ants[i].velocity * adjustedTime; ants[j].position = ants[i].position; From 84053e5d78a2fa4078b0332d520dde21dedc6fed Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Thu, 1 Jan 2026 12:54:07 -0700 Subject: [PATCH 6/8] Applied timestamp casting fix for ESP32-C3 compatibility. --- 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 e9931b52d5..15e1129ce2 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -201,7 +201,7 @@ static uint16_t mode_ants(void) { // Update and render each ant for (int i = 0; i < numAnts; i++) { - float timeSinceLastUpdate = float(strip.now - ants[i].lastBumpUpdate) / timeConversionFactor; + float timeSinceLastUpdate = float(int(strip.now - ants[i].lastBumpUpdate)) / timeConversionFactor; float newPosition = ants[i].position + ants[i].velocity * timeSinceLastUpdate; // Reset ants that wandered too far off-track (e.g., after intensity change) @@ -227,7 +227,7 @@ static uint16_t mode_ants(void) { float collisionTime = (timeConversionFactor * (ants[i].position - ants[j].position) + ants[i].velocity * timeOffset) / (ants[j].velocity - ants[i].velocity); // Check if collision occurred in valid time window - float timeSinceJ = float(strip.now - ants[j].lastBumpUpdate); + float timeSinceJ = float(int(strip.now - ants[j].lastBumpUpdate)); if (collisionTime > MIN_COLLISION_TIME_MS && collisionTime < timeSinceJ) { // Update positions to collision point float adjustedTime = (collisionTime + float(int(ants[j].lastBumpUpdate - ants[i].lastBumpUpdate))) / timeConversionFactor; @@ -247,7 +247,7 @@ static uint16_t mode_ants(void) { } // Recalculate position after collision - newPosition = ants[i].position + ants[i].velocity * (strip.now - ants[i].lastBumpUpdate) / timeConversionFactor; + newPosition = ants[i].position + ants[i].velocity * float(int(strip.now - ants[i].lastBumpUpdate)) / timeConversionFactor; } } } From 41f5e03a4c2dfcf882dc46e8989e010f9fefface Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Thu, 1 Jan 2026 13:14:08 -0700 Subject: [PATCH 7/8] defined constants for the ant size range --- usermods/user_fx/user_fx.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 15e1129ce2..f747f7d5a0 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -114,6 +114,8 @@ constexpr unsigned MAX_ANTS = 32; constexpr float MIN_COLLISION_TIME_MS = 2.0f; constexpr float VELOCITY_MIN = 2.0f; constexpr float VELOCITY_MAX = 10.0f; +constexpr unsigned ANT_SIZE_MIN = 1; +constexpr unsigned ANT_SIZE_MAX = 20; // Helper function to get food pixel color based on ant and background colors static uint32_t getFoodColor(uint32_t antColor, uint32_t backgroundColor) { @@ -173,7 +175,7 @@ static uint16_t mode_ants(void) { bool gatherFood = SEGMENT.check1; bool SmearMode = SEGMENT.check2; bool passBy = SEGMENT.check3 || gatherFood; // global no‑collision when gathering food is enabled - unsigned antSize = map(SEGMENT.custom1, 0, 255, 1, 20) + (gatherFood ? 1 : 0); + unsigned antSize = map(SEGMENT.custom1, 0, 255, ANT_SIZE_MIN, ANT_SIZE_MAX) + (gatherFood ? 1 : 0); // Initialize ants on first call if (SEGENV.call == 0) { From a89a422bfdc23f7ee73ca157f8028285b7da0959 Mon Sep 17 00:00:00 2001 From: Bob Loeffler Date: Thu, 1 Jan 2026 13:28:13 -0700 Subject: [PATCH 8/8] Edited comment for Smear mode --- 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 f747f7d5a0..f0a7264fbd 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -98,7 +98,7 @@ static const char _data_FX_MODE_DIFFUSIONFIRE[] PROGMEM = "Diffusion Fire@!,Spar * Checkbox1 is for Gathering food (enabled if you want the ants to gather food, disabled if they are just walking). * We will switch directions when they get to the beginning or end of the segment when gathering food. * When gathering food, the Pass By option will automatically be enabled so they can drop off their food easier (and look for more food). -* Checkbox2 is for Overlay mode (enabled is Overlay, disabled is no overlay) +* Checkbox2 is for Smear mode (enabled is smear pixel colors, disabled is no smearing) * Checkbox3 is for whether the ants will bump into each other (disabled) or just pass by each other (enabled) */