From 9da047f8729746c3758ee508778df5df32ec3b5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Mon, 22 Dec 2025 21:37:33 +0100 Subject: [PATCH 1/4] Zoom and rotate segment by @Brandon502 & @blazoncek - see #5202 for original implementation - does not include mirroring and always wraps - implemented as Segment property - reduced index.js footprint by coalescing several similar functions - slightly optimised push transition logic --- wled00/FX.h | 10 +++- wled00/FX_fcn.cpp | 120 +++++++++++++++++++++++++++++++++++++++---- wled00/data/index.js | 66 +++++++++++++++++++----- wled00/json.cpp | 17 +++++- 4 files changed, 187 insertions(+), 26 deletions(-) diff --git a/wled00/FX.h b/wled00/FX.h index bcbab69a59..53539d921d 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -421,7 +421,7 @@ typedef enum mapping1D2D { class WS2812FX; -// segment, 76 bytes +// segment, 80 bytes class Segment { public: uint32_t colors[NUM_COLORS]; @@ -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 @@ -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 @@ -591,6 +595,8 @@ class Segment { , check2(false) , check3(false) , blendMode(0) + , zoomAmount(8) + , rotateSpeed(0) , name(nullptr) , next_time(0) , step(0) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 3f357daa29..9ba64ec65b 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -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; @@ -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); @@ -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) { + _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) { + _pixelsO = new uint32_t[oCols * oRows]; // may use allocateBuffer() if needed + const int midXo = oCols / 2; + const int midYo = oRows / 2; + if (topSegment.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++) { @@ -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 @@ -1531,6 +1630,9 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { } } } + // clean up + if (topSegment.rotateSpeed || topSegment.zoomAmount) delete[] _pixelsN; + if (segO && (segO->rotateSpeed || segO->zoomAmount)) delete[] _pixelsO; #endif } else { const int nLen = topSegment.virtualLength(); diff --git a/wled00/data/index.js b/wled00/data/index.js index 7cb989d062..ae27b83c2f 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -741,7 +741,21 @@ function populateSegments(s) let segp = `
`+ ``+ `
`+ - ``+ + ``+ + `
`+ + `
`+ + `
`; + let zoom = `
`+ + `Zoom
`+ + `
`+ + ``+ + `
`+ + `
`+ + `
`; + let rotate =`
`+ + `Rotation
`+ + `
`+ + ``+ `
`+ `
`+ `
`; @@ -750,16 +764,16 @@ function populateSegments(s) let staY = inst.startY; let stoY = inst.stopY; let isMSeg = isM && staXReverse ${isM?'':'direction'}`; - let miXck = ``; + let rvXck = ``; + let miXck = ``; let rvYck = "", miYck =""; let smpl = simplifiedUI ? 'hide' : ''; if (isMSeg) { - rvYck = ``; - miYck = ``; + rvYck = ``; + miYck = ``; } - let map2D = `
Expand 1D FX
`+ - `
`+ ``+ ``+ ``+ @@ -768,7 +782,7 @@ function populateSegments(s) `
`+ `
`; let blend = `
Blend mode
`+ - `
`+ ``+ ``+ ``+ @@ -788,7 +802,7 @@ function populateSegments(s) `
`+ `
`; let sndSim = `
Sound sim
`+ - `
`+ ``+ ``+ ``+ @@ -844,12 +858,14 @@ function populateSegments(s) `
`+ blend + (!isMSeg ? rvXck : '') + + (isMSeg?zoom:'')+ + (isMSeg?rotate:'')+ (isMSeg&&stoY-staY>1&&stoX-staX>1 ? map2D : '') + (s.AudioReactive && s.AudioReactive.on ? "" : sndSim) + ``+ `
`+ @@ -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) { @@ -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; @@ -2320,7 +2346,7 @@ function setTp(s) var obj = {"seg": {"id": s, "tp": tp}}; requestJson(obj); } - +*/ function setGrp(s, g) { event.preventDefault(); @@ -2328,20 +2354,32 @@ function setGrp(s, g) 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 diff --git a/wled00/json.cpp b/wled00/json.cpp index f23080135f..a061b0a3f2 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -35,6 +35,9 @@ namespace { bool check1; bool check2; bool check3; + uint8_t blendMode; + uint8_t zoomAmount; + uint8_t rotateSpeed; } SegmentCopy; uint8_t differs(const Segment& b, const SegmentCopy& a) { @@ -57,6 +60,9 @@ namespace { if (a.check3 != b.check3) d |= SEG_DIFFERS_FX; if (a.startY != b.startY) d |= SEG_DIFFERS_BOUNDS; if (a.stopY != b.stopY) d |= SEG_DIFFERS_BOUNDS; + if (a.blendMode != b.blendMode) d |= SEG_DIFFERS_OPT; + if (a.zoomAmount != b.zoomAmount) d |= SEG_DIFFERS_OPT; + if (a.rotateSpeed != b.rotateSpeed) d |= SEG_DIFFERS_OPT; //bit pattern: (msb first) // set:2, sound:2, mapping:3, transposed, mirrorY, reverseY, [reset,] paused, mirrored, on, reverse, [selected] @@ -108,7 +114,10 @@ static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0) seg.custom3, seg.check1, seg.check2, - seg.check3 + seg.check3, + seg.blendMode, + seg.zoomAmount, + seg.rotateSpeed }; int start = elem["start"] | seg.start; @@ -152,6 +161,8 @@ static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0) uint16_t spc = elem[F("spc")] | seg.spacing; uint16_t of = seg.offset; uint8_t soundSim = elem["si"] | seg.soundSim; + uint8_t rotateSpeed = elem["rS"] | seg.rotateSpeed; + uint8_t zoomAmount = elem["zA"] | seg.zoomAmount; uint8_t map1D2D = elem["m12"] | seg.map1D2D; uint8_t set = elem[F("set")] | seg.set; bool selected = getBoolVal(elem["sel"], seg.selected); @@ -269,6 +280,8 @@ static bool deserializeSegment(JsonObject elem, byte it, byte presetId = 0) } #endif + seg.rotateSpeed = constrain(rotateSpeed, 0, 15); + seg.zoomAmount = constrain(zoomAmount, 0, 15); seg.set = constrain(set, 0, 3); seg.soundSim = constrain(soundSim, 0, 3); seg.selected = selected; @@ -631,6 +644,8 @@ static void serializeSegment(JsonObject& root, const Segment& seg, byte id, bool root["si"] = seg.soundSim; root["m12"] = seg.map1D2D; root["bm"] = seg.blendMode; + root["rS"] = seg.rotateSpeed; + root["zA"] = seg.zoomAmount; } void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segmentBounds, bool selectedSegmentsOnly) From 35f9b37a3346844cf9de8fffd4529b350456c614 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Tue, 23 Dec 2025 11:50:56 +0100 Subject: [PATCH 2/4] Correct zoom offset --- wled00/FX_fcn.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 9ba64ec65b..ef76ee0036 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1545,7 +1545,7 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { }; uint32_t *_pixelsN = topSegment.getPixels(); // we will use this pointer as a source later insetad of getPixelColorRaw() - if (topSegment.rotateSpeed || topSegment.zoomAmount) { + 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; @@ -1560,7 +1560,7 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { 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) { + 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; @@ -1631,8 +1631,8 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { } } // clean up - if (topSegment.rotateSpeed || topSegment.zoomAmount) delete[] _pixelsN; - if (segO && (segO->rotateSpeed || segO->zoomAmount)) delete[] _pixelsO; + if (topSegment.rotateSpeed || topSegment.zoomAmount != 8) delete[] _pixelsN; + if (segO && (segO->rotateSpeed || segO->zoomAmount != 8)) delete[] _pixelsO; #endif } else { const int nLen = topSegment.virtualLength(); From 201c3c0066c03a3194d2902e51c4a82ad7476a6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Tue, 23 Dec 2025 12:40:11 +0100 Subject: [PATCH 3/4] Address issue, use underscore to denote private variable --- wled00/FX.h | 3 ++- wled00/FX_fcn.cpp | 16 ++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/wled00/FX.h b/wled00/FX.h index 53539d921d..89c715b869 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -491,7 +491,7 @@ class Segment { bool _manualW : 1; }; }; - mutable uint16_t rotatedAngle; // current rotation angle (2D) + 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 @@ -607,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); diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index ef76ee0036..9193932e25 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1550,12 +1550,12 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { 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; + topSegment._rotatedAngle += topSegment.rotateSpeed; + while (topSegment._rotatedAngle > 3600) topSegment._rotatedAngle -= 3600; } else { - topSegment.rotatedAngle = 0; // reset angle if no rotation + topSegment._rotatedAngle = 0; // reset angle if no rotation } - RotateAndZoom(topSegment.getPixels(), _pixelsN, midX, midY, nCols, nRows, topSegment.rotatedAngle/10, topSegment.zoomAmount - 8); + 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) { @@ -1565,12 +1565,12 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { const int midXo = oCols / 2; const int midYo = oRows / 2; if (topSegment.rotateSpeed != 0) { - segO->rotatedAngle += segO->rotateSpeed; - while (segO->rotatedAngle > 3600) segO->rotatedAngle -= 3600; + segO->_rotatedAngle += segO->rotateSpeed; + while (segO->_rotatedAngle > 3600) segO->_rotatedAngle -= 3600; } else { - segO->rotatedAngle = 0; + segO->_rotatedAngle = 0; } - RotateAndZoom(segO->getPixels(), _pixelsO, midXo, midYo, oCols, oRows, segO->rotatedAngle/10, segO->zoomAmount - 8); + RotateAndZoom(segO->getPixels(), _pixelsO, midXo, midYo, oCols, oRows, segO->_rotatedAngle/10, segO->zoomAmount - 8); } } From 252311b4f5b57bd6d951bd7a817ee1f14c57204b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Kristan?= Date: Tue, 23 Dec 2025 12:49:25 +0100 Subject: [PATCH 4/4] Bugfix --- wled00/FX_fcn.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 9193932e25..cff29f7787 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1564,7 +1564,7 @@ void WS2812FX::blendSegment(const Segment &topSegment) const { _pixelsO = new uint32_t[oCols * oRows]; // may use allocateBuffer() if needed const int midXo = oCols / 2; const int midYo = oRows / 2; - if (topSegment.rotateSpeed != 0) { + if (segO->rotateSpeed != 0) { segO->_rotatedAngle += segO->rotateSpeed; while (segO->_rotatedAngle > 3600) segO->_rotatedAngle -= 3600; } else {