@@ -563,7 +563,7 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) {
563563 sOpt = extractModeDefaults (fx, " ix" ); intensity = (sOpt >= 0 ) ? sOpt : DEFAULT_INTENSITY;
564564 sOpt = extractModeDefaults (fx, " c1" ); custom1 = (sOpt >= 0 ) ? sOpt : DEFAULT_C1;
565565 sOpt = extractModeDefaults (fx, " c2" ); custom2 = (sOpt >= 0 ) ? sOpt : DEFAULT_C2;
566- sOpt = extractModeDefaults (fx, " c3" ); custom3 = (sOpt >= 0 ) ? sOpt : DEFAULT_C3;
566+ sOpt = extractModeDefaults (fx, " c3" ); custom3 = (sOpt >= 0 ) ? constrain ( sOpt , 0 , 31 ) : DEFAULT_C3;
567567 sOpt = extractModeDefaults (fx, " o1" ); check1 = (sOpt >= 0 ) ? (bool )sOpt : false ;
568568 sOpt = extractModeDefaults (fx, " o2" ); check2 = (sOpt >= 0 ) ? (bool )sOpt : false ;
569569 sOpt = extractModeDefaults (fx, " o3" ); check3 = (sOpt >= 0 ) ? (bool )sOpt : false ;
@@ -573,6 +573,8 @@ Segment &Segment::setMode(uint8_t fx, bool loadDefaults) {
573573 sOpt = extractModeDefaults (fx, " mi" ); if (sOpt >= 0 ) mirror = (bool )sOpt ; // NOTE: setting this option is a risky business
574574 sOpt = extractModeDefaults (fx, " rY" ); if (sOpt >= 0 ) reverse_y = (bool )sOpt ;
575575 sOpt = extractModeDefaults (fx, " mY" ); if (sOpt >= 0 ) mirror_y = (bool )sOpt ; // NOTE: setting this option is a risky business
576+ sOpt = extractModeDefaults (fx, " rS" ); if (sOpt >= 0 ) rotateSpeed = constrain (sOpt , 0 , 15 ); // 0 = no rotation
577+ sOpt = extractModeDefaults (fx, " zA" ); if (sOpt >= 0 ) zoomAmount = constrain (sOpt , 0 , 15 ); // 8 = no zoom
576578 }
577579 sOpt = extractModeDefaults (fx, " pal" ); // always extract 'pal' to set _default_palette
578580 if (sOpt >= 0 && loadDefaults) setPalette (sOpt );
@@ -1474,9 +1476,109 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
14741476 }
14751477 };
14761478
1479+ // zooming and rotation
1480+ auto RotateAndZoom = [](uint32_t *srcPixels, uint32_t *destPixels, int midX, int midY, int cols, int rows, int shearAngle, int zoomOffset) {
1481+ for (int i = 0 ; i < cols * rows; i++) destPixels[i] = BLACK; // fill black
1482+
1483+ constexpr uint8_t Scale_Shift = 10 ;
1484+ constexpr int Fixed_Scale = (1 << Scale_Shift);
1485+ constexpr int RoundVal = (1 << (Scale_Shift - 1 ));
1486+ constexpr int zoomRange = (Fixed_Scale * 3 ) / 4 ; // 768
1487+ int zoomScale = Fixed_Scale + (zoomOffset * zoomRange) / 8 ; // zoomOffset: -8 .. +7 -> zoomScale: 256 .. 1696
1488+ if (zoomScale <= 0 ) zoomScale = 1 ; // avoid divide-by-zero and negative zoom
1489+
1490+ const bool flip = (shearAngle > 90 && shearAngle < 270 ); // Flip to avoid instability near 180°
1491+ if (flip) shearAngle = (shearAngle + 180 ) % 360 ;
1492+
1493+ // Calculate shearX and shearY
1494+ const float angleRadians = radians (shearAngle);
1495+ int shearX = -tan_t (angleRadians / 2 ) * Fixed_Scale;
1496+ int shearY = sin_t (angleRadians) * Fixed_Scale;
1497+
1498+ const int WRAP_PAD_X = cols << 5 ; // ×32
1499+ const int WRAP_PAD_Y = rows << 5 ; // Ensures wrap works with large negative coordinates when zoomed out
1500+
1501+ // Use inverse mapping: iterate destination pixels, find source coordinates
1502+ for (int destY = 0 ; destY < rows; destY++) {
1503+ for (int destX = 0 ; destX < cols; destX++) {
1504+ // Translate destination to origin
1505+ int dx = destX - midX;
1506+ int dy = destY - midY;
1507+
1508+ // Inverse shear transformations (reverse order)
1509+ int x1 = dx - ((shearX * dy + RoundVal) >> Scale_Shift);
1510+ int y0 = dy - ((shearY * x1 + RoundVal) >> Scale_Shift);
1511+ int x0 = x1 - ((shearX * y0 + RoundVal) >> Scale_Shift);
1512+
1513+ // Apply zoom to source coordinates
1514+ x0 = (x0 * Fixed_Scale) / zoomScale;
1515+ y0 = (y0 * Fixed_Scale) / zoomScale;
1516+
1517+ // Handle flip
1518+ int srcX = flip ? (midX - x0) : (midX + x0);
1519+ int srcY = flip ? (midY - y0) : (midY + y0);
1520+
1521+ // Bounds check or wrap
1522+ // if (wrap) { // Wrap around
1523+ srcX = (srcX + WRAP_PAD_X); while (srcX >= cols) srcX -= cols; // positive modulo since % is slow
1524+ srcY = (srcY + WRAP_PAD_Y); while (srcY >= rows) srcY -= rows; // positive modulo since % is slow
1525+ // }
1526+ // else if (wrap_and_mirror) { // Wrap plus mirror
1527+ // int tileX = (srcX + WRAP_PAD_X) / cols;
1528+ // int tileY = (srcY + WRAP_PAD_Y) / rows;
1529+
1530+ // // Wrap src
1531+ // srcX = (srcX + WRAP_PAD_X); while (srcX >= cols) srcX -= cols; // positive modulo since % is slow
1532+ // srcY = (srcY + WRAP_PAD_Y); while (srcY >= rows) srcY -= rows; // positive modulo since % is slow
1533+
1534+ // // Flip on odd tiles
1535+ // if (tileX & 1) srcX = cols - 1 - srcX;
1536+ // if (tileY & 1) srcY = rows - 1 - srcY;
1537+ // }
1538+ // else
1539+ if ((unsigned )srcX >= (unsigned )cols || (unsigned )srcY >= (unsigned )rows) continue ;
1540+
1541+ // Sample from source & write to destination
1542+ destPixels[destX + destY * cols] = srcPixels[srcX + srcY * cols];
1543+ }
1544+ }
1545+ };
1546+
1547+ uint32_t *_pixelsN = topSegment.getPixels (); // we will use this pointer as a source later insetad of getPixelColorRaw()
1548+ if (topSegment.rotateSpeed || topSegment.zoomAmount ) {
1549+ _pixelsN = new uint32_t [nCols * nRows]; // may use allocateBuffer() if needed
1550+ const int midX = nCols / 2 ;
1551+ const int midY = nRows / 2 ;
1552+ if (topSegment.rotateSpeed != 0 ) {
1553+ topSegment.rotatedAngle += topSegment.rotateSpeed ;
1554+ while (topSegment.rotatedAngle > 3600 ) topSegment.rotatedAngle -= 3600 ;
1555+ } else {
1556+ topSegment.rotatedAngle = 0 ; // reset angle if no rotation
1557+ }
1558+ RotateAndZoom (topSegment.getPixels (), _pixelsN, midX, midY, nCols, nRows, topSegment.rotatedAngle /10 , topSegment.zoomAmount - 8 );
1559+ }
1560+ uint32_t *_pixelsO = topSegment.getPixels (); // we will use this pointer as a source (old segment during transition) later insetad of getPixelColorRaw()
1561+ if (segO) {
1562+ _pixelsO = segO->getPixels (); // default to unmodified old segment pixels
1563+ if (segO->rotateSpeed || segO->zoomAmount ) {
1564+ _pixelsO = new uint32_t [oCols * oRows]; // may use allocateBuffer() if needed
1565+ const int midXo = oCols / 2 ;
1566+ const int midYo = oRows / 2 ;
1567+ if (topSegment.rotateSpeed != 0 ) {
1568+ segO->rotatedAngle += segO->rotateSpeed ;
1569+ while (segO->rotatedAngle > 3600 ) segO->rotatedAngle -= 3600 ;
1570+ } else {
1571+ segO->rotatedAngle = 0 ;
1572+ }
1573+ RotateAndZoom (segO->getPixels (), _pixelsO, midXo, midYo, oCols, oRows, segO->rotatedAngle /10 , segO->zoomAmount - 8 );
1574+ }
1575+ }
1576+
14771577 // if we blend using "push" style we need to "shift" canvas to left/right/up/down
14781578 unsigned offsetX = (blendingStyle == BLEND_STYLE_PUSH_UP || blendingStyle == BLEND_STYLE_PUSH_DOWN) ? 0 : progInv * nCols / 0xFFFFU ;
14791579 unsigned offsetY = (blendingStyle == BLEND_STYLE_PUSH_LEFT || blendingStyle == BLEND_STYLE_PUSH_RIGHT) ? 0 : progInv * nRows / 0xFFFFU ;
1580+ if (blendingStyle == BLEND_STYLE_PUSH_RIGHT) offsetX = nCols - offsetX;
1581+ if (blendingStyle == BLEND_STYLE_PUSH_UP) offsetY = nRows - offsetY;
14801582
14811583 // we only traverse new segment, not old one
14821584 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 {
14851587 const Segment *seg = clipped && segO ? segO : &topSegment; // pixel is never clipped for FADE
14861588 int vCols = seg == segO ? oCols : nCols; // old segment may have different dimensions
14871589 int vRows = seg == segO ? oRows : nRows; // old segment may have different dimensions
1590+ uint32_t *_pixelsR = seg == segO ? _pixelsO : _pixelsN;
14881591 int x = c;
14891592 int y = r;
14901593 // if we blend using "push" style we need to "shift" canvas to left/right/up/down
1491- switch (blendingStyle) {
1492- case BLEND_STYLE_PUSH_RIGHT: x = (x + offsetX) % nCols; break ;
1493- case BLEND_STYLE_PUSH_LEFT: x = (x - offsetX + nCols) % nCols; break ;
1494- case BLEND_STYLE_PUSH_DOWN: y = (y + offsetY) % nRows; break ;
1495- case BLEND_STYLE_PUSH_UP: y = (y - offsetY + nRows) % nRows; break ;
1496- }
1594+ if (offsetX != 0 ) { x = (x + offsetX); while (x >= nCols) x -= nCols; }
1595+ if (offsetY != 0 ) { y = (y + offsetY); while (y >= nRows) y -= nRows; }
14971596 uint32_t c_a = BLACK;
1498- 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
1597+ if (x < vCols && y < vRows) c_a = _pixelsR[ x + y*vCols] ; // will get clipped pixel from old segment or unclipped pixel from new segment
14991598 if (segO && blendingStyle == BLEND_STYLE_FADE
15001599 && (topSegment.mode != segO->mode || (segO->name != topSegment.name && segO->name && topSegment.name && strncmp (segO->name , topSegment.name , WLED_MAX_SEGNAME_LEN) != 0 ))
15011600 && x < oCols && y < oRows) {
15021601 // we need to blend old segment using fade as pixels are not clipped
1503- c_a = color_blend16 (c_a, segO-> getPixelColorRaw ( x + y*oCols) , progInv);
1602+ c_a = color_blend16 (c_a, _pixelsO[ x + y*oCols] , progInv);
15041603 } else if (blendingStyle != BLEND_STYLE_FADE) {
15051604 // if we have global brightness change (not On/Off change) we will ignore transition style and just fade brightness (see led.cpp)
15061605 // workaround for On/Off transition
@@ -1531,6 +1630,9 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
15311630 }
15321631 }
15331632 }
1633+ // clean up
1634+ if (topSegment.rotateSpeed || topSegment.zoomAmount ) delete[] _pixelsN;
1635+ if (segO && (segO->rotateSpeed || segO->zoomAmount )) delete[] _pixelsO;
15341636#endif
15351637 } else {
15361638 const int nLen = topSegment.virtualLength ();
0 commit comments