From f57b739168f184164659d0e036fd55b0a5cd2eff Mon Sep 17 00:00:00 2001 From: jrhoffa Date: Sun, 18 Sep 2022 00:13:15 -0700 Subject: [PATCH] Add random and sparkle animation patterns Includes slight refactor of pattern factory --- main/CMakeLists.txt | 9 ++- main/leds.cpp | 56 --------------- main/leds.hpp | 48 +------------ main/main.cpp | 11 +-- main/patterns/gradient.cpp | 138 +++++++++++++++++++++++++++++++++++++ main/patterns/random.cpp | 118 +++++++++++++++++++++++++++++++ main/patterns/sparkle.cpp | 132 +++++++++++++++++++++++++++++++++++ main/presets.cpp | 58 ++++++++-------- main/presets.hpp | 11 +++ main/presets.json | 2 + 10 files changed, 442 insertions(+), 141 deletions(-) create mode 100644 main/patterns/gradient.cpp create mode 100644 main/patterns/random.cpp create mode 100644 main/patterns/sparkle.cpp diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 952eed0..07abcfd 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,4 +1,5 @@ idf_component_register( + SRCS "main.cpp" "wifi.cpp" @@ -10,6 +11,9 @@ idf_component_register( "leds.cpp" "presets.cpp" "spi_leds.cpp" + "patterns/gradient.cpp" + "patterns/random.cpp" + "patterns/sparkle.cpp" INCLUDE_DIRS "." ) @@ -27,8 +31,3 @@ endfunction() function(export) endfunction() - -# Import tinyxml2 targets -#add_subdirectory(lib/tinyxml2) -# Link tinyxml2 to main component -#target_link_libraries(${COMPONENT_LIB} PUBLIC tinyxml2) diff --git a/main/leds.cpp b/main/leds.cpp index ffd9dc9..bf67917 100644 --- a/main/leds.cpp +++ b/main/leds.cpp @@ -5,62 +5,6 @@ static const char *TAG = "leds"; #include "utils.hpp" -static inline uint8_t mix(int l, int r, Fixed x) { - return ((l << shift) + (x * (r - l))) >> shift; -} - -Color Gradient::at(Fixed x) const { - int i_left = (x * n_colors) >> shift; - int i_right = i_left < n_colors - 1 ? i_left + 1 : 0; - Fixed ratio = (x * n_colors) - (i_left << shift); - //ESP_LOGI(TAG, "at(%d) = %d between %d and %d", x, ratio, i_left, i_right); - Color cl = colors[i_left]; - Color cr = colors[i_right]; - Color color = { - mix(cl.r, cr.r, ratio), - mix(cl.g, cr.g, ratio), - mix(cl.b, cr.b, ratio) - }; - /*ESP_LOGI(TAG, "(%d, %d, %d) %f (%d, %d, %d) -> (%d, %d, %d)", - cl.r, cl.g, cl.b, - ratio / (float)factor, - cr.r, cr.g, cr.b, - color.r, color.g, color.b - );*/ - return color; -} - -void GradientPattern::step(Color pixels[], int len, Pattern::State *_state) const { - GradientPattern::State *state = (GradientPattern::State*)_state; - - int64_t now_us = time_us(); - int64_t duration_us = state->last_us == 0 ? 0 : now_us - state->last_us; - state->last_us = now_us; - //ESP_LOGI(TAG, "duration %d", duration_us); - - /* - int period_us = 1000000 / 60; - int n_skipped = (duration_us - period_us / 2) / period_us; - if (n_skipped) { - ESP_LOGW(TAG, "Skipped %d frames", n_skipped); - } - */ - // Don't make a major animation jump if it's been terribly long - if (duration_us > 100000) duration_us = 100000; - - - int offset_delta = (duration_us << shift) / (cycle_time_ms * 1000); - if (reverse) state->offset += offset_delta; else state->offset -= offset_delta; - Fixed off = march ? - (((int)state->offset * cycle_length) & ~((1 << shift) - 1)) / cycle_length : - state->offset; - //ESP_LOGI(TAG, "cycle time %d, delta %d, offset %d, off %d", cycle_time_ms, offset_delta, offset, off); - for (int i = 0; i < len; ++i) { - pixels[i] = gradient->at(off + ((i << shift) / cycle_length)); - } -} - - LEDStrip::LEDStrip(int _length) : length(_length), pattern(NULL), pixels(new Color[_length]), diff --git a/main/leds.hpp b/main/leds.hpp index e4a220a..3520c66 100644 --- a/main/leds.hpp +++ b/main/leds.hpp @@ -8,26 +8,12 @@ struct Color { uint8_t r, g, b; } __attribute__ ((packed)); + typedef uint16_t Fixed; constexpr int factor = 65536; constexpr int shift = 16; -struct Gradient { - Color at(Fixed x) const; - - static Gradient* make(int n) { - Gradient *gradient = (Gradient*)malloc( - sizeof(Gradient) + n * sizeof(*colors) - ); - gradient->n_colors = n; - return gradient; - } - - unsigned int n_colors; - Color colors[]; -}; - class Pattern { public: virtual ~Pattern() {}; @@ -39,38 +25,6 @@ public: virtual void step(Color[], int, State*) const = 0; }; -class GradientPattern : public Pattern { -public: - GradientPattern( - Gradient *_gradient, - int _cycle_length=20, int _cycle_time_ms=-1, - bool _reverse=false, bool _march=false - ) : cycle_length(_cycle_length), - cycle_time_ms( _cycle_time_ms < 0 ? - _cycle_length * 500 : _cycle_time_ms - ), - reverse(_reverse), march(_march), gradient(_gradient) - {} - - ~GradientPattern() { free(gradient); } - - State* start(int) const { return new State(); } - - void step(Color[], int, State*) const; - -private: - struct State : Pattern::State { - State() : last_us(0), offset(0) {} - int64_t last_us; - Fixed offset; - }; - - int cycle_length; - int cycle_time_ms; - bool reverse; - bool march; - Gradient *gradient; -}; class LEDStrip { public: diff --git a/main/main.cpp b/main/main.cpp index da7ac5a..5bc76a3 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -149,16 +149,17 @@ extern "C" void app_main(void) { if (pattern) { ESP_LOGI(TAG, "Setting pattern '%s'", effect.c_str()); LEDs->setPattern(pattern); + + char *config = cJSON_PrintUnformatted(json); + write_file(config_path, config); + ESP_LOGI(TAG, "Saved config"); + cJSON_free(config); + } else { ESP_LOGW(TAG, "Could not find pattern '%s'", effect.c_str()); pattern = LEDs->getPattern(); } - char *config = cJSON_PrintUnformatted(json); - write_file(config_path, config); - ESP_LOGI(TAG, "Saved config"); - - cJSON_free(config); cJSON_Delete(json); if (length <= 0 || !pattern || !is_on) { diff --git a/main/patterns/gradient.cpp b/main/patterns/gradient.cpp new file mode 100644 index 0000000..ebdf18c --- /dev/null +++ b/main/patterns/gradient.cpp @@ -0,0 +1,138 @@ +static const char *TAG = "gradient"; +#include + +#include "presets.hpp" +#include "utils.hpp" + + +struct Gradient { + Color at(Fixed x) const; + + static Gradient* make(int n) { + Gradient *gradient = (Gradient*)malloc( + sizeof(Gradient) + n * sizeof(*colors) + ); + gradient->n_colors = n; + return gradient; + } + + unsigned int n_colors; + Color colors[]; +}; + +class GradientPattern : public Pattern { +public: + GradientPattern( + Gradient *_gradient, + int _cycle_length=20, int _cycle_time_ms=-1, + bool _reverse=false, bool _march=false + ) : cycle_length(_cycle_length), + cycle_time_ms( _cycle_time_ms < 0 ? + _cycle_length * 500 : _cycle_time_ms + ), + reverse(_reverse), march(_march), gradient(_gradient) + {} + + ~GradientPattern() { free(gradient); } + + State* start(int) const { return new State(); } + + void step(Color[], int, State*) const; + +private: + struct State : Pattern::State { + State() : last_us(0), offset(0) {} + int64_t last_us; + Fixed offset; + }; + + int cycle_length; + int cycle_time_ms; + bool reverse; + bool march; + Gradient *gradient; +}; + + +static inline uint8_t mix(int l, int r, Fixed x) { + return ((l << shift) + (x * (r - l))) >> shift; +} + +Color Gradient::at(Fixed x) const { + int i_left = (x * n_colors) >> shift; + int i_right = i_left < n_colors - 1 ? i_left + 1 : 0; + Fixed ratio = (x * n_colors) - (i_left << shift); + //ESP_LOGI(TAG, "at(%d) = %d between %d and %d", x, ratio, i_left, i_right); + Color cl = colors[i_left]; + Color cr = colors[i_right]; + Color color = { + mix(cl.r, cr.r, ratio), + mix(cl.g, cr.g, ratio), + mix(cl.b, cr.b, ratio) + }; + /*ESP_LOGI(TAG, "(%d, %d, %d) %f (%d, %d, %d) -> (%d, %d, %d)", + cl.r, cl.g, cl.b, + ratio / (float)factor, + cr.r, cr.g, cr.b, + color.r, color.g, color.b + );*/ + return color; +} + +void GradientPattern::step(Color pixels[], int len, Pattern::State *_state) const { + GradientPattern::State *state = (GradientPattern::State*)_state; + + int64_t now_us = time_us(); + int64_t duration_us = state->last_us == 0 ? 0 : now_us - state->last_us; + state->last_us = now_us; + //ESP_LOGI(TAG, "duration %d", duration_us); + + /* + int period_us = 1000000 / 60; + int n_skipped = (duration_us - period_us / 2) / period_us; + if (n_skipped) { + ESP_LOGW(TAG, "Skipped %d frames", n_skipped); + } + */ + // Don't make a major animation jump if it's been terribly long + if (duration_us > 100000) duration_us = 100000; + + + int offset_delta = (duration_us << shift) / (cycle_time_ms * 1000); + if (reverse) state->offset += offset_delta; else state->offset -= offset_delta; + Fixed off = march ? + (((int)state->offset * cycle_length) & ~((1 << shift) - 1)) / cycle_length : + state->offset; + //ESP_LOGI(TAG, "cycle time %d, delta %d, offset %d, off %d", cycle_time_ms, offset_delta, offset, off); + for (int i = 0; i < len; ++i) { + pixels[i] = gradient->at(off + ((i << shift) / cycle_length)); + } +} + + +Pattern* json_gradient_pattern(cJSON *pattern, const Presets::ColorMap &colors) { + if (pattern->type != cJSON_Object) { + ESP_LOGE(TAG, "Not an object: %s", pattern->type); + return NULL; + } + cJSON *jcolors = cJSON_GetObjectItem(pattern, "colors"); + if (!jcolors) { + ESP_LOGE(TAG, "No colors in %s", pattern->string); + return NULL; + } + if (jcolors->type != cJSON_Array) { + ESP_LOGE(TAG, "Not an array: %s", jcolors->string); + return NULL; + } + + Gradient *gradient = Gradient::make(cJSON_GetArraySize(jcolors)); + + int i = 0; + cJSON *jcolor; + cJSON_ArrayForEach(jcolor, jcolors) { + Color color = Presets::json_color(jcolor, colors); + gradient->colors[i++] = color; + } + + return new GradientPattern(gradient); +} diff --git a/main/patterns/random.cpp b/main/patterns/random.cpp new file mode 100644 index 0000000..e51db94 --- /dev/null +++ b/main/patterns/random.cpp @@ -0,0 +1,118 @@ +static const char *TAG = "random"; +#include + +#include + +#include "presets.hpp" +#include "utils.hpp" + + +static Color rand_color() { + static const Color colors[] = { + {255, 0, 0}, + { 78, 25, 0}, + {255, 160, 0}, + { 0, 255, 0}, + { 0, 0, 255}, + { 38, 10, 42}, + }; + return colors[esp_random() % 6]; +} + +class RandomPattern : public Pattern { +private: + struct State : Pattern::State { + State(int _len, int gap) : + last_us(0), step(0), offset(0), + n_colors((_len + gap) / (gap + 1)), colors(new Color[n_colors]) + { + for (Color *color = colors, *end = colors + n_colors; color < end; ++color) { + *color = rand_color(); + } + } + ~State() { delete[] colors; } + + int64_t last_us; + int step; + int offset; + int n_colors; + Color *colors; + }; + +public: + RandomPattern(int _delay_ms, int _gap) : delay_us(_delay_ms * 1000), gap(_gap) {} + + State* start(int len) const { return new State(len, gap); } + + void step(Color colors[], int len, Pattern::State *_state) const { + State *state = (State*)_state; + + int64_t now_us = time_us(); + int64_t elapsed_us = now_us - state->last_us; + bool should_step = (state->last_us == 0) || (elapsed_us >= delay_us && delay_us > 0); + if (should_step) { + state->last_us = (elapsed_us >= delay_us + 100000 ? now_us : state->last_us + delay_us); + + int step = state->step; + int offset = state->offset; + Color *inc = state->colors + offset; + Color *outc = colors; + + if ((gap + 1 - step) % (gap + 1) == 0) *inc = rand_color(); + + for (int i = 0; i < len; i++) { + if ((i - step + gap + 1) % (gap + 1) == 0) { + *outc = *inc--; + if (inc < state->colors) inc += state->n_colors; + } else { + *outc = {0, 0, 0}; + } + ++outc; + } + + step++; + if (step > gap) { + step -= gap + 1; + ++offset; + if (offset >= state->n_colors) offset -= state->n_colors; + state->offset = offset; + } + state->step = step; + } + } + +private: + int delay_us; + int gap; +}; + + +Pattern* json_random_pattern(cJSON *pattern, const Presets::ColorMap &colors) { + int delay_ms = 500; + int gap = 1; + + if (pattern->type != cJSON_Object) { + ESP_LOGE(TAG, "Not an object: %s", pattern->type); + return NULL; + } + + cJSON *jdelay = cJSON_GetObjectItem(pattern, "delay"); + if (jdelay) { + if (jdelay->type != cJSON_Number) { + ESP_LOGE(TAG, "Not a number: %s", jdelay->string); + } else { + delay_ms = jdelay->valuedouble * 1000.; + } + } + + cJSON *jgap = cJSON_GetObjectItem(pattern, "gap"); + if (jgap) { + if (jgap->type != cJSON_Number) { + ESP_LOGE(TAG, "Not a number: %s", jgap->string); + } else { + gap = jgap->valueint; + } + } + + return new RandomPattern(delay_ms, gap); +} diff --git a/main/patterns/sparkle.cpp b/main/patterns/sparkle.cpp new file mode 100644 index 0000000..7dfdf80 --- /dev/null +++ b/main/patterns/sparkle.cpp @@ -0,0 +1,132 @@ +static const char *TAG = "random"; +#include + +#include +#include + +#include + +#include "presets.hpp" +#include "utils.hpp" + + +static const int8_t ndist[64] = {-127, -104, -92, -84, -77, -72, -67, -62, -58, -55, -51, -48, -45, -42, -39, -37, -34, -32, -29, -27, -25, -22, -20, -18, -16, -14, -11, -9, -7, -5, -3, -1, 1, 3, 5, 7, 9, 11, 14, 16, 18, 20, 22, 25, 27, 29, 32, 34, 37, 39, 42, 45, 48, 51, 55, 58, 62, 67, 72, 77, 84, 92, 104, 127}; + +int norm(int mean) { + return (mean * 128) / ndist[esp_random() % 64]; +} + +struct Sparkle { + Sparkle(int64_t time_us, int mean_dur_us, int len): + start_us(time_us), end_us(time_us + (mean_dur_us)), + pos(esp_random() % len) {} + + bool expired(int64_t time_us) { return time_us >= end_us; } + + uint8_t intensity_at(int64_t time_us) { + if (time_us < start_us || time_us >= end_us) return 0; + int64_t midpoint_us = (start_us + end_us) / 2; + if (time_us >= midpoint_us) time_us = start_us + (end_us - time_us); + return (((time_us - start_us) * 255) * 2) / (end_us - start_us); + } + + int64_t start_us; + int end_us; + int pos; +}; + +Color fade(Color c, unsigned int i) { + return { + (uint8_t)((c.r * i) / 255), + (uint8_t)((c.g * i) / 255), + (uint8_t)((c.b * i) / 255) + }; +} + +class SparklePattern : public Pattern { +private: + struct State : Pattern::State { + std::list sparkles; + }; + +public: + SparklePattern(Color _color, int _duration_ms, int _delay_ms, int _density) : + color(_color), duration_us(_duration_ms * 1000), + delay_us(_delay_ms * 1000), density(_density) + {} + + State* start(int len) const { return new State(); } + + void step(Color colors[], int len, Pattern::State *_state) const { + State *state = (State*)_state; + + int64_t now_us = time_us(); + + int64_t start_us = now_us; + for ( int open = (len / density) + 1 - state->sparkles.size(); + open > 0; + --open, start_us += delay_us + ) { + state->sparkles.push_front(Sparkle(start_us, duration_us, len)); + } + + memset(colors, 0, sizeof(*colors) * len); + + for (auto iter = state->sparkles.begin(); iter != state->sparkles.end(); ) { + if (iter->expired(now_us)) { + state->sparkles.erase(iter++); + } else { + colors[iter->pos] = fade(color, iter->intensity_at(now_us)); + ++iter; + } + } + } + +private: + Color color; + int duration_us; + int delay_us; + int density; +}; + + +Pattern* json_sparkle_pattern(cJSON *pattern, const Presets::ColorMap &colors) { + int duration_ms = 250; + int delay_ms = 3000; + int density = 10; + Color color = {255, 255, 255}; + + if (pattern->type != cJSON_Object) { + ESP_LOGE(TAG, "Not an object: %s", pattern->type); + return NULL; + } + + cJSON *jnum = cJSON_GetObjectItem(pattern, "delay"); + if (jnum) { + if (jnum->type != cJSON_Number) { + ESP_LOGE(TAG, "Not a number: %s", jnum->string); + } else { + delay_ms = jnum->valuedouble * 1000.; + } + } + + jnum = cJSON_GetObjectItem(pattern, "duration"); + if (jnum) { + if (jnum->type != cJSON_Number) { + ESP_LOGE(TAG, "Not a number: %s", jnum->string); + } else { + duration_ms = jnum->valuedouble * 1000.; + } + } + + jnum = cJSON_GetObjectItem(pattern, "density"); + if (jnum) { + if (jnum->type != cJSON_Number) { + ESP_LOGE(TAG, "Not a number: %s", jnum->string); + } else { + density = jnum->valuedouble; + } + } + + return new SparklePattern(color, duration_ms, delay_ms, density); +} diff --git a/main/presets.cpp b/main/presets.cpp index 6931c83..1f7da36 100644 --- a/main/presets.cpp +++ b/main/presets.cpp @@ -7,8 +7,25 @@ static const char *TAG = "presets"; #include "utils.hpp" +/* Sadly, need to define these here */ + +extern Pattern* json_gradient_pattern(cJSON *pattern, const Presets::ColorMap &colors); +extern Pattern* json_random_pattern(cJSON *pattern, const Presets::ColorMap &colors); +extern Pattern* json_sparkle_pattern(cJSON *pattern, const Presets::ColorMap &colors); + namespace Presets { +/* Factory */ + +typedef Pattern* (*PatternGenerator)(cJSON*, const Presets::ColorMap&); + +static const std::map generators { + {"gradient", json_gradient_pattern}, + {"random", json_random_pattern}, + {"sparkle", json_sparkle_pattern}, +}; + + /* Preset Map */ std::map map; @@ -51,7 +68,7 @@ static Color json_raw_color(cJSON *json) { }; } -static Color json_color(cJSON *json, const std::map &colors) { +Color json_color(cJSON *json, const ColorMap &colors) { if (json->type == cJSON_Array) return json_raw_color(json); if (json->type != cJSON_String) { ESP_LOGE(TAG, "Not a string: %s", json->string); @@ -65,7 +82,8 @@ static Color json_color(cJSON *json, const std::map &colors return iter->second; } -static Pattern* json_pattern(cJSON *json, const std::map &colors) { + +static Pattern* json_pattern(cJSON *json, const ColorMap &colors) { if (json->type != cJSON_Object) { ESP_LOGE(TAG, "Not an object: %s", json->string); return NULL; @@ -74,38 +92,22 @@ static Pattern* json_pattern(cJSON *json, const std::map &c cJSON *pattern = json->child; std::string type(pattern->string); - if (type == "gradient") { - if (pattern->type != cJSON_Object) { - ESP_LOGE(TAG, "Not an object: %s", pattern->type); - return NULL; - } - cJSON *jcolors = cJSON_GetObjectItem(pattern, "colors"); - if (!jcolors) { - ESP_LOGE(TAG, "No colors for %s", json->string); - return NULL; - } - if (jcolors->type != cJSON_Array) { - ESP_LOGE(TAG, "Not an array: %s", jcolors->string); - return NULL; - } - Gradient *gradient = Gradient::make(cJSON_GetArraySize(jcolors)); - int i = 0; - cJSON *jcolor; - cJSON_ArrayForEach(jcolor, jcolors) { - Color color = json_color(jcolor, colors); - gradient->colors[i++] = color; - } - return new GradientPattern(gradient); - - } else { + auto iter = generators.find(type); + if (iter == generators.end()) { ESP_LOGE(TAG, "Unknown pattern type: %s", type.c_str()); + return NULL; } - return NULL; + return iter->second(pattern, colors); } void reload() { - std::map colors; + if (generators.empty()) { + ESP_LOGE(TAG, "No pattern generators defined!"); + return; + } + + ColorMap colors; cJSON *jcolors = load_json("/spiffs/colors.json"); if (!jcolors) { ESP_LOGW(TAG, "No preset colors!"); diff --git a/main/presets.hpp b/main/presets.hpp index fca5767..f87ca22 100644 --- a/main/presets.hpp +++ b/main/presets.hpp @@ -1,7 +1,10 @@ #pragma once + #include #include +#include + #include "leds.hpp" @@ -14,4 +17,12 @@ std::map::const_iterator map_end(); void reload(); + +// Helpers + +typedef std::map ColorMap; + +Color json_color(cJSON *json, const ColorMap &colors); + + } // Presets diff --git a/main/presets.json b/main/presets.json index d9dd401..5f44977 100644 --- a/main/presets.json +++ b/main/presets.json @@ -5,6 +5,8 @@ "peacock": {"gradient": {"colors": ["purple", "blue", "teal", "black"]}}, "pusheen": {"gradient": {"colors": ["teal", "pink"]}}, +"random": {"random": {"gap": 1, "delay": 0.5}}, +"snow": {"sparkle": {"color": "white", "duration": 0.25, "delay": 3, "density": 10}}, "love": {"gradient": {"colors": ["pink", "black", "red", "black"]}}, "irish": {"gradient": {"colors": ["dark_green", "black", "green", "black"]}},