From b97186c5bf9e0a30742ccf0dd06023386eaba271 Mon Sep 17 00:00:00 2001 From: TripleWhy Date: Sat, 13 Jan 2024 18:49:17 +0100 Subject: [PATCH 1/4] Rotating palette effect --- wled00/FX.cpp | 93 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 80 insertions(+), 13 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index a27fc4fbe0..aac161f525 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -1929,22 +1929,89 @@ static const char _data_FX_MODE_JUGGLE[] PROGMEM = "Juggle@!,Trail;;!;;sx=64,ix= uint16_t mode_palette() { - uint16_t counter = 0; - if (SEGMENT.speed != 0) - { - counter = (strip.now * ((SEGMENT.speed >> 3) +1)) & 0xFFFF; - counter = counter >> 8; - } - - for (int i = 0; i < SEGLEN; i++) - { - uint8_t colorIndex = (i * 255 / SEGLEN) - counter; - SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(colorIndex, false, PALETTE_MOVING_WRAP, 255)); +#ifdef ESP8266 + using mathType = int32_t; + using wideMathType = int64_t; + using angleType = uint16_t; + constexpr mathType sInt16Scale = 0x7FFF; + constexpr mathType maxAngle = 0xFFFF; + constexpr mathType staticRotationScale = 256; + constexpr mathType animatedRotationScale = 1; + constexpr int16_t (*sinFunction)(uint16_t) = &sin16; + constexpr int16_t (*cosFunction)(uint16_t) = &cos16; +#else + using mathType = float; + using wideMathType = float; + using angleType = float; + constexpr mathType sInt16Scale = 1.0f; + constexpr mathType maxAngle = M_TWOPI / 256.0; + constexpr mathType staticRotationScale = 1.0f; + constexpr mathType animatedRotationScale = M_TWOPI / double(0xFFFF); + constexpr float (*sinFunction)(float) = &sin_t; + constexpr float (*cosFunction)(float) = &cos_t; +#endif + const bool isMatrix = strip.isMatrix; + const int cols = SEGMENT.virtualWidth(); + const int rows = isMatrix ? SEGMENT.virtualHeight() : strip.getSegmentsNum(); + + const int inputShift = SEGMENT.speed; + const int inputSize = SEGMENT.intensity; + const int inputRotation = SEGMENT.custom1; + const bool inputAnimateShift = SEGMENT.check1; + const bool inputAnimateRotation = SEGMENT.check2; + const bool inputAssumeSquare = SEGMENT.check3; + + const int paletteOffset = (!inputAnimateShift) ? (inputShift) : (((strip.now * ((inputShift >> 3) +1)) & 0xFFFF) >> 8); + + mathType sinTheta; + mathType cosTheta; + if (rows <= 1) { + sinTheta = 0; + cosTheta = sInt16Scale; + } else if (cols <= 1) { + sinTheta = sInt16Scale; + cosTheta = 0; + } else { + const angleType theta = (!inputAnimateRotation) ? (inputRotation * maxAngle / staticRotationScale) : (((strip.now * ((inputRotation >> 4) +1)) & 0xFFFF) * animatedRotationScale); + sinTheta = sinFunction(theta); + cosTheta = cosFunction(theta); + } + + const mathType maxX = std::max(1, cols-1); + const mathType maxY = std::max(1, rows-1); + const mathType maxXIn = inputAssumeSquare ? maxX : mathType(1); + const mathType maxYIn = inputAssumeSquare ? maxY : mathType(1); + const mathType maxXOut = !inputAssumeSquare ? maxX : mathType(1); + const mathType maxYOut = !inputAssumeSquare ? maxY : mathType(1); + const mathType centerX = sInt16Scale * maxXOut / mathType(2); + const mathType centerY = sInt16Scale * maxYOut / mathType(2); + const mathType scale = std::abs(cosTheta) + (std::abs(sinTheta) * maxYOut / maxXOut); + const int yFrom = isMatrix ? 0 : strip.getCurrSegmentId(); + const int yTo = isMatrix ? maxY : yFrom; + for (int y = yFrom; y <= yTo; ++y) { + const mathType ytSinTheta = mathType((wideMathType(sinTheta) * wideMathType(y * sInt16Scale - centerY * maxYIn))/wideMathType(maxYIn * scale)); + for (int x = 0; x < cols; ++x) { + const mathType xtCosTheta = mathType((wideMathType(cosTheta) * wideMathType(x * sInt16Scale - centerX * maxXIn))/wideMathType(maxXIn * scale)); + const mathType sourceX = xtCosTheta + ytSinTheta + centerX; + int colorIndex = (std::min(std::max(sourceX, mathType(0)), maxXOut * sInt16Scale) * 255) / (sInt16Scale * maxXOut); + if (inputSize <= 128) { + colorIndex = (colorIndex * inputSize) / 128; + } else { + // Linear function that maps colorIndex 128=>1, 256=>9 + colorIndex = ((inputSize - 112) * colorIndex) / 16; + } + colorIndex += paletteOffset; + const uint32_t color = SEGMENT.color_wheel((uint8_t)colorIndex); + if (isMatrix) { + SEGMENT.setPixelColorXY(x, y, color); + } else { + SEGMENT.setPixelColor(x, color); + } + } } - return FRAMETIME; } -static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Cycle speed;;!;;c3=0,o2=0"; +static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Shift,Size,Rotation,,,Animate Shift,Animate Rotation,Physical Square;;!;12;o1=1,o2=1"; // WLED limitation: Analog Clock overlay will NOT work when Fire2012 is active From 29af62f956053c90fc7811e175badce84744ab1f Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Fri, 19 Jan 2024 17:01:57 +0100 Subject: [PATCH 2/4] Fix for 1D & remove segment ID --- wled00/FX.cpp | 46 ++++++++++++++++++---------------------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index aac161f525..3a2e1c86b8 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -1933,49 +1933,39 @@ uint16_t mode_palette() { using mathType = int32_t; using wideMathType = int64_t; using angleType = uint16_t; - constexpr mathType sInt16Scale = 0x7FFF; - constexpr mathType maxAngle = 0xFFFF; - constexpr mathType staticRotationScale = 256; - constexpr mathType animatedRotationScale = 1; + constexpr mathType sInt16Scale = 0x7FFF; + constexpr mathType maxAngle = 0xFFFF; + constexpr mathType staticRotationScale = 256; + constexpr mathType animatedRotationScale = 1; constexpr int16_t (*sinFunction)(uint16_t) = &sin16; constexpr int16_t (*cosFunction)(uint16_t) = &cos16; #else using mathType = float; using wideMathType = float; using angleType = float; - constexpr mathType sInt16Scale = 1.0f; - constexpr mathType maxAngle = M_TWOPI / 256.0; - constexpr mathType staticRotationScale = 1.0f; + constexpr mathType sInt16Scale = 1.0f; + constexpr mathType maxAngle = M_TWOPI / 256.0; + constexpr mathType staticRotationScale = 1.0f; constexpr mathType animatedRotationScale = M_TWOPI / double(0xFFFF); - constexpr float (*sinFunction)(float) = &sin_t; - constexpr float (*cosFunction)(float) = &cos_t; + constexpr float (*sinFunction)(float) = &sin_t; + constexpr float (*cosFunction)(float) = &cos_t; #endif const bool isMatrix = strip.isMatrix; const int cols = SEGMENT.virtualWidth(); const int rows = isMatrix ? SEGMENT.virtualHeight() : strip.getSegmentsNum(); - const int inputShift = SEGMENT.speed; - const int inputSize = SEGMENT.intensity; - const int inputRotation = SEGMENT.custom1; - const bool inputAnimateShift = SEGMENT.check1; + const int inputShift = SEGMENT.speed; + const int inputSize = SEGMENT.intensity; + const int inputRotation = SEGMENT.custom1; + const bool inputAnimateShift = SEGMENT.check1; const bool inputAnimateRotation = SEGMENT.check2; - const bool inputAssumeSquare = SEGMENT.check3; + const bool inputAssumeSquare = SEGMENT.check3; const int paletteOffset = (!inputAnimateShift) ? (inputShift) : (((strip.now * ((inputShift >> 3) +1)) & 0xFFFF) >> 8); - mathType sinTheta; - mathType cosTheta; - if (rows <= 1) { - sinTheta = 0; - cosTheta = sInt16Scale; - } else if (cols <= 1) { - sinTheta = sInt16Scale; - cosTheta = 0; - } else { - const angleType theta = (!inputAnimateRotation) ? (inputRotation * maxAngle / staticRotationScale) : (((strip.now * ((inputRotation >> 4) +1)) & 0xFFFF) * animatedRotationScale); - sinTheta = sinFunction(theta); - cosTheta = cosFunction(theta); - } + const angleType theta = (!inputAnimateRotation) ? (inputRotation * maxAngle / staticRotationScale) : (((strip.now * ((inputRotation >> 4) +1)) & 0xFFFF) * animatedRotationScale); + const mathType sinTheta = sinFunction(theta); + const mathType cosTheta = cosFunction(theta); const mathType maxX = std::max(1, cols-1); const mathType maxY = std::max(1, rows-1); @@ -2011,7 +2001,7 @@ uint16_t mode_palette() { } return FRAMETIME; } -static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Shift,Size,Rotation,,,Animate Shift,Animate Rotation,Physical Square;;!;12;o1=1,o2=1"; +static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Shift,Size,Rotation,,,Animate Shift,Animate Rotation,Physical Square;;!;12;c1=64,o1=1,o2=1,o3=0"; // WLED limitation: Analog Clock overlay will NOT work when Fire2012 is active From 8a31c57bfa3c861ecb7f07b604d9eefabec02427 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Mon, 22 Jan 2024 19:38:13 +0100 Subject: [PATCH 3/4] Fix getActiveSegmentsNum Limit rotation to +-90 deg (swapping sin/cos & limit angle) Shift palette shift --- wled00/FX.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 3a2e1c86b8..cb337a12e6 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -1934,7 +1934,7 @@ uint16_t mode_palette() { using wideMathType = int64_t; using angleType = uint16_t; constexpr mathType sInt16Scale = 0x7FFF; - constexpr mathType maxAngle = 0xFFFF; + constexpr mathType maxAngle = 0x8000; constexpr mathType staticRotationScale = 256; constexpr mathType animatedRotationScale = 1; constexpr int16_t (*sinFunction)(uint16_t) = &sin16; @@ -1944,7 +1944,7 @@ uint16_t mode_palette() { using wideMathType = float; using angleType = float; constexpr mathType sInt16Scale = 1.0f; - constexpr mathType maxAngle = M_TWOPI / 256.0; + constexpr mathType maxAngle = M_PI / 256.0; constexpr mathType staticRotationScale = 1.0f; constexpr mathType animatedRotationScale = M_TWOPI / double(0xFFFF); constexpr float (*sinFunction)(float) = &sin_t; @@ -1952,7 +1952,7 @@ uint16_t mode_palette() { #endif const bool isMatrix = strip.isMatrix; const int cols = SEGMENT.virtualWidth(); - const int rows = isMatrix ? SEGMENT.virtualHeight() : strip.getSegmentsNum(); + const int rows = isMatrix ? SEGMENT.virtualHeight() : strip.getActiveSegmentsNum(); const int inputShift = SEGMENT.speed; const int inputSize = SEGMENT.intensity; @@ -1961,7 +1961,7 @@ uint16_t mode_palette() { const bool inputAnimateRotation = SEGMENT.check2; const bool inputAssumeSquare = SEGMENT.check3; - const int paletteOffset = (!inputAnimateShift) ? (inputShift) : (((strip.now * ((inputShift >> 3) +1)) & 0xFFFF) >> 8); + const int paletteOffset = (!inputAnimateShift) ? (inputShift-128) : (((strip.now * ((inputShift >> 3) +1)) & 0xFFFF) >> 8); const angleType theta = (!inputAnimateRotation) ? (inputRotation * maxAngle / staticRotationScale) : (((strip.now * ((inputRotation >> 4) +1)) & 0xFFFF) * animatedRotationScale); const mathType sinTheta = sinFunction(theta); @@ -1975,14 +1975,14 @@ uint16_t mode_palette() { const mathType maxYOut = !inputAssumeSquare ? maxY : mathType(1); const mathType centerX = sInt16Scale * maxXOut / mathType(2); const mathType centerY = sInt16Scale * maxYOut / mathType(2); - const mathType scale = std::abs(cosTheta) + (std::abs(sinTheta) * maxYOut / maxXOut); + const mathType scale = std::abs(sinTheta) + (std::abs(cosTheta) * maxYOut / maxXOut); const int yFrom = isMatrix ? 0 : strip.getCurrSegmentId(); const int yTo = isMatrix ? maxY : yFrom; for (int y = yFrom; y <= yTo; ++y) { - const mathType ytSinTheta = mathType((wideMathType(sinTheta) * wideMathType(y * sInt16Scale - centerY * maxYIn))/wideMathType(maxYIn * scale)); + const mathType ytCosTheta = mathType((wideMathType(cosTheta) * wideMathType(y * sInt16Scale - centerY * maxYIn))/wideMathType(maxYIn * scale)); for (int x = 0; x < cols; ++x) { - const mathType xtCosTheta = mathType((wideMathType(cosTheta) * wideMathType(x * sInt16Scale - centerX * maxXIn))/wideMathType(maxXIn * scale)); - const mathType sourceX = xtCosTheta + ytSinTheta + centerX; + const mathType xtSinTheta = mathType((wideMathType(sinTheta) * wideMathType(x * sInt16Scale - centerX * maxXIn))/wideMathType(maxXIn * scale)); + const mathType sourceX = xtSinTheta + ytCosTheta + centerX; int colorIndex = (std::min(std::max(sourceX, mathType(0)), maxXOut * sInt16Scale) * 255) / (sInt16Scale * maxXOut); if (inputSize <= 128) { colorIndex = (colorIndex * inputSize) / 128; @@ -2001,7 +2001,7 @@ uint16_t mode_palette() { } return FRAMETIME; } -static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Shift,Size,Rotation,,,Animate Shift,Animate Rotation,Physical Square;;!;12;c1=64,o1=1,o2=1,o3=0"; +static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Shift,Size,Rotation,,,Animate Shift,Animate Rotation,Anamorphic;;!;12;c1=128,c2=128,c3=128,o1=1,o2=1,o3=0"; // WLED limitation: Analog Clock overlay will NOT work when Fire2012 is active From 0d279cb4dd55f7f70aad2117beded36fb1598efb Mon Sep 17 00:00:00 2001 From: TripleWhy Date: Wed, 24 Jan 2024 15:33:45 +0100 Subject: [PATCH 4/4] Add some comments --- wled00/FX.cpp | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index cb337a12e6..1b95086d5c 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -1929,6 +1929,7 @@ static const char _data_FX_MODE_JUGGLE[] PROGMEM = "Juggle@!,Trail;;!;;sx=64,ix= uint16_t mode_palette() { + // Set up some compile time constants so that we can handle integer and float based modes using the same code base. #ifdef ESP8266 using mathType = int32_t; using wideMathType = int64_t; @@ -1961,35 +1962,57 @@ uint16_t mode_palette() { const bool inputAnimateRotation = SEGMENT.check2; const bool inputAssumeSquare = SEGMENT.check3; - const int paletteOffset = (!inputAnimateShift) ? (inputShift-128) : (((strip.now * ((inputShift >> 3) +1)) & 0xFFFF) >> 8); - const angleType theta = (!inputAnimateRotation) ? (inputRotation * maxAngle / staticRotationScale) : (((strip.now * ((inputRotation >> 4) +1)) & 0xFFFF) * animatedRotationScale); const mathType sinTheta = sinFunction(theta); const mathType cosTheta = cosFunction(theta); - const mathType maxX = std::max(1, cols-1); - const mathType maxY = std::max(1, rows-1); + const mathType maxX = std::max(1, cols-1); + const mathType maxY = std::max(1, rows-1); + // Set up some parameters according to inputAssumeSquare, so that we can handle anamorphic mode using the same code base. const mathType maxXIn = inputAssumeSquare ? maxX : mathType(1); const mathType maxYIn = inputAssumeSquare ? maxY : mathType(1); const mathType maxXOut = !inputAssumeSquare ? maxX : mathType(1); const mathType maxYOut = !inputAssumeSquare ? maxY : mathType(1); const mathType centerX = sInt16Scale * maxXOut / mathType(2); const mathType centerY = sInt16Scale * maxYOut / mathType(2); + // The basic idea for this effect is to rotate a rectangle that is filled with the palette along one axis, then map our + // display to it, to find what color a pixel should have. + // However, we want a) no areas of solid color (in front of or behind the palette), and b) we want to make use of the full palette. + // So the rectangle needs to have exactly the right size. That size depends on the rotation. + // This scale computation here only considers one dimension. You can think of it like the rectangle is always scaled so that + // the left and right most points always match the left and right side of the display. const mathType scale = std::abs(sinTheta) + (std::abs(cosTheta) * maxYOut / maxXOut); + // 2D simulation: + // If we are dealing with a 1D setup, we assume that each segment represents one line on a 2-dimensional display. + // The function is called once per segments, so we need to handle one line at a time. const int yFrom = isMatrix ? 0 : strip.getCurrSegmentId(); - const int yTo = isMatrix ? maxY : yFrom; + const int yTo = isMatrix ? maxY : yFrom; for (int y = yFrom; y <= yTo; ++y) { + // translate, scale, rotate const mathType ytCosTheta = mathType((wideMathType(cosTheta) * wideMathType(y * sInt16Scale - centerY * maxYIn))/wideMathType(maxYIn * scale)); for (int x = 0; x < cols; ++x) { + // translate, scale, rotate const mathType xtSinTheta = mathType((wideMathType(sinTheta) * wideMathType(x * sInt16Scale - centerX * maxXIn))/wideMathType(maxXIn * scale)); + // Map the pixel coordinate to an imaginary-rectangle-coordinate. + // The y coordinate doesn't actually matter, as our imaginary rectangle is filled with the palette from left to right, + // so all points at a given x-coordinate have the same color. const mathType sourceX = xtSinTheta + ytCosTheta + centerX; + // The computation was scaled just right so that the result should always be in range [0, maxXOut], but enforce this anyway + // to account for imprecision. Then scale it so that the range is [0, 255], which we can use with the palette. int colorIndex = (std::min(std::max(sourceX, mathType(0)), maxXOut * sInt16Scale) * 255) / (sInt16Scale * maxXOut); + // inputSize determines by how much we want to scale the palette: + // values < 128 display a fraction of a palette, + // values > 128 display multiple palettes. if (inputSize <= 128) { colorIndex = (colorIndex * inputSize) / 128; } else { - // Linear function that maps colorIndex 128=>1, 256=>9 + // Linear function that maps colorIndex 128=>1, 256=>9. + // With this function every full palette repetition is exactly 16 configuration steps wide. + // That allows displaying exactly 2 repetitions for example. colorIndex = ((inputSize - 112) * colorIndex) / 16; } + // Finally, shift the palette a bit. + const int paletteOffset = (!inputAnimateShift) ? (inputShift-128) : (((strip.now * ((inputShift >> 3) +1)) & 0xFFFF) >> 8); colorIndex += paletteOffset; const uint32_t color = SEGMENT.color_wheel((uint8_t)colorIndex); if (isMatrix) {