Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Effect blending styles #3877

Open
wants to merge 16 commits into
base: 0_15
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 28 additions & 0 deletions wled00/FX.h
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,25 @@

#define MODE_COUNT 187


#define BLEND_STYLE_FADE 0
#define BLEND_STYLE_FAIRY_DUST 1
#define BLEND_STYLE_SWIPE_RIGHT 2
#define BLEND_STYLE_SWIPE_LEFT 3
#define BLEND_STYLE_PINCH_OUT 4
#define BLEND_STYLE_INSIDE_OUT 5
#define BLEND_STYLE_SWIPE_UP 6
#define BLEND_STYLE_SWIPE_DOWN 7
#define BLEND_STYLE_OPEN_H 8
#define BLEND_STYLE_OPEN_V 9
#define BLEND_STYLE_PUSH_TL 10
#define BLEND_STYLE_PUSH_TR 11
#define BLEND_STYLE_PUSH_BR 12
#define BLEND_STYLE_PUSH_BL 13

#define BLEND_STYLE_COUNT 14


typedef enum mapping1D2D {
M12_Pixels = 0,
M12_pBar = 1,
Expand Down Expand Up @@ -419,6 +438,9 @@ typedef struct Segment {
static uint16_t _lastPaletteBlend; // blend palette according to set Transition Delay in millis()%0xFFFF
#ifndef WLED_DISABLE_MODE_BLEND
static bool _modeBlend; // mode/effect blending semaphore
// clipping
static uint16_t _clipStart, _clipStop;
static uint8_t _clipStartY, _clipStopY;
#endif

// transition data, valid only if transitional==true, holds values during transition (72 bytes)
Expand Down Expand Up @@ -578,6 +600,10 @@ typedef struct Segment {
void setPixelColor(float i, uint32_t c, bool aa = true);
inline void setPixelColor(float i, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0, bool aa = true) { setPixelColor(i, RGBW32(r,g,b,w), aa); }
inline void setPixelColor(float i, CRGB c, bool aa = true) { setPixelColor(i, RGBW32(c.r,c.g,c.b,0), aa); }
#ifndef WLED_DISABLE_MODE_BLEND
inline void setClippingRect(int startX, int stopX, int startY = 0, int stopY = 1) { _clipStart = startX; _clipStop = stopX; _clipStartY = startY; _clipStopY = stopY; };
#endif
bool isPixelClipped(int i);
uint32_t getPixelColor(int i);
// 1D support functions (some implement 2D as well)
void blur(uint8_t);
Expand Down Expand Up @@ -606,6 +632,7 @@ typedef struct Segment {
void setPixelColorXY(float x, float y, uint32_t c, bool aa = true);
inline void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) { setPixelColorXY(x, y, RGBW32(r,g,b,w), aa); }
inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), aa); }
bool isPixelXYClipped(int x, int y);
uint32_t getPixelColorXY(uint16_t x, uint16_t y);
// 2D support functions
inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t color, uint8_t blend) { setPixelColorXY(x, y, color_blend(getPixelColorXY(x,y), color, blend)); }
Expand Down Expand Up @@ -640,6 +667,7 @@ typedef struct Segment {
inline void setPixelColorXY(float x, float y, uint32_t c, bool aa = true) { setPixelColor(x, c, aa); }
inline void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) { setPixelColor(x, RGBW32(r,g,b,w), aa); }
inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) { setPixelColor(x, RGBW32(c.r,c.g,c.b,0), aa); }
inline bool isPixelXYClipped(int x, int y) { return isPixelClipped(x); }
inline uint32_t getPixelColorXY(uint16_t x, uint16_t y) { return getPixelColor(x); }
inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t c, uint8_t blend) { blendPixelColor(x, c, blend); }
inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) { blendPixelColor(x, RGBW32(c.r,c.g,c.b,0), blend); }
Expand Down
33 changes: 32 additions & 1 deletion wled00/FX_2Dfcn.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,41 @@ uint16_t IRAM_ATTR Segment::XY(uint16_t x, uint16_t y)
return isActive() ? (x%width) + (y%height) * width : 0;
}

