Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions wled00/FX.h
Original file line number Diff line number Diff line change
Expand Up @@ -455,9 +455,8 @@ class Segment {
bool check1 : 1; // checkmark 1
bool check2 : 1; // checkmark 2
bool check3 : 1; // checkmark 3
//uint8_t blendMode : 4; // segment blending modes: top, bottom, add, subtract, difference, multiply, divide, lighten, darken, screen, overlay, hardlight, softlight, dodge, burn
};
uint8_t blendMode; // segment blending modes: top, bottom, add, subtract, difference, multiply, divide, lighten, darken, screen, overlay, hardlight, softlight, dodge, burn
uint8_t blendMode; // segment blending modes: top, bottom, add, subtract, difference, multiply, divide, lighten, darken, screen, overlay, hardlight, softlight, dodge, burn, stencil
char *name; // segment name

// runtime data
Expand Down
94 changes: 57 additions & 37 deletions wled00/FX_fcn.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ void Segment::beginDraw(uint16_t prog) {
// minimum blend time is 100ms maximum is 65535ms
#ifndef WLED_SAVE_RAM
unsigned noOfBlends = ((255U * prog) / 0xFFFFU) - _t->_prevPaletteBlends;
if(noOfBlends > 255) noOfBlends = 255; // safety check
if (noOfBlends > 255) noOfBlends = 255; // safety check
for (unsigned i = 0; i < noOfBlends; i++, _t->_prevPaletteBlends++) nblendPaletteTowardPalette(_t->_palT, Segment::_currentPalette, 48);
Segment::_currentPalette = _t->_palT; // copy transitioning/temporary palette
#else
Expand Down Expand Up @@ -1292,9 +1292,6 @@ void WS2812FX::service() {
}

// https://en.wikipedia.org/wiki/Blend_modes but using a for top layer & b for bottom layer
static uint8_t _top (uint8_t a, uint8_t b) { return a; }
static uint8_t _bottom (uint8_t a, uint8_t b) { return b; }
static uint8_t _add (uint8_t a, uint8_t b) { unsigned t = a + b; return t > 255 ? 255 : t; }
static uint8_t _subtract (uint8_t a, uint8_t b) { return b > a ? (b - a) : 0; }
static uint8_t _difference(uint8_t a, uint8_t b) { return b > a ? (b - a) : (a - b); }
static uint8_t _average (uint8_t a, uint8_t b) { return (a + b) >> 1; }
Expand All @@ -1317,23 +1314,40 @@ static uint8_t _softlight (uint8_t a, uint8_t b) { return (b * b * (255 - 2 * a)
static uint8_t _dodge (uint8_t a, uint8_t b) { return _divide(~a,b); }
static uint8_t _burn (uint8_t a, uint8_t b) { return ~_divide(a,~b); }

void WS2812FX::blendSegment(const Segment &topSegment) const {

typedef uint8_t(*FuncType)(uint8_t, uint8_t);
FuncType funcs[] = {
_top, _bottom,
_add, _subtract, _difference, _average,
_multiply, _divide, _lighten, _darken, _screen, _overlay,
_hardlight, _softlight, _dodge, _burn
};

const size_t blendMode = topSegment.blendMode < (sizeof(funcs) / sizeof(FuncType)) ? topSegment.blendMode : 0;
const auto func = funcs[blendMode]; // blendMode % (sizeof(funcs) / sizeof(FuncType))
const auto blend = [&](uint32_t top, uint32_t bottom){ return RGBW32(func(R(top),R(bottom)), func(G(top),G(bottom)), func(B(top),B(bottom)), func(W(top),W(bottom))); };
static uint32_t segblend(CRGBW tcol, CRGBW bcol, CRGBW bgcol, uint8_t blendMode) {
CRGBW tC(tcol); CRGBW bC(bcol); CRGBW c; // note: using aliases shrinks code size for some weird compiler reasons, no speed difference in tests
// note2: using CRGBW instead of uint32_t improves speed as well as code size
switch (blendMode) {
case 0: return tcol.color32; // Top
case 1: return bcol.color32; // Bottom
case 2: return color_add(tcol, bcol, false); // Add
case 3: c.r = _subtract(tC.r, bC.r); c.g = _subtract(tC.g, bC.g); c.b = _subtract(tC.b, bC.b); c.w = _subtract(tC.w, bC.w); break; // Subtract
Copy link
Member

Choose a reason for hiding this comment

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

A bit of a nitpick, but I'd recommend using an inline metafunction or a macro to do the expansion for each channel.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

that would clean up the code significantly, I will check how to do that here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ok done.

case 4: c.r = _difference(tC.r, bC.r); c.g = _difference(tC.g, bC.g); c.b = _difference(tC.b, bC.b); c.w = _difference(tC.w, bC.w); break; // Difference
case 5: c.r = _average(tC.r, bC.r); c.g = _average(tC.g, bC.g); c.b = _average(tC.b, bC.b); c.w = _average(tC.w, bC.w); break; // Average
case 6: c.r = _multiply(tC.r, bC.r); c.g = _multiply(tC.g, bC.g); c.b = _multiply(tC.b, bC.b); c.w = _multiply(tC.w, bC.w); break; // Multiply
case 7: c.r = _divide(tC.r, bC.r); c.g = _divide(tC.g, bC.g); c.b = _divide(tC.b, bC.b); c.w = _divide(tC.w, bC.w); break; // Divide
case 8: c.r = _lighten(tC.r, bC.r); c.g = _lighten(tC.g, bC.g); c.b = _lighten(tC.b, bC.b); c.w = _lighten(tC.w, bC.w); break; // Lighten
case 9: c.r = _darken(tC.r, bC.r); c.g = _darken(tC.g, bC.g); c.b = _darken(tC.b, bC.b); c.w = _darken(tC.w, bC.w); break; // Darken
case 10: c.r = _screen(tC.r, bC.r); c.g = _screen(tC.g, bC.g); c.b = _screen(tC.b, bC.b); c.w = _screen(tC.w, bC.w); break; // Screen
case 11: c.r = _overlay(tC.r, bC.r); c.g = _overlay(tC.g, bC.g); c.b = _overlay(tC.b, bC.b); c.w = _overlay(tC.w, bC.w); break; // Overlay
case 12: c.r = _hardlight(tC.r, bC.r); c.g = _hardlight(tC.g, bC.g); c.b = _hardlight(tC.b, bC.b); c.w = _hardlight(tC.w, bC.w); break; // Hardlight
case 13: c.r = _softlight(tC.r, bC.r); c.g = _softlight(tC.g, bC.g); c.b = _softlight(tC.b, bC.b); c.w = _softlight(tC.w, bC.w); break; // Softlight
case 14: c.r = _dodge(tC.r, bC.r); c.g = _dodge(tC.g, bC.g); c.b = _dodge(tC.b, bC.b); c.w = _dodge(tC.w, bC.w); break; // Dodge
case 15: c.r = _burn(tC.r, bC.r); c.g = _burn(tC.g, bC.g); c.b = _burn(tC.b, bC.b); c.w = _burn(tC.w, bC.w); break; // Burn
// note: stencil mode is a special case: it works only on full color comparison and wont work correctly on a colorchannel base
// enumerate to 32 to allow future additions above in case a function array will be used again
case 32: return tcol.color32 == bgcol.color32 ? bcol.color32 : tcol.color32; // Stencil: backgroundcolor -> transparent, use top color otherwise
default: return tcol.color32; // fallback to Top
}
return c.color32;
};

void WS2812FX::blendSegment(const Segment &topSegment) const {
const uint8_t blendMode = topSegment.blendMode;
const int length = topSegment.length(); // physical segment length (counts all pixels in 2D segment)
const int width = topSegment.width();
const int height = topSegment.height();
const uint32_t bgColor = topSegment.colors[1]; // background color for blend modes that need it (e.g. stencil)
const auto XY = [](int x, int y){ return x + y*Segment::maxWidth; };
const size_t matrixSize = Segment::maxWidth * Segment::maxHeight;
const size_t startIndx = XY(topSegment.start, topSegment.startY);
Expand Down Expand Up @@ -1408,23 +1422,24 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
const Segment *segO = topSegment.getOldSegment();
const int oCols = segO ? segO->virtualWidth() : nCols;
const int oRows = segO ? segO->virtualHeight() : nRows;
bool applyMirror = topSegment.mirror || topSegment.mirror_y;

const auto setMirroredPixel = [&](int x, int y, uint32_t c, uint8_t o) {
const int baseX = topSegment.start + x;
const int baseY = topSegment.startY + y;
size_t indx = XY(baseX, baseY); // absolute address on strip
_pixels[indx] = color_blend(_pixels[indx], blend(c, _pixels[indx]), o);
_pixels[indx] = color_blend(_pixels[indx], segblend(c, _pixels[indx], bgColor, blendMode), o);
if (_pixelCCT) _pixelCCT[indx] = cct;
// Apply mirroring
if (topSegment.mirror || topSegment.mirror_y) {
if (applyMirror) {
const int mirrorX = topSegment.start + width - x - 1;
const int mirrorY = topSegment.startY + height - y - 1;
const size_t idxMX = XY(topSegment.transpose ? baseX : mirrorX, topSegment.transpose ? mirrorY : baseY);
const size_t idxMY = XY(topSegment.transpose ? mirrorX : baseX, topSegment.transpose ? baseY : mirrorY);
const size_t idxMM = XY(mirrorX, mirrorY);
if (topSegment.mirror) _pixels[idxMX] = color_blend(_pixels[idxMX], blend(c, _pixels[idxMX]), o);
if (topSegment.mirror_y) _pixels[idxMY] = color_blend(_pixels[idxMY], blend(c, _pixels[idxMY]), o);
if (topSegment.mirror && topSegment.mirror_y) _pixels[idxMM] = color_blend(_pixels[idxMM], blend(c, _pixels[idxMM]), o);
if (topSegment.mirror) _pixels[idxMX] = color_blend(_pixels[idxMX], segblend(c, _pixels[idxMX], bgColor, blendMode), o);
if (topSegment.mirror_y) _pixels[idxMY] = color_blend(_pixels[idxMY], segblend(c, _pixels[idxMY], bgColor, blendMode), o);
if (topSegment.mirror && topSegment.mirror_y) _pixels[idxMM] = color_blend(_pixels[idxMM], segblend(c, _pixels[idxMM], bgColor, blendMode), o);
if (_pixelCCT) {
if (topSegment.mirror) _pixelCCT[idxMX] = cct;
if (topSegment.mirror_y) _pixelCCT[idxMY] = cct;
Expand All @@ -1436,7 +1451,16 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
// if we blend using "push" style we need to "shift" canvas to left/right/up/down
unsigned offsetX = (blendingStyle == BLEND_STYLE_PUSH_UP || blendingStyle == BLEND_STYLE_PUSH_DOWN) ? 0 : progInv * nCols / 0xFFFFU;
unsigned offsetY = (blendingStyle == BLEND_STYLE_PUSH_LEFT || blendingStyle == BLEND_STYLE_PUSH_RIGHT) ? 0 : progInv * nRows / 0xFFFFU;

const unsigned groupLen = topSegment.groupLength();
bool applyReverse = topSegment.reverse || topSegment.reverse_y || topSegment.transpose;
int pushOffsetX = 0, pushOffsetY = 0;
// if we blend using "push" style we need to "shift" canvas to left/right/up/down
switch (blendingStyle) {
case BLEND_STYLE_PUSH_RIGHT: pushOffsetX = offsetX; break;
case BLEND_STYLE_PUSH_LEFT: pushOffsetX = -offsetX + nCols; break;
case BLEND_STYLE_PUSH_DOWN: pushOffsetY = offsetY; break;
case BLEND_STYLE_PUSH_UP: pushOffsetY = -offsetY + nRows; break;
}
// we only traverse new segment, not old one
for (int r = 0; r < nRows; r++) for (int c = 0; c < nCols; c++) {
const bool clipped = topSegment.isPixelXYClipped(c, r);
Expand All @@ -1446,13 +1470,8 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
int vRows = seg == segO ? oRows : nRows; // old segment may have different dimensions
int x = c;
int y = r;
// if we blend using "push" style we need to "shift" canvas to left/right/up/down
switch (blendingStyle) {
case BLEND_STYLE_PUSH_RIGHT: x = (x + offsetX) % nCols; break;
case BLEND_STYLE_PUSH_LEFT: x = (x - offsetX + nCols) % nCols; break;
case BLEND_STYLE_PUSH_DOWN: y = (y + offsetY) % nRows; break;
case BLEND_STYLE_PUSH_UP: y = (y - offsetY + nRows) % nRows; break;
}
if (pushOffsetX != 0) x = (x + pushOffsetX) % nCols;
if (pushOffsetY != 0) y = (y + pushOffsetY) % nRows;
uint32_t c_a = BLACK;
if (x < vCols && y < vRows) c_a = seg->getPixelColorRaw(x + y*vCols); // will get clipped pixel from old segment or unclipped pixel from new segment
if (segO && blendingStyle == BLEND_STYLE_FADE
Expand All @@ -1469,11 +1488,12 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
// map it into frame buffer
x = c; // restore coordiates if we were PUSHing
y = r;
if (topSegment.reverse ) x = nCols - x - 1;
if (topSegment.reverse_y) y = nRows - y - 1;
if (topSegment.transpose) std::swap(x,y); // swap X & Y if segment transposed
if (applyReverse) {
if (topSegment.reverse ) x = nCols - x - 1;
if (topSegment.reverse_y) y = nRows - y - 1;
if (topSegment.transpose) std::swap(x,y); // swap X & Y if segment transposed
}
// expand pixel
const unsigned groupLen = topSegment.groupLength();
if (groupLen == 1) {
setMirroredPixel(x, y, c_a, opacity);
} else {
Expand Down Expand Up @@ -1502,12 +1522,12 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
unsigned indxM = topSegment.stop - i - 1;
indxM += topSegment.offset; // offset/phase
if (indxM >= topSegment.stop) indxM -= length; // wrap
_pixels[indxM] = color_blend(_pixels[indxM], blend(c, _pixels[indxM]), o);
_pixels[indxM] = color_blend(_pixels[indxM], segblend(c, _pixels[indxM], bgColor, blendMode), o);
if (_pixelCCT) _pixelCCT[indxM] = cct;
}
indx += topSegment.offset; // offset/phase
if (indx >= topSegment.stop) indx -= length; // wrap
_pixels[indx] = color_blend(_pixels[indx], blend(c, _pixels[indx]), o);
_pixels[indx] = color_blend(_pixels[indx], segblend(c, _pixels[indx], bgColor, blendMode), o);
if (_pixelCCT) _pixelCCT[indx] = cct;
};

Expand Down Expand Up @@ -1590,7 +1610,7 @@ void WS2812FX::show() {
}

uint32_t c = _pixels[i]; // need a copy, do not modify _pixels directly (no byte access allowed on ESP32)
if(c > 0 && !(realtimeMode && arlsDisableGammaCorrection))
if (c > 0 && !(realtimeMode && arlsDisableGammaCorrection))
c = gamma32(c); // apply gamma correction if enabled note: applying gamma after brightness has too much color loss
BusManager::setPixelColor(getMappedPixelIndex(i), c);
}
Expand Down
3 changes: 2 additions & 1 deletion wled00/data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,7 @@ function populateSegments(s)
`<option value="13" ${inst.bm==13?' selected':''}>Soft Light</option>`+
`<option value="14" ${inst.bm==14?' selected':''}>Dodge</option>`+
`<option value="15" ${inst.bm==15?' selected':''}>Burn</option>`+
`<option value="32" ${inst.bm==32?' selected':''}>Stencil</option>`+
`</select></div>`+
`</div>`;
let sndSim = `<div data-snd="si" class="lbl-s hide">Sound sim<br>`+
Expand Down Expand Up @@ -2358,7 +2359,7 @@ function setSi(s)

function setBm(s)
{
var value = gId(`seg${s}bm`).selectedIndex;
var value = gId(`seg${s}bm`).value;
var obj = {"seg": {"id": s, "bm": value}};
requestJson(obj);
}
Expand Down
4 changes: 1 addition & 3 deletions wled00/json.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -302,9 +302,7 @@ static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0)
seg.check2 = getBoolVal(elem["o2"], seg.check2);
seg.check3 = getBoolVal(elem["o3"], seg.check3);

uint8_t blend = seg.blendMode;
getVal(elem["bm"], blend, 0, 15); // we can't pass reference to bitfield
seg.blendMode = constrain(blend, 0, 15);
getVal(elem["bm"], seg.blendMode);

JsonArray iarr = elem[F("i")]; //set individual LEDs
if (!iarr.isNull()) {
Expand Down