Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
11 changes: 9 additions & 2 deletions wled00/FX.h
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ typedef enum mapping1D2D {

class WS2812FX;

// segment, 76 bytes
// segment, 80 bytes
class Segment {
public:
uint32_t colors[NUM_COLORS];
Expand Down Expand Up @@ -460,9 +460,12 @@ 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
struct {
uint8_t zoomAmount : 4; // zoom amount (0-15); 8 == no zoom
uint8_t rotateSpeed : 4; // rotation speed (0-15); 0 == no rotation
};
char *name; // segment name

// runtime data
Expand All @@ -488,6 +491,7 @@ class Segment {
bool _manualW : 1;
};
};
mutable uint16_t _rotatedAngle; // current rotation angle (2D)

// static variables are use to speed up effect calculations by stashing common pre-calculated values
static unsigned _usedSegmentData; // amount of data used by all segments
Expand Down Expand Up @@ -591,6 +595,8 @@ class Segment {
, check2(false)
, check3(false)
, blendMode(0)
, zoomAmount(8)
, rotateSpeed(0)
, name(nullptr)
, next_time(0)
, step(0)
Expand All @@ -601,6 +607,7 @@ class Segment {
, _dataLen(0)
, _default_palette(6)
, _capabilities(0)
, _rotatedAngle(0)
, _t(nullptr)
{
DEBUGFX_PRINTF_P(PSTR("-- Creating segment: %p [%d,%d:%d,%d]\n"), this, (int)start, (int)stop, (int)startY, (int)stopY);
Expand Down
120 changes: 111 additions & 9 deletions wled00/FX_fcn.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) {
sOpt = extractModeDefaults(fx, "ix"); intensity = (sOpt >= 0) ? sOpt : DEFAULT_INTENSITY;
sOpt = extractModeDefaults(fx, "c1"); custom1 = (sOpt >= 0) ? sOpt : DEFAULT_C1;
sOpt = extractModeDefaults(fx, "c2"); custom2 = (sOpt >= 0) ? sOpt : DEFAULT_C2;
sOpt = extractModeDefaults(fx, "c3"); custom3 = (sOpt >= 0) ? sOpt : DEFAULT_C3;
sOpt = extractModeDefaults(fx, "c3"); custom3 = (sOpt >= 0) ? constrain(sOpt, 0, 31) : DEFAULT_C3;
sOpt = extractModeDefaults(fx, "o1"); check1 = (sOpt >= 0) ? (bool)sOpt : false;
sOpt = extractModeDefaults(fx, "o2"); check2 = (sOpt >= 0) ? (bool)sOpt : false;
sOpt = extractModeDefaults(fx, "o3"); check3 = (sOpt >= 0) ? (bool)sOpt : false;
Expand All @@ -573,6 +573,8 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) {
sOpt = extractModeDefaults(fx, "mi"); if (sOpt >= 0) mirror = (bool)sOpt; // NOTE: setting this option is a risky business
sOpt = extractModeDefaults(fx, "rY"); if (sOpt >= 0) reverse_y = (bool)sOpt;
sOpt = extractModeDefaults(fx, "mY"); if (sOpt >= 0) mirror_y = (bool)sOpt; // NOTE: setting this option is a risky business
sOpt = extractModeDefaults(fx, "rS"); if (sOpt >= 0) rotateSpeed = constrain(sOpt, 0, 15); // 0 = no rotation
sOpt = extractModeDefaults(fx, "zA"); if (sOpt >= 0) zoomAmount = constrain(sOpt, 0, 15); // 8 = no zoom
}
sOpt = extractModeDefaults(fx, "pal"); // always extract 'pal' to set _default_palette
if (sOpt >= 0 && loadDefaults) setPalette(sOpt);
Expand Down Expand Up @@ -1474,9 +1476,109 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
}
};

// zooming and rotation
auto RotateAndZoom = [](uint32_t *srcPixels, uint32_t *destPixels, int midX, int midY, int cols, int rows, int shearAngle, int zoomOffset) {
for (int i = 0; i < cols * rows; i++) destPixels[i] = BLACK; // fill black

constexpr uint8_t Scale_Shift = 10;
constexpr int Fixed_Scale = (1 << Scale_Shift);
constexpr int RoundVal = (1 << (Scale_Shift - 1));
constexpr int zoomRange = (Fixed_Scale * 3) / 4; // 768
int zoomScale = Fixed_Scale + (zoomOffset * zoomRange) / 8; // zoomOffset: -8 .. +7 -> zoomScale: 256 .. 1696
if (zoomScale <= 0) zoomScale = 1; // avoid divide-by-zero and negative zoom

const bool flip = (shearAngle > 90 && shearAngle < 270); // Flip to avoid instability near 180°
if (flip) shearAngle = (shearAngle + 180) % 360;

// Calculate shearX and shearY
const float angleRadians = radians(shearAngle);
int shearX = -tan_t(angleRadians / 2) * Fixed_Scale;
int shearY = sin_t(angleRadians) * Fixed_Scale;

const int WRAP_PAD_X = cols << 5; // ×32
const int WRAP_PAD_Y = rows << 5; // Ensures wrap works with large negative coordinates when zoomed out

// Use inverse mapping: iterate destination pixels, find source coordinates
for (int destY = 0; destY < rows; destY++) {
for (int destX = 0; destX < cols; destX++) {
// Translate destination to origin
int dx = destX - midX;
int dy = destY - midY;

// Inverse shear transformations (reverse order)
int x1 = dx - ((shearX * dy + RoundVal) >> Scale_Shift);
int y0 = dy - ((shearY * x1 + RoundVal) >> Scale_Shift);
int x0 = x1 - ((shearX * y0 + RoundVal) >> Scale_Shift);

// Apply zoom to source coordinates
x0 = (x0 * Fixed_Scale) / zoomScale;
y0 = (y0 * Fixed_Scale) / zoomScale;

// Handle flip
int srcX = flip ? (midX - x0) : (midX + x0);
int srcY = flip ? (midY - y0) : (midY + y0);

// Bounds check or wrap
//if (wrap) { // Wrap around
srcX = (srcX + WRAP_PAD_X); while (srcX >= cols) srcX -= cols; // positive modulo since % is slow
srcY = (srcY + WRAP_PAD_Y); while (srcY >= rows) srcY -= rows; // positive modulo since % is slow
//}
//else if (wrap_and_mirror) { // Wrap plus mirror
// int tileX = (srcX + WRAP_PAD_X) / cols;
// int tileY = (srcY + WRAP_PAD_Y) / rows;

// // Wrap src
// srcX = (srcX + WRAP_PAD_X); while (srcX >= cols) srcX -= cols; // positive modulo since % is slow
// srcY = (srcY + WRAP_PAD_Y); while (srcY >= rows) srcY -= rows; // positive modulo since % is slow

// // Flip on odd tiles
// if (tileX & 1) srcX = cols - 1 - srcX;
// if (tileY & 1) srcY = rows - 1 - srcY;
//}
//else
if ((unsigned)srcX >= (unsigned)cols || (unsigned)srcY >= (unsigned)rows) continue;

// Sample from source & write to destination
destPixels[destX + destY * cols] = srcPixels[srcX + srcY * cols];
}
}
};

uint32_t *_pixelsN = topSegment.getPixels(); // we will use this pointer as a source later insetad of getPixelColorRaw()
if (topSegment.rotateSpeed || topSegment.zoomAmount != 8) {
_pixelsN = new uint32_t[nCols * nRows]; // may use allocateBuffer() if needed
const int midX = nCols / 2;
const int midY = nRows / 2;
if (topSegment.rotateSpeed != 0) {
topSegment._rotatedAngle += topSegment.rotateSpeed;
while (topSegment._rotatedAngle > 3600) topSegment._rotatedAngle -= 3600;
} else {
topSegment._rotatedAngle = 0; // reset angle if no rotation
}
RotateAndZoom(topSegment.getPixels(), _pixelsN, midX, midY, nCols, nRows, topSegment._rotatedAngle/10, topSegment.zoomAmount - 8);
}
uint32_t *_pixelsO = topSegment.getPixels(); // we will use this pointer as a source (old segment during transition) later insetad of getPixelColorRaw()
if (segO) {
_pixelsO = segO->getPixels(); // default to unmodified old segment pixels
if (segO->rotateSpeed || segO->zoomAmount != 8) {
_pixelsO = new uint32_t[oCols * oRows]; // may use allocateBuffer() if needed
const int midXo = oCols / 2;
const int midYo = oRows / 2;
if (segO->rotateSpeed != 0) {
segO->_rotatedAngle += segO->rotateSpeed;
while (segO->_rotatedAngle > 3600) segO->_rotatedAngle -= 3600;
} else {
segO->_rotatedAngle = 0;
}
RotateAndZoom(segO->getPixels(), _pixelsO, midXo, midYo, oCols, oRows, segO->_rotatedAngle/10, segO->zoomAmount - 8);
}
}

// 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;
if (blendingStyle == BLEND_STYLE_PUSH_RIGHT) offsetX = nCols - offsetX;
if (blendingStyle == BLEND_STYLE_PUSH_UP) offsetY = nRows - offsetY;

// we only traverse new segment, not old one
for (int r = 0; r < nRows; r++) for (int c = 0; c < nCols; c++) {
Expand All @@ -1485,22 +1587,19 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
const Segment *seg = clipped && segO ? segO : &topSegment; // pixel is never clipped for FADE
int vCols = seg == segO ? oCols : nCols; // old segment may have different dimensions
int vRows = seg == segO ? oRows : nRows; // old segment may have different dimensions
uint32_t *_pixelsR = seg == segO ? _pixelsO : _pixelsN;
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 (offsetX != 0) { x = (x + offsetX); while (x >= nCols) x -= nCols; }
if (offsetY != 0) { y = (y + offsetY); while (y >= nRows) y -= 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 (x < vCols && y < vRows) c_a = _pixelsR[x + y*vCols]; // will get clipped pixel from old segment or unclipped pixel from new segment
if (segO && blendingStyle == BLEND_STYLE_FADE
&& (topSegment.mode != segO->mode || (segO->name != topSegment.name && segO->name && topSegment.name && strncmp(segO->name, topSegment.name, WLED_MAX_SEGNAME_LEN) != 0))
&& x < oCols && y < oRows) {
// we need to blend old segment using fade as pixels are not clipped
c_a = color_blend16(c_a, segO->getPixelColorRaw(x + y*oCols), progInv);
c_a = color_blend16(c_a, _pixelsO[x + y*oCols], progInv);
} else if (blendingStyle != BLEND_STYLE_FADE) {
// if we have global brightness change (not On/Off change) we will ignore transition style and just fade brightness (see led.cpp)
// workaround for On/Off transition
Expand Down Expand Up @@ -1531,6 +1630,9 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
}
}
}
// clean up
if (topSegment.rotateSpeed || topSegment.zoomAmount != 8) delete[] _pixelsN;
if (segO && (segO->rotateSpeed || segO->zoomAmount != 8)) delete[] _pixelsO;
#endif
} else {
const int nLen = topSegment.virtualLength();
Expand Down
66 changes: 52 additions & 14 deletions wled00/data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -741,7 +741,21 @@ function populateSegments(s)
let segp = `<div id="segp${i}" class="sbs">`+
`<i class="icons slider-icon pwr ${inst.on ? "act":""}" id="seg${i}pwr" title="Power" onclick="setSegPwr(${i})">&#xe08f;</i>`+
`<div class="sliderwrap il" title="Opacity/Brightness">`+
`<input id="seg${i}bri" class="noslide" onchange="setSegBri(${i})" oninput="updateTrail(this)" max="255" min="1" type="range" value="${inst.bri}" />`+
`<input id="seg${i}bri" class="noslide" onchange="setSegProp(${i},'bri')" oninput="updateTrail(this)" max="255" min="1" type="range" value="${inst.bri}" />`+
`<div class="sliderdisplay"></div>`+
`</div>`+
`</div>`;
let zoom = `<div id="segzm${i}" class="lbl-l">`+
`Zoom<br>`+
`<div class="sliderwrap il" title="Zoom amount">`+
`<input id="seg${i}zA" class="noslide" onchange="setSegProp(${i},'zA')" oninput="updateTrail(this)" max="15" min="0" type="range" value="${inst.zA}" />`+
`<div class="sliderdisplay"></div>`+
`</div>`+
`</div>`;
let rotate =`<div id="segrt${i}" class="lbl-l">`+
`Rotation<br>`+
`<div class="sliderwrap il" title="Rotation speed">`+
`<input id="seg${i}rS" class="noslide" onchange="setSegProp(${i},'rS')" oninput="updateTrail(this)" max="15" min="0" type="range" value="${inst.rS}" />`+
`<div class="sliderdisplay"></div>`+
`</div>`+
`</div>`;
Expand All @@ -750,16 +764,16 @@ function populateSegments(s)
let staY = inst.startY;
let stoY = inst.stopY;
let isMSeg = isM && staX<mw*mh; // 2D matrix segment
let rvXck = `<label class="check revchkl">Reverse ${isM?'':'direction'}<input type="checkbox" id="seg${i}rev" onchange="setRev(${i})" ${inst.rev?"checked":""}><span class="checkmark"></span></label>`;
let miXck = `<label class="check revchkl">Mirror<input type="checkbox" id="seg${i}mi" onchange="setMi(${i})" ${inst.mi?"checked":""}><span class="checkmark"></span></label>`;
let rvXck = `<label class="check revchkl">Reverse ${isM?'':'direction'}<input type="checkbox" id="seg${i}rev" onchange="setSegProp(${i},'rev')" ${inst.rev?"checked":""}><span class="checkmark"></span></label>`;
let miXck = `<label class="check revchkl">Mirror<input type="checkbox" id="seg${i}mi" onchange="setSegProp(${i},'mi')" ${inst.mi?"checked":""}><span class="checkmark"></span></label>`;
let rvYck = "", miYck ="";
let smpl = simplifiedUI ? 'hide' : '';
if (isMSeg) {
rvYck = `<label class="check revchkl">Reverse<input type="checkbox" id="seg${i}rY" onchange="setRevY(${i})" ${inst.rY?"checked":""}><span class="checkmark"></span></label>`;
miYck = `<label class="check revchkl">Mirror<input type="checkbox" id="seg${i}mY" onchange="setMiY(${i})" ${inst.mY?"checked":""}><span class="checkmark"></span></label>`;
rvYck = `<label class="check revchkl">Reverse<input type="checkbox" id="seg${i}rY" onchange="setSegProp(${i},'rY')" ${inst.rY?"checked":""}><span class="checkmark"></span></label>`;
miYck = `<label class="check revchkl">Mirror<input type="checkbox" id="seg${i}mY" onchange="setSegProp(${i},'mY')" ${inst.mY?"checked":""}><span class="checkmark"></span></label>`;
}
let map2D = `<div id="seg${i}map2D" data-map="map2D" class="lbl-s hide">Expand 1D FX<br>`+
`<div class="sel-p"><select class="sel-p" id="seg${i}m12" onchange="setM12(${i})">`+
let map2D = `<div id="seg${i}map2D" data-map="map2D" data-fx="${inst.fx}" class="lbl-s hide">Expand 1D FX<br>`+
`<div class="sel-p"><select class="sel-p" id="seg${i}m12" onchange="setSegProp(${i},'m12')">`+
`<option value="0" ${inst.m12==0?' selected':''}>Pixels</option>`+
`<option value="1" ${inst.m12==1?' selected':''}>Bar</option>`+
`<option value="2" ${inst.m12==2?' selected':''}>Arc</option>`+
Expand All @@ -768,7 +782,7 @@ function populateSegments(s)
`</select></div>`+
`</div>`;
let blend = `<div class="lbl-l">Blend mode<br>`+
`<div class="sel-p"><select class="sel-ple" id="seg${i}bm" onchange="setBm(${i})">`+
`<div class="sel-p"><select class="sel-ple" id="seg${i}bm" onchange="setSegProp(${i},'bm')">`+
`<option value="0" ${inst.bm==0?' selected':''}>Top/Default</option>`+
`<option value="1" ${inst.bm==1?' selected':''}>Bottom/None</option>`+
`<option value="2" ${inst.bm==2?' selected':''}>Add</option>`+
Expand All @@ -788,7 +802,7 @@ function populateSegments(s)
`</select></div>`+
`</div>`;
let sndSim = `<div data-snd="si" class="lbl-s hide">Sound sim<br>`+
`<div class="sel-p"><select class="sel-p" id="seg${i}si" onchange="setSi(${i})">`+
`<div class="sel-p"><select class="sel-p" id="seg${i}si" onchange="setSegProp(${i},'si')">`+
`<option value="0" ${inst.si==0?' selected':''}>BeatSin</option>`+
`<option value="1" ${inst.si==1?' selected':''}>WeWillRockYou</option>`+
`<option value="2" ${inst.si==2?' selected':''}>10/13</option>`+
Expand Down Expand Up @@ -844,12 +858,14 @@ function populateSegments(s)
`<div class="h bp" id="seg${i}len"></div>`+
blend +
(!isMSeg ? rvXck : '') +
(isMSeg?zoom:'')+
(isMSeg?rotate:'')+
(isMSeg&&stoY-staY>1&&stoX-staX>1 ? map2D : '') +
(s.AudioReactive && s.AudioReactive.on ? "" : sndSim) +
`<label class="check revchkl" id="seg${i}lbtm">`+
(isMSeg?'Transpose':'Mirror effect') + (isMSeg ?
'<input type="checkbox" id="seg'+i+'tp" onchange="setTp('+i+')" '+(inst.tp?"checked":"")+'>':
'<input type="checkbox" id="seg'+i+'mi" onchange="setMi('+i+')" '+(inst.mi?"checked":"")+'>') +
'<input type="checkbox" id="seg'+i+'tp" onchange="setSegProp('+i+',\'tp\')" '+(inst.tp?"checked":"")+'>':
'<input type="checkbox" id="seg'+i+'mi" onchange="setSegProp('+i+',\'mi\')" '+(inst.mi?"checked":"")+'>') +
`<span class="checkmark"></span>`+
`</label>`+
`<div class="del">`+
Expand All @@ -870,6 +886,8 @@ function populateSegments(s)
if (!gId(`seg${i}`)) continue;
updateLen(i);
updateTrail(gId(`seg${i}bri`));
let r = gId(`seg${i}rS`); if (r) updateTrail(r);
let z = gId(`seg${i}zA`); if (z) updateTrail(z);
gId(`segr${i}`).classList.add("hide");
}
if (segCount < 2) {
Expand Down Expand Up @@ -2265,6 +2283,14 @@ function delSeg(s)
requestJson(obj);
}