// pixel is clipped if it falls outside clipping range (_modeBlend==true) or is inside clipping range (_modeBlend==false)
// if clipping start > stop the clipping range is inverted
// _modeBlend==true -> old effect during transition
// _modeBlend==false -> new effect during transition
bool IRAM_ATTR Segment::isPixelXYClipped(int x, int y) {
#ifndef WLED_DISABLE_MODE_BLEND
if (_clipStart != _clipStop && blendingStyle > BLEND_STYLE_FADE) {
const bool invertX = _clipStart > _clipStop;
const bool invertY = _clipStartY > _clipStopY;
const unsigned startX = invertX ? _clipStop : _clipStart;
const unsigned stopX = invertX ? _clipStart : _clipStop;
const unsigned startY = invertY ? _clipStopY : _clipStartY;
const unsigned stopY = invertY ? _clipStartY : _clipStopY;
if (blendingStyle == BLEND_STYLE_FAIRY_DUST) {
const unsigned width = stopX - startX; // assumes full segment width (faster than virtualWidth())
const unsigned len = width * (stopY - startY); // assumes full segment height (faster than virtualHeight())
if (len < 2) return false;
const unsigned shuffled = hashInt(x + y * width) % len;
const unsigned pos = (shuffled * 0xFFFFU) / len;
return progress() <= pos;
}
bool xInside = (x >= startX && x < stopX); if (invertX) xInside = !xInside;
bool yInside = (y >= startY && y < stopY); if (invertY) yInside = !yInside;
const bool clip = (invertX && invertY) ? !_modeBlend : _modeBlend;
if (xInside && yInside) return clip; // covers window & corners (inverted)
return !clip;
}
#endif
return false;
}

