diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 036e97a75c..ff8bc9054d 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -3158,6 +3158,194 @@ static uint16_t rolling_balls(void) { static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of balls,,,,Collide,Overlay,Trails;!,!,!;!;1;m12=1"; //bar #endif // WLED_PS_DONT_REPLACE_FX + +/* +/ 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, PALETTE_SOLID_WRAP, 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 + 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"; + + /* * Sinelon stolen from FASTLED examples */ @@ -10907,6 +11095,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_BLENDS, &mode_blends, _data_FX_MODE_BLENDS); addEffect(FX_MODE_TV_SIMULATOR, &mode_tv_simulator, _data_FX_MODE_TV_SIMULATOR); addEffect(FX_MODE_DYNAMIC_SMOOTH, &mode_dynamic_smooth, _data_FX_MODE_DYNAMIC_SMOOTH); + addEffect(FX_MODE_ANTS, &mode_ants, _data_FX_MODE_ANTS); // --- 1D audio effects --- addEffect(FX_MODE_PIXELS, &mode_pixels, _data_FX_MODE_PIXELS); diff --git a/wled00/FX.h b/wled00/FX.h index 250df2646d..f5c754f634 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -378,7 +378,8 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_PS1DSONICBOOM 215 #define FX_MODE_PS1DSPRINGY 216 #define FX_MODE_PARTICLEGALAXY 217 -#define MODE_COUNT 218 +#define FX_MODE_ANTS 218 +#define MODE_COUNT 219 #define BLEND_STYLE_FADE 0x00 // universal