#include "Animations.h" #include #include #include #include "ColorPalettes.h" #include "esp_system.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/semphr.h" #include "esp_log.h" #include #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 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 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 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 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 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 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 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 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(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 activeFlag with std::atomic 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(breath) * static_cast(origBright); uint8_t finalBright = static_cast((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(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(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; }