void IRAM_ATTR Segment::setPixelColorXY(int x, int y, uint32_t col)
{
if (!isActive()) return; // not active
if (x >= virtualWidth() || y >= virtualHeight() || x<0 || y<0) return; // if pixel would fall out of virtual segment just exit
if (x >= virtualWidth() || y >= virtualHeight() || x < 0 || y < 0 || isPixelXYClipped(x,y)) return; // if pixel would fall out of virtual segment just exit

uint8_t _bri_t = currentBri();
if (_bri_t < 255) {
Expand Down
125 changes: 113 additions & 12 deletions wled00/FX_fcn.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ uint16_t Segment::_lastPaletteBlend = 0; //in millis (lowest 16 bits only)

#ifndef WLED_DISABLE_MODE_BLEND
bool Segment::_modeBlend = false;
uint16_t Segment::_clipStart = 0;
uint16_t Segment::_clipStop = 0;
uint8_t Segment::_clipStartY = 0;
uint8_t Segment::_clipStopY = 1;
#endif

// copy constructor
Expand Down Expand Up @@ -413,12 +417,19 @@ void Segment::restoreSegenv(tmpsegd_t &tmpSeg) {

uint8_t IRAM_ATTR Segment::currentBri(bool useCct) {
uint32_t prog = progress();
uint32_t curBri = useCct ? cct : (on ? opacity : 0);
if (prog < 0xFFFFU) {
uint32_t curBri = (useCct ? cct : (on ? opacity : 0)) * prog;
curBri += (useCct ? _t->_cctT : _t->_briT) * (0xFFFFU - prog);
#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
#else
uint8_t tmpBri = useCct ? _t->_cctT : _t->_briT;
#endif
curBri *= prog;
curBri += tmpBri * (0xFFFFU - prog);
return curBri / 0xFFFFU;
}
return (useCct ? cct : (on ? opacity : 0));
return curBri;
}

uint8_t IRAM_ATTR Segment::currentMode() {
Expand All @@ -431,16 +442,23 @@ uint8_t IRAM_ATTR Segment::currentMode() {

uint32_t IRAM_ATTR Segment::currentColor(uint8_t slot) {
if (slot >= NUM_COLORS) slot = 0;
uint32_t prog = progress();
if (prog == 0xFFFFU) return colors[slot];
#ifndef WLED_DISABLE_MODE_BLEND
return isInTransition() ? color_blend(_t->_segT._colorT[slot], colors[slot], progress(), true) : colors[slot];
if (blendingStyle > BLEND_STYLE_FADE) return _modeBlend ? _t->_segT._colorT[slot] : colors[slot]; // not fade/blend transition, each effect uses its color
return color_blend(_t->_segT._colorT[slot], colors[slot], prog, true);
#else
return isInTransition() ? color_blend(_t->_colorT[slot], colors[slot], progress(), true) : colors[slot];
return color_blend(_t->_colorT[slot], colors[slot], prog, true);
#endif
}

CRGBPalette16 IRAM_ATTR &Segment::currentPalette(CRGBPalette16 &targetPalette, uint8_t pal) {
loadPalette(targetPalette, pal);
uint16_t prog = progress();
#ifndef WLED_DISABLE_MODE_BLEND
if (prog < 0xFFFFU && blendingStyle > BLEND_STYLE_FADE && _modeBlend) targetPalette = _t->_palT; // not fade/blend transition, each effect uses its palette
else
#endif
if (strip.paletteFade && prog < 0xFFFFU) {
// blend palettes
// there are about 255 blend passes of 48 "blends" to completely blend two palettes (in _dur time)
Expand All @@ -456,9 +474,9 @@ CRGBPalette16 IRAM_ATTR &Segment::currentPalette(CRGBPalette16 &targetPalette, u
void Segment::handleRandomPalette() {
// is it time to generate a new palette?
if ((millis()/1000U) - _lastPaletteChange > randomPaletteChangeTime) {
_newRandomPalette = useHarmonicRandomPalette ? generateHarmonicRandomPalette(_randomPalette) : generateRandomPalette();
_lastPaletteChange = millis()/1000U;
_lastPaletteBlend = (uint16_t)(millis() & 0xFFFF)-512; // starts blending immediately
_newRandomPalette = useHarmonicRandomPalette ? generateHarmonicRandomPalette(_randomPalette) : generateRandomPalette();
_lastPaletteChange = millis()/1000U;
_lastPaletteBlend = (uint16_t)(millis() & 0xFFFF)-512; // starts blending immediately
}

// if palette transitions is enabled, blend it according to Transition Time (if longer than minimum given by service calls)
Expand Down Expand Up @@ -662,6 +680,32 @@ uint16_t IRAM_ATTR Segment::virtualLength() const {
return vLength;
}

// pixel is clipped if it falls outside clipping range (_modeBlend==true) or is inside clipping range (_modeBlend==false)
// if clipping start > stop the clipping range is inverted
// _modeBlend==true -> old effect during transition
// _modeBlend==false -> new effect during transition
bool IRAM_ATTR Segment::isPixelClipped(int i) {
blazoncek marked this conversation as resolved.
Show resolved Hide resolved
#ifndef WLED_DISABLE_MODE_BLEND
if (_clipStart != _clipStop && blendingStyle > BLEND_STYLE_FADE) {
bool invert = _clipStart > _clipStop;
unsigned start = invert ? _clipStop : _clipStart;
unsigned stop = invert ? _clipStart : _clipStop;
if (blendingStyle == BLEND_STYLE_FAIRY_DUST) {
unsigned len = stop - start;
if (len < 2) return false;
unsigned shuffled = hashInt(i) % len;
unsigned pos = (shuffled * 0xFFFFU) / len;
return progress() <= pos;
blazoncek marked this conversation as resolved.
Show resolved Hide resolved
}
const bool iInside = (i >= start && i < stop);
if (!invert && iInside) return _modeBlend;
if ( invert && !iInside) return _modeBlend;
return !_modeBlend;
blazoncek marked this conversation as resolved.
Show resolved Hide resolved
}
#endif
return false;
}

void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col)
{
if (!isActive()) return; // not active
Expand Down Expand Up @@ -732,6 +776,8 @@ void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col)
}
#endif

if (isPixelClipped(i)) return; // handle clipping on 1D

uint16_t len = length();
uint8_t _bri_t = currentBri();
if (_bri_t < 255) {
Expand Down Expand Up @@ -763,14 +809,16 @@ void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col)
indexMir += offset; // offset/phase
if (indexMir >= stop) indexMir -= len; // wrap
#ifndef WLED_DISABLE_MODE_BLEND
if (_modeBlend) tmpCol = color_blend(strip.getPixelColor(indexMir), col, 0xFFFFU - progress(), true);
// _modeBlend==true -> old effect
if (_modeBlend && blendingStyle == BLEND_STYLE_FADE) tmpCol = color_blend(strip.getPixelColor(indexMir), col, 0xFFFFU - progress(), true);
#endif
strip.setPixelColor(indexMir, tmpCol);
}
indexSet += offset; // offset/phase
if (indexSet >= stop) indexSet -= len; // wrap
#ifndef WLED_DISABLE_MODE_BLEND
if (_modeBlend) tmpCol = color_blend(strip.getPixelColor(indexSet), col, 0xFFFFU - progress(), true);
// _modeBlend==true -> old effect
if (_modeBlend && blendingStyle == BLEND_STYLE_FADE) tmpCol = color_blend(strip.getPixelColor(indexSet), col, 0xFFFFU - progress(), true);
#endif
strip.setPixelColor(indexSet, tmpCol);
}
Expand Down Expand Up @@ -1062,7 +1110,7 @@ uint32_t Segment::color_from_palette(uint16_t i, bool mapping, bool wrap, uint8_
// 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"
CRGBPalette16 curPal;
curPal = currentPalette(curPal, palette);
currentPalette(curPal, palette);
CRGB fastled_col = ColorFromPalette(curPal, paletteIndex, pbri, (strip.paletteBlend == 3)? NOBLEND:LINEARBLEND); // NOTE: paletteBlend should be global

return RGBW32(fastled_col.r, fastled_col.g, fastled_col.b, W(color));
Expand Down Expand Up @@ -1185,8 +1233,59 @@ void WS2812FX::service() {
// 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.
[[maybe_unused]] uint8_t tmpMode = seg.currentMode(); // this will return old mode while in transition
delay = (*_mode[seg.mode])(); // run new/current mode
#ifndef WLED_DISABLE_MODE_BLEND
seg.setClippingRect(0, 0); // disable clipping
blazoncek marked this conversation as resolved.
Show resolved Hide resolved
if (modeBlending && seg.mode != tmpMode) {
// 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 dw = p * w / 0xFFFFU + 1;
unsigned dh = p * h / 0xFFFFU + 1;
switch (blendingStyle) {
case BLEND_STYLE_FAIRY_DUST: // fairy dust (must set entire segment, see isPixelXYClipped())
seg.setClippingRect(0, w, 0, h);
break;
case BLEND_STYLE_SWIPE_RIGHT: // left-to-right
seg.setClippingRect(0, dw, 0, h);
break;
case BLEND_STYLE_SWIPE_LEFT: // right-to-left
seg.setClippingRect(w - dw, w, 0, h);
break;
case BLEND_STYLE_PINCH_OUT: // corners
seg.setClippingRect((w + dw)/2, (w - dw)/2, (h + dh)/2, (h - dh)/2); // inverted!!
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend making the inverted case an explicit parameter to setClippingRect. You can still encode it internally, but it'd make the API easier to follow

break;
case BLEND_STYLE_INSIDE_OUT: // outward
seg.setClippingRect((w - dw)/2, (w + dw)/2, (h - dh)/2, (h + dh)/2);
break;
case BLEND_STYLE_SWIPE_DOWN: // top-to-bottom (2D)
seg.setClippingRect(0, w, 0, dh);
break;
case BLEND_STYLE_SWIPE_UP: // bottom-to-top (2D)
seg.setClippingRect(0, w, h - dh, h);
break;
case BLEND_STYLE_OPEN_H: // horizontal-outward (2D) same look as INSIDE_OUT on 1D
seg.setClippingRect((w - dw)/2, (w + dw)/2, 0, h);
break;
case BLEND_STYLE_OPEN_V: // vertical-outward (2D)
seg.setClippingRect(0, w, (h - dh)/2, (h + dh)/2);
break;
case BLEND_STYLE_PUSH_TL: // TL-to-BR (2D)
seg.setClippingRect(0, dw, 0, dh);
break;
case BLEND_STYLE_PUSH_TR: // TR-to-BL (2D)
seg.setClippingRect(w - dw, w, 0, dh);
break;
case BLEND_STYLE_PUSH_BR: // BR-to-TL (2D)
seg.setClippingRect(w - dw, w, h - dh, h);
break;
case BLEND_STYLE_PUSH_BL: // BL-to-TR (2D)
seg.setClippingRect(0, dw, h - dh, h);
break;
}
}
delay = (*_mode[seg.mode])(); // run new/current mode
if (modeBlending && seg.mode != tmpMode) {
Segment::tmpsegd_t _tmpSegData;
Segment::modeBlend(true); // set semaphore
Expand All @@ -1197,6 +1296,8 @@ void WS2812FX::service() {
delay = MIN(delay,d2); // use shortest delay
Segment::modeBlend(false); // unset semaphore
}
#else
delay = (*_mode[seg.mode])(); // run effect mode
#endif
seg.call++;
if (seg.isInTransition() && delay > FRAMETIME) delay = FRAMETIME; // force faster updates during transition
Expand Down
20 changes: 19 additions & 1 deletion wled00/data/index.htm
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,25 @@
<div id="segutil2">
<button class="btn btn-s" id="rsbtn" onclick="rSegs()">Reset segments</button>
</div>
<p>Transition: <input id="tt" type="number" min="0" max="65.5" step="0.1" value="0.7">&nbsp;s</p>
<p>Transition: <input id="tt" type="number" min="0" max="65.5" step="0.1" value="0.7" onchange="parseFloat(this.value)===0?gId('bsp').classList.add('hide'):gId('bsp').classList.remove('hide');">&nbsp;s</p>
<p id="bsp">Blend:
<select id="bs" class="sel-sg" onchange="requestJson({'bs':parseInt(this.value)})">
<option value="0">Fade</option>
<option value="1">Fairy Dust</option>
<option value="2">Swipe left</option>
<option value="3">Swipe right</option>
<option value="4">Pinch-out</option>
<option value="5">Inside-out</option>
<option value="6">Swipe up (2D)</option>
<option value="7">Swipe down (2D)</option>
<option value="8">Open V (2D)</option>
<option value="9">Open H (2D)</option>
<option value="10">Push TL (2D)</option>
<option value="11">Push TR (2D)</option>
<option value="12">Push BR (2D)</option>
<option value="13">Push BL (2D)</option>
</select>
</p>
<p id="ledmap" class="hide"></p>
</div>

Expand Down
4 changes: 4 additions & 0 deletions wled00/data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1426,6 +1426,9 @@ function readState(s,command=false)

tr = s.transition;
gId('tt').value = tr/10;
gId('bs').value = s.bs || 0;
if (tr===0) gId('bsp').classList.add('hide')
else gId('bsp').classList.remove('hide')

populateSegments(s);
var selc=0;
Expand Down Expand Up @@ -1682,6 +1685,7 @@ function requestJson(command=null)
var tn = parseInt(t.value*10);
if (tn != tr) command.transition = tn;
}
//command.bs = parseInt(gId('bs').value);
req = JSON.stringify(command);
if (req.length > 1340) useWs = false; // do not send very long requests over websocket
if (req.length > 500 && lastinfo && lastinfo.arch == "esp8266") useWs = false; // esp8266 can only handle 500 bytes
Expand Down
1 change: 1 addition & 0 deletions wled00/fcn_declare.h
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ uint16_t crc16(const unsigned char* data_p, size_t length);
um_data_t* simulateSound(uint8_t simulationId);
void enumerateLedmaps();
uint8_t get_random_wheel_index(uint8_t pos);
uint32_t hashInt(uint32_t s);

// RAII guard class for the JSON Buffer lock
// Modeled after std::lock_guard
Expand Down
8 changes: 8 additions & 0 deletions wled00/json.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,11 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId)
}
}

#ifndef WLED_DISABLE_MODE_BLEND
blendingStyle = root[F("bs")] | blendingStyle;
blendingStyle = constrain(blendingStyle, 0, BLEND_STYLE_COUNT-1);
#endif

// temporary transition (applies only once)
tr = root[F("tt")] | -1;
if (tr >= 0) {
Expand Down Expand Up @@ -581,6 +586,9 @@ void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segme
root["on"] = (bri > 0);
root["bri"] = briLast;
root[F("transition")] = transitionDelay/100; //in 100ms
#ifndef WLED_DISABLE_MODE_BLEND
root[F("bs")] = blendingStyle;
#endif
}

if (!forPreset) {
Expand Down