From d54862e111acc8fb4894db870b2eef67514df4de Mon Sep 17 00:00:00 2001 From: "DESKTOP-BOB-DEL\\boblo" Date: Sun, 25 Oct 2026 01:37:18 -0700 Subject: [PATCH 1/8] Added PS Pendulum 1D effect --- usermods/user_fx/user_fx.cpp | 181 +++++++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 6c486a03df..652032db7a 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -2,6 +2,28 @@ // for information how FX metadata strings work see https://kno.wled.ge/interfaces/json-api/#effect-metadata +#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) + #include "FXparticleSystem.h" // include particle system code only if at least one system is enabled + #ifdef WLED_DISABLE_PARTICLESYSTEM2D + #define WLED_PS_DONT_REPLACE_2D_FX + #endif + #ifdef WLED_DISABLE_PARTICLESYSTEM1D + #define WLED_PS_DONT_REPLACE_1D_FX + #endif + #ifdef ESP8266 + #if !defined(WLED_DISABLE_PARTICLESYSTEM2D) && !defined(WLED_DISABLE_PARTICLESYSTEM1D) + #error ESP8266 does not support 1D and 2D particle systems simultaneously. Please disable one of them. + #endif + #endif +#else + #define WLED_PS_DONT_REPLACE_1D_FX + #define WLED_PS_DONT_REPLACE_2D_FX +#endif +#ifdef WLED_PS_DONT_REPLACE_FX + #define WLED_PS_DONT_REPLACE_1D_FX + #define WLED_PS_DONT_REPLACE_2D_FX +#endif + // paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) #define PALETTE_SOLID_WRAP (paletteBlend == 1 || paletteBlend == 3) @@ -1599,6 +1621,164 @@ static void mode_dissolveplus(void) { static const char _data_FX_MODE_DISSOLVEPLUS[] PROGMEM = "Dissolve Plus@Repeat speed,Dissolve speed,Fill speed,Last one delay,Fade rate,Random,Complete,Last one;!,!;!;;o2=1"; +/* +/ PS Pendulum effect (uses the Particle System) +* by Bob Loeffler and claude.ai Particle System by dedehai +* First slider (speed) is for the speed of the pendulum LED going back and forth. (0 = random speed) +* Second slider (intensity) is for the X offset. In the middle means the center of the pendulum will be in the middle of the LED strip. +* Moving the slider left will move the pendulum to the left, etc. (0 = random X offset) +* Third slider (blur) is for how much of a trail the pendulum will leave. +* Fourth slider (pause/delay) is for how long the effect will pause before restarting the pendulum movement. +* Fifth slider (gravity/damping) is for how much the pendulum movement will be dampened due to gravity or friction. (0 = random damping) +* Checkbox1 will select the LED color based on it's position on the strip. +* Checkbox2 will make the pendulum swing forever (a perpetual pendulum). +* aux0 stores the settings checksum to detect changes. +*/ +#define PENDULUM_MIN_AMPLITUDE_SUBPX ((PS_P_RADIUS_1D * 2) / 5) + +// Custom pendulum state, stored in the extra bytes after the particle system's own data +typedef struct { + int32_t currentPos; // sub-pixel position (scaled by PS_P_RADIUS_1D); matches particle.x units directly + int32_t fromPos; + int32_t toPos; + int32_t amplitude; // sub-pixel + uint16_t damping256; // Q8 fixed point: 0-255 represents 0.0-1.0 + uint32_t swingStart; + uint32_t swingDuration; + uint32_t pauseStart; + uint16_t swing_time; + uint16_t offsetX; // pixel units (kept as pixels since map() needs it that way) + uint8_t phase; + uint8_t goingLeft; +} PendulumData; + +void mode_particlePendulum(void) { + ParticleSystem1D *PartSys = nullptr; + PendulumData *pd = nullptr; + + if (SEGMENT.call == 0) { + if (!initParticleSystem1D(PartSys, 0, 1, sizeof(PendulumData))) + FX_FALLBACK_STATIC; + } else { + PartSys = reinterpret_cast(SEGENV.data); + } + if (PartSys == nullptr) FX_FALLBACK_STATIC; + + PartSys->updateSystem(); + pd = reinterpret_cast(PartSys->PSdataEnd); + + uint16_t segLen = SEGLEN; + uint16_t segCenter = segLen / 2; + uint16_t pause_delay = map(SEGMENT.custom2, 0, 255, 500, 20000); + uint16_t cutoff = segLen / 10; + + constexpr uint8_t PHASE_STOPPED = 0U; + constexpr uint8_t PHASE_SWING = 1U; + constexpr uint8_t PHASE_PAUSED = 2U; + + PartSys->setUsedParticles(0); + PartSys->setWrap(false); + PartSys->setBounce(false); + + uint32_t loss = 255 - SEGMENT.custom1; // amount of blurring; using loss spreads out the blur across the whole 1-255 range. + loss = (loss * loss) / 255; // without this, most blur occurred near the highest setting; 0 = no blurring + if (SEGMENT.custom1 > 0 && loss < 2) loss = 2; + PartSys->setMotionBlur(255 - loss); + + PartSys->setColorByPosition(SEGMENT.check1); + + uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.custom3 + SEGMENT.check2; + bool settingsChanged = (SEGENV.aux0 != settingssum); + + if (SEGENV.call == 0 || pd->phase == PHASE_STOPPED || settingsChanged) { + const uint8_t speed = SEGMENT.speed; + if (speed == 0) pd->swing_time = hw_random16(1000, 2400); + else pd->swing_time = map(speed, 1, 255, 2400, 400); + + const uint8_t intensity = SEGMENT.intensity; + if (intensity == 0) pd->offsetX = hw_random16(cutoff, segLen - cutoff); + else pd->offsetX = map(intensity, 1, 255, cutoff, segLen - cutoff); + + // Damping as Q8 fixed-point (0-256 = 0.0-1.0) + const uint8_t custom3 = SEGMENT.custom3; + uint8_t dampingPct; + if (custom3 == 0) dampingPct = hw_random8(75, 95); + else dampingPct = map(custom3, 1, 31, 95, 75); + pd->damping256 = ((uint16_t)dampingPct * 256) / 100; + + // Amplitude in pixels first, then converted to sub-pixel units + uint16_t amplitude_px; + if (pd->offsetX < segCenter) amplitude_px = pd->offsetX; + else amplitude_px = MIN(pd->offsetX, (uint16_t)(segLen - 1 - pd->offsetX)); + pd->amplitude = (int32_t)amplitude_px * PS_P_RADIUS_1D; + + pd->fromPos = (int32_t)pd->offsetX * PS_P_RADIUS_1D; + pd->toPos = pd->fromPos - pd->amplitude; + pd->swingStart = strip.now; + pd->swingDuration = pd->swing_time; + pd->phase = PHASE_SWING; + pd->goingLeft = 1; + pd->currentPos = pd->fromPos; + + PartSys->particles[0].ttl = 300; + PartSys->particleFlags[0].perpetual = true; + PartSys->particleFlags[0].fixed = true; + + SEGENV.aux0 = settingssum; + } + + if (pd->phase == PHASE_SWING) { + uint32_t elapsed = strip.now - pd->swingStart; + if (elapsed > pd->swingDuration) elapsed = pd->swingDuration; + + // angle: 0 -> 32768 represents 0 -> 180 degrees in cos16_t's 16-bit angle space + uint32_t angle = pd->swingDuration ? ((uint32_t)elapsed * 32768) / pd->swingDuration : 32768; + + int32_t cosT16 = cos16_t((uint16_t)angle); // inferred range: -32767..32767 + int32_t frac16 = (32767 - cosT16) >> 1; // 0..32767, represents ease fraction * 32767 + + int32_t delta = pd->toPos - pd->fromPos; // sub-pixel + pd->currentPos = pd->fromPos + (int32_t)(((int64_t)delta * frac16) / 32767); + + if (elapsed >= pd->swingDuration) { + bool perpetual = SEGMENT.check2; + if (!perpetual) { + pd->amplitude = (pd->amplitude * (int32_t)pd->damping256) >> 8; // (Q8 fixed-point) + } + + if (!perpetual && pd->amplitude < PENDULUM_MIN_AMPLITUDE_SUBPX) { + pd->currentPos = (int32_t)pd->offsetX * PS_P_RADIUS_1D; + pd->phase = PHASE_PAUSED; + pd->pauseStart = strip.now; + } else { + int32_t offsetSubpx = (int32_t)pd->offsetX * PS_P_RADIUS_1D; + int32_t nextTo = offsetSubpx + (pd->goingLeft ? pd->amplitude : -pd->amplitude); + pd->fromPos = pd->toPos; + pd->toPos = nextTo; + pd->goingLeft = !(pd->goingLeft); + pd->swingStart = strip.now; + + // duration scales with amplitude/half-segment-length, in Q8 fixed-point + int32_t halfSegSubpx = ((int32_t)(segLen - 1) * PS_P_RADIUS_1D) / 2; + int32_t ratio256 = halfSegSubpx ? (pd->amplitude * 256) / halfSegSubpx : 256; + uint32_t dur = ((uint32_t)pd->swing_time * ratio256) >> 8; + pd->swingDuration = (dur < 200) ? 200 : dur; + } + } + } else if (pd->phase == PHASE_PAUSED) { + pd->currentPos = (int32_t)pd->offsetX * PS_P_RADIUS_1D; + if (strip.now - pd->pauseStart >= pause_delay) { + pd->phase = PHASE_STOPPED; + } + } + + PartSys->particles[0].x = pd->currentPos; + PartSys->particles[0].vx = 0; + + PartSys->update(); +} +static const char _data_FX_MODE_PS_PENDULUM[] PROGMEM = "PS Pendulum@Speed (0=random),X Offset (0=random),Blur,Pause/Delay,Damping/Gravity (0=random), Position Color, Perpetual;!,!;!;1;c2=32,c3=4,o1=1"; + ///////////////////// // UserMod Class // @@ -1616,6 +1796,7 @@ class UserFxUsermod : public Usermod { strip.addEffect(255, &mode_morsecode, _data_FX_MODE_MORSECODE); strip.addEffect(255, &mode_2D_perlinscape, _data_FX_MODE_2D_PERLINSCAPE); strip.addEffect(255, &mode_dissolveplus, _data_FX_MODE_DISSOLVEPLUS); + strip.addEffect(255, &mode_particlePendulum, _data_FX_MODE_PS_PENDULUM); //////////////////////////////////////// // add your effect function(s) here // From cdea9e7973c7ea155d2e365381134a4c744f08c0 Mon Sep 17 00:00:00 2001 From: "DESKTOP-BOB-DEL\\boblo" Date: Tue, 27 Oct 2026 16:39:29 -0700 Subject: [PATCH 2/8] Added AI comment ranges --- usermods/user_fx/user_fx.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 41645b2c13..7804ba17c0 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -1634,6 +1634,8 @@ static const char _data_FX_MODE_DISSOLVEPLUS[] PROGMEM = "Dissolve Plus@Repeat s * Checkbox2 will make the pendulum swing forever (a perpetual pendulum). * aux0 stores the settings checksum to detect changes. */ + +// AI: below section was generated by an AI #define PENDULUM_MIN_AMPLITUDE_SUBPX ((PS_P_RADIUS_1D * 2) / 5) // Custom pendulum state, stored in the extra bytes after the particle system's own data @@ -1651,6 +1653,7 @@ typedef struct { uint8_t phase; uint8_t goingLeft; } PendulumData; +// AI: end void mode_particlePendulum(void) { ParticleSystem1D *PartSys = nullptr; @@ -1680,10 +1683,12 @@ void mode_particlePendulum(void) { PartSys->setWrap(false); PartSys->setBounce(false); + // AI: below section was generated by an AI uint32_t loss = 255 - SEGMENT.custom1; // amount of blurring; using loss spreads out the blur across the whole 1-255 range. loss = (loss * loss) / 255; // without this, most blur occurred near the highest setting; 0 = no blurring if (SEGMENT.custom1 > 0 && loss < 2) loss = 2; PartSys->setMotionBlur(255 - loss); + // AI: end PartSys->setColorByPosition(SEGMENT.check1); @@ -1699,6 +1704,7 @@ void mode_particlePendulum(void) { if (intensity == 0) pd->offsetX = hw_random16(cutoff, segLen - cutoff); else pd->offsetX = map(intensity, 1, 255, cutoff, segLen - cutoff); + // AI: below section was generated by an AI // Damping as Q8 fixed-point (0-256 = 0.0-1.0) const uint8_t custom3 = SEGMENT.custom3; uint8_t dampingPct; @@ -1719,6 +1725,7 @@ void mode_particlePendulum(void) { pd->phase = PHASE_SWING; pd->goingLeft = 1; pd->currentPos = pd->fromPos; + // AI: end PartSys->particles[0].ttl = 300; PartSys->particleFlags[0].perpetual = true; @@ -1728,6 +1735,7 @@ void mode_particlePendulum(void) { } if (pd->phase == PHASE_SWING) { + // AI: below section was generated by an AI uint32_t elapsed = strip.now - pd->swingStart; if (elapsed > pd->swingDuration) elapsed = pd->swingDuration; @@ -1771,7 +1779,8 @@ void mode_particlePendulum(void) { pd->phase = PHASE_STOPPED; } } - + // AI: end + PartSys->particles[0].x = pd->currentPos; PartSys->particles[0].vx = 0; From 360bbae6d142a8b5a267f67f0208038ce21c278f Mon Sep 17 00:00:00 2001 From: "DESKTOP-BOB-DEL\\boblo" Date: Tue, 27 Oct 2026 16:58:49 -0700 Subject: [PATCH 3/8] Added guards for WLED_DISABLE_PARTICLESYSTEM1D --- usermods/user_fx/user_fx.cpp | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 7804ba17c0..710bea885b 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -2,26 +2,8 @@ // for information how FX metadata strings work see https://kno.wled.ge/interfaces/json-api/#effect-metadata -#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) - #include "FXparticleSystem.h" // include particle system code only if at least one system is enabled - #ifdef WLED_DISABLE_PARTICLESYSTEM2D - #define WLED_PS_DONT_REPLACE_2D_FX - #endif - #ifdef WLED_DISABLE_PARTICLESYSTEM1D - #define WLED_PS_DONT_REPLACE_1D_FX - #endif - #ifdef ESP8266 - #if !defined(WLED_DISABLE_PARTICLESYSTEM2D) && !defined(WLED_DISABLE_PARTICLESYSTEM1D) - #error ESP8266 does not support 1D and 2D particle systems simultaneously. Please disable one of them. - #endif - #endif -#else - #define WLED_PS_DONT_REPLACE_1D_FX - #define WLED_PS_DONT_REPLACE_2D_FX -#endif -#ifdef WLED_PS_DONT_REPLACE_FX - #define WLED_PS_DONT_REPLACE_1D_FX - #define WLED_PS_DONT_REPLACE_2D_FX +#ifndef WLED_DISABLE_PARTICLESYSTEM1D + #include "FXparticleSystem.h" // PS Pendulum requires the 1D particle system #endif // paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) @@ -1620,7 +1602,7 @@ static void mode_dissolveplus(void) { static const char _data_FX_MODE_DISSOLVEPLUS[] PROGMEM = "Dissolve Plus@Repeat speed,Dissolve speed,Fill speed,Last one delay,Fade rate,Random,Complete,Last one;!,!;!;;o2=1"; - +#ifndef WLED_DISABLE_PARTICLESYSTEM1D /* / PS Pendulum effect (uses the Particle System) * by Bob Loeffler and claude.ai Particle System by dedehai @@ -1655,7 +1637,7 @@ typedef struct { } PendulumData; // AI: end -void mode_particlePendulum(void) { +static void mode_particlePendulum(void) { ParticleSystem1D *PartSys = nullptr; PendulumData *pd = nullptr; @@ -1780,13 +1762,14 @@ void mode_particlePendulum(void) { } } // AI: end - + PartSys->particles[0].x = pd->currentPos; PartSys->particles[0].vx = 0; PartSys->update(); } static const char _data_FX_MODE_PS_PENDULUM[] PROGMEM = "PS Pendulum@Speed (0=random),X Offset (0=random),Blur,Pause/Delay,Damping/Gravity (0=random), Position Color, Perpetual;!,!;!;1;c2=32,c3=4,o1=1"; +#endif // AI: below section was generated by an AI @@ -2074,7 +2057,9 @@ class UserFxUsermod : public Usermod { strip.addEffect(255, &mode_morsecode, _data_FX_MODE_MORSECODE); strip.addEffect(255, &mode_2D_perlinscape, _data_FX_MODE_2D_PERLINSCAPE); strip.addEffect(255, &mode_dissolveplus, _data_FX_MODE_DISSOLVEPLUS); + #ifndef WLED_DISABLE_PARTICLESYSTEM1D strip.addEffect(255, &mode_particlePendulum, _data_FX_MODE_PS_PENDULUM); + #endif strip.addEffect(255, &mode_2D_jackolantern, _data_FX_MODE_2D_JACKOLANTERN); //////////////////////////////////////// From 779e0a0fee403606bf9eb1d9bc1d15565cf9b0b0 Mon Sep 17 00:00:00 2001 From: "DESKTOP-BOB-DEL\\boblo" Date: Wed, 28 Oct 2026 21:18:36 -0700 Subject: [PATCH 4/8] Removed PS from effect name, changed dampening to gravity, set usedParticles to 1 --- usermods/user_fx/user_fx.cpp | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 710bea885b..9afbb3260a 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -1604,14 +1604,14 @@ static const char _data_FX_MODE_DISSOLVEPLUS[] PROGMEM = "Dissolve Plus@Repeat s #ifndef WLED_DISABLE_PARTICLESYSTEM1D /* -/ PS Pendulum effect (uses the Particle System) -* by Bob Loeffler and claude.ai Particle System by dedehai +/ Pendulum effect +* by Bob Loeffler and claude.ai * First slider (speed) is for the speed of the pendulum LED going back and forth. (0 = random speed) * Second slider (intensity) is for the X offset. In the middle means the center of the pendulum will be in the middle of the LED strip. * Moving the slider left will move the pendulum to the left, etc. (0 = random X offset) * Third slider (blur) is for how much of a trail the pendulum will leave. * Fourth slider (pause/delay) is for how long the effect will pause before restarting the pendulum movement. -* Fifth slider (gravity/damping) is for how much the pendulum movement will be dampened due to gravity or friction. (0 = random damping) +* Fifth slider (gravity) is for how much the pendulum movement will be changed due to gravity. (0 = random gravity) * Checkbox1 will select the LED color based on it's position on the strip. * Checkbox2 will make the pendulum swing forever (a perpetual pendulum). * aux0 stores the settings checksum to detect changes. @@ -1626,7 +1626,7 @@ typedef struct { int32_t fromPos; int32_t toPos; int32_t amplitude; // sub-pixel - uint16_t damping256; // Q8 fixed point: 0-255 represents 0.0-1.0 + uint16_t gravity256; // Q8 fixed point: 0-255 represents 0.0-1.0 uint32_t swingStart; uint32_t swingDuration; uint32_t pauseStart; @@ -1637,7 +1637,7 @@ typedef struct { } PendulumData; // AI: end -static void mode_particlePendulum(void) { +static void mode_Pendulum(void) { ParticleSystem1D *PartSys = nullptr; PendulumData *pd = nullptr; @@ -1661,7 +1661,7 @@ static void mode_particlePendulum(void) { constexpr uint8_t PHASE_SWING = 1U; constexpr uint8_t PHASE_PAUSED = 2U; - PartSys->setUsedParticles(0); + PartSys->setUsedParticles(1); PartSys->setWrap(false); PartSys->setBounce(false); @@ -1687,12 +1687,12 @@ static void mode_particlePendulum(void) { else pd->offsetX = map(intensity, 1, 255, cutoff, segLen - cutoff); // AI: below section was generated by an AI - // Damping as Q8 fixed-point (0-256 = 0.0-1.0) + // Gravity as Q8 fixed-point (0-256 = 0.0-1.0) const uint8_t custom3 = SEGMENT.custom3; - uint8_t dampingPct; - if (custom3 == 0) dampingPct = hw_random8(75, 95); - else dampingPct = map(custom3, 1, 31, 95, 75); - pd->damping256 = ((uint16_t)dampingPct * 256) / 100; + uint8_t gravityPct; + if (custom3 == 0) gravityPct = hw_random8(75, 95); + else gravityPct = map(custom3, 1, 31, 95, 75); + pd->gravity256 = ((uint16_t)gravityPct * 256) / 100; // Amplitude in pixels first, then converted to sub-pixel units uint16_t amplitude_px; @@ -1733,7 +1733,7 @@ static void mode_particlePendulum(void) { if (elapsed >= pd->swingDuration) { bool perpetual = SEGMENT.check2; if (!perpetual) { - pd->amplitude = (pd->amplitude * (int32_t)pd->damping256) >> 8; // (Q8 fixed-point) + pd->amplitude = (pd->amplitude * (int32_t)pd->gravity256) >> 8; // (Q8 fixed-point) } if (!perpetual && pd->amplitude < PENDULUM_MIN_AMPLITUDE_SUBPX) { @@ -1768,7 +1768,7 @@ static void mode_particlePendulum(void) { PartSys->update(); } -static const char _data_FX_MODE_PS_PENDULUM[] PROGMEM = "PS Pendulum@Speed (0=random),X Offset (0=random),Blur,Pause/Delay,Damping/Gravity (0=random), Position Color, Perpetual;!,!;!;1;c2=32,c3=4,o1=1"; +static const char _data_FX_MODE_PENDULUM[] PROGMEM = "Pendulum@Speed (0=random),X Offset (0=random),Blur,Pause/Delay,Gravity (0=random), Position Color, Perpetual;!,!;!;1;c2=32,c3=4,o1=1"; #endif @@ -2058,7 +2058,7 @@ class UserFxUsermod : public Usermod { strip.addEffect(255, &mode_2D_perlinscape, _data_FX_MODE_2D_PERLINSCAPE); strip.addEffect(255, &mode_dissolveplus, _data_FX_MODE_DISSOLVEPLUS); #ifndef WLED_DISABLE_PARTICLESYSTEM1D - strip.addEffect(255, &mode_particlePendulum, _data_FX_MODE_PS_PENDULUM); + strip.addEffect(255, &mode_Pendulum, _data_FX_MODE_PENDULUM); #endif strip.addEffect(255, &mode_2D_jackolantern, _data_FX_MODE_2D_JACKOLANTERN); From e38b7ad0f541746b4afb27e800b39a63c5810a59 Mon Sep 17 00:00:00 2001 From: "DESKTOP-BOB-DEL\\boblo" Date: Thu, 29 Oct 2026 23:38:44 -0700 Subject: [PATCH 5/8] Keep the mapped offset inside the segment. --- usermods/user_fx/user_fx.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 9afbb3260a..8cb00692a0 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -1684,7 +1684,7 @@ static void mode_Pendulum(void) { const uint8_t intensity = SEGMENT.intensity; if (intensity == 0) pd->offsetX = hw_random16(cutoff, segLen - cutoff); - else pd->offsetX = map(intensity, 1, 255, cutoff, segLen - cutoff); + else pd->offsetX = map(intensity, 1, 255, cutoff, segLen - 1 - cutoff); // AI: below section was generated by an AI // Gravity as Q8 fixed-point (0-256 = 0.0-1.0) From a0ad083bb24ec4b26de600f3d20bd134e01d348d Mon Sep 17 00:00:00 2001 From: "DESKTOP-BOB-DEL\\boblo" Date: Fri, 30 Oct 2026 00:36:31 -0700 Subject: [PATCH 6/8] Fall back to static FX if SEGLEN is less than 4 pixels --- usermods/user_fx/user_fx.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 8cb00692a0..3a8e3c7a8b 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -1640,6 +1640,7 @@ typedef struct { static void mode_Pendulum(void) { ParticleSystem1D *PartSys = nullptr; PendulumData *pd = nullptr; + if (SEGLEN < 4) FX_FALLBACK_STATIC; if (SEGMENT.call == 0) { if (!initParticleSystem1D(PartSys, 0, 1, sizeof(PendulumData))) From 1e2e1ea5a528281c372a13917ac7c83530493c56 Mon Sep 17 00:00:00 2001 From: "DESKTOP-BOB-DEL\\boblo" Date: Sat, 31 Oct 2026 16:41:28 -0700 Subject: [PATCH 7/8] Added XL option to select a large pendulum, and removed "Pause" from the Pause/Delay checkbox's text. --- usermods/user_fx/user_fx.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 3a8e3c7a8b..004bbf92fa 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -1610,10 +1610,11 @@ static const char _data_FX_MODE_DISSOLVEPLUS[] PROGMEM = "Dissolve Plus@Repeat s * Second slider (intensity) is for the X offset. In the middle means the center of the pendulum will be in the middle of the LED strip. * Moving the slider left will move the pendulum to the left, etc. (0 = random X offset) * Third slider (blur) is for how much of a trail the pendulum will leave. -* Fourth slider (pause/delay) is for how long the effect will pause before restarting the pendulum movement. +* Fourth slider (delay) is for how long the effect will wait before restarting the pendulum movement. * Fifth slider (gravity) is for how much the pendulum movement will be changed due to gravity. (0 = random gravity) * Checkbox1 will select the LED color based on it's position on the strip. * Checkbox2 will make the pendulum swing forever (a perpetual pendulum). +* Checkbox3 will select the large pendulum. * aux0 stores the settings checksum to detect changes. */ @@ -1656,7 +1657,8 @@ static void mode_Pendulum(void) { uint16_t segLen = SEGLEN; uint16_t segCenter = segLen / 2; uint16_t pause_delay = map(SEGMENT.custom2, 0, 255, 500, 20000); - uint16_t cutoff = segLen / 10; + uint16_t sizePx = SEGMENT.check3 ? 3 : 0; // approx. particle radius in pixels; tune by eye (either 3 or 4) + uint16_t cutoff = segLen / 10 + sizePx; constexpr uint8_t PHASE_STOPPED = 0U; constexpr uint8_t PHASE_SWING = 1U; @@ -1675,9 +1677,11 @@ static void mode_Pendulum(void) { PartSys->setColorByPosition(SEGMENT.check1); - uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.custom3 + SEGMENT.check2; + uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.custom3 + SEGMENT.check2 + SEGMENT.check3; bool settingsChanged = (SEGENV.aux0 != settingssum); + PartSys->setParticleSize(SEGMENT.check3 ? 120 : 1); // large or small particle (pendulum) + if (SEGENV.call == 0 || pd->phase == PHASE_STOPPED || settingsChanged) { const uint8_t speed = SEGMENT.speed; if (speed == 0) pd->swing_time = hw_random16(1000, 2400); @@ -1699,6 +1703,7 @@ static void mode_Pendulum(void) { uint16_t amplitude_px; if (pd->offsetX < segCenter) amplitude_px = pd->offsetX; else amplitude_px = MIN(pd->offsetX, (uint16_t)(segLen - 1 - pd->offsetX)); + amplitude_px = (amplitude_px > sizePx) ? (amplitude_px - sizePx) : 0; // leave room for the particle's width at each end of the swing pd->amplitude = (int32_t)amplitude_px * PS_P_RADIUS_1D; pd->fromPos = (int32_t)pd->offsetX * PS_P_RADIUS_1D; @@ -1736,8 +1741,8 @@ static void mode_Pendulum(void) { if (!perpetual) { pd->amplitude = (pd->amplitude * (int32_t)pd->gravity256) >> 8; // (Q8 fixed-point) } - - if (!perpetual && pd->amplitude < PENDULUM_MIN_AMPLITUDE_SUBPX) { + int32_t minAmplitude = SEGMENT.check3 ? PENDULUM_MIN_AMPLITUDE_SUBPX * 3 : PENDULUM_MIN_AMPLITUDE_SUBPX; + if (!perpetual && pd->amplitude < minAmplitude) { pd->currentPos = (int32_t)pd->offsetX * PS_P_RADIUS_1D; pd->phase = PHASE_PAUSED; pd->pauseStart = strip.now; @@ -1769,7 +1774,7 @@ static void mode_Pendulum(void) { PartSys->update(); } -static const char _data_FX_MODE_PENDULUM[] PROGMEM = "Pendulum@Speed (0=random),X Offset (0=random),Blur,Pause/Delay,Gravity (0=random), Position Color, Perpetual;!,!;!;1;c2=32,c3=4,o1=1"; +static const char _data_FX_MODE_PENDULUM[] PROGMEM = "Pendulum@Speed (0=random),X Offset (0=random),Blur,Delay,Gravity (0=random), Position Color, Perpetual, XL;!,!;!;1;c2=32,c3=4,o1=1"; #endif From 42ad423723d37fceaa49811ac0c4c3760537f302 Mon Sep 17 00:00:00 2001 From: "DESKTOP-BOB-DEL\\boblo" Date: Sun, 1 Nov 2026 00:24:31 -0700 Subject: [PATCH 8/8] Reserve the rendered XL radius before mapping the swing, and changed mode name to lowercase to make consistent with all other effects/modes. --- usermods/user_fx/user_fx.cpp | 56 ++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/usermods/user_fx/user_fx.cpp b/usermods/user_fx/user_fx.cpp index 004bbf92fa..cc5ff56cbb 100644 --- a/usermods/user_fx/user_fx.cpp +++ b/usermods/user_fx/user_fx.cpp @@ -1632,13 +1632,13 @@ typedef struct { uint32_t swingDuration; uint32_t pauseStart; uint16_t swing_time; - uint16_t offsetX; // pixel units (kept as pixels since map() needs it that way) + int32_t offsetSub; // swing center, sub-pixel units (can sit on a half pixel) uint8_t phase; uint8_t goingLeft; } PendulumData; // AI: end -static void mode_Pendulum(void) { +static void mode_pendulum(void) { ParticleSystem1D *PartSys = nullptr; PendulumData *pd = nullptr; if (SEGLEN < 4) FX_FALLBACK_STATIC; @@ -1655,10 +1655,26 @@ static void mode_Pendulum(void) { pd = reinterpret_cast(PartSys->PSdataEnd); uint16_t segLen = SEGLEN; - uint16_t segCenter = segLen / 2; uint16_t pause_delay = map(SEGMENT.custom2, 0, 255, 500, 20000); - uint16_t sizePx = SEGMENT.check3 ? 3 : 0; // approx. particle radius in pixels; tune by eye (either 3 or 4) - uint16_t cutoff = segLen / 10 + sizePx; + + // AI: below section was generated by an AI + constexpr uint8_t PENDULUM_LARGE_SIZE = 120U; + constexpr uint16_t LARGE_RADIUS_PX = 3U; + constexpr uint16_t MIN_SWING_PX = 2U; // smallest swing worth showing for the large pendulum + + bool largeSize = SEGMENT.check3; + uint16_t sizePx = largeSize ? LARGE_RADIUS_PX : 0; + uint16_t cutoff = sizePx + segLen / 10; + + if (largeSize) { + if (cutoff < sizePx + MIN_SWING_PX) cutoff = sizePx + MIN_SWING_PX; + if (segLen < 2 * cutoff + 1) { // segment is too short for the large pendulum, so use the short pendulum + largeSize = false; + sizePx = 0; + cutoff = segLen / 10; + } + } + // AI: end constexpr uint8_t PHASE_STOPPED = 0U; constexpr uint8_t PHASE_SWING = 1U; @@ -1680,7 +1696,7 @@ static void mode_Pendulum(void) { uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.custom3 + SEGMENT.check2 + SEGMENT.check3; bool settingsChanged = (SEGENV.aux0 != settingssum); - PartSys->setParticleSize(SEGMENT.check3 ? 120 : 1); // large or small particle (pendulum) + PartSys->setParticleSize(largeSize ? PENDULUM_LARGE_SIZE : 1); if (SEGENV.call == 0 || pd->phase == PHASE_STOPPED || settingsChanged) { const uint8_t speed = SEGMENT.speed; @@ -1688,8 +1704,8 @@ static void mode_Pendulum(void) { else pd->swing_time = map(speed, 1, 255, 2400, 400); const uint8_t intensity = SEGMENT.intensity; - if (intensity == 0) pd->offsetX = hw_random16(cutoff, segLen - cutoff); - else pd->offsetX = map(intensity, 1, 255, cutoff, segLen - 1 - cutoff); + if (intensity == 0) pd->offsetSub = (int32_t)hw_random16(cutoff, segLen - cutoff) * PS_P_RADIUS_1D; + else pd->offsetSub = map(intensity, 1, 255, (int32_t)cutoff * PS_P_RADIUS_1D, (int32_t)(segLen - 1 - cutoff) * PS_P_RADIUS_1D); // AI: below section was generated by an AI // Gravity as Q8 fixed-point (0-256 = 0.0-1.0) @@ -1699,14 +1715,12 @@ static void mode_Pendulum(void) { else gravityPct = map(custom3, 1, 31, 95, 75); pd->gravity256 = ((uint16_t)gravityPct * 256) / 100; - // Amplitude in pixels first, then converted to sub-pixel units - uint16_t amplitude_px; - if (pd->offsetX < segCenter) amplitude_px = pd->offsetX; - else amplitude_px = MIN(pd->offsetX, (uint16_t)(segLen - 1 - pd->offsetX)); - amplitude_px = (amplitude_px > sizePx) ? (amplitude_px - sizePx) : 0; // leave room for the particle's width at each end of the swing - pd->amplitude = (int32_t)amplitude_px * PS_P_RADIUS_1D; + // distance to the nearest strip end, minus the particle radius + int32_t distanceToEnd = (int32_t)(segLen - 1) * PS_P_RADIUS_1D - pd->offsetSub; + int32_t amp = MIN(pd->offsetSub, distanceToEnd) - (int32_t)sizePx * PS_P_RADIUS_1D; + pd->amplitude = (amp > 0) ? amp : 0; - pd->fromPos = (int32_t)pd->offsetX * PS_P_RADIUS_1D; + pd->fromPos = pd->offsetSub; pd->toPos = pd->fromPos - pd->amplitude; pd->swingStart = strip.now; pd->swingDuration = pd->swing_time; @@ -1741,14 +1755,14 @@ static void mode_Pendulum(void) { if (!perpetual) { pd->amplitude = (pd->amplitude * (int32_t)pd->gravity256) >> 8; // (Q8 fixed-point) } - int32_t minAmplitude = SEGMENT.check3 ? PENDULUM_MIN_AMPLITUDE_SUBPX * 3 : PENDULUM_MIN_AMPLITUDE_SUBPX; + + int32_t minAmplitude = largeSize ? PENDULUM_MIN_AMPLITUDE_SUBPX * 3 : PENDULUM_MIN_AMPLITUDE_SUBPX; if (!perpetual && pd->amplitude < minAmplitude) { - pd->currentPos = (int32_t)pd->offsetX * PS_P_RADIUS_1D; + pd->currentPos = pd->offsetSub; pd->phase = PHASE_PAUSED; pd->pauseStart = strip.now; } else { - int32_t offsetSubpx = (int32_t)pd->offsetX * PS_P_RADIUS_1D; - int32_t nextTo = offsetSubpx + (pd->goingLeft ? pd->amplitude : -pd->amplitude); + int32_t nextTo = pd->offsetSub + (pd->goingLeft ? pd->amplitude : -pd->amplitude); pd->fromPos = pd->toPos; pd->toPos = nextTo; pd->goingLeft = !(pd->goingLeft); @@ -1762,7 +1776,7 @@ static void mode_Pendulum(void) { } } } else if (pd->phase == PHASE_PAUSED) { - pd->currentPos = (int32_t)pd->offsetX * PS_P_RADIUS_1D; + pd->currentPos = pd->offsetSub; if (strip.now - pd->pauseStart >= pause_delay) { pd->phase = PHASE_STOPPED; } @@ -2064,7 +2078,7 @@ class UserFxUsermod : public Usermod { strip.addEffect(255, &mode_2D_perlinscape, _data_FX_MODE_2D_PERLINSCAPE); strip.addEffect(255, &mode_dissolveplus, _data_FX_MODE_DISSOLVEPLUS); #ifndef WLED_DISABLE_PARTICLESYSTEM1D - strip.addEffect(255, &mode_Pendulum, _data_FX_MODE_PENDULUM); + strip.addEffect(255, &mode_pendulum, _data_FX_MODE_PENDULUM); #endif strip.addEffect(255, &mode_2D_jackolantern, _data_FX_MODE_2D_JACKOLANTERN);