493 lines
12 KiB
C
493 lines
12 KiB
C
/**
|
|
* @file animation.c
|
|
* @brief LED animation patterns implementation
|
|
*/
|
|
|
|
#include "animation.h"
|
|
#include "led.h"
|
|
|
|
#include "esp_log.h"
|
|
#include "esp_timer.h"
|
|
#include "esp_random.h"
|
|
|
|
#include <math.h>
|
|
|
|
static const char *TAG = "ANIMATION";
|
|
|
|
#define FRAMES_PER_SECOND 60
|
|
|
|
static animation_mode_t current_mode = ANIM_BLACK;
|
|
static uint8_t global_hue = 0;
|
|
static uint32_t frame_counter = 0;
|
|
|
|
// Beat calculation helper
|
|
static int16_t beatsin16(uint8_t bpm, int16_t min_val, int16_t max_val)
|
|
{
|
|
// Use uint64_t to prevent overflow
|
|
uint64_t us = esp_timer_get_time(); // Microseconds
|
|
|
|
// Calculate beat phase (0-65535 repeating at BPM rate)
|
|
// beats_per_minute → beats_per_microsecond = bpm / 60,000,000
|
|
uint64_t beat_phase = (us * (uint64_t)bpm * 65536ULL) / 60000000ULL;
|
|
uint16_t beat16 = (uint16_t)(beat_phase & 0xFFFF);
|
|
|
|
// Convert to angle (0 to 2π)
|
|
float angle = (beat16 / 65535.0f) * 2.0f * M_PI;
|
|
float sin_val = sinf(angle);
|
|
|
|
// Map sin (-1 to +1) to output range (min_val to max_val)
|
|
int16_t range = max_val - min_val;
|
|
int16_t result = min_val + (int16_t)((sin_val + 1.0f) * range / 2.0f);
|
|
|
|
return result;
|
|
}
|
|
|
|
// Beat calculation helper (beatsin8 variant)
|
|
static uint8_t beatsin8(uint8_t bpm, uint8_t min_val, uint8_t max_val)
|
|
{
|
|
return (uint8_t)beatsin16(bpm, min_val, max_val);
|
|
}
|
|
|
|
// Random helper
|
|
static uint8_t random8(void)
|
|
{
|
|
return esp_random() & 0xFF;
|
|
}
|
|
|
|
static uint16_t random16(uint16_t max)
|
|
{
|
|
if (max == 0)
|
|
return 0;
|
|
return esp_random() % max;
|
|
}
|
|
|
|
// Animation implementations
|
|
static void anim_black(void)
|
|
{
|
|
rgb_t black = {0, 0, 0};
|
|
led_fill_a(black);
|
|
led_fill_b(black);
|
|
}
|
|
|
|
static void anim_red(void)
|
|
{
|
|
rgb_t red = {255, 0, 0};
|
|
led_fill_a(red);
|
|
led_fill_b(red);
|
|
}
|
|
|
|
static void anim_blue(void)
|
|
{
|
|
rgb_t blue = {0, 0, 255};
|
|
led_fill_a(blue);
|
|
led_fill_b(blue);
|
|
}
|
|
|
|
static void anim_green(void)
|
|
{
|
|
rgb_t green = {0, 255, 0};
|
|
led_fill_a(green);
|
|
led_fill_b(green);
|
|
}
|
|
|
|
static void anim_white(void)
|
|
{
|
|
rgb_t white = {255, 255, 255};
|
|
led_fill_a(white);
|
|
led_fill_b(white);
|
|
}
|
|
|
|
static void anim_rainbow(void)
|
|
{
|
|
// Rainbow generator
|
|
uint16_t num_leds_a = led_get_num_leds_a();
|
|
uint16_t num_leds_b = led_get_num_leds_b();
|
|
uint16_t num_leds = num_leds_a + num_leds_b;
|
|
|
|
for (uint16_t i = 0; i < num_leds; i++)
|
|
{
|
|
hsv_t hsv = {(uint8_t)(global_hue + (i * 7)), 255, 255};
|
|
rgb_t color = led_hsv_to_rgb(hsv);
|
|
|
|
if (i < num_leds_a)
|
|
{
|
|
led_set_pixel_a(num_leds_a - i - 1, color);
|
|
}
|
|
else
|
|
{
|
|
led_set_pixel_b(i - num_leds_a, color);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void add_glitter(uint8_t chance_of_glitter)
|
|
{
|
|
if (random8() < chance_of_glitter)
|
|
{
|
|
uint16_t num_leds = led_get_num_leds_a() + led_get_num_leds_b();
|
|
uint16_t pos = random16(num_leds);
|
|
rgb_t white = {255, 255, 255};
|
|
|
|
if (pos < led_get_num_leds_a())
|
|
{
|
|
led_add_pixel_a(pos, white);
|
|
}
|
|
else
|
|
{
|
|
led_add_pixel_b(pos - led_get_num_leds_a(), white);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void anim_rainbow_glitter(void)
|
|
{
|
|
anim_rainbow();
|
|
add_glitter(255);
|
|
}
|
|
|
|
static void anim_confetti(void)
|
|
{
|
|
// Random colored speckles that blink in and fade smoothly
|
|
led_fade_to_black(10);
|
|
|
|
uint16_t num_leds = led_get_num_leds_a() + led_get_num_leds_b();
|
|
uint16_t pos = random16(num_leds);
|
|
|
|
hsv_t hsv = {(uint8_t)(global_hue + random8()), 255, 255};
|
|
rgb_t color = led_hsv_to_rgb(hsv);
|
|
|
|
if (pos < led_get_num_leds_a())
|
|
{
|
|
led_set_pixel_a(led_get_num_leds_a() - pos - 1, color);
|
|
}
|
|
else
|
|
{
|
|
led_set_pixel_b(pos - led_get_num_leds_a(), color);
|
|
}
|
|
}
|
|
|
|
static void anim_sinelon(void)
|
|
{
|
|
// A colored dot sweeping back and forth, with fading trails
|
|
led_fade_to_black(20);
|
|
|
|
uint16_t num_leds = led_get_num_leds_a() + led_get_num_leds_b();
|
|
int16_t pos = beatsin16(13, 0, num_leds);
|
|
|
|
hsv_t hsv = {global_hue, 255, 192};
|
|
rgb_t color = led_hsv_to_rgb(hsv);
|
|
|
|
if (pos < led_get_num_leds_a())
|
|
{
|
|
led_add_pixel_a(led_get_num_leds_a() - pos - 1, color);
|
|
}
|
|
else
|
|
{
|
|
led_add_pixel_b(pos - led_get_num_leds_a(), color);
|
|
}
|
|
}
|
|
|
|
static void anim_bpm(void)
|
|
{
|
|
// Colored stripes pulsing at 33 BPM
|
|
uint8_t bpm = 33;
|
|
uint8_t beat = beatsin8(bpm, 64, 255);
|
|
|
|
uint16_t num_leds_a = led_get_num_leds_a();
|
|
uint16_t num_leds_b = led_get_num_leds_b();
|
|
|
|
// PartyColors palette simulation
|
|
const uint8_t palette_colors[] = {
|
|
170, 240, 90, 150, 210, 30, 180, 0,
|
|
210, 255, 150, 240, 255, 60, 255, 120};
|
|
|
|
for (uint16_t i = 0; i < num_leds_a; i++)
|
|
{
|
|
uint8_t color_index = (global_hue + (i * 2)) & 0x0F;
|
|
uint8_t brightness = beat - global_hue + (i * 10);
|
|
hsv_t hsv = {palette_colors[color_index], 255, brightness};
|
|
led_set_pixel_a(i, led_hsv_to_rgb(hsv));
|
|
}
|
|
|
|
for (uint16_t i = 0; i < num_leds_b; i++)
|
|
{
|
|
uint8_t color_index = (global_hue + ((i + num_leds_a) * 2)) & 0x0F;
|
|
uint8_t brightness = beat - global_hue + ((i + num_leds_a) * 10);
|
|
hsv_t hsv = {palette_colors[color_index], 255, brightness};
|
|
led_set_pixel_b(i, led_hsv_to_rgb(hsv));
|
|
}
|
|
}
|
|
|
|
static void anim_navigation(void)
|
|
{
|
|
// Navigation lights: left red, right green
|
|
led_clear_all();
|
|
|
|
uint16_t num_leds_a = led_get_num_leds_a();
|
|
uint16_t num_leds_b = led_get_num_leds_b();
|
|
|
|
rgb_t red = {255, 0, 0};
|
|
rgb_t green = {0, 255, 0};
|
|
|
|
// Side red (last 3 LEDs of strip A)
|
|
if (num_leds_a >= 3)
|
|
{
|
|
led_set_pixel_a(num_leds_a - 1, red);
|
|
led_set_pixel_a(num_leds_a - 2, red);
|
|
led_set_pixel_a(num_leds_a - 3, red);
|
|
}
|
|
|
|
// Side green (last 3 LEDs of strip B)
|
|
if (num_leds_b >= 3)
|
|
{
|
|
led_set_pixel_b(num_leds_b - 1, green);
|
|
led_set_pixel_b(num_leds_b - 2, green);
|
|
led_set_pixel_b(num_leds_b - 3, green);
|
|
}
|
|
}
|
|
|
|
static void anim_chase(void)
|
|
{
|
|
// Red dot sweeping with trailing dots
|
|
led_clear_all();
|
|
|
|
uint16_t num_leds_a = led_get_num_leds_a();
|
|
uint16_t num_leds_b = led_get_num_leds_b();
|
|
uint16_t total_leds = num_leds_a + num_leds_b;
|
|
|
|
// Get oscillating position across both strips
|
|
int16_t center_pos = beatsin16(40, 0, total_leds - 1);
|
|
|
|
rgb_t red = {255, 0, 0};
|
|
|
|
// Draw center dot with dimmed trailing dots (3 dots total: center ±1)
|
|
for (int8_t offset = -1; offset <= 1; offset++)
|
|
{
|
|
int16_t led_pos = center_pos + offset;
|
|
|
|
// Skip if position is out of bounds
|
|
if (led_pos < 0 || led_pos >= total_leds)
|
|
continue;
|
|
|
|
// Calculate brightness based on distance from center
|
|
uint8_t brightness = (offset == 0) ? 255 : 32; // Center: full, trailing: 12%
|
|
|
|
// Create dimmed color
|
|
rgb_t dimmed_red = {
|
|
(red.r * brightness) / 255,
|
|
(red.g * brightness) / 255,
|
|
(red.b * brightness) / 255};
|
|
|
|
// Map virtual position to physical LED
|
|
if (led_pos < num_leds_a)
|
|
{
|
|
// Strip A (mirrored: position 0 maps to last LED)
|
|
uint16_t strip_a_index = num_leds_a - led_pos - 1;
|
|
led_set_pixel_a(strip_a_index, dimmed_red);
|
|
}
|
|
else
|
|
{
|
|
// Strip B (direct mapping)
|
|
uint16_t strip_b_index = led_pos - num_leds_a;
|
|
led_set_pixel_b(strip_b_index, dimmed_red);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void anim_chase_rgb(void)
|
|
{
|
|
// RGB cycling dot sweeping with trailing dots
|
|
led_clear_all();
|
|
|
|
uint16_t num_leds_a = led_get_num_leds_a();
|
|
uint16_t num_leds_b = led_get_num_leds_b();
|
|
uint16_t total_leds = num_leds_a + num_leds_b;
|
|
|
|
// Get oscillating position across both strips
|
|
int16_t center_pos = beatsin16(40, 0, total_leds - 1);
|
|
|
|
hsv_t hsv = {global_hue, 255, 192};
|
|
rgb_t color = led_hsv_to_rgb(hsv);
|
|
|
|
// Draw center dot with dimmed trailing dots (3 dots total: center ±1)
|
|
for (int8_t offset = -1; offset <= 1; offset++)
|
|
{
|
|
int16_t led_pos = center_pos + offset;
|
|
|
|
// Skip if position is out of bounds
|
|
if (led_pos < 0 || led_pos >= total_leds)
|
|
continue;
|
|
|
|
// Calculate brightness based on distance from center
|
|
uint8_t brightness = (offset == 0) ? 255 : 32; // Center: full, trailing: 12%
|
|
|
|
// Create dimmed color
|
|
rgb_t dimmed_color = {
|
|
(color.r * brightness) / 255,
|
|
(color.g * brightness) / 255,
|
|
(color.b * brightness) / 255};
|
|
|
|
// Map virtual position to physical LED
|
|
if (led_pos < num_leds_a)
|
|
{
|
|
// Strip A (mirrored: position 0 maps to last LED)
|
|
uint16_t strip_a_index = num_leds_a - led_pos - 1;
|
|
led_set_pixel_a(strip_a_index, dimmed_color);
|
|
}
|
|
else
|
|
{
|
|
// Strip B (direct mapping)
|
|
uint16_t strip_b_index = led_pos - num_leds_a;
|
|
led_set_pixel_b(strip_b_index, dimmed_color);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void anim_random(void)
|
|
{
|
|
// Random LEDs get random colors
|
|
uint16_t num_leds = led_get_num_leds_a() + led_get_num_leds_b();
|
|
uint16_t random_pos = random16(num_leds);
|
|
|
|
rgb_t random_color = {
|
|
0,
|
|
0,
|
|
0};
|
|
|
|
// Set random LED to random basis color
|
|
switch (random16(3))
|
|
{
|
|
case 0:
|
|
random_color.r = 255;
|
|
break;
|
|
case 1:
|
|
random_color.g = 255;
|
|
break;
|
|
case 2:
|
|
random_color.b = 255;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (random_pos < led_get_num_leds_a())
|
|
{
|
|
led_set_pixel_a(random_pos, random_color);
|
|
}
|
|
else
|
|
{
|
|
led_set_pixel_b(random_pos - led_get_num_leds_a(), random_color);
|
|
}
|
|
}
|
|
|
|
esp_err_t animation_init(void)
|
|
{
|
|
current_mode = ANIM_BLACK;
|
|
global_hue = 0U;
|
|
frame_counter = 0U;
|
|
|
|
ESP_LOGI(TAG, "Animation initialized");
|
|
return ESP_OK;
|
|
}
|
|
|
|
void animation_set_mode(animation_mode_t mode)
|
|
{
|
|
if ((mode >= ANIM_MODE_COUNT) || (mode < 0U))
|
|
{
|
|
mode = ANIM_BLACK;
|
|
}
|
|
|
|
current_mode = mode;
|
|
frame_counter = 0U;
|
|
|
|
ESP_LOGI(TAG, "Animation mode set to: %s", animation_get_mode_name(mode));
|
|
}
|
|
|
|
void animation_update(void)
|
|
{
|
|
// Update global hue every frame (slowly cycles colors)
|
|
frame_counter++;
|
|
if (frame_counter % 3 == 0)
|
|
{
|
|
global_hue++;
|
|
}
|
|
|
|
// Execute current animation
|
|
switch (current_mode)
|
|
{
|
|
case ANIM_BLACK:
|
|
anim_black();
|
|
break;
|
|
case ANIM_RED:
|
|
anim_red();
|
|
break;
|
|
case ANIM_BLUE:
|
|
anim_blue();
|
|
break;
|
|
case ANIM_GREEN:
|
|
anim_green();
|
|
break;
|
|
case ANIM_WHITE:
|
|
anim_white();
|
|
break;
|
|
case ANIM_RAINBOW:
|
|
anim_rainbow();
|
|
break;
|
|
case ANIM_RAINBOW_GLITTER:
|
|
anim_rainbow_glitter();
|
|
break;
|
|
case ANIM_CONFETTI:
|
|
anim_confetti();
|
|
break;
|
|
case ANIM_SINELON:
|
|
anim_sinelon();
|
|
break;
|
|
case ANIM_BPM:
|
|
// anim_bpm();
|
|
break;
|
|
case ANIM_NAVIGATION:
|
|
anim_navigation();
|
|
break;
|
|
case ANIM_CHASE:
|
|
anim_chase();
|
|
break;
|
|
case ANIM_CHASE_RGB:
|
|
anim_chase_rgb();
|
|
break;
|
|
case ANIM_RANDOM:
|
|
anim_random();
|
|
break;
|
|
default:
|
|
anim_black();
|
|
break;
|
|
}
|
|
|
|
led_show();
|
|
}
|
|
|
|
const char *animation_get_mode_name(animation_mode_t mode)
|
|
{
|
|
static const char *mode_names[] = {
|
|
"Black",
|
|
"Red",
|
|
"Blue",
|
|
"Green",
|
|
"White",
|
|
"Rainbow",
|
|
"Rainbow with Glitter",
|
|
"Confetti",
|
|
"Sinelon",
|
|
"33BPM",
|
|
"Navigation",
|
|
"Chase",
|
|
"Chase RGB",
|
|
"Random"};
|
|
|
|
if (mode >= ANIM_MODE_COUNT)
|
|
{
|
|
return "Unknown";
|
|
}
|
|
|
|
return mode_names[mode];
|
|
}
|