1563 lines
57 KiB
C++
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;
|
|
} |