Skip to content
Merged
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 190 additions & 0 deletions usermods/user_fx/user_fx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,195 @@ 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);
}
Comment thread
BobLoeffler68 marked this conversation as resolved.

// 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);
Comment thread
softhack007 marked this conversation as resolved.
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);

// 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);
Comment thread
softhack007 marked this conversation as resolved.

// 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 +287,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