From f0d36fd76942312577d9d164d33720e6a1e89a27 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Sun, 8 May 2022 10:50:48 +0200 Subject: [PATCH] WLED 2D matrix support. - Added 2 sample effects to UI. - 2D segment details. - 1D effect upscaling to 2D. - 2D setPixelColorXY() and other utility functions - Arbitrary panel position & oritentation. - 2D settings with physical to logical mapping. - Bump version. --- package-lock.json | 2 +- package.json | 2 +- tools/cdata.js | 12 + wled00/FX.cpp | 71 +- wled00/FX.h | 150 +- wled00/FX_2Dfcn.cpp | 349 ++++ wled00/FX_fcn.cpp | 66 +- wled00/cfg.cpp | 57 + wled00/const.h | 3 + wled00/data/index.css | 9 + wled00/data/index.js | 89 +- wled00/data/settings.htm | 24 +- wled00/data/settings_2D.htm | 126 ++ wled00/html_settings.h | 185 +- wled00/html_ui.h | 3463 ++++++++++++++++++----------------- wled00/json.cpp | 29 +- wled00/set.cpp | 28 +- wled00/wled_server.cpp | 5 +- wled00/xml.cpp | 31 +- 19 files changed, 2836 insertions(+), 1865 deletions(-) create mode 100644 wled00/FX_2Dfcn.cpp create mode 100644 wled00/data/settings_2D.htm diff --git a/package-lock.json b/package-lock.json index 7689f806..3c8993e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "wled", - "version": "0.13.2-bl0", + "version": "0.14.0-bl0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index fb09e820..139ef1a1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wled", - "version": "0.13.2-bl0", + "version": "0.14.0-bl0", "description": "Tools for WLED project", "main": "tools/cdata.js", "directories": { diff --git a/tools/cdata.js b/tools/cdata.js index af879672..c40994ab 100644 --- a/tools/cdata.js +++ b/tools/cdata.js @@ -366,6 +366,18 @@ writeChunks( "" ) }, + { + file: "settings_2D.htm", + name: "PAGE_settings_2D", + method: "gzip", + filter: "html-minify", + mangle: (str) => + str + .replace( + /function GetV().*\<\/script\>/gms, + "" + ) + }, { file: "settings_pin.htm", name: "PAGE_settings_pin", diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 273541a0..9cbea814 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -2346,7 +2346,7 @@ uint16_t WS2812FX::mode_ripple_rainbow(void) { // incandescent bulbs change color as they get dim down. #define COOL_LIKE_INCANDESCENT 1 -CRGB IRAM_ATTR WS2812FX::twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool cat) +CRGB WS2812FX::twinklefox_one_twinkle(uint32_t ms, uint8_t salt, bool cat) { // Overall twinkle speed (changed) uint16_t ticks = ms / SEGENV.aux0; @@ -4226,4 +4226,71 @@ uint16_t WS2812FX::mode_aurora(void) { } return FRAMETIME; -} \ No newline at end of file +} + +/////////////////////////////////////////////////////////////////////////////// +//*************************** 2D routines *********************************** + +// sample 2D routine +uint16_t WS2812FX::mode_2DBlackHole_A() { // By: Stepko https://editor.soulmatelights.com/gallery/1012 , Modified by: Andrew Tuline + uint16_t w = SEGMENT.virtualWidth(); + uint16_t h = SEGMENT.virtualHeight(); + uint16_t dataSize = sizeof(CRGB) * w * h; + + if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + CRGB *leds = reinterpret_cast(SEGENV.data); + + uint16_t x, y; + + // initialize on first call + if (SEGENV.call == 0) { + for (y = 0; y < h; y++) for (x = 0; x < w; x++) { + leds[x + y * w] = CRGB::Black; + } + } + + fadeToBlackBy(32, leds); // create fading trails + float t = (float)(millis())/128; + for (byte i = 0; i < 8; i++) { + x = beatsin8(SEGMENT.c1x>>3, 0, w - 1, 0, ((i % 2) ? 128 : 0) + t * i); + y = beatsin8(10 , 0, h - 1, 0, ((i % 2) ? 192 : 64) + t * i); + leds[x + y * w] += CHSV(i*32, 255, 255); + } + for (byte i = 0; i < 8; i++) { + x = beatsin8(SEGMENT.c2x>>3, w/4, w - 1 - w/4, 0, ((i % 2) ? 128 : 0) + t * i); + y = beatsin8(SEGMENT.c3x>>3, h/4, h - 1 - h/4, 0, ((i % 2) ? 192 : 64) + t * i); + leds[x + y * w] += CHSV(i*32, 255, 255); + } + leds[w/2 * (1 + h)] = CHSV(0,0,255); + blur2d(16, leds); + + for (y = 0; y < h; y++) for (x = 0; x < w; x++) { + uint16_t o = x + y * w; + setPixelColorXY(x, y, leds[o]); + } + return FRAMETIME; +} // mode_2DBlackHole() + +// same as above not using leds[] +uint16_t WS2812FX::mode_2DBlackHole_B() { // By: Stepko https://editor.soulmatelights.com/gallery/1012 , Modified by: Andrew Tuline + uint16_t w = SEGMENT.virtualWidth(); + uint16_t h = SEGMENT.virtualHeight(); + uint16_t x, y; + + fade_out2D(32); // create fading trails + float t = (float)(millis())/128; + for (byte i = 0; i < 8; i++) { + x = beatsin8(SEGMENT.c1x>>3, 0, w - 1, 0, ((i % 2) ? 128 : 0) + t * i); + y = beatsin8(10 , 0, h - 1, 0, ((i % 2) ? 192 : 64) + t * i); + setPixelColorXY(x, y, col_to_crgb(getPixelColorXY(x,y)) + CHSV(i*32, 255, 255)); + } + for (byte i = 0; i < 8; i++) { + x = beatsin8(SEGMENT.c2x>>3, w/4, w - 1 - w/4, 0, ((i % 2) ? 128 : 0) + t * i); + y = beatsin8(SEGMENT.c3x>>3, h/4, h - 1 - h/4, 0, ((i % 2) ? 192 : 64) + t * i); + setPixelColorXY(x, y, col_to_crgb(getPixelColorXY(x,y)) + CHSV(i*32, 255, 255)); + } + setPixelColorXY(w/2, h/2, WHITE); + blur2d(16); + return FRAMETIME; +} // mode_2DBlackHole() + diff --git a/wled00/FX.h b/wled00/FX.h index 1cfa366b..3fb8fa76 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -38,6 +38,9 @@ #define DEFAULT_SPEED (uint8_t)128 #define DEFAULT_INTENSITY (uint8_t)128 #define DEFAULT_COLOR (uint32_t)0xFFAA00 +#define DEFAULT_C1 (uint8_t)128 +#define DEFAULT_C2 (uint8_t)128 +#define DEFAULT_C3 (uint8_t)128 #ifndef MIN #define MIN(a,b) ((a)<(b)?(a):(b)) @@ -109,6 +112,9 @@ // bit 0: segment is selected #define NO_OPTIONS (uint8_t)0x00 #define TRANSITIONAL (uint8_t)0x80 +#define TRANSPOSED (uint8_t)0x40 // rotated 90deg & reversed +#define REVERSE_Y_2D (uint8_t)0x20 +#define MIRROR_Y_2D (uint8_t)0x10 #define MIRROR (uint8_t)0x08 #define SEGMENT_ON (uint8_t)0x04 #define REVERSE (uint8_t)0x02 @@ -118,8 +124,11 @@ #define IS_SEGMENT_ON ((SEGMENT.options & SEGMENT_ON ) == SEGMENT_ON ) #define IS_REVERSE ((SEGMENT.options & REVERSE ) == REVERSE ) #define IS_SELECTED ((SEGMENT.options & SELECTED ) == SELECTED ) +#define IS_REVERSE_Y_2D ((SEGMENT.options & REVERSE_Y_2D) == REVERSE_Y_2D) +#define IS_MIRROR_Y_2D ((SEGMENT.options & MIRROR_Y_2D ) == MIRROR_Y_2D ) +#define IS_TRANSPOSED ((SEGMENT.options & TRANSPOSED ) == TRANSPOSED ) -#define MODE_COUNT 118 +#define MODE_COUNT 120 #define FX_MODE_STATIC 0 #define FX_MODE_BLINK 1 @@ -239,6 +248,8 @@ #define FX_MODE_BLENDS 115 #define FX_MODE_TV_SIMULATOR 116 #define FX_MODE_DYNAMIC_SMOOTH 117 +#define FX_MODE_BLACK_HOLE_A 118 +#define FX_MODE_BLACK_HOLE_B 119 class WS2812FX { @@ -249,24 +260,35 @@ class WS2812FX { static WS2812FX* instance; - // segment parameters public: - typedef struct Segment { // 34 (35 in memory) bytes - uint16_t start; - uint16_t stop; //segment invalid if stop == 0 + + // segment parameters + typedef struct Segment { // 35 (36 in memory) bytes + uint16_t start; // start index / start X coordinate 2D (left) + uint16_t stop; // stop index / stop X coordinate 2D (right); segment is invalid if stop == 0 uint16_t offset; uint8_t speed; uint8_t intensity; uint8_t palette; uint8_t mode; - uint8_t options; //bit pattern: msb first: transitional needspixelstate tbd tbd (paused) on reverse selected + uint16_t options; //bit pattern: msb first: [transposed mirrorY reverseY] transitional (tbd) paused needspixelstate mirrored on reverse selected uint8_t grouping, spacing; uint8_t opacity; uint32_t colors[NUM_COLORS]; uint8_t cct; //0==1900K, 255==10091K uint8_t _capabilities; - uint8_t c1x, c2x, c3x; // custom FX parameters + uint8_t c1x, c2x, c3x; // custom FX parameters + uint16_t startY; // start Y coodrinate 2D (top) + uint16_t stopY; // stop Y coordinate 2D (bottom) char *name; + inline bool getOption(uint8_t n) { return ((options >> n) & 0x01); } + inline bool isSelected() { return getOption(0); } + inline bool isActive() { return stop > start; } + inline uint16_t width() { return options & TRANSPOSED ? stopY - startY : stop - start; } + inline uint16_t height() { return options & TRANSPOSED ? stop - start : stopY - startY; } + inline uint16_t length() { return width(); } + inline uint16_t groupLength() { return grouping + spacing; } + inline uint8_t getLightCapabilities() { return _capabilities; } bool setColor(uint8_t slot, uint32_t c, uint8_t segn) { //returns true if changed if (slot >= NUM_COLORS || segn >= MAX_NUM_SEGMENTS) return false; if (c == colors[slot]) return false; @@ -291,8 +313,7 @@ class WS2812FX { ColorTransition::startTransition(opacity, colors[0], instance->_transitionDur, segn, 0); opacity = o; } - void setOption(uint8_t n, bool val, uint8_t segn = 255) - { + void setOption(uint8_t n, bool val, uint8_t segn = 255) { bool prevOn = false; if (n == SEG_OPTION_ON) { prevOn = getOption(SEG_OPTION_ON); @@ -300,48 +321,31 @@ class WS2812FX { ColorTransition::startTransition(opacity, colors[0], instance->_transitionDur, segn, 0); } } - - if (val) { - options |= 0x01 << n; - } else - { - options &= ~(0x01 << n); - } - + if (val) options |= 0x01 << n; + else options &= ~(0x01 << n); if (n == SEG_OPTION_ON && val && !prevOn) { //fade on ColorTransition::startTransition(0, colors[0], instance->_transitionDur, segn, 0); } } - bool getOption(uint8_t n) - { - return ((options >> n) & 0x01); + uint16_t virtualWidth() { + uint16_t groupLen = groupLength(); + uint16_t vWidth = (width() + groupLen - 1) / groupLen; + if (options & MIRROR) vWidth = (vWidth + 1) /2; // divide by 2 if mirror, leave at least a single LED + return vWidth; } - inline bool isSelected() - { - return getOption(0); + uint16_t virtualHeight() { + uint16_t groupLen = groupLength(); + uint16_t vHeight = (height() + groupLen - 1) / groupLen; + if (options & MIRROR_Y_2D) vHeight = (vHeight + 1) /2; // divide by 2 if mirror, leave at least a single LED + return vHeight; } - inline bool isActive() - { - return stop > start; - } - inline uint16_t length() - { - return stop - start; - } - inline uint16_t groupLength() - { - return grouping + spacing; - } - uint16_t virtualLength() - { + uint16_t virtualLength() { uint16_t groupLen = groupLength(); uint16_t vLength = (length() + groupLen - 1) / groupLen; - if (options & MIRROR) - vLength = (vLength + 1) /2; // divide by 2 if mirror, leave at least a single LED + if (options & MIRROR) vLength = (vLength + 1) /2; // divide by 2 if mirror, leave at least a single LED return vLength; } uint8_t differs(Segment& b); - inline uint8_t getLightCapabilities() {return _capabilities;} void refreshLightCapabilities(); } segment; @@ -610,6 +614,8 @@ class WS2812FX { _mode[FX_MODE_BLENDS] = &WS2812FX::mode_blends; _mode[FX_MODE_TV_SIMULATOR] = &WS2812FX::mode_tv_simulator; _mode[FX_MODE_DYNAMIC_SMOOTH] = &WS2812FX::mode_dynamic_smooth; + _mode[FX_MODE_BLACK_HOLE_A] = &WS2812FX::mode_2DBlackHole_A; + _mode[FX_MODE_BLACK_HOLE_B] = &WS2812FX::mode_2DBlackHole_B; _brightness = DEFAULT_BRIGHTNESS; currentPalette = CRGBPalette16(CRGB::Black); @@ -637,7 +643,7 @@ class WS2812FX { setTransitionMode(bool t), calcGammaTable(float), trigger(void), - setSegment(uint8_t n, uint16_t start, uint16_t stop, uint8_t grouping = 0, uint8_t spacing = 0, uint16_t offset = UINT16_MAX), + setSegment(uint8_t n, uint16_t start, uint16_t stop, uint8_t grouping = 0, uint8_t spacing = 0, uint16_t offset = UINT16_MAX, uint16_t startY=0, uint16_t stopY=0), setMainSegmentId(uint8_t n), restartRuntime(), resetSegments(), @@ -835,6 +841,62 @@ class WS2812FX { mode_tv_simulator(void), mode_dynamic_smooth(void); +// 2D support (panels) + bool + isMatrix = false; + + uint8_t + hPanels = 1, + vPanels = 1; + + uint16_t + panelH = 8, + panelW = 8, + matrixWidth = DEFAULT_LED_COUNT, + matrixHeight = 1; + + #define WLED_MAX_PANELS 64 + typedef struct panel_bitfield_t { + unsigned char + bottomStart : 1, // starts at bottom? + rightStart : 1, // starts on right? + vertical : 1, // is vertical? + serpentine : 1; // is serpentine? + } Panel; + Panel + matrix = {0,0,0,0}, + panel[WLED_MAX_PANELS] = {{0,0,0,0}}; + + void + setUpMatrix(), + setPixelColorXY(uint16_t x, uint16_t y, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0), + fill2D(uint32_t), + blur1d(fract8 blur_amount, CRGB* leds=nullptr), + blur2d(fract8 blur_amount, CRGB* leds=nullptr), + blurRows(fract8 blur_amount, CRGB* leds=nullptr), + blurColumns(fract8 blur_amount, CRGB* leds=nullptr), + fill_solid(const struct CRGB& color, CRGB* leds=nullptr), + fade_out2D(uint8_t r), + fadeToBlackBy(uint8_t fadeBy, CRGB* leds=nullptr), + nscale8(uint8_t scale, CRGB* leds=nullptr), + setPixels(CRGB* leds); + + inline void setPixelColorXY(uint16_t x, uint16_t y, uint32_t c) { setPixelColorXY(x, y, byte(c>>16), byte(c>>8), byte(c), byte(c>>24)); } + inline void setPixelColorXY(uint16_t x, uint16_t y, CRGB &c) { setPixelColorXY(x, y, c.red, c.green, c.blue); } + + uint16_t + XY(uint16_t x, uint16_t y, uint8_t seg=255); + + uint32_t + getPixelColorXY(uint16_t, uint16_t); + + // 2D modes + uint16_t + mode_2DBlackHole_A(), + mode_2DBlackHole_B(); + +// end 2D support + private: uint32_t crgb_to_col(CRGB fastled); CRGB col_to_crgb(uint32_t); @@ -905,9 +967,9 @@ class WS2812FX { uint8_t _segment_index_palette_last = 99; uint8_t _mainSegment; - segment _segments[MAX_NUM_SEGMENTS] = { // SRAM footprint: 24 bytes per element - // start, stop, offset, speed, intensity, palette, mode, options, grouping, spacing, opacity (unused), color[], capabilities - {0, 7, 0, DEFAULT_SPEED, 128, 0, DEFAULT_MODE, NO_OPTIONS, 1, 0, 255, {DEFAULT_COLOR}, 0} + segment _segments[MAX_NUM_SEGMENTS] = { // SRAM footprint: 27 bytes per element + // start, stop, offset, speed, intensity, palette, mode, options, grouping, spacing, opacity (unused), color[], capabilities, custom 1, custom 2, custom 3 + {0, 7, 0, DEFAULT_SPEED, DEFAULT_INTENSITY, 0, DEFAULT_MODE, NO_OPTIONS, 1, 0, 255, {DEFAULT_COLOR}, 0, DEFAULT_C1, DEFAULT_C2, DEFAULT_C3, 0, 1} }; segment_runtime _segment_runtimes[MAX_NUM_SEGMENTS]; // SRAM footprint: 28 bytes per element friend class Segment_runtime; diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp new file mode 100644 index 00000000..be0789e1 --- /dev/null +++ b/wled00/FX_2Dfcn.cpp @@ -0,0 +1,349 @@ +/* + FX_2Dfcn.cpp contains all 2D utility functions + + LICENSE + The MIT License (MIT) + Copyright (c) 2022 Blaz Kristan (https://blaz.at/home) + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + + Parts of the code adapted from WLED Sound Reactive +*/ +#include "wled.h" +#include "FX.h" +#include "palettes.h" + +// setUpMatrix() - constructs ledmap array from matrix of panels with WxH pixels +// this converts physical (possibly irregular) LED arrangement into well defined +// array of logical pixels: fist entry corresponds to left-topmost logical pixel +// followed by horizontal pixels, when matrixWidth logical pixels are added they +// are followed by next row (down) of matrixWidth pixels (and so forth) +// note: matrix may be comprised of multiple panels each with different orientation +// but ledmap takes care of that. ledmap is constructed upon initialization +// so matrix should disable regular ledmap processing +void WS2812FX::setUpMatrix() { + // erase old ledmap, just in case. + if (customMappingTable != nullptr) delete[] customMappingTable; + customMappingTable = nullptr; + customMappingSize = 0; + + if (isMatrix) { + matrixWidth = hPanels * panelW; + matrixHeight = vPanels * panelH; + + // safety check + if (matrixWidth * matrixHeight > MAX_LEDS) { + matrixWidth = getLengthTotal(); + matrixHeight = 1; + isMatrix = false; + return; + } + + customMappingSize = matrixWidth * matrixHeight; + customMappingTable = new uint16_t[customMappingSize]; + + if (customMappingTable != nullptr) { + uint16_t startL; // index in custom mapping array (logical strip) + uint16_t startP; // position of 1st pixel of panel on (virtual) strip + uint16_t x, y, offset; + uint8_t h = matrix.vertical ? vPanels : hPanels; + uint8_t v = matrix.vertical ? hPanels : vPanels; + + for (uint8_t j=0, p=0; j (32 * (20+6)) + 10 + 5 = 847 +uint16_t IRAM_ATTR WS2812FX::XY(uint16_t x, uint16_t y, uint8_t seg) { + if (seg == 255) seg = _segment_index; + x %= _segments[seg].width(); // just in case constrain x (wrap around) + y %= _segments[seg].height(); // just in case constrain y (wrap around) + return ((_segments[seg].startY + y) * matrixWidth) + _segments[seg].start + x; +} + +void IRAM_ATTR WS2812FX::setPixelColorXY(uint16_t x, uint16_t y, byte r, byte g, byte b, byte w) +{ + if (!isMatrix) return; // not a matrix set-up + uint8_t segIdx = SEGLEN ? _segment_index : _mainSegment; + if (SEGLEN && _bri_t < 255) { + r = scale8(r, _bri_t); + g = scale8(g, _bri_t); + b = scale8(b, _bri_t); + w = scale8(w, _bri_t); + } + uint32_t col = RGBW32(r, g, b, w); + + uint16_t width = _segments[segIdx].virtualWidth(); // segment width in logical pixels + uint16_t height = _segments[segIdx].virtualHeight(); // segment height in logical pixels + if (_segments[segIdx].options & MIRROR ) width = (width + 1) /2; // divide by 2 if mirror, leave at least a single LED + if (_segments[segIdx].options & MIRROR_Y_2D) height = (height + 1) /2; // divide by 2 if mirror, leave at least a single LED + if (_segments[segIdx].options & TRANSPOSED ) { uint16_t t = x; x = y; y = t; } // swap X & Y if segment transposed + + x *= _segments[segIdx].groupLength(); + y *= _segments[segIdx].groupLength(); + if (x >= width || y >= height) return; // if pixel would fall out of segment just exit + + for (uint8_t j = 0; j < _segments[segIdx].grouping; j++) { // groupping vertically + for (uint8_t g = 0; g < _segments[segIdx].grouping; g++) { // groupping horizontally + uint16_t index, xX = (x+g), yY = (y+j); + if (xX >= width || yY >= height) continue; // we have reached one dimension's end + + if (_segments[segIdx].options & REVERSE ) xX = width - xX - 1; + if (_segments[segIdx].options & REVERSE_Y_2D) yY = height - yY - 1; + + index = XY(xX, yY, segIdx); + if (index < customMappingSize) index = customMappingTable[index]; + busses.setPixelColor(index, col); + + if (_segments[segIdx].options & MIRROR) { //set the corresponding horizontally mirrored pixel + index = XY(_segments[segIdx].stop - xX - 1, yY, segIdx); + if (index < customMappingSize) index = customMappingTable[index]; + busses.setPixelColor(index, col); + } + if (_segments[segIdx].options & MIRROR_Y_2D) { //set the corresponding vertically mirrored pixel + index = XY(xX, _segments[segIdx].stopY - yY - 1, segIdx); + if (index < customMappingSize) index = customMappingTable[index]; + busses.setPixelColor(index, col); + } + } + } +} + + +uint32_t WS2812FX::getPixelColorXY(uint16_t x, uint16_t y) +{ + uint8_t segIdx = _segment_index; + uint16_t width = _segments[segIdx].virtualWidth(); // segment width in logical pixels + uint16_t height = _segments[segIdx].virtualHeight(); // segment height in logical pixels + if (_segments[segIdx].options & MIRROR ) width = (width + 1) /2; // divide by 2 if mirror, leave at least a single LED + if (_segments[segIdx].options & MIRROR_Y_2D) height = (height + 1) /2; // divide by 2 if mirror, leave at least a single LED + if (_segments[segIdx].options & TRANSPOSED ) { uint16_t t = x; x = y; y = t; } // swap X & Y if segment transposed + + x *= _segments[segIdx].groupLength(); + y *= _segments[segIdx].groupLength(); + if (x >= width || y >= height) return 0; + + if (_segments[segIdx].options & REVERSE ) x = width - x - 1; + if (_segments[segIdx].options & REVERSE_Y_2D) y = height - y - 1; + + uint16_t index = XY(x, y, segIdx); + if (index < customMappingSize) index = customMappingTable[index]; + + return busses.getPixelColor(index); +} + +// blurRows: perform a blur1d on every row of a rectangular matrix +void WS2812FX::blurRows(fract8 blur_amount, CRGB* leds) +{ + uint16_t width = SEGMENT.virtualWidth(); + uint16_t height = SEGMENT.virtualHeight(); + uint8_t keep = 255 - blur_amount; + uint8_t seep = blur_amount >> 1; + CRGB carryover = CRGB::Black; + for (uint16_t y = 0; y < height; y++) for (uint16_t x = 0; x < width; x++) { + CRGB cur = leds ? leds[x + y * width] : col_to_crgb(getPixelColorXY(x,y)); + CRGB part = cur; + part.nscale8(seep); + cur.nscale8(keep); + cur += carryover; + if (x) { + if (leds) leds[(x-1) + y * width] += part; + else setPixelColorXY(x-1, y, col_to_crgb(getPixelColorXY(x-1, y)) + part); + } + if (leds) leds[x + y * width] = cur; + else setPixelColorXY(x, y, cur); + carryover = part; + } +} + +// blurColumns: perform a blur1d on each column of a rectangular matrix +void WS2812FX::blurColumns(fract8 blur_amount, CRGB* leds) +{ + uint16_t width = SEGMENT.virtualWidth(); + uint16_t height = SEGMENT.virtualHeight(); + // blur columns + uint8_t keep = 255 - blur_amount; + uint8_t seep = blur_amount >> 1; + for ( uint16_t x = 0; x < width; x++) { + CRGB carryover = CRGB::Black; + for ( uint16_t y = 0; y < height; y++) { + CRGB cur = leds ? leds[x + y * width] : col_to_crgb(getPixelColorXY(x,y)); + CRGB part = cur; + part.nscale8(seep); + cur.nscale8(keep); + cur += carryover; + if (y) { + if (leds) leds[x + (y-1) * width] += part; + else setPixelColorXY(x, y-1, col_to_crgb(getPixelColorXY(x,y-1)) + part); + } + if (leds) leds[x + y * width] = cur; + else setPixelColorXY(x, y, cur); + carryover = part; + } + } +} + +// blur1d: one-dimensional blur filter. Spreads light to 2 line neighbors. +// blur2d: two-dimensional blur filter. Spreads light to 8 XY neighbors. +// +// 0 = no spread at all +// 64 = moderate spreading +// 172 = maximum smooth, even spreading +// +// 173..255 = wider spreading, but increasing flicker +// +// Total light is NOT entirely conserved, so many repeated +// calls to 'blur' will also result in the light fading, +// eventually all the way to black; this is by design so that +// it can be used to (slowly) clear the LEDs to black. + +void WS2812FX::blur1d(fract8 blur_amount, CRGB* leds) +{ + blurRows(blur_amount, leds); +} + +void WS2812FX::blur2d(fract8 blur_amount, CRGB* leds) +{ + blurRows(blur_amount, leds); + blurColumns(blur_amount, leds); +} + +//ewowi20210628: new functions moved from colorutils: add segment awareness + +/* + * Fills segment with color + */ +void WS2812FX::fill2D(uint32_t c) { + uint16_t w = SEGMENT.virtualWidth(); + uint16_t h = SEGMENT.virtualHeight(); + for(uint16_t y = 0; y < h; y++) for (uint16_t x = 0; x < w; x++) { + setPixelColorXY(x, y, c); + } +} + +void WS2812FX::fill_solid(const struct CRGB& color, CRGB* leds) { + uint16_t w = SEGMENT.virtualWidth(); + uint16_t h = SEGMENT.virtualHeight(); + for(uint16_t y = 0; y < h; y++) for (uint16_t x = 0; x < w; x++) { + if (leds) leds[x + y * w] = color; + else setPixelColorXY(x, y, color); + } +} + +/* + * fade out function, higher rate = quicker fade + * TODO: may be better to use approach of nscale8() + */ +void WS2812FX::fade_out2D(uint8_t rate) { + uint16_t w = SEGMENT.virtualWidth(); + uint16_t h = SEGMENT.virtualHeight(); + rate = (255-rate) >> 1; + float mappedRate = float(rate) +1.1; + + uint32_t color = SEGCOLOR(1); // target color + int w2 = W(color); + int r2 = R(color); + int g2 = G(color); + int b2 = B(color); + + for(uint16_t y = 0; y < h; y++) for (uint16_t x = 0; x < w; x++) { + color = getPixelColorXY(x, y); + int w1 = W(color); + int r1 = R(color); + int g1 = G(color); + int b1 = B(color); + + int wdelta = (w2 - w1) / mappedRate; + int rdelta = (r2 - r1) / mappedRate; + int gdelta = (g2 - g1) / mappedRate; + int bdelta = (b2 - b1) / mappedRate; + + // if fade isn't complete, make sure delta is at least 1 (fixes rounding issues) + wdelta += (w2 == w1) ? 0 : (w2 > w1) ? 1 : -1; + rdelta += (r2 == r1) ? 0 : (r2 > r1) ? 1 : -1; + gdelta += (g2 == g1) ? 0 : (g2 > g1) ? 1 : -1; + bdelta += (b2 == b1) ? 0 : (b2 > b1) ? 1 : -1; + + setPixelColorXY(x, y, r1 + rdelta, g1 + gdelta, b1 + bdelta, w1 + wdelta); + } +} + +void WS2812FX::fadeToBlackBy(uint8_t fadeBy, CRGB* leds) { + nscale8(255 - fadeBy, leds); +} + +void WS2812FX::nscale8(uint8_t scale, CRGB* leds) { + uint16_t w = SEGMENT.virtualWidth(); + uint16_t h = SEGMENT.virtualHeight(); + for(uint16_t y = 0; y < h; y++) for (uint16_t x = 0; x < w; x++) { + if (leds) leds[x + y * w].nscale8(scale); + else setPixelColorXY(x, y, col_to_crgb(getPixelColorXY(x, y)).nscale8(scale)); + } +} + +void WS2812FX::setPixels(CRGB* leds) { + uint16_t w = SEGMENT.virtualWidth(); + uint16_t h = SEGMENT.virtualHeight(); + for (uint16_t y = 0; y < h; y++) for (uint16_t x = 0; x < w; x++) setPixelColorXY(x, y, leds[x + y*w]); +} diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index f6655561..2efd5293 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -184,23 +184,27 @@ void WS2812FX::service() { void IRAM_ATTR WS2812FX::setPixelColor(uint16_t i, byte r, byte g, byte b, byte w) { - uint8_t segIdx; + uint8_t segIdx = SEGLEN ? _segment_index : _mainSegment; + if (isMatrix) { + // map linear pixel into 2D segment area (even for 1D segments, expanding vertically) + uint16_t h = _segments[segIdx].height(); // segment height in logical pixels + uint8_t l = _segments[segIdx].groupLength(); + for (uint16_t y = 0; y < h; y += l) { // expand 1D effect vertically + setPixelColorXY(i, y, r, g, b, w); + } + return; + } - if (SEGLEN) { // SEGLEN!=0 -> from segment/FX + if (SEGLEN || (realtimeMode && useMainSegmentOnly)) { //color_blend(getpixel, col, _bri_t); (pseudocode for future blending of segments) - if (_bri_t < 255) { + if (SEGLEN && _bri_t < 255) { // SEGLEN!=0 -> from segment/FX r = scale8(r, _bri_t); g = scale8(g, _bri_t); b = scale8(b, _bri_t); w = scale8(w, _bri_t); } - segIdx = _segment_index; - } else // from live/realtime - segIdx = _mainSegment; - - if (SEGLEN || (realtimeMode && useMainSegmentOnly)) { uint32_t col = RGBW32(r, g, b, w); - uint16_t len = _segments[segIdx].length(); + uint16_t len = _segments[segIdx].length(); // length of segment in number of pixels // get physical pixel address (taking into account start, grouping, spacing [and offset]) i = i * _segments[segIdx].groupLength(); @@ -211,7 +215,7 @@ void IRAM_ATTR WS2812FX::setPixelColor(uint16_t i, byte r, byte g, byte b, byte i = (len - 1) - i; } } - i += _segments[segIdx].start; + i += _segments[segIdx].start; // starting pixel in a group // set all the pixels in the group for (uint16_t j = 0; j < _segments[segIdx].grouping; j++) { @@ -221,15 +225,13 @@ void IRAM_ATTR WS2812FX::setPixelColor(uint16_t i, byte r, byte g, byte b, byte if (_segments[segIdx].options & MIRROR) { //set the corresponding mirrored pixel uint16_t indexMir = _segments[segIdx].stop - indexSet + _segments[segIdx].start - 1; indexMir += _segments[segIdx].offset; // offset/phase - - if (indexMir >= _segments[segIdx].stop) indexMir -= len; + if (indexMir >= _segments[segIdx].stop) indexMir -= len; // wrap if (indexMir < customMappingSize) indexMir = customMappingTable[indexMir]; busses.setPixelColor(indexMir, col); } indexSet += _segments[segIdx].offset; // offset/phase - - if (indexSet >= _segments[segIdx].stop) indexSet -= len; + if (indexSet >= _segments[segIdx].stop) indexSet -= len; // wrap if (indexSet < customMappingSize) indexSet = customMappingTable[indexSet]; busses.setPixelColor(indexSet, col); @@ -496,6 +498,8 @@ uint8_t WS2812FX::getActiveSegmentsNum(void) { uint32_t WS2812FX::getPixelColor(uint16_t i) { + if (isMatrix) return getPixelColorXY(i, 0); + // get physical pixel i = i * SEGMENT.groupLength();; if (IS_REVERSE) { @@ -566,14 +570,13 @@ uint8_t WS2812FX::Segment::differs(Segment& b) { if (c1x != b.c1x) d |= SEG_DIFFERS_FX; if (c2x != b.c2x) d |= SEG_DIFFERS_FX; if (c3x != b.c3x) d |= SEG_DIFFERS_FX; + if (startY != b.startY) d |= SEG_DIFFERS_BOUNDS; + if (stopY != b.stopY) d |= SEG_DIFFERS_BOUNDS; if ((options & 0b00101110) != (b.options & 0b00101110)) d |= SEG_DIFFERS_OPT; - if ((options & 0x01) != (b.options & 0x01)) d |= SEG_DIFFERS_SEL; + if ((options & 0x01) != (b.options & 0x01)) d |= SEG_DIFFERS_SEL; - for (uint8_t i = 0; i < NUM_COLORS; i++) - { - if (colors[i] != b.colors[i]) d |= SEG_DIFFERS_COL; - } + for (uint8_t i = 0; i < NUM_COLORS; i++) if (colors[i] != b.colors[i]) d |= SEG_DIFFERS_COL; return d; } @@ -640,12 +643,15 @@ bool WS2812FX::hasCCTBus(void) { return false; } -void WS2812FX::setSegment(uint8_t n, uint16_t i1, uint16_t i2, uint8_t grouping, uint8_t spacing, uint16_t offset) { +void WS2812FX::setSegment(uint8_t n, uint16_t i1, uint16_t i2, uint8_t grouping, uint8_t spacing, uint16_t offset, uint16_t startY, uint16_t stopY) { if (n >= MAX_NUM_SEGMENTS) return; Segment& seg = _segments[n]; //return if neither bounds nor grouping have changed bool boundsUnchanged = (seg.start == i1 && seg.stop == i2); + if (isMatrix) { + boundsUnchanged &= (seg.startY == startY && seg.stopY == stopY); + } if (boundsUnchanged && (!grouping || (seg.grouping == grouping && seg.spacing == spacing)) && (offset == UINT16_MAX || offset == seg.offset)) return; @@ -662,9 +668,17 @@ void WS2812FX::setSegment(uint8_t n, uint16_t i1, uint16_t i2, uint8_t grouping, if (n == _mainSegment) setMainSegmentId(0); return; } - if (i1 < _length) seg.start = i1; - seg.stop = i2; - if (i2 > _length) seg.stop = _length; + if (isMatrix) { + if (i1 < matrixWidth) seg.start = i1; + seg.stop = i2 > matrixWidth ? matrixWidth : i2; + if (startY < matrixHeight) seg.startY = startY; + seg.stopY = stopY > matrixHeight ? matrixHeight : MAX(1,stopY); + } else { + if (i1 < _length) seg.start = i1; + seg.stop = i2 > _length ? _length : i2; + seg.startY = 0; + seg.stopY = 1; + } if (grouping) { seg.grouping = grouping; seg.spacing = spacing; @@ -1157,6 +1171,8 @@ uint32_t IRAM_ATTR WS2812FX::color_from_palette(uint16_t i, bool mapping, bool w //load custom mapping table from JSON file (called from finalizeInit() or deserializeState()) void WS2812FX::deserializeMap(uint8_t n) { + if (isMatrix) return; // 2D support creates its own ledmap + char fileName[32]; strcpy_P(fileName, PSTR("/ledmap")); if (n) sprintf(fileName +7, "%d", n); @@ -1399,7 +1415,9 @@ const char JSON_mode_names[] PROGMEM = R"=====([ "Candy Cane@!,Width;;", "Blends@Shift speed,Blend speed;1,2,3,!", "TV Simulator", -"Dynamic Smooth" +"Dynamic Smooth", +"2D Black Hole A@!,!,C1,C2,C3;!;!", +"2D Black Hole B@!,!,C1,C2,C3;!;!" ])====="; const char JSON_palette_names[] PROGMEM = R"=====([ diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 280b7046..a600c8a4 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -91,6 +91,41 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { Bus::setCCTBlend(strip.cctBlending); strip.setTargetFps(hw_led["fps"]); //NOP if 0, default 42 FPS + // 2D Matrix Settings + JsonObject matrix = hw_led[F("matrix")]; + if (!matrix.isNull()) { + strip.isMatrix = true; + CJSON(strip.panelH, matrix[F("ph")]); + CJSON(strip.panelW, matrix[F("pw")]); + CJSON(strip.hPanels, matrix[F("mph")]); + CJSON(strip.vPanels, matrix[F("mpv")]); + CJSON(strip.matrix.bottomStart, matrix[F("pb")]); + CJSON(strip.matrix.rightStart, matrix[F("pr")]); + CJSON(strip.matrix.vertical, matrix[F("pv")]); + CJSON(strip.matrix.serpentine, matrix[F("ps")]); + + JsonArray panels = matrix[F("panels")]; + uint8_t s = 0; + if (!panels.isNull()) { + for (JsonObject pnl : panels) { + CJSON(strip.panel[s].bottomStart, pnl["b"]); + CJSON(strip.panel[s].rightStart, pnl["r"]); + CJSON(strip.panel[s].vertical, pnl["v"]); + CJSON(strip.panel[s].serpentine, pnl["s"]); + if (++s >= WLED_MAX_PANELS) break; // max panels reached + } + } + // clear remaining panels + for (; s0 && mh>0; } function populateInfo(i) @@ -665,6 +670,13 @@ function populateSegments(s)
`; + let rvXck = ``; + let miXck = ``; + let rvYck = "", miYck =""; + if (isM) { + rvYck = ``; + miYck = ``; + } cn += `