function setSegProp(s,p)
{
let o = gId(`seg${s}${p}`);
let val = o.type === "checkbox" ? o.checked : parseInt(o.value);
var obj = {"seg": {"id": s, [p]: val}};
requestJson(obj);
}
/*
function setRev(s)
{
var rev = gId(`seg${s}rev`).checked;
Expand Down Expand Up @@ -2320,28 +2346,40 @@ function setTp(s)
var obj = {"seg": {"id": s, "tp": tp}};
requestJson(obj);
}

*/
function setGrp(s, g)
{
event.preventDefault();
event.stopPropagation();
var obj = {"seg": {"id": s, "set": g}};
requestJson(obj);
}
/*
function setZoom(s)
{
var obj = {"seg": {"id": s, "zA": parseInt(gId(`seg${s}za`).value)}};
requestJson(obj);
}

function setRotation(s)
{
var obj = {"seg": {"id": s, "rS": parseInt(gId(`seg${s}rs`).value)}};
requestJson(obj);
}
*/
function setSegPwr(s)
{
var pwr = gId(`seg${s}pwr`).classList.contains('act');
var obj = {"seg": {"id": s, "on": !pwr}};
requestJson(obj);
}

/*
function setSegBri(s)
{
var obj = {"seg": {"id": s, "bri": parseInt(gId(`seg${s}bri`).value)}};
requestJson(obj);
}

*/
function tglFreeze(s=null)
{
var obj = {"seg": {"frz": "t"}}; // toggle
Expand Down
Loading