diff --git a/lib/ESP8266PWM/src/core_esp8266_waveform_phase.cpp b/lib/ESP8266PWM/src/core_esp8266_waveform_phase.cpp new file mode 100644 index 0000000000..68cb9010ec --- /dev/null +++ b/lib/ESP8266PWM/src/core_esp8266_waveform_phase.cpp @@ -0,0 +1,504 @@ +/* esp8266_waveform imported from platform source code + Modified for WLED to work around a fault in the NMI handling, + which can result in the system locking up and hard WDT crashes. + + Imported from https://github.com/esp8266/Arduino/blob/7e0d20e2b9034994f573a236364e0aef17fd66de/cores/esp8266/core_esp8266_waveform_phase.cpp +*/ + + +/* + esp8266_waveform - General purpose waveform generation and control, + supporting outputs on all pins in parallel. + + Copyright (c) 2018 Earle F. Philhower, III. All rights reserved. + Copyright (c) 2020 Dirk O. Kaar. + + The core idea is to have a programmable waveform generator with a unique + high and low period (defined in microseconds or CPU clock cycles). TIMER1 is + set to 1-shot mode and is always loaded with the time until the next edge + of any live waveforms. + + Up to one waveform generator per pin supported. + + Each waveform generator is synchronized to the ESP clock cycle counter, not the + timer. This allows for removing interrupt jitter and delay as the counter + always increments once per 80MHz clock. Changes to a waveform are + contiguous and only take effect on the next waveform transition, + allowing for smooth transitions. + + This replaces older tone(), analogWrite(), and the Servo classes. + + Everywhere in the code where "ccy" or "ccys" is used, it means ESP.getCycleCount() + clock cycle time, or an interval measured in clock cycles, but not TIMER1 + cycles (which may be 2 CPU clock cycles @ 160MHz). + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "core_esp8266_waveform.h" +#include +#include "debug.h" +#include "ets_sys.h" +#include + + +// ----- @willmmiles begin patch ----- +// Linker magic +extern "C" void usePWMFixedNMI(void) {}; + +// NMI crash workaround +// Sometimes the NMI fails to return, stalling the CPU. When this happens, +// the next NMI gets a return address /inside the NMI handler function/. +// We work around this by caching the last NMI return address, and restoring +// the epc3 and eps3 registers to the previous values if the observed epc3 +// happens to be pointing to the _NMILevelVector function. +extern "C" void _NMILevelVector(); +extern "C" void _UserExceptionVector_1(); // the next function after _NMILevelVector +static inline IRAM_ATTR void nmiCrashWorkaround() { + static uintptr_t epc3_backup, eps3_backup; + + uintptr_t epc3, eps3; + __asm__ __volatile__("rsr %0,epc3; rsr %1,eps3":"=a"(epc3),"=a" (eps3)); + if ((epc3 < (uintptr_t) &_NMILevelVector) || (epc3 >= (uintptr_t) &_UserExceptionVector_1)) { + // Address is good; save backup + epc3_backup = epc3; + eps3_backup = eps3; + } else { + // Address is inside the NMI handler -- restore from backup + __asm__ __volatile__("wsr %0,epc3; wsr %1,eps3"::"a"(epc3_backup),"a"(eps3_backup)); + } +} +// ----- @willmmiles end patch ----- + + +// No-op calls to override the PWM implementation +extern "C" void _setPWMFreq_weak(uint32_t freq) { (void) freq; } +extern "C" IRAM_ATTR bool _stopPWM_weak(int pin) { (void) pin; return false; } +extern "C" bool _setPWM_weak(int pin, uint32_t val, uint32_t range) { (void) pin; (void) val; (void) range; return false; } + + +// Timer is 80MHz fixed. 160MHz CPU frequency need scaling. +constexpr bool ISCPUFREQ160MHZ = clockCyclesPerMicrosecond() == 160; +// Maximum delay between IRQs, Timer1, <= 2^23 / 80MHz +constexpr int32_t MAXIRQTICKSCCYS = microsecondsToClockCycles(10000); +// Maximum servicing time for any single IRQ +constexpr uint32_t ISRTIMEOUTCCYS = microsecondsToClockCycles(18); +// The latency between in-ISR rearming of the timer and the earliest firing +constexpr int32_t IRQLATENCYCCYS = microsecondsToClockCycles(2); +// The SDK and hardware take some time to actually get to our NMI code +constexpr int32_t DELTAIRQCCYS = ISCPUFREQ160MHZ ? + microsecondsToClockCycles(2) >> 1 : microsecondsToClockCycles(2); + +// for INFINITE, the NMI proceeds on the waveform without expiry deadline. +// for EXPIRES, the NMI expires the waveform automatically on the expiry ccy. +// for UPDATEEXPIRY, the NMI recomputes the exact expiry ccy and transitions to EXPIRES. +// for UPDATEPHASE, the NMI recomputes the target timings +// for INIT, the NMI initializes nextPeriodCcy, and if expiryCcy != 0 includes UPDATEEXPIRY. +enum class WaveformMode : uint8_t {INFINITE = 0, EXPIRES = 1, UPDATEEXPIRY = 2, UPDATEPHASE = 3, INIT = 4}; + +// Waveform generator can create tones, PWM, and servos +typedef struct { + uint32_t nextPeriodCcy; // ESP clock cycle when a period begins. + uint32_t endDutyCcy; // ESP clock cycle when going from duty to off + int32_t dutyCcys; // Set next off cycle at low->high to maintain phase + int32_t adjDutyCcys; // Temporary correction for next period + int32_t periodCcys; // Set next phase cycle at low->high to maintain phase + uint32_t expiryCcy; // For time-limited waveform, the CPU clock cycle when this waveform must stop. If WaveformMode::UPDATE, temporarily holds relative ccy count + WaveformMode mode; + bool autoPwm; // perform PWM duty to idle cycle ratio correction under high load at the expense of precise timings +} Waveform; + +namespace { + + static struct { + Waveform pins[17]; // State of all possible pins + uint32_t states = 0; // Is the pin high or low, updated in NMI so no access outside the NMI code + uint32_t enabled = 0; // Is it actively running, updated in NMI so no access outside the NMI code + + // Enable lock-free by only allowing updates to waveform.states and waveform.enabled from IRQ service routine + int32_t toSetBits = 0; // Message to the NMI handler to start/modify exactly one waveform + int32_t toDisableBits = 0; // Message to the NMI handler to disable exactly one pin from waveform generation + + // toSetBits temporaries + // cheaper than packing them in every Waveform, since we permit only one use at a time + uint32_t phaseCcy; // positive phase offset ccy count + int8_t alignPhase; // < 0 no phase alignment, otherwise starts waveform in relative phase offset to given pin + + uint32_t(*timer1CB)() = nullptr; + + bool timer1Running = false; + + uint32_t nextEventCcy; + } waveform; + +} + +// Interrupt on/off control +static IRAM_ATTR void timer1Interrupt(); + +// Non-speed critical bits +#pragma GCC optimize ("Os") + +static void initTimer() { + timer1_disable(); + ETS_FRC_TIMER1_INTR_ATTACH(NULL, NULL); + ETS_FRC_TIMER1_NMI_INTR_ATTACH(timer1Interrupt); + timer1_enable(TIM_DIV1, TIM_EDGE, TIM_SINGLE); + waveform.timer1Running = true; + timer1_write(IRQLATENCYCCYS); // Cause an interrupt post-haste +} + +static void IRAM_ATTR deinitTimer() { + ETS_FRC_TIMER1_NMI_INTR_ATTACH(NULL); + timer1_disable(); + timer1_isr_init(); + waveform.timer1Running = false; +} + +extern "C" { + +// Set a callback. Pass in NULL to stop it +void setTimer1Callback_weak(uint32_t (*fn)()) { + waveform.timer1CB = fn; + std::atomic_thread_fence(std::memory_order_acq_rel); + if (!waveform.timer1Running && fn) { + initTimer(); + } else if (waveform.timer1Running && !fn && !waveform.enabled) { + deinitTimer(); + } +} + +// Start up a waveform on a pin, or change the current one. Will change to the new +// waveform smoothly on next low->high transition. For immediate change, stopWaveform() +// first, then it will immediately begin. +int startWaveformClockCycles_weak(uint8_t pin, uint32_t highCcys, uint32_t lowCcys, + uint32_t runTimeCcys, int8_t alignPhase, uint32_t phaseOffsetCcys, bool autoPwm) { + uint32_t periodCcys = highCcys + lowCcys; + if (periodCcys < MAXIRQTICKSCCYS) { + if (!highCcys) { + periodCcys = (MAXIRQTICKSCCYS / periodCcys) * periodCcys; + } + else if (!lowCcys) { + highCcys = periodCcys = (MAXIRQTICKSCCYS / periodCcys) * periodCcys; + } + } + // sanity checks, including mixed signed/unsigned arithmetic safety + if ((pin > 16) || isFlashInterfacePin(pin) || (alignPhase > 16) || + static_cast(periodCcys) <= 0 || + static_cast(highCcys) < 0 || static_cast(lowCcys) < 0) { + return false; + } + Waveform& wave = waveform.pins[pin]; + wave.dutyCcys = highCcys; + wave.adjDutyCcys = 0; + wave.periodCcys = periodCcys; + wave.autoPwm = autoPwm; + waveform.alignPhase = (alignPhase < 0) ? -1 : alignPhase; + waveform.phaseCcy = phaseOffsetCcys; + + std::atomic_thread_fence(std::memory_order_acquire); + const uint32_t pinBit = 1UL << pin; + if (!(waveform.enabled & pinBit)) { + // wave.nextPeriodCcy and wave.endDutyCcy are initialized by the ISR + wave.expiryCcy = runTimeCcys; // in WaveformMode::INIT, temporarily hold relative cycle count + wave.mode = WaveformMode::INIT; + if (!wave.dutyCcys) { + // If initially at zero duty cycle, force GPIO off + if (pin == 16) { + GP16O = 0; + } + else { + GPOC = pinBit; + } + } + std::atomic_thread_fence(std::memory_order_release); + waveform.toSetBits = 1UL << pin; + std::atomic_thread_fence(std::memory_order_release); + if (!waveform.timer1Running) { + initTimer(); + } + else if (T1V > IRQLATENCYCCYS) { + // Must not interfere if Timer is due shortly + timer1_write(IRQLATENCYCCYS); + } + } + else { + wave.mode = WaveformMode::INFINITE; // turn off possible expiry to make update atomic from NMI + std::atomic_thread_fence(std::memory_order_release); + if (runTimeCcys) { + wave.expiryCcy = runTimeCcys; // in WaveformMode::UPDATEEXPIRY, temporarily hold relative cycle count + wave.mode = WaveformMode::UPDATEEXPIRY; + std::atomic_thread_fence(std::memory_order_release); + waveform.toSetBits = 1UL << pin; + } else if (alignPhase >= 0) { + // @willmmiles new feature + wave.mode = WaveformMode::UPDATEPHASE; // recalculate start + std::atomic_thread_fence(std::memory_order_release); + waveform.toSetBits = 1UL << pin; + } + } + std::atomic_thread_fence(std::memory_order_acq_rel); + while (waveform.toSetBits) { + esp_yield(); // Wait for waveform to update + std::atomic_thread_fence(std::memory_order_acquire); + } + return true; +} + +// Stops a waveform on a pin +IRAM_ATTR int stopWaveform_weak(uint8_t pin) { + // Can't possibly need to stop anything if there is no timer active + if (!waveform.timer1Running) { + return false; + } + // If user sends in a pin >16 but <32, this will always point to a 0 bit + // If they send >=32, then the shift will result in 0 and it will also return false + std::atomic_thread_fence(std::memory_order_acquire); + const uint32_t pinBit = 1UL << pin; + if (waveform.enabled & pinBit) { + waveform.toDisableBits = 1UL << pin; + std::atomic_thread_fence(std::memory_order_release); + // Must not interfere if Timer is due shortly + if (T1V > IRQLATENCYCCYS) { + timer1_write(IRQLATENCYCCYS); + } + while (waveform.toDisableBits) { + /* no-op */ // Can't delay() since stopWaveform may be called from an IRQ + std::atomic_thread_fence(std::memory_order_acquire); + } + } + if (!waveform.enabled && !waveform.timer1CB) { + deinitTimer(); + } + return true; +} + +}; + +// Speed critical bits +#pragma GCC optimize ("O2") + +// For dynamic CPU clock frequency switch in loop the scaling logic would have to be adapted. +// Using constexpr makes sure that the CPU clock frequency is compile-time fixed. +static inline IRAM_ATTR int32_t scaleCcys(const int32_t ccys, const bool isCPU2X) { + if (ISCPUFREQ160MHZ) { + return isCPU2X ? ccys : (ccys >> 1); + } + else { + return isCPU2X ? (ccys << 1) : ccys; + } +} + +static IRAM_ATTR void timer1Interrupt() { + const uint32_t isrStartCcy = ESP.getCycleCount(); + //int32_t clockDrift = isrStartCcy - waveform.nextEventCcy; + + // ----- @willmmiles begin patch ----- + nmiCrashWorkaround(); + // ----- @willmmiles end patch ----- + + const bool isCPU2X = CPU2X & 1; + if ((waveform.toSetBits && !(waveform.enabled & waveform.toSetBits)) || waveform.toDisableBits) { + // Handle enable/disable requests from main app. + waveform.enabled = (waveform.enabled & ~waveform.toDisableBits) | waveform.toSetBits; // Set the requested waveforms on/off + // Find the first GPIO being generated by checking GCC's find-first-set (returns 1 + the bit of the first 1 in an int32_t) + waveform.toDisableBits = 0; + } + + if (waveform.toSetBits) { + const int toSetPin = __builtin_ffs(waveform.toSetBits) - 1; + Waveform& wave = waveform.pins[toSetPin]; + switch (wave.mode) { + case WaveformMode::INIT: + waveform.states &= ~waveform.toSetBits; // Clear the state of any just started + if (waveform.alignPhase >= 0 && waveform.enabled & (1UL << waveform.alignPhase)) { + wave.nextPeriodCcy = waveform.pins[waveform.alignPhase].nextPeriodCcy + scaleCcys(waveform.phaseCcy, isCPU2X); + } + else { + wave.nextPeriodCcy = waveform.nextEventCcy; + } + if (!wave.expiryCcy) { + wave.mode = WaveformMode::INFINITE; + break; + } + // fall through + case WaveformMode::UPDATEEXPIRY: + // in WaveformMode::UPDATEEXPIRY, expiryCcy temporarily holds relative CPU cycle count + wave.expiryCcy = wave.nextPeriodCcy + scaleCcys(wave.expiryCcy, isCPU2X); + wave.mode = WaveformMode::EXPIRES; + break; + // @willmmiles new feature + case WaveformMode::UPDATEPHASE: + // in WaveformMode::UPDATEPHASE, we recalculate the targets + if ((waveform.alignPhase >= 0) && (waveform.enabled & (1UL << waveform.alignPhase))) { + // Compute phase shift to realign with target + auto const newPeriodCcy = waveform.pins[waveform.alignPhase].nextPeriodCcy + scaleCcys(waveform.phaseCcy, isCPU2X); + auto const period = scaleCcys(wave.periodCcys, isCPU2X); + auto shift = ((static_cast (newPeriodCcy - wave.nextPeriodCcy) + period/2) % period) - (period/2); + wave.nextPeriodCcy += static_cast(shift); + if (static_cast(wave.endDutyCcy - wave.nextPeriodCcy) > 0) { + wave.endDutyCcy = wave.nextPeriodCcy; + } + } + default: + break; + } + waveform.toSetBits = 0; + } + + // Exit the loop if the next event, if any, is sufficiently distant. + const uint32_t isrTimeoutCcy = isrStartCcy + ISRTIMEOUTCCYS; + uint32_t busyPins = waveform.enabled; + waveform.nextEventCcy = isrStartCcy + MAXIRQTICKSCCYS; + + uint32_t now = ESP.getCycleCount(); + uint32_t isrNextEventCcy = now; + while (busyPins) { + if (static_cast(isrNextEventCcy - now) > IRQLATENCYCCYS) { + waveform.nextEventCcy = isrNextEventCcy; + break; + } + isrNextEventCcy = waveform.nextEventCcy; + uint32_t loopPins = busyPins; + while (loopPins) { + const int pin = __builtin_ffsl(loopPins) - 1; + const uint32_t pinBit = 1UL << pin; + loopPins ^= pinBit; + + Waveform& wave = waveform.pins[pin]; + +/* @willmmiles - wtf? We don't want to accumulate drift + if (clockDrift) { + wave.endDutyCcy += clockDrift; + wave.nextPeriodCcy += clockDrift; + wave.expiryCcy += clockDrift; + } +*/ + + uint32_t waveNextEventCcy = (waveform.states & pinBit) ? wave.endDutyCcy : wave.nextPeriodCcy; + if (WaveformMode::EXPIRES == wave.mode && + static_cast(waveNextEventCcy - wave.expiryCcy) >= 0 && + static_cast(now - wave.expiryCcy) >= 0) { + // Disable any waveforms that are done + waveform.enabled ^= pinBit; + busyPins ^= pinBit; + } + else { + const int32_t overshootCcys = now - waveNextEventCcy; + if (overshootCcys >= 0) { + const int32_t periodCcys = scaleCcys(wave.periodCcys, isCPU2X); + if (waveform.states & pinBit) { + // active configuration and forward are 100% duty + if (wave.periodCcys == wave.dutyCcys) { + wave.nextPeriodCcy += periodCcys; + wave.endDutyCcy = wave.nextPeriodCcy; + } + else { + if (wave.autoPwm) { + wave.adjDutyCcys += overshootCcys; + } + waveform.states ^= pinBit; + if (16 == pin) { + GP16O = 0; + } + else { + GPOC = pinBit; + } + } + waveNextEventCcy = wave.nextPeriodCcy; + } + else { + wave.nextPeriodCcy += periodCcys; + if (!wave.dutyCcys) { + wave.endDutyCcy = wave.nextPeriodCcy; + } + else { + int32_t dutyCcys = scaleCcys(wave.dutyCcys, isCPU2X); + if (dutyCcys <= wave.adjDutyCcys) { + dutyCcys >>= 1; + wave.adjDutyCcys -= dutyCcys; + } + else if (wave.adjDutyCcys) { + dutyCcys -= wave.adjDutyCcys; + wave.adjDutyCcys = 0; + } + wave.endDutyCcy = now + dutyCcys; + if (static_cast(wave.endDutyCcy - wave.nextPeriodCcy) > 0) { + wave.endDutyCcy = wave.nextPeriodCcy; + } + waveform.states |= pinBit; + if (16 == pin) { + GP16O = 1; + } + else { + GPOS = pinBit; + } + } + waveNextEventCcy = wave.endDutyCcy; + } + + if (WaveformMode::EXPIRES == wave.mode && static_cast(waveNextEventCcy - wave.expiryCcy) > 0) { + waveNextEventCcy = wave.expiryCcy; + } + } + + if (static_cast(waveNextEventCcy - isrTimeoutCcy) >= 0) { + busyPins ^= pinBit; + if (static_cast(waveform.nextEventCcy - waveNextEventCcy) > 0) { + waveform.nextEventCcy = waveNextEventCcy; + } + } + else if (static_cast(isrNextEventCcy - waveNextEventCcy) > 0) { + isrNextEventCcy = waveNextEventCcy; + } + } + now = ESP.getCycleCount(); + } + //clockDrift = 0; + } + + int32_t callbackCcys = 0; + if (waveform.timer1CB) { + callbackCcys = scaleCcys(waveform.timer1CB(), isCPU2X); + } + now = ESP.getCycleCount(); + int32_t nextEventCcys = waveform.nextEventCcy - now; + // Account for unknown duration of timer1CB(). + if (waveform.timer1CB && nextEventCcys > callbackCcys) { + waveform.nextEventCcy = now + callbackCcys; + nextEventCcys = callbackCcys; + } + + // Timer is 80MHz fixed. 160MHz CPU frequency need scaling. + int32_t deltaIrqCcys = DELTAIRQCCYS; + int32_t irqLatencyCcys = IRQLATENCYCCYS; + if (isCPU2X) { + nextEventCcys >>= 1; + deltaIrqCcys >>= 1; + irqLatencyCcys >>= 1; + } + + // Firing timer too soon, the NMI occurs before ISR has returned. + if (nextEventCcys < irqLatencyCcys + deltaIrqCcys) { + waveform.nextEventCcy = now + IRQLATENCYCCYS + DELTAIRQCCYS; + nextEventCcys = irqLatencyCcys; + } + else { + nextEventCcys -= deltaIrqCcys; + } + + // Register access is fast and edge IRQ was configured before. + T1L = nextEventCcys; +} diff --git a/lib/ESP8266PWM/src/core_esp8266_waveform_pwm.cpp b/lib/ESP8266PWM/src/core_esp8266_waveform_pwm.cpp deleted file mode 100644 index 78c7160d90..0000000000 --- a/lib/ESP8266PWM/src/core_esp8266_waveform_pwm.cpp +++ /dev/null @@ -1,717 +0,0 @@ -/* esp8266_waveform imported from platform source code - Modified for WLED to work around a fault in the NMI handling, - which can result in the system locking up and hard WDT crashes. - - Imported from https://github.com/esp8266/Arduino/blob/7e0d20e2b9034994f573a236364e0aef17fd66de/cores/esp8266/core_esp8266_waveform_pwm.cpp -*/ - -/* - esp8266_waveform - General purpose waveform generation and control, - supporting outputs on all pins in parallel. - - Copyright (c) 2018 Earle F. Philhower, III. All rights reserved. - - The core idea is to have a programmable waveform generator with a unique - high and low period (defined in microseconds or CPU clock cycles). TIMER1 - is set to 1-shot mode and is always loaded with the time until the next - edge of any live waveforms. - - Up to one waveform generator per pin supported. - - Each waveform generator is synchronized to the ESP clock cycle counter, not - the timer. This allows for removing interrupt jitter and delay as the - counter always increments once per 80MHz clock. Changes to a waveform are - contiguous and only take effect on the next waveform transition, - allowing for smooth transitions. - - This replaces older tone(), analogWrite(), and the Servo classes. - - Everywhere in the code where "cycles" is used, it means ESP.getCycleCount() - clock cycle count, or an interval measured in CPU clock cycles, but not - TIMER1 cycles (which may be 2 CPU clock cycles @ 160MHz). - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -*/ - - -#include -#include -#include "ets_sys.h" -#include "core_esp8266_waveform.h" -#include "user_interface.h" - -extern "C" { - -// Linker magic -void usePWMFixedNMI() {}; - -// Maximum delay between IRQs -#define MAXIRQUS (10000) - -// Waveform generator can create tones, PWM, and servos -typedef struct { - uint32_t nextServiceCycle; // ESP cycle timer when a transition required - uint32_t expiryCycle; // For time-limited waveform, the cycle when this waveform must stop - uint32_t timeHighCycles; // Actual running waveform period (adjusted using desiredCycles) - uint32_t timeLowCycles; // - uint32_t desiredHighCycles; // Ideal waveform period to drive the error signal - uint32_t desiredLowCycles; // - uint32_t lastEdge; // Cycle when this generator last changed -} Waveform; - -class WVFState { -public: - Waveform waveform[17]; // State of all possible pins - uint32_t waveformState = 0; // Is the pin high or low, updated in NMI so no access outside the NMI code - uint32_t waveformEnabled = 0; // Is it actively running, updated in NMI so no access outside the NMI code - - // Enable lock-free by only allowing updates to waveformState and waveformEnabled from IRQ service routine - uint32_t waveformToEnable = 0; // Message to the NMI handler to start a waveform on a inactive pin - uint32_t waveformToDisable = 0; // Message to the NMI handler to disable a pin from waveform generation - - uint32_t waveformToChange = 0; // Mask of pin to change. One bit set in main app, cleared when effected in the NMI - uint32_t waveformNewHigh = 0; - uint32_t waveformNewLow = 0; - - uint32_t (*timer1CB)() = NULL; - - // Optimize the NMI inner loop by keeping track of the min and max GPIO that we - // are generating. In the common case (1 PWM) these may be the same pin and - // we can avoid looking at the other pins. - uint16_t startPin = 0; - uint16_t endPin = 0; -}; -static WVFState wvfState; - - -// Ensure everything is read/written to RAM -#define MEMBARRIER() { __asm__ volatile("" ::: "memory"); } - -// Non-speed critical bits -#pragma GCC optimize ("Os") - -// Interrupt on/off control -static IRAM_ATTR void timer1Interrupt(); -static bool timerRunning = false; - -static __attribute__((noinline)) void initTimer() { - if (!timerRunning) { - timer1_disable(); - ETS_FRC_TIMER1_INTR_ATTACH(NULL, NULL); - ETS_FRC_TIMER1_NMI_INTR_ATTACH(timer1Interrupt); - timer1_enable(TIM_DIV1, TIM_EDGE, TIM_SINGLE); - timerRunning = true; - timer1_write(microsecondsToClockCycles(10)); - } -} - -static IRAM_ATTR void forceTimerInterrupt() { - if (T1L > microsecondsToClockCycles(10)) { - T1L = microsecondsToClockCycles(10); - } -} - -// PWM implementation using special purpose state machine -// -// Keep an ordered list of pins with the delta in cycles between each -// element, with a terminal entry making up the remainder of the PWM -// period. With this method sum(all deltas) == PWM period clock cycles. -// -// At t=0 set all pins high and set the timeout for the 1st edge. -// On interrupt, if we're at the last element reset to t=0 state -// Otherwise, clear that pin down and set delay for next element -// and so forth. - -constexpr int maxPWMs = 8; - -// PWM machine state -typedef struct PWMState { - uint32_t mask; // Bitmask of active pins - uint32_t cnt; // How many entries - uint32_t idx; // Where the state machine is along the list - uint8_t pin[maxPWMs + 1]; - uint32_t delta[maxPWMs + 1]; - uint32_t nextServiceCycle; // Clock cycle for next step - struct PWMState *pwmUpdate; // Set by main code, cleared by ISR -} PWMState; - -static PWMState pwmState; -static uint32_t _pwmFreq = 1000; -static uint32_t _pwmPeriod = microsecondsToClockCycles(1000000UL) / _pwmFreq; - - -// If there are no more scheduled activities, shut down Timer 1. -// Otherwise, do nothing. -static IRAM_ATTR void disableIdleTimer() { - if (timerRunning && !wvfState.waveformEnabled && !pwmState.cnt && !wvfState.timer1CB) { - ETS_FRC_TIMER1_NMI_INTR_ATTACH(NULL); - timer1_disable(); - timer1_isr_init(); - timerRunning = false; - } -} - -// Notify the NMI that a new PWM state is available through the mailbox. -// Wait for mailbox to be emptied (either busy or delay() as needed) -static IRAM_ATTR void _notifyPWM(PWMState *p, bool idle) { - p->pwmUpdate = nullptr; - pwmState.pwmUpdate = p; - MEMBARRIER(); - forceTimerInterrupt(); - while (pwmState.pwmUpdate) { - if (idle) { - esp_yield(); - } - MEMBARRIER(); - } -} - -static void _addPWMtoList(PWMState &p, int pin, uint32_t val, uint32_t range); - - -// Called when analogWriteFreq() changed to update the PWM total period -//extern void _setPWMFreq_weak(uint32_t freq) __attribute__((weak)); -void _setPWMFreq_weak(uint32_t freq) { - _pwmFreq = freq; - - // Convert frequency into clock cycles - uint32_t cc = microsecondsToClockCycles(1000000UL) / freq; - - // Simple static adjustment to bring period closer to requested due to overhead - // Empirically determined as a constant PWM delay and a function of the number of PWMs -#if F_CPU == 80000000 - cc -= ((microsecondsToClockCycles(pwmState.cnt) * 13) >> 4) + 110; -#else - cc -= ((microsecondsToClockCycles(pwmState.cnt) * 10) >> 4) + 75; -#endif - - if (cc == _pwmPeriod) { - return; // No change - } - - _pwmPeriod = cc; - - if (pwmState.cnt) { - PWMState p; // The working copy since we can't edit the one in use - p.mask = 0; - p.cnt = 0; - for (uint32_t i = 0; i < pwmState.cnt; i++) { - auto pin = pwmState.pin[i]; - _addPWMtoList(p, pin, wvfState.waveform[pin].desiredHighCycles, wvfState.waveform[pin].desiredLowCycles); - } - // Update and wait for mailbox to be emptied - initTimer(); - _notifyPWM(&p, true); - disableIdleTimer(); - } -} -/* -static void _setPWMFreq_bound(uint32_t freq) __attribute__((weakref("_setPWMFreq_weak"))); -void _setPWMFreq(uint32_t freq) { - _setPWMFreq_bound(freq); -} -*/ - -// Helper routine to remove an entry from the state machine -// and clean up any marked-off entries -static void _cleanAndRemovePWM(PWMState *p, int pin) { - uint32_t leftover = 0; - uint32_t in, out; - for (in = 0, out = 0; in < p->cnt; in++) { - if ((p->pin[in] != pin) && (p->mask & (1<pin[in]))) { - p->pin[out] = p->pin[in]; - p->delta[out] = p->delta[in] + leftover; - leftover = 0; - out++; - } else { - leftover += p->delta[in]; - p->mask &= ~(1<pin[in]); - } - } - p->cnt = out; - // Final pin is never used: p->pin[out] = 0xff; - p->delta[out] = p->delta[in] + leftover; -} - - -// Disable PWM on a specific pin (i.e. when a digitalWrite or analogWrite(0%/100%)) -//extern bool _stopPWM_weak(uint8_t pin) __attribute__((weak)); -IRAM_ATTR bool _stopPWM_weak(uint8_t pin) { - if (!((1<= _pwmPeriod) { - cc = _pwmPeriod - 1; - } - - if (p.cnt == 0) { - // Starting up from scratch, special case 1st element and PWM period - p.pin[0] = pin; - p.delta[0] = cc; - // Final pin is never used: p.pin[1] = 0xff; - p.delta[1] = _pwmPeriod - cc; - } else { - uint32_t ttl = 0; - uint32_t i; - // Skip along until we're at the spot to insert - for (i=0; (i <= p.cnt) && (ttl + p.delta[i] < cc); i++) { - ttl += p.delta[i]; - } - // Shift everything out by one to make space for new edge - for (int32_t j = p.cnt; j >= (int)i; j--) { - p.pin[j + 1] = p.pin[j]; - p.delta[j + 1] = p.delta[j]; - } - int off = cc - ttl; // The delta from the last edge to the one we're inserting - p.pin[i] = pin; - p.delta[i] = off; // Add the delta to this new pin - p.delta[i + 1] -= off; // And subtract it from the follower to keep sum(deltas) constant - } - p.cnt++; - p.mask |= 1<= maxPWMs) { - return false; // No space left - } - - // Sanity check for all-on/off - uint32_t cc = (_pwmPeriod * val) / range; - if ((cc == 0) || (cc >= _pwmPeriod)) { - digitalWrite(pin, cc ? HIGH : LOW); - return true; - } - - _addPWMtoList(p, pin, val, range); - - // Set mailbox and wait for ISR to copy it over - initTimer(); - _notifyPWM(&p, true); - disableIdleTimer(); - - // Potentially recalculate the PWM period if we've added another pin - _setPWMFreq(_pwmFreq); - - return true; -} -/* -static bool _setPWM_bound(int pin, uint32_t val, uint32_t range) __attribute__((weakref("_setPWM_weak"))); -bool _setPWM(int pin, uint32_t val, uint32_t range) { - return _setPWM_bound(pin, val, range); -} -*/ - -// Start up a waveform on a pin, or change the current one. Will change to the new -// waveform smoothly on next low->high transition. For immediate change, stopWaveform() -// first, then it will immediately begin. -//extern int startWaveformClockCycles_weak(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles, int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) __attribute__((weak)); -int startWaveformClockCycles_weak(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles, - int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) { - (void) alignPhase; - (void) phaseOffsetUS; - (void) autoPwm; - - if ((pin > 16) || isFlashInterfacePin(pin) || (timeHighCycles == 0)) { - return false; - } - Waveform *wave = &wvfState.waveform[pin]; - wave->expiryCycle = runTimeCycles ? ESP.getCycleCount() + runTimeCycles : 0; - if (runTimeCycles && !wave->expiryCycle) { - wave->expiryCycle = 1; // expiryCycle==0 means no timeout, so avoid setting it - } - - _stopPWM(pin); // Make sure there's no PWM live here - - uint32_t mask = 1<timeHighCycles = timeHighCycles; - wave->desiredHighCycles = timeHighCycles; - wave->timeLowCycles = timeLowCycles; - wave->desiredLowCycles = timeLowCycles; - wave->lastEdge = 0; - wave->nextServiceCycle = ESP.getCycleCount() + microsecondsToClockCycles(1); - wvfState.waveformToEnable |= mask; - MEMBARRIER(); - initTimer(); - forceTimerInterrupt(); - while (wvfState.waveformToEnable) { - esp_yield(); // Wait for waveform to update - MEMBARRIER(); - } - } - - return true; -} -/* -static int startWaveformClockCycles_bound(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles, int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) __attribute__((weakref("startWaveformClockCycles_weak"))); -int startWaveformClockCycles(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles, int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) { - return startWaveformClockCycles_bound(pin, timeHighCycles, timeLowCycles, runTimeCycles, alignPhase, phaseOffsetUS, autoPwm); -} - - -// This version falls-thru to the proper startWaveformClockCycles call and is invariant across waveform generators -int startWaveform(uint8_t pin, uint32_t timeHighUS, uint32_t timeLowUS, uint32_t runTimeUS, - int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) { - return startWaveformClockCycles_bound(pin, - microsecondsToClockCycles(timeHighUS), microsecondsToClockCycles(timeLowUS), - microsecondsToClockCycles(runTimeUS), alignPhase, microsecondsToClockCycles(phaseOffsetUS), autoPwm); -} -*/ - -// Set a callback. Pass in NULL to stop it -//extern void setTimer1Callback_weak(uint32_t (*fn)()) __attribute__((weak)); -void setTimer1Callback_weak(uint32_t (*fn)()) { - wvfState.timer1CB = fn; - if (fn) { - initTimer(); - forceTimerInterrupt(); - } - disableIdleTimer(); -} -/* -static void setTimer1Callback_bound(uint32_t (*fn)()) __attribute__((weakref("setTimer1Callback_weak"))); -void setTimer1Callback(uint32_t (*fn)()) { - setTimer1Callback_bound(fn); -} -*/ - -// Stops a waveform on a pin -//extern int stopWaveform_weak(uint8_t pin) __attribute__((weak)); -IRAM_ATTR int stopWaveform_weak(uint8_t pin) { - // Can't possibly need to stop anything if there is no timer active - if (!timerRunning) { - return false; - } - // If user sends in a pin >16 but <32, this will always point to a 0 bit - // If they send >=32, then the shift will result in 0 and it will also return false - uint32_t mask = 1<= (uintptr_t) &_UserExceptionVector_1)) { - // Address is good; save backup - epc3_backup = epc3; - eps3_backup = eps3; - } else { - // Address is inside the NMI handler -- restore from backup - __asm__ __volatile__("wsr %0,epc3; wsr %1,eps3"::"a"(epc3_backup),"a"(eps3_backup)); - } -} -// ----- @willmmiles end patch ----- - - -// The SDK and hardware take some time to actually get to our NMI code, so -// decrement the next IRQ's timer value by a bit so we can actually catch the -// real CPU cycle counter we want for the waveforms. - -// The SDK also sometimes is running at a different speed the the Arduino core -// so the ESP cycle counter is actually running at a variable speed. -// adjust(x) takes care of adjusting a delta clock cycle amount accordingly. -#if F_CPU == 80000000 - #define DELTAIRQ (microsecondsToClockCycles(9)/4) - #define adjust(x) ((x) << (turbo ? 1 : 0)) -#else - #define DELTAIRQ (microsecondsToClockCycles(9)/8) - #define adjust(x) ((x) >> 0) -#endif - -// When the time to the next edge is greater than this, RTI and set another IRQ to minimize CPU usage -#define MINIRQTIME microsecondsToClockCycles(6) - -static IRAM_ATTR void timer1Interrupt() { - // ----- @willmmiles begin patch ----- - nmiCrashWorkaround(); - // ----- @willmmiles end patch ----- - - // Flag if the core is at 160 MHz, for use by adjust() - bool turbo = (*(uint32_t*)0x3FF00014) & 1 ? true : false; - - uint32_t nextEventCycle = GetCycleCountIRQ() + microsecondsToClockCycles(MAXIRQUS); - uint32_t timeoutCycle = GetCycleCountIRQ() + microsecondsToClockCycles(14); - - if (wvfState.waveformToEnable || wvfState.waveformToDisable) { - // Handle enable/disable requests from main app - wvfState.waveformEnabled = (wvfState.waveformEnabled & ~wvfState.waveformToDisable) | wvfState.waveformToEnable; // Set the requested waveforms on/off - wvfState.waveformState &= ~wvfState.waveformToEnable; // And clear the state of any just started - wvfState.waveformToEnable = 0; - wvfState.waveformToDisable = 0; - // No mem barrier. Globals must be written to RAM on ISR exit. - // Find the first GPIO being generated by checking GCC's find-first-set (returns 1 + the bit of the first 1 in an int32_t) - wvfState.startPin = __builtin_ffs(wvfState.waveformEnabled) - 1; - // Find the last bit by subtracting off GCC's count-leading-zeros (no offset in this one) - wvfState.endPin = 32 - __builtin_clz(wvfState.waveformEnabled); - } else if (!pwmState.cnt && pwmState.pwmUpdate) { - // Start up the PWM generator by copying from the mailbox - pwmState.cnt = 1; - pwmState.idx = 1; // Ensure copy this cycle, cause it to start at t=0 - pwmState.nextServiceCycle = GetCycleCountIRQ(); // Do it this loop! - // No need for mem barrier here. Global must be written by IRQ exit - } - - bool done = false; - if (wvfState.waveformEnabled || pwmState.cnt) { - do { - nextEventCycle = GetCycleCountIRQ() + microsecondsToClockCycles(MAXIRQUS); - - // PWM state machine implementation - if (pwmState.cnt) { - int32_t cyclesToGo; - do { - cyclesToGo = pwmState.nextServiceCycle - GetCycleCountIRQ(); - if (cyclesToGo < 0) { - if (pwmState.idx == pwmState.cnt) { // Start of pulses, possibly copy new - if (pwmState.pwmUpdate) { - // Do the memory copy from temp to global and clear mailbox - pwmState = *(PWMState*)pwmState.pwmUpdate; - } - GPOS = pwmState.mask; // Set all active pins high - if (pwmState.mask & (1<<16)) { - GP16O = 1; - } - pwmState.idx = 0; - } else { - do { - // Drop the pin at this edge - if (pwmState.mask & (1<expiryCycle) { - int32_t expiryToGo = wave->expiryCycle - now; - if (expiryToGo < 0) { - // Done, remove! - if (i == 16) { - GP16O = 0; - } - GPOC = mask; - wvfState.waveformEnabled &= ~mask; - continue; - } - } - - // Check for toggles - int32_t cyclesToGo = wave->nextServiceCycle - now; - if (cyclesToGo < 0) { - uint32_t nextEdgeCycles; - uint32_t desired = 0; - uint32_t *timeToUpdate; - wvfState.waveformState ^= mask; - if (wvfState.waveformState & mask) { - if (i == 16) { - GP16O = 1; - } - GPOS = mask; - - if (wvfState.waveformToChange & mask) { - // Copy over next full-cycle timings - wave->timeHighCycles = wvfState.waveformNewHigh; - wave->desiredHighCycles = wvfState.waveformNewHigh; - wave->timeLowCycles = wvfState.waveformNewLow; - wave->desiredLowCycles = wvfState.waveformNewLow; - wave->lastEdge = 0; - wvfState.waveformToChange = 0; - } - if (wave->lastEdge) { - desired = wave->desiredLowCycles; - timeToUpdate = &wave->timeLowCycles; - } - nextEdgeCycles = wave->timeHighCycles; - } else { - if (i == 16) { - GP16O = 0; - } - GPOC = mask; - desired = wave->desiredHighCycles; - timeToUpdate = &wave->timeHighCycles; - nextEdgeCycles = wave->timeLowCycles; - } - if (desired) { - desired = adjust(desired); - int32_t err = desired - (now - wave->lastEdge); - if (abs(err) < desired) { // If we've lost > the entire phase, ignore this error signal - err /= 2; - *timeToUpdate += err; - } - } - nextEdgeCycles = adjust(nextEdgeCycles); - wave->nextServiceCycle = now + nextEdgeCycles; - wave->lastEdge = now; - } - nextEventCycle = earliest(nextEventCycle, wave->nextServiceCycle); - } - - // Exit the loop if we've hit the fixed runtime limit or the next event is known to be after that timeout would occur - uint32_t now = GetCycleCountIRQ(); - int32_t cycleDeltaNextEvent = nextEventCycle - now; - int32_t cyclesLeftTimeout = timeoutCycle - now; - done = (cycleDeltaNextEvent > MINIRQTIME) || (cyclesLeftTimeout < 0); - } while (!done); - } // if (wvfState.waveformEnabled) - - if (wvfState.timer1CB) { - nextEventCycle = earliest(nextEventCycle, GetCycleCountIRQ() + wvfState.timer1CB()); - } - - int32_t nextEventCycles = nextEventCycle - GetCycleCountIRQ(); - - if (nextEventCycles < MINIRQTIME) { - nextEventCycles = MINIRQTIME; - } - nextEventCycles -= DELTAIRQ; - - // Do it here instead of global function to save time and because we know it's edge-IRQ - T1L = nextEventCycles >> (turbo ? 1 : 0); -} - -}; diff --git a/platformio.ini b/platformio.ini index 5e0c01db79..fe9302d11a 100644 --- a/platformio.ini +++ b/platformio.ini @@ -140,7 +140,7 @@ lib_deps = IRremoteESP8266 @ 2.8.2 makuna/NeoPixelBus @ 2.8.0 #https://github.com/makuna/NeoPixelBus.git#CoreShaderBeta - https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.2.1 + https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.0 # for I2C interface ;Wire # ESP-NOW library @@ -236,8 +236,16 @@ lib_deps_compat = IRremoteESP8266 @ 2.8.2 makuna/NeoPixelBus @ 2.7.9 https://github.com/blazoncek/QuickESPNow.git#optional-debug - https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.2.1 + https://github.com/Aircoookie/ESPAsyncWebServer.git#v2.4.0 +[esp32_all_variants] +lib_deps = + willmmiles/AsyncTCP @ 1.3.1 + bitbank2/AnimatedGIF@^1.4.7 + https://github.com/Aircoookie/GifDecoder#bc3af18 +build_flags = + -D CONFIG_ASYNC_TCP_USE_WDT=0 + -D WLED_ENABLE_GIF [esp32] #platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.2.3/platform-espressif32-2.0.2.3.zip @@ -247,10 +255,11 @@ build_unflags = ${common.build_unflags} build_flags = -g -DARDUINO_ARCH_ESP32 #-DCONFIG_LITTLEFS_FOR_IDF_3_2 - -D CONFIG_ASYNC_TCP_USE_WDT=0 #use LITTLEFS library by lorol in ESP32 core 1.x.x instead of built-in in 2.x.x -D LOROL_LITTLEFS ; -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 + ${esp32_all_variants.build_flags} + tiny_partitions = tools/WLED_ESP32_2MB_noOTA.csv default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv extended_partitions = tools/WLED_ESP32_4MB_700k_FS.csv @@ -259,7 +268,7 @@ large_partitions = tools/WLED_ESP32_8MB.csv extreme_partitions = tools/WLED_ESP32_16MB_9MB_FS.csv lib_deps = https://github.com/lorol/LITTLEFS.git - https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 + ${esp32_all_variants.lib_deps} ${env.lib_deps} # additional build flags for audioreactive AR_build_flags = -D USERMOD_AUDIOREACTIVE @@ -280,11 +289,11 @@ build_unflags = ${common.build_unflags} build_flags = -g -Wshadow=compatible-local ;; emit warning in case a local variable "shadows" another local one -DARDUINO_ARCH_ESP32 -DESP32 - -D CONFIG_ASYNC_TCP_USE_WDT=0 -DARDUINO_USB_CDC_ON_BOOT=0 ;; this flag is mandatory for "classic ESP32" when building with arduino-esp32 >=2.0.3 + ${esp32_all_variants.build_flags} -D WLED_ENABLE_DMX_INPUT lib_deps = - https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 + ${esp32_all_variants.lib_deps} https://github.com/someweisguy/esp_dmx.git#47db25d ${env.lib_deps} board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs @@ -297,14 +306,14 @@ build_flags = -g -DARDUINO_ARCH_ESP32 -DARDUINO_ARCH_ESP32S2 -DCONFIG_IDF_TARGET_ESP32S2=1 - -D CONFIG_ASYNC_TCP_USE_WDT=0 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_USB_DFU_ON_BOOT=0 -DCO -DARDUINO_USB_MODE=0 ;; this flag is mandatory for ESP32-S2 ! ;; please make sure that the following flags are properly set (to 0 or 1) by your board.json, or included in your custom platformio_override.ini entry: ;; ARDUINO_USB_CDC_ON_BOOT + ${esp32_all_variants.build_flags} lib_deps = - https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 + ${esp32_all_variants.lib_deps} ${env.lib_deps} board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs @@ -316,13 +325,13 @@ build_flags = -g -DARDUINO_ARCH_ESP32 -DARDUINO_ARCH_ESP32C3 -DCONFIG_IDF_TARGET_ESP32C3=1 - -D CONFIG_ASYNC_TCP_USE_WDT=0 -DCO -DARDUINO_USB_MODE=1 ;; this flag is mandatory for ESP32-C3 ;; please make sure that the following flags are properly set (to 0 or 1) by your board.json, or included in your custom platformio_override.ini entry: ;; ARDUINO_USB_CDC_ON_BOOT + ${esp32_all_variants.build_flags} lib_deps = - https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 + ${esp32_all_variants.lib_deps} ${env.lib_deps} board_build.partitions = ${esp32.default_partitions} ;; default partioning for 4MB Flash - can be overridden in build envs board_build.flash_mode = qio @@ -336,13 +345,13 @@ build_flags = -g -DARDUINO_ARCH_ESP32 -DARDUINO_ARCH_ESP32S3 -DCONFIG_IDF_TARGET_ESP32S3=1 - -D CONFIG_ASYNC_TCP_USE_WDT=0 -DARDUINO_USB_MSC_ON_BOOT=0 -DARDUINO_DFU_ON_BOOT=0 -DCO ;; please make sure that the following flags are properly set (to 0 or 1) by your board.json, or included in your custom platformio_override.ini entry: ;; ARDUINO_USB_MODE, ARDUINO_USB_CDC_ON_BOOT + ${esp32_all_variants.build_flags} lib_deps = - https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 + ${esp32_all_variants.lib_deps} ${env.lib_deps} board_build.partitions = ${esp32.large_partitions} ;; default partioning for 8MB flash - can be overridden in build envs @@ -358,6 +367,7 @@ platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266\" #-DWLED_DISABLE_2D + -D WLED_DISABLE_PARTICLESYSTEM2D lib_deps = ${esp8266.lib_deps} monitor_filters = esp8266_exception_decoder @@ -367,6 +377,7 @@ extends = env:nodemcuv2 platform = ${esp8266.platform_compat} platform_packages = ${esp8266.platform_packages_compat} build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP8266_compat\" #-DWLED_DISABLE_2D + -D WLED_DISABLE_PARTICLESYSTEM2D ;; lib_deps = ${esp8266.lib_deps_compat} ;; experimental - use older NeoPixelBus 2.7.9 [env:nodemcuv2_160] @@ -374,6 +385,7 @@ extends = env:nodemcuv2 board_build.f_cpu = 160000000L build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP8266_160\" #-DWLED_DISABLE_2D -D USERMOD_AUDIOREACTIVE + -D WLED_DISABLE_PARTICLESYSTEM2D [env:esp8266_2m] board = esp_wroom_02 @@ -382,6 +394,8 @@ platform_packages = ${common.platform_packages} board_build.ldscript = ${common.ldscript_2m512k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP02\" + -D WLED_DISABLE_PARTICLESYSTEM2D + -D WLED_DISABLE_PARTICLESYSTEM1D lib_deps = ${esp8266.lib_deps} [env:esp8266_2m_compat] @@ -390,12 +404,16 @@ extends = env:esp8266_2m platform = ${esp8266.platform_compat} platform_packages = ${esp8266.platform_packages_compat} build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP02_compat\" #-DWLED_DISABLE_2D + -D WLED_DISABLE_PARTICLESYSTEM1D + -D WLED_DISABLE_PARTICLESYSTEM2D [env:esp8266_2m_160] extends = env:esp8266_2m board_build.f_cpu = 160000000L build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP02_160\" -D USERMOD_AUDIOREACTIVE + -D WLED_DISABLE_PARTICLESYSTEM1D + -D WLED_DISABLE_PARTICLESYSTEM2D [env:esp01_1m_full] board = esp01_1m @@ -405,6 +423,8 @@ board_build.ldscript = ${common.ldscript_1m128k} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP01\" -D WLED_DISABLE_OTA ; -D WLED_USE_REAL_MATH ;; may fix wrong sunset/sunrise times, at the cost of 7064 bytes FLASH and 975 bytes RAM + -D WLED_DISABLE_PARTICLESYSTEM1D + -D WLED_DISABLE_PARTICLESYSTEM2D lib_deps = ${esp8266.lib_deps} [env:esp01_1m_full_compat] @@ -413,6 +433,8 @@ extends = env:esp01_1m_full platform = ${esp8266.platform_compat} platform_packages = ${esp8266.platform_packages_compat} build_flags = ${common.build_flags} ${esp8266.build_flags_compat} -D WLED_RELEASE_NAME=\"ESP01_compat\" -D WLED_DISABLE_OTA #-DWLED_DISABLE_2D + -D WLED_DISABLE_PARTICLESYSTEM1D + -D WLED_DISABLE_PARTICLESYSTEM2D [env:esp01_1m_full_160] extends = env:esp01_1m_full @@ -420,6 +442,8 @@ board_build.f_cpu = 160000000L build_flags = ${common.build_flags} ${esp8266.build_flags} -D WLED_RELEASE_NAME=\"ESP01_160\" -D WLED_DISABLE_OTA -D USERMOD_AUDIOREACTIVE ; -D WLED_USE_REAL_MATH ;; may fix wrong sunset/sunrise times, at the cost of 7064 bytes FLASH and 975 bytes RAM + -D WLED_DISABLE_PARTICLESYSTEM1D + -D WLED_DISABLE_PARTICLESYSTEM2D [env:esp32dev] board = esp32dev @@ -633,7 +657,6 @@ build_flags = ${common.build_flags} ${esp32s2.build_flags} -D WLED_RELEASE_NAME= -DBOARD_HAS_PSRAM -DLOLIN_WIFI_FIX ; seems to work much better with this -D WLED_WATCHDOG_TIMEOUT=0 - -D CONFIG_ASYNC_TCP_USE_WDT=0 -D DATA_PINS=16 -D HW_PIN_SCL=35 -D HW_PIN_SDA=33 diff --git a/requirements.txt b/requirements.txt index 666122aa28..ee70cd689f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.12 +# This file is autogenerated by pip-compile with Python 3.11 # by the following command: # # pip-compile diff --git a/tools/stress_test.sh b/tools/stress_test.sh index d7c344c58b..d86c508642 100644 --- a/tools/stress_test.sh +++ b/tools/stress_test.sh @@ -27,6 +27,7 @@ read -a JSON_TINY_TARGETS <<< $(replicate "json/nodes") read -a JSON_SMALL_TARGETS <<< $(replicate "json/info") read -a JSON_LARGE_TARGETS <<< $(replicate "json/si") read -a JSON_LARGER_TARGETS <<< $(replicate "json/fxdata") +read -a INDEX_TARGETS <<< $(replicate "") # Expand target URLS to full arguments for curl TARGETS=(${TARGET_STR[@]}) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 0ada5f28e5..82c361bdb2 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -14,6 +14,16 @@ #include "FX.h" #include "fcn_declare.h" +#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) + #include "FXparticleSystem.h" + #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_FX +#endif ////////////// // DEV INFO // @@ -28,24 +38,18 @@ float FFT_MajorPeak = 1.0; uint8_t *fftResult = nullptr; float *fftBin = nullptr; - um_data_t *um_data; - if (usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - volumeSmth = *(float*) um_data->u_data[0]; - volumeRaw = *(float*) um_data->u_data[1]; - fftResult = (uint8_t*) um_data->u_data[2]; - samplePeak = *(uint8_t*) um_data->u_data[3]; - FFT_MajorPeak = *(float*) um_data->u_data[4]; - my_magnitude = *(float*) um_data->u_data[5]; - maxVol = (uint8_t*) um_data->u_data[6]; // requires UI element (SEGMENT.customX?), changes source element - binNum = (uint8_t*) um_data->u_data[7]; // requires UI element (SEGMENT.customX?), changes source element - fftBin = (float*) um_data->u_data[8]; - } else { - // add support for no audio data - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = getAudioData(); + volumeSmth = *(float*) um_data->u_data[0]; + volumeRaw = *(float*) um_data->u_data[1]; + fftResult = (uint8_t*) um_data->u_data[2]; + samplePeak = *(uint8_t*) um_data->u_data[3]; + FFT_MajorPeak = *(float*) um_data->u_data[4]; + my_magnitude = *(float*) um_data->u_data[5]; + maxVol = (uint8_t*) um_data->u_data[6]; // requires UI element (SEGMENT.customX?), changes source element + binNum = (uint8_t*) um_data->u_data[7]; // requires UI element (SEGMENT.customX?), changes source element + fftBin = (float*) um_data->u_data[8]; */ - #define IBN 5100 // paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) #define PALETTE_SOLID_WRAP (strip.paletteBlend == 1 || strip.paletteBlend == 3) @@ -706,7 +710,7 @@ static const char _data_FX_MODE_DISSOLVE_RANDOM[] PROGMEM = "Dissolve Rnd@Repeat * Inspired by www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/ */ uint16_t mode_sparkle(void) { - if (!SEGMENT.check2) for(unsigned i = 0; i < SEGLEN; i++) { + if (!SEGMENT.check2) for (unsigned i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 1)); } uint32_t cycleTime = 10 + (255 - SEGMENT.speed)*2; @@ -1215,7 +1219,6 @@ uint16_t mode_dual_larson_scanner(void){ } static const char _data_FX_MODE_DUAL_LARSON_SCANNER[] PROGMEM = "Scanner Dual@!,Trail,Delay,,,Dual,Bi-delay;!,!,!;!;;m12=0,c1=0"; - /* * Firing comets from one end. "Lighthouse" */ @@ -1243,7 +1246,6 @@ uint16_t mode_comet(void) { } static const char _data_FX_MODE_COMET[] PROGMEM = "Lighthouse@!,Fade rate;!,!;!"; - /* * Fireworks function. */ @@ -1287,7 +1289,6 @@ uint16_t mode_fireworks() { } static const char _data_FX_MODE_FIREWORKS[] PROGMEM = "Fireworks@,Frequency;!,!;!;12;ix=192,pal=11"; - //Twinkling LEDs running. Inspired by https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/Rain.h uint16_t mode_rain() { if (SEGLEN <= 1) return mode_static(); @@ -1322,7 +1323,6 @@ uint16_t mode_rain() { } static const char _data_FX_MODE_RAIN[] PROGMEM = "Rain@!,Spawning rate;!,!;!;12;ix=128,pal=0"; - /* * Fire flicker function */ @@ -1713,7 +1713,7 @@ uint16_t mode_tricolor_fade(void) { } static const char _data_FX_MODE_TRICOLOR_FADE[] PROGMEM = "Tri Fade@!;1,2,3;!"; - +#ifdef WLED_PS_DONT_REPLACE_FX /* * Creates random comets * Custom mode by Keith Lord: https://github.com/kitesurfer1404/WS2812FX/blob/master/src/custom/MultiComet.h @@ -1752,6 +1752,7 @@ uint16_t mode_multi_comet(void) { } static const char _data_FX_MODE_MULTI_COMET[] PROGMEM = "Multi Comet@!,Fade;!,!;!;1"; #undef MAX_COMETS +#endif // WLED_PS_DONT_REPLACE_FX /* * Running random pixels ("Stream 2") @@ -2077,7 +2078,7 @@ uint16_t mode_palette() { } static const char _data_FX_MODE_PALETTE[] PROGMEM = "Palette@Shift,Size,Rotation,,,Animate Shift,Animate Rotation,Anamorphic;;!;12;ix=112,c1=0,o1=1,o2=0,o3=1"; - +#ifdef WLED_PS_DONT_REPLACE_FX // WLED limitation: Analog Clock overlay will NOT work when Fire2012 is active // Fire2012 by Mark Kriegsman, July 2012 // as part of "Five Elements" shown here: http://youtu.be/knWiGsmgycY @@ -2164,6 +2165,7 @@ uint16_t mode_fire_2012() { return FRAMETIME; } static const char _data_FX_MODE_FIRE_2012[] PROGMEM = "Fire 2012@Cooling,Spark rate,,2D Blur,Boost;;!;1;pal=35,sx=64,ix=160,m12=1,c2=128"; // bars +#endif // WLED_PS_DONT_REPLACE_FX // colored stripes pulsing at a defined Beats-Per-Minute (BPM) uint16_t mode_bpm() { @@ -2358,12 +2360,14 @@ uint16_t mode_meteor() { for (unsigned i = 0; i < SEGLEN; i++) { uint32_t col; if (hw_random8() <= 255 - SEGMENT.intensity) { - if(meteorSmooth) { - int change = trail[i] + 4 - hw_random8(24); //change each time between -20 and +4 - trail[i] = constrain(change, 0, max); - col = SEGMENT.check1 ? SEGMENT.color_from_palette(i, true, false, 0, trail[i]) : SEGMENT.color_from_palette(trail[i], false, true, 255); + if(meteorSmooth) { + if (trail[i] > 0) { + int change = trail[i] + 4 - hw_random8(24); //change each time between -20 and +4 + trail[i] = constrain(change, 0, max); } - else { + col = SEGMENT.check1 ? SEGMENT.color_from_palette(i, true, false, 0, trail[i]) : SEGMENT.color_from_palette(trail[i], false, true, 255); + } + else { trail[i] = scale8(trail[i], 128 + hw_random8(127)); int index = trail[i]; int idx = 255; @@ -2927,7 +2931,6 @@ uint16_t mode_spots_fade() } static const char _data_FX_MODE_SPOTS_FADE[] PROGMEM = "Spots Fade@Spread,Width,,,,,Overlay;!,!;!"; - //each needs 12 bytes typedef struct Ball { unsigned long lastBounceTime; @@ -3011,7 +3014,7 @@ uint16_t mode_bouncing_balls(void) { } static const char _data_FX_MODE_BOUNCINGBALLS[] PROGMEM = "Bouncing Balls@Gravity,# of balls,,,,,Overlay;!,!,!;!;1;m12=1"; //bar - +#ifdef WLED_PS_DONT_REPLACE_FX /* * bouncing balls on a track track Effect modified from Aircoookie's bouncing balls * Courtesy of pjhatch (https://github.com/pjhatch) @@ -3110,8 +3113,8 @@ static uint16_t rolling_balls(void) { return FRAMETIME; } -static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of balls,,,,Collisions,Overlay,Trails;!,!,!;!;1;m12=1"; //bar - +static const char _data_FX_MODE_ROLLINGBALLS[] PROGMEM = "Rolling Balls@!,# of balls,,,,Collide,Overlay,Trails;!,!,!;!;1;m12=1"; //bar +#endif // WLED_PS_DONT_REPLACE_FX /* * Sinelon stolen from FASTLED examples @@ -3206,7 +3209,6 @@ uint16_t mode_solid_glitter() } static const char _data_FX_MODE_SOLID_GLITTER[] PROGMEM = "Solid Glitter@,!;Bg,,Glitter color;;;m12=0"; - //each needs 20 bytes //Spark type is used for popcorn, 1D fireworks, and drip typedef struct Spark { @@ -3243,7 +3245,7 @@ uint16_t mode_popcorn(void) { unsigned numPopcorn = SEGMENT.intensity * usablePopcorns / 255; if (numPopcorn == 0) numPopcorn = 1; - for(unsigned i = 0; i < numPopcorn; i++) { + for (unsigned i = 0; i < numPopcorn; i++) { if (popcorn[i].pos >= 0.0f) { // if kernel is active, update its position popcorn[i].pos += popcorn[i].vel; popcorn[i].vel += gravity; @@ -3282,7 +3284,6 @@ uint16_t mode_popcorn(void) { } static const char _data_FX_MODE_POPCORN[] PROGMEM = "Popcorn@!,!,,,,,Overlay;!,!,!;!;;m12=1"; //bar - //values close to 100 produce 5Hz flicker, which looks very candle-y //Inspired by https://github.com/avanhanegem/ArduinoCandleEffectNeoPixel //and https://cpldcpu.wordpress.com/2016/01/05/reverse-engineering-a-real-candle/ @@ -3375,7 +3376,7 @@ uint16_t mode_candle_multi() } static const char _data_FX_MODE_CANDLE_MULTI[] PROGMEM = "Candle Multi@!,!;!,!;!;;sx=96,ix=224,pal=0"; - +#ifdef WLED_PS_DONT_REPLACE_FX /* / Fireworks in starburst effect / based on the video: https://www.reddit.com/r/arduino/comments/c3sd46/i_made_this_fireworks_effect_for_my_led_strips/ @@ -3507,8 +3508,9 @@ uint16_t mode_starburst(void) { } #undef STARBURST_MAX_FRAG static const char _data_FX_MODE_STARBURST[] PROGMEM = "Fireworks Starburst@Chance,Fragments,,,,,Overlay;,!;!;;pal=11,m12=0"; +#endif // WLED_PS_DONT_REPLACE_FX - + #ifdef WLED_PS_DONT_REPLACE_FX /* * Exploding fireworks effect * adapted from: http://www.anirama.com/1000leds/1d-fireworks/ @@ -3645,8 +3647,8 @@ uint16_t mode_exploding_fireworks(void) return FRAMETIME; } #undef MAX_SPARKS -static const char _data_FX_MODE_EXPLODING_FIREWORKS[] PROGMEM = "Fireworks 1D@Gravity,Firing side,,,,,,Blur;!,!;!;12;pal=11,ix=128"; - +static const char _data_FX_MODE_EXPLODING_FIREWORKS[] PROGMEM = "Fireworks 1D@Gravity,Firing side;!,!;!;12;pal=11,ix=128"; +#endif // WLED_PS_DONT_REPLACE_FX /* * Drip Effect @@ -3734,7 +3736,6 @@ uint16_t mode_drip(void) } static const char _data_FX_MODE_DRIP[] PROGMEM = "Drip@Gravity,# of drips,,,,,Overlay;!,!;!;;m12=1"; //bar - /* * Tetris or Stacking (falling bricks) Effect * by Blaz Kristan (AKA blazoncek) (https://github.com/blazoncek, https://blaz.at/home) @@ -4282,17 +4283,6 @@ uint16_t mode_chunchun(void) } static const char _data_FX_MODE_CHUNCHUN[] PROGMEM = "Chunchun@!,Gap size;!,!;!"; - -//13 bytes -typedef struct Spotlight { - float speed; - uint8_t colorIdx; - int16_t position; - unsigned long lastUpdateTime; - uint8_t width; - uint8_t type; -} spotlight; - #define SPOT_TYPE_SOLID 0 #define SPOT_TYPE_GRADIENT 1 #define SPOT_TYPE_2X_GRADIENT 2 @@ -4306,6 +4296,17 @@ typedef struct Spotlight { #define SPOT_MAX_COUNT 49 //Number of simultaneous waves #endif +#ifdef WLED_PS_DONT_REPLACE_FX +//13 bytes +typedef struct Spotlight { + float speed; + uint8_t colorIdx; + int16_t position; + unsigned long lastUpdateTime; + uint8_t width; + uint8_t type; +} spotlight; + /* * Spotlights moving back and forth that cast dancing shadows. * Shine this through tree branches/leaves or other close-up objects that cast @@ -4429,7 +4430,7 @@ uint16_t mode_dancing_shadows(void) return FRAMETIME; } static const char _data_FX_MODE_DANCING_SHADOWS[] PROGMEM = "Dancing Shadows@!,# of shadows;!;!"; - +#endif // WLED_PS_DONT_REPLACE_FX /* Imitates a washing machine, rotating same waves forward, then pause, then backward. @@ -4450,6 +4451,24 @@ uint16_t mode_washing_machine(void) { static const char _data_FX_MODE_WASHING_MACHINE[] PROGMEM = "Washing Machine@!,!;;!"; +/* + Image effect + Draws a .gif image from filesystem on the matrix/strip +*/ +uint16_t mode_image(void) { + #ifndef WLED_ENABLE_GIF + return mode_static(); + #else + renderImageToSegment(SEGMENT); + return FRAMETIME; + #endif + // if (status != 0 && status != 254 && status != 255) { + // Serial.print("GIF renderer return: "); + // Serial.println(status); + // } +} +static const char _data_FX_MODE_IMAGE[] PROGMEM = "Image@!,;;;12;sx=128"; + /* Blends random colors across palette Modified, originally by Mark Kriegsman https://gist.github.com/kriegsman/1f7ccbbfa492a73c015e @@ -5858,7 +5877,7 @@ uint16_t mode_2Dcrazybees(void) { static const char _data_FX_MODE_2DCRAZYBEES[] PROGMEM = "Crazy Bees@!,Blur,,,,Smear;;!;2;pal=11,ix=0"; #undef MAX_BEES - +#ifdef WLED_PS_DONT_REPLACE_FX ///////////////////////// // 2D Ghost Rider // ///////////////////////// @@ -6046,7 +6065,7 @@ uint16_t mode_2Dfloatingblobs(void) { } static const char _data_FX_MODE_2DBLOBS[] PROGMEM = "Blobs@!,# blobs,Blur,Trail;!;!;2;c1=8"; #undef MAX_BLOBS - +#endif // WLED_PS_DONT_REPLACE_FX //////////////////////////// // 2D Scrolling text // @@ -7639,218 +7658,2683 @@ uint16_t mode_2Dwavingcell() { } static const char _data_FX_MODE_2DWAVINGCELL[] PROGMEM = "Waving Cell@!,Blur,Amplitude 1,Amplitude 2,Amplitude 3,,Flow;;!;2;ix=0"; +#ifndef WLED_DISABLE_PARTICLESYSTEM2D -#endif // WLED_DISABLE_2D - +/* + Particle System Vortex + Particles sprayed from center with a rotating spray + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +#define NUMBEROFSOURCES 8 +uint16_t mode_particlevortex(void) { + if (SEGLEN == 1) + return mode_static(); + ParticleSystem2D *PartSys = nullptr; + uint32_t i, j; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) + return mode_static(); // allocation failed + #ifdef ESP8266 + PartSys->setMotionBlur(180); + #else + PartSys->setMotionBlur(130); + #endif + for (i = 0; i < min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); i++) { + PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center + PartSys->sources[i].source.y = (PartSys->maxY + 1) >> 1; // center + PartSys->sources[i].maxLife = 900; + PartSys->sources[i].minLife = 800; + } + PartSys->setKillOutOfBounds(true); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS -////////////////////////////////////////////////////////////////////////////////////////// -// mode data -static const char _data_RESERVED[] PROGMEM = "RSVD"; + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! -// add (or replace reserved) effect mode and data into vector -// use id==255 to find unallocated gaps (with "Reserved" data string) -// if vector size() is smaller than id (single) data is appended at the end (regardless of id) -// return the actual id used for the effect or 255 if the add failed. -uint8_t WS2812FX::addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name) { - if (id == 255) { // find empty slot - for (size_t i=1; i<_mode.size(); i++) if (_modeData[i] == _data_RESERVED) { id = i; break; } - } - if (id < _mode.size()) { - if (_modeData[id] != _data_RESERVED) return 255; // do not overwrite an already added effect - _mode[id] = mode_fn; - _modeData[id] = mode_name; - return id; - } else if(_mode.size() < 255) { // 255 is reserved for indicating the effect wasn't added - _mode.push_back(mode_fn); - _modeData.push_back(mode_name); - if (_modeCount < _mode.size()) _modeCount++; - return _mode.size() - 1; - } else { - return 255; // The vector is full so return 255 + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + uint32_t spraycount = min(PartSys->numSources, (uint32_t)(1 + (SEGMENT.custom1 >> 5))); // number of sprays to display, 1-8 + #ifdef ESP8266 + for (i = 1; i < 4; i++) { // need static particles in the center to reduce blinking (would be black every other frame without this hack), just set them there fixed + int partindex = (int)PartSys->usedParticles - (int)i; + if (partindex >= 0) { + PartSys->particles[partindex].x = (PartSys->maxX + 1) >> 1; // center + PartSys->particles[partindex].y = (PartSys->maxY + 1) >> 1; // center + PartSys->particles[partindex].sat = 230; + PartSys->particles[partindex].ttl = 256; //keep alive + } } -} + #endif -void WS2812FX::setupEffectData() { - // Solid must be first! (assuming vector is empty upon call to setup) - _mode.push_back(&mode_static); - _modeData.push_back(_data_FX_MODE_STATIC); - // fill reserved word in case there will be any gaps in the array - for (size_t i=1; i<_modeCount; i++) { - _mode.push_back(&mode_static); - _modeData.push_back(_data_RESERVED); + if (SEGMENT.check1) + PartSys->setSmearBlur(90); // enable smear blur + else + PartSys->setSmearBlur(0); // disable smear blur + + // update colors of the sprays + for (i = 0; i < spraycount; i++) { + uint32_t coloroffset = 0xFF / spraycount; + PartSys->sources[i].source.hue = coloroffset * i; } - // now replace all pre-allocated effects - // --- 1D non-audio effects --- - addEffect(FX_MODE_BLINK, &mode_blink, _data_FX_MODE_BLINK); - addEffect(FX_MODE_BREATH, &mode_breath, _data_FX_MODE_BREATH); - addEffect(FX_MODE_COLOR_WIPE, &mode_color_wipe, _data_FX_MODE_COLOR_WIPE); - addEffect(FX_MODE_COLOR_WIPE_RANDOM, &mode_color_wipe_random, _data_FX_MODE_COLOR_WIPE_RANDOM); - addEffect(FX_MODE_RANDOM_COLOR, &mode_random_color, _data_FX_MODE_RANDOM_COLOR); - addEffect(FX_MODE_COLOR_SWEEP, &mode_color_sweep, _data_FX_MODE_COLOR_SWEEP); - addEffect(FX_MODE_DYNAMIC, &mode_dynamic, _data_FX_MODE_DYNAMIC); - addEffect(FX_MODE_RAINBOW, &mode_rainbow, _data_FX_MODE_RAINBOW); - addEffect(FX_MODE_RAINBOW_CYCLE, &mode_rainbow_cycle, _data_FX_MODE_RAINBOW_CYCLE); - addEffect(FX_MODE_SCAN, &mode_scan, _data_FX_MODE_SCAN); - addEffect(FX_MODE_DUAL_SCAN, &mode_dual_scan, _data_FX_MODE_DUAL_SCAN); - addEffect(FX_MODE_FADE, &mode_fade, _data_FX_MODE_FADE); - addEffect(FX_MODE_THEATER_CHASE, &mode_theater_chase, _data_FX_MODE_THEATER_CHASE); - addEffect(FX_MODE_THEATER_CHASE_RAINBOW, &mode_theater_chase_rainbow, _data_FX_MODE_THEATER_CHASE_RAINBOW); - addEffect(FX_MODE_RUNNING_LIGHTS, &mode_running_lights, _data_FX_MODE_RUNNING_LIGHTS); - addEffect(FX_MODE_SAW, &mode_saw, _data_FX_MODE_SAW); - addEffect(FX_MODE_TWINKLE, &mode_twinkle, _data_FX_MODE_TWINKLE); - addEffect(FX_MODE_DISSOLVE, &mode_dissolve, _data_FX_MODE_DISSOLVE); - addEffect(FX_MODE_DISSOLVE_RANDOM, &mode_dissolve_random, _data_FX_MODE_DISSOLVE_RANDOM); - addEffect(FX_MODE_SPARKLE, &mode_sparkle, _data_FX_MODE_SPARKLE); - addEffect(FX_MODE_FLASH_SPARKLE, &mode_flash_sparkle, _data_FX_MODE_FLASH_SPARKLE); - addEffect(FX_MODE_HYPER_SPARKLE, &mode_hyper_sparkle, _data_FX_MODE_HYPER_SPARKLE); - addEffect(FX_MODE_STROBE, &mode_strobe, _data_FX_MODE_STROBE); - addEffect(FX_MODE_STROBE_RAINBOW, &mode_strobe_rainbow, _data_FX_MODE_STROBE_RAINBOW); - addEffect(FX_MODE_MULTI_STROBE, &mode_multi_strobe, _data_FX_MODE_MULTI_STROBE); - addEffect(FX_MODE_BLINK_RAINBOW, &mode_blink_rainbow, _data_FX_MODE_BLINK_RAINBOW); - addEffect(FX_MODE_ANDROID, &mode_android, _data_FX_MODE_ANDROID); - addEffect(FX_MODE_CHASE_COLOR, &mode_chase_color, _data_FX_MODE_CHASE_COLOR); - addEffect(FX_MODE_CHASE_RANDOM, &mode_chase_random, _data_FX_MODE_CHASE_RANDOM); - addEffect(FX_MODE_CHASE_RAINBOW, &mode_chase_rainbow, _data_FX_MODE_CHASE_RAINBOW); - addEffect(FX_MODE_CHASE_FLASH, &mode_chase_flash, _data_FX_MODE_CHASE_FLASH); - addEffect(FX_MODE_CHASE_FLASH_RANDOM, &mode_chase_flash_random, _data_FX_MODE_CHASE_FLASH_RANDOM); - addEffect(FX_MODE_CHASE_RAINBOW_WHITE, &mode_chase_rainbow_white, _data_FX_MODE_CHASE_RAINBOW_WHITE); - addEffect(FX_MODE_COLORFUL, &mode_colorful, _data_FX_MODE_COLORFUL); - addEffect(FX_MODE_TRAFFIC_LIGHT, &mode_traffic_light, _data_FX_MODE_TRAFFIC_LIGHT); - addEffect(FX_MODE_COLOR_SWEEP_RANDOM, &mode_color_sweep_random, _data_FX_MODE_COLOR_SWEEP_RANDOM); - addEffect(FX_MODE_RUNNING_COLOR, &mode_running_color, _data_FX_MODE_RUNNING_COLOR); - addEffect(FX_MODE_AURORA, &mode_aurora, _data_FX_MODE_AURORA); - addEffect(FX_MODE_RUNNING_RANDOM, &mode_running_random, _data_FX_MODE_RUNNING_RANDOM); - addEffect(FX_MODE_LARSON_SCANNER, &mode_larson_scanner, _data_FX_MODE_LARSON_SCANNER); - addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET); - addEffect(FX_MODE_FIREWORKS, &mode_fireworks, _data_FX_MODE_FIREWORKS); - addEffect(FX_MODE_RAIN, &mode_rain, _data_FX_MODE_RAIN); - addEffect(FX_MODE_TETRIX, &mode_tetrix, _data_FX_MODE_TETRIX); - addEffect(FX_MODE_FIRE_FLICKER, &mode_fire_flicker, _data_FX_MODE_FIRE_FLICKER); - addEffect(FX_MODE_GRADIENT, &mode_gradient, _data_FX_MODE_GRADIENT); - addEffect(FX_MODE_LOADING, &mode_loading, _data_FX_MODE_LOADING); - addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); - addEffect(FX_MODE_FAIRY, &mode_fairy, _data_FX_MODE_FAIRY); - addEffect(FX_MODE_TWO_DOTS, &mode_two_dots, _data_FX_MODE_TWO_DOTS); - addEffect(FX_MODE_FAIRYTWINKLE, &mode_fairytwinkle, _data_FX_MODE_FAIRYTWINKLE); - addEffect(FX_MODE_RUNNING_DUAL, &mode_running_dual, _data_FX_MODE_RUNNING_DUAL); + // set rotation direction and speed + // can use direction flag to determine current direction + bool direction = SEGMENT.check2; //no automatic direction change, set it to flag + int32_t currentspeed = (int32_t)SEGENV.step; // make a signed integer out of step - addEffect(FX_MODE_TRICOLOR_CHASE, &mode_tricolor_chase, _data_FX_MODE_TRICOLOR_CHASE); - addEffect(FX_MODE_TRICOLOR_WIPE, &mode_tricolor_wipe, _data_FX_MODE_TRICOLOR_WIPE); - addEffect(FX_MODE_TRICOLOR_FADE, &mode_tricolor_fade, _data_FX_MODE_TRICOLOR_FADE); - addEffect(FX_MODE_LIGHTNING, &mode_lightning, _data_FX_MODE_LIGHTNING); - addEffect(FX_MODE_ICU, &mode_icu, _data_FX_MODE_ICU); - addEffect(FX_MODE_MULTI_COMET, &mode_multi_comet, _data_FX_MODE_MULTI_COMET); - addEffect(FX_MODE_DUAL_LARSON_SCANNER, &mode_dual_larson_scanner, _data_FX_MODE_DUAL_LARSON_SCANNER); - addEffect(FX_MODE_RANDOM_CHASE, &mode_random_chase, _data_FX_MODE_RANDOM_CHASE); - addEffect(FX_MODE_OSCILLATE, &mode_oscillate, _data_FX_MODE_OSCILLATE); - addEffect(FX_MODE_PRIDE_2015, &mode_pride_2015, _data_FX_MODE_PRIDE_2015); - addEffect(FX_MODE_JUGGLE, &mode_juggle, _data_FX_MODE_JUGGLE); - addEffect(FX_MODE_PALETTE, &mode_palette, _data_FX_MODE_PALETTE); - addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012); - addEffect(FX_MODE_COLORWAVES, &mode_colorwaves, _data_FX_MODE_COLORWAVES); - addEffect(FX_MODE_BPM, &mode_bpm, _data_FX_MODE_BPM); - addEffect(FX_MODE_FILLNOISE8, &mode_fillnoise8, _data_FX_MODE_FILLNOISE8); - addEffect(FX_MODE_NOISE16_1, &mode_noise16_1, _data_FX_MODE_NOISE16_1); - addEffect(FX_MODE_NOISE16_2, &mode_noise16_2, _data_FX_MODE_NOISE16_2); - addEffect(FX_MODE_NOISE16_3, &mode_noise16_3, _data_FX_MODE_NOISE16_3); - addEffect(FX_MODE_NOISE16_4, &mode_noise16_4, _data_FX_MODE_NOISE16_4); - addEffect(FX_MODE_COLORTWINKLE, &mode_colortwinkle, _data_FX_MODE_COLORTWINKLE); - addEffect(FX_MODE_LAKE, &mode_lake, _data_FX_MODE_LAKE); - addEffect(FX_MODE_METEOR, &mode_meteor, _data_FX_MODE_METEOR); - //addEffect(FX_MODE_METEOR_SMOOTH, &mode_meteor_smooth, _data_FX_MODE_METEOR_SMOOTH); // merged with mode_meteor - addEffect(FX_MODE_RAILWAY, &mode_railway, _data_FX_MODE_RAILWAY); - addEffect(FX_MODE_RIPPLE, &mode_ripple, _data_FX_MODE_RIPPLE); - addEffect(FX_MODE_TWINKLEFOX, &mode_twinklefox, _data_FX_MODE_TWINKLEFOX); - addEffect(FX_MODE_TWINKLECAT, &mode_twinklecat, _data_FX_MODE_TWINKLECAT); - addEffect(FX_MODE_HALLOWEEN_EYES, &mode_halloween_eyes, _data_FX_MODE_HALLOWEEN_EYES); - addEffect(FX_MODE_STATIC_PATTERN, &mode_static_pattern, _data_FX_MODE_STATIC_PATTERN); - addEffect(FX_MODE_TRI_STATIC_PATTERN, &mode_tri_static_pattern, _data_FX_MODE_TRI_STATIC_PATTERN); - addEffect(FX_MODE_SPOTS, &mode_spots, _data_FX_MODE_SPOTS); - addEffect(FX_MODE_SPOTS_FADE, &mode_spots_fade, _data_FX_MODE_SPOTS_FADE); - addEffect(FX_MODE_GLITTER, &mode_glitter, _data_FX_MODE_GLITTER); - addEffect(FX_MODE_CANDLE, &mode_candle, _data_FX_MODE_CANDLE); - addEffect(FX_MODE_STARBURST, &mode_starburst, _data_FX_MODE_STARBURST); - addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS); - addEffect(FX_MODE_BOUNCINGBALLS, &mode_bouncing_balls, _data_FX_MODE_BOUNCINGBALLS); - addEffect(FX_MODE_SINELON, &mode_sinelon, _data_FX_MODE_SINELON); - addEffect(FX_MODE_SINELON_DUAL, &mode_sinelon_dual, _data_FX_MODE_SINELON_DUAL); - addEffect(FX_MODE_SINELON_RAINBOW, &mode_sinelon_rainbow, _data_FX_MODE_SINELON_RAINBOW); - addEffect(FX_MODE_POPCORN, &mode_popcorn, _data_FX_MODE_POPCORN); - addEffect(FX_MODE_DRIP, &mode_drip, _data_FX_MODE_DRIP); - addEffect(FX_MODE_PLASMA, &mode_plasma, _data_FX_MODE_PLASMA); - addEffect(FX_MODE_PERCENT, &mode_percent, _data_FX_MODE_PERCENT); - addEffect(FX_MODE_RIPPLE_RAINBOW, &mode_ripple_rainbow, _data_FX_MODE_RIPPLE_RAINBOW); - addEffect(FX_MODE_HEARTBEAT, &mode_heartbeat, _data_FX_MODE_HEARTBEAT); - addEffect(FX_MODE_PACIFICA, &mode_pacifica, _data_FX_MODE_PACIFICA); - addEffect(FX_MODE_CANDLE_MULTI, &mode_candle_multi, _data_FX_MODE_CANDLE_MULTI); - addEffect(FX_MODE_SOLID_GLITTER, &mode_solid_glitter, _data_FX_MODE_SOLID_GLITTER); - addEffect(FX_MODE_SUNRISE, &mode_sunrise, _data_FX_MODE_SUNRISE); - addEffect(FX_MODE_PHASED, &mode_phased, _data_FX_MODE_PHASED); - addEffect(FX_MODE_TWINKLEUP, &mode_twinkleup, _data_FX_MODE_TWINKLEUP); - addEffect(FX_MODE_NOISEPAL, &mode_noisepal, _data_FX_MODE_NOISEPAL); - addEffect(FX_MODE_SINEWAVE, &mode_sinewave, _data_FX_MODE_SINEWAVE); - addEffect(FX_MODE_PHASEDNOISE, &mode_phased_noise, _data_FX_MODE_PHASEDNOISE); - addEffect(FX_MODE_FLOW, &mode_flow, _data_FX_MODE_FLOW); - addEffect(FX_MODE_CHUNCHUN, &mode_chunchun, _data_FX_MODE_CHUNCHUN); - addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); - addEffect(FX_MODE_WASHING_MACHINE, &mode_washing_machine, _data_FX_MODE_WASHING_MACHINE); + if (SEGMENT.custom2 > 0) { // automatic direction change enabled + uint32_t changeinterval = 1040 - ((uint32_t)SEGMENT.custom2 << 2); + direction = SEGENV.aux1 & 0x01; //set direction according to flag - addEffect(FX_MODE_BLENDS, &mode_blends, _data_FX_MODE_BLENDS); - addEffect(FX_MODE_TV_SIMULATOR, &mode_tv_simulator, _data_FX_MODE_TV_SIMULATOR); - addEffect(FX_MODE_DYNAMIC_SMOOTH, &mode_dynamic_smooth, _data_FX_MODE_DYNAMIC_SMOOTH); + if (SEGMENT.check3) // random interval + changeinterval = 20 + changeinterval + hw_random16(changeinterval); - // --- 1D audio effects --- - addEffect(FX_MODE_PIXELS, &mode_pixels, _data_FX_MODE_PIXELS); - addEffect(FX_MODE_PIXELWAVE, &mode_pixelwave, _data_FX_MODE_PIXELWAVE); - addEffect(FX_MODE_JUGGLES, &mode_juggles, _data_FX_MODE_JUGGLES); - addEffect(FX_MODE_MATRIPIX, &mode_matripix, _data_FX_MODE_MATRIPIX); - addEffect(FX_MODE_GRAVIMETER, &mode_gravimeter, _data_FX_MODE_GRAVIMETER); - addEffect(FX_MODE_PLASMOID, &mode_plasmoid, _data_FX_MODE_PLASMOID); - addEffect(FX_MODE_PUDDLES, &mode_puddles, _data_FX_MODE_PUDDLES); - addEffect(FX_MODE_MIDNOISE, &mode_midnoise, _data_FX_MODE_MIDNOISE); - addEffect(FX_MODE_NOISEMETER, &mode_noisemeter, _data_FX_MODE_NOISEMETER); - addEffect(FX_MODE_FREQWAVE, &mode_freqwave, _data_FX_MODE_FREQWAVE); - addEffect(FX_MODE_FREQMATRIX, &mode_freqmatrix, _data_FX_MODE_FREQMATRIX); + if (SEGMENT.call % changeinterval == 0) { //flip direction on next frame + SEGENV.aux1 |= 0x02; // set the update flag (for random interval update) + if (direction) + SEGENV.aux1 &= ~0x01; // clear the direction flag + else + SEGENV.aux1 |= 0x01; // set the direction flag + } + } - addEffect(FX_MODE_WATERFALL, &mode_waterfall, _data_FX_MODE_WATERFALL); - addEffect(FX_MODE_FREQPIXELS, &mode_freqpixels, _data_FX_MODE_FREQPIXELS); + int32_t targetspeed = (direction ? 1 : -1) * (SEGMENT.speed << 3); + int32_t speeddiff = targetspeed - currentspeed; + int32_t speedincrement = speeddiff / 50; - addEffect(FX_MODE_NOISEFIRE, &mode_noisefire, _data_FX_MODE_NOISEFIRE); - addEffect(FX_MODE_PUDDLEPEAK, &mode_puddlepeak, _data_FX_MODE_PUDDLEPEAK); - addEffect(FX_MODE_NOISEMOVE, &mode_noisemove, _data_FX_MODE_NOISEMOVE); + if (speedincrement == 0) { //if speeddiff is not zero, make the increment at least 1 so it reaches target speed + if (speeddiff < 0) + speedincrement = -1; + else if (speeddiff > 0) + speedincrement = 1; + } - addEffect(FX_MODE_PERLINMOVE, &mode_perlinmove, _data_FX_MODE_PERLINMOVE); - addEffect(FX_MODE_RIPPLEPEAK, &mode_ripplepeak, _data_FX_MODE_RIPPLEPEAK); + currentspeed += speedincrement; + SEGENV.aux0 += currentspeed; + SEGENV.step = (uint32_t)currentspeed; //save it back - addEffect(FX_MODE_FREQMAP, &mode_freqmap, _data_FX_MODE_FREQMAP); - addEffect(FX_MODE_GRAVCENTER, &mode_gravcenter, _data_FX_MODE_GRAVCENTER); - addEffect(FX_MODE_GRAVCENTRIC, &mode_gravcentric, _data_FX_MODE_GRAVCENTRIC); - addEffect(FX_MODE_GRAVFREQ, &mode_gravfreq, _data_FX_MODE_GRAVFREQ); - addEffect(FX_MODE_DJLIGHT, &mode_DJLight, _data_FX_MODE_DJLIGHT); + uint16_t angleoffset = 0xFFFF / spraycount; // angle offset for an even distribution + uint32_t skip = PS_P_HALFRADIUS / (SEGMENT.intensity + 1) + 1; // intensity is emit speed, emit less on low speeds + if (SEGMENT.call % skip == 0) { + j = hw_random16(spraycount); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. + for (i = 0; i < spraycount; i++) { // emit one particle per spray (if available) + PartSys->sources[j].var = (SEGMENT.custom3 >> 1); //update speed variation + #ifdef ESP8266 + if (SEGMENT.call & 0x01) // every other frame, do not emit to save particles + #endif + PartSys->angleEmit(PartSys->sources[j], SEGENV.aux0 + angleoffset * j, (SEGMENT.intensity >> 2)+1); + j = (j + 1) % spraycount; + } + } + PartSys->update(); //update all particles and render to frame + return FRAMETIME; +} +#undef NUMBEROFSOURCES +static const char _data_FX_MODE_PARTICLEVORTEX[] PROGMEM = "PS Vortex@Rotation Speed,Particle Speed,Arms,Flip,Nozzle,Smear,Direction,Random Flip;;!;2;pal=27,c1=200,c2=0,c3=0"; - addEffect(FX_MODE_BLURZ, &mode_blurz, _data_FX_MODE_BLURZ); +/* + Particle Fireworks + Rockets shoot up and explode in a random color, sometimes in a defined pattern + by DedeHai (Damian Schneider) +*/ +#define NUMBEROFSOURCES 8 - addEffect(FX_MODE_FLOWSTRIPE, &mode_FlowStripe, _data_FX_MODE_FLOWSTRIPE); +uint16_t mode_particlefireworks(void) { + ParticleSystem2D *PartSys = nullptr; + uint32_t numRockets; - addEffect(FX_MODE_WAVESINS, &mode_wavesins, _data_FX_MODE_WAVESINS); - addEffect(FX_MODE_ROCKTAVES, &mode_rocktaves, _data_FX_MODE_ROCKTAVES); + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) + return mode_static(); // allocation failed - // --- 2D effects --- -#ifndef WLED_DISABLE_2D - addEffect(FX_MODE_2DPLASMAROTOZOOM, &mode_2Dplasmarotozoom, _data_FX_MODE_2DPLASMAROTOZOOM); - addEffect(FX_MODE_2DSPACESHIPS, &mode_2Dspaceships, _data_FX_MODE_2DSPACESHIPS); - addEffect(FX_MODE_2DCRAZYBEES, &mode_2Dcrazybees, _data_FX_MODE_2DCRAZYBEES); + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setWallHardness(120); // ground bounce is fixed + numRockets = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); + for (uint32_t j = 0; j < numRockets; j++) { + PartSys->sources[j].source.ttl = 500 * j; // first rocket starts immediately, others follow soon + PartSys->sources[j].source.vy = -1; // at negative speed, no particles are emitted and if rocket dies, it will be relaunched + } + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + numRockets = map(SEGMENT.speed, 0 , 255, 4, min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES)); + + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceY(SEGMENT.check2); + PartSys->setGravity(map(SEGMENT.custom3, 0, 31, SEGMENT.check2 ? 1 : 0, 10)); // if bounded, set gravity to minimum of 1 or they will bounce at top + PartSys->setMotionBlur(SEGMENT.custom2);//map(SEGMENT.custom2, 0, 255, 0, 170)); // anable motion blur + uint8_t smearing = 0; + if (SEGMENT.custom2 > 200) + smearing = SEGMENT.custom2 - 200; + //PartSys->setSmearBlur(smearing); // enable 2D blurring (smearing) + + // update the rockets, set the speed state + for (uint32_t j = 0; j < numRockets; j++) { + PartSys->applyGravity(PartSys->sources[j].source); + PartSys->particleMoveUpdate(PartSys->sources[j].source, PartSys->sources[j].sourceFlags); + if (PartSys->sources[j].source.ttl == 0) { + if (PartSys->sources[j].source.vy > 0) { // rocket has died and is moving up. stop it so it will explode (is handled in the code below) + PartSys->sources[j].source.vy = 0; + } + else if (PartSys->sources[j].source.vy < 0) { // rocket is exploded and time is up (ttl=0 and negative speed), relaunch it + PartSys->sources[j].source.y = PS_P_RADIUS; // start from bottom + PartSys->sources[j].source.x = (PartSys->maxX >> 2) + hw_random(PartSys->maxX >> 1); // centered half + PartSys->sources[j].source.vy = (SEGMENT.custom3) + random16(SEGMENT.custom1 >> 3) + 5; // rocket speed TODO: need to adjust for segment height + PartSys->sources[j].source.vx = hw_random16(7) - 3; // not perfectly straight up + PartSys->sources[j].source.sat = 30; // low saturation -> exhaust is off-white + PartSys->sources[j].source.ttl = hw_random16(SEGMENT.custom1) + (SEGMENT.custom1 >> 1); // set fuse time + PartSys->sources[j].maxLife = 40; // exhaust particle life + PartSys->sources[j].minLife = 10; + PartSys->sources[j].vx = 0; // emitting speed + PartSys->sources[j].vy = -5; // emitting speed + PartSys->sources[j].var = 4; // speed variation around vx,vy (+/- var) + } + } + } + // check each rocket's state and emit particles according to its state: moving up = emit exhaust, at top = explode; falling down = standby time + uint32_t emitparticles, frequency, baseangle, hueincrement; // number of particles to emit for each rocket's state + // variables for circular explosions + [[maybe_unused]] int32_t speed, currentspeed, speedvariation, percircle; + int32_t counter = 0; + [[maybe_unused]] uint16_t angle; + [[maybe_unused]] unsigned angleincrement; + bool circularexplosion = false; + + // emit particles for each rocket + for (uint32_t j = 0; j < numRockets; j++) { + // determine rocket state by its speed: + if (PartSys->sources[j].source.vy > 0) { // moving up, emit exhaust + emitparticles = 1; + } + else if (PartSys->sources[j].source.vy < 0) { // falling down, standby time + emitparticles = 0; + } + else { // speed is zero, explode! + PartSys->sources[j].source.hue = hw_random16(); // random color + PartSys->sources[j].source.sat = hw_random16(55) + 200; + PartSys->sources[j].maxLife = 200; + PartSys->sources[j].minLife = 100; + PartSys->sources[j].source.ttl = hw_random16((2000 - ((uint32_t)SEGMENT.speed << 2))) + 550 - (SEGMENT.speed << 1); // standby time til next launch + PartSys->sources[j].var = ((SEGMENT.intensity >> 4) + 5); // speed variation around vx,vy (+/- var) + PartSys->sources[j].source.vy = -1; // set speed negative so it will emit no more particles after this explosion until relaunch + #ifdef ESP8266 + emitparticles = hw_random16(SEGMENT.intensity >> 3) + (SEGMENT.intensity >> 3) + 5; // defines the size of the explosion + #else + emitparticles = hw_random16(SEGMENT.intensity >> 2) + (SEGMENT.intensity >> 2) + 5; // defines the size of the explosion + #endif + + if (random16() & 1) { // 50% chance for circular explosion + circularexplosion = true; + speed = 2 + hw_random16(3) + ((SEGMENT.intensity >> 6)); + currentspeed = speed; + angleincrement = 2730 + hw_random16(5461); // minimum 15° + random(30°) + angle = hw_random16(); // random start angle + baseangle = angle; // save base angle for modulation + percircle = 0xFFFF / angleincrement + 1; // number of particles to make complete circles + hueincrement = hw_random16() & 127; // &127 is equivalent to %128 + int circles = 1 + hw_random16(3) + ((SEGMENT.intensity >> 6)); + frequency = hw_random16() & 127; // modulation frequency (= "waves per circle"), x.4 fixed point + emitparticles = percircle * circles; + PartSys->sources[j].var = angle & 1; // 0 or 1 variation, angle is random + } + } + uint32_t i; + for (i = 0; i < emitparticles; i++) { + if (circularexplosion) { + int32_t sineMod = 0xEFFF + sin16_t((uint16_t)(((angle * frequency) >> 4) + baseangle)); // shifted to positive values + currentspeed = (speed/2 + ((sineMod * speed) >> 16)) >> 1; // sine modulation on speed based on emit angle + PartSys->angleEmit(PartSys->sources[j], angle, currentspeed); // note: compiler warnings can be ignored, variables are set just above + counter++; + if (counter > percircle) { // full circle completed, increase speed + counter = 0; + speed += 3 + ((SEGMENT.intensity >> 6)); // increase speed to form a second wave + PartSys->sources[j].source.hue += hueincrement; // new color for next circle + PartSys->sources[j].source.sat = min((uint16_t)150, random16()); + } + angle += angleincrement; // set angle for next particle + } + else { // random explosion or exhaust + PartSys->sprayEmit(PartSys->sources[j]); + if ((j % 3) == 0) { + PartSys->sources[j].source.hue = hw_random16(); // random color for each particle (this is also true for exhaust, but that is white anyways) + } + } + } + if (i == 0) // no particles emitted, this rocket is falling + PartSys->sources[j].source.y = 1000; // reset position so gravity wont pull it to the ground and bounce it (vy MUST stay negative until relaunch) + circularexplosion = false; // reset for next rocket + } + if (SEGMENT.check3) { // fast speed, move particles twice + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i], nullptr, nullptr); + } + } + PartSys->update(); // update and render + return FRAMETIME; +} +#undef NUMBEROFSOURCES +static const char _data_FX_MODE_PARTICLEFIREWORKS[] PROGMEM = "PS Fireworks@Launches,Explosion Size,Fuse,Blur,Gravity,Cylinder,Ground,Fast;;!;2;pal=11,ix=50,c1=40,c2=0,c3=12"; + +/* + Particle Volcano + Particles are sprayed from below, spray moves back and forth if option is set + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +#define NUMBEROFSOURCES 1 +uint16_t mode_particlevolcano(void) { + ParticleSystem2D *PartSys = nullptr; + PSsettings2D volcanosettings; + volcanosettings.asByte = 0b00000100; // PS settings for volcano movement: bounceX is enabled + uint8_t numSprays; // note: so far only one tested but more is possible + uint32_t i = 0; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, no additional data needed + return mode_static(); // allocation failed or not 2D + + PartSys->setBounceY(true); + PartSys->setGravity(); // enable with default gforce + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setMotionBlur(230); // anable motion blur + + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); // number of sprays + for (i = 0; i < numSprays; i++) { + PartSys->sources[i].source.hue = hw_random16(); + PartSys->sources[i].source.x = PartSys->maxX / (numSprays + 1) * (i + 1); // distribute evenly + PartSys->sources[i].maxLife = 300; // lifetime in frames + PartSys->sources[i].minLife = 250; + PartSys->sources[i].sourceFlags.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[i].sourceFlags.perpetual = true; // source never dies + } + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); // number of volcanoes + + // change source emitting color from time to time, emit one particle per spray + if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) { // every nth frame, cycle color and emit particles (and update the sources) + for (i = 0; i < numSprays; i++) { + PartSys->sources[i].source.y = PS_P_RADIUS + 5; // reset to just above the lower edge that is allowed for bouncing particles, if zero, particles already 'bounce' at start and loose speed. + PartSys->sources[i].source.vy = 0; //reset speed (so no extra particlesettin is required to keep the source 'afloat') + PartSys->sources[i].source.hue++; // = hw_random16(); //change hue of spray source (note: random does not look good) + PartSys->sources[i].source.vx = PartSys->sources[i].source.vx > 0 ? (SEGMENT.custom1 >> 2) : -(SEGMENT.custom1 >> 2); // set moving speed but keep the direction given by PS + PartSys->sources[i].vy = SEGMENT.speed >> 2; // emitting speed (upwards) + PartSys->sources[i].vx = 0; + PartSys->sources[i].var = SEGMENT.custom3 >> 1; // emiting variation = nozzle size (custom 3 goes from 0-31) + PartSys->sprayEmit(PartSys->sources[i]); + PartSys->setWallHardness(255); // full hardness for source bounce + PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &volcanosettings); //move the source + } + } + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setColorByAge(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setWallHardness(SEGMENT.custom2); + + if (SEGMENT.check3) // collisions enabled + PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); + + PartSys->update(); // update and render + return FRAMETIME; +} +#undef NUMBEROFSOURCES +static const char _data_FX_MODE_PARTICLEVOLCANO[] PROGMEM = "PS Volcano@Speed,Intensity,Move,Bounce,Spread,AgeColor,Walls,Collide;;!;2;pal=35,sx=100,ix=190,c1=0,c2=160,c3=6,o1=1"; + +/* + Particle Fire + realistic fire effect using particles. heat based and using perlin-noise for wind + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particlefire(void) { + ParticleSystem2D *PartSys = nullptr; + uint32_t i; // index variable + uint32_t numFlames; // number of flames: depends on fire width. for a fire width of 16 pixels, about 25-30 flames give good results + + if (SEGMENT.call == 0) { // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + if (!initParticleSystem2D(PartSys, SEGMENT.virtualWidth(), 4)) //maximum number of source (PS may limit based on segment size); need 4 additional bytes for time keeping (uint32_t lastcall) + return mode_static(); // allocation failed or not 2D + SEGENV.aux0 = hw_random16(); // aux0 is wind position (index) in the perlin noise + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check2); + PartSys->setMotionBlur(SEGMENT.check1 * 170); // anable/disable motion blur + + uint32_t firespeed = max((uint8_t)100, SEGMENT.speed); //limit speed to 100 minimum, reduce frame rate to make it slower (slower speeds than 100 do not look nice) + if (SEGMENT.speed < 100) { //slow, limit FPS + uint32_t *lastcall = reinterpret_cast(PartSys->PSdataEnd); + uint32_t period = strip.now - *lastcall; + if (period < (uint32_t)map(SEGMENT.speed, 0, 99, 50, 10)) { // limit to 90FPS - 20FPS + SEGMENT.call--; //skipping a frame, decrement the counter (on call0, this is never executed as lastcall is 0, so its fine to not check if >0) + //still need to render the frame or flickering will occur in transitions + PartSys->updateFire(SEGMENT.intensity, true); // render the fire without updating particles (render only) + return FRAMETIME; //do not update this frame + } + *lastcall = strip.now; + } + + uint32_t spread = (PartSys->maxX >> 5) * (SEGMENT.custom3 + 1); //fire around segment center (in subpixel points) + numFlames = min((uint32_t)PartSys->numSources, (4 + ((spread / PS_P_RADIUS) << 1))); // number of flames used depends on spread with, good value is (fire width in pixel) * 2 + uint32_t percycle = (numFlames * 2) / 3; // maximum number of particles emitted per cycle (TODO: for ESP826 maybe use flames/2) + + // update the flame sprays: + for (i = 0; i < numFlames; i++) { + if (SEGMENT.call & 1 && PartSys->sources[i].source.ttl > 0) { // every second frame + PartSys->sources[i].source.ttl--; + } else { // flame source is dead: initialize new flame: set properties of source + PartSys->sources[i].source.x = (PartSys->maxX >> 1) - (spread >> 1) + hw_random(spread); // change flame position: distribute randomly on chosen width + PartSys->sources[i].source.y = -(PS_P_RADIUS << 2); // set the source below the frame + PartSys->sources[i].source.ttl = 20 + hw_random16((SEGMENT.custom1 * SEGMENT.custom1) >> 8) / (1 + (firespeed >> 5)); //'hotness' of fire, faster flames reduce the effect or flame height will scale too much with speed + PartSys->sources[i].maxLife = hw_random16(SEGMENT.virtualHeight() >> 1) + 16; // defines flame height together with the vy speed, vy speed*maxlife/PS_P_RADIUS is the average flame height + PartSys->sources[i].minLife = PartSys->sources[i].maxLife >> 1; + PartSys->sources[i].vx = hw_random16(4) - 2; // emitting speed (sideways) + PartSys->sources[i].vy = (SEGMENT.virtualHeight() >> 1) + (firespeed >> 4) + (SEGMENT.custom1 >> 4); // emitting speed (upwards) + PartSys->sources[i].var = 2 + hw_random16(2 + (firespeed >> 4)); // speed variation around vx,vy (+/- var) + } + } + + if (SEGMENT.call % 3 == 0) { // update noise position and add wind + SEGENV.aux0++; // position in the perlin noise matrix for wind generation + if (SEGMENT.call % 10 == 0) + SEGENV.aux1++; // move in noise y direction so noise does not repeat as often + // add wind force to all particles + int8_t windspeed = ((int16_t)(inoise8(SEGENV.aux0, SEGENV.aux1) - 127) * SEGMENT.custom2) >> 7; + PartSys->applyForce(windspeed, 0); + } + SEGENV.step++; + + if (SEGMENT.check3) { //add turbulance (parameters and algorithm found by experimentation) + if (SEGMENT.call % map(firespeed, 0, 255, 4, 15) == 0) { + for (i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].y < PartSys->maxY / 4) { // do not apply turbulance everywhere -> bottom quarter seems a good balance + int32_t curl = ((int32_t)inoise8(PartSys->particles[i].x, PartSys->particles[i].y, SEGENV.step << 4) - 127); + PartSys->particles[i].vx += (curl * (firespeed + 10)) >> 9; + } + } + } + } + + uint8_t j = hw_random16(); // start with a random flame (so each flame gets the chance to emit a particle if available particles is smaller than number of flames) + for (i = 0; i < percycle; i++) { + j = (j + 1) % numFlames; + PartSys->flameEmit(PartSys->sources[j]); + } + + PartSys->updateFire(SEGMENT.intensity, false); // update and render the fire + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEFIRE[] PROGMEM = "PS Fire@Speed,Intensity,Flame Height,Wind,Spread,Smooth,Cylinder,Turbulence;;!;2;pal=35,sx=110,c1=110,c2=50,c3=31,o1=1"; + +/* + PS Ballpit: particles falling down, user can enable these three options: X-wraparound, side bounce, ground bounce + sliders control falling speed, intensity (number of particles spawned), inter-particle collision hardness (0 means no particle collisions) and render saturation + this is quite versatile, can be made to look like rain or snow or confetti etc. + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particlepit(void) { + ParticleSystem2D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1, 0, true, false)) // init, request one source (actually dont really need one TODO: test if using zero sources also works) + return mode_static(); // allocation failed or not 2D + PartSys->setKillOutOfBounds(true); + PartSys->setGravity(); // enable with default gravity + PartSys->setUsedParticles(170); // use 75% of available particles + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setBounceY(SEGMENT.check3); + PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)150)); // limit to 100 min (if collisions are disabled, still want bouncy) + if (SEGMENT.custom2 > 0) + PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); + + uint32_t i; + if (SEGMENT.call % (128 - (SEGMENT.intensity >> 1)) == 0 && SEGMENT.intensity > 0) { // every nth frame emit particles, stop emitting if set to zero + for (i = 0; i < PartSys->usedParticles; i++) { // emit particles + if (PartSys->particles[i].ttl == 0) { // find a dead particle + // emit particle at random position over the top of the matrix (random16 is not random enough) + PartSys->particles[i].ttl = 1500 - (SEGMENT.speed << 2) + hw_random16(500); // if speed is higher, make them die sooner + PartSys->particles[i].x = hw_random(PartSys->maxX); //random(PartSys->maxX >> 1) + (PartSys->maxX >> 2); + PartSys->particles[i].y = (PartSys->maxY << 1); // particles appear somewhere above the matrix, maximum is double the height + PartSys->particles[i].vx = (int16_t)hw_random16(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); // side speed is +/- + PartSys->particles[i].vy = map(SEGMENT.speed, 0, 255, -5, -100); // downward speed + PartSys->particles[i].hue = hw_random16(); // set random color + PartSys->particleFlags[i].collide = true; // enable collision for particle + PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7; + // set particle size + if (SEGMENT.custom1 == 255) { + PartSys->setParticleSize(1); // set global size to 1 for advanced rendering + PartSys->advPartProps[i].size = hw_random16(SEGMENT.custom1); // set each particle to random size + } else { + PartSys->setParticleSize(SEGMENT.custom1); // set global size + PartSys->advPartProps[i].size = 0; // use global size + } + break; // emit only one particle per round + } + } + } + + uint32_t frictioncoefficient = 1 + SEGMENT.check1; //need more friction if wrapX is set, see below note + if (SEGMENT.speed < 50) // for low speeds, apply more friction + frictioncoefficient = 50 - SEGMENT.speed; + + if (SEGMENT.call % 6 == 0)// (3 + max(3, (SEGMENT.speed >> 2))) == 0) // note: if friction is too low, hard particles uncontrollably 'wander' left and right if wrapX is enabled + PartSys->applyFriction(frictioncoefficient); + + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEPIT[] PROGMEM = "PS Ballpit@Speed,Intensity,Size,Hardness,Saturation,Cylinder,Walls,Ground;;!;2;pal=11,sx=100,ix=220,c1=120,c2=130,c3=31,o3=1"; + +/* + Particle Waterfall + Uses palette for particle color, spray source at top emitting particles, many config options + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particlewaterfall(void) { + ParticleSystem2D *PartSys = nullptr; + uint8_t numSprays; + uint32_t i = 0; + + if (SEGMENT.call == 0) { // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + if (!initParticleSystem2D(PartSys, 12)) // init, request 12 sources, no additional data needed + return mode_static(); // allocation failed or not 2D + + PartSys->setGravity(); // enable with default gforce + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setMotionBlur(190); // anable motion blur + PartSys->setSmearBlur(30); // enable 2D blurring (smearing) + for (i = 0; i < PartSys->numSources; i++) { + PartSys->sources[i].source.hue = i*90; + PartSys->sources[i].sourceFlags.collide = true; // seeded particles will collide + #ifdef ESP8266 + PartSys->sources[i].maxLife = 250; // lifetime in frames (ESP8266 has less particles, make them short lived to keep the water flowing) + PartSys->sources[i].minLife = 100; + #else + PartSys->sources[i].maxLife = 400; // lifetime in frames + PartSys->sources[i].minLife = 150; + #endif + } + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check1); // cylinder + PartSys->setBounceX(SEGMENT.check2); // walls + PartSys->setBounceY(SEGMENT.check3); // ground + PartSys->setWallHardness(SEGMENT.custom2); + numSprays = min((int32_t)PartSys->numSources, max(PartSys->maxXpixel / 6, (int32_t)2)); // number of sprays depends on segment width + if (SEGMENT.custom2 > 0) // collisions enabled + PartSys->enableParticleCollisions(true, SEGMENT.custom2); // enable collisions and set particle collision hardness + else { + PartSys->enableParticleCollisions(false); + PartSys->setWallHardness(120); // set hardness (for ground bounce) to fixed value if not using collisions + } + + for (i = 0; i < numSprays; i++) { + PartSys->sources[i].source.hue += 1 + hw_random16(SEGMENT.custom1>>1); // change hue of spray source + } + + if (SEGMENT.call % (12 - (SEGMENT.intensity >> 5)) == 0 && SEGMENT.intensity > 0) { // every nth frame, emit particles, do not emit if intensity is zero + for (i = 0; i < numSprays; i++) { + PartSys->sources[i].vy = -SEGMENT.speed >> 3; // emitting speed, down + //PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (PartSys->maxXpixel - numSprays * 2) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position + PartSys->sources[i].source.x = map(SEGMENT.custom3, 0, 31, 0, (PartSys->maxXpixel - numSprays) * PS_P_RADIUS) + i * PS_P_RADIUS * 2; // emitter position + PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS * ((i<<2) + 4)); // source y position, few pixels above the top to increase spreading before entering the matrix + PartSys->sources[i].var = (SEGMENT.custom1 >> 3); // emiting variation 0-32 + PartSys->sprayEmit(PartSys->sources[i]); + } + } + + if (SEGMENT.call % 20 == 0) + PartSys->applyFriction(1); // add just a tiny amount of friction to help smooth things + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEWATERFALL[] PROGMEM = "PS Waterfall@Speed,Intensity,Variation,Collide,Position,Cylinder,Walls,Ground;;!;2;pal=9,sx=15,ix=200,c1=32,c2=160,o3=1"; + +/* + Particle Box, applies gravity to particles in either a random direction or random but only downwards (sloshing) + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particlebox(void) { + ParticleSystem2D *PartSys = nullptr; + uint32_t i; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1)) // init + return mode_static(); // allocation failed or not 2D + PartSys->setBounceX(true); + PartSys->setBounceY(true); + SEGENV.aux0 = hw_random16(); // position in perlin noise + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setParticleSize(SEGMENT.custom3<<3); + PartSys->setWallHardness(min(SEGMENT.custom2, (uint8_t)200)); // wall hardness is 200 or more + PartSys->enableParticleCollisions(true, max(2, (int)SEGMENT.custom2)); // enable collisions and set particle collision hardness + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 2, 153)); // 1% - 60% + // add in new particles if amount has changed + for (i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl < 260) { // initialize handed over particles and dead particles + PartSys->particles[i].ttl = 260; // full brigthness + PartSys->particles[i].x = hw_random16(PartSys->maxX); + PartSys->particles[i].y = hw_random16(PartSys->maxY); + PartSys->particles[i].hue = hw_random8(); // make it colorful + PartSys->particleFlags[i].perpetual = true; // never die + PartSys->particleFlags[i].collide = true; // all particles colllide + break; // only spawn one particle per frame for less chaotic transitions + } + } + + if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0 && SEGMENT.speed > 0) { // how often the force is applied depends on speed setting + int32_t xgravity; + int32_t ygravity; + int32_t increment = (SEGMENT.speed >> 6) + 1; + + if (SEGMENT.check2) { // washing machine + int speed = tristate_square8(strip.now >> 7, 90, 15) / ((400 - SEGMENT.speed) >> 3); + SEGENV.aux0 += speed; + if (speed == 0) SEGENV.aux0 = 190; //down (= 270°) + } + else + SEGENV.aux0 -= increment; + + if (SEGMENT.check1) { // random, use perlin noise + xgravity = ((int16_t)inoise8(SEGENV.aux0) - 127); + ygravity = ((int16_t)inoise8(SEGENV.aux0 + 10000) - 127); + // scale the gravity force + xgravity = (xgravity * SEGMENT.custom1) / 128; + ygravity = (ygravity * SEGMENT.custom1) / 128; + } + else { // go in a circle + xgravity = ((int32_t)(SEGMENT.custom1) * cos16_t(SEGENV.aux0 << 8)) / 0xFFFF; + ygravity = ((int32_t)(SEGMENT.custom1) * sin16_t(SEGENV.aux0 << 8)) / 0xFFFF; + } + if (SEGMENT.check3) { // sloshing, y force is always downwards + if (ygravity > 0) + ygravity = -ygravity; + } + + PartSys->applyForce(xgravity, ygravity); + } + + if ((SEGMENT.call & 0x0F) == 0) // every 16th frame + PartSys->applyFriction(1); + + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEBOX[] PROGMEM = "PS Box@!,Particles,Tilt,Hardness,Size,Random,Washing Machine,Sloshing;;!;2;pal=53,ix=50,c3=1,o1=1"; + +/* + Fuzzy Noise: Perlin noise 'gravity' mapping as in particles on 'noise hills' viewed from above + calculates slope gradient at the particle positions and applies 'downhill' force, resulting in a fuzzy perlin noise display + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleperlin(void) { + ParticleSystem2D *PartSys = nullptr; + uint32_t i; + + if (SEGMENT.call == 0) { // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + if (!initParticleSystem2D(PartSys, 1, 0, true)) // init with 1 source and advanced properties + return mode_static(); // allocation failed or not 2D + + PartSys->setKillOutOfBounds(true); // should never happen, but lets make sure there are no stray particles + PartSys->setMotionBlur(230); // anable motion blur + PartSys->setBounceY(true); + SEGENV.aux0 = rand(); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceX(!SEGMENT.check1); + PartSys->setWallHardness(SEGMENT.custom1); // wall hardness + PartSys->enableParticleCollisions(SEGMENT.check3, SEGMENT.custom1); // enable collisions and set particle collision hardness + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 128)); // min is 10%, max is 50% + PartSys->setSmearBlur(SEGMENT.check2 * 15); // enable 2D blurring (smearing) + + // apply 'gravity' from a 2D perlin noise map + SEGENV.aux0 += 1 + (SEGMENT.speed >> 5); // noise z-position + // update position in noise + for (i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl == 0) { // revive dead particles (do not keep them alive forever, they can clump up, need to reseed) + PartSys->particles[i].ttl = hw_random16(500) + 200; + PartSys->particles[i].x = hw_random(PartSys->maxX); + PartSys->particles[i].y = hw_random(PartSys->maxY); + PartSys->particleFlags[i].collide = true; // particle colllides + } + uint32_t scale = 16 - ((31 - SEGMENT.custom3) >> 1); + uint16_t xnoise = PartSys->particles[i].x / scale; // position in perlin noise, scaled by slider + uint16_t ynoise = PartSys->particles[i].y / scale; + int16_t baseheight = inoise8(xnoise, ynoise, SEGENV.aux0); // noise value at particle position + PartSys->particles[i].hue = baseheight; // color particles to perlin noise value + if (SEGMENT.call % 8 == 0) { // do not apply the force every frame, is too chaotic + int8_t xslope = (baseheight + (int16_t)inoise8(xnoise - 10, ynoise, SEGENV.aux0)); + int8_t yslope = (baseheight + (int16_t)inoise8(xnoise, ynoise - 10, SEGENV.aux0)); + PartSys->applyForce(i, xslope, yslope); + } + } + + if (SEGMENT.call % (16 - (SEGMENT.custom2 >> 4)) == 0) + PartSys->applyFriction(2); + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEPERLIN[] PROGMEM = "PS Fuzzy Noise@Speed,Particles,Bounce,Friction,Scale,Cylinder,Smear,Collide;;!;2;pal=64,sx=50,ix=200,c1=130,c2=30,c3=5,o3=1"; + +/* + Particle smashing down like meteors and exploding as they hit the ground, has many parameters to play with + by DedeHai (Damian Schneider) +*/ +#define NUMBEROFSOURCES 8 +uint16_t mode_particleimpact(void) { + ParticleSystem2D *PartSys = nullptr; + uint32_t i = 0; + uint8_t MaxNumMeteors; + PSsettings2D meteorsettings; + meteorsettings.asByte = 0b00101000; // PS settings for meteors: bounceY and gravity enabled + + if (SEGMENT.call == 0) { // initialization TODO: make this a PSinit function, this is needed in every particle FX but first, get this working. + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, no additional data needed + return mode_static(); // allocation failed or not 2D + PartSys->setKillOutOfBounds(true); + PartSys->setGravity(); // enable default gravity + PartSys->setBounceY(true); // always use ground bounce + PartSys->setWallRoughness(220); // high roughness + MaxNumMeteors = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); + for (i = 0; i < MaxNumMeteors; i++) { + // PartSys->sources[i].source.y = 500; + PartSys->sources[i].source.ttl = hw_random16(10 * i); // set initial delay for meteors + PartSys->sources[i].source.vy = 10; // at positive speeds, no particles are emitted and if particle dies, it will be relaunched + } + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! (TODO: ask how to handle this so it always works) + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setMotionBlur(SEGMENT.custom3<<3); + uint8_t hardness = map(SEGMENT.custom2, 0, 255, PS_P_MINSURFACEHARDNESS - 2, 255); + PartSys->setWallHardness(hardness); + PartSys->enableParticleCollisions(SEGMENT.check3, hardness); // enable collisions and set particle collision hardness + MaxNumMeteors = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); + uint8_t numMeteors = MaxNumMeteors; // TODO: clean this up map(SEGMENT.custom3, 0, 31, 1, MaxNumMeteors); // number of meteors to use for animation + + uint32_t emitparticles; // number of particles to emit for each rocket's state + + for (i = 0; i < numMeteors; i++) { + // determine meteor state by its speed: + if ( PartSys->sources[i].source.vy < 0) { // moving down, emit sparks + #ifdef ESP8266 + emitparticles = 1; + #else + emitparticles = 2; + #endif + } + else if ( PartSys->sources[i].source.vy > 0) // moving up means meteor is on 'standby' + emitparticles = 0; + else { // speed is zero, explode! + PartSys->sources[i].source.vy = 10; // set source speed positive so it goes into timeout and launches again + #ifdef ESP8266 + emitparticles = hw_random16(SEGMENT.intensity >> 3) + 5; // defines the size of the explosion + #else + emitparticles = map(SEGMENT.intensity, 0, 255, 10, hw_random16(PartSys->usedParticles>>2)); // defines the size of the explosion !!!TODO: check if this works on ESP8266, drop esp8266 def if it does + #endif + } + for (int e = emitparticles; e > 0; e--) { + PartSys->sprayEmit(PartSys->sources[i]); + } + } + + // update the meteors, set the speed state + for (i = 0; i < numMeteors; i++) { + if (PartSys->sources[i].source.ttl) { + PartSys->sources[i].source.ttl--; // note: this saves an if statement, but moving down particles age twice + if (PartSys->sources[i].source.vy < 0) { // move down + PartSys->applyGravity(PartSys->sources[i].source); + PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &meteorsettings); + + // if source reaches the bottom, set speed to 0 so it will explode on next function call (handled above) + if (PartSys->sources[i].source.y < PS_P_RADIUS<<1) { // reached the bottom pixel on its way down + PartSys->sources[i].source.vy = 0; // set speed zero so it will explode + PartSys->sources[i].source.vx = 0; + PartSys->sources[i].sourceFlags.collide = true; + #ifdef ESP8266 + PartSys->sources[i].maxLife = 180; + PartSys->sources[i].minLife = 20; + #else + PartSys->sources[i].maxLife = 250; + PartSys->sources[i].minLife = 50; + #endif + PartSys->sources[i].source.ttl = hw_random16((512 - (SEGMENT.speed << 1))) + 40; // standby time til next launch (in frames) + PartSys->sources[i].vy = (SEGMENT.custom1 >> 2); // emitting speed y + PartSys->sources[i].var = (SEGMENT.custom1 >> 2); // speed variation around vx,vy (+/- var) + } + } + } + else if (PartSys->sources[i].source.vy > 0) { // meteor is exploded and time is up (ttl==0 and positive speed), relaunch it + // reinitialize meteor + PartSys->sources[i].source.y = PartSys->maxY + (PS_P_RADIUS << 2); // start 4 pixels above the top + PartSys->sources[i].source.x = hw_random(PartSys->maxX); + PartSys->sources[i].source.vy = -hw_random16(30) - 30; // meteor downward speed + PartSys->sources[i].source.vx = hw_random16(50) - 25; // TODO: make this dependent on position so they do not move out of frame + PartSys->sources[i].source.hue = hw_random16(); // random color + PartSys->sources[i].source.ttl = 500; // long life, will explode at bottom + PartSys->sources[i].sourceFlags.collide = false; // trail particles will not collide + PartSys->sources[i].maxLife = 60; // spark particle life + PartSys->sources[i].minLife = 20; + PartSys->sources[i].vy = -9; // emitting speed (down) + PartSys->sources[i].var = 3; // speed variation around vx,vy (+/- var) + } + } + + PartSys->update(); // update and render + return FRAMETIME; +} +#undef NUMBEROFSOURCES +static const char _data_FX_MODE_PARTICLEIMPACT[] PROGMEM = "PS Impact@Launches,!,Force,Hardness,Blur,Cylinder,Walls,Collide;;!;2;pal=0,sx=32,ix=85,c1=70,c2=130,c3=0,o3=1"; + +/* + Particle Attractor, a particle attractor sits in the matrix center, a spray bounces around and seeds particles + uses inverse square law like in planetary motion + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleattractor(void) { + ParticleSystem2D *PartSys = nullptr; + PSsettings2D sourcesettings; + sourcesettings.asByte = 0b00001100; // PS settings for bounceY, bounceY used for source movement (it always bounces whereas particles do not) + PSparticleFlags attractorFlags; + attractorFlags.asByte = 0; // no flags set + PSparticle *attractor; // particle pointer to the attractor + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1, sizeof(PSparticle), true)) // init using 1 source and advanced particle settings + return mode_static(); // allocation failed or not 2D + PartSys->sources[0].source.hue = hw_random16(); + PartSys->sources[0].source.vx = -7; // will collied with wall and get random bounce direction + PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide + PartSys->sources[0].sourceFlags.perpetual = true; //source does not age + #ifdef ESP8266 + PartSys->sources[0].maxLife = 200; // lifetime in frames (ESP8266 has less particles) + PartSys->sources[0].minLife = 30; + #else + PartSys->sources[0].maxLife = 350; // lifetime in frames + PartSys->sources[0].minLife = 50; + #endif + PartSys->sources[0].var = 4; // emiting variation + PartSys->setWallHardness(255); //bounce forever + PartSys->setWallRoughness(200); //randomize wall bounce + } + else { + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + } + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + attractor = reinterpret_cast(PartSys->PSdataEnd); + + PartSys->setColorByAge(SEGMENT.check1); + PartSys->setParticleSize(SEGMENT.custom1 >> 1); //set size globally + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 190)); + + if (SEGMENT.custom2 > 0) // collisions enabled + PartSys->enableParticleCollisions(true, map(SEGMENT.custom2, 1, 255, 120, 255)); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); + + if (SEGMENT.call == 0) { + attractor->vx = PartSys->sources[0].source.vy; // set to spray movemement but reverse x and y + attractor->vy = PartSys->sources[0].source.vx; + } + + // set attractor properties + attractor->ttl = 100; // never dies + if (SEGMENT.check2) { + if ((SEGMENT.call % 3) == 0) // move slowly + PartSys->particleMoveUpdate(*attractor, attractorFlags, &sourcesettings); // move the attractor + } + else { + attractor->x = PartSys->maxX >> 1; // set to center + attractor->y = PartSys->maxY >> 1; + } + + if (SEGMENT.call % 5 == 0) + PartSys->sources[0].source.hue++; + + SEGENV.aux0 += 256; // emitting angle, one full turn in 255 frames (0xFFFF is 360°) + if (SEGMENT.call % 2 == 0) // alternate direction of emit + PartSys->angleEmit(PartSys->sources[0], SEGENV.aux0, 12); + else + PartSys->angleEmit(PartSys->sources[0], SEGENV.aux0 + 0x7FFF, 12); // emit at 180° as well + // apply force + uint32_t strength = SEGMENT.speed; + #ifdef USERMOD_AUDIOREACTIVE + um_data_t *um_data; + if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // AR active, do not use simulated data + uint32_t volumeSmth = (uint32_t)(*(float*) um_data->u_data[0]); // 0-255 + strength = (SEGMENT.speed * volumeSmth) >> 8; + } + #endif + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->pointAttractor(i, *attractor, strength, SEGMENT.check3); + } + + + if (SEGMENT.call % (33 - SEGMENT.custom3) == 0) + PartSys->applyFriction(2); + PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags, &sourcesettings); // move the source + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEATTRACTOR[] PROGMEM = "PS Attractor@Mass,Particles,Size,Collide,Friction,AgeColor,Move,Swallow;;!;2;pal=9,sx=100,ix=82,c1=2,c2=0"; + +/* + Particle Spray, just a particle spray with many parameters + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particlespray(void) { + ParticleSystem2D *PartSys = nullptr; + const uint8_t hardness = 200; // collision hardness is fixed + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1)) // init, no additional data needed + return mode_static(); // allocation failed or not 2D + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->setBounceY(true); + PartSys->setMotionBlur(200); // anable motion blur + PartSys->setSmearBlur(10); // anable motion blur + PartSys->sources[0].source.hue = hw_random16(); + PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[0].var = 3; + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setBounceX(!SEGMENT.check2); + PartSys->setWrapX(SEGMENT.check2); + PartSys->setWallHardness(hardness); + PartSys->setGravity(8 * SEGMENT.check1); // enable gravity if checked (8 is default strength) + //numSprays = min(PartSys->numSources, (uint8_t)1); // number of sprays + + if (SEGMENT.check3) // collisions enabled + PartSys->enableParticleCollisions(true, hardness); // enable collisions and set particle collision hardness + else + PartSys->enableParticleCollisions(false); + + //position according to sliders + PartSys->sources[0].source.x = map(SEGMENT.custom1, 0, 255, 0, PartSys->maxX); + PartSys->sources[0].source.y = map(SEGMENT.custom2, 0, 255, 0, PartSys->maxY); + uint16_t angle = (256 - (((int32_t)SEGMENT.custom3 + 1) << 3)) << 8; + + #ifdef USERMOD_AUDIOREACTIVE + um_data_t *um_data; + if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // get AR data, do not use simulated data + uint32_t volumeSmth = (uint8_t)(*(float*) um_data->u_data[0]); //0 to 255 + uint32_t volumeRaw = *(int16_t*)um_data->u_data[1]; //0 to 255 + PartSys->sources[0].minLife = 30; + + if (SEGMENT.call % 20 == 0 || SEGMENT.call % (11 - volumeSmth / 25) == 0) { // defines interval of particle emit + PartSys->sources[0].maxLife = (volumeSmth >> 1) + (SEGMENT.intensity >> 1); // lifetime in frames + PartSys->sources[0].var = 1 + ((volumeRaw * SEGMENT.speed) >> 12); + uint32_t emitspeed = (SEGMENT.speed >> 2) + (volumeRaw >> 3); + PartSys->sources[0].source.hue += volumeSmth/30; + PartSys->angleEmit(PartSys->sources[0], angle, emitspeed); + } + } + else { //no AR data, fall back to normal mode + // change source properties + if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) { // every nth frame, cycle color and emit particles + PartSys->sources[0].maxLife = 300 + SEGMENT.intensity; // lifetime in frames + PartSys->sources[0].minLife = 150 + SEGMENT.intensity; + PartSys->sources[0].source.hue++; // = hw_random16(); //change hue of spray source + PartSys->angleEmit(PartSys->sources[0], angle, SEGMENT.speed >> 2); + } + } + #else + // change source properties + if (SEGMENT.call % (11 - (SEGMENT.intensity / 25)) == 0) { // every nth frame, cycle color and emit particles + PartSys->sources[0].maxLife = 300; // lifetime in frames. note: could be done in init part, but AR moderequires this to be dynamic + PartSys->sources[0].minLife = 100; + PartSys->sources[0].source.hue++; // = hw_random16(); //change hue of spray source + // PartSys->sources[i].var = SEGMENT.custom3; // emiting variation = nozzle size (custom 3 goes from 0-32) + // spray[j].source.hue = hw_random16(); //set random color for each particle (using palette) + PartSys->angleEmit(PartSys->sources[0], angle, SEGMENT.speed >> 2); + } + #endif + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLESPRAY[] PROGMEM = "PS Spray@Speed,!,Left/Right,Up/Down,Angle,Gravity,Cylinder/Square,Collide;;!;2v;pal=0,sx=150,ix=150,c1=220,c2=30,c3=21"; + + +/* + Particle base Graphical Equalizer + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleGEQ(void) { + ParticleSystem2D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1)) + return mode_static(); // allocation failed or not 2D + PartSys->setKillOutOfBounds(true); + PartSys->setUsedParticles(170); // use 2/3 of available particles + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + uint32_t i; + // set particle system properties + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setWrapX(SEGMENT.check1); + PartSys->setBounceX(SEGMENT.check2); + PartSys->setBounceY(SEGMENT.check3); + //PartSys->enableParticleCollisions(false); + PartSys->setWallHardness(SEGMENT.custom2); + PartSys->setGravity(SEGMENT.custom3 << 2); // set gravity strength + + um_data_t *um_data = getAudioData(); + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + + //map the bands into 16 positions on x axis, emit some particles according to frequency loudness + i = 0; + uint32_t binwidth = (PartSys->maxX + 1)>>4; //emit poisition variation for one bin (+/-) is equal to width/16 (for 16 bins) + uint32_t threshold = 300 - SEGMENT.intensity; + uint32_t emitparticles = 0; + + for (uint32_t bin = 0; bin < 16; bin++) { + uint32_t xposition = binwidth*bin + (binwidth>>1); // emit position according to frequency band + uint8_t emitspeed = ((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 9; // emit speed according to loudness of band (127 max!) + emitparticles = 0; + + if (fftResult[bin] > threshold) { + emitparticles = 1;// + (fftResult[bin]>>6); + } + else if (fftResult[bin] > 0) { // band has low volue + uint32_t restvolume = ((threshold - fftResult[bin])>>2) + 2; + if (hw_random16() % restvolume == 0) + emitparticles = 1; + } + + while (i < PartSys->usedParticles && emitparticles > 0) { // emit particles if there are any left, low frequencies take priority + if (PartSys->particles[i].ttl == 0) { // find a dead particle + //set particle properties TODO: could also use the spray... + PartSys->particles[i].ttl = 20 + map(SEGMENT.intensity, 0,255, emitspeed>>1, emitspeed + hw_random16(emitspeed)) ; // set particle alive, particle lifespan is in number of frames + PartSys->particles[i].x = xposition + hw_random16(binwidth) - (binwidth>>1); // position randomly, deviating half a bin width + PartSys->particles[i].y = PS_P_RADIUS; // start at the bottom (PS_P_RADIUS is minimum position a particle is fully in frame) + PartSys->particles[i].vx = hw_random16(SEGMENT.custom1>>1)-(SEGMENT.custom1>>2) ; //x-speed variation: +/- custom1/4 + PartSys->particles[i].vy = emitspeed; + PartSys->particles[i].hue = (bin<<4) + hw_random16(17) - 8; // color from palette according to bin + emitparticles--; + } + i++; + } + } + + PartSys->update(); // update and render + return FRAMETIME; +} + +static const char _data_FX_MODE_PARTICLEGEQ[] PROGMEM = "PS GEQ 2D@Speed,Intensity,Diverge,Bounce,Gravity,Cylinder,Walls,Floor;;!;2f;pal=0,sx=155,ix=200,c1=0"; + +/* + Particle rotating GEQ + Particles sprayed from center with rotating spray + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +#define NUMBEROFSOURCES 16 +uint16_t mode_particlecenterGEQ(void) { + ParticleSystem2D *PartSys = nullptr; + uint8_t numSprays; + uint32_t i; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, NUMBEROFSOURCES)) // init, request 16 sources + return mode_static(); // allocation failed or not 2D + + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); + for (i = 0; i < numSprays; i++) { + PartSys->sources[i].source.x = (PartSys->maxX + 1) >> 1; // center + PartSys->sources[i].source.y = (PartSys->maxY + 1) >> 1; // center + PartSys->sources[i].source.hue = i * 16; // even color distribution + PartSys->sources[i].maxLife = 400; + PartSys->sources[i].minLife = 200; + } + PartSys->setKillOutOfBounds(true); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + numSprays = min(PartSys->numSources, (uint32_t)NUMBEROFSOURCES); + + um_data_t *um_data = getAudioData(); + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + uint32_t threshold = 300 - SEGMENT.intensity; + + if (SEGMENT.check2) + SEGENV.aux0 += SEGMENT.custom1 << 2; + else + SEGENV.aux0 -= SEGMENT.custom1 << 2; + + uint16_t angleoffset = (uint16_t)0xFFFF / (uint16_t)numSprays; + uint32_t j = hw_random16(numSprays); // start with random spray so all get a chance to emit a particle if maximum number of particles alive is reached. + for (i = 0; i < numSprays; i++) { + if (SEGMENT.call % (32 - (SEGMENT.custom2 >> 3)) == 0 && SEGMENT.custom2 > 0) + PartSys->sources[j].source.hue += 1 + (SEGMENT.custom2 >> 4); + + PartSys->sources[j].var = SEGMENT.custom3 >> 2; + int8_t emitspeed = 5 + (((uint32_t)fftResult[j] * ((uint32_t)SEGMENT.speed + 20)) >> 10); // emit speed according to loudness of band + uint16_t emitangle = j * angleoffset + SEGENV.aux0; + + uint32_t emitparticles = 0; + if (fftResult[j] > threshold) + emitparticles = 1; + else if (fftResult[j] > 0) { // band has low value + uint32_t restvolume = ((threshold - fftResult[j]) >> 2) + 2; + if (hw_random16() % restvolume == 0) + emitparticles = 1; + } + if (emitparticles) + PartSys->angleEmit(PartSys->sources[j], emitangle, emitspeed); + + j = (j + 1) % numSprays; + } + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLECIRCULARGEQ[] PROGMEM = "PS GEQ Nova@Speed,Intensity,Rotation Speed,Color Change,Nozzle,,Direction;;!;2f;pal=13,ix=180,c1=0,c2=0,c3=8"; + +/* + Particle replacement of Ghost Rider by DedeHai (Damian Schneider), original FX by stepko adapted by Blaz Kristan (AKA blazoncek) +*/ +#define MAXANGLESTEP 2200 //32767 means 180° +uint16_t mode_particleghostrider(void) { + ParticleSystem2D *PartSys = nullptr; + PSsettings2D ghostsettings; + ghostsettings.asByte = 0b0000011; //enable wrapX and wrapY + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem2D(PartSys, 1)) // init, no additional data needed + return mode_static(); // allocation failed or not 2D + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->sources[0].maxLife = 260; // lifetime in frames + PartSys->sources[0].minLife = 250; + PartSys->sources[0].source.x = hw_random16(PartSys->maxX); + PartSys->sources[0].source.y = hw_random16(PartSys->maxY); + SEGENV.step = hw_random16(MAXANGLESTEP) - (MAXANGLESTEP>>1); // angle increment + } + else { + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + } + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + if (SEGMENT.intensity > 0) { // spiraling + if (SEGENV.aux1) { + SEGENV.step += SEGMENT.intensity>>3; + if ((int32_t)SEGENV.step > MAXANGLESTEP) + SEGENV.aux1 = 0; + } + else { + SEGENV.step -= SEGMENT.intensity>>3; + if ((int32_t)SEGENV.step < -MAXANGLESTEP) + SEGENV.aux1 = 1; + } + } + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(SEGMENT.custom1); + PartSys->sources[0].var = SEGMENT.custom3 >> 1; + + // color by age (PS 'color by age' always starts with hue = 255, don't want that here) + if (SEGMENT.check1) { + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->particles[i].hue = PartSys->sources[0].source.hue + (PartSys->particles[i].ttl<<2); + } + } + + // enable/disable walls + ghostsettings.bounceX = SEGMENT.check2; + ghostsettings.bounceY = SEGMENT.check2; + + SEGENV.aux0 += (int32_t)SEGENV.step; // step is angle increment + uint16_t emitangle = SEGENV.aux0 + 32767; // +180° + int32_t speed = map(SEGMENT.speed, 0, 255, 12, 64); + PartSys->sources[0].source.vx = ((int32_t)cos16_t(SEGENV.aux0) * speed) / (int32_t)32767; + PartSys->sources[0].source.vy = ((int32_t)sin16_t(SEGENV.aux0) * speed) / (int32_t)32767; + PartSys->sources[0].source.ttl = 500; // source never dies (note: setting 'perpetual' is not needed if replenished each frame) + PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags, &ghostsettings); + // set head (steal one of the particles) + PartSys->particles[PartSys->usedParticles-1].x = PartSys->sources[0].source.x; + PartSys->particles[PartSys->usedParticles-1].y = PartSys->sources[0].source.y; + PartSys->particles[PartSys->usedParticles-1].ttl = 255; + PartSys->particles[PartSys->usedParticles-1].sat = 0; //white + // emit two particles + PartSys->angleEmit(PartSys->sources[0], emitangle, speed); + PartSys->angleEmit(PartSys->sources[0], emitangle, speed); + if (SEGMENT.call % (11 - (SEGMENT.custom2 / 25)) == 0) { // every nth frame, cycle color and emit particles //TODO: make this a segment call % SEGMENT.custom2 for better control + PartSys->sources[0].source.hue++; + } + if (SEGMENT.custom2 > 190) //fast color change + PartSys->sources[0].source.hue += (SEGMENT.custom2 - 190) >> 2; + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEGHOSTRIDER[] PROGMEM = "PS Ghost Rider@Speed,Spiral,Blur,Color Cycle,Spread,AgeColor,Walls;;!;2;pal=1,sx=70,ix=0,c1=220,c2=30,c3=21,o1=1"; + +/* + PS Blobs: large particles bouncing around, changing size and form + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleblobs(void) { + ParticleSystem2D *PartSys = nullptr; + + if (SEGMENT.call == 0) { + if (!initParticleSystem2D(PartSys, 1, 0, true, true)) //init, request one source, no additional bytes, advanced size & size control (actually dont really need one TODO: test if using zero sources also works) + return mode_static(); // allocation failed or not 2D + PartSys->setBounceX(true); + PartSys->setBounceY(true); + PartSys->setWallHardness(255); + PartSys->setWallRoughness(255); + PartSys->setCollisionHardness(255); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 25, 128)); // minimum 10%, maximum 50% of available particles (note: PS ensures at least 1) + PartSys->enableParticleCollisions(SEGMENT.check2); + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // update particles + if (SEGENV.aux0 != SEGMENT.speed || PartSys->particles[i].ttl == 0) { // speed changed or dead + PartSys->particles[i].vx = (int8_t)hw_random16(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); // +/- speed/4 + PartSys->particles[i].vy = (int8_t)hw_random16(SEGMENT.speed >> 1) - (SEGMENT.speed >> 2); + } + if (SEGENV.aux1 != SEGMENT.custom1 || PartSys->particles[i].ttl == 0) // size changed or dead + PartSys->advPartSize[i].maxsize = 60 + (SEGMENT.custom1 >> 1) + hw_random16((SEGMENT.custom1 >> 2)); // set each particle to slightly randomized size + + //PartSys->particles[i].perpetual = SEGMENT.check2; //infinite life if set + if (PartSys->particles[i].ttl == 0) { // find dead particle, renitialize + PartSys->particles[i].ttl = 300 + hw_random16(((uint16_t)SEGMENT.custom2 << 3) + 100); + PartSys->particles[i].x = hw_random(PartSys->maxX); + PartSys->particles[i].y = hw_random16(PartSys->maxY); + PartSys->particles[i].hue = hw_random16(); // set random color + PartSys->particleFlags[i].collide = true; // enable collision for particle + PartSys->advPartProps[i].size = 0; // start out small + PartSys->advPartSize[i].asymmetry = hw_random16(220); + PartSys->advPartSize[i].asymdir = hw_random16(255); + // set advanced size control properties + PartSys->advPartSize[i].grow = true; + PartSys->advPartSize[i].growspeed = 1 + hw_random16(9); + PartSys->advPartSize[i].shrinkspeed = 1 + hw_random16(9); + PartSys->advPartSize[i].wobblespeed = 1 + hw_random16(3); + } + //PartSys->advPartSize[i].asymmetry++; + PartSys->advPartSize[i].pulsate = SEGMENT.check3; + PartSys->advPartSize[i].wobble = SEGMENT.check1; + } + SEGENV.aux0 = SEGMENT.speed; //write state back + SEGENV.aux1 = SEGMENT.custom1; + + #ifdef USERMOD_AUDIOREACTIVE + um_data_t *um_data; + if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // get AR data, do not use simulated data + uint8_t volumeSmth = (uint8_t)(*(float*)um_data->u_data[0]); + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // update particles + if (SEGMENT.check3) //pulsate selected + PartSys->advPartProps[i].size = volumeSmth; + } + } + #endif + + PartSys->setMotionBlur(((SEGMENT.custom3) << 3) + 7); + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEBLOBS[] PROGMEM = "PS Blobs@Speed,Blobs,Size,Life,Blur,Wobble,Collide,Pulsate;;!;2v;sx=30,ix=64,c1=200,c2=130,c3=0,o3=1"; +#endif //WLED_DISABLE_PARTICLESYSTEM2D +#endif // WLED_DISABLE_2D + +/////////////////////////// +// 1D Particle System FX // +/////////////////////////// + +#ifndef WLED_DISABLE_PARTICLESYSTEM1D +/* + Particle version of Drip and Rain + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleDrip(void) { + ParticleSystem1D *PartSys = nullptr; + //uint8_t numSprays; + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 4)) // init + return mode_static(); // allocation failed or single pixel + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return (except on top, taken care of by gravity setting) + PartSys->sources[0].source.hue = hw_random16(); + SEGENV.aux1 = 0xFFFF; // invalidate + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setBounce(true); + PartSys->setWallHardness(50); + + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setGravity(SEGMENT.custom3 >> 1); // set gravity (8 is default strength) + PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering + + if (SEGMENT.check2) { //collisions enabled + PartSys->enableParticleCollisions(true); //enable, full hardness + } + else + PartSys->enableParticleCollisions(false); + + PartSys->sources[0].sourceFlags.collide = false; //drops do not collide + + if (SEGMENT.check1) { //rain mode, emit at random position, short life (3-8 seconds at 50fps) + if (SEGMENT.custom1 == 0) //splash disabled, do not bounce raindrops + PartSys->setBounce(false); + PartSys->sources[0].var = 5; + PartSys->sources[0].v = -(8 + (SEGMENT.speed >> 2)); //speed + var must be < 128, inverted speed (=down) + // lifetime in frames + PartSys->sources[0].minLife = 30; + PartSys->sources[0].maxLife = 200; + PartSys->sources[0].source.x = hw_random(PartSys->maxX); //random emit position + } + else { //drip + PartSys->sources[0].var = 0; + PartSys->sources[0].v = -(SEGMENT.speed >> 1); //speed + var must be < 128, inverted speed (=down) + PartSys->sources[0].minLife = 3000; + PartSys->sources[0].maxLife = 3000; + PartSys->sources[0].source.x = PartSys->maxX - PS_P_RADIUS_1D; + } + + if (SEGENV.aux1 != SEGMENT.intensity) //slider changed + SEGENV.aux0 = 1; //must not be zero or "% 0" happens below which crashes on ESP32 + + SEGENV.aux1 = SEGMENT.intensity; // save state + + // every nth frame emit a particle + if (SEGMENT.call % SEGENV.aux0 == 0) { + int32_t interval = 300 / ((SEGMENT.intensity) + 1); + SEGENV.aux0 = interval + hw_random(interval + 5); + // if (SEGMENT.check1) // rain mode + // PartSys->sources[0].source.hue = 0; + // else + PartSys->sources[0].source.hue = hw_random8(); //set random color TODO: maybe also not random but color cycling? need another slider or checkmark for this. + PartSys->sprayEmit(PartSys->sources[0]); + } + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { //check all particles + if (PartSys->particles[i].ttl && PartSys->particleFlags[i].collide == false) { // use collision flag to identify splash particles + if (SEGMENT.custom1 > 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D << 1)) { //splash enabled and reached bottom + PartSys->particles[i].ttl = 0; //kill origin particle + PartSys->sources[0].maxLife = 80; + PartSys->sources[0].minLife = 20; + PartSys->sources[0].var = 10 + (SEGMENT.custom1 >> 3); + PartSys->sources[0].v = 0; + PartSys->sources[0].source.hue = PartSys->particles[i].hue; + PartSys->sources[0].source.x = PS_P_RADIUS_1D; + PartSys->sources[0].sourceFlags.collide = true; //splashes do collide if enabled + for (int j = 0; j < 2 + (SEGMENT.custom1 >> 2); j++) { + PartSys->sprayEmit(PartSys->sources[0]); + } + } + } + + if (SEGMENT.check1) { //rain mode, fade hue to max + if (PartSys->particles[i].hue < 245) + PartSys->particles[i].hue += 8; + } + //increase speed on high settings by calling the move function twice + if (SEGMENT.speed > 200) + PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]); + } + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEDRIP[] PROGMEM = "PS DripDrop@Speed,!,Splash,Blur,Gravity,Rain,PushSplash,Smooth;,!;!;1;pal=0,sx=150,ix=25,c1=220,c2=30,c3=21"; + + +/* + Particle Replacement for "Bbouncing Balls by Aircoookie" + Also replaces rolling balls and juggle (and maybe popcorn) + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particlePinball(void) { + ParticleSystem1D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 128, 0, true)) // init + return mode_static(); // allocation failed or is single pixel + PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide (if enabled) + PartSys->sources[0].source.x = PS_P_RADIUS_1D; //emit at bottom + PartSys->setKillOutOfBounds(true); // out of bounds particles dont return + PartSys->setUsedParticles(255); // use all available particles for init + SEGENV.aux0 = 1; + SEGENV.aux1 = 5000; //set out of range to ensure uptate on first call + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + //uint32_t hardness = 240 + (SEGMENT.custom1>>4); + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setGravity(map(SEGMENT.custom3, 0 , 31, 0 , 16)); // set gravity (8 is default strength) + PartSys->setBounce(SEGMENT.custom3); // disables bounce if no gravity is used + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->enableParticleCollisions(SEGMENT.check1, 255); // enable collisions and set particle collision to high hardness + PartSys->setUsedParticles(SEGMENT.intensity); + PartSys->setColorByPosition(SEGMENT.check3); + + bool updateballs = false; + if (SEGENV.aux1 != SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1 + PartSys->usedParticles) { // user settings change or more particles are available + SEGENV.step = SEGMENT.call; // reset delay + updateballs = true; + PartSys->sources[0].maxLife = SEGMENT.custom3 ? 5000 : 0xFFFF; // maximum lifetime in frames/2 (very long if not using gravity, this is enough to travel 4000 pixels at min speed) + PartSys->sources[0].minLife = PartSys->sources[0].maxLife >> 1; + } + + if (SEGMENT.check2) { //rolling balls + PartSys->setGravity(0); + PartSys->setWallHardness(255); + int speedsum = 0; + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->particles[i].ttl = 260; // keep particles alive + if (updateballs) { //speed changed or particle is dead, set particle properties + PartSys->particleFlags[i].collide = true; + if (PartSys->particles[i].x == 0) { // still at initial position (when not switching from a PS) + PartSys->particles[i].x = hw_random16(PartSys->maxX); // random initial position for all particles + PartSys->particles[i].vx = (hw_random16() & 0x01) ? 1 : -1; // random initial direction + } + PartSys->particles[i].hue = hw_random8(); //set ball colors to random + PartSys->advPartProps[i].sat = 255; + PartSys->advPartProps[i].size = SEGMENT.custom1; + } + speedsum += abs(PartSys->particles[i].vx); + } + int32_t avgSpeed = speedsum / PartSys->usedParticles; + int32_t setSpeed = 2 + (SEGMENT.speed >> 3); + if (avgSpeed < setSpeed) { // if balls are slow, speed up some of them at random to keep the animation going + for (int i = 0; i < setSpeed - avgSpeed; i++) { + int idx = hw_random16(PartSys->usedParticles); + PartSys->particles[idx].vx += PartSys->particles[idx].vx >= 0 ? 1 : -1; // add 1, keep direction + } + } + else if (avgSpeed > setSpeed + 8) // if avg speed is too high, apply friction to slow them down + PartSys->applyFriction(1); + } + else { //bouncing balls + PartSys->setWallHardness(220); + PartSys->sources[0].var = SEGMENT.speed >> 3; + int32_t newspeed = 2 + (SEGMENT.speed >> 1) - (SEGMENT.speed >> 3); + PartSys->sources[0].v = newspeed; + //check for balls that are 'laying on the ground' and remove them + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].vx == 0 && PartSys->particles[i].x < (PS_P_RADIUS_1D + SEGMENT.custom1)) + PartSys->particles[i].ttl = 0; + if (updateballs) { + PartSys->advPartProps[i].size = SEGMENT.custom1; + if (SEGMENT.custom3 == 0) //gravity off, update speed + PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? newspeed : -newspeed; //keep the direction + } + } + + // every nth frame emit a ball + if (SEGMENT.call > SEGENV.step) { + int interval = 260 - ((int)SEGMENT.intensity); + SEGENV.step += interval + hw_random16(interval); + PartSys->sources[0].source.hue = hw_random16(); //set ball color + PartSys->sources[0].sat = 255; + PartSys->sources[0].size = SEGMENT.custom1; + PartSys->sprayEmit(PartSys->sources[0]); + } + } + SEGENV.aux1 = SEGMENT.speed + SEGMENT.intensity + SEGMENT.check2 + SEGMENT.custom1 + PartSys->usedParticles; + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i]); // double the speed + } + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PSPINBALL[] PROGMEM = "PS Pinball@Speed,!,Size,Blur,Gravity,Collide,Rolling,Position Color;,!;!;1;pal=0,ix=220,c2=0,c3=8,o1=1"; + +/* + Particle Replacement for original Dancing Shadows: + "Spotlights moving back and forth that cast dancing shadows. + Shine this through tree branches/leaves or other close-up objects that cast + interesting shadows onto a ceiling or tarp. + By Steve Pomeroy @xxv" + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleDancingShadows(void) { + ParticleSystem1D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1)) // init, one source + return mode_static(); // allocation failed or is single pixel + PartSys->sources[0].maxLife = 1000; //set long life (kill out of bounds is done in custom way) + PartSys->sources[0].minLife = PartSys->sources[0].maxLife; + } + else { + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + } + + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(SEGMENT.custom1); + if (SEGMENT.check1) + PartSys->setSmearBlur(120); // enable smear blur + else + PartSys->setSmearBlur(0); // disable smear blur + PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering + PartSys->setColorByPosition(SEGMENT.check2); // color fixed by position + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, 255)); // set percentage of particles to use + + uint32_t deadparticles = 0; + //kill out of bounds and moving away plus change color + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (((SEGMENT.call & 0x07) == 0) && PartSys->particleFlags[i].outofbounds) { //check if out of bounds particle move away from strip, only update every 8th frame + if ((int32_t)PartSys->particles[i].vx * PartSys->particles[i].x > 0) PartSys->particles[i].ttl = 0; //particle is moving away, kill it + } + PartSys->particleFlags[i].perpetual = true; //particles do not age + if (SEGMENT.call % (32 / (1 + (SEGMENT.custom2 >> 3))) == 0) + PartSys->particles[i].hue += 2 + (SEGMENT.custom2 >> 5); + //note: updating speed on the fly is not accurately possible, since it is unknown which particles are assigned to which spot + if (SEGENV.aux0 != SEGMENT.speed) { //speed changed + //update all particle speed by setting them to current value + PartSys->particles[i].vx = PartSys->particles[i].vx > 0 ? SEGMENT.speed >> 3 : -SEGMENT.speed >> 3; + } + if (PartSys->particles[i].ttl == 0) deadparticles++; // count dead particles + } + SEGENV.aux0 = SEGMENT.speed; + + //generate a spotlight: generates particles just outside of view + if (deadparticles > 5 && (SEGMENT.call & 0x03) == 0) { + //random color, random type + uint32_t type = hw_random16(SPOT_TYPES_COUNT); + int8_t speed = 2 + hw_random16(2 + (SEGMENT.speed >> 1)) + (SEGMENT.speed >> 4); + int32_t width = hw_random16(1, 10); + uint32_t ttl = 300; //ttl is particle brightness (below perpetual is set so it does not age, i.e. ttl stays at this value) + int32_t position; + //choose random start position, left and right from the segment + if (hw_random() & 0x01) { + position = PartSys->maxXpixel; + speed = -speed; + } + else + position = -width; + + PartSys->sources[0].v = speed; //emitted particle speed + PartSys->sources[0].source.hue = hw_random8(); //random spotlight color + for (int32_t i = 0; i < width; i++) { + if (width > 1) { + switch (type) { + case SPOT_TYPE_SOLID: + //nothing to do + break; + + case SPOT_TYPE_GRADIENT: + ttl = cubicwave8(map(i, 0, width - 1, 0, 255)); + ttl = ttl*ttl >> 8; //make gradient more pronounced + break; + + case SPOT_TYPE_2X_GRADIENT: + ttl = cubicwave8(2 * map(i, 0, width - 1, 0, 255)); + ttl = ttl*ttl >> 8; + break; + + case SPOT_TYPE_2X_DOT: + if (i > 0) position++; //skip one pixel + i++; + break; + + case SPOT_TYPE_3X_DOT: + if (i > 0) position += 2; //skip two pixels + i+=2; + break; + + case SPOT_TYPE_4X_DOT: + if (i > 0) position += 3; //skip three pixels + i+=3; + break; + } + } + //emit particle + //set the particle source position: + PartSys->sources[0].source.x = position * PS_P_RADIUS_1D; + uint32_t partidx = PartSys->sprayEmit(PartSys->sources[0]); + PartSys->particles[partidx].ttl = ttl; + position++; //do the next pixel + } + } + + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PARTICLEDANCINGSHADOWS[] PROGMEM = "PS Dancing Shadows@Speed,!,Blur,Color Cycle,,Smear,Position Color,Smooth;,!;!;1;sx=100,ix=180,c1=0,c2=0"; + +/* + Particle Fireworks 1D replacement + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleFireworks1D(void) { + ParticleSystem1D *PartSys = nullptr; + uint8_t *forcecounter; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 4, 150, 4, true)) // init + return mode_static(); // allocation failed or is single pixel + PartSys->setKillOutOfBounds(true); + PartSys->sources[0].sourceFlags.custom1 = 1; // set rocket state to standby + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + forcecounter = PartSys->PSdataEnd; + PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + + int32_t gravity = (1 + (SEGMENT.speed >> 3)); + if (!SEGMENT.check1) // gravity enabled for sparks + PartSys->setGravity(0); // disable + else + PartSys->setGravity(gravity); // set gravity + + if (PartSys->sources[0].sourceFlags.custom1 == 1) { // rocket is on standby + PartSys->sources[0].source.ttl--; + if (PartSys->sources[0].source.ttl == 0) { // time is up, relaunch + + if (hw_random8() < SEGMENT.custom1) // randomly choose direction according to slider, fire at start of segment if true + SEGENV.aux0 = 1; + else + SEGENV.aux0 = 0; + + PartSys->sources[0].sourceFlags.custom1 = 0; //flag used for rocket state + PartSys->sources[0].source.hue = hw_random16(); + PartSys->sources[0].var = 10; // emit variation + PartSys->sources[0].v = -10; // emit speed + PartSys->sources[0].minLife = 100; + PartSys->sources[0].maxLife = 300; + PartSys->sources[0].source.x = 0; // start from bottom + uint32_t speed = sqrt((gravity * ((PartSys->maxX >> 2) + hw_random16(PartSys->maxX >> 1))) >> 4); // set speed such that rocket explods in frame + PartSys->sources[0].source.vx = min(speed, (uint32_t)127); + PartSys->sources[0].source.ttl = 4000; + PartSys->sources[0].sat = 30; // low saturation exhaust + PartSys->sources[0].size = 0; // default size + PartSys->sources[0].sourceFlags.reversegrav = false ; // normal gravity + + if (SEGENV.aux0) { // inverted rockets launch from end + PartSys->sources[0].sourceFlags.reversegrav = true; + PartSys->sources[0].source.x = PartSys->maxX; // start from top + PartSys->sources[0].source.vx = -PartSys->sources[0].source.vx; // revert direction + PartSys->sources[0].v = -PartSys->sources[0].v; // invert exhaust emit speed + } + } + } + else { // rocket is launched + int32_t rocketgravity = -gravity; + int32_t speed = PartSys->sources[0].source.vx; + if (SEGENV.aux0) { // negative speed rocket + rocketgravity = -rocketgravity; + speed = -speed; + } + PartSys->applyForce(PartSys->sources[0].source, rocketgravity, forcecounter[0]); + PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags); + PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags); // increase speed by calling the move function twice, also ages twice + uint32_t rocketheight = SEGENV.aux0 ? PartSys->maxX - PartSys->sources[0].source.x : PartSys->sources[0].source.x; + + if (speed < 0 && PartSys->sources[0].source.ttl > 50) // reached apogee + PartSys->sources[0].source.ttl = min((uint32_t)50, rocketheight >> (PS_P_RADIUS_SHIFT_1D + 3)); // alive for a few more frames + + if (PartSys->sources[0].source.ttl < 2) { // explode + PartSys->sources[0].sourceFlags.custom1 = 1; // set standby state + PartSys->sources[0].var = 5 + ((((PartSys->maxX >> 1) + rocketheight) * (200 + SEGMENT.intensity)) / (PartSys->maxX << 2)); // set explosion particle speed + PartSys->sources[0].minLife = 600; + PartSys->sources[0].maxLife = 1300; + PartSys->sources[0].source.ttl = 100 + hw_random16(64 - (SEGMENT.speed >> 2)); // standby time til next launch + PartSys->sources[0].sat = 7 + (SEGMENT.custom3 << 3); //color saturation TODO: replace saturation with something more useful? + PartSys->sources[0].size = hw_random16(64); // random particle size in explosion + uint32_t explosionsize = 8 + (PartSys->maxXpixel >> 2) + (PartSys->sources[0].source.x >> (PS_P_RADIUS_SHIFT_1D - 1)); + explosionsize += hw_random16((explosionsize * SEGMENT.intensity) >> 8); + for (uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles + if (SEGMENT.check2) + PartSys->sources[0].source.hue = hw_random16(); //random color for each particle + PartSys->sprayEmit(PartSys->sources[0]); // emit a particle + } + } + } + if ((SEGMENT.call & 0x01) == 0 && PartSys->sources[0].sourceFlags.custom1 == false) // every second frame and not in standby + PartSys->sprayEmit(PartSys->sources[0]); // emit exhaust particle + if ((SEGMENT.call & 0x03) == 0) // every fourth frame + PartSys->applyFriction(1); // apply friction to all particles + + PartSys->update(); // update and render + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl > 10) PartSys->particles[i].ttl -= 10; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan + else PartSys->particles[i].ttl = 0; + } + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur,Saturation,,Colorful,Smooth;,!;!;1;sx=150,c2=30,c3=31,o2=1"; + +/* + Particle based Sparkle effect + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleSparkler(void) { + ParticleSystem1D *PartSys = nullptr; + uint32_t numSparklers; + PSsettings1D sparklersettings; + sparklersettings.asByte = 0; // PS settings for sparkler (set below) + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 16, 128 ,0, true)) // init, no additional data needed + return mode_static(); // allocation failed or is single pixel + } else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + + sparklersettings.wrap = !SEGMENT.check2; + sparklersettings.bounce = SEGMENT.check2; // note: bounce always takes priority over wrap + + numSparklers = PartSys->numSources; + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur/overlay + //PartSys->setSmearBlur(SEGMENT.custom2); // anable smearing blur + + for (uint32_t i = 0; i < numSparklers; i++) { + PartSys->sources[i].source.hue = hw_random16(); + PartSys->sources[i].var = 0; // sparks stationary + PartSys->sources[i].minLife = 150 + SEGMENT.intensity; + PartSys->sources[i].maxLife = 250 + (SEGMENT.intensity << 1); + uint32_t speed = SEGMENT.speed >> 1; + if (SEGMENT.check1) // sparks move (slide option) + PartSys->sources[i].var = SEGMENT.intensity >> 3; + PartSys->sources[i].source.vx = speed; // update speed, do not change direction + PartSys->sources[i].source.ttl = 400; // replenish its life (setting it perpetual uses more code) + PartSys->sources[i].sat = SEGMENT.custom1; // color saturation + PartSys->sources[i].size = SEGMENT.check3 ? 120 : 0; + if (SEGMENT.speed == 255) // random position at highest speed setting + PartSys->sources[i].source.x = hw_random16(PartSys->maxX); + else + PartSys->particleMoveUpdate(PartSys->sources[i].source, PartSys->sources[i].sourceFlags, &sparklersettings); //move sparkler + } + + numSparklers = min(1 + (SEGMENT.custom3 >> 1), (int)numSparklers); // set used sparklers, 1 to 16 + + if (SEGENV.aux0 != SEGMENT.custom3) { //number of used sparklers changed, redistribute + for (uint32_t i = 1; i < numSparklers; i++) { + PartSys->sources[i].source.x = (PartSys->sources[0].source.x + (PartSys->maxX / numSparklers) * i ) % PartSys->maxX; //distribute evenly + } + } + SEGENV.aux0 = SEGMENT.custom3; + + for (uint32_t i = 0; i < numSparklers; i++) { + if (hw_random() % (((271 - SEGMENT.intensity) >> 4)) == 0) + PartSys->sprayEmit(PartSys->sources[i]); //emit a particle + } + + PartSys->update(); // update and render + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl > (64 - (SEGMENT.intensity >> 2))) PartSys->particles[i].ttl -= (64 - (SEGMENT.intensity >> 2)); //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan + else PartSys->particles[i].ttl = 0; + } + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_SPARKLER[] PROGMEM = "PS Sparkler@Move,!,Saturation,Blur,Sparklers,Slide,Bounce,Large;,!;!;1;pal=0,sx=255,c1=0,c2=0,c3=6"; + +/* + Particle based Hourglass, particles falling at defined intervals + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleHourglass(void) { + ParticleSystem1D *PartSys = nullptr; + constexpr int positionOffset = PS_P_RADIUS_1D / 2;; // resting position offset + bool* direction; + uint32_t* settingTracker; + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 0, 255, 8, false)) // init + return mode_static(); // allocation failed or is single pixel + PartSys->setBounce(true); + PartSys->setWallHardness(100); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + settingTracker = reinterpret_cast(PartSys->PSdataEnd); //assign data pointer + direction = reinterpret_cast(PartSys->PSdataEnd + 4); //assign data pointer + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 1, 255)); + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setGravity(map(SEGMENT.custom3, 0, 31, 1, 30)); + PartSys->enableParticleCollisions(true, 34); // hardness value found by experimentation on different settings + + uint32_t colormode = SEGMENT.custom1 >> 5; // 0-7 + + if ((SEGMENT.intensity | (PartSys->getAvailableParticles() << 8)) != *settingTracker) { // initialize, getAvailableParticles changes while in FX transition + *settingTracker = SEGMENT.intensity | (PartSys->getAvailableParticles() << 8); + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->particleFlags[i].reversegrav = true; + *direction = 0; // down + SEGENV.aux1 = 1; // initialize below + } + SEGENV.aux0 = PartSys->usedParticles - 1; // initial state, start with highest number particle + } + + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { // check if particle reached target position after falling + int32_t targetposition; + if (PartSys->particleFlags[i].fixed == false) { // && abs(PartSys->particles[i].vx) < 8) { + // calculate target position depending on direction + bool closeToTarget = false; + bool reachedTarget = false; + if (PartSys->particleFlags[i].reversegrav) { // up + targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D) - positionOffset; // target resting position + if (targetposition - PartSys->particles[i].x <= 5 * PS_P_RADIUS_1D) + closeToTarget = true; + if (PartSys->particles[i].x >= targetposition) // particle has reached target position, pin it. if not pinned, they do not stack well on larger piles + reachedTarget = true; + } + else { // down, highest index particle drops first + targetposition = (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionOffset; // target resting position note: using -offset instead of -1 + offset + if (PartSys->particles[i].x - targetposition <= 5 * PS_P_RADIUS_1D) + closeToTarget = true; + if (PartSys->particles[i].x <= targetposition) // particle has reached target position, pin it. if not pinned, they do not stack well on larger piles + reachedTarget = true; + } + if (reachedTarget || (closeToTarget && abs(PartSys->particles[i].vx) < 10)) { // reached target or close to target and slow speed + PartSys->particles[i].x = targetposition; // set exact position + PartSys->particleFlags[i].fixed = true; // pin particle + } + } + if (colormode == 7) + PartSys->setColorByPosition(true); // color fixed by position + else { + PartSys->setColorByPosition(false); + uint8_t basehue = ((SEGMENT.custom1 & 0x1F) << 3); // use 5 LSBs to select color + switch(colormode) { + case 0: PartSys->particles[i].hue = 120; break; // fixed at 120, if flip is activated, this can make red and green (use palette 34) + case 1: PartSys->particles[i].hue = basehue; break; // fixed selectable color + case 2: // 2 colors inverleaved (same code as 3) + case 3: PartSys->particles[i].hue = ((SEGMENT.custom1 & 0x1F) << 1) + (i % colormode)*74; break; // interleved colors (every 2 or 3 particles) + case 4: PartSys->particles[i].hue = basehue + (i * 255) / PartSys->usedParticles; break; // gradient palette colors + case 5: PartSys->particles[i].hue = basehue + (i * 1024) / PartSys->usedParticles; break; // multi gradient palette colors + case 6: PartSys->particles[i].hue = i + (strip.now >> 3); break; // disco! moving color gradient + default: break; + } + } + if (SEGMENT.check1 && !PartSys->particleFlags[i].reversegrav) // flip color when fallen + PartSys->particles[i].hue += 120; + } + + if (SEGENV.aux1 == 1) { // last countdown call before dropping starts, reset all particles + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + PartSys->particleFlags[i].collide = true; + PartSys->particleFlags[i].perpetual = true; + PartSys->particles[i].ttl = 260; + uint32_t targetposition; + //calculate target position depending on direction + if (PartSys->particleFlags[i].reversegrav) + targetposition = PartSys->maxX - (i * PS_P_RADIUS_1D + positionOffset); // target resting position + else + targetposition = (PartSys->usedParticles - i) * PS_P_RADIUS_1D - positionOffset; // target resting position -5 - PS_P_RADIUS_1D/2 + + PartSys->particles[i].x = targetposition; + PartSys->particleFlags[i].fixed = true; + } + } + + if (SEGENV.aux1 == 0) { // countdown passed, run + if (strip.now >= SEGENV.step) { // drop a particle, do not drop more often than every second frame or particles tangle up quite badly + // set next drop time + if (SEGMENT.check3 && *direction) // fast reset + SEGENV.step = strip.now + 100; // drop one particle every 100ms + else // normal interval + SEGENV.step = strip.now + max(20, SEGMENT.speed * 20); // map speed slider from 0.1s to 5s + if (SEGENV.aux0 < PartSys->usedParticles) { + PartSys->particleFlags[SEGENV.aux0].reversegrav = *direction; // let this particle fall or rise + PartSys->particleFlags[SEGENV.aux0].fixed = false; // unpin + } + else { // overflow + *direction = !(*direction); // flip direction + SEGENV.aux1 = SEGMENT.virtualLength() + 100; // set countdown + } + if (*direction == 0) // down, start dropping the highest number particle + SEGENV.aux0--; // next particle + else + SEGENV.aux0++; + } + } + else if (SEGMENT.check2) // auto reset + SEGENV.aux1--; // countdown + + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_HOURGLASS[] PROGMEM = "PS Hourglass@Interval,!,Color,Blur,Gravity,Colorflip,Start,Fast Reset;,!;!;1;pal=34,sx=50,ix=200,c1=140,c2=80,c3=4,o1=1,o2=1,o3=1"; + +/* + Particle based Spray effect (like a volcano, possible replacement for popcorn) + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particle1Dspray(void) { + ParticleSystem1D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1)) + return mode_static(); // allocation failed or is single pixel + PartSys->setKillOutOfBounds(true); + PartSys->setWallHardness(150); + PartSys->setParticleSize(1); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setBounce(SEGMENT.check2); + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + int32_t gravity = -((int32_t)SEGMENT.custom3 - 16); // gravity setting, 0-15 is positive (down), 17 - 31 is negative (up) + PartSys->setGravity(abs(gravity)); // use reversgrav setting to invert gravity (for proper 'floor' and out of bounce handling) + + PartSys->sources[0].source.hue = SEGMENT.aux0; // hw_random16(); + PartSys->sources[0].var = 20; + PartSys->sources[0].minLife = 200; + PartSys->sources[0].maxLife = 400; + PartSys->sources[0].source.x = map(SEGMENT.custom1, 0 , 255, 0, PartSys->maxX); // spray position + PartSys->sources[0].v = map(SEGMENT.speed, 0 , 255, -127 + PartSys->sources[0].var, 127 - PartSys->sources[0].var); // particle emit speed + PartSys->sources[0].sourceFlags.reversegrav = gravity < 0 ? true : false; + + if (hw_random() % (1 + ((255 - SEGMENT.intensity) >> 3)) == 0) { + PartSys->sprayEmit(PartSys->sources[0]); // emit a particle + SEGMENT.aux0++; // increment hue + } + + //update color settings + PartSys->setColorByAge(SEGMENT.check1); // overruled by 'color by position' + PartSys->setColorByPosition(SEGMENT.check3); + for (uint i = 0; i < PartSys->usedParticles; i++) { + PartSys->particleFlags[i].reversegrav = PartSys->sources[0].sourceFlags.reversegrav; // update gravity direction + } + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_1DSPRAY[] PROGMEM = "PS Spray 1D@Speed(+/-),!,Position,Blur,Gravity(+/-),AgeColor,Bounce,Position Color;,!;!;1;sx=200,ix=220,c1=0,c2=0"; + +/* + Particle based balance: particles move back and forth (1D pendent to 2D particle box) + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleBalance(void) { + ParticleSystem1D *PartSys = nullptr; + uint32_t i; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 128)) // init, no additional data needed, use half of max particles + return mode_static(); // allocation failed or is single pixel + //PartSys->setKillOutOfBounds(true); + PartSys->setParticleSize(1); + SEGENV.aux0 = 0; + SEGENV.aux1 = 0; //TODO: really need to set to zero or is it calloc'd? + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setBounce(!SEGMENT.check2); + PartSys->setWrap(SEGMENT.check2); + uint8_t hardness = SEGMENT.custom1 > 0 ? map(SEGMENT.custom1, 0, 255, 50, 250) : 200; // set hardness, make the walls hard if collisions are disabled + PartSys->enableParticleCollisions(SEGMENT.custom1, hardness); // enable collisions if custom1 > 0 + PartSys->setWallHardness(200); + PartSys->setUsedParticles(map(SEGMENT.intensity, 0, 255, 10, 255)); + if (PartSys->usedParticles > SEGENV.aux1) { // more particles, reinitialize + for (i = 0; i < PartSys->usedParticles; i++) { + PartSys->particles[i].x = i * PS_P_RADIUS_1D; + PartSys->particles[i].ttl = 300; + PartSys->particleFlags[i].perpetual = true; + PartSys->particleFlags[i].collide = true; + } + } + SEGENV.aux1 = PartSys->usedParticles; + + if (SEGMENT.call % (((255 - SEGMENT.speed) >> 6) + 1) == 0) { // how often the force is applied depends on speed setting + int32_t xgravity; + int32_t increment = (SEGMENT.speed >> 6) + 1; + SEGENV.aux0 += increment; + if (SEGMENT.check3) // random, use perlin noise + xgravity = ((int16_t)inoise8(SEGENV.aux0) - 128); + else // sinusoidal + xgravity = (int16_t)cos8(SEGENV.aux0) - 128;//((int32_t)(SEGMENT.custom3 << 2) * cos8(SEGENV.aux0) + // scale the force + xgravity = (xgravity * ((SEGMENT.custom3+1) << 2)) / 128; // xgravity: -127 to +127 + PartSys->applyForce(xgravity); + } + + uint32_t randomindex = hw_random16(PartSys->usedParticles); + PartSys->particles[randomindex].vx = ((int32_t)PartSys->particles[randomindex].vx * 200) / 255; // apply friction to random particle to reduce clumping (without collisions) + + //if (SEGMENT.check2 && (SEGMENT.call & 0x07) == 0) // no walls, apply friction to smooth things out + if ((SEGMENT.call & 0x0F) == 0 && SEGMENT.custom3 > 2) // apply friction every 16th frame to smooth things out (except for low tilt) + PartSys->applyFriction(1); // apply friction to all particles + + //update colors + PartSys->setColorByPosition(SEGMENT.check1); + if (!SEGMENT.check1) { + for (i = 0; i < PartSys->usedParticles; i++) { + PartSys->particles[i].hue = (1024 * i) / PartSys->usedParticles; // color by particle index + } + } + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PS_BALANCE[] PROGMEM = "PS 1D Balance@!,!,Hardness,Blur,Tilt,Position Color,Wrap,Random;,!;!;1;pal=18,sx=64,c1=200,c2=0,c3=5,o1=1"; + +/* +Particle based Chase effect +Uses palette for particle color +by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleChase(void) { + ParticleSystem1D *PartSys = nullptr; + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 255, 3, true)) // init + return mode_static(); // allocation failed or is single pixel + SEGENV.aux0 = 0xFFFF; // invalidate + *PartSys->PSdataEnd = 1; // huedir + *(PartSys->PSdataEnd + 1) = 1; // sizedir + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setColorByPosition(SEGMENT.check3); + PartSys->setMotionBlur(8 + ((SEGMENT.custom3) << 3)); // anable motion blur + // uint8_t* basehue = (PartSys->PSdataEnd + 2); //assign data pointer + + uint32_t settingssum = SEGMENT.speed + SEGMENT.intensity + SEGMENT.custom1 + SEGMENT.custom2 + SEGMENT.check1 + SEGMENT.check2 + SEGMENT.check3 + PartSys->getAvailableParticles(); // note: getAvailableParticles is used to enforce update during transitions + if (SEGENV.aux0 != settingssum) { // settings changed changed, update + uint32_t numParticles = map(SEGMENT.intensity, 0, 255, 2, 255 / (1 + (SEGMENT.custom1 >> 6))); // depends on intensity and particle size (custom1) + if (numParticles == 0) numParticles = 1; // minimum 1 particle + PartSys->setUsedParticles(numParticles); + SEGENV.step = (PartSys->maxX + (PS_P_RADIUS_1D << 5)) / PartSys->usedParticles; // spacing between particles + for (int32_t i = 0; i < (int32_t)PartSys->usedParticles; i++) { + PartSys->advPartProps[i].sat = 255; + PartSys->particles[i].x = (i - 1) * SEGENV.step; // distribute evenly (starts out of frame for i=0) + PartSys->particles[i].vx = SEGMENT.speed >> 1; + PartSys->advPartProps[i].size = SEGMENT.custom1; + if (SEGMENT.custom2 < 255) + PartSys->particles[i].hue = (i * (SEGMENT.custom2 << 3)) / PartSys->usedParticles; // gradient distribution + else + PartSys->particles[i].hue = hw_random16(); + } + SEGENV.aux0 = settingssum; + } + + int32_t huestep = (((uint32_t)SEGMENT.custom2 << 19) / PartSys->usedParticles) >> 16; // hue increment + + // wrap around (cannot use particle system wrap if distributing colors manually, it also wraps rendering which does not look good) + for (int32_t i = (int32_t)PartSys->usedParticles - 1; i >= 0; i--) { // check from the back, last particle wraps first, multiple particles can overrun per frame + if (PartSys->particles[i].x > PartSys->maxX + PS_P_RADIUS_1D + PartSys->advPartProps[i].size) { // wrap it around + uint32_t nextindex = (i + 1) % PartSys->usedParticles; + PartSys->particles[i].x = PartSys->particles[nextindex].x - (int)SEGENV.step; + if (SEGMENT.custom2 < 255) + PartSys->particles[i].hue = PartSys->particles[nextindex].hue - huestep; + else + PartSys->particles[i].hue = hw_random16(); + } + PartSys->particles[i].ttl = 300; // reset ttl, cannot use perpetual because memmanager can change pointer at any time + } + + PartSys->setParticleSize(SEGMENT.custom1); // if custom1 == 0 this sets rendering size to one pixel + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PS_CHASE[] PROGMEM = "PS Chase@!,Density,Size,Hue,Blur,,,Position Color;,!;!;1;pal=11,sx=50,c2=5,c3=0"; + +/* + Particle Fireworks Starburst replacement (smoother rendering, more settings) + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleStarburst(void) { + ParticleSystem1D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 200, 0, true)) // init + return mode_static(); // allocation failed or is single pixel + PartSys->setKillOutOfBounds(true); + PartSys->enableParticleCollisions(true, 200); + PartSys->sources[0].source.ttl = 1; // set initial stanby time + PartSys->sources[0].sat = 0; // emitted particles start out white + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + PartSys->setGravity(SEGMENT.check1 * 8); // enable gravity + + if (PartSys->sources[0].source.ttl-- == 0) { // stanby time elapsed TODO: make it a timer? + uint32_t explosionsize = 4 + hw_random16(SEGMENT.intensity >> 2); + PartSys->sources[0].source.hue = hw_random16(); + PartSys->sources[0].var = 10 + (explosionsize << 1); + PartSys->sources[0].minLife = 250; + PartSys->sources[0].maxLife = 300; + PartSys->sources[0].source.x = hw_random(PartSys->maxX); //random explosion position + PartSys->sources[0].source.ttl = 10 + hw_random16(255 - SEGMENT.speed); + PartSys->sources[0].size = SEGMENT.custom1; // Fragment size + PartSys->setParticleSize(SEGMENT.custom1); // enable advanced size rendering + PartSys->sources[0].sourceFlags.collide = SEGMENT.check3; + for (uint32_t e = 0; e < explosionsize; e++) { // emit particles + if (SEGMENT.check2) + PartSys->sources[0].source.hue = hw_random16(); //random color for each particle + PartSys->sprayEmit(PartSys->sources[0]); //emit a particle + } + } + //shrink all particles + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->advPartProps[i].size) + PartSys->advPartProps[i].size--; + if (PartSys->advPartProps[i].sat < 251) + PartSys->advPartProps[i].sat += 1 + (SEGMENT.custom3 >> 2); //note: it should be >> 3, the >> 2 creates overflows resulting in blinking if custom3 > 27, which is a bonus feature + } + + if (SEGMENT.call % 5 == 0) { + PartSys->applyFriction(1); //slow down particles + } + + PartSys->update(); // update and render + return FRAMETIME; +} +static const char _data_FX_MODE_PS_STARBURST[] PROGMEM = "PS Starburst@Chance,Fragments,Size,Blur,Cooling,Gravity,Colorful,Push;,!;!;1;pal=52,sx=150,ix=150,c1=120,c2=0,c3=21"; + +/* + Particle based 1D GEQ effect, each frequency bin gets an emitter, distributed over the strip + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particle1DGEQ(void) { + ParticleSystem1D *PartSys = nullptr; + uint32_t numSources; + uint32_t i; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 16, 255, 0, true)) // init, no additional data needed + return mode_static(); // allocation failed or is single pixel + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + numSources = PartSys->numSources; + PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur + + uint32_t spacing = PartSys->maxX / numSources; + for (i = 0; i < numSources; i++) { + PartSys->sources[i].source.hue = i * 16; // hw_random16(); //TODO: make adjustable, maybe even colorcycle? + PartSys->sources[i].var = SEGMENT.speed >> 2; + PartSys->sources[i].minLife = 180 + (SEGMENT.intensity >> 1); + PartSys->sources[i].maxLife = 240 + SEGMENT.intensity; + PartSys->sources[i].sat = 255; + PartSys->sources[i].size = SEGMENT.custom1; + PartSys->setParticleSize(SEGMENT.custom1); + PartSys->sources[i].source.x = (spacing >> 1) + spacing * i; //distribute evenly + } + + for (i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl > 20) PartSys->particles[i].ttl -= 20; //ttl is linked to brightness, this allows to use higher brightness but still a short lifespan + else PartSys->particles[i].ttl = 0; + } + + um_data_t *um_data = getAudioData(); + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + + //map the bands into 16 positions on x axis, emit some particles according to frequency loudness + i = 0; + uint32_t bin = hw_random16(numSources); //current bin , start with random one to distribute available particles fairly + uint32_t threshold = 300 - SEGMENT.intensity; + + for (i = 0; i < numSources; i++) { + bin++; + bin = bin % numSources; + uint32_t emitparticle = 0; + // uint8_t emitspeed = ((uint32_t)fftResult[bin] * (uint32_t)SEGMENT.speed) >> 10; // emit speed according to loudness of band (127 max!) + if (fftResult[bin] > threshold) { + emitparticle = 1; + } + else if (fftResult[bin] > 0) { // band has low volue + uint32_t restvolume = ((threshold - fftResult[bin]) >> 2) + 2; + if (hw_random() % restvolume == 0) { + emitparticle = 1; + } + } + + if (emitparticle) + PartSys->sprayEmit(PartSys->sources[bin]); + } + //TODO: add color control? + + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_1D_GEQ[] PROGMEM = "PS GEQ 1D@Speed,!,Size,Blur,,,,;,!;!;1f;pal=0,sx=50,ix=200,c1=0,c2=0,c3=0,o1=1,o2=1"; + +/* + Particle based Fire effect + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particleFire1D(void) { + ParticleSystem1D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 5)) // init + return mode_static(); // allocation failed or is single pixel + PartSys->setKillOutOfBounds(true); + PartSys->setParticleSize(1); + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(128 + (SEGMENT.custom2 >> 1)); // enable motion blur + PartSys->setColorByAge(true); + uint32_t emitparticles = 1; + uint32_t j = hw_random16(); + for (uint i = 0; i < 3; i++) { // 3 base flames TODO: check if this is ok or needs adjustments + if (PartSys->sources[i].source.ttl > 50) + PartSys->sources[i].source.ttl -= 10; // TODO: in 2D making the source fade out slow results in much smoother flames, need to check if it can be done the same + else + PartSys->sources[i].source.ttl = 100 + hw_random16(200); + } + for (uint i = 0; i < PartSys->numSources; i++) { + j = (j + 1) % PartSys->numSources; + PartSys->sources[j].source.x = 0; + PartSys->sources[j].var = 2 + (SEGMENT.speed >> 4); + // base flames + if (j > 2) { + PartSys->sources[j].minLife = 150 + SEGMENT.intensity + (j << 2); // TODO: in 2D, min life is maxlife/2 and that looks very nice + PartSys->sources[j].maxLife = 200 + SEGMENT.intensity + (j << 3); + PartSys->sources[j].v = (SEGMENT.speed >> (2 + (j << 1))); + if (emitparticles) { + emitparticles--; + PartSys->sprayEmit(PartSys->sources[j]); // emit a particle + } + } + else { + PartSys->sources[j].minLife = PartSys->sources[j].source.ttl + SEGMENT.intensity; // TODO: in 2D, emitted particle ttl depends on source TTL, mimic here the same way? OR: change 2D to the same way it is done here and ditch special fire treatment in emit? + PartSys->sources[j].maxLife = PartSys->sources[j].minLife + 50; + PartSys->sources[j].v = SEGMENT.speed >> 2; + if (SEGENV.call & 0x01) // every second frame + PartSys->sprayEmit(PartSys->sources[j]); // emit a particle + } + } + + for (uint i = 0; i < PartSys->usedParticles; i++) { + PartSys->particles[i].x += PartSys->particles[i].ttl >> 7; // 'hot' particles are faster, apply some extra velocity + if (PartSys->particles[i].ttl > 3 + ((255 - SEGMENT.custom1) >> 1)) + PartSys->particles[i].ttl -= map(SEGMENT.custom1, 0, 255, 1, 3); // age faster + } + + PartSys->update(); // update and render + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_FIRE1D[] PROGMEM = "PS Fire 1D@!,!,Cooling,Blur;,!;!;1;pal=35,sx=100,ix=50,c1=80,c2=100,c3=28,o1=1,o2=1"; + +/* + Particle based AR effect, swoop particles along the strip with selected frequency loudness + Uses palette for particle color + by DedeHai (Damian Schneider) +*/ +uint16_t mode_particle1Dsonicstream(void) { + ParticleSystem1D *PartSys = nullptr; + + if (SEGMENT.call == 0) { // initialization + if (!initParticleSystem1D(PartSys, 1, 255, 0, true)) // init, no additional data needed + return mode_static(); // allocation failed or is single pixel + PartSys->setKillOutOfBounds(true); + PartSys->sources[0].source.x = 0; // at start + //PartSys->sources[1].source.x = PartSys->maxX; // at end + PartSys->sources[0].var = 0;//SEGMENT.custom1 >> 3; + PartSys->sources[0].sat = 255; + } + else + PartSys = reinterpret_cast(SEGENV.data); // if not first call, just set the pointer to the PS + if (PartSys == nullptr) + return mode_static(); // something went wrong, no data! + + // Particle System settings + PartSys->updateSystem(); // update system properties (dimensions and data pointers) + PartSys->setMotionBlur(20 + (SEGMENT.custom2 >> 1)); // anable motion blur + PartSys->setSmearBlur(200); // smooth out the edges + + PartSys->sources[0].v = 5 + (SEGMENT.speed >> 2); + + // FFT processing + um_data_t *um_data = getAudioData(); + uint8_t *fftResult = (uint8_t *)um_data->u_data[2]; // 16 bins with FFT data, log mapped already, each band contains frequency amplitude 0-255 + uint32_t loudness; + uint32_t baseBin = SEGMENT.custom3 >> 1; // 0 - 15 map(SEGMENT.custom3, 0, 31, 0, 14); + + loudness = fftResult[baseBin];// + fftResult[baseBin + 1]; + int mids = sqrt16((int)fftResult[5] + (int)fftResult[6] + (int)fftResult[7] + (int)fftResult[8] + (int)fftResult[9] + (int)fftResult[10]); // average the mids, bin 5 is ~500Hz, bin 10 is ~2kHz (see audio_reactive.h) + if (baseBin > 12) + loudness = loudness << 2; // double loudness for high frequencies (better detecion) + + uint32_t threshold = 150 - (SEGMENT.intensity >> 1); + if (SEGMENT.check2) { // enable low pass filter for dynamic threshold + SEGMENT.step = (SEGMENT.step * 31500 + loudness * (32768 - 31500)) >> 15; // low pass filter for simple beat detection: add average to base threshold + threshold = 20 + (threshold >> 1) + SEGMENT.step; // add average to threshold + } + + // color + uint32_t hueincrement = (SEGMENT.custom1 >> 3); // 0-31 + if (SEGMENT.custom1 < 255) + PartSys->setColorByPosition(false); + else + PartSys->setColorByPosition(true); + + // particle manipulation + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->sources[0].sourceFlags.perpetual == false) { // age faster if not perpetual + if (PartSys->particles[i].ttl > 2) { + PartSys->particles[i].ttl -= 2; //ttl is linked to brightness, this allows to use higher brightness but still a short lifespan + } + else PartSys->particles[i].ttl = 0; + } + if (SEGMENT.check1) // modulate colors by mid frequencies + PartSys->particles[i].hue += (mids * inoise8(PartSys->particles[i].x << 2, SEGMENT.step << 2)) >> 9; // color by perlin noise from mid frequencies + } + + if (loudness > threshold) { + SEGMENT.aux0 += hueincrement; // change color + PartSys->sources[0].minLife = 100 + (((unsigned)SEGMENT.intensity * loudness * loudness) >> 13); + PartSys->sources[0].maxLife = PartSys->sources[0].minLife; + PartSys->sources[0].source.hue = SEGMENT.aux0; + PartSys->sources[0].size = SEGMENT.speed; + if (PartSys->particles[SEGMENT.aux1].x > 3 * PS_P_RADIUS_1D || PartSys->particles[SEGMENT.aux1].ttl == 0) { // only emit if last particle is far enough away or dead + int partindex = PartSys->sprayEmit(PartSys->sources[0]); // emit a particle + if (partindex >= 0) SEGMENT.aux1 = partindex; // track last emitted particle + } + } + else loudness = 0; // required for push mode + + PartSys->update(); // update and render (needs to be done before manipulation for initial particle spacing to be right) + + if (SEGMENT.check3) { // push mode + PartSys->sources[0].sourceFlags.perpetual = true; // emitted particles dont age + PartSys->applyFriction(1); //slow down particles + int32_t movestep = (((int)SEGMENT.speed + 2) * loudness) >> 10; + if (movestep) { + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].ttl) { + PartSys->particles[i].x += movestep; // push particles + PartSys->particles[i].vx = 10 + (SEGMENT.speed >> 4) ; // give particles some speed for smooth movement (friction will slow them down) + } + } + } + } + else { + PartSys->sources[0].sourceFlags.perpetual = false; // emitted particles age + // move all particles (again) to allow faster speeds + for (uint32_t i = 0; i < PartSys->usedParticles; i++) { + if (PartSys->particles[i].vx == 0) + PartSys->particles[i].vx = PartSys->sources[0].v; // move static particles (after disabling push mode) + PartSys->particleMoveUpdate(PartSys->particles[i], PartSys->particleFlags[i], nullptr, &PartSys->advPartProps[i]); + } + } + + return FRAMETIME; +} +static const char _data_FX_MODE_PS_SONICSTREAM[] PROGMEM = "PS Sonic Stream@!,!,Color,Blur,Bin,Mod,Filter,Push;,!;!;1f;c3=0,o2=1"; +#endif // WLED_DISABLE_PARTICLESYSTEM1D + +////////////////////////////////////////////////////////////////////////////////////////// +// mode data +static const char _data_RESERVED[] PROGMEM = "RSVD"; + +// add (or replace reserved) effect mode and data into vector +// use id==255 to find unallocated gaps (with "Reserved" data string) +// if vector size() is smaller than id (single) data is appended at the end (regardless of id) +// return the actual id used for the effect or 255 if the add failed. +uint8_t WS2812FX::addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name) { + if (id == 255) { // find empty slot + for (size_t i=1; i<_mode.size(); i++) if (_modeData[i] == _data_RESERVED) { id = i; break; } + } + if (id < _mode.size()) { + if (_modeData[id] != _data_RESERVED) return 255; // do not overwrite an already added effect + _mode[id] = mode_fn; + _modeData[id] = mode_name; + return id; + } else if (_mode.size() < 255) { // 255 is reserved for indicating the effect wasn't added + _mode.push_back(mode_fn); + _modeData.push_back(mode_name); + if (_modeCount < _mode.size()) _modeCount++; + return _mode.size() - 1; + } else { + return 255; // The vector is full so return 255 + } +} + +void WS2812FX::setupEffectData() { + // Solid must be first! (assuming vector is empty upon call to setup) + _mode.push_back(&mode_static); + _modeData.push_back(_data_FX_MODE_STATIC); + // fill reserved word in case there will be any gaps in the array + for (size_t i=1; i<_modeCount; i++) { + _mode.push_back(&mode_static); + _modeData.push_back(_data_RESERVED); + } + // now replace all pre-allocated effects + // --- 1D non-audio effects --- + addEffect(FX_MODE_BLINK, &mode_blink, _data_FX_MODE_BLINK); + addEffect(FX_MODE_BREATH, &mode_breath, _data_FX_MODE_BREATH); + addEffect(FX_MODE_COLOR_WIPE, &mode_color_wipe, _data_FX_MODE_COLOR_WIPE); + addEffect(FX_MODE_COLOR_WIPE_RANDOM, &mode_color_wipe_random, _data_FX_MODE_COLOR_WIPE_RANDOM); + addEffect(FX_MODE_RANDOM_COLOR, &mode_random_color, _data_FX_MODE_RANDOM_COLOR); + addEffect(FX_MODE_COLOR_SWEEP, &mode_color_sweep, _data_FX_MODE_COLOR_SWEEP); + addEffect(FX_MODE_DYNAMIC, &mode_dynamic, _data_FX_MODE_DYNAMIC); + addEffect(FX_MODE_RAINBOW, &mode_rainbow, _data_FX_MODE_RAINBOW); + addEffect(FX_MODE_RAINBOW_CYCLE, &mode_rainbow_cycle, _data_FX_MODE_RAINBOW_CYCLE); + addEffect(FX_MODE_SCAN, &mode_scan, _data_FX_MODE_SCAN); + addEffect(FX_MODE_DUAL_SCAN, &mode_dual_scan, _data_FX_MODE_DUAL_SCAN); + addEffect(FX_MODE_FADE, &mode_fade, _data_FX_MODE_FADE); + addEffect(FX_MODE_THEATER_CHASE, &mode_theater_chase, _data_FX_MODE_THEATER_CHASE); + addEffect(FX_MODE_THEATER_CHASE_RAINBOW, &mode_theater_chase_rainbow, _data_FX_MODE_THEATER_CHASE_RAINBOW); + addEffect(FX_MODE_RUNNING_LIGHTS, &mode_running_lights, _data_FX_MODE_RUNNING_LIGHTS); + addEffect(FX_MODE_SAW, &mode_saw, _data_FX_MODE_SAW); + addEffect(FX_MODE_TWINKLE, &mode_twinkle, _data_FX_MODE_TWINKLE); + addEffect(FX_MODE_DISSOLVE, &mode_dissolve, _data_FX_MODE_DISSOLVE); + addEffect(FX_MODE_DISSOLVE_RANDOM, &mode_dissolve_random, _data_FX_MODE_DISSOLVE_RANDOM); + addEffect(FX_MODE_FLASH_SPARKLE, &mode_flash_sparkle, _data_FX_MODE_FLASH_SPARKLE); + addEffect(FX_MODE_HYPER_SPARKLE, &mode_hyper_sparkle, _data_FX_MODE_HYPER_SPARKLE); + addEffect(FX_MODE_STROBE, &mode_strobe, _data_FX_MODE_STROBE); + addEffect(FX_MODE_STROBE_RAINBOW, &mode_strobe_rainbow, _data_FX_MODE_STROBE_RAINBOW); + addEffect(FX_MODE_MULTI_STROBE, &mode_multi_strobe, _data_FX_MODE_MULTI_STROBE); + addEffect(FX_MODE_BLINK_RAINBOW, &mode_blink_rainbow, _data_FX_MODE_BLINK_RAINBOW); + addEffect(FX_MODE_ANDROID, &mode_android, _data_FX_MODE_ANDROID); + addEffect(FX_MODE_CHASE_COLOR, &mode_chase_color, _data_FX_MODE_CHASE_COLOR); + addEffect(FX_MODE_CHASE_RANDOM, &mode_chase_random, _data_FX_MODE_CHASE_RANDOM); + addEffect(FX_MODE_CHASE_RAINBOW, &mode_chase_rainbow, _data_FX_MODE_CHASE_RAINBOW); + addEffect(FX_MODE_CHASE_FLASH, &mode_chase_flash, _data_FX_MODE_CHASE_FLASH); + addEffect(FX_MODE_CHASE_FLASH_RANDOM, &mode_chase_flash_random, _data_FX_MODE_CHASE_FLASH_RANDOM); + addEffect(FX_MODE_CHASE_RAINBOW_WHITE, &mode_chase_rainbow_white, _data_FX_MODE_CHASE_RAINBOW_WHITE); + addEffect(FX_MODE_COLORFUL, &mode_colorful, _data_FX_MODE_COLORFUL); + addEffect(FX_MODE_TRAFFIC_LIGHT, &mode_traffic_light, _data_FX_MODE_TRAFFIC_LIGHT); + addEffect(FX_MODE_COLOR_SWEEP_RANDOM, &mode_color_sweep_random, _data_FX_MODE_COLOR_SWEEP_RANDOM); + addEffect(FX_MODE_RUNNING_COLOR, &mode_running_color, _data_FX_MODE_RUNNING_COLOR); + addEffect(FX_MODE_AURORA, &mode_aurora, _data_FX_MODE_AURORA); + addEffect(FX_MODE_RUNNING_RANDOM, &mode_running_random, _data_FX_MODE_RUNNING_RANDOM); + addEffect(FX_MODE_LARSON_SCANNER, &mode_larson_scanner, _data_FX_MODE_LARSON_SCANNER); + addEffect(FX_MODE_RAIN, &mode_rain, _data_FX_MODE_RAIN); + addEffect(FX_MODE_PRIDE_2015, &mode_pride_2015, _data_FX_MODE_PRIDE_2015); + addEffect(FX_MODE_COLORWAVES, &mode_colorwaves, _data_FX_MODE_COLORWAVES); + addEffect(FX_MODE_FIREWORKS, &mode_fireworks, _data_FX_MODE_FIREWORKS); + addEffect(FX_MODE_TETRIX, &mode_tetrix, _data_FX_MODE_TETRIX); + addEffect(FX_MODE_FIRE_FLICKER, &mode_fire_flicker, _data_FX_MODE_FIRE_FLICKER); + addEffect(FX_MODE_GRADIENT, &mode_gradient, _data_FX_MODE_GRADIENT); + addEffect(FX_MODE_LOADING, &mode_loading, _data_FX_MODE_LOADING); + addEffect(FX_MODE_FAIRY, &mode_fairy, _data_FX_MODE_FAIRY); + addEffect(FX_MODE_TWO_DOTS, &mode_two_dots, _data_FX_MODE_TWO_DOTS); + addEffect(FX_MODE_FAIRYTWINKLE, &mode_fairytwinkle, _data_FX_MODE_FAIRYTWINKLE); + addEffect(FX_MODE_RUNNING_DUAL, &mode_running_dual, _data_FX_MODE_RUNNING_DUAL); + #ifdef WLED_ENABLE_GIF + addEffect(FX_MODE_IMAGE, &mode_image, _data_FX_MODE_IMAGE); + #endif + addEffect(FX_MODE_TRICOLOR_CHASE, &mode_tricolor_chase, _data_FX_MODE_TRICOLOR_CHASE); + addEffect(FX_MODE_TRICOLOR_WIPE, &mode_tricolor_wipe, _data_FX_MODE_TRICOLOR_WIPE); + addEffect(FX_MODE_TRICOLOR_FADE, &mode_tricolor_fade, _data_FX_MODE_TRICOLOR_FADE); + addEffect(FX_MODE_LIGHTNING, &mode_lightning, _data_FX_MODE_LIGHTNING); + addEffect(FX_MODE_ICU, &mode_icu, _data_FX_MODE_ICU); + addEffect(FX_MODE_DUAL_LARSON_SCANNER, &mode_dual_larson_scanner, _data_FX_MODE_DUAL_LARSON_SCANNER); + addEffect(FX_MODE_RANDOM_CHASE, &mode_random_chase, _data_FX_MODE_RANDOM_CHASE); + addEffect(FX_MODE_OSCILLATE, &mode_oscillate, _data_FX_MODE_OSCILLATE); + addEffect(FX_MODE_JUGGLE, &mode_juggle, _data_FX_MODE_JUGGLE); + addEffect(FX_MODE_PALETTE, &mode_palette, _data_FX_MODE_PALETTE); + addEffect(FX_MODE_BPM, &mode_bpm, _data_FX_MODE_BPM); + addEffect(FX_MODE_FILLNOISE8, &mode_fillnoise8, _data_FX_MODE_FILLNOISE8); + addEffect(FX_MODE_NOISE16_1, &mode_noise16_1, _data_FX_MODE_NOISE16_1); + addEffect(FX_MODE_NOISE16_2, &mode_noise16_2, _data_FX_MODE_NOISE16_2); + addEffect(FX_MODE_NOISE16_3, &mode_noise16_3, _data_FX_MODE_NOISE16_3); + addEffect(FX_MODE_NOISE16_4, &mode_noise16_4, _data_FX_MODE_NOISE16_4); + addEffect(FX_MODE_COLORTWINKLE, &mode_colortwinkle, _data_FX_MODE_COLORTWINKLE); + addEffect(FX_MODE_LAKE, &mode_lake, _data_FX_MODE_LAKE); + addEffect(FX_MODE_METEOR, &mode_meteor, _data_FX_MODE_METEOR); + //addEffect(FX_MODE_METEOR_SMOOTH, &mode_meteor_smooth, _data_FX_MODE_METEOR_SMOOTH); // merged with mode_meteor + addEffect(FX_MODE_RAILWAY, &mode_railway, _data_FX_MODE_RAILWAY); + addEffect(FX_MODE_RIPPLE, &mode_ripple, _data_FX_MODE_RIPPLE); + addEffect(FX_MODE_TWINKLEFOX, &mode_twinklefox, _data_FX_MODE_TWINKLEFOX); + addEffect(FX_MODE_TWINKLECAT, &mode_twinklecat, _data_FX_MODE_TWINKLECAT); + addEffect(FX_MODE_HALLOWEEN_EYES, &mode_halloween_eyes, _data_FX_MODE_HALLOWEEN_EYES); + addEffect(FX_MODE_STATIC_PATTERN, &mode_static_pattern, _data_FX_MODE_STATIC_PATTERN); + addEffect(FX_MODE_TRI_STATIC_PATTERN, &mode_tri_static_pattern, _data_FX_MODE_TRI_STATIC_PATTERN); + addEffect(FX_MODE_SPOTS, &mode_spots, _data_FX_MODE_SPOTS); + addEffect(FX_MODE_SPOTS_FADE, &mode_spots_fade, _data_FX_MODE_SPOTS_FADE); + addEffect(FX_MODE_COMET, &mode_comet, _data_FX_MODE_COMET); + #ifdef WLED_PS_DONT_REPLACE_FX + addEffect(FX_MODE_MULTI_COMET, &mode_multi_comet, _data_FX_MODE_MULTI_COMET); + addEffect(FX_MODE_ROLLINGBALLS, &rolling_balls, _data_FX_MODE_ROLLINGBALLS); + addEffect(FX_MODE_SPARKLE, &mode_sparkle, _data_FX_MODE_SPARKLE); + addEffect(FX_MODE_GLITTER, &mode_glitter, _data_FX_MODE_GLITTER); + addEffect(FX_MODE_SOLID_GLITTER, &mode_solid_glitter, _data_FX_MODE_SOLID_GLITTER); + addEffect(FX_MODE_STARBURST, &mode_starburst, _data_FX_MODE_STARBURST); + addEffect(FX_MODE_DANCING_SHADOWS, &mode_dancing_shadows, _data_FX_MODE_DANCING_SHADOWS); + addEffect(FX_MODE_FIRE_2012, &mode_fire_2012, _data_FX_MODE_FIRE_2012); + addEffect(FX_MODE_EXPLODING_FIREWORKS, &mode_exploding_fireworks, _data_FX_MODE_EXPLODING_FIREWORKS); + #endif + addEffect(FX_MODE_CANDLE, &mode_candle, _data_FX_MODE_CANDLE); + addEffect(FX_MODE_BOUNCINGBALLS, &mode_bouncing_balls, _data_FX_MODE_BOUNCINGBALLS); + addEffect(FX_MODE_POPCORN, &mode_popcorn, _data_FX_MODE_POPCORN); + addEffect(FX_MODE_DRIP, &mode_drip, _data_FX_MODE_DRIP); + addEffect(FX_MODE_SINELON, &mode_sinelon, _data_FX_MODE_SINELON); + addEffect(FX_MODE_SINELON_DUAL, &mode_sinelon_dual, _data_FX_MODE_SINELON_DUAL); + addEffect(FX_MODE_SINELON_RAINBOW, &mode_sinelon_rainbow, _data_FX_MODE_SINELON_RAINBOW); + addEffect(FX_MODE_PLASMA, &mode_plasma, _data_FX_MODE_PLASMA); + addEffect(FX_MODE_PERCENT, &mode_percent, _data_FX_MODE_PERCENT); + addEffect(FX_MODE_RIPPLE_RAINBOW, &mode_ripple_rainbow, _data_FX_MODE_RIPPLE_RAINBOW); + addEffect(FX_MODE_HEARTBEAT, &mode_heartbeat, _data_FX_MODE_HEARTBEAT); + addEffect(FX_MODE_PACIFICA, &mode_pacifica, _data_FX_MODE_PACIFICA); + addEffect(FX_MODE_CANDLE_MULTI, &mode_candle_multi, _data_FX_MODE_CANDLE_MULTI); + addEffect(FX_MODE_SUNRISE, &mode_sunrise, _data_FX_MODE_SUNRISE); + addEffect(FX_MODE_PHASED, &mode_phased, _data_FX_MODE_PHASED); + addEffect(FX_MODE_TWINKLEUP, &mode_twinkleup, _data_FX_MODE_TWINKLEUP); + addEffect(FX_MODE_NOISEPAL, &mode_noisepal, _data_FX_MODE_NOISEPAL); + addEffect(FX_MODE_SINEWAVE, &mode_sinewave, _data_FX_MODE_SINEWAVE); + addEffect(FX_MODE_PHASEDNOISE, &mode_phased_noise, _data_FX_MODE_PHASEDNOISE); + addEffect(FX_MODE_FLOW, &mode_flow, _data_FX_MODE_FLOW); + addEffect(FX_MODE_CHUNCHUN, &mode_chunchun, _data_FX_MODE_CHUNCHUN); + addEffect(FX_MODE_WASHING_MACHINE, &mode_washing_machine, _data_FX_MODE_WASHING_MACHINE); + addEffect(FX_MODE_BLENDS, &mode_blends, _data_FX_MODE_BLENDS); + addEffect(FX_MODE_TV_SIMULATOR, &mode_tv_simulator, _data_FX_MODE_TV_SIMULATOR); + addEffect(FX_MODE_DYNAMIC_SMOOTH, &mode_dynamic_smooth, _data_FX_MODE_DYNAMIC_SMOOTH); + + // --- 1D audio effects --- + addEffect(FX_MODE_PIXELS, &mode_pixels, _data_FX_MODE_PIXELS); + addEffect(FX_MODE_PIXELWAVE, &mode_pixelwave, _data_FX_MODE_PIXELWAVE); + addEffect(FX_MODE_JUGGLES, &mode_juggles, _data_FX_MODE_JUGGLES); + addEffect(FX_MODE_MATRIPIX, &mode_matripix, _data_FX_MODE_MATRIPIX); + addEffect(FX_MODE_GRAVIMETER, &mode_gravimeter, _data_FX_MODE_GRAVIMETER); + addEffect(FX_MODE_PLASMOID, &mode_plasmoid, _data_FX_MODE_PLASMOID); + addEffect(FX_MODE_PUDDLES, &mode_puddles, _data_FX_MODE_PUDDLES); + addEffect(FX_MODE_MIDNOISE, &mode_midnoise, _data_FX_MODE_MIDNOISE); + addEffect(FX_MODE_NOISEMETER, &mode_noisemeter, _data_FX_MODE_NOISEMETER); + addEffect(FX_MODE_FREQWAVE, &mode_freqwave, _data_FX_MODE_FREQWAVE); + addEffect(FX_MODE_FREQMATRIX, &mode_freqmatrix, _data_FX_MODE_FREQMATRIX); + addEffect(FX_MODE_WATERFALL, &mode_waterfall, _data_FX_MODE_WATERFALL); + addEffect(FX_MODE_FREQPIXELS, &mode_freqpixels, _data_FX_MODE_FREQPIXELS); + addEffect(FX_MODE_NOISEFIRE, &mode_noisefire, _data_FX_MODE_NOISEFIRE); + addEffect(FX_MODE_PUDDLEPEAK, &mode_puddlepeak, _data_FX_MODE_PUDDLEPEAK); + addEffect(FX_MODE_NOISEMOVE, &mode_noisemove, _data_FX_MODE_NOISEMOVE); + addEffect(FX_MODE_PERLINMOVE, &mode_perlinmove, _data_FX_MODE_PERLINMOVE); + addEffect(FX_MODE_RIPPLEPEAK, &mode_ripplepeak, _data_FX_MODE_RIPPLEPEAK); + addEffect(FX_MODE_FREQMAP, &mode_freqmap, _data_FX_MODE_FREQMAP); + addEffect(FX_MODE_GRAVCENTER, &mode_gravcenter, _data_FX_MODE_GRAVCENTER); + addEffect(FX_MODE_GRAVCENTRIC, &mode_gravcentric, _data_FX_MODE_GRAVCENTRIC); + addEffect(FX_MODE_GRAVFREQ, &mode_gravfreq, _data_FX_MODE_GRAVFREQ); + addEffect(FX_MODE_DJLIGHT, &mode_DJLight, _data_FX_MODE_DJLIGHT); + addEffect(FX_MODE_BLURZ, &mode_blurz, _data_FX_MODE_BLURZ); + addEffect(FX_MODE_FLOWSTRIPE, &mode_FlowStripe, _data_FX_MODE_FLOWSTRIPE); + addEffect(FX_MODE_WAVESINS, &mode_wavesins, _data_FX_MODE_WAVESINS); + addEffect(FX_MODE_ROCKTAVES, &mode_rocktaves, _data_FX_MODE_ROCKTAVES); + + // --- 2D effects --- +#ifndef WLED_DISABLE_2D + addEffect(FX_MODE_2DPLASMAROTOZOOM, &mode_2Dplasmarotozoom, _data_FX_MODE_2DPLASMAROTOZOOM); + addEffect(FX_MODE_2DSPACESHIPS, &mode_2Dspaceships, _data_FX_MODE_2DSPACESHIPS); + addEffect(FX_MODE_2DCRAZYBEES, &mode_2Dcrazybees, _data_FX_MODE_2DCRAZYBEES); + + #ifdef WLED_PS_DONT_REPLACE_FX addEffect(FX_MODE_2DGHOSTRIDER, &mode_2Dghostrider, _data_FX_MODE_2DGHOSTRIDER); addEffect(FX_MODE_2DBLOBS, &mode_2Dfloatingblobs, _data_FX_MODE_2DBLOBS); + #endif + addEffect(FX_MODE_2DSCROLLTEXT, &mode_2Dscrollingtext, _data_FX_MODE_2DSCROLLTEXT); addEffect(FX_MODE_2DDRIFTROSE, &mode_2Ddriftrose, _data_FX_MODE_2DDRIFTROSE); addEffect(FX_MODE_2DDISTORTIONWAVES, &mode_2Ddistortionwaves, _data_FX_MODE_2DDISTORTIONWAVES); - addEffect(FX_MODE_2DGEQ, &mode_2DGEQ, _data_FX_MODE_2DGEQ); // audio - addEffect(FX_MODE_2DNOISE, &mode_2Dnoise, _data_FX_MODE_2DNOISE); - addEffect(FX_MODE_2DFIRENOISE, &mode_2Dfirenoise, _data_FX_MODE_2DFIRENOISE); addEffect(FX_MODE_2DSQUAREDSWIRL, &mode_2Dsquaredswirl, _data_FX_MODE_2DSQUAREDSWIRL); @@ -7859,15 +10343,12 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DMATRIX, &mode_2Dmatrix, _data_FX_MODE_2DMATRIX); addEffect(FX_MODE_2DMETABALLS, &mode_2Dmetaballs, _data_FX_MODE_2DMETABALLS); addEffect(FX_MODE_2DFUNKYPLANK, &mode_2DFunkyPlank, _data_FX_MODE_2DFUNKYPLANK); // audio - addEffect(FX_MODE_2DPULSER, &mode_2DPulser, _data_FX_MODE_2DPULSER); - addEffect(FX_MODE_2DDRIFT, &mode_2DDrift, _data_FX_MODE_2DDRIFT); addEffect(FX_MODE_2DWAVERLY, &mode_2DWaverly, _data_FX_MODE_2DWAVERLY); // audio addEffect(FX_MODE_2DSUNRADIATION, &mode_2DSunradiation, _data_FX_MODE_2DSUNRADIATION); addEffect(FX_MODE_2DCOLOREDBURSTS, &mode_2DColoredBursts, _data_FX_MODE_2DCOLOREDBURSTS); addEffect(FX_MODE_2DJULIA, &mode_2DJulia, _data_FX_MODE_2DJULIA); - addEffect(FX_MODE_2DGAMEOFLIFE, &mode_2Dgameoflife, _data_FX_MODE_2DGAMEOFLIFE); addEffect(FX_MODE_2DTARTAN, &mode_2Dtartan, _data_FX_MODE_2DTARTAN); addEffect(FX_MODE_2DPOLARLIGHTS, &mode_2DPolarLights, _data_FX_MODE_2DPOLARLIGHTS); @@ -7875,7 +10356,6 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DLISSAJOUS, &mode_2DLissajous, _data_FX_MODE_2DLISSAJOUS); addEffect(FX_MODE_2DFRIZZLES, &mode_2DFrizzles, _data_FX_MODE_2DFRIZZLES); addEffect(FX_MODE_2DPLASMABALL, &mode_2DPlasmaball, _data_FX_MODE_2DPLASMABALL); - addEffect(FX_MODE_2DHIPHOTIC, &mode_2DHiphotic, _data_FX_MODE_2DHIPHOTIC); addEffect(FX_MODE_2DSINDOTS, &mode_2DSindots, _data_FX_MODE_2DSINDOTS); addEffect(FX_MODE_2DDNASPIRAL, &mode_2DDNASpiral, _data_FX_MODE_2DDNASPIRAL); @@ -7883,8 +10363,41 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DSOAP, &mode_2Dsoap, _data_FX_MODE_2DSOAP); addEffect(FX_MODE_2DOCTOPUS, &mode_2Doctopus, _data_FX_MODE_2DOCTOPUS); addEffect(FX_MODE_2DWAVINGCELL, &mode_2Dwavingcell, _data_FX_MODE_2DWAVINGCELL); - addEffect(FX_MODE_2DAKEMI, &mode_2DAkemi, _data_FX_MODE_2DAKEMI); // audio + +#ifndef WLED_DISABLE_PARTICLESYSTEM2D + addEffect(FX_MODE_PARTICLEVOLCANO, &mode_particlevolcano, _data_FX_MODE_PARTICLEVOLCANO); + addEffect(FX_MODE_PARTICLEFIRE, &mode_particlefire, _data_FX_MODE_PARTICLEFIRE); + addEffect(FX_MODE_PARTICLEFIREWORKS, &mode_particlefireworks, _data_FX_MODE_PARTICLEFIREWORKS); + addEffect(FX_MODE_PARTICLEVORTEX, &mode_particlevortex, _data_FX_MODE_PARTICLEVORTEX); + addEffect(FX_MODE_PARTICLEPERLIN, &mode_particleperlin, _data_FX_MODE_PARTICLEPERLIN); + addEffect(FX_MODE_PARTICLEPIT, &mode_particlepit, _data_FX_MODE_PARTICLEPIT); + addEffect(FX_MODE_PARTICLEBOX, &mode_particlebox, _data_FX_MODE_PARTICLEBOX); + addEffect(FX_MODE_PARTICLEATTRACTOR, &mode_particleattractor, _data_FX_MODE_PARTICLEATTRACTOR); // 872 bytes + addEffect(FX_MODE_PARTICLEIMPACT, &mode_particleimpact, _data_FX_MODE_PARTICLEIMPACT); + addEffect(FX_MODE_PARTICLEWATERFALL, &mode_particlewaterfall, _data_FX_MODE_PARTICLEWATERFALL); + addEffect(FX_MODE_PARTICLESPRAY, &mode_particlespray, _data_FX_MODE_PARTICLESPRAY); + addEffect(FX_MODE_PARTICLESGEQ, &mode_particleGEQ, _data_FX_MODE_PARTICLEGEQ); + addEffect(FX_MODE_PARTICLECENTERGEQ, &mode_particlecenterGEQ, _data_FX_MODE_PARTICLECIRCULARGEQ); + addEffect(FX_MODE_PARTICLEGHOSTRIDER, &mode_particleghostrider, _data_FX_MODE_PARTICLEGHOSTRIDER); + addEffect(FX_MODE_PARTICLEBLOBS, &mode_particleblobs, _data_FX_MODE_PARTICLEBLOBS); +#endif // WLED_DISABLE_PARTICLESYSTEM2D #endif // WLED_DISABLE_2D +#ifndef WLED_DISABLE_PARTICLESYSTEM1D +addEffect(FX_MODE_PSDRIP, &mode_particleDrip, _data_FX_MODE_PARTICLEDRIP); +addEffect(FX_MODE_PSPINBALL, &mode_particlePinball, _data_FX_MODE_PSPINBALL); //potential replacement for: bouncing balls, rollingballs, popcorn +addEffect(FX_MODE_PSDANCINGSHADOWS, &mode_particleDancingShadows, _data_FX_MODE_PARTICLEDANCINGSHADOWS); +addEffect(FX_MODE_PSFIREWORKS1D, &mode_particleFireworks1D, _data_FX_MODE_PS_FIREWORKS1D); +addEffect(FX_MODE_PSSPARKLER, &mode_particleSparkler, _data_FX_MODE_PS_SPARKLER); +addEffect(FX_MODE_PSHOURGLASS, &mode_particleHourglass, _data_FX_MODE_PS_HOURGLASS); +addEffect(FX_MODE_PS1DSPRAY, &mode_particle1Dspray, _data_FX_MODE_PS_1DSPRAY); +addEffect(FX_MODE_PSBALANCE, &mode_particleBalance, _data_FX_MODE_PS_BALANCE); +addEffect(FX_MODE_PSCHASE, &mode_particleChase, _data_FX_MODE_PS_CHASE); +addEffect(FX_MODE_PSSTARBURST, &mode_particleStarburst, _data_FX_MODE_PS_STARBURST); +addEffect(FX_MODE_PS1DGEQ, &mode_particle1DGEQ, _data_FX_MODE_PS_1D_GEQ); +addEffect(FX_MODE_PSFIRE1D, &mode_particleFire1D, _data_FX_MODE_PS_FIRE1D); +addEffect(FX_MODE_PS1DSONICSTREAM, &mode_particle1Dsonicstream, _data_FX_MODE_PS_SONICSTREAM); +#endif // WLED_DISABLE_PARTICLESYSTEM1D + } diff --git a/wled00/FX.h b/wled00/FX.h index c877c17f22..5954046907 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -184,7 +184,7 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_TWO_DOTS 50 #define FX_MODE_FAIRYTWINKLE 51 //was Two Areas prior to 0.13.0-b6 (use "Two Dots" with full intensity) #define FX_MODE_RUNNING_DUAL 52 -// #define FX_MODE_HALLOWEEN 53 // removed in 0.14! +#define FX_MODE_IMAGE 53 #define FX_MODE_TRICOLOR_CHASE 54 #define FX_MODE_TRICOLOR_WIPE 55 #define FX_MODE_TRICOLOR_FADE 56 @@ -322,8 +322,35 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_WAVESINS 184 #define FX_MODE_ROCKTAVES 185 #define FX_MODE_2DAKEMI 186 - -#define MODE_COUNT 187 +#define FX_MODE_PARTICLEVOLCANO 187 +#define FX_MODE_PARTICLEFIRE 188 +#define FX_MODE_PARTICLEFIREWORKS 189 +#define FX_MODE_PARTICLEVORTEX 190 +#define FX_MODE_PARTICLEPERLIN 191 +#define FX_MODE_PARTICLEPIT 192 +#define FX_MODE_PARTICLEBOX 193 +#define FX_MODE_PARTICLEATTRACTOR 194 +#define FX_MODE_PARTICLEIMPACT 195 +#define FX_MODE_PARTICLEWATERFALL 196 +#define FX_MODE_PARTICLESPRAY 197 +#define FX_MODE_PARTICLESGEQ 198 +#define FX_MODE_PARTICLECENTERGEQ 199 +#define FX_MODE_PARTICLEGHOSTRIDER 200 +#define FX_MODE_PARTICLEBLOBS 201 +#define FX_MODE_PSDRIP 202 +#define FX_MODE_PSPINBALL 203 +#define FX_MODE_PSDANCINGSHADOWS 204 +#define FX_MODE_PSFIREWORKS1D 205 +#define FX_MODE_PSSPARKLER 206 +#define FX_MODE_PSHOURGLASS 207 +#define FX_MODE_PS1DSPRAY 208 +#define FX_MODE_PSBALANCE 209 +#define FX_MODE_PSCHASE 210 +#define FX_MODE_PSSTARBURST 211 +#define FX_MODE_PS1DGEQ 212 +#define FX_MODE_PSFIRE1D 213 +#define FX_MODE_PS1DSONICSTREAM 214 +#define MODE_COUNT 215 #define BLEND_STYLE_FADE 0x00 // universal @@ -480,6 +507,7 @@ typedef struct Segment { uint8_t _prevPaletteBlends; // number of previous palette blends (there are max 255 blends possible) unsigned long _start; // must accommodate millis() uint16_t _dur; + // -> here is one byte of padding Transition(uint16_t dur=750) : _palT(CRGBPalette16(CRGB::Black)) , _prevPaletteBlends(0) @@ -576,6 +604,7 @@ typedef struct Segment { inline static void addUsedSegmentData(int len) { Segment::_usedSegmentData += len; } #ifndef WLED_DISABLE_MODE_BLEND inline static void modeBlend(bool blend) { _modeBlend = blend; } + inline static bool getmodeBlend(void) { return _modeBlend; } #endif inline static unsigned vLength() { return Segment::_vLength; } inline static unsigned vWidth() { return Segment::_vWidth; } @@ -623,6 +652,7 @@ typedef struct Segment { uint8_t currentMode() const; // currently active effect/mode (while in transition) [[gnu::hot]] uint32_t currentColor(uint8_t slot) const; // currently active segment color (blended while in transition) CRGBPalette16 &loadPalette(CRGBPalette16 &tgt, uint8_t pal); + void loadOldPalette(); // loads old FX palette into _currentPalette // 1D strip [[gnu::hot]] uint16_t virtualLength() const; @@ -1007,4 +1037,4 @@ class WS2812FX { // 96 bytes extern const char JSON_mode_names[]; extern const char JSON_palette_names[]; -#endif +#endif \ No newline at end of file diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 17af4e24ba..05c8076c79 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -11,6 +11,7 @@ */ #include "wled.h" #include "FX.h" +#include "FXparticleSystem.h" // TODO: better define the required function (mem service) in FX.h? #include "palettes.h" /* @@ -199,6 +200,9 @@ void Segment::resetIfRequired() { if (data && _dataLen > 0) memset(data, 0, _dataLen); // prevent heap fragmentation (just erase buffer instead of deallocateData()) next_time = 0; step = 0; call = 0; aux0 = 0; aux1 = 0; reset = false; + #ifdef WLED_ENABLE_GIF + endImagePlayback(this); + #endif } CRGBPalette16 &Segment::loadPalette(CRGBPalette16 &targetPalette, uint8_t pal) { @@ -467,6 +471,12 @@ void Segment::beginDraw() { } } +// loads palette of the old FX during transitions (used by particle system) +void Segment::loadOldPalette(void) { + if(isInTransition()) + loadPalette(_currentPalette, _t->_palTid); +} + // relies on WS2812FX::service() to call it for each frame void Segment::handleRandomPalette() { // is it time to generate a new palette? @@ -1543,6 +1553,9 @@ void WS2812FX::service() { _segment_index++; } Segment::setClippingRect(0, 0); // disable clipping for overlays + #if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) + servicePSmem(); // handle segment particle system memory + #endif _isServicing = false; _triggered = false; @@ -1987,4 +2000,4 @@ const char JSON_palette_names[] PROGMEM = R"=====([ "Aurora","Atlantica","C9 2","C9 New","Temperature","Aurora 2","Retro Clown","Candy","Toxy Reaf","Fairy Reaf", "Semi Blue","Pink Candy","Red Reaf","Aqua Flash","Yelblu Hot","Lite Light","Red Flash","Blink Red","Red Shift","Red Tide", "Candy2","Traffic Light" -])====="; +])====="; \ No newline at end of file diff --git a/wled00/FXparticleSystem.cpp b/wled00/FXparticleSystem.cpp new file mode 100644 index 0000000000..910263f1d3 --- /dev/null +++ b/wled00/FXparticleSystem.cpp @@ -0,0 +1,2373 @@ +/* + FXparticleSystem.cpp + + Particle system with functions for particle generation, particle movement and particle rendering to RGB matrix. + by DedeHai (Damian Schneider) 2013-2024 + + Copyright (c) 2024 Damian Schneider + Licensed under the EUPL v. 1.2 or later +*/ + +#ifdef WLED_DISABLE_2D +#define WLED_DISABLE_PARTICLESYSTEM2D +#endif + +#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled +#include "FXparticleSystem.h" + +// local shared functions (used both in 1D and 2D system) +static int32_t calcForce_dv(const int8_t force, uint8_t &counter); +static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, const bool wrap); // returns false if out of bounds by more than particleradius +static void fast_color_add(CRGB &c1, const CRGB &c2, uint32_t scale = 255); // fast and accurate color adding with scaling (scales c2 before adding) +static void fast_color_scale(CRGB &c, const uint32_t scale); // fast scaling function using 32bit variable and pointer. note: keep 'scale' within 0-255 +//static CRGB *allocateCRGBbuffer(uint32_t length); + +// global variables for memory management +std::vector partMemList; // list of particle memory pointers +partMem *pmem = nullptr; // pointer to particle memory of current segment, updated in particleMemoryManager() +CRGB *framebuffer = nullptr; // local frame buffer for rendering +CRGB *renderbuffer = nullptr; // local particle render buffer for advanced particles +uint16_t frameBufferSize = 0; // size in pixels, used to check if framebuffer is large enough for current segment +uint16_t renderBufferSize = 0; // size in pixels, if allcoated by a 1D system it needs to be updated for 2D +bool renderSolo = false; // is set to true if this is the only particle system using the so it can use the buffer continuously (faster blurring) +int32_t globalBlur = 0; // motion blur to apply if multiple PS are using the buffer +int32_t globalSmear = 0; // smear-blur to apply if multiple PS are using the buffer +#endif + +#ifndef WLED_DISABLE_PARTICLESYSTEM2D +ParticleSystem2D::ParticleSystem2D(uint32_t width, uint32_t height, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced, bool sizecontrol) { + PSPRINTLN("\n ParticleSystem2D constructor"); + effectID = SEGMENT.mode; // new FX called init, save the effect ID + numSources = numberofsources; // number of sources allocated in init + numParticles = numberofparticles; // number of particles allocated in init + availableParticles = 0; // let the memory manager assign + fractionOfParticlesUsed = 255; // use all particles by default, usedParticles is updated in updatePSpointers() + advPartProps = nullptr; //make sure we start out with null pointers (just in case memory was not cleared) + advPartSize = nullptr; + updatePSpointers(isadvanced, sizecontrol); // set the particle and sources pointer (call this before accessing sprays or particles) + setMatrixSize(width, height); + setWallHardness(255); // set default wall hardness to max + setWallRoughness(0); // smooth walls by default + setGravity(0); //gravity disabled by default + setParticleSize(1); // 2x2 rendering size by default + motionBlur = 0; //no fading by default + smearBlur = 0; //no smearing by default + emitIndex = 0; + collisionStartIdx = 0; + + //initialize some default non-zero values most FX use + for (uint32_t i = 0; i < numSources; i++) { + sources[i].source.sat = 255; //set saturation to max by default + sources[i].source.ttl = 1; //set source alive + } + +} + +// update function applies gravity, moves the particles, handles collisions and renders the particles +void ParticleSystem2D::update(void) { + //apply gravity globally if enabled + if (particlesettings.useGravity) + applyGravity(); + + //update size settings before handling collisions + if (advPartSize) { + for (uint32_t i = 0; i < usedParticles; i++) { + if(updateSize(&advPartProps[i], &advPartSize[i]) == false) { // if particle shrinks to 0 size + particles[i].ttl = 0; // kill particle + } + } + } + + // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) + if (particlesettings.useCollisions) + handleCollisions(); + + //move all particles + for (uint32_t i = 0; i < usedParticles; i++) { + particleMoveUpdate(particles[i], particleFlags[i], nullptr, advPartProps ? &advPartProps[i] : nullptr); // note: splitting this into two loops is slower and uses more flash + } + + ParticleSys_render(); +} + +// update function for fire animation +void ParticleSystem2D::updateFire(const uint8_t intensity,const bool renderonly) { + if (!renderonly) + fireParticleupdate(); + fireIntesity = intensity > 0 ? intensity : 1; // minimum of 1, zero checking is used in render function + ParticleSys_render(); +} + +// set percentage of used particles as uint8_t i.e 127 means 50% for example +void ParticleSystem2D::setUsedParticles(uint8_t percentage) { + fractionOfParticlesUsed = percentage; // note usedParticles is updated in memory manager + updateUsedParticles(numParticles, availableParticles, fractionOfParticlesUsed, usedParticles); + PSPRINT(" SetUsedpaticles: allocated particles: "); + PSPRINT(numParticles); + PSPRINT(" available particles: "); + PSPRINT(availableParticles); + PSPRINT(" ,used percentage: "); + PSPRINT(fractionOfParticlesUsed); + PSPRINT(" ,used particles: "); + PSPRINTLN(usedParticles); +} + +void ParticleSystem2D::setWallHardness(uint8_t hardness) { + wallHardness = hardness; +} + +void ParticleSystem2D::setWallRoughness(uint8_t roughness) { + wallRoughness = roughness; +} + +void ParticleSystem2D::setCollisionHardness(uint8_t hardness) { + collisionHardness = (int)hardness + 1; +} + +void ParticleSystem2D::setMatrixSize(uint32_t x, uint32_t y) { + maxXpixel = x - 1; // last physical pixel that can be drawn to + maxYpixel = y - 1; + maxX = x * PS_P_RADIUS - 1; // particle system boundary for movements + maxY = y * PS_P_RADIUS - 1; // this value is often needed (also by FX) to calculate positions +} + +void ParticleSystem2D::setWrapX(bool enable) { + particlesettings.wrapX = enable; +} + +void ParticleSystem2D::setWrapY(bool enable) { + particlesettings.wrapY = enable; +} + +void ParticleSystem2D::setBounceX(bool enable) { + particlesettings.bounceX = enable; +} + +void ParticleSystem2D::setBounceY(bool enable) { + particlesettings.bounceY = enable; +} + +void ParticleSystem2D::setKillOutOfBounds(bool enable) { + particlesettings.killoutofbounds = enable; +} + +void ParticleSystem2D::setColorByAge(bool enable) { + particlesettings.colorByAge = enable; +} + +void ParticleSystem2D::setMotionBlur(uint8_t bluramount) { + if (particlesize < 2) // only allow motion blurring on default particle sizes or advanced size (cannot combine motion blur with normal blurring used for particlesize, would require another buffer) + motionBlur = bluramount; +} + +void ParticleSystem2D::setSmearBlur(uint8_t bluramount) { + smearBlur = bluramount; +} + + +// render size using smearing (see blur function) +void ParticleSystem2D::setParticleSize(uint8_t size) { + particlesize = size; + particleHardRadius = PS_P_MINHARDRADIUS; // ~1 pixel + if(particlesize > 1) { + particleHardRadius = max(particleHardRadius, (uint32_t)particlesize); // radius used for wall collisions & particle collisions + motionBlur = 0; // disable motion blur if particle size is set + } + else if (particlesize == 0) + particleHardRadius = particleHardRadius >> 1; // single pixel particles have half the radius (i.e. 1/2 pixel) +} + +// enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable +// if enabled, gravity is applied to all particles in ParticleSystemUpdate() +// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results) +void ParticleSystem2D::setGravity(int8_t force) { + if (force) { + gforce = force; + particlesettings.useGravity = true; + } else { + particlesettings.useGravity = false; + } +} + +void ParticleSystem2D::enableParticleCollisions(bool enable, uint8_t hardness) { // enable/disable gravity, optionally, set the force (force=8 is default) can be 1-255, 0 is also disable + particlesettings.useCollisions = enable; + collisionHardness = (int)hardness + 1; +} + +// emit one particle with variation, returns index of emitted particle (or -1 if no particle emitted) +int32_t ParticleSystem2D::sprayEmit(const PSsource &emitter) { + bool success = false; + for (uint32_t i = 0; i < usedParticles; i++) { + emitIndex++; + if (emitIndex >= usedParticles) + emitIndex = 0; + if (particles[emitIndex].ttl == 0) { // find a dead particle + success = true; + particles[emitIndex].vx = emitter.vx + hw_random16(emitter.var << 1) - emitter.var; // random(-var, var) + particles[emitIndex].vy = emitter.vy + hw_random16(emitter.var << 1) - emitter.var; // random(-var, var) + particles[emitIndex].x = emitter.source.x; + particles[emitIndex].y = emitter.source.y; + particles[emitIndex].hue = emitter.source.hue; + particles[emitIndex].sat = emitter.source.sat; + particleFlags[emitIndex].collide = emitter.sourceFlags.collide; + particles[emitIndex].ttl = hw_random16(emitter.minLife, emitter.maxLife); + if (advPartProps) + advPartProps[emitIndex].size = emitter.size; + break; + } + } + if (success) + return emitIndex; + else + return -1; +} + +// Spray emitter for particles used for flames (particle TTL depends on source TTL) +void ParticleSystem2D::flameEmit(const PSsource &emitter) { + int emitIndex = sprayEmit(emitter); + if(emitIndex > 0) particles[emitIndex].ttl += emitter.source.ttl; +} + +// Emits a particle at given angle and speed, angle is from 0-65535 (=0-360deg), speed is also affected by emitter->var +// angle = 0 means in positive x-direction (i.e. to the right) +int32_t ParticleSystem2D::angleEmit(PSsource &emitter, const uint16_t angle, const int32_t speed) { + emitter.vx = ((int32_t)cos16_t(angle) * speed) / (int32_t)32600; // cos16_t() and sin16_t() return signed 16bit, division should be 32767 but 32600 gives slightly better rounding + emitter.vy = ((int32_t)sin16_t(angle) * speed) / (int32_t)32600; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + return sprayEmit(emitter); +} + +// particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 +// uses passed settings to set bounce or wrap, if useGravity is enabled, it will never bounce at the top and killoutofbounds is not applied over the top +void ParticleSystem2D::particleMoveUpdate(PSparticle &part, PSparticleFlags &partFlags, PSsettings2D *options, PSadvancedParticle *advancedproperties) { + if (options == nullptr) + options = &particlesettings; //use PS system settings by default + + if (part.ttl > 0) { + if (!partFlags.perpetual) + part.ttl--; // age + if (options->colorByAge) + part.hue = min(part.ttl, (uint16_t)255); //set color to ttl + + int32_t renderradius = PS_P_HALFRADIUS; // used to check out of bounds + int32_t newX = part.x + (int32_t)part.vx; + int32_t newY = part.y + (int32_t)part.vy; + partFlags.outofbounds = false; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) note: moving this to checks below adds code and is not faster + + if (advancedproperties) { //using individual particle size? + setParticleSize(particlesize); // updates default particleHardRadius + if (advancedproperties->size > PS_P_MINHARDRADIUS) { + particleHardRadius += (advancedproperties->size - PS_P_MINHARDRADIUS); // update radius + renderradius = particleHardRadius; + } + } + // note: if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle does not go half out of view + if (options->bounceY) { + if ((newY < (int32_t)particleHardRadius) || ((newY > (int32_t)(maxY - particleHardRadius)) && !options->useGravity)) { // reached floor / ceiling + bounce(part.vy, part.vx, newY, maxY); + } + } + + if(!checkBoundsAndWrap(newY, maxY, renderradius, options->wrapY)) { // check out of bounds note: this must not be skipped. if gravity is enabled, particles will never bounce at the top + partFlags.outofbounds = true; + if (options->killoutofbounds) { + if (newY < 0) // if gravity is enabled, only kill particles below ground + part.ttl = 0; + else if (!options->useGravity) + part.ttl = 0; + } + } + + if(part.ttl) { //check x direction only if still alive + if (options->bounceX) { + if ((newX < (int32_t)particleHardRadius) || (newX > (int32_t)(maxX - particleHardRadius))) // reached a wall + bounce(part.vx, part.vy, newX, maxX); + } + else if(!checkBoundsAndWrap(newX, maxX, renderradius, options->wrapX)) { // check out of bounds + partFlags.outofbounds = true; + if (options->killoutofbounds) + part.ttl = 0; + } + } + + part.x = (int16_t)newX; // set new position + part.y = (int16_t)newY; // set new position + } +} + +// move function for fire particles +void ParticleSystem2D::fireParticleupdate() { + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[i].ttl > 0) + { + particles[i].ttl--; // age + int32_t newY = particles[i].y + (int32_t)particles[i].vy + (particles[i].ttl >> 2); // younger particles move faster upward as they are hotter + int32_t newX = particles[i].x + (int32_t)particles[i].vx; + particleFlags[i].outofbounds = 0; // reset out of bounds flag note: moving this to checks below is not faster but adds code + // check if particle is out of bounds, wrap x around to other side if wrapping is enabled + // as fire particles start below the frame, lots of particles are out of bounds in y direction. to improve speed, only check x direction if y is not out of bounds + if (newY < -PS_P_HALFRADIUS) + particleFlags[i].outofbounds = 1; + else if (newY > int32_t(maxY + PS_P_HALFRADIUS)) // particle moved out at the top + particles[i].ttl = 0; + else // particle is in frame in y direction, also check x direction now Note: using checkBoundsAndWrap() is slower, only saves a few bytes + { + if ((newX < 0) || (newX > (int32_t)maxX)) { // handle out of bounds & wrap + if (particlesettings.wrapX) { + newX = newX % (maxX + 1); + if (newX < 0) // handle negative modulo + newX += maxX + 1; + } + else if ((newX < -PS_P_HALFRADIUS) || (newX > int32_t(maxX + PS_P_HALFRADIUS))) { //if fully out of view + particles[i].ttl = 0; + } + } + particles[i].x = newX; + } + particles[i].y = newY; + } + } +} + +// update advanced particle size control, returns false if particle shrinks to 0 size +bool ParticleSystem2D::updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize) { + if (advsize == nullptr) // safety check + return false; + // grow/shrink particle + int32_t newsize = advprops->size; + uint32_t counter = advsize->sizecounter; + uint32_t increment = 0; + // calculate grow speed using 0-8 for low speeds and 9-15 for higher speeds + if (advsize->grow) increment = advsize->growspeed; + else if (advsize->shrink) increment = advsize->shrinkspeed; + if (increment < 9) { // 8 means +1 every frame + counter += increment; + if (counter > 7) { + counter -= 8; + increment = 1; + } else + increment = 0; + advsize->sizecounter = counter; + } else { + increment = (increment - 8) << 1; // 9 means +2, 10 means +4 etc. 15 means +14 + } + + if (advsize->grow) { + if (newsize < advsize->maxsize) { + newsize += increment; + if (newsize >= advsize->maxsize) { + advsize->grow = false; // stop growing, shrink from now on if enabled + newsize = advsize->maxsize; // limit + if (advsize->pulsate) advsize->shrink = true; + } + } + } else if (advsize->shrink) { + if (newsize > advsize->minsize) { + newsize -= increment; + if (newsize <= advsize->minsize) { + if (advsize->minsize == 0) + return false; // particle shrunk to zero + advsize->shrink = false; // disable shrinking + newsize = advsize->minsize; // limit + if (advsize->pulsate) advsize->grow = true; + } + } + } + advprops->size = newsize; + // handle wobbling + if (advsize->wobble) { + advsize->asymdir += advsize->wobblespeed; // note: if need better wobblespeed control a counter is already in the struct + } + return true; +} + +// calculate x and y size for asymmetrical particles (advanced size control) +void ParticleSystem2D::getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize) { + if (advsize == nullptr) // if advsize is valid, also advanced properties pointer is valid (handled by updatePSpointers()) + return; + int32_t size = advprops->size; + int32_t asymdir = advsize->asymdir; + int32_t deviation = ((uint32_t)size * (uint32_t)advsize->asymmetry) / 255; // deviation from symmetrical size + // Calculate x and y size based on deviation and direction (0 is symmetrical, 64 is x, 128 is symmetrical, 192 is y) + if (asymdir < 64) { + deviation = (asymdir * deviation) / 64; + } else if (asymdir < 192) { + deviation = ((128 - asymdir) * deviation) / 64; + } else { + deviation = ((asymdir - 255) * deviation) / 64; + } + // Calculate x and y size based on deviation, limit to 255 (rendering function cannot handle larger sizes) + xsize = min((size - deviation), (int32_t)255); + ysize = min((size + deviation), (int32_t)255);; +} + +// function to bounce a particle from a wall using set parameters (wallHardness and wallRoughness) +void ParticleSystem2D::bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition) { + incomingspeed = -incomingspeed; + incomingspeed = (incomingspeed * wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + if (position < (int32_t)particleHardRadius) + position = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better + else + position = maxposition - particleHardRadius; + if (wallRoughness) { + int32_t incomingspeed_abs = abs((int32_t)incomingspeed); + int32_t totalspeed = incomingspeed_abs + abs((int32_t)parallelspeed); + // transfer an amount of incomingspeed speed to parallel speed + int32_t donatespeed = ((hw_random16(incomingspeed_abs << 1) - incomingspeed_abs) * (int32_t)wallRoughness) / (int32_t)255; // take random portion of + or - perpendicular speed, scaled by roughness + parallelspeed = limitSpeed((int32_t)parallelspeed + donatespeed); + // give the remainder of the speed to perpendicular speed + donatespeed = int8_t(totalspeed - abs(parallelspeed)); // keep total speed the same + incomingspeed = incomingspeed > 0 ? donatespeed : -donatespeed; + } +} + +// apply a force in x,y direction to individual particle +// caller needs to provide a 8bit counter (for each particle) that holds its value between calls +// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results) +void ParticleSystem2D::applyForce(PSparticle &part, const int8_t xforce, const int8_t yforce, uint8_t &counter) { + // for small forces, need to use a delay counter + uint8_t xcounter = counter & 0x0F; // lower four bits + uint8_t ycounter = counter >> 4; // upper four bits + + // velocity increase + int32_t dvx = calcForce_dv(xforce, xcounter); + int32_t dvy = calcForce_dv(yforce, ycounter); + + // save counter values back + counter = xcounter & 0x0F; // write lower four bits, make sure not to write more than 4 bits + counter |= (ycounter << 4) & 0xF0; // write upper four bits + + // apply the force to particle + part.vx = limitSpeed((int32_t)part.vx + dvx); + part.vy = limitSpeed((int32_t)part.vy + dvy); +} + +// apply a force in x,y direction to individual particle using advanced particle properties +void ParticleSystem2D::applyForce(const uint32_t particleindex, const int8_t xforce, const int8_t yforce) { + if (advPartProps == nullptr) + return; // no advanced properties available + applyForce(particles[particleindex], xforce, yforce, advPartProps[particleindex].forcecounter); +} + +// apply a force in x,y direction to all particles +// force is in 3.4 fixed point notation (see above) +void ParticleSystem2D::applyForce(const int8_t xforce, const int8_t yforce) { + // for small forces, need to use a delay counter + uint8_t tempcounter; + // note: this is not the most computationally efficient way to do this, but it saves on duplicate code and is fast enough + for (uint32_t i = 0; i < usedParticles; i++) { + tempcounter = forcecounter; + applyForce(particles[i], xforce, yforce, tempcounter); + } + forcecounter = tempcounter; // save value back +} + +// apply a force in angular direction to single particle +// caller needs to provide a 8bit counter that holds its value between calls (if using single particles, a counter for each particle is needed) +// angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) +// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame (useful force range is +/- 127) +void ParticleSystem2D::applyAngleForce(PSparticle &part, const int8_t force, const uint16_t angle, uint8_t &counter) { + int8_t xforce = ((int32_t)force * cos16_t(angle)) / 32767; // force is +/- 127 + int8_t yforce = ((int32_t)force * sin16_t(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + applyForce(part, xforce, yforce, counter); +} + +void ParticleSystem2D::applyAngleForce(const uint32_t particleindex, const int8_t force, const uint16_t angle) { + if (advPartProps == nullptr) + return; // no advanced properties available + applyAngleForce(particles[particleindex], force, angle, advPartProps[particleindex].forcecounter); +} + +// apply a force in angular direction to all particles +// angle is from 0-65535 (=0-360deg) angle = 0 means in positive x-direction (i.e. to the right) +void ParticleSystem2D::applyAngleForce(const int8_t force, const uint16_t angle) { + int8_t xforce = ((int32_t)force * cos16_t(angle)) / 32767; // force is +/- 127 + int8_t yforce = ((int32_t)force * sin16_t(angle)) / 32767; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + applyForce(xforce, yforce); +} + +// apply gravity to all particles using PS global gforce setting +// force is in 3.4 fixed point notation, see note above +// note: faster than apply force since direction is always down and counter is fixed for all particles +void ParticleSystem2D::applyGravity() { + int32_t dv = calcForce_dv(gforce, gforcecounter); + if(dv == 0) return; + for (uint32_t i = 0; i < usedParticles; i++) { + // Note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways + particles[i].vy = limitSpeed((int32_t)particles[i].vy - dv); + } +} + +// apply gravity to single particle using system settings (use this for sources) +// function does not increment gravity counter, if gravity setting is disabled, this cannot be used +void ParticleSystem2D::applyGravity(PSparticle &part) { + uint32_t counterbkp = gforcecounter; // backup PS gravity counter + int32_t dv = calcForce_dv(gforce, gforcecounter); + gforcecounter = counterbkp; //save it back + part.vy = limitSpeed((int32_t)part.vy - dv); +} + +// slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) +// note: a coefficient smaller than 0 will speed them up (this is a feature, not a bug), coefficient larger than 255 inverts the speed, so don't do that +void ParticleSystem2D::applyFriction(PSparticle &part, const int32_t coefficient) { + int32_t friction = 255 - coefficient; + // note: not checking if particle is dead can be done by caller (or can be omitted) + // note2: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate + part.vx = ((int32_t)part.vx * friction) / 255; + part.vy = ((int32_t)part.vy * friction) / 255; +} + +// apply friction to all particles +void ParticleSystem2D::applyFriction(const int32_t coefficient) { + int32_t friction = 255 - coefficient; + for (uint32_t i = 0; i < usedParticles; i++) { + // note: not checking if particle is dead is faster as most are usually alive and if few are alive, rendering is fast anyways + particles[i].vx = ((int32_t)particles[i].vx * friction) / 255; + particles[i].vy = ((int32_t)particles[i].vy * friction) / 255; + } +} + +// attracts a particle to an attractor particle using the inverse square-law +void ParticleSystem2D::pointAttractor(const uint32_t particleindex, PSparticle &attractor, const uint8_t strength, const bool swallow) { + if (advPartProps == nullptr) + return; // no advanced properties available + + // Calculate the distance between the particle and the attractor + int32_t dx = attractor.x - particles[particleindex].x; + int32_t dy = attractor.y - particles[particleindex].y; + + // Calculate the force based on inverse square law + int32_t distanceSquared = dx * dx + dy * dy; + if (distanceSquared < 8192) { + if (swallow) { // particle is close, age it fast so it fades out, do not attract further + if (particles[particleindex].ttl > 7) + particles[particleindex].ttl -= 8; + else { + particles[particleindex].ttl = 0; + return; + } + } + distanceSquared = 2 * PS_P_RADIUS * PS_P_RADIUS; // limit the distance to avoid very high forces + } + + int32_t force = ((int32_t)strength << 16) / distanceSquared; + int8_t xforce = (force * dx) / 1024; // scale to a lower value, found by experimenting + int8_t yforce = (force * dy) / 1024; // note: cannot use bit shifts as bit shifting is asymmetrical for positive and negative numbers and this needs to be accurate! + applyForce(particleindex, xforce, yforce); +} + +// render particles to the LED buffer (uses palette to render the 8bit particle color value) +// if wrap is set, particles half out of bounds are rendered to the other side of the matrix +// warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds +// firemode is only used for PS Fire FX +void ParticleSystem2D::ParticleSys_render() { + CRGB baseRGB; + uint32_t brightness; // particle brightness, fades if dying + static bool useAdditiveTransfer = false; // use add instead of set for buffer transferring (must persist between calls) + bool isNonFadeTransition = (pmem->inTransition || pmem->finalTransfer) && blendingStyle != BLEND_STYLE_FADE; + bool isOverlay = segmentIsOverlay(); + + // update global blur (used for blur transitions) + int32_t motionbluramount = motionBlur; + int32_t smearamount = smearBlur; + if(pmem->inTransition == effectID && blendingStyle == BLEND_STYLE_FADE) { // FX transition and this is the new FX: fade blur amount but only if using fade style + motionbluramount = globalBlur + (((motionbluramount - globalBlur) * (int)SEGMENT.progress()) >> 16); // fade from old blur to new blur during transitions + smearamount = globalSmear + (((smearamount - globalSmear) * (int)SEGMENT.progress()) >> 16); + } + globalBlur = motionbluramount; + globalSmear = smearamount; + + if(isOverlay) { + globalSmear = 0; // do not apply smear or blur in overlay or it turns everything into a blurry mess + globalBlur = 0; + } + // handle blurring and framebuffer update + if (framebuffer) { + if(!pmem->inTransition) + useAdditiveTransfer = false; // additive transfer is only usd in transitions (or in overlay) + // handle buffer blurring or clearing + bool bufferNeedsUpdate = !pmem->inTransition || pmem->inTransition == effectID || isNonFadeTransition; // not a transition; or new FX or not fading style: update buffer (blur, or clear) + if(bufferNeedsUpdate) { + bool loadfromSegment = !renderSolo || isNonFadeTransition; + if (globalBlur > 0 || globalSmear > 0) { // blurring active: if not a transition or is newFX, read data from segment before blurring (old FX can render to it afterwards) + for (int32_t y = 0; y <= maxYpixel; y++) { + int index = y * (maxXpixel + 1); + for (int32_t x = 0; x <= maxXpixel; x++) { + if (loadfromSegment) { // sharing the framebuffer with another segment or not using fade style blending: update buffer by reading back from segment + framebuffer[index] = SEGMENT.getPixelColorXY(x, y); // read from segment + } + fast_color_scale(framebuffer[index], globalBlur); // note: could skip if only globalsmear is active but usually they are both active and scaling is fast enough + index++; + } + } + } + else { // no blurring: clear buffer + memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); + } + } + // handle buffer for global large particle size rendering + if(particlesize > 1 && pmem->inTransition) { // if particle size is used by FX we need a clean buffer + if(bufferNeedsUpdate && !globalBlur) { // transfer without adding if buffer was not cleared above (happens if this is the new FX and other FX does not use blurring) + useAdditiveTransfer = false; // no blurring and big size particle FX is the new FX (rendered first after clearing), can just render normally + } + else { // this is the old FX (rendering second) or blurring is active: new FX already rendered to the buffer and blurring was applied above; transfer it to segment and clear it + transferBuffer(maxXpixel + 1, maxYpixel + 1, isOverlay); + memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); // clear the buffer after transfer + useAdditiveTransfer = true; // additive transfer reads from segment, adds that to the frame-buffer and writes back to segment, after transfer, segment and buffer are identical + } + } + } + else { // no local buffer available, apply blur to segment + if (motionBlur > 0) + SEGMENT.fadeToBlackBy(255 - motionBlur); + else + SEGMENT.fill(BLACK); //clear the buffer before rendering next frame + } + + // go over particles and render them to the buffer + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[i].ttl == 0 || particleFlags[i].outofbounds) + continue; + // generate RGB values for particle + if (fireIntesity) { // fire mode + brightness = (uint32_t)particles[i].ttl * (3 + (fireIntesity >> 5)) + 20; + brightness = min(brightness, (uint32_t)255); + baseRGB = ColorFromPaletteWLED(SEGPALETTE, brightness, 255); + } + else { + brightness = min((particles[i].ttl << 1), (int)255); + baseRGB = ColorFromPaletteWLED(SEGPALETTE, particles[i].hue, 255); + if (particles[i].sat < 255) { + CHSV32 baseHSV; + rgb2hsv((uint32_t((byte(baseRGB.r) << 16) | (byte(baseRGB.g) << 8) | (byte(baseRGB.b)))), baseHSV); // convert to HSV + baseHSV.s = particles[i].sat; // set the saturation + uint32_t tempcolor; + hsv2rgb(baseHSV, tempcolor); // convert back to RGB + baseRGB = (CRGB)tempcolor; + } + } + renderParticle(i, brightness, baseRGB, particlesettings.wrapX, particlesettings.wrapY); + } + + if (particlesize > 1) { + uint32_t passes = particlesize / 64 + 1; // number of blur passes, four passes max + uint32_t bluramount = particlesize; + uint32_t bitshift = 0; + for (uint32_t i = 0; i < passes; i++) { + if (i == 2) // for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) + bitshift = 1; + + if (framebuffer) + blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, bluramount << bitshift, bluramount << bitshift); + else { + SEGMENT.blur(bluramount << bitshift, true); + } + bluramount -= 64; + } + } + // apply 2D blur to rendered frame + if(globalSmear > 0) { + if (framebuffer) + blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, globalSmear, globalSmear); + else + SEGMENT.blur(globalSmear, true); + } + // transfer framebuffer to segment if available + if (pmem->inTransition != effectID || isNonFadeTransition) // not in transition or is old FX (rendered second) or not fade style + transferBuffer(maxXpixel + 1, maxYpixel + 1, useAdditiveTransfer | isOverlay); +} + +// calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer +void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint32_t brightness, const CRGB& color, const bool wrapX, const bool wrapY) { + if(particlesize == 0) { // single pixel rendering + uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT; + uint32_t y = particles[particleindex].y >> PS_P_RADIUS_SHIFT; + if (x <= (uint32_t)maxXpixel && y <= (uint32_t)maxYpixel) { + if (framebuffer) + fast_color_add(framebuffer[x + (maxYpixel - y) * (maxXpixel + 1)], color, brightness); + else + SEGMENT.addPixelColorXY(x, maxYpixel - y, color.scale8(brightness), true); + } + return; + } + int32_t pxlbrightness[4]; // brightness values for the four pixels representing a particle + int32_t pixco[4][2]; // physical pixel coordinates of the four pixels a particle is rendered to. x,y pairs + bool pixelvalid[4] = {true, true, true, true}; // is set to false if pixel is out of bounds + bool advancedrender = false; // rendering for advanced particles + // check if particle has advanced size properties and buffer is available + if (advPartProps && advPartProps[particleindex].size > 0) { + if (renderbuffer) { + advancedrender = true; + memset(renderbuffer, 0, 100 * sizeof(CRGB)); // clear the buffer, renderbuffer is 10x10 pixels + } + else return; // cannot render without buffers + } + // add half a radius as the rendering algorithm always starts at the bottom left, this leaves things positive, so shifts can be used, then shift coordinate by a full pixel (x--/y-- below) + int32_t xoffset = particles[particleindex].x + PS_P_HALFRADIUS; + int32_t yoffset = particles[particleindex].y + PS_P_HALFRADIUS; + int32_t dx = xoffset & (PS_P_RADIUS - 1); // relativ particle position in subpixel space + int32_t dy = yoffset & (PS_P_RADIUS - 1); // modulo replaced with bitwise AND, as radius is always a power of 2 + int32_t x = (xoffset >> PS_P_RADIUS_SHIFT); // divide by PS_P_RADIUS which is 64, so can bitshift (compiler can not optimize integer) + int32_t y = (yoffset >> PS_P_RADIUS_SHIFT); + + // set the four raw pixel coordinates, the order is bottom left [0], bottom right[1], top right [2], top left [3] + pixco[1][0] = pixco[2][0] = x; // bottom right & top right + pixco[2][1] = pixco[3][1] = y; // top right & top left + x--; // shift by a full pixel here, this is skipped above to not do -1 and then +1 + y--; + pixco[0][0] = pixco[3][0] = x; // bottom left & top left + pixco[0][1] = pixco[1][1] = y; // bottom left & bottom right + + // calculate brightness values for all four pixels representing a particle using linear interpolation + // could check for out of frame pixels here but calculating them is faster (very few are out) + // precalculate values for speed optimization + int32_t precal1 = (int32_t)PS_P_RADIUS - dx; + int32_t precal2 = ((int32_t)PS_P_RADIUS - dy) * brightness; + int32_t precal3 = dy * brightness; + pxlbrightness[0] = (precal1 * precal2) >> PS_P_SURFACE; // bottom left value equal to ((PS_P_RADIUS - dx) * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE + pxlbrightness[1] = (dx * precal2) >> PS_P_SURFACE; // bottom right value equal to (dx * (PS_P_RADIUS-dy) * brightness) >> PS_P_SURFACE + pxlbrightness[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightness) >> PS_P_SURFACE + pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightness) >> PS_P_SURFACE + + if (advancedrender) { + //render particle to a bigger size + //particle size to pixels: < 64 is 4x4, < 128 is 6x6, < 192 is 8x8, bigger is 10x10 + //first, render the pixel to the center of the renderbuffer, then apply 2D blurring + fast_color_add(renderbuffer[4 + (4 * 10)], color, pxlbrightness[0]); // order is: bottom left, bottom right, top right, top left + fast_color_add(renderbuffer[5 + (4 * 10)], color, pxlbrightness[1]); + fast_color_add(renderbuffer[5 + (5 * 10)], color, pxlbrightness[2]); + fast_color_add(renderbuffer[4 + (5 * 10)], color, pxlbrightness[3]); + uint32_t rendersize = 2; // initialize render size, minimum is 4x4 pixels, it is incremented int he loop below to start with 4 + uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below) + uint32_t maxsize = advPartProps[particleindex].size; + uint32_t xsize = maxsize; + uint32_t ysize = maxsize; + if (advPartSize) { // use advanced size control + if (advPartSize[particleindex].asymmetry > 0) + getParticleXYsize(&advPartProps[particleindex], &advPartSize[particleindex], xsize, ysize); + maxsize = (xsize > ysize) ? xsize : ysize; // choose the bigger of the two + } + maxsize = maxsize/64 + 1; // number of blur passes depends on maxsize, four passes max + uint32_t bitshift = 0; + for(uint32_t i = 0; i < maxsize; i++) { + if (i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) + bitshift = 1; + rendersize += 2; + offset--; + blur2D(renderbuffer, rendersize, rendersize, xsize << bitshift, ysize << bitshift, offset, offset, true); + xsize = xsize > 64 ? xsize - 64 : 0; + ysize = ysize > 64 ? ysize - 64 : 0; + } + + // calculate origin coordinates to render the particle to in the framebuffer + uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; + uint32_t yfb_orig = y - (rendersize>>1) + 1 - offset; + uint32_t xfb, yfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked (spits a warning though) + + //note on y-axis flip: WLED has the y-axis defined from top to bottom, so y coordinates must be flipped. doing this in the buffer xfer clashes with 1D/2D combined rendering, which does not invert y + // transferring the 1D buffer in inverted fashion will flip the x-axis of overlaid 2D FX, so the y-axis flip is done here so the buffer is flipped in y, giving correct results + + // transfer particle renderbuffer to framebuffer + for (uint32_t xrb = offset; xrb < rendersize + offset; xrb++) { + xfb = xfb_orig + xrb; + if (xfb > (uint32_t)maxXpixel) { + if (wrapX) { // wrap x to the other side if required + if (xfb > (uint32_t)maxXpixel << 1) // xfb is "negative", handle it + xfb = (maxXpixel + 1) + (int32_t)xfb; // this always overflows to within bounds + else + xfb = xfb % (maxXpixel + 1); // note: without the above "negative" check, this works only for powers of 2 + } + else + continue; + } + + for (uint32_t yrb = offset; yrb < rendersize + offset; yrb++) { + yfb = yfb_orig + yrb; + if (yfb > (uint32_t)maxYpixel) { + if (wrapY) {// wrap y to the other side if required + if (yfb > (uint32_t)maxYpixel << 1) // yfb is "negative", handle it + yfb = (maxYpixel + 1) + (int32_t)yfb; // this always overflows to within bounds + else + yfb = yfb % (maxYpixel + 1); // note: without the above "negative" check, this works only for powers of 2 + } + else + continue; + } + if (framebuffer) + fast_color_add(framebuffer[xfb + (maxYpixel - yfb) * (maxXpixel + 1)], renderbuffer[xrb + yrb * 10]); + else + SEGMENT.addPixelColorXY(xfb, maxYpixel - yfb, renderbuffer[xrb + yrb * 10],true); + } + } + } else { // standard rendering (2x2 pixels) + // check for out of frame pixels and wrap them if required: x,y is bottom left pixel coordinate of the particle + if (x < 0) { // left pixels out of frame + if (wrapX) { // wrap x to the other side if required + pixco[0][0] = pixco[3][0] = maxXpixel; + } else { + pixelvalid[0] = pixelvalid[3] = false; // out of bounds + } + } + else if (pixco[1][0] > (int32_t)maxXpixel) { // right pixels, only has to be checked if left pixel is in frame + if (wrapX) { // wrap y to the other side if required + pixco[1][0] = pixco[2][0] = 0; + } else { + pixelvalid[1] = pixelvalid[2] = false; // out of bounds + } + } + + if (y < 0) { // bottom pixels out of frame + if (wrapY) { // wrap y to the other side if required + pixco[0][1] = pixco[1][1] = maxYpixel; + } else { + pixelvalid[0] = pixelvalid[1] = false; // out of bounds + } + } + else if (pixco[2][1] > maxYpixel) { // top pixels + if (wrapY) { // wrap y to the other side if required + pixco[2][1] = pixco[3][1] = 0; + } else { + pixelvalid[2] = pixelvalid[3] = false; // out of bounds + } + } + if (framebuffer) { + for (uint32_t i = 0; i < 4; i++) { + if (pixelvalid[i]) + fast_color_add(framebuffer[pixco[i][0] + (maxYpixel - pixco[i][1]) * (maxXpixel + 1)], color, pxlbrightness[i]); // order is: bottom left, bottom right, top right, top left + } + } + else { + for (uint32_t i = 0; i < 4; i++) { + if (pixelvalid[i]) + SEGMENT.addPixelColorXY(pixco[i][0], maxYpixel - pixco[i][1], color.scale8((uint8_t)pxlbrightness[i]), true); + } + } + } +} + +// detect collisions in an array of particles and handle them +// uses binning by dividing the frame into slices in x direction which is efficient if using gravity in y direction (but less efficient for FX that use forces in x direction) +// for code simplicity, no y slicing is done, making very tall matrix configurations less efficient +// note: also tested adding y slicing, it gives diminishing returns, some FX even get slower. FX not using gravity would benefit with a 10% FPS improvement +void ParticleSystem2D::handleCollisions() { + int32_t collDistSq = particleHardRadius << 1; // distance is double the radius note: particleHardRadius is updated when setting global particle size + collDistSq = collDistSq * collDistSq; // square it for faster comparison (square is one operation) + // note: partices are binned in x-axis, assumption is that no more than half of the particles are in the same bin + // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) + constexpr int BIN_WIDTH = 6 * PS_P_RADIUS; // width of a bin in sub-pixels + int32_t overlap = particleHardRadius << 1; // overlap bins to include edge particles to neighbouring bins + if (advPartProps) //may be using individual particle size + overlap += 512; // add 2 * max radius (approximately) + uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 2); // assume no more than half of the particles are in the same bin, do not bin small amounts of particles + uint32_t numBins = (maxX + (BIN_WIDTH - 1)) / BIN_WIDTH; // number of bins in x direction + uint16_t binIndices[maxBinParticles]; // creat array on stack for indices, 2kB max for 1024 particles (ESP32_MAXPARTICLES/2) + uint32_t binParticleCount; // number of particles in the current bin + uint16_t nextFrameStartIdx = hw_random16(usedParticles); // index of the first particle in the next frame (set to fixed value if bin overflow) + uint32_t pidx = collisionStartIdx; //start index in case a bin is full, process remaining particles next frame + + // fill the binIndices array for this bin + for (uint32_t bin = 0; bin < numBins; bin++) { + binParticleCount = 0; // reset for this bin + int32_t binStart = bin * BIN_WIDTH - overlap; // note: first bin will extend to negative, but that is ok as out of bounds particles are ignored + int32_t binEnd = binStart + BIN_WIDTH + overlap; // note: last bin can be out of bounds, see above; + + // fill the binIndices array for this bin + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[pidx].ttl > 0 && particleFlags[pidx].outofbounds == 0 && particleFlags[pidx].collide) { // colliding particle + if (particles[pidx].x >= binStart && particles[pidx].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins) + if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame + nextFrameStartIdx = pidx; // bin overflow can only happen once as bin size is at least half of the particles (or half +1) + break; + } + binIndices[binParticleCount++] = pidx; + } + } + pidx++; + if (pidx >= usedParticles) pidx = 0; // wrap around + } + + for (uint32_t i = 0; i < binParticleCount; i++) { // go though all 'higher number' particles in this bin and see if any of those are in close proximity and if they are, make them collide + uint32_t idx_i = binIndices[i]; + for (uint32_t j = i + 1; j < binParticleCount; j++) { // check against higher number particles + uint32_t idx_j = binIndices[j]; + if (advPartProps) { //may be using individual particle size + setParticleSize(particlesize); // updates base particleHardRadius + collDistSq = (particleHardRadius << 1) + (((uint32_t)advPartProps[idx_i].size + (uint32_t)advPartProps[idx_j].size) >> 1); // collision distance note: not 100% clear why the >> 1 is needed, but it is. + collDistSq = collDistSq * collDistSq; // square it for faster comparison + } + int32_t dx = particles[idx_j].x - particles[idx_i].x; + if (dx * dx < collDistSq) { // check x direction, if close, check y direction (squaring is faster than abs() or dual compare) + int32_t dy = particles[idx_j].y - particles[idx_i].y; + if (dy * dy < collDistSq) // particles are close + collideParticles(particles[idx_i], particles[idx_j], dx, dy, collDistSq); + } + } + } + } + collisionStartIdx = nextFrameStartIdx; // set the start index for the next frame +} + +// handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS +// takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) +void ParticleSystem2D::collideParticles(PSparticle &particle1, PSparticle &particle2, int32_t dx, int32_t dy, const int32_t collDistSq) { + int32_t distanceSquared = dx * dx + dy * dy; + // Calculate relative velocity (if it is zero, could exit but extra check does not overall speed but deminish it) + int32_t relativeVx = (int32_t)particle2.vx - (int32_t)particle1.vx; + int32_t relativeVy = (int32_t)particle2.vy - (int32_t)particle1.vy; + + // if dx and dy are zero (i.e. same position) give them an offset, if speeds are also zero, also offset them (pushes particles apart if they are clumped before enabling collisions) + if (distanceSquared == 0) { + // Adjust positions based on relative velocity direction + dx = -1; + if (relativeVx < 0) // if true, particle2 is on the right side + dx = 1; + else if (relativeVx == 0) + relativeVx = 1; + + dy = -1; + if (relativeVy < 0) + dy = 1; + else if (relativeVy == 0) + relativeVy = 1; + + distanceSquared = 2; // 1 + 1 + } + + // Calculate dot product of relative velocity and relative distance + int32_t dotProduct = (dx * relativeVx + dy * relativeVy); // is always negative if moving towards each other + + if (dotProduct < 0) {// particles are moving towards each other + // integer math used to avoid floats. + // overflow check: dx/dy are 7bit, relativV are 8bit -> dotproduct is 15bit, dotproduct/distsquared ist 8b, multiplied by collisionhardness of 8bit. so a 16bit shift is ok, make it 15 to be sure no overflows happen + // note: cannot use right shifts as bit shifting in right direction is asymmetrical for positive and negative numbers and this needs to be accurate! the trick is: only shift positive numers + // Calculate new velocities after collision + int32_t surfacehardness = max(collisionHardness, (int32_t)PS_P_MINSURFACEHARDNESS); // if particles are soft, the impulse must stay above a limit or collisions slip through at higher speeds, 170 seems to be a good value + int32_t impulse = -(((((-dotProduct) << 15) / distanceSquared) * surfacehardness) >> 8); // note: inverting before bitshift corrects for asymmetry in right-shifts (and is slightly faster) + int32_t ximpulse = ((impulse) * dx) / 32767; // cannot use bit shifts here, it can be negative, use division by 2^bitshift + int32_t yimpulse = ((impulse) * dy) / 32767; + particle1.vx += ximpulse; + particle1.vy += yimpulse; + particle2.vx -= ximpulse; + particle2.vy -= yimpulse; + + if (collisionHardness < PS_P_MINSURFACEHARDNESS && (SEGMENT.call & 0x07) == 0) { // if particles are soft, they become 'sticky' i.e. apply some friction (they do pile more nicely and stop sloshing around) + const uint32_t coeff = collisionHardness + (255 - PS_P_MINSURFACEHARDNESS); + particle1.vx = ((int32_t)particle1.vx * coeff) / 255; // Note: could call applyFriction, but this is faster and speed is key here + particle1.vy = ((int32_t)particle1.vy * coeff) / 255; + + particle2.vx = ((int32_t)particle2.vx * coeff) / 255; + particle2.vy = ((int32_t)particle2.vy * coeff) / 255; + } + + // particles have volume, push particles apart if they are too close + // tried lots of configurations, it works best if not moved but given a little velocity, it tends to oscillate less this way + // when hard pushing by offsetting position, they sink into each other under gravity + // a problem with giving velocity is, that on harder collisions, this adds up as it is not dampened enough, so add friction in the FX if required + if(distanceSquared < collDistSq && dotProduct > -250) { // too close and also slow, push them apart + int32_t notsorandom = dotProduct & 0x01; //dotprouct LSB should be somewhat random, so no need to calculate a random number + int32_t pushamount = 1 + ((250 + dotProduct) >> 6); // the closer dotproduct is to zero, the closer the particles are + int32_t push = 0; + if (dx < 0) // particle 1 is on the right + push = pushamount; + else if (dx > 0) + push = -pushamount; + else { // on the same x coordinate, shift it a little so they do not stack + if (notsorandom) + particle1.x++; // move it so pile collapses + else + particle1.x--; + } + particle1.vx += push; + push = 0; + if (dy < 0) + push = pushamount; + else if (dy > 0) + push = -pushamount; + else { // dy==0 + if (notsorandom) + particle1.y++; // move it so pile collapses + else + particle1.y--; + } + particle1.vy += push; + + // note: pushing may push particles out of frame, if bounce is active, it will move it back as position will be limited to within frame, if bounce is disabled: bye bye + if (collisionHardness < 5) { // if they are very soft, stop slow particles completely to make them stick to each other + particle1.vx = 0; + particle1.vy = 0; + particle2.vx = 0; + particle2.vy = 0; + //push them apart + particle1.x += push; + particle1.y += push; + } + } + } +} + +// update size and pointers (memory location and size can change dynamically) +// note: do not access the PS class in FX befor running this function (or it messes up SEGENV.data) +void ParticleSystem2D::updateSystem(void) { + PSPRINTLN("updateSystem2D"); + setMatrixSize(SEGMENT.vWidth(), SEGMENT.vHeight()); + updateRenderingBuffer(SEGMENT.vWidth() * SEGMENT.vHeight(), true, false); // update rendering buffer (segment size can change at any time) + updatePSpointers(advPartProps != nullptr, advPartSize != nullptr); // update pointers to PS data, also updates availableParticles + setUsedParticles(fractionOfParticlesUsed); // update used particles based on percentage (can change during transitions, execute each frame for code simplicity) + if (partMemList.size() == 1) // if number of vector elements is one, this is the only system + renderSolo = true; + else + renderSolo = false; + PSPRINTLN("\n END update System2D, running FX..."); +} + +// set the pointers for the class (this only has to be done once and not on every FX call, only the class pointer needs to be reassigned to SEGENV.data every time) +// function returns the pointer to the next byte available for the FX (if it assigned more memory for other stuff using the above allocate function) +// FX handles the PSsources, need to tell this function how many there are +void ParticleSystem2D::updatePSpointers(bool isadvanced, bool sizecontrol) { + PSPRINTLN("updatePSpointers"); + // DEBUG_PRINT(F("*** PS pointers ***")); + // DEBUG_PRINTF_P(PSTR("this PS %p "), this); + // Note on memory alignment: + // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. + // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. + // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. + + // memory manager needs to know how many particles the FX wants to use so transitions can be handled properly (i.e. pointer will stop changing if enough particles are available during transitions) + uint32_t usedByFX = (numParticles * ((uint32_t)fractionOfParticlesUsed + 1)) >> 8; // final number of particles the FX wants to use (fractionOfParticlesUsed is 0-255) + particles = reinterpret_cast(particleMemoryManager(0, sizeof(PSparticle), availableParticles, usedByFX, effectID)); // get memory, leave buffer size as is (request 0) + particleFlags = reinterpret_cast(this + 1); // pointer to particle flags + sources = reinterpret_cast(particleFlags + numParticles); // pointer to source(s) at data+sizeof(ParticleSystem2D) + PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data + if (isadvanced) { + advPartProps = reinterpret_cast(sources + numSources); + PSdataEnd = reinterpret_cast(advPartProps + numParticles); + if (sizecontrol) { + advPartSize = reinterpret_cast(advPartProps + numParticles); + PSdataEnd = reinterpret_cast(advPartSize + numParticles); + } + } +#ifdef DEBUG_PS + Serial.printf_P(PSTR(" particles %p "), particles); + Serial.printf_P(PSTR(" sources %p "), sources); + Serial.printf_P(PSTR(" adv. props %p "), advPartProps); + Serial.printf_P(PSTR(" adv. ctrl %p "), advPartSize); + Serial.printf_P(PSTR("end %p\n"), PSdataEnd); + #endif + +} + +// blur a matrix in x and y direction, blur can be asymmetric in x and y +// for speed, 1D array and 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined +// to blur a subset of the buffer, change the xsize/ysize and set xstart/ystart to the desired starting coordinates (default start is 0/0) +// subset blurring only works on 10x10 buffer (single particle rendering), if other sizes are needed, buffer width must be passed as parameter +void blur2D(CRGB *colorbuffer, uint32_t xsize, uint32_t ysize, uint32_t xblur, uint32_t yblur, uint32_t xstart, uint32_t ystart, bool isparticle) { + CRGB seeppart, carryover; + uint32_t seep = xblur >> 1; + uint32_t width = xsize; // width of the buffer, used to calculate the index of the pixel + + if (isparticle) { //first and last row are always black in first pass of particle rendering + ystart++; + ysize--; + width = 10; // buffer size is 10x10 + } + + for(uint32_t y = ystart; y < ystart + ysize; y++) { + carryover = BLACK; + uint32_t indexXY = xstart + y * width; + for(uint32_t x = xstart; x < xstart + xsize; x++) { + seeppart = colorbuffer[indexXY]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if (x > 0) { + fast_color_add(colorbuffer[indexXY - 1], seeppart); + if(carryover) // note: check adds overhead but is faster on average + fast_color_add(colorbuffer[indexXY], carryover); + } + carryover = seeppart; + indexXY++; // next pixel in x direction + } + } + + if (isparticle) { // first and last row are now smeared + ystart--; + ysize++; + } + + seep = yblur >> 1; + for(uint32_t x = xstart; x < xstart + xsize; x++) { + carryover = BLACK; + uint32_t indexXY = x + ystart * width; + for(uint32_t y = ystart; y < ystart + ysize; y++) { + seeppart = colorbuffer[indexXY]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if (y > 0) { + fast_color_add(colorbuffer[indexXY - width], seeppart); + if(carryover) // note: check adds overhead but is faster on average + fast_color_add(colorbuffer[indexXY], carryover); + } + carryover = seeppart; + indexXY += width; // next pixel in y direction + } + } +} + +//non class functions to use for initialization +uint32_t calculateNumberOfParticles2D(uint32_t const pixels, const bool isadvanced, const bool sizecontrol) { + uint32_t numberofParticles = pixels; // 1 particle per pixel (for example 512 particles on 32x16) +#ifdef ESP8266 + uint32_t particlelimit = ESP8266_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 16x16 and 4k effect ram) +#elif ARDUINO_ARCH_ESP32S2 + uint32_t particlelimit = ESP32S2_MAXPARTICLES; // maximum number of paticles allowed (based on one segment of 32x32 and 24k effect ram) +#else + uint32_t particlelimit = ESP32_MAXPARTICLES; // maximum number of paticles allowed (based on two segments of 32x32 and 40k effect ram) +#endif + numberofParticles = max((uint32_t)4, min(numberofParticles, particlelimit)); // limit to 4 - particlelimit + if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount + numberofParticles = (numberofParticles * sizeof(PSparticle)) / (sizeof(PSparticle) + sizeof(PSadvancedParticle)); + if (sizecontrol) // advanced property array needs ram, reduce number of particles + numberofParticles /= 8; // if advanced size control is used, much fewer particles are needed note: if changing this number, adjust FX using this accordingly + + //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) + numberofParticles = ((numberofParticles+3) >> 2) << 2; // note: with a separate particle buffer, this is probably unnecessary + return numberofParticles; +} + +uint32_t calculateNumberOfSources2D(uint32_t pixels, uint32_t requestedsources) { +#ifdef ESP8266 + int numberofSources = min((pixels) / 8, (uint32_t)requestedsources); + numberofSources = max(1, min(numberofSources, ESP8266_MAXSOURCES)); // limit to 1 - 16 +#elif ARDUINO_ARCH_ESP32S2 + int numberofSources = min((pixels) / 6, (uint32_t)requestedsources); + numberofSources = max(1, min(numberofSources, ESP32S2_MAXSOURCES)); // limit to 1 - 48 +#else + int numberofSources = min((pixels) / 4, (uint32_t)requestedsources); + numberofSources = max(1, min(numberofSources, ESP32_MAXSOURCES)); // limit to 1 - 64 +#endif + // make sure it is a multiple of 4 for proper memory alignment + numberofSources = ((numberofSources+3) >> 2) << 2; + return numberofSources; +} + +//allocate memory for particle system class, particles, sprays plus additional memory requested by FX //TODO: add percentofparticles like in 1D to reduce memory footprint of some FX? +bool allocateParticleSystemMemory2D(uint32_t numparticles, uint32_t numsources, bool isadvanced, bool sizecontrol, uint32_t additionalbytes) { + PSPRINTLN("PS 2D alloc"); + uint32_t requiredmemory = sizeof(ParticleSystem2D); + uint32_t dummy; // dummy variable + if((particleMemoryManager(numparticles, sizeof(PSparticle), dummy, dummy, SEGMENT.mode)) == nullptr) // allocate memory for particles + return false; // not enough memory, function ensures a minimum of numparticles are available + + // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) + requiredmemory += sizeof(PSparticleFlags) * numparticles; + if (isadvanced) + requiredmemory += sizeof(PSadvancedParticle) * numparticles; + if (sizecontrol) + requiredmemory += sizeof(PSsizeControl) * numparticles; + requiredmemory += sizeof(PSsource) * numsources; + requiredmemory += additionalbytes; + PSPRINTLN("mem alloc: " + String(requiredmemory)); + return(SEGMENT.allocateData(requiredmemory)); +} + +// initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) +bool initParticleSystem2D(ParticleSystem2D *&PartSys, uint32_t requestedsources, uint32_t additionalbytes, bool advanced, bool sizecontrol) { + PSPRINT("PS 2D init "); + if(!strip.isMatrix) return false; // only for 2D + uint32_t cols = SEGMENT.virtualWidth(); + uint32_t rows = SEGMENT.virtualHeight(); + uint32_t pixels = cols * rows; + + if(advanced) + updateRenderingBuffer(100, false, true); // allocate a 10x10 buffer for rendering advanced particles + uint32_t numparticles = calculateNumberOfParticles2D(pixels, advanced, sizecontrol); + PSPRINT(" segmentsize:" + String(cols) + " " + String(rows)); + PSPRINT(" request numparticles:" + String(numparticles)); + uint32_t numsources = calculateNumberOfSources2D(pixels, requestedsources); + if (!allocateParticleSystemMemory2D(numparticles, numsources, advanced, sizecontrol, additionalbytes)) + { + DEBUG_PRINT(F("PS init failed: memory depleted")); + return false; + } + + PartSys = new (SEGENV.data) ParticleSystem2D(cols, rows, numparticles, numsources, advanced, sizecontrol); // particle system constructor + updateRenderingBuffer(SEGMENT.vWidth() * SEGMENT.vHeight(), true, true); // update or create rendering buffer note: for fragmentation it might be better to allocate this first, but if memory is scarce, system has a buffer but no particles and will return false + + PSPRINTLN("******init done, pointers:"); + #ifdef WLED_DEBUG_PS + PSPRINT("framebfr size:"); + PSPRINT(frameBufferSize); + PSPRINT(" @ addr: 0x"); + Serial.println((uintptr_t)framebuffer, HEX); + PSPRINT("renderbfr size:"); + PSPRINT(renderBufferSize); + PSPRINT(" @ addr: 0x"); + Serial.println((uintptr_t)renderbuffer, HEX); + #endif + return true; +} + +#endif // WLED_DISABLE_PARTICLESYSTEM2D + + +//////////////////////// +// 1D Particle System // +//////////////////////// +#ifndef WLED_DISABLE_PARTICLESYSTEM1D + +ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles, uint32_t numberofsources, bool isadvanced) { + effectID = SEGMENT.mode; + numSources = numberofsources; + numParticles = numberofparticles; // number of particles allocated in init + availableParticles = 0; // let the memory manager assign + fractionOfParticlesUsed = 255; // use all particles by default + advPartProps = nullptr; //make sure we start out with null pointers (just in case memory was not cleared) + //advPartSize = nullptr; + updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles) + setSize(length); + setWallHardness(255); // set default wall hardness to max + setGravity(0); //gravity disabled by default + setParticleSize(0); // 1 pixel size by default + motionBlur = 0; //no fading by default + smearBlur = 0; //no smearing by default + emitIndex = 0; + collisionStartIdx = 0; + // initialize some default non-zero values most FX use + for (uint32_t i = 0; i < numSources; i++) { + sources[i].source.ttl = 1; //set source alive + } + + if(isadvanced) { + for (uint32_t i = 0; i < numParticles; i++) { + advPartProps[i].sat = 255; // set full saturation (for particles that are transferred from non-advanced system) + } + } +} + +// update function applies gravity, moves the particles, handles collisions and renders the particles +void ParticleSystem1D::update(void) { + //apply gravity globally if enabled + if (particlesettings.useGravity) //note: in 1D system, applying gravity after collisions also works but may be worse + applyGravity(); + + // handle collisions (can push particles, must be done before updating particles or they can render out of bounds, causing a crash if using local buffer for speed) + if (particlesettings.useCollisions) + handleCollisions(); + + //move all particles + for (uint32_t i = 0; i < usedParticles; i++) { + particleMoveUpdate(particles[i], particleFlags[i], nullptr, advPartProps ? &advPartProps[i] : nullptr); + } + + if (particlesettings.colorByPosition) { + uint32_t scale = (255 << 16) / maxX; // speed improvement: multiplication is faster than division + for (uint32_t i = 0; i < usedParticles; i++) { + particles[i].hue = (scale * particles[i].x) >> 16; // note: x is > 0 if not out of bounds + } + } + + ParticleSys_render(); +} + +// set percentage of used particles as uint8_t i.e 127 means 50% for example +void ParticleSystem1D::setUsedParticles(const uint8_t percentage) { + fractionOfParticlesUsed = percentage; // note usedParticles is updated in memory manager + updateUsedParticles(numParticles, availableParticles, fractionOfParticlesUsed, usedParticles); + PSPRINT(" SetUsedpaticles: allocated particles: "); + PSPRINT(numParticles); + PSPRINT(" available particles: "); + PSPRINT(availableParticles); + PSPRINT(" ,used percentage: "); + PSPRINT(fractionOfParticlesUsed); + PSPRINT(" ,used particles: "); + PSPRINTLN(usedParticles); +} + +void ParticleSystem1D::setWallHardness(const uint8_t hardness) { + wallHardness = hardness; +} + +void ParticleSystem1D::setSize(const uint32_t x) { + maxXpixel = x - 1; // last physical pixel that can be drawn to + maxX = x * PS_P_RADIUS_1D - 1; // particle system boundary for movements +} + +void ParticleSystem1D::setWrap(const bool enable) { + particlesettings.wrap = enable; +} + +void ParticleSystem1D::setBounce(const bool enable) { + particlesettings.bounce = enable; +} + +void ParticleSystem1D::setKillOutOfBounds(const bool enable) { + particlesettings.killoutofbounds = enable; +} + +void ParticleSystem1D::setColorByAge(const bool enable) { + particlesettings.colorByAge = enable; +} + +void ParticleSystem1D::setColorByPosition(const bool enable) { + particlesettings.colorByPosition = enable; +} + +void ParticleSystem1D::setMotionBlur(const uint8_t bluramount) { + motionBlur = bluramount; +} + +void ParticleSystem1D::setSmearBlur(const uint8_t bluramount) { + smearBlur = bluramount; +} + +// render size, 0 = 1 pixel, 1 = 2 pixel (interpolated), bigger sizes require adanced properties +void ParticleSystem1D::setParticleSize(const uint8_t size) { + particlesize = size > 0 ? 1 : 0; // TODO: add support for global sizes? see note above (motion blur) + particleHardRadius = PS_P_MINHARDRADIUS_1D >> (!particlesize); // 2 pixel sized particles or single pixel sized particles +} + +// enable/disable gravity, optionally, set the force (force=8 is default) can be -127 to +127, 0 is disable +// if enabled, gravity is applied to all particles in ParticleSystemUpdate() +// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame (gives good results) +void ParticleSystem1D::setGravity(const int8_t force) { + if (force) { + gforce = force; + particlesettings.useGravity = true; + } + else + particlesettings.useGravity = false; +} + +void ParticleSystem1D::enableParticleCollisions(const bool enable, const uint8_t hardness) { + particlesettings.useCollisions = enable; + collisionHardness = hardness; +} + +// emit one particle with variation, returns index of last emitted particle (or -1 if no particle emitted) +int32_t ParticleSystem1D::sprayEmit(const PSsource1D &emitter) { + for (uint32_t i = 0; i < usedParticles; i++) { + emitIndex++; + if (emitIndex >= usedParticles) + emitIndex = 0; + if (particles[emitIndex].ttl == 0) { // find a dead particle + particles[emitIndex].vx = emitter.v + hw_random16(emitter.var << 1) - emitter.var; // random(-var,var) + particles[emitIndex].x = emitter.source.x; + particles[emitIndex].hue = emitter.source.hue; + particles[emitIndex].ttl = hw_random16(emitter.minLife, emitter.maxLife); + particleFlags[emitIndex].collide = emitter.sourceFlags.collide; + particleFlags[emitIndex].reversegrav = emitter.sourceFlags.reversegrav; + particleFlags[emitIndex].perpetual = emitter.sourceFlags.perpetual; + if (advPartProps) { + advPartProps[emitIndex].sat = emitter.sat; + advPartProps[emitIndex].size = emitter.size; + } + return emitIndex; + } + } + return -1; +} + +// particle moves, decays and dies, if killoutofbounds is set, out of bounds particles are set to ttl=0 +// uses passed settings to set bounce or wrap, if useGravity is set, it will never bounce at the top and killoutofbounds is not applied over the top +void ParticleSystem1D::particleMoveUpdate(PSparticle1D &part, PSparticleFlags1D &partFlags, PSsettings1D *options, PSadvancedParticle1D *advancedproperties) { + if (options == nullptr) + options = &particlesettings; // use PS system settings by default + + if (part.ttl > 0) { + if (!partFlags.perpetual) + part.ttl--; // age + if (options->colorByAge) + part.hue = min(part.ttl, (uint16_t)255); // set color to ttl + + int32_t renderradius = PS_P_HALFRADIUS_1D; // used to check out of bounds, default for 2 pixel rendering + int32_t newX = part.x + (int32_t)part.vx; + partFlags.outofbounds = false; // reset out of bounds (in case particle was created outside the matrix and is now moving into view) + + if (advancedproperties) { // using individual particle size? + if (advancedproperties->size > 1) + particleHardRadius = PS_P_MINHARDRADIUS_1D + (advancedproperties->size >> 1); + else // single pixel particles use half the collision distance for walls + particleHardRadius = PS_P_MINHARDRADIUS_1D >> 1; + renderradius = particleHardRadius; // note: for single pixel particles, it should be zero, but it does not matter as out of bounds checking is done in rendering function + } + + // if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle is not half out of view + if (options->bounce) { + if ((newX < (int32_t)particleHardRadius) || ((newX > (int32_t)(maxX - particleHardRadius)))) { // reached a wall + bool bouncethis = true; + if (options->useGravity) { + if (partFlags.reversegrav) { // skip bouncing at x = 0 + if (newX < (int32_t)particleHardRadius) + bouncethis = false; + } else if (newX > (int32_t)particleHardRadius) { // skip bouncing at x = max + bouncethis = false; + } + } + if (bouncethis) { + part.vx = -part.vx; // invert speed + part.vx = ((int32_t)part.vx * (int32_t)wallHardness) / 255; // reduce speed as energy is lost on non-hard surface + if (newX < (int32_t)particleHardRadius) + newX = particleHardRadius; // fast particles will never reach the edge if position is inverted, this looks better + else + newX = maxX - particleHardRadius; + } + } + } + + if (!checkBoundsAndWrap(newX, maxX, renderradius, options->wrap)) { // check out of bounds note: this must not be skipped or it can lead to crashes + partFlags.outofbounds = true; + if (options->killoutofbounds) { + bool killthis = true; + if (options->useGravity) { // if gravity is used, only kill below 'floor level' + if (partFlags.reversegrav) { // skip at x = 0, do not skip far out of bounds + if (newX < 0 || newX > maxX << 2) + killthis = false; + } else { // skip at x = max, do not skip far out of bounds + if (newX > 0 && newX < maxX << 2) + killthis = false; + } + } + if (killthis) + part.ttl = 0; + } + } + + if (!partFlags.fixed) + part.x = newX; // set new position + else + part.vx = 0; // set speed to zero. note: particle can get speed in collisions, if unfixed, it should not speed away + } +} + +// apply a force in x direction to individual particle (or source) +// caller needs to provide a 8bit counter (for each paticle) that holds its value between calls +// force is in 3.4 fixed point notation so force=16 means apply v+1 each frame default of 8 is every other frame +void ParticleSystem1D::applyForce(PSparticle1D &part, const int8_t xforce, uint8_t &counter) { + int32_t dv = calcForce_dv(xforce, counter); // velocity increase + part.vx = limitSpeed((int32_t)part.vx + dv); // apply the force to particle +} + +// apply a force to all particles +// force is in 3.4 fixed point notation (see above) +void ParticleSystem1D::applyForce(const int8_t xforce) { + int32_t dv = calcForce_dv(xforce, forcecounter); // velocity increase + for (uint32_t i = 0; i < usedParticles; i++) { + particles[i].vx = limitSpeed((int32_t)particles[i].vx + dv); + } +} + +// apply gravity to all particles using PS global gforce setting +// gforce is in 3.4 fixed point notation, see note above +void ParticleSystem1D::applyGravity() { + int32_t dv_raw = calcForce_dv(gforce, gforcecounter); + for (uint32_t i = 0; i < usedParticles; i++) { + int32_t dv = dv_raw; + if (particleFlags[i].reversegrav) dv = -dv_raw; + // note: not checking if particle is dead is omitted as most are usually alive and if few are alive, rendering is fast anyways + particles[i].vx = limitSpeed((int32_t)particles[i].vx - dv); + } +} + +// apply gravity to single particle using system settings (use this for sources) +// function does not increment gravity counter, if gravity setting is disabled, this cannot be used +void ParticleSystem1D::applyGravity(PSparticle1D &part, PSparticleFlags1D &partFlags) { + uint32_t counterbkp = gforcecounter; + int32_t dv = calcForce_dv(gforce, gforcecounter); + if (partFlags.reversegrav) dv = -dv; + gforcecounter = counterbkp; //save it back + part.vx = limitSpeed((int32_t)part.vx - dv); +} + + +// slow down particle by friction, the higher the speed, the higher the friction. a high friction coefficient slows them more (255 means instant stop) +// note: a coefficient smaller than 0 will speed them up (this is a feature, not a bug), coefficient larger than 255 inverts the speed, so don't do that +void ParticleSystem1D::applyFriction(int32_t coefficient) { + int32_t friction = 255 - coefficient; + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[i].ttl) + particles[i].vx = ((int32_t)particles[i].vx * friction) / 255; // note: cannot use bitshift as vx can be negative + } +} + + +// render particles to the LED buffer (uses palette to render the 8bit particle color value) +// if wrap is set, particles half out of bounds are rendered to the other side of the matrix +// warning: do not render out of bounds particles or system will crash! rendering does not check if particle is out of bounds +void ParticleSystem1D::ParticleSys_render() { + CRGB baseRGB; + uint32_t brightness; // particle brightness, fades if dying + // bool useAdditiveTransfer; // use add instead of set for buffer transferring + bool isNonFadeTransition = (pmem->inTransition || pmem->finalTransfer) && blendingStyle != BLEND_STYLE_FADE; + bool isOverlay = segmentIsOverlay(); + + // update global blur (used for blur transitions) + int32_t motionbluramount = motionBlur; + int32_t smearamount = smearBlur; + if(pmem->inTransition == effectID) { // FX transition and this is the new FX: fade blur amount + motionbluramount = globalBlur + (((motionbluramount - globalBlur) * (int)SEGMENT.progress()) >> 16); // fade from old blur to new blur during transitions + smearamount = globalSmear + (((smearamount - globalSmear) * (int)SEGMENT.progress()) >> 16); + } + globalBlur = motionbluramount; + globalSmear = smearamount; + + if (framebuffer) { + // handle buffer blurring or clearing + bool bufferNeedsUpdate = !pmem->inTransition || pmem->inTransition == effectID || isNonFadeTransition; // not a transition; or new FX: update buffer (blur, or clear) + if(bufferNeedsUpdate) { + bool loadfromSegment = !renderSolo || isNonFadeTransition; + if (globalBlur > 0 || globalSmear > 0) { // blurring active: if not a transition or is newFX, read data from segment before blurring (old FX can render to it afterwards) + for (int32_t x = 0; x <= maxXpixel; x++) { + if (loadfromSegment) // sharing the framebuffer with another segment: read buffer back from segment + framebuffer[x] = SEGMENT.getPixelColor(x); // copy to local buffer + fast_color_scale(framebuffer[x], motionBlur); + } + } + else { // no blurring: clear buffer + memset(framebuffer, 0, frameBufferSize * sizeof(CRGB)); + } + } + } + else { // no local buffer available + if (motionBlur > 0) + SEGMENT.fadeToBlackBy(255 - motionBlur); + else + SEGMENT.fill(BLACK); // clear the buffer before rendering to it + } + + // go over particles and render them to the buffer + for (uint32_t i = 0; i < usedParticles; i++) { + if ( particles[i].ttl == 0 || particleFlags[i].outofbounds) + continue; + + // generate RGB values for particle + brightness = min(particles[i].ttl << 1, (int)255); + baseRGB = ColorFromPaletteWLED(SEGPALETTE, particles[i].hue, 255); + + if (advPartProps) { //saturation is advanced property in 1D system + if (advPartProps[i].sat < 255) { + CHSV32 baseHSV; + rgb2hsv((uint32_t((byte(baseRGB.r) << 16) | (byte(baseRGB.g) << 8) | (byte(baseRGB.b)))), baseHSV); // convert to HSV + baseHSV.s = advPartProps[i].sat; // set the saturation + uint32_t tempcolor; + hsv2rgb(baseHSV, tempcolor); // convert back to RGB + baseRGB = (CRGB)tempcolor; + } + } + renderParticle(i, brightness, baseRGB, particlesettings.wrap); + } + // apply smear-blur to rendered frame + if(globalSmear > 0) { + if (framebuffer) + blur1D(framebuffer, maxXpixel + 1, globalSmear, 0); + else + SEGMENT.blur(globalSmear, true); + } + + // add background color + uint32_t bg_color = SEGCOLOR(1); + if (bg_color > 0) { //if not black + for(int32_t i = 0; i <= maxXpixel; i++) { + if (framebuffer) + fast_color_add(framebuffer[i], bg_color); + else + SEGMENT.addPixelColor(i, bg_color, true); + } + } + // transfer local buffer back to segment (if available) + if (pmem->inTransition != effectID || isNonFadeTransition) + transferBuffer(maxXpixel + 1, 0, isOverlay); +} + +// calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer +void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint32_t brightness, const CRGB &color, const bool wrap) { + uint32_t size = particlesize; + if (advPartProps) {// use advanced size properties + size = advPartProps[particleindex].size; + } + if (size == 0) { //single pixel particle, can be out of bounds as oob checking is made for 2-pixel particles (and updating it uses more code) + uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT_1D; + if (x <= (uint32_t)maxXpixel) { //by making x unsigned there is no need to check < 0 as it will overflow + if (framebuffer) + fast_color_add(framebuffer[x], color, brightness); + else + SEGMENT.addPixelColor(x, color.scale8((uint8_t)brightness), true); + } + return; + } + //render larger particles + bool pxlisinframe[2] = {true, true}; + int32_t pxlbrightness[2]; + int32_t pixco[2]; // physical pixel coordinates of the two pixels representing a particle + + // add half a radius as the rendering algorithm always starts at the bottom left, this leaves things positive, so shifts can be used, then shift coordinate by a full pixel (x-- below) + int32_t xoffset = particles[particleindex].x + PS_P_HALFRADIUS_1D; + int32_t dx = xoffset & (PS_P_RADIUS_1D - 1); //relativ particle position in subpixel space, modulo replaced with bitwise AND + int32_t x = xoffset >> PS_P_RADIUS_SHIFT_1D; // divide by PS_P_RADIUS, bitshift of negative number stays negative -> checking below for x < 0 works (but does not when using division) + + // set the raw pixel coordinates + pixco[1] = x; // right pixel + x--; // shift by a full pixel here, this is skipped above to not do -1 and then +1 + pixco[0] = x; // left pixel + + //calculate the brightness values for both pixels using linear interpolation (note: in standard rendering out of frame pixels could be skipped but if checks add more clock cycles over all) + pxlbrightness[0] = (((int32_t)PS_P_RADIUS_1D - dx) * brightness) >> PS_P_SURFACE_1D; + pxlbrightness[1] = (dx * brightness) >> PS_P_SURFACE_1D; + + // check if particle has advanced size properties and buffer is available + if (advPartProps && advPartProps[particleindex].size > 1) { + if (renderbuffer) { + memset(renderbuffer, 0, 10 * sizeof(CRGB)); // clear the buffer, renderbuffer is 10 pixels + } + else + return; // cannot render advanced particles without buffer + + //render particle to a bigger size + //particle size to pixels: 2 - 63 is 4 pixels, < 128 is 6pixels, < 192 is 8 pixels, bigger is 10 pixels + //first, render the pixel to the center of the renderbuffer, then apply 1D blurring + fast_color_add(renderbuffer[4], color, pxlbrightness[0]); + fast_color_add(renderbuffer[5], color, pxlbrightness[1]); + uint32_t rendersize = 2; // initialize render size, minimum is 4 pixels, it is incremented int he loop below to start with 4 + uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below) + uint32_t blurpasses = size/64 + 1; // number of blur passes depends on size, four passes max + uint32_t bitshift = 0; + for (uint32_t i = 0; i < blurpasses; i++) { + if (i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges) + bitshift = 1; + rendersize += 2; + offset--; + blur1D(renderbuffer, rendersize, size << bitshift, offset); + size = size > 64 ? size - 64 : 0; + } + + // calculate origin coordinates to render the particle to in the framebuffer + uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset; //note: using uint is fine + uint32_t xfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked + + // transfer particle renderbuffer to framebuffer + for (uint32_t xrb = offset; xrb < rendersize+offset; xrb++) { + xfb = xfb_orig + xrb; + if (xfb > (uint32_t)maxXpixel) { + if (wrap) { // wrap x to the other side if required + if (xfb > (uint32_t)maxXpixel << 1) // xfb is "negative" + xfb = (maxXpixel + 1) + (int32_t)xfb; // this always overflows to within bounds + else + xfb = xfb % (maxXpixel + 1); // note: without the above "negative" check, this works only for powers of 2 + } + else + continue; + } + if (framebuffer) + fast_color_add(framebuffer[xfb], renderbuffer[xrb]); + else + SEGMENT.addPixelColor(xfb, renderbuffer[xrb]); + } + } + else { // standard rendering (2 pixels per particle) + // check if any pixels are out of frame + if (x < 0) { // left pixels out of frame + if (wrap) // wrap x to the other side if required + pixco[0] = maxXpixel; + else + pxlisinframe[0] = false; // pixel is out of matrix boundaries, do not render + } + else if (pixco[1] > (int32_t)maxXpixel) { // right pixel, only has to be checkt if left pixel did not overflow + if (wrap) // wrap y to the other side if required + pixco[1] = 0; + else + pxlisinframe[1] = false; + } + for(uint32_t i = 0; i < 2; i++) { + if (pxlisinframe[i]) { + if (framebuffer) + fast_color_add(framebuffer[pixco[i]], color, pxlbrightness[i]); + else + SEGMENT.addPixelColor(pixco[i], color.scale8((uint8_t)pxlbrightness[i]), true); + } + } + } + +} + +// detect collisions in an array of particles and handle them +void ParticleSystem1D::handleCollisions() { + int32_t collisiondistance = particleHardRadius << 1; + // note: partices are binned by position, assumption is that no more than half of the particles are in the same bin + // if they are, collisionStartIdx is increased so each particle collides at least every second frame (which still gives decent collisions) + constexpr int BIN_WIDTH = 32 * PS_P_RADIUS_1D; // width of each bin, a compromise between speed and accuracy (lareger bins are faster but collapse more) + int32_t overlap = particleHardRadius << 1; // overlap bins to include edge particles to neighbouring bins + if (advPartProps) //may be using individual particle size + overlap += 256; // add 2 * max radius (approximately) + uint32_t maxBinParticles = max((uint32_t)50, (usedParticles + 1) / 4); // do not bin small amounts, limit max to 1/2 of particles + uint32_t numBins = (maxX + (BIN_WIDTH - 1)) / BIN_WIDTH; // calculate number of bins + uint16_t binIndices[maxBinParticles]; // array to store indices of particles in a bin + uint32_t binParticleCount; // number of particles in the current bin + uint16_t nextFrameStartIdx = hw_random16(usedParticles); // index of the first particle in the next frame (set to fixed value if bin overflow) + uint32_t pidx = collisionStartIdx; //start index in case a bin is full, process remaining particles next frame + for (uint32_t bin = 0; bin < numBins; bin++) { + binParticleCount = 0; // reset for this bin + int32_t binStart = bin * BIN_WIDTH - overlap; // note: first bin will extend to negative, but that is ok as out of bounds particles are ignored + int32_t binEnd = binStart + BIN_WIDTH + overlap; // note: last bin can be out of bounds, see above + + // fill the binIndices array for this bin + for (uint32_t i = 0; i < usedParticles; i++) { + if (particles[pidx].ttl > 0 && particleFlags[pidx].outofbounds == 0 && particleFlags[pidx].collide) { // colliding particle + if (particles[pidx].x >= binStart && particles[pidx].x <= binEnd) { // >= and <= to include particles on the edge of the bin (overlap to ensure boarder particles collide with adjacent bins) + if (binParticleCount >= maxBinParticles) { // bin is full, more particles in this bin so do the rest next frame + nextFrameStartIdx = pidx; // bin overflow can only happen once as bin size is at least half of the particles (or half +1) + break; + } + binIndices[binParticleCount++] = pidx; + } + } + pidx++; + if (pidx >= usedParticles) pidx = 0; // wrap around + } + + for (uint32_t i = 0; i < binParticleCount; i++) { // go though all 'higher number' particles and see if any of those are in close proximity and if they are, make them collide + uint32_t idx_i = binIndices[i]; + for (uint32_t j = i + 1; j < binParticleCount; j++) { // check against higher number particles + uint32_t idx_j = binIndices[j]; + if (advPartProps) { // use advanced size properties + collisiondistance = (PS_P_MINHARDRADIUS_1D << particlesize) + (((uint32_t)advPartProps[idx_i].size + (uint32_t)advPartProps[idx_j].size) >> 1); + } + int32_t dx = particles[idx_j].x - particles[idx_i].x; + int32_t dv = (int32_t)particles[idx_j].vx - (int32_t)particles[idx_i].vx; + int32_t proximity = collisiondistance; + if (dv >= proximity) // particles would go past each other in next move update + proximity += abs(dv); // add speed difference to catch fast particles + if (dx <= proximity && dx >= -proximity) { // collide if close + collideParticles(particles[idx_i], particleFlags[idx_i], particles[idx_j], particleFlags[idx_j], dx, dv, collisiondistance); + } + } + } + } + collisionStartIdx = nextFrameStartIdx; // set the start index for the next frame +} +// handle a collision if close proximity is detected, i.e. dx and/or dy smaller than 2*PS_P_RADIUS +// takes two pointers to the particles to collide and the particle hardness (softer means more energy lost in collision, 255 means full hard) +void ParticleSystem1D::collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, int32_t dx, int32_t relativeVx, const int32_t collisiondistance) { + int32_t dotProduct = (dx * relativeVx); // is always negative if moving towards each other + uint32_t distance = abs(dx); + if (dotProduct < 0) { // particles are moving towards each other + uint32_t surfacehardness = max(collisionHardness, (int32_t)PS_P_MINSURFACEHARDNESS_1D); // if particles are soft, the impulse must stay above a limit or collisions slip through + // Calculate new velocities after collision + int32_t impulse = relativeVx * surfacehardness / 255; + particle1.vx += impulse; + particle2.vx -= impulse; + + // if one of the particles is fixed, transfer the impulse back so it bounces + if (particle1flags.fixed) + particle2.vx = -particle1.vx; + else if (particle2flags.fixed) + particle1.vx = -particle2.vx; + + if (collisionHardness < PS_P_MINSURFACEHARDNESS_1D && (SEGMENT.call & 0x07) == 0) { // if particles are soft, they become 'sticky' i.e. apply some friction + const uint32_t coeff = collisionHardness + (250 - PS_P_MINSURFACEHARDNESS_1D); + particle1.vx = ((int32_t)particle1.vx * coeff) / 255; + particle2.vx = ((int32_t)particle2.vx * coeff) / 255; + } + } + else if (distance < collisiondistance || relativeVx == 0) // moving apart or moving along and/or distance too close, push particles apart + { + // particles have volume, push particles apart if they are too close + // behaviour is different than in 2D, we need pixel accurate stacking here, push the top particle + // note: like in 2D, pushing by a distance makes softer piles collapse, giving particles speed prevents that and looks nicer + int32_t pushamount = 1; + if (dx < 0) // particle2.x < particle1.x + pushamount = -pushamount; + particle1.vx -= pushamount; + particle2.vx += pushamount; + + if(distance < collisiondistance >> 1 ) { // too close, force push particles + pushamount = (collisiondistance - distance) >> 3; // note: push amount found by experimentation + if(particle1.x < (maxX >> 1)) { // lower half, push particle with larger x in positive direction + if (dx < 0 && !particle1flags.fixed) // particle2.x < particle1.x -> push particle 1 + particle1.vx += pushamount; + else if (!particle2flags.fixed) // particle1.x < particle2.x -> push particle 2 + particle2.vx += pushamount; + } + else { // upper half, push particle with smaller x + if (dx < 0 && !particle2flags.fixed) // particle2.x < particle1.x -> push particle 2 + particle2.vx -= pushamount; + else if (!particle2flags.fixed) // particle1.x < particle2.x -> push particle 1 + particle1.vx -= pushamount; + } + } + } +} + +// update size and pointers (memory location and size can change dynamically) +// note: do not access the PS class in FX befor running this function (or it messes up SEGENV.data) +void ParticleSystem1D::updateSystem(void) { + setSize(SEGMENT.vLength()); // update size + updateRenderingBuffer(SEGMENT.vLength(), true, false); // update rendering buffer (segment size can change at any time) + updatePSpointers(advPartProps != nullptr); + setUsedParticles(fractionOfParticlesUsed); // update used particles based on percentage (can change during transitions, execute each frame for code simplicity) + if (partMemList.size() == 1) // if number of vector elements is one, this is the only system + renderSolo = true; + else + renderSolo = false; +} + +// set the pointers for the class (this only has to be done once and not on every FX call, only the class pointer needs to be reassigned to SEGENV.data every time) +// function returns the pointer to the next byte available for the FX (if it assigned more memory for other stuff using the above allocate function) +// FX handles the PSsources, need to tell this function how many there are +void ParticleSystem1D::updatePSpointers(bool isadvanced) { + // Note on memory alignment: + // a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment. + // The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock. + // by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes. + + // memory manager needs to know how many particles the FX wants to use so transitions can be handled properly (i.e. pointer will stop changing if enough particles are available during transitions) + uint32_t usedByFX = (numParticles * ((uint32_t)fractionOfParticlesUsed + 1)) >> 8; // final number of particles the FX wants to use (fractionOfParticlesUsed is 0-255) + particles = reinterpret_cast(particleMemoryManager(0, sizeof(PSparticle1D), availableParticles, usedByFX, effectID)); // get memory, leave buffer size as is (request 0) + particleFlags = reinterpret_cast(this + 1); // pointer to particle flags + sources = reinterpret_cast(particleFlags + numParticles); // pointer to source(s) + PSdataEnd = reinterpret_cast(sources + numSources); // pointer to first available byte after the PS for FX additional data + if (isadvanced) { + advPartProps = reinterpret_cast(sources + numSources); + PSdataEnd = reinterpret_cast(advPartProps + numParticles); + } + #ifdef WLED_DEBUG_PS + PSPRINTLN(" PS Pointers: "); + PSPRINT(" PS : 0x"); + Serial.println((uintptr_t)this, HEX); + PSPRINT(" Sources : 0x"); + Serial.println((uintptr_t)sources, HEX); + PSPRINT(" Particles : 0x"); + Serial.println((uintptr_t)particles, HEX); + #endif +} + +//non class functions to use for initialization, fraction is uint8_t: 255 means 100% +uint32_t calculateNumberOfParticles1D(const uint32_t fraction, const bool isadvanced) { + uint32_t numberofParticles = SEGMENT.virtualLength(); // one particle per pixel (if possible) +#ifdef ESP8266 + uint32_t particlelimit = ESP8266_MAXPARTICLES_1D; // maximum number of paticles allowed +#elif ARDUINO_ARCH_ESP32S2 + uint32_t particlelimit = ESP32S2_MAXPARTICLES_1D; // maximum number of paticles allowed +#else + uint32_t particlelimit = ESP32_MAXPARTICLES_1D; // maximum number of paticles allowed +#endif + numberofParticles = min(numberofParticles, particlelimit); // limit to particlelimit + if (isadvanced) // advanced property array needs ram, reduce number of particles to use the same amount + numberofParticles = (numberofParticles * sizeof(PSparticle1D)) / (sizeof(PSparticle1D) + sizeof(PSadvancedParticle1D)); + numberofParticles = (numberofParticles * (fraction + 1)) >> 8; // calculate fraction of particles + numberofParticles = numberofParticles < 20 ? 20 : numberofParticles; // 20 minimum + //make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes) + numberofParticles = ((numberofParticles+3) >> 2) << 2; // note: with a separate particle buffer, this is probably unnecessary + return numberofParticles; +} + +uint32_t calculateNumberOfSources1D(const uint32_t requestedsources) { +#ifdef ESP8266 + int numberofSources = max(1, min((int)requestedsources,ESP8266_MAXSOURCES_1D)); // limit to 1 - 8 +#elif ARDUINO_ARCH_ESP32S2 + int numberofSources = max(1, min((int)requestedsources, ESP32S2_MAXSOURCES_1D)); // limit to 1 - 16 +#else + int numberofSources = max(1, min((int)requestedsources, ESP32_MAXSOURCES_1D)); // limit to 1 - 32 +#endif + // make sure it is a multiple of 4 for proper memory alignment (so minimum is acutally 4) + numberofSources = ((numberofSources+3) >> 2) << 2; + return numberofSources; +} + +//allocate memory for particle system class, particles, sprays plus additional memory requested by FX +bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t numsources, const bool isadvanced, const uint32_t additionalbytes) { + uint32_t requiredmemory = sizeof(ParticleSystem1D); + uint32_t dummy; // dummy variable + if(particleMemoryManager(numparticles, sizeof(PSparticle1D), dummy, dummy, SEGMENT.mode) == nullptr) // allocate memory for particles + return false; // not enough memory, function ensures a minimum of numparticles are avialable + // functions above make sure these are a multiple of 4 bytes (to avoid alignment issues) + requiredmemory += sizeof(PSparticleFlags1D) * numparticles; + requiredmemory += sizeof(PSsource1D) * numsources; + requiredmemory += additionalbytes; + if (isadvanced) + requiredmemory += sizeof(PSadvancedParticle1D) * numparticles; + return(SEGMENT.allocateData(requiredmemory)); +} + +// initialize Particle System, allocate additional bytes if needed (pointer to those bytes can be read from particle system class: PSdataEnd) +// note: percentofparticles is in uint8_t, for example 191 means 75%, (deafaults to 255 or 100% meaning one particle per pixel), can be more than 100% (but not recommended, can cause out of memory) +bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedsources, const uint8_t fractionofparticles, const uint32_t additionalbytes, const bool advanced) { + if (SEGLEN == 1) return false; // single pixel not supported + if(advanced) + updateRenderingBuffer(10, false, true); // buffer for advanced particles, fixed size + uint32_t numparticles = calculateNumberOfParticles1D(fractionofparticles, advanced); + uint32_t numsources = calculateNumberOfSources1D(requestedsources); + if (!allocateParticleSystemMemory1D(numparticles, numsources, advanced, additionalbytes)) { + DEBUG_PRINT(F("PS init failed: memory depleted")); + return false; + } + PartSys = new (SEGENV.data) ParticleSystem1D(SEGMENT.virtualLength(), numparticles, numsources, advanced); // particle system constructor + updateRenderingBuffer(SEGMENT.vLength(), true, true); // update/create frame rendering buffer note: for fragmentation it might be better to allocate this first, but if memory is scarce, system has a buffer but no particles and will return false + return true; +} + +// blur a 1D buffer, sub-size blurring can be done using start and size +// for speed, 32bit variables are used, make sure to limit them to 8bit (0-255) or result is undefined +// to blur a subset of the buffer, change the size and set start to the desired starting coordinates +void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, uint32_t start) +{ + CRGB seeppart, carryover; + uint32_t seep = blur >> 1; + carryover = BLACK; + for(uint32_t x = start; x < start + size; x++) { + seeppart = colorbuffer[x]; // create copy of current color + fast_color_scale(seeppart, seep); // scale it and seep to neighbours + if (x > 0) { + fast_color_add(colorbuffer[x-1], seeppart); + if(carryover) // note: check adds overhead but is faster on average + fast_color_add(colorbuffer[x], carryover); // is black on first pass + } + carryover = seeppart; + } +} +#endif // WLED_DISABLE_PARTICLESYSTEM1D + +#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled + +////////////////////////////// +// Shared Utility Functions // +////////////////////////////// + +// calculate the delta speed (dV) value and update the counter for force calculation (is used several times, function saves on codesize) +// force is in 3.4 fixedpoint notation, +/-127 +static int32_t calcForce_dv(const int8_t force, uint8_t &counter) { + if (force == 0) + return 0; + // for small forces, need to use a delay counter + int32_t force_abs = abs(force); // absolute value (faster than lots of if's only 7 instructions) + int32_t dv = 0; + // for small forces, need to use a delay counter, apply force only if it overflows + if (force_abs < 16) { + counter += force_abs; + if (counter > 15) { + counter -= 16; + dv = force < 0 ? -1 : 1; // force is either 1 or -1 if it is small (zero force is handled above) + } + } + else + dv = force / 16; // MSBs, note: cannot use bitshift as dv can be negative + + return dv; +} + +// check if particle is out of bounds and wrap it around if required, returns false if out of bounds +static bool checkBoundsAndWrap(int32_t &position, const int32_t max, const int32_t particleradius, const bool wrap) { + if ((uint32_t)position > (uint32_t)max) { // check if particle reached an edge, cast to uint32_t to save negative checking (max is always positive) + if (wrap) { + position = position % (max + 1); // note: cannot optimize modulo, particles can be far out of bounds when wrap is enabled + if (position < 0) + position += max + 1; + } + else if (((position < -particleradius) || (position > max + particleradius))) // particle is leaving boundaries, out of bounds if it has fully left + return false; // out of bounds + } + return true; // particle is in bounds +} + +// fastled color adding is very inaccurate in color preservation (but it is fast) +// a better color add function is implemented in colors.cpp but it uses 32bit RGBW. to use it colors need to be shifted just to then be shifted back by that function, which is slow +// this is a fast version for RGB (no white channel, PS does not handle white) and with native CRGB including scaling of second color +// note: result is stored in c1, not using a return value is faster as the CRGB struct does not need to be copied upon return +// note2: function is mainly used to add scaled colors, so checking if one color is black is slower +// note3: scale is 255 when using blur, checking for that makes blur faster +static void fast_color_add(CRGB &c1, const CRGB &c2, const uint32_t scale) { + uint32_t r, g, b; + if (scale < 255) { + r = c1.r + ((c2.r * scale) >> 8); + g = c1.g + ((c2.g * scale) >> 8); + b = c1.b + ((c2.b * scale) >> 8); + } else { + r = c1.r + c2.r; + g = c1.g + c2.g; + b = c1.b + c2.b; + } + + uint32_t max = std::max(r,g); // check for overflow, using max() is faster as the compiler can optimize + max = std::max(max,b); + if (max < 256) { + c1.r = r; // save result to c1 + c1.g = g; + c1.b = b; + } else { + uint32_t newscale = (255U << 16) / max; + c1.r = (r * newscale) >> 16; + c1.g = (g * newscale) >> 16; + c1.b = (b * newscale) >> 16; + } +} + +// faster than fastled color scaling as it does in place scaling +static void fast_color_scale(CRGB &c, const uint32_t scale) { + c.r = ((c.r * scale) >> 8); + c.g = ((c.g * scale) >> 8); + c.b = ((c.b * scale) >> 8); +} + + +////////////////////////////////////////////////////////// +// memory and transition management for particle system // +////////////////////////////////////////////////////////// +// note: these functions can only be called while strip is servicing + +// allocate memory using the FX data limit, if overridelimit is set, temporarily ignore the limit +void* allocatePSmemory(size_t size, bool overridelimit) { + PSPRINT(" PS mem alloc: "); + PSPRINTLN(size); + // buffer uses effect data, check if there is enough space + if (!overridelimit && Segment::getUsedSegmentData() + size > MAX_SEGMENT_DATA) { + // not enough memory + PSPRINT(F("!!! Effect RAM depleted: ")); + DEBUG_PRINTF_P(PSTR("%d/%d !!!\n"), size, Segment::getUsedSegmentData()); + errorFlag = ERR_NORAM; + return nullptr; + } + void* buffer = calloc(size, sizeof(byte)); + if (buffer == nullptr) { + PSPRINT(F("!!! Memory allocation failed !!!")); + errorFlag = ERR_NORAM; + return nullptr; + } + Segment::addUsedSegmentData(size); + #ifdef WLED_DEBUG_PS + PSPRINT("Pointer address: 0x"); + Serial.println((uintptr_t)buffer, HEX); + #endif + return buffer; +} + +// deallocate memory and update data usage, use with care! +void deallocatePSmemory(void* dataptr, uint32_t size) { + PSPRINTLN("deallocating PSmemory:" + String(size)); + if(dataptr == nullptr) return; // safety check + free(dataptr); // note: setting pointer null must be done by caller, passing a reference to a cast void pointer is not possible + Segment::addUsedSegmentData(size <= Segment::getUsedSegmentData() ? -size : -Segment::getUsedSegmentData()); +} + +// Particle transition manager, creates/extends buffer if needed and handles transition memory-handover +void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize, uint32_t &availableToPS, uint32_t numParticlesUsed, const uint8_t effectID) { + pmem = getPartMem(); + void* buffer = nullptr; + PSPRINTLN("PS MemManager"); + if (pmem) { // segment has a buffer + if (requestedParticles) { // request for a new buffer, this is an init call + PSPRINTLN("Buffer exists, request for particles: " + String(requestedParticles)); + pmem->transferParticles = true; // set flag to transfer particles + uint32_t requestsize = structSize * requestedParticles; // required buffer size + if (requestsize > pmem->buffersize) { // request is larger than buffer, try to extend it + if (Segment::getUsedSegmentData() + requestsize - pmem->buffersize <= MAX_SEGMENT_DATA) { // enough memory available to extend buffer + PSPRINTLN("Extending buffer"); + buffer = allocatePSmemory(requestsize, true); // calloc new memory in FX data, override limit (temporary buffer) + if (buffer) { // allocaction successful, copy old particles to new buffer + memcpy(buffer, pmem->particleMemPointer, pmem->buffersize); // copy old particle buffer note: only required if transition but copy is fast and rarely happens + deallocatePSmemory(pmem->particleMemPointer, pmem->buffersize); // free old memory + pmem->particleMemPointer = buffer; // set new buffer + pmem->buffersize = requestsize; // update buffer size + } + else + return nullptr; // no memory available + } + } + if (pmem->watchdog == 1) { // if a PS already exists during particle request, it kicked the watchdog in last frame, servicePSmem() adds 1 afterwards -> PS to PS transition + if(pmem->currentFX == effectID) // if the new effect is the same as the current one, do not transition: transferParticles is set above, so this will transfer all particles back if called during transition + pmem->inTransition = false; // reset transition flag + else + pmem->inTransition = effectID; // save the ID of the new effect (required to determine blur amount in rendering function) + PSPRINTLN("PS to PS transition"); + } + return pmem->particleMemPointer; // return the available buffer on init call + } + pmem->watchdog = 0; // kick watchdog + buffer = pmem->particleMemPointer; // buffer is already allocated + } + else { // if the id was not found create a buffer and add an element to the list + PSPRINTLN("New particle buffer request: " + String(requestedParticles)); + uint32_t requestsize = structSize * requestedParticles; // required buffer size + buffer = allocatePSmemory(requestsize, false); // allocate new memory + if (buffer) + partMemList.push_back({buffer, requestsize, 0, strip.getCurrSegmentId(), 0, 0, 0, false, true}); // add buffer to list, set flag to transfer/init the particles note: if pushback fails, it may crash + else + return nullptr; // there is no memory available TODO: if localbuffer is allocated, free it and try again, its no use having a buffer but no particles + pmem = getPartMem(); // get the pointer to the new element (check that it was added) + if (!pmem) { // something went wrong + free(buffer); + return nullptr; + } + return buffer; // directly return the buffer on init call + } + + // now we have a valid buffer, if this is a PS to PS FX transition: transfer particles slowly to new FX + if(!SEGMENT.isInTransition()) pmem->inTransition = false; // transition has ended, invoke final transfer + if (pmem->inTransition) { + uint32_t maxParticles = pmem->buffersize / structSize; // maximum number of particles that fit in the buffer + uint16_t progress = SEGMENT.progress(); // transition progress + uint32_t newAvailable = 0; + if (SEGMENT.mode == effectID) { // new effect ID -> function was called from new FX + PSPRINTLN("new effect"); + newAvailable = (maxParticles * progress) >> 16; // update total particles available to this PS (newAvailable is guaranteed to be smaller than maxParticles) + if(newAvailable < 2) newAvailable = 2; // give 2 particle minimum (some FX may crash with less as they do i+1 access) + if(newAvailable > numParticlesUsed) newAvailable = numParticlesUsed; // limit to number of particles used, do not move the pointer anymore (will be set to base in final handover) + uint32_t bufferoffset = (maxParticles - 1) - newAvailable; // offset to new effect particles (in particle structs, not bytes) + if(bufferoffset < maxParticles) // safety check + buffer = (void*)((uint8_t*)buffer + bufferoffset * structSize); // new effect gets the end of the buffer + int32_t totransfer = newAvailable - availableToPS; // number of particles to transfer in this transition update + if(totransfer > 0) // safety check + particleHandover(buffer, structSize, totransfer); + } + else { // this was called from the old FX + PSPRINTLN("old effect"); + SEGMENT.loadOldPalette(); // load the old palette into segment palette + progress = 0xFFFFU - progress; // inverted transition progress + newAvailable = ((maxParticles * progress) >> 16); // result is guaranteed to be smaller than maxParticles + if(newAvailable > 0) newAvailable--; // -1 to avoid overlapping memory in 1D<->2D transitions + if(newAvailable < 2) newAvailable = 2; // give 2 particle minimum (some FX may crash with less as they do i+1 access) + // note: buffer pointer stays the same, number of available particles is reduced + } + availableToPS = newAvailable; + } else if(pmem->transferParticles) { // no PS transition, full buffer available + // transition ended (or blending is disabled) -> transfer all remaining particles + PSPRINTLN("PS transition ended, final particle handover"); + uint32_t maxParticles = pmem->buffersize / structSize; // maximum number of particles that fit in the buffer + if (maxParticles > availableToPS) { // not all particles transferred yet + uint32_t totransfer = maxParticles - availableToPS; // transfer all remaining particles + if(totransfer <= maxParticles) // safety check + particleHandover(buffer, structSize, totransfer); + if(maxParticles > numParticlesUsed) { // FX uses less than max: move the already existing particles to the beginning of the buffer + uint32_t usedbytes = availableToPS * structSize; + int32_t bufferoffset = (maxParticles - 1) - availableToPS; // offset to existing particles (see above) + if(bufferoffset < (int)maxParticles) { // safety check + void* currentBuffer = (void*)((uint8_t*)buffer + bufferoffset * structSize); // pointer to current buffer start + memmove(buffer, currentBuffer, usedbytes); // move the existing particles to the beginning of the buffer + } + } + } + // kill unused particles so they do not re-appear when transitioning to next FX + //TODO: should this be done in the handover function? maybe with a "cleanup" parameter? + //TODO2: the memmove above should be done here (or in handover function): it should copy all alive particles to the beginning of the buffer (to TTL=0 particles maybe?) + // -> currently when moving form blobs to ballpit particles disappear + #ifndef WLED_DISABLE_PARTICLESYSTEM2D + if (structSize == sizeof(PSparticle)) { // 2D particle + PSparticle *particles = (PSparticle*)buffer; + for (uint32_t i = availableToPS; i < maxParticles; i++) { + particles[i].ttl = 0; // kill unused particles + } + } + else // 1D particle system + #endif + { + #ifndef WLED_DISABLE_PARTICLESYSTEM1D + PSparticle1D *particles = (PSparticle1D*)buffer; + for (uint32_t i = availableToPS; i < maxParticles; i++) { + particles[i].ttl = 0; // kill unused particles + } + #endif + } + availableToPS = maxParticles; // now all particles are available to new FX + PSPRINTLN("final available particles: " + String(availableToPS)); + pmem->particleType = structSize; // update particle type + pmem->transferParticles = false; + pmem->finalTransfer = true; // let rendering function update its buffer if required + pmem->currentFX = effectID; // FX has now settled in, update the FX ID to track future transitions + } + else // no transition + pmem->finalTransfer = false; + + #ifdef WLED_DEBUG_PS + PSPRINT(" Particle memory Pointer address: 0x"); + Serial.println((uintptr_t)buffer, HEX); + #endif + return buffer; +} + +// (re)initialize particles in the particle buffer for use in the new FX +void particleHandover(void *buffer, size_t structSize, int32_t numToTransfer) { + if (pmem->particleType != structSize) { // check if we are being handed over from a different system (1D<->2D), clear buffer if so + memset(buffer, 0, numToTransfer * structSize); // clear buffer + } + uint16_t maxTTL = 0; + uint32_t TTLrandom = 0; + maxTTL = ((unsigned)strip.getTransition() << 1) / FRAMETIME_FIXED; // tie TTL to transition time: limit to double the transition time + some randomness + #ifndef WLED_DISABLE_PARTICLESYSTEM2D + if (structSize == sizeof(PSparticle)) { // 2D particle + PSparticle *particles = (PSparticle *)buffer; + for (int32_t i = 0; i < numToTransfer; i++) { + if (blendingStyle == BLEND_STYLE_FADE) { + if(particles[i].ttl > maxTTL) + particles[i].ttl = maxTTL + hw_random16(150); // reduce TTL so it will die soon + } + else + particles[i].ttl = 0; // kill transferred particles if not using fade blending style + particles[i].sat = 255; // full saturation + } + } + else // 1D particle system + #endif + { + #ifndef WLED_DISABLE_PARTICLESYSTEM1D + PSparticle1D *particles = (PSparticle1D *)buffer; + for (int32_t i = 0; i < numToTransfer; i++) { + if (blendingStyle == BLEND_STYLE_FADE) { + if(particles[i].ttl > maxTTL) + particles[i].ttl = maxTTL + hw_random16(150); // reduce TTL so it will die soon + } + else + particles[i].ttl = 0; // kill transferred particles if not using fade blending style + } + #endif + } +} + +// update number of particles to use, limit to allocated (= particles allocated by the calling system) in case more are available in the buffer +void updateUsedParticles(const uint32_t allocated, const uint32_t available, const uint8_t percentage, uint32_t &used) { + uint32_t wantsToUse = 1 + ((allocated * ((uint32_t)percentage + 1)) >> 8); // always give 1 particle minimum + used = max((uint32_t)2, min(available, wantsToUse)); // limit to available particles, use a minimum of 2 +} + +// check if a segment is fully overlapping with an underlying segment (used to enable overlay rendering i.e. adding instead of overwriting pixels) +bool segmentIsOverlay(void) { // TODO: this only needs to be checked when segment is created, could move this to segment class or PS init + unsigned segID = strip.getCurrSegmentId(); + if(segID > 0) { // lower number segments exist, check coordinates of underlying segments + unsigned xMin, yMin = 0xFFFF; // note: overlay is activated even if there is gaps in underlying segments, this is an intentional choice + unsigned xMax, yMax = 0; + for (unsigned i = 0; i < segID; i++) { + xMin = strip._segments[i].start < xMin ? strip._segments[i].start : xMin; + yMin = strip._segments[i].startY < yMin ? strip._segments[i].startY : yMin; + xMax = strip._segments[i].stop > xMax ? strip._segments[i].stop : xMax; + yMax = strip._segments[i].stopY > yMax ? strip._segments[i].stopY : yMax; + if(xMin <= strip._segments[segID].start && xMax >= strip._segments[segID].stop && + yMin <= strip._segments[segID].startY && yMax >= strip._segments[segID].stopY) + return true; + } + } + return false; +} + +// get the pointer to the particle memory for the segment +partMem* getPartMem(void) { + uint8_t segID = strip.getCurrSegmentId(); + for (partMem &pmem : partMemList) { + if (pmem.id == segID) { + return &pmem; + } + } + return nullptr; +} + +// function to update the framebuffer and renderbuffer +void updateRenderingBuffer(uint32_t requiredpixels, bool isFramebuffer, bool initialize) { + PSPRINTLN("updateRenderingBuffer"); + uint16_t& targetBufferSize = isFramebuffer ? frameBufferSize : renderBufferSize; // corresponding buffer size + + // if(isFramebuffer) return; // debug/testing only: disable frame-buffer + + if(targetBufferSize < requiredpixels) { // check current buffer size + CRGB** targetBuffer = isFramebuffer ? &framebuffer : &renderbuffer; // pointer to target buffer + if(*targetBuffer || initialize) { // update only if initilizing or if buffer exists (prevents repeatet allocation attempts if initial alloc failed) + if(*targetBuffer) // buffer exists, free it + deallocatePSmemory((void*)(*targetBuffer), targetBufferSize * sizeof(CRGB)); + *targetBuffer = reinterpret_cast(allocatePSmemory(requiredpixels * sizeof(CRGB), false)); + if(*targetBuffer) + targetBufferSize = requiredpixels; + else + targetBufferSize = 0; + } + } +} + +// service the particle system memory, free memory if idle too long +// note: doing it this way makes it independent of the implementation of segment management but is not the most memory efficient way +void servicePSmem() { + // Increment watchdog for each entry and deallocate if idle too long (i.e. no PS running on that segment) + if(partMemList.size() > 0) { + for (size_t i = 0; i < partMemList.size(); i++) { + if(strip.getSegmentsNum() > i) { // segment still exists + if(strip._segments[i].freeze) continue; // skip frozen segments (incrementing watchdog will delete memory, leading to crash) + } + partMemList[i].watchdog++; // Increment watchdog counter + PSPRINT("pmem servic. list size: "); + PSPRINT(partMemList.size()); + PSPRINT(" element: "); + PSPRINT(i); + PSPRINT(" watchdog: "); + PSPRINTLN(partMemList[i].watchdog); + if (partMemList[i].watchdog > MAX_MEMIDLE) { + deallocatePSmemory(partMemList[i].particleMemPointer, partMemList[i].buffersize); // Free memory + partMemList.erase(partMemList.begin() + i); // Remove entry + //partMemList.shrink_to_fit(); // partMemList is small, memory operations should be unproblematic (this may lead to mem fragmentation, removed for now) + } + } + } + else { // no particle system running, release buffer memory + if(framebuffer) { + deallocatePSmemory((void*)framebuffer, frameBufferSize * sizeof(CRGB)); // free the buffers + framebuffer = nullptr; + frameBufferSize = 0; + } + if(renderbuffer) { + deallocatePSmemory((void*)renderbuffer, renderBufferSize * sizeof(CRGB)); + renderbuffer = nullptr; + renderBufferSize = 0; + } + } +} + +// transfer the frame buffer to the segment and handle transitional rendering (both FX render to the same buffer so they mix) +void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer) { + if(!framebuffer) return; // no buffer, nothing to transfer + PSPRINT(" xfer buf "); + #ifndef WLED_DISABLE_MODE_BLEND + bool tempBlend = SEGMENT.getmodeBlend(); + if(pmem->inTransition && blendingStyle == BLEND_STYLE_FADE) { + SEGMENT.modeBlend(false); // temporarily disable FX blending in PS to PS transition (using local buffer to do PS blending) + } + #endif + if(height) { // is 2D, 1D passes height = 0 + for (uint32_t y = 0; y < height; y++) { + int index = y * width; // current row index for 1D buffer + for (uint32_t x = 0; x < width; x++) { + CRGB *c = &framebuffer[index++]; + uint32_t clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color + if(useAdditiveTransfer) { + uint32_t segmentcolor = SEGMENT.getPixelColorXY((int)x, (int)y); + CRGB segmentRGB = CRGB(segmentcolor); + if(clr == 0) // frame buffer is black, just update the framebuffer + *c = segmentRGB; + else { // color to add to segment is not black + if(segmentcolor) { + fast_color_add(*c, segmentRGB); // add segment color back to buffer if not black + clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color (again) and set the segment + } + SEGMENT.setPixelColorXY((int)x, (int)y, clr); // save back to segment after adding local buffer + } + } + //if(clr > 0) // not black TODO: not transferring black is faster and enables overlay, but requires proper handling of buffer clearing, which is quite complex and probably needs a change to SEGMENT handling. + else + SEGMENT.setPixelColorXY((int)x, (int)y, clr); + } + } + } else { // 1D system + for (uint32_t x = 0; x < width; x++) { + CRGB *c = &framebuffer[x]; + uint32_t clr = RGBW32(c->r,c->g,c->b,0); + if(useAdditiveTransfer) { + uint32_t segmentcolor = SEGMENT.getPixelColor((int)x);; + CRGB segmentRGB = CRGB(segmentcolor); + if(clr == 0) // frame buffer is black, just load the color (for next frame) + *c = segmentRGB; + else { // color to add to segment is not black + if(segmentcolor) { + fast_color_add(*c, segmentRGB); // add segment color back to buffer if not black + clr = RGBW32(c->r,c->g,c->b,0); // convert to 32bit color (again) + } + SEGMENT.setPixelColor((int)x, clr); // save back to segment after adding local buffer + } + } + //if(color > 0) // not black + else + SEGMENT.setPixelColor((int)x, clr); + } + } + #ifndef WLED_DISABLE_MODE_BLEND + SEGMENT.modeBlend(tempBlend); // restore blending mode + #endif +} + +#endif // !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) diff --git a/wled00/FXparticleSystem.h b/wled00/FXparticleSystem.h new file mode 100644 index 0000000000..23f5aae985 --- /dev/null +++ b/wled00/FXparticleSystem.h @@ -0,0 +1,416 @@ +/* + FXparticleSystem.cpp + + Particle system with functions for particle generation, particle movement and particle rendering to RGB matrix. + by DedeHai (Damian Schneider) 2013-2024 + + Copyright (c) 2024 Damian Schneider + Licensed under the EUPL v. 1.2 or later +*/ + +#ifdef WLED_DISABLE_2D +#define WLED_DISABLE_PARTICLESYSTEM2D +#endif + +#if !(defined(WLED_DISABLE_PARTICLESYSTEM2D) && defined(WLED_DISABLE_PARTICLESYSTEM1D)) // not both disabled + +#include +#include "wled.h" + +#define PS_P_MAXSPEED 120 // maximum speed a particle can have (vx/vy is int8) +#define MAX_MEMIDLE 10 // max idle time (in frames) before memory is deallocated (if deallocated during an effect, it will crash!) + +//#define WLED_DEBUG_PS // note: enabling debug uses ~3k of flash + +#ifdef WLED_DEBUG_PS + #define PSPRINT(x) Serial.print(x) + #define PSPRINTLN(x) Serial.println(x) +#else + #define PSPRINT(x) + #define PSPRINTLN(x) +#endif + +// memory and transition manager +struct partMem { + void* particleMemPointer; // pointer to particle memory + uint32_t buffersize; // buffer size in bytes + uint8_t particleType; // type of particles currently in memory: 0 = none, particle struct size otherwise (required for 1D<->2D transitions) + uint8_t id; // ID of segment this memory belongs to + uint8_t watchdog; // counter to handle deallocation + uint8_t inTransition; // to track PS to PS FX transitions (is set to new FX ID during transitions), not set if not both FX are PS FX + uint8_t currentFX; // current FX ID, is set when transition is complete, used to detect back and forth transitions + bool finalTransfer; // used to update buffer in rendering function after transition has ended + bool transferParticles; // if set, particles in buffer are transferred to new FX +}; + +void* particleMemoryManager(const uint32_t requestedParticles, size_t structSize, uint32_t &availableToPS, uint32_t numParticlesUsed, const uint8_t effectID); // update particle memory pointer, handles memory transitions +void particleHandover(void *buffer, size_t structSize, int32_t numParticles); +void updateUsedParticles(const uint32_t allocated, const uint32_t available, const uint8_t percentage, uint32_t &used); +bool segmentIsOverlay(void); // check if segment is fully overlapping with at least one underlying segment +partMem* getPartMem(void); // returns pointer to memory struct for current segment or nullptr +void updateRenderingBuffer(uint32_t requiredpixels, bool isFramebuffer, bool initialize); // allocate CRGB rendering buffer, update size if needed +void transferBuffer(uint32_t width, uint32_t height, bool useAdditiveTransfer = false); // transfer the buffer to the segment (supports 1D and 2D) +void servicePSmem(); // increments watchdog, frees memory if idle too long + +// limit speed of particles (used in 1D and 2D) +static inline int32_t limitSpeed(const int32_t speed) { + return speed > PS_P_MAXSPEED ? PS_P_MAXSPEED : (speed < -PS_P_MAXSPEED ? -PS_P_MAXSPEED : speed); // note: this is slightly faster than using min/max at the cost of 50bytes of flash +} +#endif + +#ifndef WLED_DISABLE_PARTICLESYSTEM2D +// memory allocation +#define ESP8266_MAXPARTICLES 300 // enough up to 20x20 pixels +#define ESP8266_MAXSOURCES 24 +#define ESP32S2_MAXPARTICLES 1024 // enough up to 32x32 pixels +#define ESP32S2_MAXSOURCES 64 +#define ESP32_MAXPARTICLES 2048 // enough up to 64x32 pixels +#define ESP32_MAXSOURCES 128 + +// particle dimensions (subpixel division) +#define PS_P_RADIUS 64 // subpixel size, each pixel is divided by this for particle movement (must be a power of 2) +#define PS_P_HALFRADIUS (PS_P_RADIUS >> 1) +#define PS_P_RADIUS_SHIFT 6 // shift for RADIUS +#define PS_P_SURFACE 12 // shift: 2^PS_P_SURFACE = (PS_P_RADIUS)^2 +#define PS_P_MINHARDRADIUS 64 // minimum hard surface radius for collisions +#define PS_P_MINSURFACEHARDNESS 128 // minimum hardness used in collision impulse calculation, below this hardness, particles become sticky + +// struct for PS settings (shared for 1D and 2D class) +typedef union { + struct{ // one byte bit field for 2D settings + bool wrapX : 1; + bool wrapY : 1; + bool bounceX : 1; + bool bounceY : 1; + bool killoutofbounds : 1; // if set, out of bound particles are killed immediately + bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top + bool useCollisions : 1; + bool colorByAge : 1; // if set, particle hue is set by ttl value in render function + }; + byte asByte; // access as a byte, order is: LSB is first entry in the list above +} PSsettings2D; + +//struct for a single particle +typedef struct { // 10 bytes + int16_t x; // x position in particle system + int16_t y; // y position in particle system + uint16_t ttl; // time to live in frames + int8_t vx; // horizontal velocity + int8_t vy; // vertical velocity + uint8_t hue; // color hue + uint8_t sat; // particle color saturation +} PSparticle; + +//struct for particle flags note: this is separate from the particle struct to save memory (ram alignment) +typedef union { + struct { // 1 byte + bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area + bool collide : 1; // if set, particle takes part in collisions + bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) + bool custom1 : 1; // unused custom flags, can be used by FX to track particle states + bool custom2 : 1; + bool custom3 : 1; + bool custom4 : 1; + bool custom5 : 1; + }; + byte asByte; // access as a byte, order is: LSB is first entry in the list above +} PSparticleFlags; + +// struct for additional particle settings (option) +typedef struct { // 2 bytes + uint8_t size; // particle size, 255 means 10 pixels in diameter + uint8_t forcecounter; // counter for applying forces to individual particles +} PSadvancedParticle; + +// struct for advanced particle size control (option) +typedef struct { // 8 bytes + uint8_t asymmetry; // asymmetrical size (0=symmetrical, 255 fully asymmetric) + uint8_t asymdir; // direction of asymmetry, 64 is x, 192 is y (0 and 128 is symmetrical) + uint8_t maxsize; // target size for growing + uint8_t minsize; // target size for shrinking + uint8_t sizecounter : 4; // counters used for size contol (grow/shrink/wobble) + uint8_t wobblecounter : 4; + uint8_t growspeed : 4; + uint8_t shrinkspeed : 4; + uint8_t wobblespeed : 4; + bool grow : 1; // flags + bool shrink : 1; + bool pulsate : 1; // grows & shrinks & grows & ... + bool wobble : 1; // alternate x and y size +} PSsizeControl; + + +//struct for a particle source (20 bytes) +typedef struct { + uint16_t minLife; // minimum ttl of emittet particles + uint16_t maxLife; // maximum ttl of emitted particles + PSparticle source; // use a particle as the emitter source (speed, position, color) + PSparticleFlags sourceFlags; // flags for the source particle + int8_t var; // variation of emitted speed (adds random(+/- var) to speed) + int8_t vx; // emitting speed + int8_t vy; + uint8_t size; // particle size (advanced property) +} PSsource; + +// class uses approximately 60 bytes +class ParticleSystem2D { +public: + ParticleSystem2D(const uint32_t width, const uint32_t height, const uint32_t numberofparticles, const uint32_t numberofsources, const bool isadvanced = false, const bool sizecontrol = false); // constructor + // note: memory is allcated in the FX function, no deconstructor needed + void update(void); //update the particles according to set options and render to the matrix + void updateFire(const uint8_t intensity, const bool renderonly); // update function for fire, if renderonly is set, particles are not updated (required to fix transitions with frameskips) + void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions + void particleMoveUpdate(PSparticle &part, PSparticleFlags &partFlags, PSsettings2D *options = NULL, PSadvancedParticle *advancedproperties = NULL); // move function + // particle emitters + int32_t sprayEmit(const PSsource &emitter); + void flameEmit(const PSsource &emitter); + int32_t angleEmit(PSsource& emitter, const uint16_t angle, const int32_t speed); + //particle physics + void applyGravity(PSparticle &part); // applies gravity to single particle (use this for sources) + [[gnu::hot]] void applyForce(PSparticle &part, const int8_t xforce, const int8_t yforce, uint8_t &counter); + [[gnu::hot]] void applyForce(const uint32_t particleindex, const int8_t xforce, const int8_t yforce); // use this for advanced property particles + void applyForce(const int8_t xforce, const int8_t yforce); // apply a force to all particles + void applyAngleForce(PSparticle &part, const int8_t force, const uint16_t angle, uint8_t &counter); + void applyAngleForce(const uint32_t particleindex, const int8_t force, const uint16_t angle); // use this for advanced property particles + void applyAngleForce(const int8_t force, const uint16_t angle); // apply angular force to all particles + void applyFriction(PSparticle &part, const int32_t coefficient); // apply friction to specific particle + void applyFriction(const int32_t coefficient); // apply friction to all used particles + void pointAttractor(const uint32_t particleindex, PSparticle &attractor, const uint8_t strength, const bool swallow); + // set options note: inlining the set function uses more flash so dont optimize + void setUsedParticles(const uint8_t percentage); // set the percentage of particles used in the system, 255=100% + inline uint32_t getAvailableParticles(void) { return availableParticles; } // available particles in the buffer, use this to check if buffer changed during FX init + void setCollisionHardness(const uint8_t hardness); // hardness for particle collisions (255 means full hard) + void setWallHardness(const uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set + void setWallRoughness(const uint8_t roughness); // wall roughness randomizes wall collisions + void setMatrixSize(const uint32_t x, const uint32_t y); + void setWrapX(const bool enable); + void setWrapY(const bool enable); + void setBounceX(const bool enable); + void setBounceY(const bool enable); + void setKillOutOfBounds(const bool enable); // if enabled, particles outside of matrix instantly die + void setSaturation(const uint8_t sat); // set global color saturation + void setColorByAge(const bool enable); + void setMotionBlur(const uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero + void setSmearBlur(const uint8_t bluramount); // enable 2D smeared blurring of full frame + void setParticleSize(const uint8_t size); + void setGravity(const int8_t force = 8); + void enableParticleCollisions(const bool enable, const uint8_t hardness = 255); + + PSparticle *particles; // pointer to particle array + PSparticleFlags *particleFlags; // pointer to particle flags array + PSsource *sources; // pointer to sources + PSadvancedParticle *advPartProps; // pointer to advanced particle properties (can be NULL) + PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL) + uint8_t* PSdataEnd; // points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data + int32_t maxX, maxY; // particle system size i.e. width-1 / height-1 in subpixels, Note: all "max" variables must be signed to compare to coordinates (which are signed) + int32_t maxXpixel, maxYpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 / height-1 + uint32_t numSources; // number of sources + uint32_t usedParticles; // number of particles used in animation, is relative to 'numParticles' + //note: some variables are 32bit for speed and code size at the cost of ram + +private: + //rendering functions + void ParticleSys_render(); + [[gnu::hot]] void renderParticle(const uint32_t particleindex, const uint32_t brightness, const CRGB& color, const bool wrapX, const bool wrapY); + //paricle physics applied by system if flags are set + void applyGravity(); // applies gravity to all particles + void handleCollisions(); + [[gnu::hot]] void collideParticles(PSparticle &particle1, PSparticle &particle2, const int32_t dx, const int32_t dy, const int32_t collDistSq); + void fireParticleupdate(); + //utility functions + void updatePSpointers(const bool isadvanced, const bool sizecontrol); // update the data pointers to current segment data space + bool updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control + void getParticleXYsize(PSadvancedParticle *advprops, PSsizeControl *advsize, uint32_t &xsize, uint32_t &ysize); + [[gnu::hot]] void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition); // bounce on a wall + // note: variables that are accessed often are 32bit for speed + PSsettings2D particlesettings; // settings used when updating particles (can also used by FX to move sources), do not edit properties directly, use functions above + uint32_t numParticles; // total number of particles allocated by this system note: during transitions, less are available, use availableParticles + uint32_t availableParticles; // number of particles available for use (can be more or less than numParticles, assigned by memory manager) + uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster + int32_t collisionHardness; + uint32_t wallHardness; + uint32_t wallRoughness; // randomizes wall collisions + uint32_t particleHardRadius; // hard surface radius of a particle, used for collision detection (32bit for speed) + uint16_t collisionStartIdx; // particle array start index for collision detection + uint8_t fireIntesity = 0; // fire intensity, used for fire mode (flash use optimization, better than passing an argument to render function) + uint8_t fractionOfParticlesUsed; // percentage of particles used in the system (255=100%), used during transition updates + uint8_t forcecounter; // counter for globally applied forces + uint8_t gforcecounter; // counter for global gravity + int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) + // global particle properties for basic particles + uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels, 255 = 10 pixels (note: this is also added to individual sized particles) + uint8_t motionBlur; // motion blur, values > 100 gives smoother animations. Note: motion blurring does not work if particlesize is > 0 + uint8_t smearBlur; // 2D smeared blurring of full frame + uint8_t effectID; // ID of the effect that is using this particle system, used for transitions +}; + +void blur2D(CRGB *colorbuffer, const uint32_t xsize, uint32_t ysize, const uint32_t xblur, const uint32_t yblur, const uint32_t xstart = 0, uint32_t ystart = 0, const bool isparticle = false); +// initialization functions (not part of class) +bool initParticleSystem2D(ParticleSystem2D *&PartSys, const uint32_t requestedsources, const uint32_t additionalbytes = 0, const bool advanced = false, const bool sizecontrol = false); +uint32_t calculateNumberOfParticles2D(const uint32_t pixels, const bool advanced, const bool sizecontrol); +uint32_t calculateNumberOfSources2D(const uint32_t pixels, const uint32_t requestedsources); +bool allocateParticleSystemMemory2D(const uint32_t numparticles, const uint32_t numsources, const bool advanced, const bool sizecontrol, const uint32_t additionalbytes); +#endif // WLED_DISABLE_PARTICLESYSTEM2D + +//////////////////////// +// 1D Particle System // +//////////////////////// +#ifndef WLED_DISABLE_PARTICLESYSTEM1D +// memory allocation +#define ESP8266_MAXPARTICLES_1D 450 +#define ESP8266_MAXSOURCES_1D 16 +#define ESP32S2_MAXPARTICLES_1D 1300 +#define ESP32S2_MAXSOURCES_1D 32 +#define ESP32_MAXPARTICLES_1D 2600 +#define ESP32_MAXSOURCES_1D 64 + +// particle dimensions (subpixel division) +#define PS_P_RADIUS_1D 32 // subpixel size, each pixel is divided by this for particle movement, if this value is changed, also change the shift defines (next two lines) +#define PS_P_HALFRADIUS_1D (PS_P_RADIUS_1D >> 1) +#define PS_P_RADIUS_SHIFT_1D 5 // 1 << PS_P_RADIUS_SHIFT = PS_P_RADIUS +#define PS_P_SURFACE_1D 5 // shift: 2^PS_P_SURFACE = PS_P_RADIUS_1D +#define PS_P_MINHARDRADIUS_1D 32 // minimum hard surface radius note: do not change or hourglass effect will be broken +#define PS_P_MINSURFACEHARDNESS_1D 120 // minimum hardness used in collision impulse calculation + +// struct for PS settings (shared for 1D and 2D class) +typedef union { + struct{ + // one byte bit field for 1D settings + bool wrap : 1; + bool bounce : 1; + bool killoutofbounds : 1; // if set, out of bound particles are killed immediately + bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top + bool useCollisions : 1; + bool colorByAge : 1; // if set, particle hue is set by ttl value in render function + bool colorByPosition : 1; // if set, particle hue is set by its position in the strip segment + bool unused : 1; + }; + byte asByte; // access as a byte, order is: LSB is first entry in the list above +} PSsettings1D; + +//struct for a single particle (8 bytes) +typedef struct { + int32_t x; // x position in particle system + uint16_t ttl; // time to live in frames + int8_t vx; // horizontal velocity + uint8_t hue; // color hue +} PSparticle1D; + +//struct for particle flags +typedef union { + struct { // 1 byte + bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area + bool collide : 1; // if set, particle takes part in collisions + bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds) + bool reversegrav : 1; // if set, gravity is reversed on this particle + bool forcedirection : 1; // direction the force was applied, 1 is positive x-direction (used for collision stacking, similar to reversegrav) TODO: not used anymore, can be removed + bool fixed : 1; // if set, particle does not move (and collisions make other particles revert direction), + bool custom1 : 1; // unused custom flags, can be used by FX to track particle states + bool custom2 : 1; + }; + byte asByte; // access as a byte, order is: LSB is first entry in the list above +} PSparticleFlags1D; + +// struct for additional particle settings (optional) +typedef struct { + uint8_t sat; //color saturation + uint8_t size; // particle size, 255 means 10 pixels in diameter + uint8_t forcecounter; +} PSadvancedParticle1D; + +//struct for a particle source (20 bytes) +typedef struct { + uint16_t minLife; // minimum ttl of emittet particles + uint16_t maxLife; // maximum ttl of emitted particles + PSparticle1D source; // use a particle as the emitter source (speed, position, color) + PSparticleFlags1D sourceFlags; // flags for the source particle + int8_t var; // variation of emitted speed (adds random(+/- var) to speed) + int8_t v; // emitting speed + uint8_t sat; // color saturation (advanced property) + uint8_t size; // particle size (advanced property) + // note: there is 3 bytes of padding added here +} PSsource1D; + +class ParticleSystem1D +{ +public: + ParticleSystem1D(const uint32_t length, const uint32_t numberofparticles, const uint32_t numberofsources, const bool isadvanced = false); // constructor + // note: memory is allcated in the FX function, no deconstructor needed + void update(void); //update the particles according to set options and render to the matrix + void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions + // particle emitters + int32_t sprayEmit(const PSsource1D &emitter); + void particleMoveUpdate(PSparticle1D &part, PSparticleFlags1D &partFlags, PSsettings1D *options = NULL, PSadvancedParticle1D *advancedproperties = NULL); // move function + //particle physics + [[gnu::hot]] void applyForce(PSparticle1D &part, const int8_t xforce, uint8_t &counter); //apply a force to a single particle + void applyForce(const int8_t xforce); // apply a force to all particles + void applyGravity(PSparticle1D &part, PSparticleFlags1D &partFlags); // applies gravity to single particle (use this for sources) + void applyFriction(const int32_t coefficient); // apply friction to all used particles + // set options + void setUsedParticles(const uint8_t percentage); // set the percentage of particles used in the system, 255=100% + inline uint32_t getAvailableParticles(void) { return availableParticles; } // available particles in the buffer, use this to check if buffer changed during FX init + void setWallHardness(const uint8_t hardness); // hardness for bouncing on the wall if bounceXY is set + void setSize(const uint32_t x); //set particle system size (= strip length) + void setWrap(const bool enable); + void setBounce(const bool enable); + void setKillOutOfBounds(const bool enable); // if enabled, particles outside of matrix instantly die + // void setSaturation(uint8_t sat); // set global color saturation + void setColorByAge(const bool enable); + void setColorByPosition(const bool enable); + void setMotionBlur(const uint8_t bluramount); // note: motion blur can only be used if 'particlesize' is set to zero + void setSmearBlur(const uint8_t bluramount); // enable 1D smeared blurring of full frame + void setParticleSize(const uint8_t size); //size 0 = 1 pixel, size 1 = 2 pixels, is overruled by advanced particle size + void setGravity(int8_t force = 8); + void enableParticleCollisions(bool enable, const uint8_t hardness = 255); + + PSparticle1D *particles; // pointer to particle array + PSparticleFlags1D *particleFlags; // pointer to particle flags array + PSsource1D *sources; // pointer to sources + PSadvancedParticle1D *advPartProps; // pointer to advanced particle properties (can be NULL) + //PSsizeControl *advPartSize; // pointer to advanced particle size control (can be NULL) + uint8_t* PSdataEnd; // points to first available byte after the PSmemory, is set in setPointers(). use this for FX custom data + int32_t maxX; // particle system size i.e. width-1, Note: all "max" variables must be signed to compare to coordinates (which are signed) + int32_t maxXpixel; // last physical pixel that can be drawn to (FX can read this to read segment size if required), equal to width-1 + uint32_t numSources; // number of sources + uint32_t usedParticles; // number of particles used in animation, is relative to 'numParticles' + +private: + //rendering functions + void ParticleSys_render(void); + void renderParticle(const uint32_t particleindex, const uint32_t brightness, const CRGB &color, const bool wrap); + + //paricle physics applied by system if flags are set + void applyGravity(); // applies gravity to all particles + void handleCollisions(); + [[gnu::hot]] void collideParticles(PSparticle1D &particle1, const PSparticleFlags1D &particle1flags, PSparticle1D &particle2, const PSparticleFlags1D &particle2flags, int32_t dx, int32_t relativeVx, const int32_t collisiondistance); + + //utility functions + void updatePSpointers(const bool isadvanced); // update the data pointers to current segment data space + //void updateSize(PSadvancedParticle *advprops, PSsizeControl *advsize); // advanced size control + [[gnu::hot]] void bounce(int8_t &incomingspeed, int8_t ¶llelspeed, int32_t &position, const uint32_t maxposition); // bounce on a wall + // note: variables that are accessed often are 32bit for speed + PSsettings1D particlesettings; // settings used when updating particles + uint32_t numParticles; // total number of particles allocated by this system note: never use more than this, even if more are available (only this many advanced particles are allocated) + uint32_t availableParticles; // number of particles available for use (can be more or less than numParticles, assigned by memory manager) + uint8_t fractionOfParticlesUsed; // percentage of particles used in the system (255=100%), used during transition updates + uint32_t emitIndex; // index to count through particles to emit so searching for dead pixels is faster + int32_t collisionHardness; + uint32_t particleHardRadius; // hard surface radius of a particle, used for collision detection + uint32_t wallHardness; + uint8_t gforcecounter; // counter for global gravity + int8_t gforce; // gravity strength, default is 8 (negative is allowed, positive is downwards) + uint8_t forcecounter; // counter for globally applied forces + uint16_t collisionStartIdx; // particle array start index for collision detection + //global particle properties for basic particles + uint8_t particlesize; // global particle size, 0 = 1 pixel, 1 = 2 pixels + uint8_t motionBlur; // enable motion blur, values > 100 gives smoother animations + uint8_t smearBlur; // smeared blurring of full frame + uint8_t effectID; // ID of the effect that is using this particle system, used for transitions +}; + +bool initParticleSystem1D(ParticleSystem1D *&PartSys, const uint32_t requestedsources, const uint8_t fractionofparticles = 255, const uint32_t additionalbytes = 0, const bool advanced = false); +uint32_t calculateNumberOfParticles1D(const uint32_t fraction, const bool isadvanced); +uint32_t calculateNumberOfSources1D(const uint32_t requestedsources); +bool allocateParticleSystemMemory1D(const uint32_t numparticles, const uint32_t numsources, const bool isadvanced, const uint32_t additionalbytes); +void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, uint32_t start); +#endif // WLED_DISABLE_PARTICLESYSTEM1D diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 6e159a82b2..3abf61412b 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -16,6 +16,9 @@ #define LEDC_MUTEX_UNLOCK() #endif #endif +#ifdef ESP8266 +#include "core_esp8266_waveform.h" +#endif #include "const.h" #include "pin_manager.h" #include "bus_wrapper.h" @@ -462,10 +465,7 @@ BusPwm::BusPwm(const BusConfig &bc) for (unsigned i = 0; i < numPins; i++) pins[i] = {(int8_t)bc.pins[i], true}; if (!PinManager::allocateMultiplePins(pins, numPins, PinOwner::BusPwm)) return; -#ifdef ESP8266 - analogWriteRange((1<<_depth)-1); - analogWriteFreq(_frequency); -#else +#ifdef ARDUINO_ARCH_ESP32 // for 2 pin PWM CCT strip pinManager will make sure both LEDC channels are in the same speed group and sharing the same timer _ledcStart = PinManager::allocateLedc(numPins); if (_ledcStart == 255) { //no more free LEDC channels @@ -556,13 +556,19 @@ uint32_t BusPwm::getPixelColor(unsigned pix) const { void BusPwm::show() { if (!_valid) return; + const unsigned numPins = getPins(); +#ifdef ESP8266 + const unsigned analogPeriod = F_CPU / _frequency; + const unsigned maxBri = analogPeriod; // compute to clock cycle accuracy + constexpr bool dithering = false; + constexpr unsigned bitShift = 8; // 256 clocks for dead time, ~3us at 80MHz +#else // if _needsRefresh is true (UI hack) we are using dithering (credit @dedehai & @zalatnaicsongor) // https://github.com/Aircoookie/WLED/pull/4115 and https://github.com/zalatnaicsongor/WLED/pull/1) const bool dithering = _needsRefresh; // avoid working with bitfield - const unsigned numPins = getPins(); const unsigned maxBri = (1<<_depth); // possible values: 16384 (14), 8192 (13), 4096 (12), 2048 (11), 1024 (10), 512 (9) and 256 (8) - [[maybe_unused]] const unsigned bitShift = dithering * 4; // if dithering, _depth is 12 bit but LEDC channel is set to 8 bit (using 4 fractional bits) - + const unsigned bitShift = dithering * 4; // if dithering, _depth is 12 bit but LEDC channel is set to 8 bit (using 4 fractional bits) +#endif // use CIE brightness formula (linear + cubic) to approximate human eye perceived brightness // see: https://en.wikipedia.org/wiki/Lightness unsigned pwmBri = _bri; @@ -582,20 +588,25 @@ void BusPwm::show() { // Phase shifting requires that LEDC timers are synchronised (see setup()). For PWM CCT (and H-bridge) it is // also mandatory that both channels use the same timer (pinManager takes care of that). for (unsigned i = 0; i < numPins; i++) { - unsigned duty = (_data[i] * pwmBri) / 255; - #ifdef ESP8266 - if (_reversed) duty = maxBri - duty; - analogWrite(_pins[i], duty); - #else - int deadTime = 0; + unsigned duty = (_data[i] * pwmBri) / 255; + unsigned deadTime = 0; + if (_type == TYPE_ANALOG_2CH && Bus::getCCTBlend() == 0) { // add dead time between signals (when using dithering, two full 8bit pulses are required) deadTime = (1+dithering) << bitShift; // we only need to take care of shortening the signal at (almost) full brightness otherwise pulses may overlap - if (_bri >= 254 && duty >= maxBri / 2 && duty < maxBri) duty -= deadTime << 1; // shorten duty of larger signal except if full on - if (_reversed) deadTime = -deadTime; // need to invert dead time to make phaseshift go the opposite way so low signals dont overlap + if (_bri >= 254 && duty >= maxBri / 2 && duty < maxBri) { + duty -= deadTime << 1; // shorten duty of larger signal except if full on + } } - if (_reversed) duty = maxBri - duty; + if (_reversed) { + if (i) hPoint += duty; // align start at time zero + duty = maxBri - duty; + } + #ifdef ESP8266 + //stopWaveform(_pins[i]); // can cause the waveform to miss a cycle. instead we risk crossovers. + startWaveformClockCycles(_pins[i], duty, analogPeriod - duty, 0, i ? _pins[0] : -1, hPoint, false); + #else unsigned channel = _ledcStart + i; unsigned gr = channel/8; // high/low speed group unsigned ch = channel%8; // group channel @@ -604,9 +615,11 @@ void BusPwm::show() { LEDC.channel_group[gr].channel[ch].duty.duty = duty << ((!dithering)*4); // lowest 4 bits are used for dithering, shift by 4 bits if not using dithering LEDC.channel_group[gr].channel[ch].hpoint.hpoint = hPoint >> bitShift; // hPoint is at _depth resolution (needs shifting if dithering) ledc_update_duty((ledc_mode_t)gr, (ledc_channel_t)ch); - hPoint += duty + deadTime; // offset to cascade the signals - if (hPoint >= maxBri) hPoint = 0; // offset it out of bounds, reset #endif + + if (!_reversed) hPoint += duty; + hPoint += deadTime; // offset to cascade the signals + if (hPoint >= maxBri) hPoint -= maxBri; // offset is out of bounds, reset } } diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 00cfc60d7e..807948460b 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -1234,4 +1234,4 @@ void serializeConfigSec() { if (f) serializeJson(root, f); f.close(); releaseJSONBufferLock(); -} +} \ No newline at end of file diff --git a/wled00/const.h b/wled00/const.h index 3f82219d31..1ebcb9397d 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -560,8 +560,25 @@ #endif #endif -//#define MIN_HEAP_SIZE (8k for AsyncWebServer) -#define MIN_HEAP_SIZE 8192 +//#define MIN_HEAP_SIZE +#define MIN_HEAP_SIZE 2048 + +// Web server limits +#ifdef ESP8266 +// Minimum heap to consider handling a request +#define WLED_REQUEST_MIN_HEAP (8*1024) +// Estimated maximum heap required by any one request +#define WLED_REQUEST_HEAP_USAGE (6*1024) +#else +// ESP32 TCP stack needs much more RAM than ESP8266 +// Minimum heap remaining before queuing a request +#define WLED_REQUEST_MIN_HEAP (12*1024) +// Estimated maximum heap required by any one request +#define WLED_REQUEST_HEAP_USAGE (12*1024) +#endif +// Maximum number of requests in queue; absolute cap on web server resource usage. +// Websockets do not count against this limit. +#define WLED_REQUEST_MAX_QUEUE 6 // Maximum size of node map (list of other WLED instances) #ifdef ESP8266 diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index d46fd642da..9f7dd81f14 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -219,6 +219,19 @@ void onHueConnect(void* arg, AsyncClient* client); void sendHuePoll(); void onHueData(void* arg, AsyncClient* client, void *data, size_t len); +#include "FX.h" // must be below colors.cpp declarations (potentially due to duplicate declarations of e.g. color_blend) + +//image_loader.cpp +#ifdef WLED_ENABLE_GIF +bool fileSeekCallback(unsigned long position); +unsigned long filePositionCallback(void); +int fileReadCallback(void); +int fileReadBlockCallback(void * buffer, int numberOfBytes); +int fileSizeCallback(void); +byte renderImageToSegment(Segment &seg); +void endImagePlayback(Segment* seg); +#endif + //improv.cpp enum ImprovRPCType { Command_Wifi = 0x01, diff --git a/wled00/image_loader.cpp b/wled00/image_loader.cpp new file mode 100644 index 0000000000..9665057942 --- /dev/null +++ b/wled00/image_loader.cpp @@ -0,0 +1,144 @@ +#include "wled.h" + +#ifdef WLED_ENABLE_GIF + +#include "GifDecoder.h" + + +/* + * Functions to render images from filesystem to segments, used by the "Image" effect + */ + +File file; +char lastFilename[34] = "/"; +GifDecoder<320,320,12,true> decoder; +bool gifDecodeFailed = false; +unsigned long lastFrameDisplayTime = 0, currentFrameDelay = 0; + +bool fileSeekCallback(unsigned long position) { + return file.seek(position); +} + +unsigned long filePositionCallback(void) { + return file.position(); +} + +int fileReadCallback(void) { + return file.read(); +} + +int fileReadBlockCallback(void * buffer, int numberOfBytes) { + return file.read((uint8_t*)buffer, numberOfBytes); +} + +int fileSizeCallback(void) { + return file.size(); +} + +bool openGif(const char *filename) { + file = WLED_FS.open(filename, "r"); + + if (!file) return false; + return true; +} + +Segment* activeSeg; +uint16_t gifWidth, gifHeight; + +void screenClearCallback(void) { + activeSeg->fill(0); +} + +void updateScreenCallback(void) {} + +void drawPixelCallback(int16_t x, int16_t y, uint8_t red, uint8_t green, uint8_t blue) { + // simple nearest-neighbor scaling + int16_t outY = y * activeSeg->height() / gifHeight; + int16_t outX = x * activeSeg->width() / gifWidth; + // set multiple pixels if upscaling + for (int16_t i = 0; i < (activeSeg->width()+(gifWidth-1)) / gifWidth; i++) { + for (int16_t j = 0; j < (activeSeg->height()+(gifHeight-1)) / gifHeight; j++) { + activeSeg->setPixelColorXY(outX + i, outY + j, gamma8(red), gamma8(green), gamma8(blue)); + } + } +} + +#define IMAGE_ERROR_NONE 0 +#define IMAGE_ERROR_NO_NAME 1 +#define IMAGE_ERROR_SEG_LIMIT 2 +#define IMAGE_ERROR_UNSUPPORTED_FORMAT 3 +#define IMAGE_ERROR_FILE_MISSING 4 +#define IMAGE_ERROR_DECODER_ALLOC 5 +#define IMAGE_ERROR_GIF_DECODE 6 +#define IMAGE_ERROR_FRAME_DECODE 7 +#define IMAGE_ERROR_WAITING 254 +#define IMAGE_ERROR_PREV 255 + +// renders an image (.gif only; .bmp and .fseq to be added soon) from FS to a segment +byte renderImageToSegment(Segment &seg) { + if (!seg.name) return IMAGE_ERROR_NO_NAME; + // disable during effect transition, causes flickering, multiple allocations and depending on image, part of old FX remaining + if (seg.mode != seg.currentMode()) return IMAGE_ERROR_WAITING; + if (activeSeg && activeSeg != &seg) return IMAGE_ERROR_SEG_LIMIT; // only one segment at a time + activeSeg = &seg; + + if (strncmp(lastFilename +1, seg.name, 32) != 0) { // segment name changed, load new image + strncpy(lastFilename +1, seg.name, 32); + gifDecodeFailed = false; + if (strcmp(lastFilename + strlen(lastFilename) - 4, ".gif") != 0) { + gifDecodeFailed = true; + return IMAGE_ERROR_UNSUPPORTED_FORMAT; + } + if (file) file.close(); + openGif(lastFilename); + if (!file) { gifDecodeFailed = true; return IMAGE_ERROR_FILE_MISSING; } + decoder.setScreenClearCallback(screenClearCallback); + decoder.setUpdateScreenCallback(updateScreenCallback); + decoder.setDrawPixelCallback(drawPixelCallback); + decoder.setFileSeekCallback(fileSeekCallback); + decoder.setFilePositionCallback(filePositionCallback); + decoder.setFileReadCallback(fileReadCallback); + decoder.setFileReadBlockCallback(fileReadBlockCallback); + decoder.setFileSizeCallback(fileSizeCallback); + decoder.alloc(); + DEBUG_PRINTLN(F("Starting decoding")); + if(decoder.startDecoding() < 0) { gifDecodeFailed = true; return IMAGE_ERROR_GIF_DECODE; } + DEBUG_PRINTLN(F("Decoding started")); + } + + if (gifDecodeFailed) return IMAGE_ERROR_PREV; + if (!file) { gifDecodeFailed = true; return IMAGE_ERROR_FILE_MISSING; } + //if (!decoder) { gifDecodeFailed = true; return IMAGE_ERROR_DECODER_ALLOC; } + + // speed 0 = half speed, 128 = normal, 255 = full FX FPS + // TODO: 0 = 4x slow, 64 = 2x slow, 128 = normal, 192 = 2x fast, 255 = 4x fast + uint32_t wait = currentFrameDelay * 2 - seg.speed * currentFrameDelay / 128; + + // TODO consider handling this on FX level with a different frametime, but that would cause slow gifs to speed up during transitions + if (millis() - lastFrameDisplayTime < wait) return IMAGE_ERROR_WAITING; + + decoder.getSize(&gifWidth, &gifHeight); + + int result = decoder.decodeFrame(false); + if (result < 0) { gifDecodeFailed = true; return IMAGE_ERROR_FRAME_DECODE; } + + currentFrameDelay = decoder.getFrameDelay_ms(); + unsigned long tooSlowBy = (millis() - lastFrameDisplayTime) - wait; // if last frame was longer than intended, compensate + currentFrameDelay = tooSlowBy > currentFrameDelay ? 0 : currentFrameDelay - tooSlowBy; + lastFrameDisplayTime = millis(); + + return IMAGE_ERROR_NONE; +} + +void endImagePlayback(Segment *seg) { + DEBUG_PRINTLN(F("Image playback end called")); + if (!activeSeg || activeSeg != seg) return; + if (file) file.close(); + decoder.dealloc(); + gifDecodeFailed = false; + activeSeg = nullptr; + lastFilename[1] = '\0'; + DEBUG_PRINTLN(F("Image playback ended")); +} + +#endif \ No newline at end of file diff --git a/wled00/json.cpp b/wled00/json.cpp index 5fae9544ef..d4f0d77714 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -1060,7 +1060,7 @@ void serveJson(AsyncWebServerRequest* request) } if (!requestJSONBufferLock(17)) { - serveJsonError(request, 503, ERR_NOBUF); + request->deferResponse(); return; } // releaseJSONBufferLock() will be called when "response" is destroyed (from AsyncWebServer) diff --git a/wled00/set.cpp b/wled00/set.cpp index 88249d3c45..c0977f262c 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -628,7 +628,10 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) //USERMODS if (subPage == SUBPAGE_UM) { - if (!requestJSONBufferLock(5)) return; + if (!requestJSONBufferLock(5)) { + request->deferResponse(); + return; + } // global I2C & SPI pins int8_t hw_sda_pin = !request->arg(F("SDA")).length() ? -1 : (int)request->arg(F("SDA")).toInt(); diff --git a/wled00/wled.cpp b/wled00/wled.cpp index da1c33044c..dd260b1c26 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -303,6 +303,7 @@ void WLED::loop() DEBUG_PRINTF_P(PSTR("Strip time[ms]:%u/%lu\n"), avgStripMillis/loops, maxStripMillis); } strip.printSize(); + server.printStatus(DEBUGOUT); loops = 0; maxLoopMillis = 0; maxUsermodMillis = 0; diff --git a/wled00/wled.h b/wled00/wled.h index 3715466133..a18199446c 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -874,7 +874,7 @@ WLED_GLOBAL bool ledStatusState _INIT(false); // the current LED state #endif // server library objects -WLED_GLOBAL AsyncWebServer server _INIT_N(((80))); +WLED_GLOBAL AsyncWebServer server _INIT_N(((80, {0, WLED_REQUEST_MAX_QUEUE, WLED_REQUEST_MIN_HEAP, WLED_REQUEST_HEAP_USAGE}))); #ifdef WLED_ENABLE_WEBSOCKETS WLED_GLOBAL AsyncWebSocket ws _INIT_N((("/ws"))); #endif diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 8768b2b4e7..da7fd2a3aa 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -288,7 +288,7 @@ void initServer() bool isConfig = false; if (!requestJSONBufferLock(14)) { - serveJsonError(request, 503, ERR_NOBUF); + request->deferResponse(); return; }