Skip to content
192 changes: 192 additions & 0 deletions usermods/user_fx/user_fx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,197 @@ 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 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)
*/

// 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;
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) {
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<Ant*>(SEGENV.data);

// Extract configuration from segment settings
unsigned numAnts = min(1 + (SEGLEN * SEGMENT.intensity >> 12), MAX_ANTS);
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, ANT_SIZE_MIN, ANT_SIZE_MAX) + (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 Smear mode
if (!SmearMode) SEGMENT.fill(backgroundColor);

// Update and render each ant
for (int i = 0; i < numAnts; i++) {
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)
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(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
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;
ants[i].position += ants[i].velocity * adjustedTime;
ants[j].position = ants[i].position;

// Update collision time
unsigned long collisionMoment = static_cast<unsigned long>(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 * float(int(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,Smear,Pass by;!,!,!;!;1;sx=192,ix=255,c1=32,c2=0,o1=1,o3=1";



/////////////////////
// UserMod Class //
/////////////////////
Expand All @@ -98,6 +289,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 //
Expand Down