/** * @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 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; } // 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_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_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", "Navigation", "Chase", "Chase RGB", "Random"}; if (mode >= ANIM_MODE_COUNT) { return "Unknown"; } return mode_names[mode]; }