Merge branch 'main' into blending-styles

This commit is contained in:
Blaž Kristan
2025-01-14 22:39:20 +01:00
98 changed files with 4316 additions and 3870 deletions

View File

@@ -2,24 +2,10 @@
WS2812FX_fcn.cpp contains all utility functions
Harm Aldick - 2016
www.aldick.org
LICENSE
The MIT License (MIT)
Copyright (c) 2016 Harm Aldick
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.
Licensed under the EUPL v. 1.2 or later
Adapted from code originally licensed under the MIT license
Modified heavily for WLED
*/
@@ -80,15 +66,21 @@ static constexpr bool validatePinsAndTypes(const unsigned* types, unsigned numTy
///////////////////////////////////////////////////////////////////////////////
// Segment class implementation
///////////////////////////////////////////////////////////////////////////////
uint16_t Segment::_usedSegmentData = 0U; // amount of RAM all segments use for their data[]
uint16_t Segment::maxWidth = DEFAULT_LED_COUNT;
uint16_t Segment::maxHeight = 1;
unsigned Segment::_usedSegmentData = 0U; // amount of RAM all segments use for their data[]
uint16_t Segment::maxWidth = DEFAULT_LED_COUNT;
uint16_t Segment::maxHeight = 1;
unsigned Segment::_vLength = 0;
unsigned Segment::_vWidth = 0;
unsigned Segment::_vHeight = 0;
uint8_t Segment::_segBri = 0;
uint32_t Segment::_currentColors[NUM_COLORS] = {0,0,0};
bool Segment::_colorScaled = false;
CRGBPalette16 Segment::_currentPalette = CRGBPalette16(CRGB::Black);
CRGBPalette16 Segment::_randomPalette = generateRandomPalette(); // was CRGBPalette16(DEFAULT_COLOR);
CRGBPalette16 Segment::_newRandomPalette = generateRandomPalette(); // was CRGBPalette16(DEFAULT_COLOR);
uint16_t Segment::_lastPaletteChange = 0; // perhaps it should be per segment
uint16_t Segment::_lastPaletteBlend = 0; //in millis (lowest 16 bits only)
uint16_t Segment::_transitionprogress = 0xFFFF;
#ifndef WLED_DISABLE_MODE_BLEND
bool Segment::_modeBlend = false;
@@ -213,24 +205,12 @@ CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) {
if (pal < 245 && pal > GRADIENT_PALETTE_COUNT+13) pal = 0;
if (pal > 245 && (strip.customPalettes.size() == 0 || 255U-pal > strip.customPalettes.size()-1)) pal = 0; // TODO remove strip dependency by moving customPalettes out of strip
//default palette. Differs depending on effect
if (pal == 0) switch (mode) {
case FX_MODE_FIRE_2012 : pal = 35; break; // heat palette
case FX_MODE_COLORWAVES : pal = 26; break; // landscape 33
case FX_MODE_FILLNOISE8 : pal = 9; break; // ocean colors
case FX_MODE_NOISE16_1 : pal = 20; break; // Drywet
case FX_MODE_NOISE16_2 : pal = 43; break; // Blue cyan yellow
case FX_MODE_NOISE16_3 : pal = 35; break; // heat palette
case FX_MODE_NOISE16_4 : pal = 26; break; // landscape 33
case FX_MODE_GLITTER : pal = 11; break; // rainbow colors
case FX_MODE_SUNRISE : pal = 35; break; // heat palette
case FX_MODE_RAILWAY : pal = 3; break; // prim + sec
case FX_MODE_2DSOAP : pal = 11; break; // rainbow colors
}
if (pal == 0) pal = _default_palette; //load default palette set in FX _data, party colors as default
switch (pal) {
case 0: //default palette. Exceptions for specific effects above
targetPalette = PartyColors_p; break;
case 1: //randomly generated palette
targetPalette = _randomPalette; //random palette is generated at intervals in handleRandomPalette()
targetPalette = _randomPalette; //random palette is generated at intervals in handleRandomPalette()
break;
case 2: {//primary color only
CRGB prim = gamma32(colors[0]);
@@ -254,23 +234,11 @@ CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) {
targetPalette = CRGBPalette16(prim,prim,prim,prim,prim,prim,prim,prim,sec,sec,sec,sec,sec,sec,sec,sec);
}
break;}
case 6: //Party colors
targetPalette = PartyColors_p; break;
case 7: //Cloud colors
targetPalette = CloudColors_p; break;
case 8: //Lava colors
targetPalette = LavaColors_p; break;
case 9: //Ocean colors
targetPalette = OceanColors_p; break;
case 10: //Forest colors
targetPalette = ForestColors_p; break;
case 11: //Rainbow colors
targetPalette = RainbowColors_p; break;
case 12: //Rainbow stripe colors
targetPalette = RainbowStripeColors_p; break;
default: //progmem palettes
if (pal>245) {
targetPalette = strip.customPalettes[255-pal]; // we checked bounds above
} else if (pal < 13) { // palette 6 - 12, fastled palettes
targetPalette = *fastledPalettes[pal-6];
} else {
byte tcp[72];
memcpy_P(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[pal-13])), 72);
@@ -332,12 +300,12 @@ void Segment::stopTransition() {
}
// transition progression between 0-65535
uint16_t IRAM_ATTR Segment::progress() const {
inline void Segment::updateTransitionProgress() {
_transitionprogress = 0xFFFFU;
if (isInTransition()) {
unsigned diff = millis() - _t->_start;
if (_t->_dur > 0 && diff < _t->_dur) return diff * 0xFFFFU / _t->_dur;
if (_t->_dur > 0 && diff < _t->_dur) _transitionprogress = diff * 0xFFFFU / _t->_dur;
}
return 0xFFFFU;
}
#ifndef WLED_DISABLE_MODE_BLEND
@@ -411,13 +379,14 @@ void Segment::restoreSegenv(tmpsegd_t &tmpSeg) {
}
#endif
uint8_t IRAM_ATTR Segment::currentBri(bool useCct) const {
uint32_t prog = progress();
uint8_t Segment::currentBri(bool useCct) const {
unsigned prog = progress();
uint32_t curBri = useCct ? cct : (on ? opacity : 0);
if (prog < 0xFFFFU) {
#ifndef WLED_DISABLE_MODE_BLEND
uint8_t tmpBri = useCct ? _t->_cctT : (_t->_segT._optionsT & 0x0004 ? _t->_briT : 0);
if (blendingStyle > BLEND_STYLE_FADE) return _modeBlend ? tmpBri : curBri; // not fade/blend transition, each effect uses its brightness
// _modeBlend==true -> old effect
if (blendingStyle != BLEND_STYLE_FADE) return _modeBlend ? tmpBri : curBri; // not fade/blend transition, each effect uses its brightness
#else
uint8_t tmpBri = useCct ? _t->_cctT : _t->_briT;
#endif
@@ -432,48 +401,51 @@ uint8_t Segment::currentMode() const {
#ifndef WLED_DISABLE_MODE_BLEND
unsigned prog = progress();
if (prog == 0xFFFFU) return mode;
if (blendingStyle > BLEND_STYLE_FADE) {
if (blendingStyle != BLEND_STYLE_FADE) {
// workaround for on/off transition to respect blending style
uint8_t modeT = (bri != briT) && bri ? FX_MODE_STATIC : _t->_modeT; // On/Off transition active (bri!=briT) and final bri>0 : old mode is STATIC
uint8_t modeS = (bri != briT) && !bri ? FX_MODE_STATIC : mode; // On/Off transition active (bri!=briT) and final bri==0 : new mode is STATIC
return _modeBlend ? modeT : modeS;
return _modeBlend ? modeT : modeS; // _modeBlend==true -> old effect
}
return _modeBlend ? _t->_modeT : mode;
return _modeBlend ? _t->_modeT : mode; // _modeBlend==true -> old effect
#else
return mode;
#endif
}
uint32_t IRAM_ATTR_YN Segment::currentColor(uint8_t slot) const {
uint32_t Segment::currentColor(uint8_t slot) const {
if (slot >= NUM_COLORS) slot = 0;
uint32_t prog = progress();
unsigned prog = progress();
if (prog == 0xFFFFU) return colors[slot];
#ifndef WLED_DISABLE_MODE_BLEND
if (blendingStyle > BLEND_STYLE_FADE) {
if (blendingStyle != BLEND_STYLE_FADE) {
// workaround for on/off transition to respect blending style
uint32_t colT = (bri != briT) && bri ? BLACK : _t->_segT._colorT[slot]; // On/Off transition active (bri!=briT) and final bri>0 : old color is BLACK
uint32_t colS = (bri != briT) && !bri ? BLACK : colors[slot]; // On/Off transition active (bri!=briT) and final bri==0 : new color is BLACK
return _modeBlend ? colT : colS;
return _modeBlend ? colT : colS; // _modeBlend==true -> old effect
}
return color_blend(_t->_segT._colorT[slot], colors[slot], prog, true);
return color_blend16(_t->_segT._colorT[slot], colors[slot], prog);
#else
return color_blend(_t->_colorT[slot], colors[slot], prog, true);
return color_blend16(_t->_colorT[slot], colors[slot], prog);
#endif
}
uint8_t IRAM_ATTR Segment::currentPalette() const {
unsigned prog = progress();
if (prog < 0xFFFFU) {
#ifndef WLED_DISABLE_MODE_BLEND
if (blendingStyle > BLEND_STYLE_FADE && _modeBlend) return _t->_palTid;
#else
return _t->_palTid;
#endif
// pre-calculate drawing parameters for faster access (based on the idea from @softhack007 from MM fork)
void Segment::beginDraw() {
_vWidth = virtualWidth();
_vHeight = virtualHeight();
_vLength = virtualLength();
_segBri = currentBri();
// adjust gamma for effects
for (unsigned i = 0; i < NUM_COLORS; i++) {
#ifndef WLED_DISABLE_MODE_BLEND
uint32_t col = isInTransition() ? color_blend16(_t->_segT._colorT[i], colors[i], progress()) : colors[i];
#else
uint32_t col = isInTransition() ? color_blend16(_t->_colorT[i], colors[i], progress()) : colors[i];
#endif
_currentColors[i] = gamma32(col);
}
return palette;
}
void Segment::setCurrentPalette() {
// load palette into _currentPalette
loadPalette(_currentPalette, palette);
unsigned prog = progress();
if (prog < 0xFFFFU) {
@@ -510,8 +482,10 @@ void Segment::handleRandomPalette() {
nblendPaletteTowardPalette(_randomPalette, _newRandomPalette, 48);
}
// segId is given when called from network callback, changes are queued if that segment is currently in its effect function
void Segment::setUp(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t ofs, uint16_t i1Y, uint16_t i2Y) {
// sets Segment geometry (length or width/height and grouping, spacing and offset as well as 2D mapping)
// strip must be suspended (strip.suspend()) before calling this function
// this function may call fill() to clear pixels if spacing or mapping changed (which requires setting _vWidth, _vHeight, _vLength or beginDraw())
void Segment::setGeometry(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t ofs, uint16_t i1Y, uint16_t i2Y, uint8_t m12) {
// return if neither bounds nor grouping have changed
bool boundsUnchanged = (start == i1 && stop == i2);
#ifndef WLED_DISABLE_2D
@@ -519,11 +493,19 @@ void Segment::setUp(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t
#endif
if (boundsUnchanged
&& (!grp || (grouping == grp && spacing == spc))
&& (ofs == UINT16_MAX || ofs == offset)) return;
&& (ofs == UINT16_MAX || ofs == offset)
&& (m12 == map1D2D)
) return;
stateChanged = true; // send UDP/WS broadcast
if (stop) fill(BLACK); // turn old segment range off (clears pixels if changing spacing)
if (stop || spc != spacing || m12 != map1D2D) {
_vWidth = virtualWidth();
_vHeight = virtualHeight();
_vLength = virtualLength();
_segBri = currentBri();
fill(BLACK); // turn old segment range off or clears pixels if changing spacing (requires _vWidth/_vHeight/_vLength/_segBri)
}
if (grp) { // prevent assignment of 0
grouping = grp;
spacing = spc;
@@ -532,6 +514,7 @@ void Segment::setUp(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t
spacing = 0;
}
if (ofs < UINT16_MAX) offset = ofs;
map1D2D = constrain(m12, 0, 7);
DEBUG_PRINT(F("setUp segment: ")); DEBUG_PRINT(i1);
DEBUG_PRINT(','); DEBUG_PRINT(i2);
@@ -564,49 +547,54 @@ void Segment::setUp(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t
}
bool Segment::setColor(uint8_t slot, uint32_t c) { //returns true if changed
if (slot >= NUM_COLORS || c == colors[slot]) return false;
Segment &Segment::setColor(uint8_t slot, uint32_t c) {
if (slot >= NUM_COLORS || c == colors[slot]) return *this;
if (!_isRGB && !_hasW) {
if (slot == 0 && c == BLACK) return false; // on/off segment cannot have primary color black
if (slot == 1 && c != BLACK) return false; // on/off segment cannot have secondary color non black
if (slot == 0 && c == BLACK) return *this; // on/off segment cannot have primary color black
if (slot == 1 && c != BLACK) return *this; // on/off segment cannot have secondary color non black
}
//DEBUG_PRINTF_P(PSTR("- Starting color transition: %d [0x%X]\n"), slot, c);
startTransition(strip.getTransition()); // start transition prior to change
colors[slot] = c;
stateChanged = true; // send UDP/WS broadcast
return true;
return *this;
}
void Segment::setCCT(uint16_t k) {
Segment &Segment::setCCT(uint16_t k) {
if (k > 255) { //kelvin value, convert to 0-255
if (k < 1900) k = 1900;
if (k > 10091) k = 10091;
k = (k - 1900) >> 5;
}
if (cct == k) return;
//DEBUG_PRINTF_P(PSTR("- Starting CCT transition: %d\n"), k);
startTransition(strip.getTransition()); // start transition prior to change
cct = k;
stateChanged = true; // send UDP/WS broadcast
if (cct != k) {
//DEBUG_PRINTF_P(PSTR("- Starting CCT transition: %d\n"), k);
startTransition(strip.getTransition()); // start transition prior to change
cct = k;
stateChanged = true; // send UDP/WS broadcast
}
return *this;
}
void Segment::setOpacity(uint8_t o) {
if (opacity == o) return;
//DEBUG_PRINTF_P(PSTR("- Starting opacity transition: %d\n"), o);
startTransition(strip.getTransition()); // start transition prior to change
opacity = o;
stateChanged = true; // send UDP/WS broadcast
Segment &Segment::setOpacity(uint8_t o) {
if (opacity != o) {
//DEBUG_PRINTF_P(PSTR("- Starting opacity transition: %d\n"), o);
startTransition(strip.getTransition()); // start transition prior to change
opacity = o;
stateChanged = true; // send UDP/WS broadcast
}
return *this;
}
void Segment::setOption(uint8_t n, bool val) {
Segment &Segment::setOption(uint8_t n, bool val) {
bool prevOn = on;
if (n == SEG_OPTION_ON && val != prevOn) startTransition(strip.getTransition()); // start transition prior to change
if (val) options |= 0x01 << n;
else options &= ~(0x01 << n);
if (!(n == SEG_OPTION_SELECTED || n == SEG_OPTION_RESET)) stateChanged = true; // send UDP/WS broadcast
return *this;
}
void Segment::setMode(uint8_t fx, bool loadDefaults) {
Segment &Segment::setMode(uint8_t fx, bool loadDefaults) {
// skip reserved
while (fx < strip.getModeCount() && strncmp_P("RSVD", strip.getModeData(fx), 4) == 0) fx++;
if (fx >= strip.getModeCount()) fx = 0; // set solid mode
@@ -617,9 +605,9 @@ void Segment::setMode(uint8_t fx, bool loadDefaults) {
startTransition(strip.getTransition()); // set effect transitions
#endif
mode = fx;
int sOpt;
// load default values from effect string
if (loadDefaults) {
int sOpt;
sOpt = extractModeDefaults(fx, "sx"); speed = (sOpt >= 0) ? sOpt : DEFAULT_SPEED;
sOpt = extractModeDefaults(fx, "ix"); intensity = (sOpt >= 0) ? sOpt : DEFAULT_INTENSITY;
sOpt = extractModeDefaults(fx, "c1"); custom1 = (sOpt >= 0) ? sOpt : DEFAULT_C1;
@@ -636,12 +624,16 @@ void Segment::setMode(uint8_t fx, bool loadDefaults) {
sOpt = extractModeDefaults(fx, "mY"); if (sOpt >= 0) mirror_y = (bool)sOpt; // NOTE: setting this option is a risky business
sOpt = extractModeDefaults(fx, "pal"); if (sOpt >= 0) setPalette(sOpt); //else setPalette(0);
}
sOpt = extractModeDefaults(fx, "pal"); // always extract 'pal' to set _default_palette
if(sOpt <= 0) sOpt = 6; // partycolors if zero or not set
_default_palette = sOpt; // _deault_palette is loaded into pal0 in loadPalette() (if selected)
markForReset();
stateChanged = true; // send UDP/WS broadcast
}
return *this;
}
void Segment::setPalette(uint8_t pal) {
Segment &Segment::setPalette(uint8_t pal) {
if (pal < 245 && pal > GRADIENT_PALETTE_COUNT+13) pal = 0; // built in palettes
if (pal > 245 && (strip.customPalettes.size() == 0 || 255U-pal > strip.customPalettes.size()-1)) pal = 0; // custom palettes
if (pal != palette) {
@@ -650,37 +642,24 @@ void Segment::setPalette(uint8_t pal) {
palette = pal;
stateChanged = true; // send UDP/WS broadcast
}
return *this;
}
// 2D matrix
uint16_t IRAM_ATTR Segment::virtualWidth() const {
unsigned Segment::virtualWidth() const {
unsigned groupLen = groupLength();
unsigned vWidth = ((transpose ? height() : width()) + groupLen - 1) / groupLen;
if (mirror) vWidth = (vWidth + 1) /2; // divide by 2 if mirror, leave at least a single LED
return vWidth;
}
uint16_t IRAM_ATTR Segment::virtualHeight() const {
unsigned Segment::virtualHeight() const {
unsigned groupLen = groupLength();
unsigned vHeight = ((transpose ? width() : height()) + groupLen - 1) / groupLen;
if (mirror_y) vHeight = (vHeight + 1) /2; // divide by 2 if mirror, leave at least a single LED
return vHeight;
}
uint16_t IRAM_ATTR_YN Segment::nrOfVStrips() const {
unsigned vLen = 1;
#ifndef WLED_DISABLE_2D
if (is2D()) {
switch (map1D2D) {
case M12_pBar:
vLen = virtualWidth();
break;
}
}
#endif
return vLen;
}
// Constants for mapping mode "Pinwheel"
#ifndef WLED_DISABLE_2D
constexpr int Pinwheel_Steps_Small = 72; // no holes up to 16x16
@@ -718,7 +697,7 @@ static int getPinwheelLength(int vW, int vH) {
#endif
// 1D strip
uint16_t IRAM_ATTR Segment::virtualLength() const {
uint16_t Segment::virtualLength() const {
#ifndef WLED_DISABLE_2D
if (is2D()) {
unsigned vW = virtualWidth();
@@ -779,19 +758,31 @@ bool IRAM_ATTR_YN Segment::isPixelClipped(int i) const {
void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col)
{
if (!isActive()) return; // not active
if (!isActive() || i < 0) return; // not active or invalid index
#ifndef WLED_DISABLE_2D
int vStrip = i>>16; // hack to allow running on virtual strips (2D segment columns/rows)
int vStrip = 0;
#endif
i &= 0xFFFF;
int vL = virtualLength();
if (i >= vL || i < 0) return; // if pixel would fall out of segment just exit
int vL = vLength();
// if the 1D effect is using virtual strips "i" will have virtual strip id stored in upper 16 bits
// in such case "i" will be > virtualLength()
if (i >= vL) {
// check if this is a virtual strip
#ifndef WLED_DISABLE_2D
vStrip = i>>16; // hack to allow running on virtual strips (2D segment columns/rows)
i &= 0xFFFF; //truncate vstrip index
if (i >= vL) return; // if pixel would still fall out of segment just exit
#else
return;
#endif
}
#ifndef WLED_DISABLE_2D
if (is2D()) {
int vH = virtualHeight(); // segment height in logical pixels
int vW = virtualWidth();
const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive)
const int vH = vHeight(); // segment height in logical pixels (is always >= 1)
// pre-scale color for all pixels
col = color_fade(col, _segBri);
_colorScaled = true;
switch (map1D2D) {
case M12_Pixels:
// use all available pixels as a long strip
@@ -799,12 +790,12 @@ void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col)
break;
case M12_pBar:
// expand 1D effect vertically or have it play on virtual strips
if (vStrip>0) setPixelColorXY(vStrip - 1, vH - i - 1, col);
else for (int x = 0; x < vW; x++) setPixelColorXY(x, vH - i - 1, col);
if (vStrip > 0) setPixelColorXY(vStrip - 1, vH - i - 1, col);
else for (int x = 0; x < vW; x++) setPixelColorXY(x, vH - i - 1, col);
break;
case M12_pArc:
// expand in circular fashion from center
if (i==0)
if (i == 0)
setPixelColorXY(0, 0, col);
else {
float r = i;
@@ -861,7 +852,7 @@ void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col)
// Odd rays start further from center if prevRay started at center.
static int prevRay = INT_MIN; // previous ray number
if ((i % 2 == 1) && (i - 1 == prevRay || i + 1 == prevRay)) {
int jump = min(vW/3, vH/3); // can add 2 if using medium pinwheel
int jump = min(vW/3, vH/3); // can add 2 if using medium pinwheel
posx += inc_x * jump;
posy += inc_y * jump;
}
@@ -883,13 +874,14 @@ void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col)
break;
}
}
_colorScaled = false;
return;
} else if (Segment::maxHeight!=1 && (width()==1 || height()==1)) {
} else if (Segment::maxHeight != 1 && (width() == 1 || height() == 1)) {
if (start < Segment::maxWidth*Segment::maxHeight) {
// we have a vertical or horizontal 1D segment (WARNING: virtual...() may be transposed)
int x = 0, y = 0;
if (virtualHeight()>1) y = i;
if (virtualWidth() >1) x = i;
if (vHeight() > 1) y = i;
if (vWidth() > 1) x = i;
setPixelColorXY(x, y, col);
return;
}
@@ -909,10 +901,8 @@ void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col)
if (i >= vL || i < 0 || isPixelClipped(i)) return; // handle clipping on 1D
unsigned len = length();
uint8_t _bri_t = currentBri();
if (_bri_t < 255) {
col = color_fade(col, _bri_t);
}
// if color is unscaled
if (!_colorScaled) col = color_fade(col, _segBri);
// expand pixel (taking into account start, grouping, spacing [and offset])
i = i * groupLength();
@@ -936,7 +926,7 @@ void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col)
if (indexMir >= stop) indexMir -= len; // wrap
#ifndef WLED_DISABLE_MODE_BLEND
// _modeBlend==true -> old effect
if (_modeBlend && blendingStyle == BLEND_STYLE_FADE) tmpCol = color_blend(strip.getPixelColor(indexMir), col, 0xFFFFU - progress(), true);
if (_modeBlend && blendingStyle == BLEND_STYLE_FADE) tmpCol = color_blend16(strip.getPixelColor(indexMir), col, 0xFFFFU - progress());
#endif
strip.setPixelColor(indexMir, tmpCol);
}
@@ -944,7 +934,7 @@ void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col)
if (indexSet >= stop) indexSet -= len; // wrap
#ifndef WLED_DISABLE_MODE_BLEND
// _modeBlend==true -> old effect
if (_modeBlend && blendingStyle == BLEND_STYLE_FADE) tmpCol = color_blend(strip.getPixelColor(indexSet), col, 0xFFFFU - progress(), true);
if (_modeBlend && blendingStyle == BLEND_STYLE_FADE) tmpCol = color_blend16(strip.getPixelColor(indexSet), col, 0xFFFFU - progress());
#endif
strip.setPixelColor(indexSet, tmpCol);
}
@@ -989,26 +979,23 @@ void Segment::setPixelColor(float i, uint32_t col, bool aa)
uint32_t IRAM_ATTR_YN Segment::getPixelColor(int i) const
{
if (!isActive()) return 0; // not active
#ifndef WLED_DISABLE_2D
int vStrip = i>>16;
#endif
i &= 0xFFFF;
int vL = virtualLength();
int vL = vLength();
if (i >= vL || i < 0) return 0;
#ifndef WLED_DISABLE_2D
if (is2D()) {
int vH = virtualHeight(); // segment height in logical pixels
int vW = virtualWidth();
const int vW = vWidth(); // segment width in logical pixels (can be 0 if segment is inactive)
const int vH = vHeight(); // segment height in logical pixels (is always >= 1)
switch (map1D2D) {
case M12_Pixels:
return getPixelColorXY(i % vW, i / vW);
break;
case M12_pBar:
if (vStrip>0) return getPixelColorXY(vStrip - 1, vH - i -1);
else return getPixelColorXY(0, vH - i -1);
break;
case M12_pBar: {
int vStrip = i>>16; // virtual strips are only relevant in Bar expansion mode
if (vStrip > 0) return getPixelColorXY(vStrip - 1, vH - (i & 0xFFFF) -1);
else return getPixelColorXY(0, vH - i -1);
break; }
case M12_pArc:
if (i >= vW && i >= vH) {
unsigned vI = sqrt16(i*i/2);
@@ -1072,7 +1059,7 @@ uint32_t IRAM_ATTR_YN Segment::getPixelColor(int i) const
return strip.getPixelColor(i);
}
uint8_t Segment::differs(Segment& b) const {
uint8_t Segment::differs(const Segment& b) const {
uint8_t d = 0;
if (start != b.start) d |= SEG_DIFFERS_BOUNDS;
if (stop != b.stop) d |= SEG_DIFFERS_BOUNDS;
@@ -1152,12 +1139,16 @@ void Segment::refreshLightCapabilities() {
*/
void Segment::fill(uint32_t c) {
if (!isActive()) return; // not active
const int cols = is2D() ? virtualWidth() : virtualLength();
const int rows = virtualHeight(); // will be 1 for 1D
const int cols = is2D() ? vWidth() : vLength();
const int rows = vHeight(); // will be 1 for 1D
// pre-scale color for all pixels
c = color_fade(c, _segBri);
_colorScaled = true;
for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) {
if (is2D()) setPixelColorXY(x, y, c);
else setPixelColor(x, c);
}
_colorScaled = false;
}
/*
@@ -1165,8 +1156,8 @@ void Segment::fill(uint32_t c) {
*/
void Segment::fade_out(uint8_t rate) {
if (!isActive()) return; // not active
const int cols = is2D() ? virtualWidth() : virtualLength();
const int rows = virtualHeight(); // will be 1 for 1D
const int cols = is2D() ? vWidth() : vLength();
const int rows = vHeight(); // will be 1 for 1D
rate = (255-rate) >> 1;
float mappedRate = 1.0f / (float(rate) + 1.1f);
@@ -1204,8 +1195,8 @@ void Segment::fade_out(uint8_t rate) {
// fades all pixels to black using nscale8()
void Segment::fadeToBlackBy(uint8_t fadeBy) {
if (!isActive() || fadeBy == 0) return; // optimization - no scaling to apply
const int cols = is2D() ? virtualWidth() : virtualLength();
const int rows = virtualHeight(); // will be 1 for 1D
const int cols = is2D() ? vWidth() : vLength();
const int rows = vHeight(); // will be 1 for 1D
for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) {
if (is2D()) setPixelColorXY(x, y, color_fade(getPixelColorXY(x,y), 255-fadeBy));
@@ -1215,20 +1206,21 @@ void Segment::fadeToBlackBy(uint8_t fadeBy) {
/*
* blurs segment content, source: FastLED colorutils.cpp
* Note: for blur_amount > 215 this function does not work properly (creates alternating pattern)
*/
void Segment::blur(uint8_t blur_amount, bool smear) {
if (!isActive() || blur_amount == 0) return; // optimization: 0 means "don't blur"
#ifndef WLED_DISABLE_2D
if (is2D()) {
// compatibility with 2D
blur2D(blur_amount, smear);
blur2D(blur_amount, blur_amount, smear); // symmetrical 2D blur
//box_blur(map(blur_amount,1,255,1,3), smear);
return;
}
#endif
uint8_t keep = smear ? 255 : 255 - blur_amount;
uint8_t seep = blur_amount >> (1 + smear);
unsigned vlength = virtualLength();
uint8_t seep = blur_amount >> 1;
unsigned vlength = vLength();
uint32_t carryover = BLACK;
uint32_t lastnew;
uint32_t last;
@@ -1238,12 +1230,11 @@ void Segment::blur(uint8_t blur_amount, bool smear) {
uint32_t part = color_fade(cur, seep);
curnew = color_fade(cur, keep);
if (i > 0) {
if (carryover) curnew = color_add(curnew, carryover, true);
uint32_t prev = color_add(lastnew, part, true);
if (carryover) curnew = color_add(curnew, carryover);
uint32_t prev = color_add(lastnew, part);
// optimization: only set pixel if color has changed
if (last != prev) setPixelColor(i - 1, prev);
} else // first pixel
setPixelColor(i, curnew);
} else setPixelColor(i, curnew); // first pixel
lastnew = curnew;
last = cur; // save original value for comparison on next iteration
carryover = part;
@@ -1258,11 +1249,11 @@ void Segment::blur(uint8_t blur_amount, bool smear) {
*/
uint32_t Segment::color_wheel(uint8_t pos) const {
if (palette) return color_from_palette(pos, false, true, 0); // perhaps "strip.paletteBlend < 2" should be better instead of "true"
uint8_t w = W(currentColor(0));
uint8_t w = W(getCurrentColor(0));
pos = 255 - pos;
if (pos < 85) {
return RGBW32((255 - pos * 3), 0, (pos * 3), w);
} else if(pos < 170) {
} else if (pos < 170) {
pos -= 85;
return RGBW32(0, (pos * 3), (255 - pos * 3), w);
} else {
@@ -1281,18 +1272,21 @@ uint32_t Segment::color_wheel(uint8_t pos) const {
* @returns Single color from palette
*/
uint32_t Segment::color_from_palette(uint16_t i, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri) const {
uint32_t color = gamma32(currentColor(mcol));
uint32_t color = getCurrentColor(mcol < NUM_COLORS ? mcol : 0);
// default palette or no RGB support on segment
if ((currentPalette() == 0 && mcol < NUM_COLORS) || !_isRGB) return (pbri == 255) ? color : color_fade(color, pbri, true);
if ((palette == 0 && mcol < NUM_COLORS) || !_isRGB) {
return color_fade(color, pbri, true);
}
const int vL = vLength();
unsigned paletteIndex = i;
if (mapping && virtualLength() > 1) paletteIndex = (i*255)/(virtualLength() -1);
if (mapping && vL > 1) paletteIndex = (i*255)/(vL -1);
// paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined)
if (!wrap && strip.paletteBlend != 3) paletteIndex = scale8(paletteIndex, 240); //cut off blend at palette "end"
CRGB fastled_col = ColorFromPalette(_currentPalette, paletteIndex, pbri, (strip.paletteBlend == 3)? NOBLEND:LINEARBLEND); // NOTE: paletteBlend should be global
CRGBW palcol = ColorFromPaletteWLED(_currentPalette, paletteIndex, pbri, (strip.paletteBlend == 3)? NOBLEND:LINEARBLEND); // NOTE: paletteBlend should be global
palcol.w = W(color);
return RGBW32(fastled_col.r, fastled_col.g, fastled_col.b, W(color));
return palcol.color32;
}
@@ -1303,10 +1297,7 @@ uint32_t Segment::color_from_palette(uint16_t i, bool mapping, bool wrap, uint8_
//do not call this method from system context (network callback)
void WS2812FX::finalizeInit() {
//reset segment runtimes
for (segment &seg : _segments) {
seg.markForReset();
seg.resetIfRequired();
}
restartRuntime();
// for the lack of better place enumerate ledmaps here
// if we do it in json.cpp (serializeInfo()) we are getting flashes on LEDs
@@ -1328,7 +1319,7 @@ void WS2812FX::finalizeInit() {
static_assert(validatePinsAndTypes(defDataTypes, defNumTypes, defNumPins),
"The default pin list defined in DATA_PINS does not match the pin requirements for the default buses defined in LED_TYPES");
unsigned prevLen = 0;
unsigned pinsIndex = 0;
for (unsigned i = 0; i < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; i++) {
@@ -1339,7 +1330,7 @@ void WS2812FX::finalizeInit() {
// if we need more pins than available all outputs have been configured
if (pinsIndex + busPins > defNumPins) break;
// Assign all pins first so we can check for conflicts on this bus
for (unsigned j = 0; j < busPins && j < OUTPUT_MAX_PINS; j++) defPin[j] = defDataPins[pinsIndex + j];
@@ -1392,7 +1383,7 @@ void WS2812FX::finalizeInit() {
// if we have less counts than pins and they do not align, use last known count to set current count
unsigned count = defCounts[(i < defNumCounts) ? i : defNumCounts -1];
// analog always has length 1
if (Bus::isPWM(dataType)) count = 1;
if (Bus::isPWM(dataType) || Bus::isOnOff(dataType)) count = 1;
prevLen += count;
BusConfig defCfg = BusConfig(dataType, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY, 0, useGlobalLedBuffer);
if (BusManager::add(defCfg) == -1) break;
@@ -1410,14 +1401,9 @@ void WS2812FX::finalizeInit() {
_isOffRefreshRequired |= bus->isOffRefreshRequired() && !bus->isPWM(); // use refresh bit for phase shift with analog
unsigned busEnd = bus->getStart() + bus->getLength();
if (busEnd > _length) _length = busEnd;
#ifdef ESP8266
// why do we need to reinitialise GPIO3???
//if (!bus->isDigital() || bus->is2Pin()) continue;
//uint8_t pins[5];
//if (!bus->getPins(pins)) continue;
//BusDigital* bd = static_cast<BusDigital*>(bus);
//if (pins[0] == 3) bd->reinit();
#endif
// This must be done after all buses have been created, as some kinds (parallel I2S) interact
bus->begin();
}
Segment::maxWidth = _length;
@@ -1433,9 +1419,15 @@ void WS2812FX::finalizeInit() {
void WS2812FX::service() {
unsigned long nowUp = millis(); // Be aware, millis() rolls over every 49 days
now = nowUp + timebase;
if (nowUp - _lastShow < MIN_SHOW_DELAY || _suspend) return;
if (_suspend) return;
unsigned long elapsed = nowUp - _lastServiceShow;
if (elapsed <= MIN_FRAME_DELAY) return; // keep wifi alive - no matter if triggered or unlimited
if ( !_triggered && (_targetFps != FPS_UNLIMITED)) { // unlimited mode = no frametime
if (elapsed < _frametime) return; // too early for service
}
bool doShow = false;
int pal = -1; // optimise palette loading
_isServicing = true;
_segment_index = 0;
@@ -1451,19 +1443,13 @@ void WS2812FX::service() {
if (!seg.isActive()) continue;
// last condition ensures all solid segments are updated at the same time
if (nowUp > seg.next_time || _triggered || (doShow && seg.mode == FX_MODE_STATIC))
if (nowUp >= seg.next_time || _triggered || (doShow && seg.mode == FX_MODE_STATIC))
{
doShow = true;
unsigned delay = FRAMETIME;
unsigned frameDelay = FRAMETIME;
if (!seg.freeze) { //only run effect function if not frozen
int oldCCT = BusManager::getSegmentCCT(); // store original CCT value (actually it is not Segment based)
_virtualSegmentLength = seg.virtualLength(); //SEGLEN
_colors_t[0] = gamma32(seg.currentColor(0));
_colors_t[1] = gamma32(seg.currentColor(1));
_colors_t[2] = gamma32(seg.currentColor(2));
if (seg.currentPalette() != pal) seg.setCurrentPalette(); // load actual palette
pal = seg.currentPalette();
// when correctWB is true we need to correct/adjust RGB value according to desired CCT value, but it will also affect actual WW/CW ratio
// when cctFromRgb is true we implicitly calculate WW and CW from RGB values
if (cctFromRgb) BusManager::setSegmentCCT(-1);
@@ -1474,14 +1460,15 @@ void WS2812FX::service() {
// The blending will largely depend on the effect behaviour since actual output (LEDs) may be
// overwritten by later effect. To enable seamless blending for every effect, additional LED buffer
// would need to be allocated for each effect and then blended together for each pixel.
seg.beginDraw(); // set up parameters for get/setPixelColor()
#ifndef WLED_DISABLE_MODE_BLEND
Segment::setClippingRect(0, 0); // disable clipping (just in case)
if (seg.isInTransition()) {
// set clipping rectangle
// new mode is run inside clipping area and old mode outside clipping area
unsigned p = seg.progress();
unsigned w = seg.is2D() ? seg.virtualWidth() : _virtualSegmentLength;
unsigned h = seg.virtualHeight();
unsigned w = seg.is2D() ? Segment::vWidth() : Segment::vLength();
unsigned h = Segment::vHeight();
unsigned dw = p * w / 0xFFFFU + 1;
unsigned dh = p * h / 0xFFFFU + 1;
unsigned orgBS = blendingStyle;
@@ -1531,51 +1518,44 @@ void WS2812FX::service() {
Segment::setClippingRect(0, dw, h - dh, h);
break;
}
delay = (*_mode[seg.currentMode()])(); // run new/current mode
frameDelay = (*_mode[seg.currentMode()])(); // run new/current mode
// now run old/previous mode
Segment::tmpsegd_t _tmpSegData;
Segment::modeBlend(true); // set semaphore
seg.swapSegenv(_tmpSegData); // temporarily store new mode state (and swap it with transitional state)
_virtualSegmentLength = seg.virtualLength(); // update SEGLEN (mapping may have changed)
_colors_t[0] = gamma32(seg.currentColor(0));
_colors_t[1] = gamma32(seg.currentColor(1));
_colors_t[2] = gamma32(seg.currentColor(2));
if (seg.currentPalette() != pal) {
seg.setCurrentPalette(); // load actual palette
pal = seg.currentPalette();
}
unsigned d2 = (*_mode[seg.currentMode()])(); // run old mode
seg.beginDraw(); // set up parameters for get/setPixelColor()
frameDelay = min(frameDelay, (unsigned)(*_mode[seg.currentMode()])()); // run old mode
seg.call++; // increment old mode run counter
seg.restoreSegenv(_tmpSegData); // restore mode state (will also update transitional state)
delay = MIN(delay,d2); // use shortest delay
Segment::modeBlend(false); // unset semaphore
blendingStyle = orgBS; // restore blending style if it was modified for single pixel segment
} else
#endif
delay = (*_mode[seg.mode])(); // run effect mode (not in transition)
frameDelay = (*_mode[seg.mode])(); // run effect mode (not in transition)
seg.call++;
if (seg.isInTransition() && delay > FRAMETIME) delay = FRAMETIME; // force faster updates during transition
BusManager::setSegmentCCT(oldCCT); // restore old CCT for ABL adjustments
if (seg.isInTransition() && frameDelay > FRAMETIME) frameDelay = FRAMETIME; // force faster updates during transition
BusManager::setSegmentCCT(oldCCT); // restore old CCT for ABL adjustments
}
seg.next_time = nowUp + delay;
seg.next_time = nowUp + frameDelay;
}
_segment_index++;
}
Segment::setClippingRect(0, 0); // disable clipping for overlays
_virtualSegmentLength = 0;
_isServicing = false;
_triggered = false;
#ifdef WLED_DEBUG
if (millis() - nowUp > _frametime) DEBUG_PRINTF_P(PSTR("Slow effects %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime);
if ((_targetFps != FPS_UNLIMITED) && (millis() - nowUp > _frametime)) DEBUG_PRINTF_P(PSTR("Slow effects %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime);
#endif
if (doShow) {
yield();
Segment::handleRandomPalette(); // slowly transition random palette; move it into for loop when each segment has individual random palette
show();
_lastServiceShow = nowUp; // update timestamp, for precise FPS control
}
#ifdef WLED_DEBUG
if (millis() - nowUp > _frametime) DEBUG_PRINTF_P(PSTR("Slow strip %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime);
if ((_targetFps != FPS_UNLIMITED) && (millis() - nowUp > _frametime)) DEBUG_PRINTF_P(PSTR("Slow strip %u/%d.\n"), (unsigned)(millis()-nowUp), (int)_frametime);
#endif
}
@@ -1585,7 +1565,7 @@ void IRAM_ATTR WS2812FX::setPixelColor(unsigned i, uint32_t col) {
BusManager::setPixelColor(i, col);
}
uint32_t IRAM_ATTR WS2812FX::getPixelColor(uint16_t i) const {
uint32_t IRAM_ATTR WS2812FX::getPixelColor(unsigned i) const {
i = getMappedPixelIndex(i);
if (i >= _length) return 0;
return BusManager::getPixelColor(i);
@@ -1595,61 +1575,26 @@ void WS2812FX::show() {
// avoid race condition, capture _callback value
show_callback callback = _callback;
if (callback) callback();
unsigned long showNow = millis();
// some buses send asynchronously and this method will return before
// all of the data has been sent.
// See https://github.com/Makuna/NeoPixelBus/wiki/ESP32-NeoMethods#neoesp32rmt-methods
BusManager::show();
unsigned long showNow = millis();
size_t diff = showNow - _lastShow;
size_t fpsCurr = 200;
if (diff > 0) fpsCurr = 1000 / diff;
_cumulativeFps = (3 * _cumulativeFps + fpsCurr +2) >> 2; // "+2" for proper rounding (2/4 = 0.5)
_lastShow = showNow;
}
/**
* Returns a true value if any of the strips are still being updated.
* On some hardware (ESP32), strip updates are done asynchronously.
*/
bool WS2812FX::isUpdating() const {
return !BusManager::canAllShow();
}
/**
* Returns the refresh rate of the LED strip. Useful for finding out whether a given setup is fast enough.
* Only updates on show() or is set to 0 fps if last show is more than 2 secs ago, so accuracy varies
*/
uint16_t WS2812FX::getFps() const {
if (millis() - _lastShow > 2000) return 0;
return _cumulativeFps +1;
}
void WS2812FX::setTargetFps(uint8_t fps) {
if (fps > 0 && fps <= 120) _targetFps = fps;
_frametime = 1000 / _targetFps;
}
void WS2812FX::setMode(uint8_t segid, uint8_t m) {
if (segid >= _segments.size()) return;
if (m >= getModeCount()) m = getModeCount() - 1;
if (_segments[segid].mode != m) {
_segments[segid].setMode(m); // do not load defaults
if (diff > 0) { // skip calculation if no time has passed
size_t fpsCurr = (1000 << FPS_CALC_SHIFT) / diff; // fixed point math
_cumulativeFps = (FPS_CALC_AVG * _cumulativeFps + fpsCurr + FPS_CALC_AVG / 2) / (FPS_CALC_AVG + 1); // "+FPS_CALC_AVG/2" for proper rounding
_lastShow = showNow;
}
}
//applies to all active and selected segments
void WS2812FX::setColor(uint8_t slot, uint32_t c) {
if (slot >= NUM_COLORS) return;
for (segment &seg : _segments) {
if (seg.isActive() && seg.isSelected()) {
seg.setColor(slot, c);
}
}
void WS2812FX::setTargetFps(unsigned fps) {
if (fps <= 250) _targetFps = fps;
if (_targetFps > 0) _frametime = 1000 / _targetFps;
else _frametime = MIN_FRAME_DELAY; // unlimited mode
}
void WS2812FX::setCCT(uint16_t k) {
@@ -1676,7 +1621,7 @@ void WS2812FX::setBrightness(uint8_t b, bool direct) {
BusManager::setBrightness(b);
if (!direct) {
unsigned long t = millis();
if (_segments[0].next_time > t + 22 && t - _lastShow > MIN_SHOW_DELAY) trigger(); //apply brightness change immediately if no refresh soon
if (_segments[0].next_time > t + 22 && t - _lastShow > MIN_FRAME_DELAY) trigger(); //apply brightness change immediately if no refresh soon
}
}
@@ -1698,7 +1643,7 @@ uint8_t WS2812FX::getFirstSelectedSegId() const {
return getMainSegmentId();
}
void WS2812FX::setMainSegmentId(uint8_t n) {
void WS2812FX::setMainSegmentId(unsigned n) {
_mainSegment = 0;
if (n < _segments.size()) {
_mainSegment = n;
@@ -1774,23 +1719,10 @@ void WS2812FX::purgeSegments() {
}
}
Segment& WS2812FX::getSegment(uint8_t id) {
Segment& WS2812FX::getSegment(unsigned id) {
return _segments[id >= _segments.size() ? getMainSegmentId() : id]; // vectors
}
// sets new segment bounds, queues if that segment is currently running
void WS2812FX::setSegment(uint8_t segId, uint16_t i1, uint16_t i2, uint8_t grouping, uint8_t spacing, uint16_t offset, uint16_t startY, uint16_t stopY) {
if (segId >= getSegmentsNum()) {
if (i2 <= i1) return; // do not append empty/inactive segments
appendSegment(Segment(0, strip.getLengthTotal()));
segId = getSegmentsNum()-1; // segments are added at the end of list
}
suspend();
_segments[segId].setUp(i1, i2, grouping, spacing, offset, startY, stopY);
resume();
if (segId > 0 && segId == getSegmentsNum()-1 && i2 <= i1) _segments.pop_back(); // if last segment was deleted remove it from vector
}
void WS2812FX::resetSegments() {
_segments.clear(); // destructs all Segment as part of clearing
#ifndef WLED_DISABLE_2D
@@ -1989,7 +1921,7 @@ void WS2812FX::loadCustomPalettes() {
}
//load custom mapping table from JSON file (called from finalizeInit() or deserializeState())
bool WS2812FX::deserializeMap(uint8_t n) {
bool WS2812FX::deserializeMap(unsigned n) {
// 2D support creates its own ledmap (on the fly) if a ledmap.json exists it will overwrite built one.
char fileName[32];
@@ -2041,14 +1973,6 @@ bool WS2812FX::deserializeMap(uint8_t n) {
return (customMappingSize > 0);
}
uint16_t IRAM_ATTR WS2812FX::getMappedPixelIndex(uint16_t index) const {
// convert logical address to physical
if (index < customMappingSize
&& (realtimeMode == REALTIME_MODE_INACTIVE || realtimeRespectLedMaps)) index = customMappingTable[index];
return index;
}
WS2812FX* WS2812FX::instance = nullptr;
@@ -2061,5 +1985,5 @@ const char JSON_palette_names[] PROGMEM = R"=====([
"Magenta","Magred","Yelmag","Yelblu","Orange & Teal","Tiamat","April Night","Orangery","C9","Sakura",
"Aurora","Atlantica","C9 2","C9 New","Temperature","Aurora 2","Retro Clown","Candy","Toxy Reaf","Fairy Reaf",
"Semi Blue","Pink Candy","Red Reaf","Aqua Flash","Yelblu Hot","Lite Light","Red Flash","Blink Red","Red Shift","Red Tide",
"Candy2"
"Candy2","Traffic Light"
])=====";