boothifier/src/Animations.cpp
2025-09-28 23:18:18 -07:00

1563 lines
57 KiB
C++

#include "Animations.h"
#include <Arduino.h>
#include <memory>
#include <FastLED.h>
#include "ColorPalettes.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_log.h"
#include <functional>
#include "PWM_Output.h"
#include "esp_log.h"
static const char* tag = "anims";
typedef struct{
float minSpeed;
float maxSpeed;
int rampCycles;
}SPEED_PROPERTIES;
void Animation_Init(void){
// Create Palettes
}
// Animation Loop Template
/*
void Animation_Loop(bool volatile& loop_active_flag, int speed, std::function<int()> callback) {
if (!callback) {
ESP_LOGE("Animation_Loop", "Invalid callback function");
return;
}
loop_active_flag = true;
speed = constrain(speed, 0, MaxSpeed);
int loopDelay = max(MaxSpeed - speed, MinLoopDelay);
ulTaskNotifyTake(pdTRUE, 0); // Clear any pending notifications
TickType_t xLastWakeTime;
TickType_t elapsedTicks;
TickType_t delayTicks;
int retVal = 0;
while(!retVal && loop_active_flag) {
xLastWakeTime = xTaskGetTickCount();
try {
retVal = callback(); // Call animation function
} catch (const std::exception& e) {
ESP_LOGE("Animation_Loop", "Callback exception: %s", e.what());
break;
}
if(!loop_active_flag) return;
// Calculate remaining time with overflow protection
elapsedTicks = xTaskGetTickCount() - xLastWakeTime;
delayTicks = (elapsedTicks < loopDelay) ? (loopDelay - elapsedTicks) : 0;
// Check for termination request
if (ulTaskNotifyTake(pdTRUE, delayTicks)) { break; }
}
loop_active_flag = false;
}
*/
void Animation_Loop(bool volatile& loop_active_flag, int speed, std::function<int()> callback) {
if (!callback) {
ESP_LOGE("Animation_Loop", "Invalid callback function");
return;
}
loop_active_flag = true;
speed = constrain(speed, 0, MaxSpeed);
// compute desired loop delay in milliseconds and clamp
int loopDelayMs = max(MaxSpeed - speed, MinLoopDelay);
const int MAX_LOOP_DELAY_MS = 60 * 1000; // 60s safety cap
if (loopDelayMs > MAX_LOOP_DELAY_MS) loopDelayMs = MAX_LOOP_DELAY_MS;
// Convert ms -> RTOS ticks
TickType_t loopDelayTicks = pdMS_TO_TICKS(loopDelayMs);
ulTaskNotifyTake(pdTRUE, 0); // Clear any pending notifications
TickType_t xLastWakeTime;
TickType_t elapsedTicks;
TickType_t delayTicks;
int retVal = 0;
while(!retVal && loop_active_flag) {
xLastWakeTime = xTaskGetTickCount();
try {
retVal = callback(); // Call animation function
} catch (const std::exception& e) {
ESP_LOGE("Animation_Loop", "Callback exception: %s", e.what());
break;
} catch (...) {
ESP_LOGE("Animation_Loop", "Callback unknown exception");
break;
}
if(!loop_active_flag) break;
// compute elapsed ticks since callback start and remaining ticks
elapsedTicks = xTaskGetTickCount() - xLastWakeTime;
delayTicks = (elapsedTicks < loopDelayTicks) ? (loopDelayTicks - elapsedTicks) : 0;
if (delayTicks == 0) {
// yield a tick to avoid busy-spin
vTaskDelay(1);
if (ulTaskNotifyTake(pdTRUE, 0)) break; // notified -> exit
} else {
if (ulTaskNotifyTake(pdTRUE, delayTicks)) { break; } // notified -> exit
}
}
loop_active_flag = false;
}
/*
void Animation_Loop_Variable(bool volatile& loop_active_flag, std::function<int()> callback) {
if (!callback) {
ESP_LOGE("Animation_Loop", "Invalid callback function");
return;
}
loop_active_flag = true;
ulTaskNotifyTake(pdTRUE, 0); // Clear any pending notifications
TickType_t xLastWakeTime;
TickType_t elapsedTicks;
TickType_t delayTicks;
while(loop_active_flag) {
xLastWakeTime = xTaskGetTickCount();
int loopDelay = 0;
try {
loopDelay = callback(); // Call animation function and get delay value
} catch (const std::exception& e) {
ESP_LOGE("Animation_Loop", "Callback exception: %s", e.what());
break;
}
if(!loop_active_flag) return;
// Ensure minimum delay protection
loopDelay = max(loopDelay, MinLoopDelay);
// Calculate remaining time with overflow protection
elapsedTicks = xTaskGetTickCount() - xLastWakeTime;
delayTicks = (elapsedTicks < loopDelay) ? (loopDelay - elapsedTicks) : 0;
// Check for termination request
if (ulTaskNotifyTake(pdTRUE, delayTicks)) { break; }
}
loop_active_flag = false;
}
*/
void Animation_Loop_Variable(bool volatile& loop_active_flag, std::function<int()> callback) {
if (!callback) {
ESP_LOGE("Animation_Loop", "Invalid callback function");
return;
}
loop_active_flag = true;
// clear any pending notification
ulTaskNotifyTake(pdTRUE, 0);
TickType_t xLastWakeTime;
TickType_t elapsedTicks;
TickType_t delayTicks;
// Define sensible upper bound for delay (ms) to avoid indefinite blocking due to bad callback
const int MAX_LOOP_DELAY_MS = 60 * 1000; // 60 seconds
while (loop_active_flag) {
xLastWakeTime = xTaskGetTickCount();
int loopDelayMs = 0;
try {
loopDelayMs = callback(); // callback returns desired delay in milliseconds
} catch (const std::exception& e) {
ESP_LOGE("Animation_Loop", "Callback exception: %s", e.what());
break;
} catch (...) {
ESP_LOGE("Animation_Loop", "Callback unknown exception");
break;
}
if (!loop_active_flag) break;
// sanitize returned ms value and enforce minimum/maximum
if (loopDelayMs < (int)MinLoopDelay) loopDelayMs = MinLoopDelay;
if (loopDelayMs > MAX_LOOP_DELAY_MS) loopDelayMs = MAX_LOOP_DELAY_MS;
// convert ms -> RTOS ticks (safe conversion)
TickType_t loopDelayTicks = pdMS_TO_TICKS(loopDelayMs);
// compute elapsed ticks since callback start
elapsedTicks = xTaskGetTickCount() - xLastWakeTime;
// compute remaining delay (in ticks)
delayTicks = (elapsedTicks < loopDelayTicks) ? (loopDelayTicks - elapsedTicks) : 0;
if (delayTicks == 0) {
// Ensure we yield at least one tick to avoid busy-looping
vTaskDelay(1);
// Check if notified during the yield
if (ulTaskNotifyTake(pdTRUE, 0)) break;
} else {
// Wait for either a notification (termination) or the timeout
if (ulTaskNotifyTake(pdTRUE, delayTicks)) {
// notified => exit loop
break;
}
}
}
loop_active_flag = false;
}
// Animation Loop Template
/*
void Animation_Loop_Duration(bool volatile& loop_active_flag, int speed, TickType_t durationMs, std::function<int()> callback) {
loop_active_flag = true;
speed = constrain(speed, 0, MaxSpeed);
if (durationMs < 0) durationMs = 0;
ulTaskNotifyTake(pdTRUE, 0); // Clear any pending notifications
TickType_t startTicks = xTaskGetTickCount();
TickType_t xLastWakeTime;
for(;;) {
xLastWakeTime = xTaskGetTickCount();
// Call animation function
int speedIncrease = 0;
try {
speedIncrease = callback(); // Call animation function
} catch (const std::exception& e) {
ESP_LOGE("Animation_Loop_Duration", "Callback exception: %s", e.what());
break;
} catch (...) {
ESP_LOGE("Animation_Loop_Duration", "Callback unknown exception");
break;
}
if(!loop_active_flag) return;
// Calculate combined speed with bounds protection
int totalSpeed = constrain(speed + speedIncrease, 0, MaxSpeed);
// Calculate delay with minimum protection
int loopDelay = MaxSpeed - totalSpeed;
loopDelay = max(loopDelay, MinLoopDelay);
// Calculate remaining time with overflow protection
TickType_t elapsedTicks = xTaskGetTickCount() - xLastWakeTime;
TickType_t delayTicks = (elapsedTicks < loopDelay) ? (loopDelay - elapsedTicks) : 0;
// Delay and Check for termination request
if (ulTaskNotifyTake(pdTRUE, delayTicks)) { break; }
// Check if duration reached and wait for loop_active_flag
if (durationMs >=0) {
TickType_t totalElapsed = xTaskGetTickCount() - startTicks;
if (totalElapsed >= durationMs) {
// Auto-terminate the loop when duration reached
break;
}
}
}
loop_active_flag = false;
}
*/
void Animation_Loop_Duration(bool volatile& loop_active_flag, int speed, TickType_t durationMs, std::function<int()> callback) {
loop_active_flag = true;
speed = constrain(speed, 0, MaxSpeed);
// treat durationMs as milliseconds (convert to ticks)
const TickType_t durationTicks = pdMS_TO_TICKS(durationMs);
ulTaskNotifyTake(pdTRUE, 0); // Clear any pending notifications
TickType_t startTicks = xTaskGetTickCount();
TickType_t xLastWakeTime;
const int MAX_LOOP_DELAY_MS = 60 * 1000; // safety cap
while (loop_active_flag) {
xLastWakeTime = xTaskGetTickCount();
// Call animation function
int speedIncrease = 0;
try {
speedIncrease = callback();
} catch (const std::exception& e) {
ESP_LOGE("Animation_Loop_Duration", "Callback exception: %s", e.what());
break;
} catch (...) {
ESP_LOGE("Animation_Loop_Duration", "Callback unknown exception");
break;
}
if(!loop_active_flag) break;
// Calculate combined speed with bounds protection
int totalSpeed = constrain(speed + speedIncrease, 0, MaxSpeed);
// Calculate delay with minimum protection (ms)
int loopDelayMs = MaxSpeed - totalSpeed;
loopDelayMs = max(loopDelayMs, MinLoopDelay);
if (loopDelayMs > MAX_LOOP_DELAY_MS) loopDelayMs = MAX_LOOP_DELAY_MS;
TickType_t loopDelayTicks = pdMS_TO_TICKS(loopDelayMs);
// Calculate remaining time with overflow protection
TickType_t elapsedTicks = xTaskGetTickCount() - xLastWakeTime;
TickType_t delayTicks = (elapsedTicks < loopDelayTicks) ? (loopDelayTicks - elapsedTicks) : 0;
// Delay and Check for termination request
if (delayTicks == 0) {
vTaskDelay(1);
if (ulTaskNotifyTake(pdTRUE, 0)) break;
} else {
if (ulTaskNotifyTake(pdTRUE, delayTicks)) break;
}
// Check if duration reached and wait for loop_active_flag
if (durationTicks > 0) {
TickType_t totalElapsed = xTaskGetTickCount() - startTicks;
if (totalElapsed >= durationTicks) {
break; // Auto-terminate when duration reached
}
}
}
loop_active_flag = false;
}
// Animation Loop Template
/*
void Animation_Loop_Cycles(bool volatile& loop_active_flag, int speed, uint32_t loop_cycles, std::function<int()> callback) {
loop_active_flag = true;
uint32_t loop_cycle_count = 0;
speed = constrain(speed, 0, MaxSpeed);
ulTaskNotifyTake(pdTRUE, 0); // Clear any pending notifications
TickType_t startTicks = xTaskGetTickCount();
TickType_t xLastWakeTime;
TickType_t elapsedTicks;
TickType_t delayTicks;
for(;;) {
xLastWakeTime = xTaskGetTickCount();
int speedIncrease = 0;
try {
speedIncrease = callback(); // Call animation function
} catch (const std::exception& e) {
ESP_LOGE("Animation_Loop_Cycles", "Callback exception: %s", e.what());
break;
} catch (...) {
ESP_LOGE("Animation_Loop_Cycles", "Callback unknown exception");
break;
}
if(!loop_active_flag) return;
// Calculate combined speed with bounds protection
int totalSpeed = constrain(speed + speedIncrease, 0, MaxSpeed);
// Calculate delay with minimum protection
int loopDelay = MaxSpeed - totalSpeed;
loopDelay = max(loopDelay, MinLoopDelay);
// Calculate remaining time with overflow protection
elapsedTicks = xTaskGetTickCount() - xLastWakeTime;
delayTicks = (elapsedTicks < loopDelay) ? (loopDelay - elapsedTicks) : 0;
// Delay and Check for termination request
if (ulTaskNotifyTake(pdTRUE, delayTicks)) { break; }
// Check if cycles reached and exit
if (loop_cycle_count >= loop_cycles) {
break;
}
loop_cycle_count++;
}
loop_active_flag = false;
}
*/
void Animation_Loop_Cycles(bool volatile& loop_active_flag, int speed, uint32_t loop_cycles, std::function<int()> callback) {
loop_active_flag = true;
uint32_t loop_cycle_count = 0;
speed = constrain(speed, 0, MaxSpeed);
ulTaskNotifyTake(pdTRUE, 0); // Clear any pending notifications
TickType_t xLastWakeTime;
TickType_t elapsedTicks;
TickType_t delayTicks;
const int MAX_LOOP_DELAY_MS = 60 * 1000; // safety cap
for(;;) {
xLastWakeTime = xTaskGetTickCount();
int speedIncrease = 0;
try {
speedIncrease = callback(); // Call animation function
} catch (const std::exception& e) {
ESP_LOGE("Animation_Loop_Cycles", "Callback exception: %s", e.what());
break;
} catch (...) {
ESP_LOGE("Animation_Loop_Cycles", "Callback unknown exception");
break;
}
if(!loop_active_flag) break;
// Calculate combined speed with bounds protection
int totalSpeed = constrain(speed + speedIncrease, 0, MaxSpeed);
// Calculate delay with minimum protection (ms)
int loopDelayMs = MaxSpeed - totalSpeed;
loopDelayMs = max(loopDelayMs, MinLoopDelay);
if (loopDelayMs > MAX_LOOP_DELAY_MS) loopDelayMs = MAX_LOOP_DELAY_MS;
TickType_t loopDelayTicks = pdMS_TO_TICKS(loopDelayMs);
// Calculate remaining time with overflow protection
elapsedTicks = xTaskGetTickCount() - xLastWakeTime;
delayTicks = (elapsedTicks < loopDelayTicks) ? (loopDelayTicks - elapsedTicks) : 0;
// Delay and Check for termination request
if (delayTicks == 0) {
vTaskDelay(1);
if (ulTaskNotifyTake(pdTRUE, 0)) break;
} else {
if (ulTaskNotifyTake(pdTRUE, delayTicks)) break;
}
// Check if cycles reached and exit
if (loop_cycle_count >= loop_cycles) {
break;
}
loop_cycle_count++;
}
loop_active_flag = false;
}
/********************************************************************************
*
* Animations
*
*******************************************************************************/
void Anim_Rainbow(bool volatile& activeFlag, CRGB* leds, int size, int speed){
// Initialize rainbow pattern once
fill_rainbow_circular(leds, size, 0);
CRGB temp;
Animation_Loop(activeFlag, speed, [&]() -> int {
// Rotate pixels by 1 position
temp = leds[0]; // Save first pixel
memmove(&leds[0], &leds[1], (size-1) * sizeof(CRGB));
leds[size-1] = temp; // Move first pixel to end
FastLED.show();
return 0;
});
}
// Fire parameters (adjustable)
const uint8_t FIRE_COOLING = 66;
const uint8_t FIRE_SPARKING = 55;
const uint8_t FIRE_brightness = 240;
void Anim_Fire(bool volatile& activeFlag, CRGB* leds, int size, int speed, const CRGBPalette16& firePalette, int shift = 0) {
if (!leds || size <= 0) return;
// Calculate half size for mirroring
const int halfSize = size / 2;
if (halfSize <= 0) return;
// Create heat array for half the size
uint8_t* heat = new (std::nothrow) uint8_t[halfSize];
if (!heat) return;
memset(heat, 0, halfSize * sizeof(uint8_t));
CRGB color;
uint8_t colorindex;
int pos1, pos2, y;
Animation_Loop(activeFlag, speed, [&]() -> int {
// Random cooling
for(int i = 0; i < halfSize; i++) {
heat[i] = qsub8(heat[i], random8(0, ((FIRE_COOLING * 10) / halfSize) + 2));
}
// Heat rises and diffuses
for(int k = halfSize - 1; k >= 2; k--) {
heat[k] = (heat[k - 1] + heat[k - 2] + heat[k - 2]) / 3;
}
// Randomly ignite new sparks at bottom
if(random8() < FIRE_SPARKING) {
// ensure y is in-bounds for small strips
y = min<int>(random8(7), halfSize - 1);
heat[y] = qadd8(heat[y], random8(160, 240));
}
// Map heat to colors with mirroring and shifting
for(int j = 0; j < halfSize; j++) {
colorindex = scale8(heat[j], 240);
color = ColorFromPalette(firePalette, colorindex, FIRE_brightness, LINEARBLEND);
// Apply shift and wrap around
pos1 = (j + shift + size) % size;
pos2 = (size - 1 - j + shift + size) % size;
leds[pos1] = color;
leds[pos2] = color; // Mirror
}
FastLED.show();
return 0;
});
delete[] heat;
}
void Anim_Color_Sectors(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colorPack, bool gaps, uint8_t numRepeats, int minSpeed, int maxSpeed) {
// Validate inputs
if (size <= 0 || numRepeats <= 0 || colorPack.size <= 0) return;
// Calculate effective colors per repeat (original colors + gaps if enabled)
int effectiveColorsPerRepeat = gaps ? (colorPack.size * 2) : colorPack.size;
// Calculate sector size
int sectorSize = size / (effectiveColorsPerRepeat * numRepeats);
if (sectorSize < 1) sectorSize = 1;
// Initialize pattern
for (int repeat = 0; repeat < numRepeats; repeat++) {
for (int color = 0; color < colorPack.size; color++) {
if (gaps) {
// With gaps: each color takes position color*2, gap takes color*2+1
int colorStartIdx = (repeat * effectiveColorsPerRepeat + color * 2) * sectorSize;
int colorEndIdx = colorStartIdx + sectorSize;
if (colorEndIdx > size) colorEndIdx = size;
// Fill sector with the specified color
if (colorStartIdx < size) {
fill_solid(&leds[colorStartIdx], colorEndIdx - colorStartIdx, colorPack.col[color]);
}
// Add black gap after the color
int gapStartIdx = (repeat * effectiveColorsPerRepeat + color * 2 + 1) * sectorSize;
int gapEndIdx = gapStartIdx + sectorSize;
if (gapEndIdx > size) gapEndIdx = size;
if (gapStartIdx < size) {
fill_solid(&leds[gapStartIdx], gapEndIdx - gapStartIdx, CRGB::Black);
}
} else {
// Without gaps: original behavior
int startIdx = (repeat * colorPack.size + color) * sectorSize;
int endIdx = startIdx + sectorSize;
if (endIdx > size) endIdx = size;
// Fill sector with the specified color
fill_solid(&leds[startIdx], endIdx - startIdx, colorPack.col[color]);
}
}
}
// Animate rotation
bool direction = true; // true = forward, false = backward
int loopCounter = 0;
int speed = minSpeed;
int loopDuration = (size * CYCLES_PER_DIRECTION);
int thirdLoop = loopDuration / 3;
int twoThirdLoop = (2 * loopDuration) / 3;
CRGB temp;
Animation_Loop_Variable(activeFlag, [&]() -> int {
if (direction) {
temp = leds[0];
memmove(&leds[0], &leds[1], (size-1) * sizeof(CRGB));
leds[size-1] = temp;
} else {
temp = leds[size-1];
memmove(&leds[1], &leds[0], (size-1) * sizeof(CRGB));
leds[0] = temp;
}
// Speed ramping: 1/3 aggressive ramp up, 1/3 constant, 1/3 aggressive ramp down
if (loopCounter < thirdLoop) {
// First third: aggressive ease-out acceleration (very fast start, sharp slowdown)
float progress = (float)loopCounter / (float)thirdLoop; // 0.0 to 1.0
float eased = 1.0f - pow(1.0f - progress, 4.0f); // ease-out quartic (power of 4)
speed = minSpeed + (int)((maxSpeed - minSpeed) * eased);
}
else if (loopCounter < twoThirdLoop) {
// Middle third: constant at maxSpeed
speed = maxSpeed;
}
else {
// Last third: aggressive ease-in deceleration (sharp start, fast finish)
float progress = (float)(loopCounter - twoThirdLoop) / (float)thirdLoop; // 0.0 to 1.0
float eased = pow(progress, 4.0f); // ease-in quartic (power of 4)
speed = maxSpeed - (int)((maxSpeed - minSpeed) * eased);
}
// Direction switching logic (single increment)
if (++loopCounter >= loopDuration) {
direction = !direction;
loopCounter = 0;
}
FastLED.show();
return max(MaxSpeed - speed, MinLoopDelay);
});
}
/********************************************************************************
* Comets Animation
*******************************************************************************/
#define COMET_SIZE_FACTOR 0.2
#define COMET_FADE_FACTOR1 32 /* longer tail */
#define COMET_FADE_FACTOR2 192 /* shorter tail */
//#define COMET_FADE_FACTOR COMET_FADE_FACTOR2
#define MAX_COMETS 8 // Maximum number of comets supported
/*
void Anim_Comets(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colorPack, int minSpeed, int maxSpeed, bool randomDecay, int cometMultiplier) {
// Validate inputs
int numComets = colorPack.size;
int totalComets = numComets * cometMultiplier;
if (size <= 0 || numComets <= 0 || colorPack.size <= 0 || cometMultiplier <= 0 || totalComets > MAX_COMETS) {
ESP_LOGE("Anim_Comets", "Invalid input parameters or too many comets (max: %d, requested: %d)", MAX_COMETS, totalComets);
return;
}
// Calculate comet size (use float math and round to avoid integer-division truncation)
int cometSize = (int)(((float)size * (float)COMET_SIZE_FACTOR) / (float)totalComets + 0.5f);
if (cometSize < 1) cometSize = 1;
// Set fade factor
uint8_t fadeFactor = map(totalComets, 1, MAX_COMETS, COMET_FADE_FACTOR1, COMET_FADE_FACTOR2);
fadeFactor = constrain(fadeFactor, COMET_FADE_FACTOR1, COMET_FADE_FACTOR2);
// Initialize comet positions with fixed array, evenly distributed
int cometPositions[MAX_COMETS] = {0};
for (int i = 0; i < totalComets; i++) {
// Even distribution even when size not divisible by totalComets
cometPositions[i] = (i * size) / totalComets;
}
// Animation loop
bool direction = true; // true = forward, false = backward
int loopCounter = 0;
int pos;
CRGB color;
int speed = minSpeed;
int loopDuration = size * CYCLES_PER_DIRECTION;
// Defensive: ensure loopDuration isn't zero to avoid divide-by-zero later
if (loopDuration < 1) loopDuration = 1;
int thirdLoop = loopDuration / 3;
// Defensive: ensure thirdLoop is at least 1 for progress calculations
if (thirdLoop < 1) thirdLoop = 1;
// Keep twoThirdLoop consistent with thirdLoop to avoid rounding surprises
int twoThirdLoop = 2 * thirdLoop;
try {
Animation_Loop_Variable(activeFlag, [&]() -> int {
// Fade all LEDs
for (int i = 0; i < size; i++) {
if (!randomDecay) {
leds[i].fadeToBlackBy(fadeFactor);
} else if (getRandomValue(10) > 5) {
leds[i].fadeToBlackBy(fadeFactor);
}
}
// Move and draw comets
for (int i = 0; i < totalComets; i++) {
if (direction) {
cometPositions[i] = (cometPositions[i] + 1) % size;
} else {
cometPositions[i] = (cometPositions[i] - 1 + size) % size;
}
// Draw comet with solid color
color = colorPack.col[i % colorPack.size];
for (int j = 0; j < cometSize; j++) {
// Tail follows the direction of movement
pos = direction ? (cometPositions[i] - j) : (cometPositions[i] + j);
pos = (pos % size + size) % size; // safe modulus
leds[pos] += color;
}
}
// Speed ramping: 1/3 aggressive ramp up, 1/3 constant, 1/3 aggressive ramp down
if (loopCounter < thirdLoop) {
// First third: aggressive ease-out acceleration (very fast start, sharp slowdown)
float progress = (float)loopCounter / (float)thirdLoop; // 0.0 to 1.0
float eased = 1.0f - pow(1.0f - progress, 4.0f); // ease-out quartic (power of 4)
speed = minSpeed + (int)((maxSpeed - minSpeed) * eased);
}
else if (loopCounter < twoThirdLoop) {
// Middle third: constant at maxSpeed
speed = maxSpeed;
}
else {
// Last third: aggressive ease-in deceleration (sharp start, fast finish)
float progress = (float)(loopCounter - twoThirdLoop) / (float)thirdLoop; // 0.0 to 1.0
float eased = pow(progress, 4.0f); // ease-in quartic (power of 4)
speed = maxSpeed - (int)((maxSpeed - minSpeed) * eased);
}
if (++loopCounter >= loopDuration) {
direction = !direction;
loopCounter = 0;
speed = minSpeed;
}
FastLED.show();
// Compute delay to return to Animation_Loop_Variable.
// Ensure we never return a value less than MinLoopDelay.
int computed = MaxSpeed - speed;
if (computed < MinLoopDelay) computed = MinLoopDelay;
return computed;
});
fill_solid(leds, size, CRGB::Black);
} catch (const std::exception& e) {
ESP_LOGE("Anim_Comets", "Exception in Animation_Loop: %s", e.what());
} catch (...) {
ESP_LOGE("Anim_Comets", "Unknown exception in Animation_Loop");
}
}
*/
void Anim_Comets(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colorPack, int minSpeed, int maxSpeed, bool randomDecay, int cometMultiplier) {
// Validate inputs
int numComets = colorPack.size;
int totalComets = numComets * cometMultiplier;
if (size <= 0 || numComets <= 0 || colorPack.size <= 0 || cometMultiplier <= 0 || totalComets > MAX_COMETS) {
ESP_LOGE("Anim_Comets", "Invalid input parameters or too many comets (max: %d, requested: %d)", MAX_COMETS, totalComets);
return;
}
// Sanitize speed inputs to known bounds
minSpeed = constrain(minSpeed, 0, MaxSpeed);
maxSpeed = constrain(maxSpeed, 0, MaxSpeed);
if (minSpeed > maxSpeed) { int t=minSpeed; minSpeed=maxSpeed; maxSpeed=t; }
// Calculate comet size (use float math and round to avoid integer-division truncation)
int cometSize = (int)(((float)size * (float)COMET_SIZE_FACTOR) / (float)totalComets + 0.5f);
if (cometSize < 1) cometSize = 1;
// Set fade factor
uint8_t fadeFactor = map(totalComets, 1, MAX_COMETS, COMET_FADE_FACTOR1, COMET_FADE_FACTOR2);
fadeFactor = constrain(fadeFactor, COMET_FADE_FACTOR1, COMET_FADE_FACTOR2);
// Initialize comet positions with fixed array, evenly distributed
int cometPositions[MAX_COMETS] = {0};
for (int i = 0; i < totalComets; i++) {
// Even distribution even when size not divisible by totalComets
cometPositions[i] = (i * size) / totalComets;
}
// Animation loop
bool direction = true; // true = forward, false = backward
int loopCounter = 0;
int pos;
CRGB color;
int speed = minSpeed;
// Cache size/limits locally for speed
const int localSize = size;
const int localTotalComets = totalComets;
int loopDuration = localSize * CYCLES_PER_DIRECTION;
// Defensive: ensure loopDuration isn't zero to avoid divide-by-zero later
if (loopDuration < 1) loopDuration = 1;
int thirdLoop = loopDuration / 3;
if (thirdLoop < 1) thirdLoop = 1;
int twoThirdLoop = 2 * thirdLoop;
try {
Animation_Loop_Variable(activeFlag, [&]() -> int {
// Fade all LEDs (keep this O(N) operation but keep it tight)
for (int i = 0; i < localSize; i++) {
if (!randomDecay) {
leds[i].fadeToBlackBy(fadeFactor);
} else if (getRandomValue(10) > 5) {
leds[i].fadeToBlackBy(fadeFactor);
}
}
// Move and draw comets
for (int i = 0; i < localTotalComets; i++) {
if (direction) {
cometPositions[i] = (cometPositions[i] + 1) % localSize;
} else {
cometPositions[i] = (cometPositions[i] - 1 + localSize) % localSize;
}
// Draw comet with solid color (safe modulo)
color = colorPack.col[i % colorPack.size];
for (int j = 0; j < cometSize; j++) {
// Tail follows the direction of movement
pos = direction ? (cometPositions[i] - j) : (cometPositions[i] + j);
pos = (pos % localSize + localSize) % localSize; // safe modulus
leds[pos] += color;
}
}
// Speed ramping: 1/3 aggressive ramp up, 1/3 constant, 1/3 aggressive ramp down
if (loopCounter < thirdLoop) {
// First third: ease-out quartic implemented without pow()
float progress = (float)loopCounter / (float)thirdLoop; // 0.0 to 1.0
float oneMinus = 1.0f - progress;
float eased = 1.0f - (oneMinus * oneMinus * oneMinus * oneMinus); // 1 - (1-t)^4
speed = minSpeed + (int)((maxSpeed - minSpeed) * eased);
}
else if (loopCounter < twoThirdLoop) {
// Middle third: constant at maxSpeed
speed = maxSpeed;
}
else {
// Last third: ease-in quartic implemented without pow()
float progress = (float)(loopCounter - twoThirdLoop) / (float)thirdLoop; // 0.0 to 1.0
if (progress < 0.0f) progress = 0.0f; if (progress > 1.0f) progress = 1.0f;
float eased = (progress * progress * progress * progress); // t^4
speed = maxSpeed - (int)((maxSpeed - minSpeed) * eased);
}
if (++loopCounter >= loopDuration) {
direction = !direction;
loopCounter = 0;
speed = minSpeed;
}
FastLED.show();
// Compute delay to return to Animation_Loop_Variable.
// Ensure we never return a value less than MinLoopDelay.
int computed = MaxSpeed - speed;
if (computed < MinLoopDelay) computed = MinLoopDelay;
return computed;
});
fill_solid(leds, size, CRGB::Black);
} catch (const std::exception& e) {
ESP_LOGE("Anim_Comets", "Exception in Animation_Loop: %s", e.what());
} catch (...) {
ESP_LOGE("Anim_Comets", "Unknown exception in Animation_Loop");
}
// NOTE: For production/stability consider:
// - Replacing volatile<bool> activeFlag with std::atomic<bool> or task-notify mechanism.
// - If FastLED.show() blocks too long for very long strips consider chunked updates or an alternate driver.
// - If pow() usage was widespread, replace with fixed-point or inline multiplies as done above.
}
void Anim_TimedFill(bool volatile& activeFlag, CRGB* leds, int size, CRGB baseCol, CRGB fillCol, int totalDurationMs, int shift = 0)
{
if (!leds || size <= 1 || totalDurationMs <= 0) return;
const int halfSize = size / 2;
const float msPerLed = totalDurationMs / (float)halfSize;
unsigned long startTime = millis();
fill_solid(leds, size, baseCol);
int prevLedsToLight = 0;
unsigned long currentTime;
unsigned long elapsedTime;
int ledsToLight, pos;
int loopInterval = 90;
Animation_Loop(activeFlag, loopInterval, [&]() -> int {
currentTime = millis();
elapsedTime = currentTime - startTime;
// return 0 to loop infinitely
if(elapsedTime > (totalDurationMs + loopInterval)) return 0;
// Calculate how many LEDs should be lit based on elapsed time
ledsToLight = (elapsedTime / msPerLed);
if (ledsToLight > halfSize) ledsToLight = halfSize;
// Fill LEDs up to current position
for (int i = 0; i < ledsToLight; i++) {
pos = (i + shift + size) % size;
leds[pos] = fillCol;
leds[(size - 1 - i + shift + size) % size] = fillCol; // Correct mirroring calculation
}
// Update LEDs only when necessary
if(prevLedsToLight < ledsToLight){
FastLED.show();
}
prevLedsToLight = ledsToLight;
return 0; // Always return 0 to continue looping
});
}
void Anim_TimedFill_Flash(bool volatile& activeFlag, CRGB* leds, int size, PWM_Output* pwmOut, PWM_OUT_SETTINGS* pwmSettings, int pwmOutDelay, int flashTimeout, CRGB baseCol, CRGB fillCol, int totalDurationMs, int shift)
{
if (!leds || size <= 1 || totalDurationMs <= 0 || !pwmOut || !pwmSettings) return;
const int halfSize = size / 2;
const float msPerLed = totalDurationMs / (float)halfSize;
unsigned long startTime = millis();
unsigned long flashStartTime = 0;
bool flashTimeoutStarted = false;
bool pwmStarted = false;
// Calculate PWM parameters
const int pwmRampDuration = totalDurationMs - pwmOutDelay;
const float pwmMin = pwmSettings->min;
const float pwmMax = pwmSettings->max;
const float pwmRange = pwmMax - pwmMin;
fill_solid(leds, size, baseCol);
// Set initial PWM to minimum
pwmOut->setOutput(pwmMin);
int prevLedsToLight = 0;
unsigned long currentTime;
unsigned long elapsedTime;
int ledsToLight, pos;
float pwmValue;
int loopInterval = 90;
Animation_Loop(activeFlag, loopInterval, [&]() -> int {
currentTime = millis();
elapsedTime = currentTime - startTime;
// Phase 1: Fill animation with synchronized PWM ramp
if (elapsedTime <= (totalDurationMs + loopInterval)) {
// Calculate how many LEDs should be lit based on elapsed time
ledsToLight = (elapsedTime / msPerLed);
if (ledsToLight > halfSize) ledsToLight = halfSize;
// Fill LEDs up to current position
for (int i = 0; i < ledsToLight; i++) {
pos = (i + shift + size) % size;
leds[pos] = fillCol;
leds[(size - 1 - i + shift + size) % size] = fillCol; // Correct mirroring calculation
}
// Update LEDs only when necessary
if(prevLedsToLight < ledsToLight){
FastLED.show();
}
prevLedsToLight = ledsToLight;
// PWM control: start ramping after pwmOutDelay
if (elapsedTime >= pwmOutDelay && !pwmStarted) {
pwmStarted = true;
ESP_LOGI("Anim_TimedFill_Flash", "Starting PWM ramp from %.1f to %.1f over %dms", pwmMin, pwmMax, pwmRampDuration);
}
if (pwmStarted && pwmRampDuration > 0) {
// Linear interpolation from min to max over remaining time
int pwmElapsed = elapsedTime - pwmOutDelay;
if (pwmElapsed < 0) pwmElapsed = 0;
if (pwmElapsed > pwmRampDuration) pwmElapsed = pwmRampDuration;
float progress = (float)pwmElapsed / (float)pwmRampDuration;
pwmValue = pwmMin + (progress * pwmRange);
pwmOut->setOutput(pwmValue);
}
}
// Phase 2: Animation complete, start flash timeout
else if (!flashTimeoutStarted) {
flashTimeoutStarted = true;
flashStartTime = currentTime;
ESP_LOGI("Anim_TimedFill_Flash", "Fill complete, starting flash timeout of %dms", flashTimeout);
}
// Phase 3: Flash timeout period
else {
unsigned long flashElapsed = currentTime - flashStartTime;
if (flashElapsed >= flashTimeout) {
// Flash timeout expired, set PWM to minimum
pwmOut->setOutput(pwmMin);
// exit the loop
activeFlag = false;
ESP_LOGI("Anim_TimedFill_Flash", "Flash timeout expired, PWM set to minimum");
}
// Continue in infinite loop regardless of timeout status
}
return 0; // Always return 0 to continue looping infinitely
});
pwmOut->setOutput(pwmMin);
}
void Anim_SolidWhite(bool volatile& activeFlag, CRGB* leds, PWM_Output* pwmOut, int size, int brightness, int timeoutMs) {
if (!leds || size <= 0 || !pwmOut) return;
const uint8_t origBright = FastLED.getBrightness();
const uint8_t fullBrightness = brightness;
const uint8_t reducedBrightness = brightness / 2; // 50% brightness
const float reducedPwmOutput = 50.0f; // 50% PWM output
unsigned long startTime = millis();
bool timeoutReached = false;
bool brightnessReduced = false;
// Set initial full brightness
FastLED.setBrightness(fullBrightness);
pwmOut->setOutput(100);
fill_solid(leds, size, CRGB::White);
FastLED.show();
Animation_Loop(activeFlag, 50, [&]() -> int {
// Check if timeout is specified and has been reached
if (timeoutMs > 0 && !timeoutReached) {
unsigned long currentTime = millis();
unsigned long elapsedTime = currentTime - startTime;
if (elapsedTime >= timeoutMs) {
timeoutReached = true;
// Reduce brightness to 50% for both RGB and PWM
if (!brightnessReduced) {
if(pwmOut){
pwmOut->setOutput(reducedPwmOutput);
}
FastLED.setBrightness(reducedBrightness);
FastLED.show(); // Update display with new brightness
brightnessReduced = true;
//ESP_LOGI("Anim_SolidWhite", "Timeout reached - brightness reduced to 50%%");
}
}
}
// No animation, just maintain the solid white state
return 0;
});
if(pwmOut){ pwmOut->setOutput(0); }// Turn off PWM output
FastLED.setBrightness(origBright); // Restore original brightness
}
#define MIN_BRIGHTNESS 2
void Anim_ColorBreath(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colors, uint32_t timeMs, int speed) {
if (!leds || size <= 0 || colors.size <= 0 || timeMs <= 0) return;
uint8_t colorIndex = 0;
unsigned long startTime = millis();
const uint32_t halfTime = timeMs / 2;
const uint8_t origBright = FastLED.getBrightness();
FastLED.setBrightness(255);
uint8_t breath = MIN_BRIGHTNESS;
uint32_t elapsedTime;
unsigned long currentTime;
CRGB outColor;
Animation_Loop(activeFlag, speed, [&]() -> int {
// Elapsed time in the current breath cycle
currentTime = millis();
elapsedTime = currentTime - startTime;
// Triangle wave brightness: up for half, down for half
if (elapsedTime < halfTime) {
breath = map(elapsedTime, 0, halfTime, MIN_BRIGHTNESS, 255); // Brighten
} else {
breath = map(elapsedTime, halfTime, timeMs, 255, MIN_BRIGHTNESS); // Dim
}
// Combine breath with original global brightness (rounded)
uint16_t prod = static_cast<uint16_t>(breath) * static_cast<uint16_t>(origBright);
uint8_t finalBright = static_cast<uint8_t>((prod + 127) / 255);
// Apply perceptual scaling once with combined brightness
outColor = colors.col[colorIndex];
outColor.nscale8_video(finalBright);
// Fill all LEDs with scaled color
fill_solid(leds, size, outColor);
FastLED.show();
if (elapsedTime >= timeMs) {
colorIndex = (colorIndex + 1) % colors.size;
startTime = currentTime;
}
return 0;
});
FastLED.setBrightness(origBright);
}
void Anim_GradientRotate(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colors, int speed) {
if (!leds || size <= 0 || colors.size < 2) return;
CRGB color1, color2;
// Create initial gradient: evenly distribute blends across the strip
// For i in [0..size-1], compute position in color space [0..colors.size)
for (int i = 0; i < size; i++) {
uint32_t pos256 = (uint32_t)i * (uint32_t)colors.size * 256u / (uint32_t)size; // 8.8 fixed point
int segment = (int)(pos256 >> 8); // 0..colors.size-1
uint8_t blendPos = (uint8_t)(pos256 & 0xFF); // 0..255
int nextSegment = (segment + 1) % colors.size;
// Local copies for blending
color1 = colors.col[segment];
color2 = colors.col[nextSegment];
leds[i] = blend(color1, color2, blendPos);
}
// Rotation animation loop
CRGB temp;
Animation_Loop(activeFlag, speed, [&]() -> int {
// Rotate one position
temp = leds[0];
memmove(&leds[0], &leds[1], (size-1) * sizeof(CRGB));
leds[size-1] = temp;
FastLED.show();
return 0;
});
}
void Anim_ColorWipe(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colors, int speed) {
if (!leds || size <= 0 || colors.size <= 0) return;
int currentIndex = 0;
Animation_Loop(activeFlag, speed, [&]() -> int {
// Wipe color across the strip
fill_solid(leds, size, colors.col[currentIndex]);
FastLED.show();
// Move to the next color
currentIndex = (currentIndex + 1) % colors.size;
return 0;
});
}
void Anim_Sparkle(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colors, int speed, uint8_t sparkleChance) {
if (!leds || size <= 0 || colors.size <= 0) return;
Animation_Loop(activeFlag, speed, [&]() -> int {
// Randomly light up LEDs with colors from the color pack
for (int i = 0; i < size; i++) {
if (getRandomValue(100) < sparkleChance) {
int colorIndex = getRandomValue(colors.size);
leds[i] = colors.col[colorIndex];
} else {
leds[i] = CRGB::Black; // Turn off LED
}
}
FastLED.show();
return 0;
});
}
void Anim_TheaterChase(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colors, int speed, uint8_t spacing) {
if (!leds || size <= 0 || colors.size <= 0 || spacing == 0) return;
int colorIndex = 0;
Animation_Loop(activeFlag, speed, [&]() -> int {
// Clear all LEDs
fill_solid(leds, size, CRGB::Black);
// Light up every 'spacing' LED with the current color
for (int i = 0; i < size; i += spacing) {
leds[i] = colors.col[colorIndex];
}
FastLED.show();
// Move to the next color
colorIndex = (colorIndex + 1) % colors.size;
return 0;
});
}
#define SNAKE_CYCLES_PER_ROTATION 4
void Anim_Snakes(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colorPack, int speed , int multiplier) {
if (!leds || size <= 0 || colorPack.size <= 0 || multiplier <= 0) return;
// Determine number of snakes (colors * multiplier), cap to a safe maximum
const int USER_NUM = colorPack.size * multiplier;
const int MAX_SNAKES = 32;
int numSnakes = USER_NUM;
if (numSnakes > MAX_SNAKES) numSnakes = MAX_SNAKES;
if (numSnakes <= 0) return;
// Snake parameters: compute per-snake max length based on total snakes
const int maxSnakeLengthPerSection = size / numSnakes; // Full space available per snake
if (maxSnakeLengthPerSection < 2) return; // Need at least 2 pixels per snake section
enum SnakeState : uint8_t { GROWING = 0, FILLING = 1, SHRINKING = 2 };
struct Snake {
int head;
int length;
int maxLength;
int startPos;
int endPos;
bool forward;
CRGB color;
SnakeState state;
};
// Allocate snakes and per-snake cycle flags
Snake* snakes = new (std::nothrow) Snake[numSnakes];
if (!snakes) return;
bool* cycleDone = new (std::nothrow) bool[numSnakes];
if (!cycleDone) { delete[] snakes; return; }
for (int i = 0; i < numSnakes; i++) cycleDone[i] = false;
// Initialize
bool globalDirection = true; // rotation direction and snake forward when cycle resets
int completedCycles = 0;
int rotationOffset = 0; // total rotation applied to whole-array
for (int i = 0; i < numSnakes; i++) {
snakes[i].startPos = (i * size) / numSnakes;
snakes[i].endPos = ((i + 1) * size) / numSnakes - 1;
snakes[i].head = snakes[i].startPos;
snakes[i].length = 1;
snakes[i].maxLength = maxSnakeLengthPerSection;
snakes[i].forward = globalDirection;
snakes[i].color = colorPack.col[i % colorPack.size];
snakes[i].state = GROWING;
}
// Temporary buffer to draw snakes before rotating whole array
CRGB* buf = new (std::nothrow) CRGB[size];
if (!buf) { delete[] snakes; delete[] cycleDone; return; }
Animation_Loop(activeFlag, speed, [&]() -> int {
// Clear drawing buffer
fill_solid(buf, size, CRGB::Black);
// Update snake states
bool anyGrowing = false;
for (int s = 0; s < numSnakes; s++) {
Snake& sn = snakes[s];
switch (sn.state) {
case GROWING:
// Move head toward wall and extend
if (sn.forward) {
if (sn.head < sn.endPos) { sn.head++; sn.length++; }
else { sn.state = FILLING; }
} else {
if (sn.head > sn.startPos) { sn.head--; sn.length++; }
else { sn.state = FILLING; }
}
if (sn.length > sn.maxLength) sn.length = sn.maxLength;
break;
case FILLING:
if (sn.length < sn.maxLength) { sn.length++; }
else { sn.state = SHRINKING; }
break;
case SHRINKING:
if (sn.length > 1) { sn.length--; }
else {
// Completed one grow+shrink cycle for this snake
sn.forward = globalDirection; // align to current global direction
sn.state = GROWING;
sn.length = 1;
cycleDone[s] = true;
}
break;
}
if (sn.state == GROWING || sn.state == FILLING) anyGrowing = true;
}
// If all snakes signaled completion, count a completed cycle and reset flags
bool allDone = true;
for (int s = 0; s < numSnakes; s++) { if (!cycleDone[s]) { allDone = false; break; } }
if (allDone) {
completedCycles++;
for (int s = 0; s < numSnakes; s++) cycleDone[s] = false;
}
// After required cycles, flip global direction
if (completedCycles >= SNAKE_CYCLES_PER_ROTATION) {
globalDirection = !globalDirection;
completedCycles = 0;
// apply new direction to snakes so their next grow starts in that direction
for (int s = 0; s < numSnakes; s++) snakes[s].forward = globalDirection;
}
// Rotation speed: 2x while any snake is growing/filling, 1x otherwise
int rotationSpeed = anyGrowing ? 2 : 1;
if (globalDirection) rotationOffset = (rotationOffset + rotationSpeed) % size;
else rotationOffset = (rotationOffset - rotationSpeed + size) % size;
// Draw snakes into buffer (no rotation applied here)
for (int s = 0; s < numSnakes; s++) {
Snake& sn = snakes[s];
for (int i = 0; i < sn.length; i++) {
int pos = sn.forward ? (sn.head - i) : (sn.head + i);
if (pos < sn.startPos || pos > sn.endPos) continue;
if (pos < 0 || pos >= size) continue;
float brightness = 1.0f - (float(i) / float(sn.maxLength)) * 0.7f;
uint8_t scale = static_cast<uint8_t>(constrain((int)(brightness * 255.0f), 0, 255));
CRGB pixel = sn.color;
pixel.nscale8_video(scale);
if (buf[pos] == CRGB::Black) buf[pos] = pixel; else buf[pos] += pixel;
}
}
// Rotate the whole buffer into the real LED array using rotationOffset
for (int i = 0; i < size; i++) {
int dst = (i + rotationOffset) % size;
leds[dst] = buf[i];
}
FastLED.show();
return 0;
});
delete[] buf;
delete[] snakes;
delete[] cycleDone;
}
#define BIRD_CYCLES_PER_ROTATION 4
void Anim_Birds(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colorPack, int speed , int multiplier) {
if (!leds || size <= 0 || colorPack.size <= 0 || multiplier <= 0) return;
// Determine number of snakes (colors * multiplier), cap to a safe maximum
const int USER_NUM = colorPack.size * multiplier;
const int MAX_SNAKES = 32;
int numSnakes = USER_NUM;
if (numSnakes > MAX_SNAKES) numSnakes = MAX_SNAKES;
if (numSnakes <= 0) return;
// Snake parameters: compute per-snake max length based on total snakes
const int maxSnakeLengthPerSection = size / numSnakes; // Full space available per snake
if (maxSnakeLengthPerSection < 2) return; // Need at least 2 pixels per snake section
enum SnakeState : uint8_t { GROWING = 0, FILLING = 1, SHRINKING = 2 };
struct Snake {
int head;
int length;
int maxLength;
int startPos;
int endPos;
bool forward;
CRGB color;
SnakeState state;
};
// Allocate snakes and per-snake cycle flags
Snake* snakes = new (std::nothrow) Snake[numSnakes];
if (!snakes) return;
bool* cycleDone = new (std::nothrow) bool[numSnakes];
if (!cycleDone) { delete[] snakes; return; }
for (int i = 0; i < numSnakes; i++) cycleDone[i] = false;
// Initialize
bool globalDirection = true; // rotation direction and snake forward when cycle resets
int completedCycles = 0;
int rotationOffset = 0; // total rotation applied to whole-array
for (int i = 0; i < numSnakes; i++) {
snakes[i].startPos = (i * size) / numSnakes;
snakes[i].endPos = ((i + 1) * size) / numSnakes - 1;
snakes[i].head = snakes[i].startPos;
snakes[i].length = 1;
snakes[i].maxLength = maxSnakeLengthPerSection;
snakes[i].forward = globalDirection;
snakes[i].color = colorPack.col[i % colorPack.size];
snakes[i].state = GROWING;
}
// Temporary buffer to draw snakes before rotating whole array
CRGB* buf = new (std::nothrow) CRGB[size];
if (!buf) { delete[] snakes; delete[] cycleDone; return; }
Animation_Loop(activeFlag, speed, [&]() -> int {
// Clear drawing buffer
fill_solid(buf, size, CRGB::Black);
// Update snake states
bool anyGrowing = false;
for (int s = 0; s < numSnakes; s++) {
Snake& sn = snakes[s];
switch (sn.state) {
case GROWING:
// Move head toward wall and extend
if (sn.forward) {
if (sn.head < sn.endPos) { sn.head++; sn.length++; }
else { sn.state = FILLING; }
} else {
if (sn.head > sn.startPos) { sn.head--; sn.length++; }
else { sn.state = FILLING; }
}
if (sn.length > sn.maxLength) sn.length = sn.maxLength;
break;
case FILLING:
if (sn.length < sn.maxLength) { sn.length++; }
else { sn.state = SHRINKING; }
break;
case SHRINKING:
if (sn.length > 1) { sn.length--; }
else {
// Completed one grow+shrink cycle for this snake
sn.forward = globalDirection; // align to current global direction
sn.state = GROWING;
sn.length = 1;
cycleDone[s] = true;
}
break;
}
if (sn.state == GROWING || sn.state == FILLING) anyGrowing = true;
}
// If all snakes signaled completion, count a completed cycle and reset flags
bool allDone = true;
for (int s = 0; s < numSnakes; s++) { if (!cycleDone[s]) { allDone = false; break; } }
if (allDone) {
completedCycles++;
for (int s = 0; s < numSnakes; s++) cycleDone[s] = false;
}
// After required cycles, flip global direction
if (completedCycles >= SNAKE_CYCLES_PER_ROTATION) {
globalDirection = !globalDirection;
completedCycles = 0;
// apply new direction to snakes so their next grow starts in that direction
for (int s = 0; s < numSnakes; s++) snakes[s].forward = globalDirection;
}
// Rotation speed: 2x while any snake is growing/filling, 1x otherwise
int rotationSpeed = anyGrowing ? 2 : 1;
if (globalDirection) rotationOffset = (rotationOffset + rotationSpeed) % size;
else rotationOffset = (rotationOffset - rotationSpeed + size) % size;
// Draw snakes into buffer (no rotation applied here)
for (int s = 0; s < numSnakes; s++) {
Snake& sn = snakes[s];
for (int i = 0; i < sn.length; i++) {
int pos = sn.forward ? (sn.head - i) : (sn.head + i);
if (pos < sn.startPos || pos > sn.endPos) continue;
if (pos < 0 || pos >= size) continue;
float brightness = 1.0f - (float(i) / float(sn.maxLength)) * 0.7f;
uint8_t scale = static_cast<uint8_t>(constrain((int)(brightness * 255.0f), 0, 255));
CRGB pixel = sn.color;
pixel.nscale8_video(scale);
if (buf[pos] == CRGB::Black) buf[pos] = pixel; else buf[pos] += pixel;
}
}
// Rotate the whole buffer into the real LED array using rotationOffset
for (int i = 0; i < size; i++) {
int dst = (i + rotationOffset) % size;
leds[dst] = buf[i];
}
FastLED.show();
return 0;
});
delete[] buf;
delete[] snakes;
delete[] cycleDone;
}
// Morph between colors in the color pack smoothly
void Anim_Morph(bool volatile& activeFlag, CRGB* leds, int size, const COLOR_PACK& colorPack, int speed , int morphSteps) {
if (!leds || size <= 0 || colorPack.size < 2 || morphSteps <= 0) return;
int currentColorIndex = 0;
int nextColorIndex = 1;
int step = 0;
CRGB startColor, endColor, blendedColor;
Animation_Loop(activeFlag, speed, [&]() -> int {
// Get start and end colors for current morph
startColor = colorPack.col[currentColorIndex];
endColor = colorPack.col[nextColorIndex];
// Blend colors based on current step.
// Compute blendAmount in [0..255] so that step==0 => 0, step==morphSteps => 255.
uint32_t ba = 0;
if (morphSteps > 0) {
ba = (uint32_t)step * 255u / (uint32_t)morphSteps;
if (ba > 255u) ba = 255u;
}
uint8_t blendAmount = (uint8_t)ba;
blendedColor = blend(startColor, endColor, blendAmount);
// Fill all LEDs with the blended color
fill_solid(leds, size, blendedColor);
FastLED.show();
// Advance to next step. When step reaches morphSteps we have shown the
// final blended color (endColor). After that, advance to the next pair.
if (step < morphSteps) {
step++;
} else {
// completed this morph
step = 0;
currentColorIndex = nextColorIndex;
nextColorIndex = (nextColorIndex + 1) % colorPack.size;
}
return 0;
});
}
uint32_t getRandomValue(uint32_t maxValue) {
return esp_random() % maxValue;
}