Repository navigation
Pendulum (1D) FX in the user_fx usermod #5939
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d54862e
ec4c376
cdea9e7
360bbae
779e0a0
e38b7ad
a0ad083
1e2e1ea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,10 @@ | |
|
|
||
| // for information how FX metadata strings work see https://kno.wled.ge/interfaces/json-api/#effect-metadata | ||
|
|
||
| #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) | ||
| #define PALETTE_SOLID_WRAP (paletteBlend == 1 || paletteBlend == 3) | ||
|
|
||
|
|
@@ -1598,6 +1602,181 @@ 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 | ||
| /* | ||
| / 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 (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. | ||
| */ | ||
|
|
||
| // 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 | ||
| 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 gravity256; // 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; | ||
| // AI: end | ||
|
|
||
| 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))) | ||
| FX_FALLBACK_STATIC; | ||
| } else { | ||
| PartSys = reinterpret_cast<ParticleSystem1D *>(SEGENV.data); | ||
| } | ||
| if (PartSys == nullptr) FX_FALLBACK_STATIC; | ||
|
|
||
| PartSys->updateSystem(); | ||
| pd = reinterpret_cast<PendulumData *>(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; | ||
|
|
||
| constexpr uint8_t PHASE_STOPPED = 0U; | ||
| constexpr uint8_t PHASE_SWING = 1U; | ||
| constexpr uint8_t PHASE_PAUSED = 2U; | ||
|
|
||
| PartSys->setUsedParticles(1); | ||
| 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); | ||
|
|
||
| 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); | ||
| 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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prevent zero-amplitude motion on short segments. For
|
||
|
|
||
| // AI: below section was generated by an AI | ||
| // Gravity as Q8 fixed-point (0-256 = 0.0-1.0) | ||
| const uint8_t custom3 = SEGMENT.custom3; | ||
| 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; | ||
| 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; | ||
| 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; | ||
| // AI: end | ||
|
|
||
| PartSys->particles[0].ttl = 300; | ||
| PartSys->particleFlags[0].perpetual = true; | ||
| PartSys->particleFlags[0].fixed = true; | ||
|
|
||
| SEGENV.aux0 = settingssum; | ||
| } | ||
|
|
||
| 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; | ||
|
|
||
| // 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->gravity256) >> 8; // (Q8 fixed-point) | ||
| } | ||
| 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; | ||
| } 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; | ||
| } | ||
| } | ||
| // AI: end | ||
|
|
||
| PartSys->particles[0].x = pd->currentPos; | ||
| PartSys->particles[0].vx = 0; | ||
|
|
||
| PartSys->update(); | ||
| } | ||
| 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 | ||
|
|
||
|
|
||
| // AI: below section was generated by an AI | ||
| /* | ||
|
|
@@ -1884,6 +2063,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_Pendulum, _data_FX_MODE_PENDULUM); | ||
| #endif | ||
| strip.addEffect(255, &mode_2D_jackolantern, _data_FX_MODE_2D_JACKOLANTERN); | ||
|
|
||
| //////////////////////////////////////// | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reserve the rendered XL radius before mapping the swing.
XL mode reserves three pixels, but particle size 120 renders to a radius of approximately 4.8 pixels. On a 20-pixel segment, the leftmost mapped center is 5; the calculated amplitude is 2, so the particle reaches center 3 and its left side is clipped. On segments of 4–8 pixels, the same clearance calculation leaves zero amplitude for every mapped center. Use a clearance derived from the rendered size. If the segment cannot fit that clearance and a nonzero swing, use the static fallback or a smaller particle. (raw.githubusercontent.com)
As per path instructions: “VERIFY numerical stability / accuracy of arithmetic expressions.”
Source: Path instructions