├── .gitignore ├── library.properties ├── release_notes.md ├── src ├── Array.h ├── patterns │ ├── spatial.h │ └── linear.h ├── MappingRunner.h ├── utils.h ├── Pattern.h ├── LEDuino.h ├── ColorPicker.h ├── StripSegment.h ├── Point.h └── PatternMapping.h ├── LICENSE.md ├── library.json ├── examples ├── SingleSegmentLinearMapping.ino ├── MultiplePatternMapping.ino ├── MultipleSegmentLinearMapping.ino └── SpatialPatternMapping.ino └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=LEDuino 2 | version=0.2.2 3 | author=Finn Andersen 4 | maintainer=Finn Andersen 5 | sentence=Framework for defining patterns and mapping them to a configuration of addressable LEDs using FastLED. 6 | paragraph=Makes it easy to define configurable linear or 3D patterns and map them to segments of an addressable LED strip. 7 | category=Display 8 | url=https://github.com/Finndersen/LEDuino 9 | architectures=* 10 | includes=LEDuino.h 11 | depends=FastLED -------------------------------------------------------------------------------- /release_notes.md: -------------------------------------------------------------------------------- 1 | LEDuino 0.1.0 2 | ============= 3 | Initial Release 4 | 5 | LEDuino 0.2.0 6 | ============= 7 | - Rework Linear Patterns 8 | - LinearPatternMapper optimisations 9 | - Add MappingRunner 10 | - Add ColorPicker 11 | 12 | LEDuino 0.2.1 13 | ============= 14 | - Fixed issue with importing Teensy-specific in Point.h 15 | 16 | LEDuino 0.2.2 17 | ============= 18 | - Add RandomColorFadePattern 19 | - Fixed issues with clashes with min and max macros 20 | - Updated minimum hardware requirements -------------------------------------------------------------------------------- /src/Array.h: -------------------------------------------------------------------------------- 1 | #include 2 | // Basic array class similar to std:array. Allows passing by value, length() and iteration 3 | template 4 | struct Array { 5 | // Storage 6 | T data[N]; 7 | 8 | static size_t length() { return N; } 9 | using type = T; 10 | 11 | // Item access 12 | T &operator[](size_t index) { return data[index]; } 13 | const T &operator[](size_t index) const { return data[index]; } 14 | 15 | // Iterators 16 | T *begin() { return &data[0]; } 17 | const T *begin() const { return &data[0]; } 18 | T *end() { return &data[N]; } 19 | const T *end() const { return &data[N]; } 20 | 21 | // Comparisons 22 | bool operator==(const Array &rhs) const { 23 | if (this == &rhs) 24 | return true; 25 | for (size_t i = 0; i < N; i++) 26 | if ((*this)[i] != rhs[i]) 27 | return false; 28 | return true; 29 | } 30 | bool operator!=(const Array &rhs) const { 31 | return !(*this == rhs); 32 | } 33 | }; -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Finn Andersen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "LEDuino", 3 | "description": "Framework for defining patterns and mapping them to a configuration of addressable LEDs using FastLED.", 4 | "keywords": "led,pattern,fastled", 5 | "authors": [ 6 | { 7 | "name": "Finn Andersen", 8 | "url": "https://github.com/Finndersen", 9 | "maintainer": true 10 | } 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/Finndersen/LEDuino.git" 15 | }, 16 | "version": "0.2.2", 17 | "license": "MIT", 18 | "homepage": "https://github.com/Finndersen/LEDuino", 19 | "frameworks": "arduino", 20 | "platforms": "atmelavr, atmelsam, freescalekinetis, nordicnrf51, nxplpc, ststm32, teensy, espressif8266, espressif32, nordicnrf52", 21 | "headers": "LEDuino.h", 22 | "export": { 23 | "exclude": [ 24 | "docs", 25 | "extras" 26 | ] 27 | }, 28 | "build": { 29 | "srcFilter": [ 30 | "+<*.c>", 31 | "+<*.cpp>", 32 | "+<*.h>" 33 | ], 34 | "libArchive": false 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/patterns/spatial.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include "Pattern.h" 3 | 4 | class GrowingSpherePattern: public SpatialPattern { 5 | public: 6 | GrowingSpherePattern( 7 | uint8_t speed=1, 8 | const ColorPicker& color_picker=RainbowColors_picker 9 | ) : SpatialPattern(color_picker), 10 | speed(speed) {} 11 | 12 | void reset() override { 13 | SpatialPattern::reset(); 14 | this->radius = 0; 15 | this->growing = true; 16 | } 17 | 18 | void frameAction(uint32_t frame_time) override { 19 | if (this->growing) { 20 | this->radius += this->speed; 21 | if (this->radius >= this->resolution) { 22 | this->growing = false; 23 | } 24 | } else { 25 | if (this->radius <= this->speed) { 26 | this->growing = true; 27 | } else { 28 | this->radius -= this->speed; 29 | } 30 | } 31 | }; 32 | 33 | CRGB getPixelValue(Point point) const override { 34 | float point_distance = point.norm(); 35 | if (point_distance > this->radius) { 36 | return CRGB::Black; 37 | } else { 38 | return this->getColor((255*point_distance)/this->resolution); 39 | } 40 | } 41 | private: 42 | const uint8_t speed; // Speed at which sphere grows and shrinks 43 | uint16_t radius; // Current radius of sphere 44 | bool growing; // Whether sphere is growing or shrinking 45 | }; -------------------------------------------------------------------------------- /examples/SingleSegmentLinearMapping.ino: -------------------------------------------------------------------------------- 1 | // This is a basic example using an LED strip configured as a single segment, that will cycle through 3 linear patterns 2 | #include 3 | #include 4 | 5 | #define LED_DATA_PIN 2 6 | // Using a WS2812b strip of 60 LEDs which we want to map patterns to 7 | #define NUM_LEDS 60 8 | // Declare LED array 9 | CRGB leds[NUM_LEDS]; 10 | 11 | // If we wanted to use patterns that would benefit from higher resolution and interpolation for smooth looking motion, 12 | // we could declare another CRGB pixel array that is larger than the actual LED array and can be used by patterns 13 | // to create animations with higher resolution than the number of physical LEDs. 14 | // In this simple example we will just use the LED array directly 15 | 16 | // Define segment and segment array 17 | StripSegment full_strip_segment(0, NUM_LEDS, NUM_LEDS); 18 | StripSegment segment_array[1] = {full_strip_segment}; 19 | 20 | 21 | // Define patterns to use 22 | MovingPulsePattern pulse_pattern(15); 23 | TwinklePattern twinkle_pattern; 24 | PridePattern pride_pattern(8); 25 | 26 | // Define mapping of patterns to segment 27 | LinearPatternMapper pulse_mapping(pulse_pattern, leds, NUM_LEDS, segment_array, 1); 28 | LinearPatternMapper twinkle_mapping(twinkle_pattern, leds, NUM_LEDS, segment_array, 1); 29 | LinearPatternMapper pride_mapping(pride_pattern, leds, NUM_LEDS, segment_array, 1); 30 | 31 | // Define array of MappingRunners for controller to use 32 | MappingRunner mappings[3] = { 33 | MappingRunner(pulse_mapping, 15, 10), // 20ms delay between frames (66FPS), duration of 10 seconds 34 | MappingRunner(twinkle_mapping, 30), // 30ms frame delay (33 FPS) and default duration (15) 35 | MappingRunner(pride_mapping) // Will use default frame delay (20) and duration (15) 36 | }; 37 | 38 | // Define controller 39 | LEDuinoController controller(leds, NUM_LEDS, mappings, 3, false); 40 | 41 | void setup() { 42 | // Initialise FastLED 43 | FastLED.addLeds(leds, NUM_LEDS).setCorrection(TypicalLEDStrip); 44 | controller.initialise(); 45 | } 46 | 47 | void loop() { 48 | // put your main code here, to run repeatedly: 49 | controller.loop(); 50 | } 51 | -------------------------------------------------------------------------------- /examples/MultiplePatternMapping.ino: -------------------------------------------------------------------------------- 1 | // This is an example using an LED strip split into 2 segments, with 2 different patterns mapped at the same time 2 | #include 3 | #include 4 | 5 | #define LED_DATA_PIN 2 6 | // Using a WS2812b strip of 60 LEDs which we want to map patterns to 7 | #define NUM_LEDS 60 8 | // Split the LED strip into 3 segments each with length of 20 LEDs 9 | #define SEGMENT_LEN 30 10 | #define NUM_SEGMENTS 2 11 | 12 | // Declare LED array 13 | CRGB leds[NUM_LEDS]; 14 | 15 | // Declare Pixel array for patterns to use 16 | CRGB pixel_data[SEGMENT_LEN]; 17 | 18 | // Define segments 19 | StripSegment first_segment(0, SEGMENT_LEN, NUM_LEDS); 20 | StripSegment second_segment(SEGMENT_LEN, SEGMENT_LEN, NUM_LEDS); 21 | 22 | // Create array of segments 23 | StripSegment first_segment_array[1] = {first_segment}; 24 | StripSegment second_segment_array[1] = {second_segment}; 25 | 26 | // Define patterns to use 27 | SkippingSpikePattern skipping_spike_pattern(6); 28 | TwinklePattern twinkle_pattern; 29 | 30 | // Define mapping of patterns to segment 31 | // Can only reuse a pixel_data array between different mapper configurations to be run at the same time if 32 | // no pattern depends on (reads) the previous state of the pixel_data. If so, it will need to use its own 33 | // dedicated array 34 | // Pulse pattern on first segment 35 | LinearPatternMapper skipping_spike_mapping(skipping_spike_pattern, pixel_data, SEGMENT_LEN, first_segment_array, 1); 36 | // Twinkle pattern on second segment (does not benefit from extra resolution, so just use SEGMENT_LEN) 37 | LinearPatternMapper twinkle_mapping(twinkle_pattern, pixel_data, SEGMENT_LEN, second_segment_array, 1); 38 | 39 | // MultiplePatternMapping configuration to run both at the same time 40 | BasePatternMapper* mapper_array[NUM_SEGMENTS] = { 41 | &skipping_spike_mapping, 42 | &twinkle_mapping 43 | }; 44 | MultiplePatternMapper multi_mapping(mapper_array, NUM_SEGMENTS); 45 | 46 | // Define array of MappingRunners for controller to use 47 | MappingRunner mappings[1] = { 48 | MappingRunner(multi_mapping) 49 | }; 50 | 51 | // Define controller 52 | LEDuinoController controller(leds, NUM_LEDS, mappings, 1, false); 53 | 54 | void setup() { 55 | // Initialise FastLED 56 | FastLED.addLeds(leds, NUM_LEDS).setCorrection(TypicalLEDStrip); 57 | controller.initialise(); 58 | } 59 | 60 | void loop() { 61 | // put your main code here, to run repeatedly: 62 | controller.loop(); 63 | } 64 | -------------------------------------------------------------------------------- /examples/MultipleSegmentLinearMapping.ino: -------------------------------------------------------------------------------- 1 | // This is an example using an LED strip split into 2 segments, and a variety of pattern mapping configurations 2 | #include 3 | #include 4 | 5 | #define LED_DATA_PIN 2 6 | // Using a WS2812b strip of 60 LEDs which we want to map patterns to 7 | #define NUM_LEDS 60 8 | // Split the LED strip into 2 segments each with length of 30 LEDs 9 | #define SEGMENT_LEN 30 10 | #define NUM_SEGMENTS 2 11 | 12 | // Declare LED array 13 | CRGB leds[NUM_LEDS]; 14 | 15 | // Declare Pixel array with 2* segment length resolution for smooth motion of Pulse pattern 16 | CRGB pixel_data[SEGMENT_LEN*2]; 17 | 18 | // Define segment and segment array 19 | StripSegment first_segment(0, SEGMENT_LEN, NUM_LEDS); 20 | StripSegment second_segment(SEGMENT_LEN, SEGMENT_LEN, NUM_LEDS); 21 | StripSegment full_strip_segment(0, NUM_LEDS, NUM_LEDS); 22 | 23 | // Create array of segments 24 | StripSegment segments_normal[NUM_SEGMENTS] = {first_segment, second_segment}; 25 | // Create another configuration, but with the 2nd segment reversed 26 | StripSegment segments_second_reversed[NUM_SEGMENTS] = {first_segment, -second_segment}; 27 | // 28 | StripSegment full_strip_segments[1] = {full_strip_segment}; 29 | 30 | // Define patterns to use 31 | MovingPulsePattern pulse_pattern(8); 32 | PridePattern pride_pattern(8); 33 | 34 | // Define mapping of patterns to segment 35 | // Pulse pattern duplicated on both segments 36 | LinearPatternMapper pulse_mapping(pulse_pattern, pixel_data, SEGMENT_LEN*2, segments_normal, NUM_SEGMENTS); 37 | // Pulse pattern but reversed on second segment (so pulses will meet in middle) 38 | LinearPatternMapper pulse_reversed_mapping(pulse_pattern, pixel_data, SEGMENT_LEN*2, segments_second_reversed, NUM_SEGMENTS); 39 | // Pride pattern on the full LED strip 40 | LinearPatternMapper pride_mapping(pride_pattern, pixel_data, SEGMENT_LEN*2, full_strip_segments, 1); 41 | 42 | // Define array of MappingRunners for controller to use 43 | MappingRunner mappings[3] = { 44 | MappingRunner(pulse_mapping), 45 | MappingRunner(pulse_reversed_mapping), 46 | MappingRunner(pride_mapping) 47 | }; 48 | 49 | // Define controller 50 | LEDuinoController controller(leds, NUM_LEDS, mappings, 3, false); 51 | 52 | void setup() { 53 | // Initialise FastLED 54 | FastLED.addLeds(leds, NUM_LEDS).setCorrection(TypicalLEDStrip); 55 | controller.initialise(); 56 | } 57 | 58 | void loop() { 59 | // put your main code here, to run repeatedly: 60 | controller.loop(); 61 | } 62 | -------------------------------------------------------------------------------- /src/MappingRunner.h: -------------------------------------------------------------------------------- 1 | #ifndef PatternConfiguration_h 2 | #define PatternConfiguration_h 3 | #include "PatternMapping.h" 4 | // Can override these defaults by setting before including LEDuino 5 | #ifndef LEDUINO_DEFAULT_DURATION 6 | #define LEDUINO_DEFAULT_DURATION 10 // 10 second duration 7 | #endif 8 | #ifndef LEDUINO_DEFAULT_FRAME_DELAY 9 | #define LEDUINO_DEFAULT_FRAME_DELAY 20 // 20 ms frame delay (50 FPS) 10 | #endif 11 | 12 | // Manages the duration and frame rate of a PatternMapper configuration 13 | // Can be assigned a name for identification 14 | class MappingRunner { 15 | public: 16 | MappingRunner( 17 | BasePatternMapper& pattern_mapper, 18 | uint16_t frame_delay=LEDUINO_DEFAULT_FRAME_DELAY, // Delay between pattern frames (in ms) 19 | uint16_t duration=LEDUINO_DEFAULT_DURATION, // Duration in seconds 20 | const char* name="" 21 | ): 22 | name(name), 23 | pattern_mapper(pattern_mapper), 24 | duration(duration*1000), 25 | frame_delay(frame_delay) {} 26 | 27 | // Initialise/Reset pattern state 28 | void reset() { 29 | #ifdef LEDUINO_DEBUG 30 | Serial.print("Initialising pattern mapping configuration: "); 31 | Serial.println(this->name); 32 | Serial.flush(); 33 | #endif 34 | this->start_time = millis(); 35 | this->frame_time = 0; 36 | this->pattern_mapper.reset(); 37 | }; 38 | 39 | // Excute new frame of pattern and map results to LED array 40 | void newFrame(CRGB* leds) { 41 | this->frame_time = millis() - this->start_time; 42 | this->pattern_mapper.newFrame(leds, this->frame_time); 43 | } 44 | 45 | // Determine whether pattern has expired (exceeded duration) 46 | bool expired() { 47 | return this->frame_time >= this->duration; 48 | }; 49 | 50 | // Return whether it is time to start a new frame (frame_delay has elapsed since previous frame time) 51 | bool frameReady() { 52 | return (millis() - this->start_time - this->frame_time) >= this->frame_delay; 53 | }; 54 | 55 | const char* name; // Name or description of pattern 56 | protected: 57 | BasePatternMapper& pattern_mapper; 58 | uint16_t frame_time; // Time of the current frame since pattern started (in ms) 59 | uint32_t start_time; // Absolute time pattern was initialised (in ms) 60 | const uint16_t duration; // Duration of pattern mapping configuration (in ms) 61 | const uint16_t frame_delay; // Delay between pattern frames (in ms) 62 | }; 63 | 64 | #endif -------------------------------------------------------------------------------- /src/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef utils_h 2 | #define utils_h 3 | 4 | #include 5 | 6 | #define sgn(x) ((x > 0) - (x < 0)) 7 | #define same_sign(x, y) ((x<0) == (y<0)) 8 | 9 | // Check whether a is between x and y (inclusive) 10 | #define between(a, x, y) (y >= x ? (a >= x && a <= y) : (a >= y && a <= x)) 11 | 12 | // Limit maximum value 13 | #define limit(x, max) (x > max ? max : x) 14 | 15 | // Used for interpolating values on a linear gradient determined by two provided points 16 | class Interpolator { 17 | public: 18 | Interpolator(float x1, float y1, float x2, float y2) : 19 | x1(x1), 20 | y1(y1), 21 | gradient((y2-y1)/(x2-x1)) { 22 | 23 | } 24 | // Interpolate y value for x input 25 | float get_value(float x) { 26 | return y1 + (x-x1)*gradient; 27 | } 28 | 29 | protected: 30 | const float x1, y1, gradient; 31 | }; 32 | 33 | // Subtraction with support for wrapping around back to max_value 34 | uint16_t wrap_subtract(uint16_t value, uint16_t subtract, uint16_t max_value) { 35 | if (subtract <= value) { 36 | return value - subtract; 37 | } else { 38 | return max_value - (subtract - value-1); 39 | } 40 | } 41 | 42 | // Get a new random number from 0-255, but with a minimum distance away from the previous one 43 | uint8_t new_random_value8(uint8_t old_value, uint8_t min_distance=42) { 44 | uint8_t r = 0, x = 0, y = 0, d = 0; 45 | 46 | while(d < min_distance) { 47 | r = random8(); 48 | x = abs(old_value - r); 49 | y = 255 - x; 50 | d = min(x, y); 51 | } 52 | return r; 53 | } 54 | 55 | /* 56 | This function is like 'triwave8', which produces a 57 | symmetrical up-and-down triangle sawtooth waveform, except that this 58 | function produces a triangle wave with a faster attack and a slower decay: 59 | 60 | / \ 61 | / \ 62 | / \ 63 | / \ 64 | */ 65 | 66 | uint8_t attackDecayWave8( uint8_t i) 67 | { 68 | if( i < 86) { 69 | return i * 3; 70 | } else { 71 | i -= 86; 72 | return 255 - (i + (i/2)); 73 | } 74 | } 75 | 76 | #ifdef __arm__ 77 | // should use uinstd.h to define sbrk but Due causes a conflict 78 | extern "C" char* sbrk(int incr); 79 | #else // __ARM__ 80 | extern char *__brkval; 81 | #endif // __arm__ 82 | 83 | int freeMemory() { 84 | char top; 85 | #ifdef __arm__ 86 | return &top - reinterpret_cast(sbrk(0)); 87 | #elif defined(CORE_TEENSY) || (ARDUINO > 103 && ARDUINO != 151) 88 | return &top - __brkval; 89 | #else // __arm__ 90 | return __brkval ? &top - __brkval : &top - __malloc_heap_start; 91 | #endif // __arm__ 92 | } 93 | 94 | #endif -------------------------------------------------------------------------------- /src/Pattern.h: -------------------------------------------------------------------------------- 1 | #ifndef Pattern_h 2 | #define Pattern_h 3 | #include 4 | #include "Point.h" 5 | #include "utils.h" 6 | #include "ColorPicker.h" 7 | 8 | // Abstract Base class for patterns. Subclasses override frameAction() to implement pattern logic 9 | // Pattern logic can be defined in terms of frames (so that speed will be determined by framerate), 10 | // or by absolute time (using frame_time or FastLED beatX functions) 11 | class BasePattern { 12 | public: 13 | // Constructor 14 | BasePattern( 15 | const ColorPicker& color_picker=Basic_picker // Colour picker/palette to use for pattern 16 | ): 17 | color_picker(color_picker) {} 18 | 19 | 20 | // Initialise/Reset pattern state 21 | virtual void reset() {}; 22 | 23 | protected: 24 | 25 | // Select colour from current picker/palette 26 | CRGB getColor(uint8_t hue, uint8_t brightness=255) const { 27 | return this->color_picker.getColor(hue, brightness); 28 | } 29 | 30 | const ColorPicker& color_picker; 31 | }; 32 | 33 | // Base class for patterns defined on a simple linear axis 34 | // Contains logic to update state on each frame, and populate a pixel array which will then be mapped to strip segments 35 | class LinearPattern: public BasePattern { 36 | public: 37 | LinearPattern( 38 | const ColorPicker& color_picker=Basic_picker // Colour picker/palette to use for pattern 39 | ): 40 | BasePattern(color_picker) {} 41 | 42 | 43 | // Overidde frameAction() for updating pattern state with each frame, and setting the pixel values in pixel_data 44 | virtual void frameAction(CRGB* pixel_data, uint16_t num_pixels, uint32_t frame_time) = 0; 45 | 46 | }; 47 | 48 | // Pattern defined in 3D space. Converts a 3D coordinate of a pixel into a colour value 49 | // The pattern occupies a 3D cube of space with boundaries at +/- 'resolution' on each axis 50 | class SpatialPattern : public BasePattern { 51 | public: 52 | SpatialPattern( 53 | const ColorPicker& color_picker=Basic_picker, // Colour picker/palette to use for pattern 54 | uint16_t resolution=256 // maximum magnitude of pattern space in +/- x, y and z directions 55 | ): 56 | BasePattern(color_picker), 57 | resolution(resolution) {} 58 | 59 | // Contains main logic for pattern to update state (for sublcasses to override) 60 | // Provided time in ms since pattern started 61 | virtual void frameAction(uint32_t frame_time) = 0; 62 | 63 | // Get value for pixel at point coordinate. 64 | virtual CRGB getPixelValue(Point point) const { return CRGB::Black; } 65 | 66 | const uint16_t resolution; 67 | }; 68 | #endif -------------------------------------------------------------------------------- /examples/SpatialPatternMapping.ino: -------------------------------------------------------------------------------- 1 | 2 | // This is an example using an LED strip split into 4 segments, arranged in a square so that each segment is an edge of the square 3 | #include 4 | #include 5 | 6 | #define LED_DATA_PIN 2 7 | // Using a WS2812b strip of 120 LEDs which we want to map patterns to 8 | #define NUM_LEDS 120 9 | // Split the LED strip into 4 segments each with length of 30 LEDs 10 | #define SEGMENT_LEN 30 11 | #define NUM_SEGMENTS 4 12 | // Declare LED array 13 | CRGB leds[NUM_LEDS]; 14 | 15 | // Declare Pixel array for linear pattern to use. Length depends on desired pattern resolution 16 | #define NUM_PIXELS 40 17 | CRGB pixel_data[NUM_PIXELS]; 18 | 19 | // Define segments 20 | StripSegment segment1(0, SEGMENT_LEN, NUM_LEDS); 21 | StripSegment segment2(SEGMENT_LEN, SEGMENT_LEN, NUM_LEDS); 22 | StripSegment segment3(SEGMENT_LEN*2, SEGMENT_LEN, NUM_LEDS); 23 | StripSegment segment4(SEGMENT_LEN*3, SEGMENT_LEN, NUM_LEDS); 24 | 25 | // Define coordinates of corners of square (start and end positions of segments) 26 | // We will define a coordinate system with origin at centre of square, and 100 distance to each edge 27 | Point corner1 = Point(-100, 100, 0); 28 | Point corner2 = Point(100, 100, 0); 29 | Point corner3 = Point(100, -100, 0); 30 | Point corner4 = Point(-100, -100, 0); 31 | 32 | // Define spatial positioning of each segment 33 | // Coordinates of LEDs will be automatically calculated using start and end position of segment 34 | SpatialStripSegment spatial_segment1(segment1, corner1, corner2); 35 | SpatialStripSegment spatial_segment2(segment2, corner2, corner3); 36 | SpatialStripSegment spatial_segment3(segment3, corner3, corner4); 37 | SpatialStripSegment spatial_segment4(segment4, corner4, corner1); 38 | 39 | // Define array of pointers to spatial strip segments 40 | SpatialStripSegment_T* spatial_segments[NUM_SEGMENTS] = { 41 | &spatial_segment1, 42 | &spatial_segment2, 43 | &spatial_segment3, 44 | &spatial_segment4 45 | }; 46 | 47 | // Define linear patterns to map 48 | MovingPulsePattern pulse_pattern(8); 49 | FirePattern fire_pattern; 50 | 51 | // Define mapping of linear patterns to spatial coordinates 52 | LinearToSpatialPatternMapper spatial_pulse( 53 | pulse_pattern, 54 | pixel_data, NUM_PIXELS, 55 | Point(1,1,0), // Direction for pulse to move (diagonally up-right from bottom left corner to opposite) 56 | spatial_segments, NUM_SEGMENTS); 57 | 58 | LinearToSpatialPatternMapper spatial_fire( 59 | fire_pattern, 60 | pixel_data, NUM_PIXELS, 61 | Point(0,1,0), // Direction for pulse to move (from bottom upwards) 62 | spatial_segments, NUM_SEGMENTS); 63 | 64 | 65 | // Define array of MappingRunners for controller to use 66 | MappingRunner mappings[2] = { 67 | MappingRunner(spatial_pulse), 68 | MappingRunner(spatial_fire) 69 | }; 70 | 71 | // Define controller 72 | LEDuinoController controller(leds, NUM_LEDS, mappings, 2, false); 73 | 74 | void setup() { 75 | // Initialise FastLED 76 | FastLED.addLeds(leds, NUM_LEDS).setCorrection(TypicalLEDStrip); 77 | controller.initialise(); 78 | } 79 | 80 | void loop() { 81 | // put your main code here, to run repeatedly: 82 | controller.loop(); 83 | } 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | LEDuino is a powerful framework for defining animated patterns and mapping them to segments of an addressable LED strip controlled by an Arduino and FastLED. It consists of a collection of components which can be configured and combined in a declarative way to achieve a high level of customisation for your LED project. 3 | 4 | LEDuino makes it easy to: 5 | - Define animation patterns (linear or 3D) which can be initialised with parameter values and colour palettes to adjust behaviour 6 | - Split a single LED strip into segments which can have patterns mapped to them independently 7 | - Map linear patterns to LED strip segments of different lengths, with automatic scaling and interpolation 8 | - Define the position of LEDs in space to allow spatial (3D) pattern mapping 9 | - Run multiple different pattern mapping configurations at the same time 10 | - Project a linear pattern along a vector in 3D space 11 | - Cycle through a sequence of pattern mapping configurations 12 | 13 | 14 | ## Demonstration 15 | The below video demonstrates the same simple moving, colour changing pulse pattern being mapped in 3 different ways to my Infinity Cube project. 16 | 17 | [![InfinityCubePulseMapping](https://i.imgur.com/is2atVj.gif)](https://www.youtube.com/watch?v=DZlHctGWVvo "InfinityCubePulseMapping") 18 | 19 | 1. Linear mapped to each axis in positive direction (same pattern repeated on all edges, all pulses originate in same corner and move outwards then terminate at the end) 20 | 2. Same as #1 but one axis is reversed (looks like pulses are moving continuously and split or merge at corners). 21 | 3. Spatial mapping - instead of being mapped to each edge separately, the pulse moves along a spatial vector from the top right to the bottom left corners of the cube, moving along the different edges as required 22 | 23 | ## Requirements 24 | 25 | - Ardunio-compatible micocontroller. Bare minimum of 1kB of RAM and 16kB Flash for a basic linear pattern mapping configuration with a short LED strip. At least 8kB RAM, 64kB Flash and decent CPU is required for spatial pattern mapping or multiple concurrent patterns, depending on the complexity of your project and number of LEDs. A [Teensy 3.1+](https://www.pjrc.com/teensy/index.html) works great (can comfortably run complex pattern configurations on 300+ LEDs at 100+ FPS) 26 | - [FastLED](http://fastled.io/) Library 27 | - Individually addressable LED strip compatible with FastLED (e.g. Neopixel, WS2801, WS2811, WS2812B, LPD8806, TM1809, and [more](https://github.com/FastLED/FastLED/wiki/Chipset-reference)) 28 | 29 | ## Installation 30 | LEDuino can be installed from the Arduino or PlatformIO library manager. 31 | 32 | Otherwise, for manual installation: 33 | 1. Download library from [here](https://github.com/Finndersen/LEDuino/archive/refs/heads/master.zip). 34 | 2. Unzip and move to Arduino library folder (e.g. Documents/Arduino/libraries/) or PlatformIO project local library folder (lib/) 35 | 3. Rename folder to LEDuino 36 | 4. Restart Arduino IDE 37 | 38 | ## Getting Started 39 | The best way to get started and learn how to use the library is to check out the [tutorial](https://github.com/Finndersen/LEDuino/wiki/Tutorial). 40 | 41 | Then take a look at the [Reference](https://github.com/Finndersen/LEDuino/wiki/Reference) for more in-depth details, and check out some [Examples](https://github.com/Finndersen/LEDuino/tree/master/examples) to see how it might work for your project. 42 | 43 | ## Development & Support 44 | This project is still under development and may be subject to changes of the API. I made it for my own personal use but figured could be quite useful to others as well, so it has not been tested extensively in many configurations. Please jump on the [Discord](https://discord.gg/txfrrKSWPF) to let me know what you think about it, or if you have any issues or ideas! 45 | 46 | **TODO / Future work:** 47 | - Add more patterns and palettes 48 | - Integrate audio reactivity into the framework 49 | - Add support for easily configuring LED matrix displays 50 | - Bluetooth/remote control support 51 | -------------------------------------------------------------------------------- /src/LEDuino.h: -------------------------------------------------------------------------------- 1 | /* 2 | Full implementations in header file due to issues with templated classes and linking... 3 | */ 4 | #ifndef LEDuino_h 5 | #define LEDuino_h 6 | #include 7 | #include "utils.h" 8 | #include "Point.h" 9 | #include "StripSegment.h" 10 | #include "Pattern.h" 11 | #include "PatternMapping.h" 12 | #include "MappingRunner.h" 13 | 14 | #include "patterns/linear.h" 15 | #include "patterns/spatial.h" 16 | 17 | // Controller object which manages a collection of MappingRunners 18 | // Chooses which mapping to run, and handles running it at the desired framerate 19 | class LEDuinoController { 20 | public: 21 | //Constructor 22 | LEDuinoController( 23 | CRGB* leds, // Pointer to Array of CRGB LEDs which is registered with FastLED 24 | uint16_t num_leds, // Number of LEDS (length of leds) 25 | MappingRunner* mapping_runners, // Array of PatternConfigurations to run 26 | uint8_t num_mappings, // Number of pattern configurations (length of mapping_runners) 27 | bool randomize=false // Whether to randomize pattern order 28 | ): 29 | leds(leds), 30 | num_leds(num_leds), 31 | mapping_runners(mapping_runners), 32 | num_mappings(num_mappings), 33 | randomize(randomize), 34 | current_runner_id(num_mappings-1) {} 35 | 36 | void initialise() { 37 | this->setNewPatternMapping(); 38 | } 39 | 40 | void clear_leds() { 41 | // Reset LED state 42 | FastLED.clear(); 43 | FastLED.show(); 44 | } 45 | 46 | // Run pattern newFrame() if ready, set new pattern if required 47 | void loop() { 48 | // Check if pattern config needs to be changed 49 | if (this->current_runner->expired() && this->auto_change_pattern) { 50 | this->setNewPatternMapping(); 51 | } 52 | // New pattern frame 53 | if (this->current_runner->frameReady()) { 54 | #ifdef LEDUINO_DEBUG 55 | long pre_frame_time = micros(); 56 | #endif 57 | // Run pattern frame logic 58 | this->current_runner->newFrame(this->leds); 59 | 60 | #ifdef LEDUINO_DEBUG 61 | long pre_show_time = micros(); 62 | #endif 63 | // Show LEDs 64 | FastLED.show(); 65 | 66 | // Print frame logic execution time and FastLED.show() time if DEBUG is enabled 67 | #ifdef LEDUINO_DEBUG 68 | Serial.print("Frame Time: "); 69 | Serial.print(pre_show_time-pre_frame_time); 70 | Serial.print(" Show time: "); 71 | Serial.println(micros()-pre_show_time); 72 | Serial.flush(); 73 | #endif 74 | } 75 | } 76 | // Set current active pattern mapper by array index 77 | void setPatternMapping(uint8_t runner_id) { 78 | runner_id = limit(runner_id, this->num_mappings-1); 79 | this->current_runner_id = runner_id; 80 | this->current_runner = &(this->mapping_runners[runner_id]); 81 | #ifdef LEDUINO_DEBUG 82 | Serial.print("Choosing new pattern: " ); 83 | Serial.println(this->current_runner->name); 84 | Serial.flush(); 85 | #endif 86 | this->current_runner->reset(); 87 | this->clear_leds(); 88 | } 89 | 90 | MappingRunner* current_runner; // Currently selected mapping runner 91 | bool auto_change_pattern=true; // Can be set to false to stop automatically changing pattern mapping configurations 92 | private: 93 | 94 | CRGB* leds; 95 | const uint16_t num_leds; 96 | MappingRunner* mapping_runners; 97 | const uint8_t num_mappings; 98 | const bool randomize; 99 | long last_frame_time; 100 | uint8_t current_runner_id; 101 | 102 | // Set ID of new pattern configuration 103 | void setNewPatternMapping() { 104 | uint8_t new_pattern_id; 105 | if (this->randomize) { 106 | // Choose random pattern 107 | new_pattern_id = random(0, this->num_mappings); 108 | } else { 109 | // Choose next pattern 110 | new_pattern_id = (this->current_runner_id + 1)%(this->num_mappings); 111 | } 112 | 113 | // TODO: Add transition between patterns? 114 | setPatternMapping(new_pattern_id); 115 | } 116 | }; 117 | 118 | #endif -------------------------------------------------------------------------------- /src/ColorPicker.h: -------------------------------------------------------------------------------- 1 | #ifndef colorpicker_h 2 | #define colorpicker_h 3 | #include 4 | 5 | 6 | // Base class for picking a color for use by a pattern 7 | // Allows for subclasses which wrap and provides an interface to the various types of palettes used in FastLED 8 | // Can be subclassed to make custom palette implementations (doesnt have to involve using FastLEDs palette utilities) 9 | class ColorPicker { 10 | public: 11 | // Basic placeholder implementation just gets colour from provided hue and brightness 12 | virtual CRGB getColor(uint8_t hue, uint8_t brightness=255, uint8_t saturation=255) const { 13 | return CHSV(hue, saturation, brightness); 14 | }; 15 | 16 | }; 17 | 18 | // Color picker that always chooses the same constant color (hue) 19 | class ConstantHuePicker : public ColorPicker { 20 | public: 21 | ConstantHuePicker( 22 | uint8_t hue 23 | ): hue(hue) {} 24 | 25 | virtual CRGB getColor(uint8_t hue, uint8_t brightness=255, uint8_t saturation=255) const { 26 | return CHSV(this->hue, saturation, brightness); 27 | }; 28 | 29 | protected: 30 | uint8_t hue; 31 | }; 32 | 33 | 34 | // Templated Colour Picker class for using FastLED 16-entry RGB palette types (CRGBPalette16 and TProgmemRGBPalette16) 35 | template 36 | class PaletteColorPicker: public ColorPicker { 37 | public: 38 | PaletteColorPicker( 39 | const T& colour_palette, 40 | TBlendType blendType=LINEARBLEND // Set the blend type to use when choosing palette colour 41 | ): _palette(colour_palette), 42 | blendType(blendType) {} 43 | 44 | // Select colour from palette 45 | CRGB getColor(uint8_t hue, uint8_t brightness=255, uint8_t saturation=255) const override { 46 | return ColorFromPalette(this->_palette, hue, brightness, this->blendType); 47 | } 48 | 49 | protected: 50 | const T& _palette; 51 | TBlendType blendType; 52 | }; 53 | 54 | // Allows using a FastLED CRGBPalette16 defined in RAM 55 | typedef PaletteColorPicker RGBPalettePicker; 56 | 57 | // Allows using a FastLED TProgmemPalette16 type static palette which has its data stored in flash, 58 | // such as the FastLED 'preset' palettes: RainbowColors_p, RainbowStripeColors_p, OceanColors_p, etc 59 | typedef PaletteColorPicker ProgmemRGBPalettePicker; 60 | 61 | // Allows using a FastLED static gradient palette, declared using DEFINE_GRADIENT_PALETTE macro and stored in PROGMEM (Flash) 62 | // Creates a CRGBPalette16 instance in memory when initialised 63 | class GradientPalettePicker: public ColorPicker { 64 | public: 65 | GradientPalettePicker( 66 | const TProgmemRGBGradientPalettePtr colour_palette // Equivalent to TProgmemRGBGradientPalette_bytes 67 | ): _palette(colour_palette) {} 68 | 69 | // Select colour from palette 70 | CRGB getColor(uint8_t hue, uint8_t brightness=255, uint8_t saturation=255) const override { 71 | return ColorFromPalette(this->_palette, hue, brightness, LINEARBLEND); 72 | } 73 | 74 | protected: 75 | const CRGBPalette16 _palette; 76 | }; 77 | 78 | 79 | // Use when pattern does not require a palette 80 | ColorPicker Basic_picker; 81 | 82 | // LEDuino versions of some FastLED preset palettes 83 | ProgmemRGBPalettePicker RainbowColors_picker(RainbowColors_p); 84 | ProgmemRGBPalettePicker HeatColors_picker(HeatColors_p); 85 | 86 | 87 | // Additional Example Palettes 88 | 89 | // A pure "fairy light" palette with some brightness variations 90 | #define HALFFAIRY ((CRGB::FairyLight & 0xFEFEFE) / 2) 91 | #define QUARTERFAIRY ((CRGB::FairyLight & 0xFCFCFC) / 4) 92 | const TProgmemRGBPalette16 FairyLight_p FL_PROGMEM = 93 | { CRGB::FairyLight, CRGB::FairyLight, CRGB::FairyLight, CRGB::FairyLight, 94 | HALFFAIRY, HALFFAIRY, CRGB::FairyLight, CRGB::FairyLight, 95 | QUARTERFAIRY, QUARTERFAIRY, CRGB::FairyLight, CRGB::FairyLight, 96 | CRGB::FairyLight, CRGB::FairyLight, CRGB::FairyLight, CRGB::FairyLight 97 | }; 98 | ProgmemRGBPalettePicker FairyLight_picker(FairyLight_p, NOBLEND); 99 | 100 | 101 | // A palette reminiscent of large 'old-school' C9-size tree lights 102 | // in the five classic colors: red, orange, green, blue, and white. 103 | #define C9_Red 0xB80400 104 | #define C9_Orange 0x902C02 105 | #define C9_Green 0x046002 106 | #define C9_Blue 0x070758 107 | #define C9_White 0x606820 108 | const TProgmemRGBPalette16 RetroC9_p FL_PROGMEM = 109 | { C9_Red, C9_Orange, C9_Red, C9_Orange, 110 | C9_Orange, C9_Red, C9_Orange, C9_Red, 111 | C9_Green, C9_Green, C9_Green, C9_Green, 112 | C9_Blue, C9_Blue, C9_Blue, 113 | C9_White 114 | }; 115 | ProgmemRGBPalettePicker RetroC9Colors_picker(RetroC9_p); 116 | 117 | // define some shorthands for the Halloween colors 118 | #define PURP 0x6611FF 119 | #define ORAN 0xFF6600 120 | #define GREN 0x00FF11 121 | #define WHIT 0xCCCCCC 122 | 123 | // set up a new 16-color palette with the Halloween colors 124 | const TProgmemRGBPalette16 HalloweenColors_p FL_PROGMEM = 125 | { 126 | PURP, PURP, PURP, PURP, 127 | ORAN, ORAN, ORAN, ORAN, 128 | PURP, PURP, PURP, PURP, 129 | GREN, GREN, GREN, WHIT 130 | }; 131 | ProgmemRGBPalettePicker HalloweenColors_picker(HalloweenColors_p, NOBLEND); 132 | 133 | // Example Gradient palette 134 | // DEFINE_GRADIENT_PALETTE(xmas_p) { 135 | // 0, 255, 0, 0, // red 136 | // 64, 0, 255, 0, // green 137 | // 128, 0, 0, 255, // blue 138 | // 192, 255, 255, 0, // yellow 139 | // 255, 235, 40, 200 // purple 140 | // }; 141 | 142 | // GradientPalettePicker XMasColors_picker(xmas_p); 143 | 144 | #endif -------------------------------------------------------------------------------- /src/StripSegment.h: -------------------------------------------------------------------------------- 1 | #ifndef STRIPSEGMENT_H 2 | #define STRIPSEGMENT_H 3 | #include "Point.h" 4 | #include "utils.h" 5 | #include "Array.h" 6 | 7 | //Class to define an StripSegment which corresponds to a sub-section of an LED Strip. 8 | //Specify starting offset and lenth of segment. Allows extending over LED strip limits (wrap around from end back to start) 9 | class StripSegment { 10 | public: 11 | // Constructor 12 | StripSegment( 13 | uint16_t start_offset, // Start offset of segment (relative to start of LED strip) 14 | uint16_t segment_len, // Length of segment (number of LEDS) 15 | uint16_t strip_len, // Full length of LED strip (to enable wrap-over) 16 | bool reverse=false // Whether segment is reversed (LED strip ID decreases with increasing segment index) 17 | ): 18 | start_offset(start_offset), 19 | segment_len(segment_len), 20 | strip_len(strip_len), 21 | reverse(reverse) {} 22 | 23 | // Get LED Strip ID from segment position 24 | uint16_t getLEDId(uint16_t segment_pos) const { 25 | uint16_t led_id; 26 | // Limit value to maximum length 27 | segment_pos = limit(segment_pos, this->segment_len-1); 28 | if (this->reverse) { 29 | // Detect wrap around from 0 back to end of LED strip 30 | led_id = wrap_subtract(this->start_offset, segment_pos+1, this->strip_len-1); 31 | } else { 32 | // Modulo with strip len to enable wrap-around 33 | led_id = (this->start_offset + segment_pos)%this->strip_len; 34 | } 35 | 36 | return led_id; 37 | } 38 | 39 | // Negation operator overloading to get reverse version of segment 40 | // New segment will cover the same set of LEDs, but have shifted start_offset and be in reverse direction 41 | StripSegment operator-() { 42 | int reverse_start_offset; 43 | if (this->reverse) { 44 | reverse_start_offset = this->start_offset - this->segment_len; 45 | // Handle wrap-around below 0 46 | if (reverse_start_offset < 0) { 47 | reverse_start_offset = this->segment_len + reverse_start_offset; 48 | } 49 | return StripSegment(reverse_start_offset, this->segment_len, this->strip_len, false); 50 | 51 | } else { 52 | reverse_start_offset = (this->start_offset + this->segment_len)%this->strip_len; 53 | return StripSegment(reverse_start_offset, this->segment_len, this->strip_len, true); 54 | } 55 | } 56 | 57 | const uint16_t start_offset, segment_len, strip_len; 58 | const bool reverse; 59 | }; 60 | 61 | // Base interface class for SpatialStripSegment, used for typing without template 62 | class SpatialStripSegment_T { 63 | public: 64 | SpatialStripSegment_T( 65 | const StripSegment& strip_segment // LED Strip segment 66 | ): strip_segment(strip_segment) {} 67 | 68 | // Get spatial bounding area covered by this spatial segment 69 | virtual Bounds get_bounds() = 0; 70 | // Get spatial position of an LED on the segment 71 | virtual Point getSpatialPosition(uint16_t segment_pos) = 0; 72 | 73 | const StripSegment& strip_segment; // LED Strip segment for axis 74 | 75 | }; 76 | 77 | // Class to define spatial positioning of a strip segment for use with a SpatialPatternMapper 78 | // Provide a StripSegment along with an array of Points which define the positions of each LED in the segment 79 | // If the segment is straight and LEDs are evenly spaced, can initialise with the start and end positions of the segment 80 | // and the coordinates for each LED will be automatically calculated. 81 | // Generally want to define axis positions such that the coordinate origin is at the physical centre of your project 82 | template 83 | class SpatialStripSegment : public SpatialStripSegment_T { 84 | public: 85 | // Construct with pre-defined array of LED positions 86 | SpatialStripSegment( 87 | const StripSegment& strip_segment, // LED Strip segment 88 | Array led_positions // Array of coordinates of segment LEDs (same length as segment) 89 | ): SpatialStripSegment_T(strip_segment), led_positions(led_positions) {} 90 | 91 | // If the segment is straight and LEDs are evenly spaced, can initialise with the start and end positions 92 | // of the segment and the coordinates for each LED will be automatically calculated 93 | SpatialStripSegment( 94 | const StripSegment& strip_segment, // LED Strip segment 95 | Point start_pos, // Start position of straight segment in 3D (Position of first LED) 96 | Point end_pos // End position of straight segment in 3D space (Position of last LED) 97 | ): SpatialStripSegment(strip_segment, led_positions) { 98 | // Pre-Calculate coordinate positions of each LED in strip segment 99 | for (uint16_t i=0; i < strip_segment.segment_len; i++) { 100 | this->led_positions[i] = start_pos + (end_pos-start_pos)*(((float) i)/(strip_segment.segment_len-1)); 101 | } 102 | } 103 | 104 | // Get spatial bounding area covered by this spatial segment 105 | virtual Bounds get_bounds() { 106 | return get_bounds_of_points(this->led_positions.data, this->strip_segment.segment_len); 107 | }; 108 | 109 | // Get spatial position of an LED on the segment 110 | Point getSpatialPosition(uint16_t segment_pos) { 111 | // Constrain to max position 112 | segment_pos = limit(segment_pos, t_segment_length-1); 113 | return this->led_positions[segment_pos]; 114 | } 115 | 116 | protected: 117 | // Use Array class to allow providing position array inline to constructor 118 | Array led_positions; 119 | }; 120 | 121 | // Get the bounding box of a collection of Spatial Segments 122 | Bounds get_spatial_segment_bounds(SpatialStripSegment_T* spatial_segments[], uint16_t num_segments) { 123 | Point global_max(FLT_MIN, FLT_MIN, FLT_MIN); 124 | Point global_min(FLT_MAX, FLT_MAX, FLT_MAX); 125 | 126 | for (uint16_t i=0; iget_bounds(); 129 | // Update minimums 130 | if (segment_bounds.min_point.x < global_min.x) global_min.x = segment_bounds.min_point.x; 131 | if (segment_bounds.min_point.y < global_min.y) global_min.y = segment_bounds.min_point.y; 132 | if (segment_bounds.min_point.z < global_min.z) global_min.z = segment_bounds.min_point.z; 133 | 134 | if (segment_bounds.max_point.x > global_max.x) global_max.x = segment_bounds.max_point.x; 135 | if (segment_bounds.max_point.y > global_max.y) global_max.y = segment_bounds.max_point.y; 136 | if (segment_bounds.max_point.z > global_max.z) global_max.z = segment_bounds.max_point.z; 137 | } 138 | return Bounds(global_min, global_max); 139 | } 140 | 141 | #endif -------------------------------------------------------------------------------- /src/Point.h: -------------------------------------------------------------------------------- 1 | #ifndef Point_h 2 | #define Point_h 3 | 4 | #include 5 | #include "utils.h" 6 | #include "Arduino.h" 7 | 8 | 9 | // Structure to represent a cartesian coordinate or vector 10 | class Point: public Printable { 11 | public: 12 | float x=0, y=0, z=0; 13 | 14 | //Initialise explicitly 15 | Point(float x, float y, float z): x(x), y(y), z(z) {}; 16 | // 2D (z default to 0) 17 | Point(float x, float y): Point(x, y, 0) {}; 18 | // Initialise from array 19 | Point(float* arr): Point(arr[0], arr[1], arr[2]) {}; 20 | // Default constructor 21 | Point(): Point(0, 0, 0) {}; 22 | 23 | // Vector Addition and subtraction 24 | Point& operator+=(const Point &RHS) { 25 | x += RHS.x; 26 | y += RHS.y; 27 | z += RHS.z; 28 | return *this; 29 | }; 30 | Point& operator-=(const Point &RHS) { 31 | x -= RHS.x; 32 | y -= RHS.y; 33 | z -= RHS.z; 34 | return *this; }; 35 | 36 | Point operator+(const Point &RHS) const { return Point(x + RHS.x, y + RHS.y, z + RHS.z); }; 37 | Point operator-(const Point &RHS) const { return Point(x - RHS.x, y - RHS.y, z - RHS.z); }; 38 | 39 | // Scalar addition and subtraction 40 | Point& operator+=(const float &RHS) { 41 | x += RHS; 42 | y += RHS; 43 | z += RHS; 44 | return *this; 45 | }; 46 | Point& operator-=(const float &RHS) { 47 | x -= RHS; 48 | y -= RHS; 49 | z -= RHS; 50 | return *this; 51 | }; 52 | 53 | Point operator+(const float &RHS) const { return Point(x+RHS, y+RHS, z+RHS);}; 54 | Point operator-(const float &RHS) const { return Point(x-RHS, y-RHS, z-RHS);}; 55 | 56 | // Scalar product and division 57 | template 58 | Point& operator*=(const T RHS) { 59 | this->x *= RHS; 60 | this->y *= RHS; 61 | this->z *= RHS; 62 | return *this; 63 | }; 64 | template 65 | Point& operator/=(const T RHS) { 66 | this->x /= RHS; 67 | this->y /= RHS; 68 | this->z /= RHS; 69 | return *this; 70 | }; 71 | 72 | template 73 | Point operator*(const T RHS) const { return Point(*this) *= RHS; }; 74 | template 75 | Point operator/(const T RHS) const { return Point(*this) /= RHS; }; 76 | 77 | // Element-wise multiplication and division 78 | Point hadamard_product(const Point &RHS) {return Point(this->x*RHS.x, this->y*RHS.y, this->z*RHS.z); }; 79 | Point hadamard_divide(const Point &RHS) {return Point(this->x/RHS.x, this->y/RHS.y, this->z/RHS.z); }; 80 | 81 | // Negation 82 | Point operator-() const {return Point(-x, -y, -z); }; 83 | 84 | // Euclidean norm 85 | const float norm() const { 86 | return sqrt(x*x + y*y + z*z); 87 | }; 88 | 89 | // Calculate distance of this point from plane defined by a normal vector and point 90 | float distance_to_plane(Point& norm_vector, Point& plane_point) const { 91 | // Calculate coefficent D of plane equation 92 | float D = norm_vector.x*plane_point.x + norm_vector.y*plane_point.y + norm_vector.z*plane_point.z; 93 | // Get numerator of distance equation 94 | float num = abs(norm_vector.x*x + norm_vector.y*y + norm_vector.z*z - D); 95 | return num / norm_vector.norm(); 96 | 97 | } 98 | 99 | // Distance to other point 100 | float distance(const Point& other) const { 101 | return sqrt(this->distance_squared(other)); 102 | }; 103 | 104 | // Square of Distance to other point (useful for doing distance comparisons and dont want to square root) 105 | float distance_squared(const Point& other) const { 106 | return pow(other.x-this->x, 2) + pow(other.y-this->y, 2) + pow(other.z-this->z, 2); 107 | }; 108 | 109 | 110 | size_t printTo(Print& p) const { 111 | size_t size; 112 | size = p.print("("); 113 | size += p.print(this->x); 114 | size += p.print(", "); 115 | size += p.print(this->y); 116 | size += p.print(", "); 117 | size += p.print(this->z); 118 | size += p.print(")"); 119 | return size; 120 | } 121 | }; 122 | // Implement binary operators as free (non-member) functions to enable symmetry 123 | inline bool operator==(const Point& lhs, const Point& rhs){ return lhs.x==rhs.x && lhs.y==rhs.y && lhs.z==rhs.z; } 124 | inline bool operator!=(const Point& lhs, const Point& rhs){return !operator==(lhs,rhs);} 125 | 126 | template 127 | inline Point operator/(const T lhs, const Point &rhs) { return Point(lhs/rhs.x, lhs/rhs.y, lhs/rhs.z); } 128 | template 129 | inline Point operator*(const T lhs, const Point &rhs) { return rhs*lhs; } 130 | //inline bool operator< (const Point& lhs, const Point& rhs){ /* do actual comparison */ } 131 | //inline bool operator> (const Point& lhs, const Point& rhs){return operator< (rhs,lhs);} 132 | //inline bool operator<=(const Point& lhs, const Point& rhs){return !operator> (lhs,rhs);} 133 | //inline bool operator>=(const Point& lhs, const Point& rhs){return !operator< (lhs,rhs);} 134 | 135 | // Direction vectors 136 | Point v_x(1, 0, 0); 137 | Point v_y(0, 1, 0); 138 | Point v_z(0, 0, 1); 139 | 140 | Point undefinedPoint(FLT_MIN, FLT_MIN, FLT_MIN); 141 | 142 | // Class to define bounding box (rectangular prism) defined by minimum (bottom left) and maximum (top right) points 143 | class Bounds { 144 | public: 145 | Bounds(Point min_point, Point max_point): min_point(min_point), max_point(max_point) {} 146 | 147 | // Get vector which represents magnitude of bounds in each coordinate (width, length and depth) 148 | Point magnitude() { 149 | return Point( 150 | this->max_point.x - this->min_point.x, 151 | this->max_point.y - this->min_point.y, 152 | this->max_point.z - this->min_point.z); 153 | } 154 | 155 | // Centre point of bounds 156 | Point centre() { 157 | return Point( 158 | this->max_point.x + this->min_point.x, 159 | this->max_point.y + this->min_point.y, 160 | this->max_point.z + this->min_point.z)/2; 161 | } 162 | 163 | // Whether or not point is contained in bounds 164 | bool contains(Point point) { 165 | return ((point.x <= this->max_point.x) && (point.x >= this->min_point.x) && 166 | (point.y <= this->max_point.y) && (point.y >= this->min_point.y) && 167 | (point.z <= this->max_point.z) && (point.z >= this->min_point.z) 168 | ); 169 | } 170 | 171 | Point min_point, max_point; 172 | }; 173 | 174 | // Get Bounds of an array of points 175 | Bounds get_bounds_of_points(Point* points, uint16_t num_points) { 176 | Point max_point(FLT_MIN, FLT_MIN, FLT_MIN); 177 | Point min_point(FLT_MAX, FLT_MAX, FLT_MAX); 178 | for (uint16_t i=0; i < num_points; i++) { 179 | Point& point = points[i]; 180 | if (point.x > max_point.x) max_point.x = point.x; 181 | if (point.y > max_point.y) max_point.y = point.y; 182 | if (point.z > max_point.z) max_point.z = point.z; 183 | 184 | if (point.x < min_point.x) min_point.x = point.x; 185 | if (point.y < min_point.y) min_point.y = point.y; 186 | if (point.z < min_point.z) min_point.z = point.z; 187 | } 188 | return Bounds(min_point, max_point); 189 | }; 190 | 191 | #endif 192 | -------------------------------------------------------------------------------- /src/PatternMapping.h: -------------------------------------------------------------------------------- 1 | #ifndef PatternMapping_h 2 | #define PatternMapping_h 3 | #include 4 | #include 5 | #include "StripSegment.h" 6 | #include "Pattern.h" 7 | #include "Point.h" 8 | 9 | 10 | // Base interface class for defining a mapping of a pattern to some kind of configuration of LEDS 11 | // E.g. a linear segment (single axis) or 2D/3D spatial array of LEDs composed of multiple axes 12 | class BasePatternMapper { 13 | public: 14 | // Initialise/Reset pattern state 15 | virtual void reset() const {}; 16 | 17 | // Excute new frame of pattern and map results to LED array 18 | virtual void newFrame(CRGB* leds, uint16_t frame_time) const = 0; 19 | 20 | }; 21 | 22 | // Base class for Mappings that use a LinearPattern 23 | class BaseLinearPatternMapper: public BasePatternMapper { 24 | public: 25 | BaseLinearPatternMapper( 26 | LinearPattern& pattern, // LinearPattern object 27 | CRGB* pixel_data, // Pixel array for LinearPattern to mutate (length equal to num_pixels) 28 | uint16_t num_pixels // Number of pixels for linear pattern to use (pattern resolution) 29 | ): 30 | pattern(pattern), 31 | pixel_data(pixel_data), 32 | num_pixels(num_pixels) {} 33 | 34 | // Initialise/Reset pattern state 35 | void reset() const override { 36 | fill_solid(this->pixel_data, this->num_pixels, CRGB::Black); 37 | this->pattern.reset(); 38 | } 39 | 40 | protected: 41 | LinearPattern& pattern; 42 | CRGB* pixel_data; 43 | const uint16_t num_pixels; 44 | 45 | }; 46 | 47 | // Handles the mapping of a LinearPattern to a collection of LED Strip Segments 48 | // The pattern will be interpolated to the length of each strip segment 49 | class LinearPatternMapper: public BaseLinearPatternMapper { 50 | public: 51 | // Constructor 52 | LinearPatternMapper( 53 | LinearPattern& pattern, // LinearPattern to map to segments 54 | CRGB* pixel_data, // Pixel array for LinearPattern to mutate (length equal to num_pixels) 55 | uint16_t num_pixels, // Number of pixels for linear pattern to use (pattern resolution) 56 | StripSegment strip_segments[], // Array of StripSegments to map pattern to 57 | uint8_t num_segments // Number of axes (length of strip_segments) 58 | ): 59 | BaseLinearPatternMapper(pattern, pixel_data, num_pixels), 60 | strip_segments(strip_segments), 61 | num_segments(num_segments) {} 62 | 63 | // Excute new frame of pattern and map results to LED array 64 | // This implementation involves calling pattern.getPixelValue() multiple times for the same pattern pixel index which is inefficient 65 | // Alternatively, it could be called once for every index and the results stored in an array which can be re-used 66 | // The two approaches are a trade-off between memory and CPU usage, but generally for linear patterns CPU is not a bottleneck, 67 | // and LinearStatePatterns have their own pixel array anyway and can be used if required 68 | void newFrame(CRGB* leds, uint16_t frame_time) const override { 69 | // Run pattern logic 70 | this->pattern.frameAction(this->pixel_data, this->num_pixels, frame_time); 71 | uint16_t pat_len = this->num_pixels; 72 | // Map pattern to all registered strip segments (will be scaled to each segment length) 73 | for (uint8_t seg_id=0; seg_id < this->num_segments; seg_id++) { 74 | StripSegment& strip_segment = this->strip_segments[seg_id]; 75 | 76 | if (strip_segment.segment_len == pat_len) { 77 | // When segment length is equal to pattern pixel resolution, no need to downsample. 78 | interpolate_equal_length(leds, strip_segment); 79 | } else if (pat_len % strip_segment.segment_len == 0) { 80 | // Optimisation for when pattern length is an integer multiple of the segment length 81 | interpolate_integer_multiple_length(leds, strip_segment); 82 | } else { 83 | // General case of interpolating arbitrary length pattern data (resolution) to strip segment 84 | interpolate_arbitrary_length(leds, strip_segment); 85 | } 86 | } 87 | }; 88 | 89 | protected: 90 | // Interpolate pattern pixel data to the provided strip segment, when pattern length (resolution) is equal to segment length 91 | void interpolate_equal_length(CRGB* leds, StripSegment& strip_segment) const { 92 | for (uint16_t led_seg_ind=0; led_seg_indpixel_data[led_seg_ind]; 97 | } 98 | }; 99 | 100 | // Interpolate pattern pixel data to the provided strip segment, when pattern length (resolution) is an integer multiple of segment length 101 | void interpolate_integer_multiple_length(CRGB* leds, StripSegment& strip_segment) const { 102 | uint8_t scale_factor = this->num_pixels / strip_segment.segment_len; 103 | for (uint16_t led_seg_ind=0; led_seg_indpixel_data[pixel_id]; 111 | r += led_val.red; 112 | g += led_val.green; 113 | b += led_val.blue; 114 | }; 115 | 116 | leds[led_strip_ind] = CRGB(r/scale_factor, g/scale_factor, b/scale_factor); 117 | } 118 | }; 119 | 120 | // Interpolate pattern pixel data to the provided strip segment, for an arbitrary pattern length (resolution) 121 | void interpolate_arbitrary_length(CRGB* leds, StripSegment& strip_segment) const { 122 | uint16_t seg_len = strip_segment.segment_len; 123 | uint16_t pat_len = this->num_pixels; 124 | for (uint16_t led_seg_ind=0; led_seg_indpixel_data[pat_ind]; 144 | uint16_t weight; 145 | if (pat_ind == start_index) { 146 | weight = first_weight; 147 | } else if (remaining_weight > seg_len) { 148 | weight = seg_len; 149 | } else { 150 | weight = remaining_weight; 151 | } 152 | r += weight*led_val.red; 153 | g += weight*led_val.green; 154 | b += weight*led_val.blue; 155 | pat_ind += 1; 156 | remaining_weight -= weight; 157 | 158 | } while (remaining_weight>0); 159 | 160 | // Assign downsampled pixel value 161 | leds[led_strip_ind] = CRGB(r/pat_len, g/pat_len, b/pat_len); 162 | } 163 | }; 164 | 165 | StripSegment* strip_segments; 166 | const uint8_t num_segments; // Number of configured strip segments to map pattern to 167 | 168 | }; 169 | 170 | 171 | // Class for handling the mapping of a 3DPattern to set of segments with spatial positioning 172 | // The SpatialPattern has its own coordinate system (bounds of +/- resolution on each axis), 173 | // and there is also the physical project coordinate system (the spatial positions of LEDS as defined in SpatialStripSegments) 174 | // The 'scale' and 'offset' vectors are used to map the pattern coordinate system to project space 175 | // If not specified, scale is calcualted automatically based on bounds of SpatialStripSegment, and offset is equal to project centroid 176 | class SpatialPatternMapper: public BasePatternMapper { 177 | public: 178 | // Constructor 179 | SpatialPatternMapper( 180 | SpatialPattern& pattern, // Reference to SpatialPattern object 181 | SpatialStripSegment_T* spatial_segments[], // Array of SpatialStripSegment pointers to map pattern to 182 | uint8_t num_segments, // Number of SpatialStripSegments (length of spatial_segments) 183 | Point offset=undefinedPoint, // Translational offset to apply to Project coordinate system before scaling 184 | Point scale_factors=undefinedPoint // Scaling factors to apply to Project coordinate system to map to Pattern coordinates 185 | ): 186 | pattern(pattern), 187 | spatial_segments(spatial_segments), 188 | num_segments(num_segments), 189 | offset(offset), 190 | scale_factors(scale_factors) { 191 | // Calculate Project space scale 192 | Bounds project_bounds = get_spatial_segment_bounds(spatial_segments, num_segments); 193 | this->project_centroid = project_bounds.centre(); 194 | 195 | // Set automatically if not specified (scale project bounds to fit pattern space) 196 | if (this->scale_factors == undefinedPoint) { 197 | this->scale_factors = (2.0*pattern.resolution)/project_bounds.magnitude(); 198 | 199 | } 200 | // Default offset to centre of project bounds so it is translated to be centered on origin of pattern space 201 | if (this->offset == undefinedPoint) { 202 | this->offset = this->project_centroid; 203 | } 204 | }; 205 | 206 | // Initialise/Reset pattern state 207 | void reset() const override { 208 | BasePatternMapper::reset(); 209 | this->pattern.reset(); 210 | }; 211 | 212 | // Excute new frame of pattern and map results to LED array 213 | void newFrame(CRGB* leds, uint16_t frame_time) const override { 214 | // Run pattern frame logic 215 | this->pattern.frameAction(frame_time); 216 | // Loop through every LED (segment and segment index combination), determine spatial position and get value 217 | for (uint8_t segment_id=0; segment_id < this->num_segments; segment_id++) { 218 | SpatialStripSegment_T* spatial_segment = this->spatial_segments[segment_id]; 219 | // Loop through all positions on axis 220 | for (uint16_t segment_pos=0; segment_pos < spatial_segment->strip_segment.segment_len; segment_pos++) { 221 | // Get position from spatial axis 222 | Point pos = spatial_segment->getSpatialPosition(segment_pos); 223 | // Get LED ID from strip segment 224 | uint16_t led_id = spatial_segment->strip_segment.getLEDId(segment_pos); 225 | // Translate spatial position to pattern coordinates 226 | Point pattern_pos = ((pos - this->offset)).hadamard_product(this->scale_factors); 227 | // Get and assign LED value from pattern 228 | leds[led_id] = this->pattern.getPixelValue(pattern_pos); 229 | } 230 | } 231 | }; 232 | 233 | protected: 234 | SpatialPattern& pattern; 235 | SpatialStripSegment_T** spatial_segments; // Array of SpatialStripSegment pointers to map pattern to 236 | const uint8_t num_segments; // Number of configured strip segments to map pattern to 237 | Point offset; // Offset of Pattern space from Project space (in Project coordinates, before scaling applied) 238 | Point scale_factors; // Scaling vector for Project space to Pattern space transformation 239 | Point project_centroid; // Centre point of project coordinate bounds 240 | }; 241 | 242 | // Allows for mapping a linear pattern to a vector (linear path/direction) in 3D space 243 | // The pattern pixel applied to each LED is determined by the LED's distance from the perpendicular plane at the start of the vector path 244 | // By default, the linear pattern path will start on the edge of the projects bounds and move through it in the direction of the vector and end at the bounds on the other side 245 | // The start position and length of the path can be adjusted using the offset and scale parameters 246 | // Since the pattern pixel for an LED is determine by its distance from the start position, be default the effect will be mirrored about the start of the vector path 247 | // If start position is outside the bounds of the LEDs, then this will not make any difference. Otherwise, this can be disabled by setting mirrored=false (with an extra performance cost) 248 | class LinearToSpatialPatternMapper : public BaseLinearPatternMapper { 249 | public: 250 | // Constructor 251 | LinearToSpatialPatternMapper ( 252 | LinearPattern& pattern, // LinearPattern object 253 | CRGB* pixel_data, // Pixel array for LinearPattern to mutate (length equal to pattern resolution) 254 | uint16_t num_pixels, // Number of pixels for linear pattern to use (pattern resolution) 255 | Point pattern_vector, // Direction vector to map pattern to 256 | SpatialStripSegment_T* spatial_segments[], // Array of SpatialStripSegments to map pattern to 257 | uint8_t num_segments, // Number of SpatialStripSegments (length of spatial_segments) 258 | int16_t offset=0, // Offset of pattern vector start position 259 | float scale=1, // Scaling factor to apply to linear pattern vector length 260 | bool mirrored=true // Whether linear pattern is mirrored around start position on vector 261 | ): 262 | BaseLinearPatternMapper(pattern, pixel_data, num_pixels), 263 | pattern_vector(pattern_vector), 264 | spatial_segments(spatial_segments), 265 | num_segments(num_segments), 266 | mirrored(mirrored) { 267 | // Get vector length 268 | float vector_len = this->pattern_vector.norm(); 269 | 270 | // Get bounding box of all Spatial Segments 271 | Bounds bounds = get_spatial_segment_bounds(spatial_segments, num_segments); 272 | Point bounds_size = bounds.magnitude(); 273 | // Get full length of linear pattern vector within spatial bounding box 274 | uint16_t unscaled_path_len = (abs(this->pattern_vector.x*bounds_size.x) + abs(this->pattern_vector.y*bounds_size.y) + abs(this->pattern_vector.z*bounds_size.z))/vector_len; 275 | 276 | // Get start position of pattern path (centre of bounds - (unscaled_path_len/2 - offset) in direction of pattern_vector) 277 | this->path_start_pos = bounds.centre() - (((unscaled_path_len/2 - offset) * this->pattern_vector)/vector_len); 278 | // Apply scale to full vector length to get desired path length 279 | this->path_length = scale * unscaled_path_len; 280 | this->path_end_pos = this->path_start_pos + (this->path_length * this->pattern_vector)/vector_len; 281 | 282 | // Pre-Calculate coefficent D of plane equation (for calculating distance from plane) 283 | this->plane_eq_D = pattern_vector.x*this->path_start_pos.x + pattern_vector.y*this->path_start_pos.y + pattern_vector.z*this->path_start_pos.z; 284 | // Pre-calculate inverse of pattern vector norm 285 | this->inv_pattern_vect_norm = 1/vector_len; 286 | // Pre-calculate pattern resolution / length constant 287 | this->res_per_len = ((float) this->num_pixels-1.0)/this->path_length; 288 | }; 289 | 290 | // Excute new frame of pattern and map results to LED array 291 | void newFrame(CRGB* leds, uint16_t frame_time) const override { 292 | // Run pattern logic 293 | this->pattern.frameAction(this->pixel_data, this->num_pixels, frame_time); 294 | // Loop through every LED (axis and axis position combination), determine spatial position and appropriate state from pattern 295 | for (uint8_t segment_id=0; segment_id < this->num_segments; segment_id++) { 296 | SpatialStripSegment_T* spatial_axis = this->spatial_segments[segment_id]; 297 | // Loop through all positions on segment 298 | for (uint16_t segment_pos=0; segment_posstrip_segment.segment_len; segment_pos++) { 299 | // Get global LED ID from strip segment 300 | uint16_t led_id = spatial_axis->strip_segment.getLEDId(segment_pos); 301 | // Get spatial position of LED 302 | Point led_pos = spatial_axis->getSpatialPosition(segment_pos); 303 | // If mirroring is not enabled, need check if LED pos is in pattern_vector direction from start_pos 304 | if (!this->mirrored) { 305 | // Get projected LED position on pattern path 306 | Point pos_on_path = led_pos.hadamard_product(this->pattern_vector) * this->inv_pattern_vect_norm; 307 | if (!between(pos_on_path.x, this->path_start_pos.x, this->path_end_pos.x) || 308 | !between(pos_on_path.y, this->path_start_pos.y, this->path_end_pos.y) || 309 | !between(pos_on_path.y, this->path_start_pos.z, this->path_end_pos.z)) { 310 | leds[led_id] = CRGB::Black; 311 | continue; 312 | } 313 | } 314 | // Get distance of LED from plane through pattern path start position (use pre-calculated constants instead of Point.distance_to_plane() for efficiency) 315 | uint16_t dist_from_start = abs(this->pattern_vector.x*led_pos.x + this->pattern_vector.y*led_pos.y + this->pattern_vector.z*led_pos.z - this->plane_eq_D) * this->inv_pattern_vect_norm; 316 | if (dist_from_start > this->path_length) { 317 | // All LEDS beyond the end of the path should be set to black 318 | leds[led_id] = CRGB::Black; 319 | } else { 320 | // Get pattern value at same proportional position along pattern axis 321 | // For now just round to nearest, could do interpolation between two 322 | uint16_t pattern_axis_pos = round(dist_from_start*this->res_per_len); 323 | leds[led_id] = this->pixel_data[pattern_axis_pos]; 324 | } 325 | } 326 | } 327 | } 328 | 329 | protected: 330 | const Point pattern_vector; // Vector of direction to apply linear pattern 331 | SpatialStripSegment_T** spatial_segments; 332 | const uint8_t num_segments; // Number of configured strip segments to map pattern to 333 | const bool mirrored; 334 | 335 | Point path_start_pos, path_end_pos; 336 | uint16_t path_length; // Length of path that linear pattern will travel through 337 | float plane_eq_D, inv_pattern_vect_norm, res_per_len; // Pre-calculated constants for plane distance calculation 338 | }; 339 | 340 | // Allows for multiple pattern mappings to be applied at the same time 341 | // Can have multiple LinearPatternMapper or SpatialPatternMappings running concurrently on different parts of the same strip of LEDS 342 | class MultiplePatternMapper : public BasePatternMapper { 343 | public: 344 | // Constructor 345 | MultiplePatternMapper( 346 | BasePatternMapper** mappings, // Array of pointers to other PatternMappings to apply 347 | uint8_t num_mappings // Number of Pattern Mappings (length of mappings) 348 | ): 349 | mappings(mappings), 350 | num_mappings(num_mappings) {} 351 | 352 | // Initialise/Reset pattern state 353 | void reset() const override { 354 | for (uint8_t i=0; i < this->num_mappings; i++) { 355 | this->mappings[i]->reset(); 356 | } 357 | }; 358 | 359 | // Excute new frame of all pattern mappings 360 | void newFrame(CRGB* leds, uint16_t frame_time) const override { 361 | for (uint8_t i=0; i < this->num_mappings; i++) { 362 | this->mappings[i]->newFrame(leds, frame_time); 363 | } 364 | }; 365 | 366 | protected: 367 | BasePatternMapper** mappings; 368 | const uint8_t num_mappings; 369 | }; 370 | #endif -------------------------------------------------------------------------------- /src/patterns/linear.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include "Pattern.h" 3 | #include "ColorPicker.h" 4 | 5 | // BASIC PATTERNS 6 | 7 | // Lights all LEDs up in one random color, then fades to the next random color. 8 | // Can use a ConstantHuePicker to just have a constant solid color 9 | // Adapted from WLED by Aircookie 10 | class RandomColorFadePattern: public LinearPattern { 11 | public: 12 | RandomColorFadePattern( 13 | uint8_t cycle_time=128, // Color cycle time in 16th of a second 14 | uint8_t fade_time=128, // Time taken to fade to next colour as fraction of cycle_time 15 | const ColorPicker& color_picker=Basic_picker): 16 | LinearPattern(color_picker), 17 | cycle_time(cycle_time), 18 | fadedur(uint16_t(fade_time*cycle_time) >> 8) {} 19 | 20 | void frameAction(CRGB* pixel_data, uint16_t num_pixels, uint32_t frame_time) override { 21 | static uint32_t prev_change_time = 0; 22 | static uint16_t cycle_time_ms = this->cycle_time << 6; 23 | static uint8_t color, prev_color; 24 | uint16_t fade; 25 | uint32_t change_time = frame_time / cycle_time_ms; 26 | uint32_t rem = frame_time % cycle_time_ms; 27 | 28 | if (change_time != prev_change_time) //new color 29 | { 30 | prev_color = color; 31 | color = new_random_value8(prev_color); 32 | prev_change_time = change_time; 33 | } 34 | 35 | if (this->fadedur) { 36 | fade = (rem << 2) / (this->fadedur); 37 | if (fade > 255) fade = 255; 38 | } else { 39 | fade = 255; 40 | } 41 | fill_solid(pixel_data, num_pixels, blend(this->getColor(prev_color), this->getColor(color), fade)); 42 | } 43 | protected: 44 | uint8_t cycle_time, fadedur; // Cycle time and fade duration in 16th of a second 45 | }; 46 | 47 | // SCROLLING & WAVE PATTERNS 48 | 49 | // Pride2015 50 | // Animated, ever-changing rainbows. 51 | // by Mark Kriegsman. https://github.com/FastLED/FastLED/blob/master/examples/Pride2015/Pride2015.ino 52 | // Recommend setting resolution equal to or close to number of leds in strip segment 53 | class PridePattern: public LinearPattern { 54 | public: 55 | PridePattern(uint8_t speed_factor=4): 56 | LinearPattern(), 57 | speed_factor(speed_factor) {} 58 | 59 | void frameAction(CRGB* pixel_data, uint16_t num_pixels, uint32_t frame_time) override { 60 | static uint32_t sPseudotime = 0; // pseudo-time elapsed since pattern start 61 | static uint32_t sLastMillis = 0; // actual time of last frame 62 | static uint16_t sHue16 = 0; // Hue offset with 16-bit resolution 63 | // beatsin88 is used to get more granular low BPMs 64 | // Vary saturation slightly over time 65 | uint8_t sat8 = beatsin88( 87*this->speed_factor, 220, 250); 66 | // varies proportion of brightness which is determined by varying sine wave, vs constant 67 | uint8_t brightdepth = beatsin88( 341*this->speed_factor, 96, 224); 68 | // Vary brightness increment over time (measure of wavelength) 69 | uint16_t brightnessthetainc16 = beatsin88( 203*this->speed_factor, (25 * 256), (40 * 256)); 70 | // varying time multiplyer, for varying rate of change of hue and brightness 71 | uint8_t msmultiplier = beatsin88(240*this->speed_factor, 40, 240); 72 | 73 | uint16_t hue16 = sHue16;//gHue * 256; 74 | // Vary hue increment over time (measure of rainbow colour gradient) 75 | uint16_t hueinc16 = beatsin88(113*this->speed_factor, 1, 3000); 76 | 77 | uint16_t deltams = frame_time - sLastMillis ; // Time since last frame 78 | sLastMillis = frame_time; 79 | sPseudotime += deltams * msmultiplier; 80 | // Increase hue offset by varying amount 81 | sHue16 += deltams * beatsin88( 400, 5,9); 82 | // wave offset 83 | uint16_t brightnesstheta16 = sPseudotime; 84 | 85 | for (uint16_t i = 0 ; i < num_pixels; i++) { 86 | hue16 += hueinc16; 87 | uint8_t hue8 = hue16 / 256; 88 | 89 | brightnesstheta16 += brightnessthetainc16; 90 | uint16_t b16 = sin16( brightnesstheta16 ) + 32768; 91 | // Perform squared scaling to make sine wave sharper 92 | uint16_t bri16 = (uint32_t)((uint32_t)b16 * (uint32_t)b16) / 65536; 93 | // Scale value from 0-65536 to 0-brightdepth 94 | uint8_t bri8 = (uint32_t)(((uint32_t)bri16) * brightdepth) / 65536; 95 | // Scale to 0-255 (add constant amount) 96 | bri8 += (255 - brightdepth); 97 | 98 | CRGB newcolor = CHSV( hue8, sat8, bri8); 99 | 100 | nblend(pixel_data[i], newcolor, 64); 101 | } 102 | } 103 | protected: 104 | const uint8_t speed_factor; // Factor to increase rate of change of pattern parameters 105 | }; 106 | 107 | //Moing sine wave with randomised speed, duration and rainbow colour offset, and changes direction 108 | // Benefits from using higher resolution than segment length 109 | class RandomRainbowsPattern: public LinearPattern { 110 | public: 111 | RandomRainbowsPattern(): LinearPattern() {} 112 | 113 | void reset() override{ 114 | LinearPattern::reset(); 115 | this->pos = 0; 116 | this->randomize_state(); 117 | } 118 | 119 | void randomize_state() { 120 | this->speed=random(1,6); 121 | this->scale_factor = random(1,4); 122 | if (random(2) == 1) { 123 | this->direction=!this->direction; 124 | } 125 | this->randomize_time = random(10, 200); 126 | this->colour_offset = random(0,255); 127 | this->dim = random(0, 7) == 6; 128 | } 129 | 130 | void frameAction(CRGB* pixel_data, uint16_t num_pixels, uint32_t frame_time) override { 131 | if (randomize_time) { 132 | randomize_time--; 133 | } else { 134 | this->randomize_state(); 135 | } 136 | if (direction) { 137 | this->pos = (this->pos + this->speed) % num_pixels; 138 | } else { 139 | this->pos = wrap_subtract(this->pos, this->speed, num_pixels); 140 | } 141 | for (uint16_t i = 0; i < num_pixels; i++) { 142 | pixel_data[i] = this->get_pixel_value(num_pixels, i); 143 | } 144 | 145 | } 146 | 147 | CRGB get_pixel_value(uint16_t num_pixels, uint16_t i) { 148 | uint8_t virtual_pos = (255*(i + this->pos))/(num_pixels); 149 | uint8_t val = cubicwave8(uint16_t(virtual_pos*this->scale_factor) & 0xFF); 150 | return this->getColor((val+this->colour_offset) & 0xFF, this->dim ? val>>1 : val); 151 | } 152 | 153 | protected: 154 | uint8_t speed; 155 | bool direction=false; 156 | uint8_t pos; // Position from 0 to resolution 157 | uint8_t colour_offset; 158 | uint16_t randomize_time; // How long until state is randomized again 159 | uint8_t scale_factor; 160 | bool dim; //Whether to make pattern very dim (can look cool) 161 | }; 162 | 163 | // Extends head to end of strip then retracts tail 164 | class GrowThenShrinkPattern : public LinearPattern { 165 | public: 166 | GrowThenShrinkPattern(const ColorPicker& color_picker=Basic_picker): 167 | LinearPattern(color_picker) {} 168 | 169 | void reset() override { 170 | LinearPattern::reset(); 171 | this->head_pos = this->tail_pos = 0; 172 | this->reverse = false; 173 | } 174 | 175 | void frameAction(CRGB* pixel_data, uint16_t num_pixels, uint32_t frame_time) override { 176 | // Update head and tail positions 177 | if (this->reverse) { 178 | if (this->tail_pos > 0) { 179 | // Extend along light strip 180 | this->tail_pos--; 181 | } else if (this->head_pos > 0) { 182 | // Retract tail 183 | this->head_pos--; 184 | } else { 185 | // Reverse 186 | this->reverse = false; 187 | } 188 | } else { 189 | if (this->head_pos < num_pixels-1) { 190 | // Extend along light strip 191 | this->head_pos++; 192 | } else if (this->tail_pos < this->head_pos) { 193 | // Retract tail 194 | this->tail_pos++; 195 | } else { 196 | // Reverse 197 | this->reverse = true; 198 | } 199 | } 200 | 201 | // Set pixel data 202 | for (uint16_t i=0; itail_pos <= i) && (i <= this->head_pos)) { 204 | pixel_data[i] = this->getColor((i*255)/num_pixels); 205 | } else { 206 | pixel_data[i] = CRGB::Black; 207 | } 208 | } 209 | } 210 | 211 | protected: 212 | uint16_t head_pos, tail_pos; 213 | bool reverse; 214 | }; 215 | 216 | 217 | // DYNAMIC MOVEMENT & ACTIVE PATTERNS 218 | 219 | // Simple moving pulse of light along axis. Pulse has a bright head with a tapering tail 220 | class MovingPulsePattern: public LinearPattern { 221 | public: 222 | MovingPulsePattern( 223 | uint8_t pulse_len=3, // Length of pulse 224 | const ColorPicker& color_picker=Basic_picker): 225 | LinearPattern(color_picker), 226 | head_pos(0), 227 | pulse_len(pulse_len), 228 | tail_interpolator(Interpolator(0, 255, pulse_len + 1, 0)) {} 229 | 230 | // Update pulse position (on virtual axis) 231 | void frameAction(CRGB* pixel_data, uint16_t num_pixels, uint32_t frame_time) override { 232 | this->head_pos = (this->head_pos + 1) % num_pixels; 233 | for (uint16_t i=0; iget_pixel_value(num_pixels, i); 235 | } 236 | } 237 | 238 | // Construct pulse from head position 239 | // i is from 0 -> resolution 240 | CRGB get_pixel_value(uint16_t num_pixels, uint16_t i) { 241 | // Figure out distance behind pulse head to get brightness 242 | int distance_behind_head = this->head_pos - i; 243 | // Case of when position is in front of pulse head (after head has looped around to start), so distance_behind_head is negative 244 | if (distance_behind_head < 0) { 245 | distance_behind_head = num_pixels + distance_behind_head; 246 | } 247 | // If not within pulse width, return black 248 | if (distance_behind_head > this->pulse_len) { 249 | return CRGB::Black; 250 | } 251 | // Use interpolator to get brightness 252 | uint8_t lum = tail_interpolator.get_value(distance_behind_head); 253 | uint8_t hue = (i*255) / num_pixels; // Change colour along axis 254 | return this->getColor(hue, lum); 255 | } 256 | 257 | private: 258 | 259 | uint16_t head_pos; // Position of head of pulse 260 | uint8_t pulse_len; // Length of pulse 261 | Interpolator tail_interpolator; // Linear Interpolator for pulse tail brightness 262 | 263 | }; 264 | 265 | //https://gist.github.com/kriegsman/626dca2f9d2189bd82ca 266 | // *Flashing* rainbow lights that zoom back and forth to a beat. 267 | class DiscoStrobePattern : public LinearPattern { 268 | public: 269 | DiscoStrobePattern( 270 | const ColorPicker& color_picker=HalloweenColors_picker): 271 | LinearPattern(color_picker) {} 272 | 273 | void frameAction(CRGB* pixel_data, uint16_t num_pixels, uint32_t frame_time) override { 274 | // First, we black out all the LEDs 275 | fill_solid(pixel_data, num_pixels, CRGB::Black); 276 | 277 | // To achive the strobe effect, we actually only draw lit pixels 278 | // every Nth frame (e.g. every 4th frame). 279 | // sStrobePhase is a counter that runs from zero to kStrobeCycleLength-1, 280 | // and then resets to zero. 281 | const uint8_t kStrobeCycleLength = 4; // light every Nth frame 282 | static uint8_t sStrobePhase = 0; 283 | sStrobePhase = sStrobePhase + 1; 284 | if( sStrobePhase >= kStrobeCycleLength ) { 285 | sStrobePhase = 0; 286 | } 287 | 288 | // We only draw lit pixels when we're in strobe phase zero; 289 | // in all the other phases we leave the LEDs all black. 290 | if( sStrobePhase == 0 ) { 291 | // The dash spacing cycles from 4 to 9 and back, 8x/min (about every 7.5 sec) 292 | uint8_t dashperiod= beatsin8( 8/*cycles per minute*/, 4,10); 293 | // The width of the dashes is a fraction of the dashperiod, with a minimum of one pixel 294 | uint8_t dashwidth = (dashperiod / 4) + 1; 295 | 296 | // The distance that the dashes move each cycles varies 297 | // between 1 pixel/cycle and half-the-dashperiod/cycle. 298 | // At the maximum speed, it's impossible to visually distinguish 299 | // whether the dashes are moving left or right, and the code takes 300 | // advantage of that moment to reverse the direction of the dashes. 301 | // So it looks like they're speeding up faster and faster to the 302 | // right, and then they start slowing down, but as they do it becomes 303 | // visible that they're no longer moving right; they've been 304 | // moving left. Easier to see than t o explain. 305 | // 306 | // The dashes zoom back and forth at a speed that 'goes well' with 307 | // most dance music, a little faster than 120 Beats Per Minute. You 308 | // can adjust this for faster or slower 'zooming' back and forth. 309 | int8_t dashmotionspeed = beatsin8( (bpm /2), 1,dashperiod); 310 | // This is where we reverse the direction under cover of high speed 311 | // visual aliasing. 312 | if( dashmotionspeed >= (dashperiod/2)) { 313 | dashmotionspeed = 0 - (dashperiod - dashmotionspeed ); 314 | } 315 | 316 | // The hueShift controls how much the hue of each dash varies from 317 | // the adjacent dash. If hueShift is zero, all the dashes are the 318 | // same color. If hueShift is 128, alterating dashes will be two 319 | // different colors. And if hueShift is range of 10..40, the 320 | // dashes will make rainbows. 321 | // Initially, I just had hueShift cycle from 0..130 using beatsin8. 322 | // It looked great with very low values, and with high values, but 323 | // a bit 'busy' in the middle, which I didnt like. 324 | // uint8_t hueShift = beatsin8(2,0,130); 325 | // 326 | // So instead I layered in a bunch of 'cubic easings' 327 | // (see http://easings.net/#easeInOutCubic ) 328 | // so that the resultant wave cycle spends a great deal of time 329 | // "at the bottom" (solid color dashes), and at the top ("two 330 | // color stripes"), and makes quick transitions between them. 331 | uint8_t cycle = beat8(2); // two cycles per minute 332 | uint8_t easedcycle = ease8InOutCubic( ease8InOutCubic( cycle)); 333 | uint8_t wavecycle = cubicwave8( easedcycle); 334 | uint8_t hueShift = scale8( wavecycle,130); 335 | 336 | 337 | // Each frame of the animation can be repeated multiple times. 338 | // This slows down the apparent motion, and gives a more static 339 | // strobe effect. After experimentation, I set the default to 1. 340 | uint8_t strobesPerPosition = 1; // try 1..4 341 | 342 | 343 | // Now that all the parameters for this frame are calculated, 344 | // we call the 'worker' function that does the next part of the work. 345 | this->discoWorker(pixel_data, num_pixels, dashperiod, dashwidth, dashmotionspeed, strobesPerPosition, hueShift); 346 | } 347 | 348 | } 349 | protected: 350 | // discoWorker updates the positions of the dashes, and calls the draw function 351 | void discoWorker( 352 | CRGB* pixel_data, uint16_t num_pixels, 353 | uint8_t dashperiod, uint8_t dashwidth, int8_t dashmotionspeed, 354 | uint8_t stroberepeats, 355 | uint8_t huedelta) 356 | { 357 | static uint8_t sRepeatCounter = 0; 358 | static int8_t sStartPosition = 0; 359 | static uint8_t sStartHue = 0; 360 | 361 | // Always keep the hue shifting a little 362 | sStartHue += 1; 363 | 364 | // Increment the strobe repeat counter, and 365 | // move the dash starting position when needed. 366 | sRepeatCounter = sRepeatCounter + 1; 367 | if( sRepeatCounter>= stroberepeats) { 368 | sRepeatCounter = 0; 369 | 370 | sStartPosition = sStartPosition + dashmotionspeed; 371 | 372 | // These adjustments take care of making sure that the 373 | // starting hue is adjusted to keep the apparent color of 374 | // each dash the same, even when the state position wraps around. 375 | if( sStartPosition >= dashperiod ) { 376 | while( sStartPosition >= dashperiod) { sStartPosition -= dashperiod; } 377 | sStartHue -= huedelta; 378 | } else if( sStartPosition < 0) { 379 | while( sStartPosition < 0) { sStartPosition += dashperiod; } 380 | sStartHue += huedelta; 381 | } 382 | } 383 | 384 | // draw dashes with full brightness (value), and somewhat 385 | // desaturated (whitened) so that the LEDs actually throw more light. 386 | const uint8_t kSaturation = 208; 387 | const uint8_t kValue = 255; 388 | 389 | // call the function that actually just draws the dashes now 390 | this->drawRainbowDashes(pixel_data, num_pixels, 391 | sStartPosition, dashperiod, dashwidth, 392 | sStartHue, huedelta, kSaturation, kValue); 393 | } 394 | // drawRainbowDashes - draw rainbow-colored 'dashes' of light along the led strip: 395 | // starting from 'startpos', up to and including 'lastpos' 396 | // with a given 'period' and 'width' 397 | // starting from a given hue, which changes for each successive dash by a 'huedelta' 398 | // at a given saturation and value. 399 | // 400 | // period = 5, width = 2 would be _ _ _ X X _ _ _ Y Y _ _ _ Z Z _ _ _ A A _ _ _ 401 | // \-------/ \-/ 402 | // period 5 width 2 403 | // 404 | void drawRainbowDashes( 405 | CRGB* pixel_data, uint16_t num_pixels, 406 | uint8_t startpos, uint8_t period, uint8_t width, 407 | uint8_t huestart, uint8_t huedelta, uint8_t saturation, uint8_t value) 408 | { 409 | uint8_t hue = huestart; 410 | for( uint16_t i = startpos; i <= num_pixels-1; i += period) { 411 | // Switched from HSV color wheel to color palette 412 | // Was: CRGB color = CHSV( hue, saturation, value); 413 | CRGB color = this->getColor(hue, value); 414 | 415 | // draw one dash 416 | uint16_t pos = i; 417 | for( uint8_t w = 0; w < width; w++) { 418 | pixel_data[pos] = color; 419 | pos++; 420 | if( pos >= num_pixels) { 421 | break; 422 | } 423 | } 424 | 425 | hue += huedelta; 426 | } 427 | } 428 | uint8_t bpm=61; 429 | }; 430 | 431 | // Pulse which jumps to random position on segment and flashes 432 | class SkippingSpikePattern: public LinearPattern { 433 | public: 434 | SkippingSpikePattern( 435 | uint8_t max_pulse_width, 436 | uint8_t pulse_speed=1, 437 | const ColorPicker& color_picker=RainbowColors_picker): 438 | LinearPattern(color_picker), 439 | max_pulse_width(max_pulse_width), 440 | pulse_speed(pulse_speed) {} 441 | 442 | void reset() override { 443 | LinearPattern::reset(); 444 | this->pulse_pos = this->max_pulse_width; 445 | this->ramp = 0; 446 | this->ramp_up= true; 447 | } 448 | 449 | void frameAction(CRGB* pixel_data, uint16_t num_pixels, uint32_t frame_time) override { 450 | if (this->ramp_up) { // Pulse expanding 451 | if (this->max_pulse_width-this->ramp <= this->pulse_speed) { // Reached top of pulse 452 | this->ramp_up = false; 453 | } else { 454 | this->ramp += this->pulse_speed; // Increase pulse brightness 455 | } 456 | } else { // Pulse contracting 457 | if (this->ramp <= this->pulse_speed) { // End of pulse 458 | // Move pulse position 459 | this->pulse_pos = (this->max_pulse_width/4) + random(0, num_pixels - this->max_pulse_width/4); 460 | /* 461 | if ((this->resolution - pulse_pos) <= pulse_offset) { 462 | // Loop over position back to start 463 | this->pulse_pos = pulse_offset/2; 464 | } else { 465 | this->pulse_pos += pulse_offset; 466 | } 467 | */ 468 | this->ramp_up = true; // Begin next pulse 469 | } else { 470 | this->ramp -= this->pulse_speed; 471 | } 472 | } 473 | 474 | // Fill pixel array 475 | for (uint16_t i=0; i < num_pixels; i++) { 476 | // Get distance of pixel from pulse_pos 477 | uint8_t diff = i >= this->pulse_pos ? i - this->pulse_pos : this->pulse_pos - i; 478 | if (diff > this->ramp) { 479 | pixel_data[i] = CRGB::Black; 480 | } else { 481 | uint8_t lum = (255 - (diff*255)/this->ramp); 482 | pixel_data[i] = this->getColor(255-lum, lum); 483 | } 484 | } 485 | } 486 | 487 | protected: 488 | const uint8_t max_pulse_width, pulse_speed; 489 | uint16_t pulse_pos; //Position of current pulse 490 | uint8_t ramp; 491 | bool ramp_up; 492 | }; 493 | 494 | 495 | // OTHER PATTERNS 496 | 497 | // This function takes a pixel, and if its in the 'fading down' 498 | // part of the cycle, it adjusts the color a little bit like the 499 | // way that incandescent bulbs fade toward 'red' as they dim. 500 | void coolLikeIncandescent( CRGB& c, uint8_t phase) 501 | { 502 | if ( phase < 128) return; 503 | 504 | uint8_t cooling = (phase - 128) >> 4; 505 | c.g = qsub8( c.g, cooling); 506 | c.b = qsub8( c.b, cooling * 2); 507 | } 508 | 509 | // Adapted from pattern by Mark Kriegsman 510 | // https://gist.github.com/kriegsman/756ea6dcae8e30845b5a 511 | // The idea behind this (new) implementation is that there's one 512 | // basic, repeating pattern that each pixel follows like a waveform: 513 | // The brightness rises from 0..255 and then falls back down to 0. 514 | // The brightness at any given point in time can be determined as 515 | // as a function of time, for example: 516 | // brightness = sine( time ); // a sine wave of brightness over time 517 | // 518 | // So the way this implementation works is that every pixel follows 519 | // the exact same wave function over time. In this particular case, 520 | // I chose a sawtooth triangle wave (triwave8) rather than a sine wave, 521 | // but the idea is the same: brightness = triwave8( time ). 522 | // Works well when resolution is equal to segment length 523 | class TwinklePattern : public LinearPattern { 524 | public: 525 | TwinklePattern( 526 | uint8_t twinkle_speed = 6, 527 | uint8_t twinkle_density = 4, 528 | const ColorPicker& color_picker = FairyLight_picker, 529 | CRGB bg = CRGB::Black): 530 | LinearPattern(color_picker), 531 | bg(bg), 532 | bg_brightness(bg.getAverageLight()), 533 | twinkle_speed(twinkle_speed), 534 | twinkle_density(twinkle_density) {} 535 | 536 | void frameAction(CRGB* pixel_data, uint16_t num_pixels, uint32_t frame_time) override { 537 | // "this->PRNG16" is the pseudorandom number generator 538 | this->PRNG16 = 11337; 539 | // Set pixel data 540 | for (uint16_t i=0; iget_pixel_value(frame_time, i); 542 | } 543 | } 544 | 545 | CRGB get_pixel_value(uint16_t frame_time, uint16_t i) { 546 | CRGB pixel; 547 | this->PRNG16 = (uint16_t)(this->PRNG16 * 2053) + 1384; // next 'random' number 548 | uint16_t myclockoffset16 = this->PRNG16; // use that number as clock offset 549 | this->PRNG16 = (uint16_t)(this->PRNG16 * 2053) + 1384; // next 'random' number 550 | // use that number as clock speed adjustment factor (in 8ths, from 8/8ths to 23/8ths) 551 | uint8_t myspeedmultiplierQ5_3 = ((((this->PRNG16 & 0xFF) >> 4) + (this->PRNG16 & 0x0F)) & 0x0F) + 0x08; 552 | uint32_t myclock30 = (uint32_t)((frame_time * myspeedmultiplierQ5_3) >> 3) + myclockoffset16; 553 | uint8_t myunique8 = this->PRNG16 >> 8; // get 'salt' value for this pixel 554 | 555 | // We now have the adjusted 'clock' for this pixel, now we call 556 | // the function that computes what color the pixel should be based 557 | // on the "brightness = f( time )" idea. 558 | CRGB c = computeOneTwinkle( myclock30, myunique8); 559 | uint8_t cbright = c.getAverageLight(); 560 | int16_t deltabright = cbright - bg_brightness; 561 | if ( deltabright >= 32 || (!bg)) { 562 | // If the new pixel is significantly brighter than the background color, 563 | // use the new color. 564 | pixel = c; 565 | } else if ( deltabright > 0 ) { 566 | // If the new pixel is just slightly brighter than the background color, 567 | // mix a blend of the new color and the background color 568 | pixel = blend( bg, c, deltabright * 8); 569 | } else { 570 | // if the new pixel is not at all brighter than the background color, 571 | // just use the background color. 572 | pixel = bg; 573 | } 574 | return pixel; 575 | } 576 | protected: 577 | CRGB computeOneTwinkle( uint32_t ms, uint8_t salt) { 578 | uint16_t ticks = ms >> (8 - twinkle_speed); 579 | uint8_t fastcycle8 = ticks; 580 | uint16_t slowcycle16 = (ticks >> 8) + salt; 581 | slowcycle16 += sin8( slowcycle16); 582 | slowcycle16 = (slowcycle16 * 2053) + 1384; 583 | uint8_t slowcycle8 = (slowcycle16 & 0xFF) + (slowcycle16 >> 8); 584 | 585 | uint8_t bright = 0; 586 | if ( ((slowcycle8 & 0x0E) / 2) < twinkle_density) { 587 | bright = attackDecayWave8( fastcycle8); 588 | } 589 | 590 | uint8_t hue = slowcycle8 - salt; 591 | CRGB c; 592 | if ( bright > 0) { 593 | c = this->getColor(hue, bright); 594 | coolLikeIncandescent( c, fastcycle8); 595 | } else { 596 | c = CRGB::Black; 597 | } 598 | return c; 599 | } 600 | 601 | // Background colour 602 | CRGB bg; 603 | uint8_t bg_brightness; 604 | 605 | uint8_t twinkle_speed; // 0-8 606 | uint8_t twinkle_density; // 0-8 607 | uint16_t PRNG16; 608 | }; 609 | 610 | 611 | class SparkleFillPattern : public LinearPattern { 612 | public: 613 | SparkleFillPattern(const ColorPicker& color_picker=Basic_picker): 614 | LinearPattern(color_picker) {} 615 | 616 | void reset() { 617 | LinearPattern::reset(); 618 | this->fill = true; 619 | this->pixels_changed = 0; 620 | } 621 | 622 | void frameAction(CRGB* pixel_data, uint16_t num_pixels, uint32_t frame_time) override { 623 | uint16_t remaining = num_pixels - this->pixels_changed; 624 | for (uint16_t i=0; i < num_pixels; i++) { 625 | uint8_t brightness = pixel_data[i].getAverageLight(); 626 | // Probabilty to fill/un-fill the pixel is inversely proportional to amount remaining 627 | if (random(0,remaining) == 0) { 628 | // Fill with random palette value, or unfill 629 | if (brightness) { 630 | // Brighten existing pixel if filling 631 | if (this->fill) { 632 | if (brightness < 200) { 633 | pixel_data[i]*=2; 634 | } 635 | } else { 636 | // reduce brightness or set to black if un-filling 637 | if (brightness > 32) { 638 | pixel_data[i]/= 2; 639 | } else { 640 | pixel_data[i] = CRGB::Black; 641 | this->pixels_changed++; 642 | } 643 | } 644 | } else if (this->fill) { 645 | // Fill with new colour 646 | pixel_data[i] = this->getColor(random(0,256), 32); 647 | this->pixels_changed++; 648 | } 649 | 650 | if (this->pixels_changed == num_pixels) { 651 | this->pixels_changed = 0; 652 | this->fill = !this->fill; 653 | } 654 | } 655 | } 656 | 657 | } 658 | 659 | protected: 660 | bool fill=true; // Whether pattern is in fill mode (True) or un-fill 661 | uint16_t pixels_changed=0; // Number of remaining pixels to fill/un-fill 662 | 663 | }; 664 | 665 | //https://github.com/FastLED/FastLED/blob/master/examples/Fire2012WithPalette/Fire2012WithPalette.ino 666 | template 667 | class FirePattern: public LinearPattern { 668 | public: 669 | FirePattern( 670 | uint8_t cooling=60, // Less cooling = taller flames. More cooling = shorter flames. Default 60, suggested range 20-100 671 | uint8_t sparking=100, // Higher chance = more roaring fire. Lower chance = more flickery fire. Default 100, suggested range 50-200. 672 | const ColorPicker& color_picker=HeatColors_picker): 673 | LinearPattern(color_picker), 674 | cooling(cooling), 675 | sparking(sparking) 676 | {}; 677 | 678 | void reset() override { 679 | LinearPattern::reset(); 680 | // Reset heat array to 0 681 | for (uint8_t i=0; iheat[i] = 0; 683 | } 684 | } 685 | 686 | void frameAction(CRGB* pixel_data, uint16_t num_pixels, uint32_t frame_time) override { 687 | random16_add_entropy(random()); 688 | // Step 1. Cool down every cell a little 689 | for(uint8_t i = 0; i < num_pixels; i++) { 690 | this->heat[i] = qsub8( this->heat[i], random8(0, ((this->cooling * 10) / num_pixels) + 2)); 691 | } 692 | 693 | // Step 2. Heat from each cell drifts 'up' and diffuses a little 694 | for(uint8_t k= num_pixels - 1; k >= 2; k--) { 695 | this->heat[k] = (this->heat[k - 1] + 2*this->heat[k - 2]) / 3; 696 | } 697 | 698 | // Step 3. Randomly ignite new 'sparks' of heat near the bottom 699 | if(random8() < this->sparking ) { 700 | uint8_t y = random8(num_pixels/5 + 1); 701 | this->heat[y] = qadd8( this->heat[y], random8(160,220) ); 702 | } 703 | 704 | // Fill pixel array 705 | for (uint16_t i=0; i < num_pixels; i++) { 706 | // Get heat value, Scale from 0-255 down to 0-240, select colour from palette 707 | uint8_t colorindex = scale8(this->heat[i], 240); 708 | // Constrain base heat (so base of fire doesnt look too bright 709 | if (i < (num_pixels/10) + 1) { 710 | colorindex = constrain(colorindex, 40, 120); 711 | } 712 | pixel_data[i] = this->getColor(colorindex); 713 | } 714 | } 715 | 716 | protected: 717 | uint8_t heat[t_resolution]; // Array to store heat values 718 | const uint8_t cooling, sparking; 719 | 720 | }; 721 | 722 | --------------------------------------------------------------------------------