From 65fa498e0cdf5083f02175d53fa7c3d268286639 Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr" Date: Tue, 2 Apr 2024 18:13:15 -0400 Subject: [PATCH 01/31] Install "Xmas Twinkle" effect. --- wled00/FX.cpp | 159 ++++++++++++++++++++++++++++++++++++++++++++++ wled00/FX.h | 3 +- wled00/FX_fcn.cpp | 2 +- 3 files changed, 162 insertions(+), 2 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 3f720385f2..40d2cacd4c 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7415,6 +7415,163 @@ uint16_t mode_2DAkemi(void) { static const char _data_FX_MODE_2DAKEMI[] PROGMEM = "Akemi@Color speed,Dance;Head palette,Arms & Legs,Eyes & Mouth;Face palette;2f;si=0"; //beatsin +///////////////////////// +// Xmas Twinkle // +///////////////////////// + +/* We need to keep data for each twinkle light. + * Except for the color, we smash all other data into a single + * uint32_t to keep memory short. We use time in centiseconds. + * Be careful to not overflow the limited size of these timers. */ +typedef struct XTwinkleLight { + uint8_t colorIdx; + uint32_t twData; + +// (Be aware of operator precedent when accessing & modifying.) +#define TWINKLE_ON 0x80000000 // 1 bit +#define TIME_TO_EVENT 0x7fe00000 // 10 bits >> 21 +#define TIME_TO_EVENT_SHIFT 21 +#define MAX_CYCLE 0x001ff800 // 10 bits >> 11 +#define MAX_CYCLE_SHIFT 11 +#define T_RETWINKLE 0x000007ff // 11 bits >> 0 +} XTwinkleLight; + +// For creating skewed random numbers toward the shorter end. +// The sum of percentages must = 100% +const uint16_t pSize = 20; +int16_t percentages[pSize] = {12, 11, 10, 10, 6, 6, 5, 5, 3, 3, 1, 1, 1, 1, 1, 1, 2, 3, 3, 15}; + +// Input is 0-100, Ouput is skewed 0-100. +// PArray may be any size, but elements must add up to 100. +// Note: Single precision floating point is just as fast on an ESP-32 as fixed arithmetic. +int32_t skewedRandom( int32_t rand100, + uint16_t pArraySize, + int16_t *pArray) +{ + int index = 0; + int cumulativePercentage = 0; + + // Find the range in the table based on randomValue. + while (index < pArraySize - 1 && rand100 >= cumulativePercentage + pArray[index]) { + cumulativePercentage += pArray[index]; + index++; + } + + // Calculate linear interpolation + float t = float((rand100 - cumulativePercentage) / float(pArray[index])); + int result = int((float(index) + t) * 100.0 / pArraySize); + + return result; +} + +uint16_t mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. + uint16_t numTwiklers = SEGLEN * SEGMENT.intensity / 255; + if (numTwiklers <= 0) + numTwiklers = 1; // Divide checks are not cool. + + // Reinitialize evertying if the number of twinklers has changed. + if (numTwiklers != SEGMENT.aux0) + SEGMENT.aux0 = 0; + + // The maximum twinkle time varies based on the time slider + int32_t maximumTime = (255 - SEGMENT.speed) * 900 / 256 + 100; // Between 100 & 1000 centiseconds + + // uint8_t flasherDistance = ((255 - SEGMENT.intensity) / 28) +1; //1-10 + // uint16_t numFlashers = (SEGLEN / flasherDistance) +1; + + uint16_t dataSize = sizeof(XTwinkleLight) * numTwiklers; + if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + XTwinkleLight* twinklers = reinterpret_cast(SEGENV.data); + + // Initialize the twinkle lights. + if (SEGMENT.aux0 == 0) + { + for (int i = 0; i < numTwiklers; ++i) + { + XTwinkleLight *light = &twinklers[i]; + + light->colorIdx = random8(); + light->twData = 0; // Everything 0 + int cycleTime = skewedRandom(random(100), pSize, percentages) * maximumTime / 100 + 20; + + light->twData |= cycleTime << MAX_CYCLE_SHIFT & MAX_CYCLE; + light->twData |= random(50, cycleTime) << TIME_TO_EVENT_SHIFT & TIME_TO_EVENT; + light->twData |= (random(2, 20) * 100) & T_RETWINKLE; // 2 - 20 seconds 1st time around + } + + SEGMENT.step = millis(); + SEGMENT.aux0 = numTwiklers; // Initialized. + } + + // Get the current time, handling overflows. + uint32_t lastTime = SEGMENT.step; + uint32_t currTime = millis(); + if (currTime < lastTime) + lastTime = 0; + + // We're doing our work in centiseconds so we don't overflow our 10 bit counters. + // The interval may be zero if the refresh rate is fast enought. + uint32_t interval = (currTime - lastTime) / 10; + + // Note the time passed to the LEDs, and process any events that occured. + for (int i = 0; i < numTwiklers; ++i) + { + XTwinkleLight *light = &twinklers[i]; + + // See if we are at the end of twinkle on o off cycle. + int16_t eventTime = ((light->twData & TIME_TO_EVENT) >> TIME_TO_EVENT_SHIFT) - interval; + if (eventTime <= 0) + { + // Twinkle on cycles are 1/3 length of twinkle off cycles. We're' twinkling after all. + if (light->twData & TWINKLE_ON) + eventTime += random(50, ((light->twData & MAX_CYCLE) >> MAX_CYCLE_SHIFT)); // turn OFF + else + eventTime += random(10, ((light->twData & MAX_CYCLE) >> MAX_CYCLE_SHIFT) / 3); // turn ON + + light->twData ^= TWINKLE_ON; + } + // Put the updated event time back. + light->twData = (light->twData & ~TIME_TO_EVENT) | (eventTime << TIME_TO_EVENT_SHIFT & TIME_TO_EVENT); + + // See if we are at the end of a major cycle, recalculate the max cycle time. + int16_t cycleTime = (light->twData & T_RETWINKLE) - interval; + if (cycleTime <= 0) + { + int maxTime = skewedRandom(random(100), pSize, percentages) * maximumTime / 100 + 20; + light->twData = (light->twData & ~MAX_CYCLE) | (maxTime << MAX_CYCLE_SHIFT & MAX_CYCLE); + cycleTime += 2000; // 20 seconds + } + light->twData = (light->twData & ~T_RETWINKLE) | (cycleTime & T_RETWINKLE); + } + + // Remember the last time as ms. + SEGMENT.step += interval * 10; + + // Turm off all the LEDS. + for (int i = 0; i < SEGLEN; ++i) + SEGMENT.setPixelColor(i, CRGB::Black); + + // Turn on only those leds that should be. + for (int i = 0; i < numTwiklers; ++i) + { + XTwinkleLight *light = &twinklers[i]; + + if ((light->twData & TWINKLE_ON) == 0) + continue; + + // Compute the offset of the light in the string. + short inset = i * SEGLEN / numTwiklers; + if (inset > SEGLEN) // Safety + break; + + SEGMENT.setPixelColor(inset, CRGB(SEGMENT.color_wheel(light->colorIdx))); + } + + return FRAMETIME; +} // mode_XmasTwinkle +static const char _data_FX_MODE_XMASTWINKLE[] PROGMEM = "Xmas Twinkle@Twinkle speed,Density;;!;;m12=0"; + + // Distortion waves - ldirko // https://editor.soulmatelights.com/gallery/1089-distorsion-waves // adapted for WLED by @blazoncek @@ -7814,6 +7971,8 @@ void WS2812FX::setupEffectData() { 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); + addEffect(FX_MODE_XMASTWINKLE, &mode_XmasTwinkle, _data_FX_MODE_XMASTWINKLE); + // --- 1D audio effects --- addEffect(FX_MODE_PIXELS, &mode_pixels, _data_FX_MODE_PIXELS); addEffect(FX_MODE_PIXELWAVE, &mode_pixelwave, _data_FX_MODE_PIXELWAVE); diff --git a/wled00/FX.h b/wled00/FX.h index 0d679ba649..97a8c9d2a8 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -314,8 +314,9 @@ #define FX_MODE_WAVESINS 184 #define FX_MODE_ROCKTAVES 185 #define FX_MODE_2DAKEMI 186 +#define FX_MODE_XMASTWINKLE 187 -#define MODE_COUNT 187 +#define MODE_COUNT 188 typedef enum mapping1D2D { M12_Pixels = 0, diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index f9cf3e1e73..c27721eafe 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1837,5 +1837,5 @@ const char JSON_palette_names[] PROGMEM = R"=====([ "Magenta","Magred","Yelmag","Yelblu","Orange & Teal","Tiamat","April Night","Orangery","C9","Sakura", "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" +"Candy2","Xmas Twinkle" ])====="; From e48415323340039fe36549b25c23a7485f1932e2 Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr" Date: Wed, 3 Apr 2024 06:12:54 -0400 Subject: [PATCH 02/31] Convert skewedRandom() to float in anticipation of future changes. Minimum number of LEDs lit is now 2. --- wled00/FX.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 40d2cacd4c..91ae1c8b2b 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7428,6 +7428,7 @@ typedef struct XTwinkleLight { uint32_t twData; // (Be aware of operator precedent when accessing & modifying.) +// (Tried using C++ bit fields, but code broke.) #define TWINKLE_ON 0x80000000 // 1 bit #define TIME_TO_EVENT 0x7fe00000 // 10 bits >> 21 #define TIME_TO_EVENT_SHIFT 21 @@ -7439,17 +7440,18 @@ typedef struct XTwinkleLight { // For creating skewed random numbers toward the shorter end. // The sum of percentages must = 100% const uint16_t pSize = 20; -int16_t percentages[pSize] = {12, 11, 10, 10, 6, 6, 5, 5, 3, 3, 1, 1, 1, 1, 1, 1, 2, 3, 3, 15}; +float percentages[pSize] = {12, 11, 10, 10, 6, 6, 5, 5, 3, 3, 1, 1, 1, 1, 1, 1, 2, 3, 3, 15}; // Input is 0-100, Ouput is skewed 0-100. // PArray may be any size, but elements must add up to 100. // Note: Single precision floating point is just as fast on an ESP-32 as fixed arithmetic. -int32_t skewedRandom( int32_t rand100, +// Fun fact: Float multiply-add operations run at a faster rate than the ESP-32 clock . +int32_t skewedRandom( float rand100, uint16_t pArraySize, - int16_t *pArray) + float *pArray) { int index = 0; - int cumulativePercentage = 0; + float cumulativePercentage = 0; // Find the range in the table based on randomValue. while (index < pArraySize - 1 && rand100 >= cumulativePercentage + pArray[index]) { @@ -7458,16 +7460,16 @@ int32_t skewedRandom( int32_t rand100, } // Calculate linear interpolation - float t = float((rand100 - cumulativePercentage) / float(pArray[index])); - int result = int((float(index) + t) * 100.0 / pArraySize); + float t = (rand100 - cumulativePercentage) / pArray[index]; + float result = (float(index) + t) * 100.0 / pArraySize; return result; } uint16_t mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. uint16_t numTwiklers = SEGLEN * SEGMENT.intensity / 255; - if (numTwiklers <= 0) - numTwiklers = 1; // Divide checks are not cool. + if (numTwiklers <= 1) + numTwiklers = 2; // Divide checks are not cool. // Reinitialize evertying if the number of twinklers has changed. if (numTwiklers != SEGMENT.aux0) From 25ba7633943a90207b370c5c2f748bc0bc025102 Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr" Date: Sat, 6 Apr 2024 09:40:33 -0400 Subject: [PATCH 03/31] Change wieghting table toward slower Xmas Twinkle times. --- wled00/FX.cpp | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 91ae1c8b2b..f6dcfd9800 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7441,6 +7441,8 @@ typedef struct XTwinkleLight { // The sum of percentages must = 100% const uint16_t pSize = 20; float percentages[pSize] = {12, 11, 10, 10, 6, 6, 5, 5, 3, 3, 1, 1, 1, 1, 1, 1, 2, 3, 3, 15}; +float slowPercentages[pSize] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 6, 7, 7, 7, 10, 12, 12, 15, 19}; +float wkgPercentages[pSize]; // Input is 0-100, Ouput is skewed 0-100. // PArray may be any size, but elements must add up to 100. @@ -7466,6 +7468,19 @@ int32_t skewedRandom( float rand100, return result; } +// Take two percentage tables and average them using the weighting factor. +// Both tables and the result must be the same size. +void weightPercentages(float *arg1, + float *arg2, + int cnt, float // 0.0-1.0 weight given to arg2. + factor, float + *result) +{ + float arg1Factor = 1.0 - factor; + for (int i = 0; i < cnt; ++i) + result[i] = arg1[i] * arg1Factor + arg2[i] * factor; +} + uint16_t mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. uint16_t numTwiklers = SEGLEN * SEGMENT.intensity / 255; if (numTwiklers <= 1) @@ -7476,7 +7491,15 @@ uint16_t mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. SEGMENT.aux0 = 0; // The maximum twinkle time varies based on the time slider - int32_t maximumTime = (255 - SEGMENT.speed) * 900 / 256 + 100; // Between 100 & 1000 centiseconds + float slowWeight = (255 - SEGMENT.speed) / 255.0; // 0.0 - 1.0 + int32_t maximumTime = (slowWeight * 900.0) + 100.0; // Between 100 & 1000 centiseconds + + // We have two tables, one of 'normal' weights, 1 of slow weights. + // use more of the slow percentages in he last quarter of the segment times. + slowWeight = (slowWeight - 0.75) * 4; + if (slowWeight < 0) + slowWeight = 0.0; + weightPercentages(percentages, slowPercentages, pSize, slowWeight, wkgPercentages); // uint8_t flasherDistance = ((255 - SEGMENT.intensity) / 28) +1; //1-10 // uint16_t numFlashers = (SEGLEN / flasherDistance) +1; @@ -7494,7 +7517,7 @@ uint16_t mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. light->colorIdx = random8(); light->twData = 0; // Everything 0 - int cycleTime = skewedRandom(random(100), pSize, percentages) * maximumTime / 100 + 20; + int cycleTime = skewedRandom(random(100), pSize, wkgPercentages) * maximumTime / 100 + 20; light->twData |= cycleTime << MAX_CYCLE_SHIFT & MAX_CYCLE; light->twData |= random(50, cycleTime) << TIME_TO_EVENT_SHIFT & TIME_TO_EVENT; @@ -7539,7 +7562,7 @@ uint16_t mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. int16_t cycleTime = (light->twData & T_RETWINKLE) - interval; if (cycleTime <= 0) { - int maxTime = skewedRandom(random(100), pSize, percentages) * maximumTime / 100 + 20; + int maxTime = skewedRandom(random(100), pSize, wkgPercentages) * maximumTime / 100 + 20; light->twData = (light->twData & ~MAX_CYCLE) | (maxTime << MAX_CYCLE_SHIFT & MAX_CYCLE); cycleTime += 2000; // 20 seconds } From 8fa21d51ef7408cb0a079069405c1b0620e493b5 Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Sun, 3 Aug 2025 10:58:34 -0400 Subject: [PATCH 04/31] Removed 'Xmas Twinkle' out of 'JSON_palette_names'. The constant is no longer used. --- wled00/FX_fcn.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index e2c8d22226..32e34faf98 100755 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -2076,5 +2076,5 @@ const char JSON_palette_names[] PROGMEM = R"=====([ "Magenta","Magred","Yelmag","Yelblu","Orange & Teal","Tiamat","April Night","Orangery","C9","Sakura", "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","Xmas Twinkle" +"Candy2","Traffic Light" ])====="; From 623325d907c81621beb24fc430b7dcfe3aeb11e3 Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Thu, 14 Aug 2025 15:47:03 -0400 Subject: [PATCH 05/31] Include changes from 'Elastic_Collision_Work'. --- wled00/FX.cpp | 405 ++++++++++++++++++++++++++++++++++++++++++++++++-- wled00/FX.h | 3 +- 2 files changed, 396 insertions(+), 12 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 4066fd5fe7..f08af209db 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7502,8 +7502,8 @@ typedef struct XTwinkleLight { // For creating skewed random numbers toward the shorter end. // The sum of percentages must = 100% const uint16_t pSize = 20; -float percentages[pSize] = {12, 11, 10, 10, 6, 6, 5, 5, 3, 3, 1, 1, 1, 1, 1, 1, 2, 3, 3, 15}; -float slowPercentages[pSize] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 6, 7, 7, 7, 10, 12, 12, 15, 19}; +const float percentages[pSize] = {12, 11, 10, 10, 6, 6, 5, 5, 3, 3, 1, 1, 1, 1, 1, 1, 2, 3, 3, 15}; +const float slowPercentages[pSize] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 6, 7, 7, 7, 10, 12, 12, 15, 19}; float wkgPercentages[pSize]; // Input is 0-100, Ouput is skewed 0-100. @@ -7511,8 +7511,8 @@ float wkgPercentages[pSize]; // Note: Single precision floating point is just as fast on an ESP-32 as fixed arithmetic. // Fun fact: Float multiply-add operations run at a faster rate than the ESP-32 clock . int32_t skewedRandom( float rand100, - uint16_t pArraySize, - float *pArray) + const uint16_t pArraySize, + const float *pArray) { int index = 0; float cumulativePercentage = 0; @@ -7532,11 +7532,11 @@ int32_t skewedRandom( float rand100, // Take two percentage tables and average them using the weighting factor. // Both tables and the result must be the same size. -void weightPercentages(float *arg1, - float *arg2, - int cnt, float // 0.0-1.0 weight given to arg2. - factor, float - *result) +void weightPercentages(const float *arg1, + const float *arg2, + const int cnt, + const float factor, // 0.0-1.0 weight given to arg2. + float *result) { float arg1Factor = 1.0 - factor; for (int i = 0; i < cnt; ++i) @@ -7613,7 +7613,12 @@ uint16_t mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. if (light->twData & TWINKLE_ON) eventTime += random(50, ((light->twData & MAX_CYCLE) >> MAX_CYCLE_SHIFT)); // turn OFF else + { + // Based on the check box, either use a constant palette index or a new one each time it turns on. + if (SEGMENT.check1) + light->colorIdx = random8(); eventTime += random(10, ((light->twData & MAX_CYCLE) >> MAX_CYCLE_SHIFT) / 3); // turn ON + } light->twData ^= TWINKLE_ON; } @@ -7651,12 +7656,389 @@ uint16_t mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. if (inset > SEGLEN) // Safety break; - SEGMENT.setPixelColor(inset, CRGB(SEGMENT.color_wheel(light->colorIdx))); + SEGMENT.setPixelColor(inset, ColorFromPalette(SEGPALETTE,light->colorIdx)); } return FRAMETIME; } // mode_XmasTwinkle -static const char _data_FX_MODE_XMASTWINKLE[] PROGMEM = "Xmas Twinkle@Twinkle speed,Density;;!;;m12=0"; +static const char _data_FX_MODE_XMASTWINKLE[] PROGMEM = "Xmas Twinkle@Twinkle speed,Density,,,,Color indices vary;;!;;m12=0"; + +//////////////////////////// +// Elastic Collisions // +//////////////////////////// + +#define SPACE_FACTOR 10 // Ratio between internal and LED address spaces +#define DE_SPACE_FACTOR 0.1F // Inverst to avoid divides. +#define SLOWDOWN_FACTOR 0.4 // (Make this a variable?) for very large spheres. +#define BOUNCE_CYCLE_TIME 50 // ms. +#define RESET_CYCLE_TIME 1200 // Number of cycles (60 * 1000 / 50) +#define WALL_COLLAPSE_INTR 125 // Cycles left till regen. + +class MBSphere +{ + float x, y; // Position + float vx, vy; // Velocity + float radius; // Radius + float _density = 1.0f; // Density is 1 for bouncing, other values for gravity + uint8_t colorIdx; +#if false + AbstractList *attrocters; // Null unless this object is affected by gravity. +#endif + + +public: + MBSphere(float radius, float x, float y, float vx, float vy, uint8_t color) + : x(x), y(y), vx(vx), vy(vy), radius(radius), colorIdx(color) /*, attrocters(nullptr) */ + { + } + ~MBSphere() { } +#if false + // For effects with gravity. + void addAttractor(MBSphere *sp) + { + if (! attrocters) + attrocters = new List; + + attrocters->add(sp); + } +#endif + float density() { return _density; } + void setDensity(float newD) { _density = newD; } + float mass() { return pow(radius, 3) * density(); } + + // Update the sphere's position and velocity + void update(float dt) { x += vx * dt; y += vy * dt; } + void newLoc(float newX, float newY) { x = newX; y = newY; } + + // Detect if two circles are colliding (simple distance check) + bool areSpheresColliding(MBSphere sp) + { + float distSq = (sp.x - this->x) * (sp.x - this->x) + (sp.y - this->y) * (sp.y - this->y); + float radiusSum = this->radius + sp.radius; + return distSq <= radiusSum * radiusSum; + } + + // Function to simulate the elastic collision and update velocities + void handleCollision(MBSphere *sp, bool is2D) + { + float m1 = this->mass(); + float m2 = sp->mass(); + + // Calculate the normal and tangent vectors + float nx = sp->x - x; + float ny = sp->y - y; + float dist = std::sqrt(nx * nx + ny * ny); + nx /= dist; + ny /= dist; + + // Tangent is perpendicular to normal + float tx = -ny; + float ty = nx; + + // Use canned values if 1D, otherwise an x velocity creeps in. + if (!is2D) + { + nx = 0; + ny = (sp->y >= y) ? 1 : -1; + tx = -ny; + ty = 0; + } + + // Project velocities onto the normal and tangent + float v1n = vx * nx + vy * ny; + float v1t = vx * tx + vy * ty; + float v2n = sp->vx * nx + sp->vy * ny; + float v2t = sp->vx * tx + sp->vy * ty; + + // Apply 1D elastic collision for the normal components + float v1n_final = (v1n * (m1 - m2) + 2 * m2 * v2n) / (m1 + m2); + float v2n_final = (v2n * (m2 - m1) + 2 * m1 * v1n) / (m1 + m2); + + // Final velocity vectors (tangential velocity remains the same) + vx = v1n_final * nx + v1t * tx; + vy = v1n_final * ny + v1t * ty; + sp->vx = v2n_final * nx + v2t * tx; + sp->vy = v2n_final * ny + v2t * ty; + } + + // Function to handle wall collisions + void handleWallCollision(float windowWidth, float windowHeight) + { + if (x - radius < 0) { + x = radius; // Keep inside the left wall + vx = -vx; // Reverse x velocity + } else if (x + radius > windowWidth) { + x = windowWidth - radius; // Keep inside the right wall + vx = -vx; // Reverse x velocity + } + + if (y - radius < 0) { + y = radius; // Keep inside the top wall + vy = -vy; // Reverse y velocity + } else if (y + radius > windowHeight) { + y = windowHeight - radius; // Keep inside the bottom wall + vy = -vy; // Reverse y velocity + } + } + +#if false + // Apply the force of gravity with another sphere over the time period in ms. + void applyAttractorGravity(long overTime); + void applyGravity(MBSphere *sp, long overTime); + + // Calculate the initial velocity for a circular orbit. + void initializeOrbit(MBSphere *sp, float dx, float dy); +#endif + float clamp(float value, float minVal, float maxVal) + { + if (value < minVal) return minVal; + if (value > maxVal) return maxVal; + return value; + } + + float smoothstep(float edge0, float edge1, float x) + { + float t = clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); + return t * t * (3.0f - 2.0f * t); + } + + // For generality, the spere uses the segment passed in, not a global. + void drawMe(Segment &seg, bool draw) + { + const bool is2D = seg.is2D(); + const int gridW = (is2D) ? (int)seg.vWidth() : 1; + const int gridH = (is2D) ? (int)seg.vHeight() : seg.vLength(); + + CRGB sphereColor = ColorFromPalette(seg.getCurrentPalette(), colorIdx); + CRGB drawColor; + + /* Thank you Code Copilot: "Using C++, I have a coordinate space that is 10 times + * an LED array. I want to draw a solid circle of diameter 'r' and position 'x' + * and 'y' in the LED array, anti-aliasing the pixels." + * Optimize the loop to only working on pixels near the object. Don't do the + * whole panel. */ + float edge0 = radius - 0.5f * SPACE_FACTOR; // Soft transition start + float edge1 = radius + 0.5f * SPACE_FACTOR; // Soft transition end + int lowX = floor((x - edge1) * DE_SPACE_FACTOR) - 6; // We don't need to cut it close. + int highX = ceil((x + edge1) * DE_SPACE_FACTOR) + 6; + int lowY = floor((y - edge1) * DE_SPACE_FACTOR) - 6; + int highY = ceil((y + edge1) * DE_SPACE_FACTOR) + 6; + if (lowX < 0) + lowX = 0; + if (highX > gridW) + highX = gridW; + if (lowY < 0) + lowY = 0; + if (highY > gridH) + highY = gridH; + for (int lY = lowY; lY < highY; lY++) { + for (int lX = lowX; lX < highX; lX++) { + // LED pixel center in high-resolution space + float pixelX = (lX + 0.5f) * SPACE_FACTOR; + float pixelY = (lY + 0.5f) * SPACE_FACTOR; + + // Distance from the circle center + float dist = sqrt((pixelX - x) * (pixelX - x) + (pixelY - y) * (pixelY - y)); + + // Compute anti-aliasing weight + float alpha = clamp(1.0f - smoothstep(edge0, edge1, dist), 0.0f, 1.0f); + + // Store intensity in LED array (0-1 range) + if (draw) + { + drawColor = sphereColor; + drawColor.nscale8(alpha * 255); + } + else + drawColor = CRGB::Black; + + if (alpha > 0.0) + { + if (is2D) + seg.setPixelColorXY(lX, lY, drawColor); + else + seg.setPixelColor(lY, drawColor); + } + } + } + } +}; + +// Given 0-255 from SEGMENT.custom2, return in number of 50ms cycles. +uint32_t elasticLifetime() +{ + // 8 categories. + switch (SEGMENT.custom2 >> 5) // /32 + { + case 0: + return 300; // 15s + case 1: + return 600; // 30s + case 2: + return 1200; // 1m + case 3: + return 2400; // 2m + case 4: + return 6000; // 5m + case 5: + return 12000; // 10m + case 6: + return 36000; // 30m + case 7: + return 72000; // 1hr + } +} + +uint16_t mode_ElasticCollisions(void) { // by Nicholas Pisarro, Jr. + + int numSpheres = 1 + (SEGMENT.intensity * 29) / 255; // 1-30 + + /* + * SEGMENT.aux0.0 = desired number of spheres. + * SEGMENT.aux0.1 = actual number allocated. Might be < aux0.0. + * SEGMENT.step = Next movement intereval + * SEGMENT.aux1 = Next total rebuild as a number of increments. + */ + #define SPHERES_DESIRED 0xff00 + #define SPHERES_DESIRED_SHIFT 8 + #define SPHERES_ALLOCATED 0x00ff + + const bool is2D = strip.isMatrix && SEGMENT.is2D(); + const int cols = (is2D) ? SEG_W : 1; + const int rows = (is2D) ? SEG_H : SEGLEN; + + // Make a virtual coordinate space that is SPACE_FACTOR times the led array. + const int internalX = SPACE_FACTOR * cols; + const int internalY = SPACE_FACTOR * rows; + const int halfInternalY = internalY >> 1; + + // Radius distribution. + const int dmTableSize = 20; + const float dmPercentages[20] = {40, 20, 10, 4, 3, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3}; + + // Reinitialize evertying if the number of spheres has changed. + // (We need a separate counter for the number wanted, vs. the number actually initialized.) + if (numSpheres != ((SEGMENT.aux0 & SPHERES_DESIRED) >> SPHERES_DESIRED_SHIFT)) + SEGMENT.aux0 = 0; + + // Point to the sheres. + uint16_t dataSize = sizeof(MBSphere) * numSpheres; + if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + MBSphere* spheres = reinterpret_cast(SEGENV.data); + + // Initialize the spheres. + if ((SEGMENT.aux0 & SPHERES_DESIRED) == 0) + { + SEGMENT.aux0 &= SPHERES_DESIRED; + const float complementUniformity = 100 - ((int32_t) SEGMENT.custom1) * 100 / 255; + + for (int i = 0; i < numSpheres; ++i) + { + // Diameter is based on the uniformity. + float radius = 10.0 + skewedRandom(random(100), dmTableSize, dmPercentages) * complementUniformity / 250.0; // 10-50 + radius *= 0.7; + float massFactor = 15.0 / radius * SLOWDOWN_FACTOR; // Big things should move slower to keep momentum down. + float vx = (-50.0 + random(100)) * massFactor / 5.0; // ±10 + float vy = (-50.0 + random(100)) * massFactor * complementUniformity / 500.0; // ±10 + if (complementUniformity == 0) // Just one sphere has motion intially, if uniformity = 100%. + { + if (i == 0) + vx = 5; + else + vx = 0; + } + if (!is2D) + { + vy = vx; + vx = 0; + } + + MBSphere *candidate = new (&spheres[i]) MBSphere(radius, 0, 0, vx, vy, random8()); + + // Make sure the sphere doesn't land on another one. + bool conflicted = false; + int safety = 100; // Don't try a fit too many items. + do + { + // Give it a random location—closer to the vertical center based on the uniformity. + // (Gotcha! WLED random() returns unsigned. It can't go negative.) + float x = random(internalX); + float y = halfInternalY + (((int32_t)(random(internalY)) - halfInternalY) * complementUniformity / 100.0); + if (!is2D) + { + y = random(internalY); + x = 0; + } + candidate->newLoc(x, y); + + // Make sure it doesn't land on anything else. + conflicted = false; + for (int j = 0; j < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++j) + if (spheres[j].areSpheresColliding(*candidate)) + { + conflicted = true; + break; + } + } while (conflicted && --safety >= 0); + + // Stop, if we were unsuccessful. + if (conflicted) + break; + + ++SEGMENT.aux0; // Increments SPHERES_ALLOCATED + } + + SEGMENT.aux0 = (numSpheres << SPHERES_DESIRED_SHIFT) | (SEGMENT.aux0 & SPHERES_ALLOCATED); + SEGMENT.step = millis() + BOUNCE_CYCLE_TIME; + SEGMENT.aux1 = elasticLifetime(); + } + + // If it is time to do something. + if (millis() > SEGMENT.step) + { + // Turm off all the LEDS. + for (int i = 0; i < SEGLEN; ++i) + SEGMENT.setPixelColor(i, CRGB::Black); + + // Draw the spheres. + for (int i = 0; i < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++i) + spheres[i].drawMe(SEGMENT, true); + + // Move the spheres and check for collisions with the walls. + float a = 0.0002503; // We want of range from 0.1->1->10. + float b = -0.01347; + float c = 0.1; + float speed = SEGMENT.speed; + speed = a * speed * speed + b * speed + c; + for (int i = 0; i < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++i) + { + spheres[i].update(speed); + + // If nearing a regeneration, let the walls fall and the spheres fly off! + if (SEGMENT.aux1 > WALL_COLLAPSE_INTR) + spheres[i].handleWallCollision(internalX, internalY); + } + + // Check for collisions with other spheres. + for (int i = 0; i < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++i) + for (int j = i + 1; j < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++j) + if (spheres[i].areSpheresColliding(spheres[j])) + spheres[i].handleCollision(spheres + j, is2D); + + // After a while, force a complete recalculation + if (--SEGMENT.aux1 == 0) + { + SEGMENT.aux1 = elasticLifetime(); + SEGMENT.aux0 = 0; + } + + // Remember the last time + SEGMENT.step += BOUNCE_CYCLE_TIME; + } + + return FRAMETIME; +} // mode_ElasticCollisions +static const char _data_FXMODE_ELASTICCOLLISIONS[] PROGMEM = "Elastic Collisions@Speed,Count,Uniformity,Lifetime;!,!;!;c1=0,sx=120,c2=64"; // Distortion waves - ldirko @@ -10960,6 +11342,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_DYNAMIC_SMOOTH, &mode_dynamic_smooth, _data_FX_MODE_DYNAMIC_SMOOTH); addEffect(FX_MODE_XMASTWINKLE, &mode_XmasTwinkle, _data_FX_MODE_XMASTWINKLE); + addEffect(FX_MODE_ELASTICCOLLISIONS, &mode_ElasticCollisions, _data_FXMODE_ELASTICCOLLISIONS); // --- 1D audio effects --- addEffect(FX_MODE_PIXELS, &mode_pixels, _data_FX_MODE_PIXELS); diff --git a/wled00/FX.h b/wled00/FX.h index 27834be4a5..78e91d00e0 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -375,7 +375,8 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_PS1DSPRINGY 216 #define FX_MODE_PARTICLEGALAXY 217 #define FX_MODE_XMASTWINKLE 218 -#define MODE_COUNT 219 +#define FX_MODE_ELASTICCOLLISIONS 219 +#define MODE_COUNT 220 #define BLEND_STYLE_FADE 0x00 // universal From a1ccaa881227363edcf41363354a54d98679f621 Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Fri, 15 Aug 2025 08:32:33 -0400 Subject: [PATCH 06/31] Update the "Elastic Collision" metadata to show the proper user icons. --- wled00/FX.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index f08af209db..99dca9260d 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -8038,7 +8038,7 @@ uint16_t mode_ElasticCollisions(void) { // by Nicholas Pisarro, Jr. return FRAMETIME; } // mode_ElasticCollisions -static const char _data_FXMODE_ELASTICCOLLISIONS[] PROGMEM = "Elastic Collisions@Speed,Count,Uniformity,Lifetime;!,!;!;c1=0,sx=120,c2=64"; +static const char _data_FXMODE_ELASTICCOLLISIONS[] PROGMEM = "Elastic Collisions@Speed,Count,Uniformity,Lifetime;;!;12;c1=0,sx=120,c2=64"; // Distortion waves - ldirko From 135d7d5dae737a017dede3029b6bd303dcafabce Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Fri, 15 Aug 2025 10:18:02 -0400 Subject: [PATCH 07/31] And we fix up "Xmas Twinkle" to note that it works great on a single LED. --- wled00/FX.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 99dca9260d..76fc872205 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7661,7 +7661,7 @@ uint16_t mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. return FRAMETIME; } // mode_XmasTwinkle -static const char _data_FX_MODE_XMASTWINKLE[] PROGMEM = "Xmas Twinkle@Twinkle speed,Density,,,,Color indices vary;;!;;m12=0"; +static const char _data_FX_MODE_XMASTWINKLE[] PROGMEM = "Xmas Twinkle@Twinkle speed,Density,,,,Color indices vary;;!;012;m12=0"; //////////////////////////// // Elastic Collisions // From cad92c75defd0ea67fefcdaf55791959a357c17e Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Sat, 16 Aug 2025 08:10:06 -0400 Subject: [PATCH 08/31] In Elastic Collisions, make sure two spheres haven't crashed so hard one ends up inside the other. This fix is not perfect, but makes it is rarer and clears it faster. --- wled00/FX.cpp | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 76fc872205..476394a89f 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7718,8 +7718,35 @@ class MBSphere return distSq <= radiusSum * radiusSum; } - // Function to simulate the elastic collision and update velocities - void handleCollision(MBSphere *sp, bool is2D) + /* Make sure two spheres haven't gotten too close. + * Note: There is a pathological case where two spheres + * can crash into each other so hard, that one actually + * ends up insde the other. This function prevents that. */ + void enforceMinDist(MBSphere *sp) + { + float dist = radius + sp->radius; + + float dx = sp->x - x; + float dy = sp->y - y; + float length = sqrt(dx * dx + dy * dy); + + if (length >= dist || length == 0.0) + return; // Already long enough, or degenerate point + + // Normalize direction + float scale = (dist - length) / (2.0 * length); + + float offsetX = dx * scale; + float offsetY = dy * scale; + + x -= offsetX; + y -= offsetY; + sp->x += offsetX; + sp->y += offsetY; + } + + // Function to simulate the elastic collision and update velocities + void handleCollision(MBSphere *sp, bool is2D) { float m1 = this->mass(); float m2 = sp->mass(); @@ -8023,7 +8050,12 @@ uint16_t mode_ElasticCollisions(void) { // by Nicholas Pisarro, Jr. for (int i = 0; i < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++i) for (int j = i + 1; j < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++j) if (spheres[i].areSpheresColliding(spheres[j])) + { + /* Make sure the two spheres haven't collided so hard that + * one is inside the other. */ + spheres[i].enforceMinDist(spheres + j); spheres[i].handleCollision(spheres + j, is2D); + } // After a while, force a complete recalculation if (--SEGMENT.aux1 == 0) From 3af0de1f6ddc43bd2fa99373ace395c93942167a Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Tue, 2 Sep 2025 05:08:29 -0400 Subject: [PATCH 09/31] Convert 'skewedRandom' and 'weightPercentages' to integer functions and make their parameters uint8_t, instead of float. --- wled00/FX.cpp | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 476394a89f..8417e305bf 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7501,21 +7501,20 @@ typedef struct XTwinkleLight { // For creating skewed random numbers toward the shorter end. // The sum of percentages must = 100% -const uint16_t pSize = 20; -const float percentages[pSize] = {12, 11, 10, 10, 6, 6, 5, 5, 3, 3, 1, 1, 1, 1, 1, 1, 2, 3, 3, 15}; -const float slowPercentages[pSize] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 6, 7, 7, 7, 10, 12, 12, 15, 19}; -float wkgPercentages[pSize]; +const uint8_t pSize = 20; +const uint8_t percentages[pSize] = {12, 11, 10, 10, 6, 6, 5, 5, 3, 3, 1, 1, 1, 1, 1, 1, 2, 3, 3, 15}; +const uint8_t slowPercentages[pSize] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 6, 7, 7, 7, 10, 12, 12, 15, 19}; +uint8_t wkgPercentages[pSize]; // Input is 0-100, Ouput is skewed 0-100. // PArray may be any size, but elements must add up to 100. -// Note: Single precision floating point is just as fast on an ESP-32 as fixed arithmetic. -// Fun fact: Float multiply-add operations run at a faster rate than the ESP-32 clock . -int32_t skewedRandom( float rand100, - const uint16_t pArraySize, - const float *pArray) +#define RAND_PREC_SHIFT 10 // Vertual binary point from the right +int32_t skewedRandom( uint8_t rand100, + const uint8_t pArraySize, + const uint8_t *pArray) { - int index = 0; - float cumulativePercentage = 0; + int32_t index = 0; + int32_t cumulativePercentage = 0; // Find the range in the table based on randomValue. while (index < pArraySize - 1 && rand100 >= cumulativePercentage + pArray[index]) { @@ -7524,23 +7523,23 @@ int32_t skewedRandom( float rand100, } // Calculate linear interpolation - float t = (rand100 - cumulativePercentage) / pArray[index]; - float result = (float(index) + t) * 100.0 / pArraySize; + int32_t t = ((rand100 - cumulativePercentage) << RAND_PREC_SHIFT) / pArray[index]; + int32_t result = ((index << RAND_PREC_SHIFT) + t) * 100 / pArraySize >> RAND_PREC_SHIFT; return result; } // Take two percentage tables and average them using the weighting factor. // Both tables and the result must be the same size. -void weightPercentages(const float *arg1, - const float *arg2, +void weightPercentages(const uint8_t *arg1, + const uint8_t *arg2, const int cnt, - const float factor, // 0.0-1.0 weight given to arg2. - float *result) + const uint32_t factor, // 0.0-1.0 weight given to arg2 << RAND_PREC_SHIFT + uint8_t *result) { - float arg1Factor = 1.0 - factor; + uint32_t arg1Factor = (1 << RAND_PREC_SHIFT) - factor; for (int i = 0; i < cnt; ++i) - result[i] = arg1[i] * arg1Factor + arg2[i] * factor; + result[i] = arg1[i] * arg1Factor + arg2[i] * factor >> RAND_PREC_SHIFT; } uint16_t mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. @@ -7561,7 +7560,7 @@ uint16_t mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. slowWeight = (slowWeight - 0.75) * 4; if (slowWeight < 0) slowWeight = 0.0; - weightPercentages(percentages, slowPercentages, pSize, slowWeight, wkgPercentages); + weightPercentages(percentages, slowPercentages, pSize, slowWeight * (1 << RAND_PREC_SHIFT), wkgPercentages); // uint8_t flasherDistance = ((255 - SEGMENT.intensity) / 28) +1; //1-10 // uint16_t numFlashers = (SEGLEN / flasherDistance) +1; @@ -7941,7 +7940,7 @@ uint16_t mode_ElasticCollisions(void) { // by Nicholas Pisarro, Jr. // Radius distribution. const int dmTableSize = 20; - const float dmPercentages[20] = {40, 20, 10, 4, 3, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3}; + const uint8_t dmPercentages[20] = {40, 20, 10, 4, 3, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3}; // Reinitialize evertying if the number of spheres has changed. // (We need a separate counter for the number wanted, vs. the number actually initialized.) From d8f7be1d129a96fe5180268659e66c24f878f921 Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Fri, 5 Sep 2025 08:37:16 -0400 Subject: [PATCH 10/31] Ripped out 'float' and replaced with mostly Q16.16 fixed point arithmetic. --- wled00/FX.cpp | 343 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 240 insertions(+), 103 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 8417e305bf..63b3a106b1 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7666,19 +7666,43 @@ static const char _data_FX_MODE_XMASTWINKLE[] PROGMEM = "Xmas Twinkle@Twinkle sp // Elastic Collisions // //////////////////////////// -#define SPACE_FACTOR 10 // Ratio between internal and LED address spaces -#define DE_SPACE_FACTOR 0.1F // Inverst to avoid divides. +// For print diagnostics, only. + #define FLOAT_IT(x) ((float)(x) / (1 << SPHERE_PREC_SHIFT)) + + /* Note: When you multiply two fixed numbers, the binary point shifts left by the sum of + * binary points. In division the binary point shift right by the difference between + * divident - divisor. */ +#define SPHERE_PREC_SHIFT 16 // Vertual binary point from the right +typedef int32_t nfixed; // These represent fixed point fractional numbers as Q16.16 + #define SLOWDOWN_FACTOR 0.4 // (Make this a variable?) for very large spheres. #define BOUNCE_CYCLE_TIME 50 // ms. #define RESET_CYCLE_TIME 1200 // Number of cycles (60 * 1000 / 50) #define WALL_COLLAPSE_INTR 125 // Cycles left till regen. +// --- Portable countLeadingZeros64 for faster SQRT --- +int countLeadingZeros64(uint64_t x) +{ +#if defined(__GNUC__) || defined(__clang__) + return __builtin_clzll(x); +#else + if (x == 0) return 64; + int n = 0; + uint64_t mask = 1ULL << 63; + while ((x & mask) == 0) { + n++; + mask >>= 1; + } + return n; +#endif +} + class MBSphere { - float x, y; // Position - float vx, vy; // Velocity - float radius; // Radius - float _density = 1.0f; // Density is 1 for bouncing, other values for gravity + nfixed x, y; // Position + nfixed vx, vy; // Velocity + nfixed radius; // Radius + nfixed _density = (1 << SPHERE_PREC_SHIFT); // Density is 1 for bouncing, other values for gravity uint8_t colorIdx; #if false AbstractList *attrocters; // Null unless this object is affected by gravity. @@ -7686,8 +7710,8 @@ class MBSphere public: - MBSphere(float radius, float x, float y, float vx, float vy, uint8_t color) - : x(x), y(y), vx(vx), vy(vy), radius(radius), colorIdx(color) /*, attrocters(nullptr) */ + MBSphere(nfixed radius, nfixed x, nfixed y, nfixed vx, nfixed vy, int8_t color) + : x(x), y(y), vx(vx), vy(vy), radius(radius), colorIdx(color) /*, attrocters(nullptr) */ { } ~MBSphere() { } @@ -7701,20 +7725,62 @@ class MBSphere attrocters->add(sp); } #endif - float density() { return _density; } - void setDensity(float newD) { _density = newD; } - float mass() { return pow(radius, 3) * density(); } + nfixed density() { return _density; } + void setDensity(nfixed newD) { _density = newD; } + nfixed mass() { return fixedMult(fixedMult(fixedMult(radius, radius), radius), density()); } + + static nfixed fixedMult(nfixed a, nfixed b) + { + return (int64_t)a * b >> SPHERE_PREC_SHIFT; + } + + static nfixed fixedDiv(nfixed a, nfixed b) + { + return ((int64_t)a << SPHERE_PREC_SHIFT) / b; + } + + static nfixed fixedSqrt(nfixed x) + { + // Promote to 64-bit and scale up for precision + uint64_t n = (uint64_t)x << SPHERE_PREC_SHIFT; // Q16.16 -> Q32.32 + return fixed64Sqrt(n); + } + + // Faster SQRT function curtesy Code Copilot 5. + static nfixed fixed64Sqrt(int64_t n) + { + if (n <= 0) return 0; + + // Initial guess from highest bit. + int lz = 63 - countLeadingZeros64(n); + uint64_t res = 1ULL << (lz / 2); + + // Newton–Raphson refinement (3–4 iterations are plenty) + res = (res + n / res) >> 1; + res = (res + n / res) >> 1; + res = (res + n / res) >> 1; + + // Clamp back to 32-bit Q16.16 + return (nfixed)res; + } + + /* Squaring coordinates can blow out the range of nfixed. + * Work with the 64 bit intermediate result. */ + static nfixed fixedDist(nfixed a, nfixed b) + { + int64_t n = (int64_t)a * a + (int64_t)b * b; + return fixed64Sqrt(n); + } // Update the sphere's position and velocity - void update(float dt) { x += vx * dt; y += vy * dt; } - void newLoc(float newX, float newY) { x = newX; y = newY; } + void update(nfixed dt) { x += fixedMult(vx, dt); y += fixedMult(vy, dt); } + void newLoc(nfixed newX, nfixed newY) { x = newX; y = newY; } // Detect if two circles are colliding (simple distance check) bool areSpheresColliding(MBSphere sp) { - float distSq = (sp.x - this->x) * (sp.x - this->x) + (sp.y - this->y) * (sp.y - this->y); - float radiusSum = this->radius + sp.radius; - return distSq <= radiusSum * radiusSum; + nfixed dist = fixedDist(sp.x - this->x, sp.y - this->y); + return dist <= this->radius + sp.radius; } /* Make sure two spheres haven't gotten too close. @@ -7723,20 +7789,26 @@ class MBSphere * ends up insde the other. This function prevents that. */ void enforceMinDist(MBSphere *sp) { - float dist = radius + sp->radius; + nfixed dist = radius + sp->radius; - float dx = sp->x - x; - float dy = sp->y - y; - float length = sqrt(dx * dx + dy * dy); + nfixed dx = sp->x - x; + nfixed dy = sp->y - y; + nfixed length = fixedDist(dx, dy); if (length >= dist || length == 0.0) return; // Already long enough, or degenerate point // Normalize direction - float scale = (dist - length) / (2.0 * length); + if (length << 1 == 0) + { + // handle gracefully, but this shouldn't happen. + Serial.println("At 0 #1"); + return; + } + nfixed scale = fixedDiv(dist - length, length << 1); - float offsetX = dx * scale; - float offsetY = dy * scale; + nfixed offsetX = fixedMult(dx, scale); + nfixed offsetY = fixedMult(dy, scale); x -= offsetX; y -= offsetY; @@ -7747,64 +7819,72 @@ class MBSphere // Function to simulate the elastic collision and update velocities void handleCollision(MBSphere *sp, bool is2D) { - float m1 = this->mass(); - float m2 = sp->mass(); - - // Calculate the normal and tangent vectors - float nx = sp->x - x; - float ny = sp->y - y; - float dist = std::sqrt(nx * nx + ny * ny); - nx /= dist; - ny /= dist; - - // Tangent is perpendicular to normal - float tx = -ny; - float ty = nx; + nfixed m1 = this->mass(); + nfixed m2 = sp->mass(); + + // Calculate the normal and tangent vectors + nfixed nx = sp->x - x; + nfixed ny = sp->y - y; + nfixed dist = fixedDist(nx, ny); + while (dist == 0) { + // handle gracefully + Serial.println("Two objects on top of each other!"); + + x += 1 << (SPHERE_PREC_SHIFT -2); + nx += 1 << (SPHERE_PREC_SHIFT -2); + dist = fixedDist(nx, ny); + } + nx = fixedDiv(nx, dist); + ny = fixedDiv(ny, dist); + + // Tangent is perpendicular to normal + nfixed tx = -ny; + nfixed ty = nx; // Use canned values if 1D, otherwise an x velocity creeps in. if (!is2D) { nx = 0; - ny = (sp->y >= y) ? 1 : -1; + ny = ((sp->y >= y) ? 1 : -1) << SPHERE_PREC_SHIFT; tx = -ny; ty = 0; } - - // Project velocities onto the normal and tangent - float v1n = vx * nx + vy * ny; - float v1t = vx * tx + vy * ty; - float v2n = sp->vx * nx + sp->vy * ny; - float v2t = sp->vx * tx + sp->vy * ty; - - // Apply 1D elastic collision for the normal components - float v1n_final = (v1n * (m1 - m2) + 2 * m2 * v2n) / (m1 + m2); - float v2n_final = (v2n * (m2 - m1) + 2 * m1 * v1n) / (m1 + m2); - - // Final velocity vectors (tangential velocity remains the same) - vx = v1n_final * nx + v1t * tx; - vy = v1n_final * ny + v1t * ty; - sp->vx = v2n_final * nx + v2t * tx; - sp->vy = v2n_final * ny + v2t * ty; + + // Project velocities onto the normal and tangent + nfixed v1n = fixedMult(vx, nx) + fixedMult(vy, ny); + nfixed v1t = fixedMult(vx, tx) + fixedMult(vy, ty); + nfixed v2n = fixedMult(sp->vx, nx) + fixedMult(sp->vy, ny); + nfixed v2t = fixedMult(sp->vx, tx) + fixedMult(sp->vy, ty); + + // Apply 1D elastic collision for the normal components + nfixed v1n_final = fixedDiv(fixedMult(v1n, m1 - m2) + fixedMult(2 * m2, v2n), m1 + m2); + nfixed v2n_final = fixedDiv(fixedMult(v2n, m2 - m1) + fixedMult(2 * m1, v1n), m1 + m2); + + // Final velocity vectors (tangential velocity remains the same) + vx = fixedMult(v1n_final, nx) + fixedMult(v1t, tx); + vy = fixedMult(v1n_final, ny) + fixedMult(v1t, ty); + sp->vx = fixedMult(v2n_final, nx) + fixedMult(v2t, tx); + sp->vy = fixedMult(v2n_final, ny) + fixedMult(v2t, ty); } // Function to handle wall collisions - void handleWallCollision(float windowWidth, float windowHeight) + void handleWallCollision(nfixed windowWidth, nfixed windowHeight) { - if (x - radius < 0) { - x = radius; // Keep inside the left wall - vx = -vx; // Reverse x velocity - } else if (x + radius > windowWidth) { - x = windowWidth - radius; // Keep inside the right wall - vx = -vx; // Reverse x velocity - } - - if (y - radius < 0) { - y = radius; // Keep inside the top wall - vy = -vy; // Reverse y velocity - } else if (y + radius > windowHeight) { - y = windowHeight - radius; // Keep inside the bottom wall - vy = -vy; // Reverse y velocity - } + if (x - radius < 0) { + x = radius; // Keep inside the left wall + vx = -vx; // Reverse x velocity + } else if (x + radius > windowWidth) { + x = windowWidth - radius; // Keep inside the right wall + vx = -vx; // Reverse x velocity + } + + if (y - radius < 0) { + y = radius; // Keep inside the top wall + vy = -vy; // Reverse y velocity + } else if (y + radius > windowHeight) { + y = windowHeight - radius; // Keep inside the bottom wall + vy = -vy; // Reverse y velocity + } } #if false @@ -7815,21 +7895,30 @@ class MBSphere // Calculate the initial velocity for a circular orbit. void initializeOrbit(MBSphere *sp, float dx, float dy); #endif - float clamp(float value, float minVal, float maxVal) + + nfixed clamp(nfixed value, nfixed minVal, nfixed maxVal) { if (value < minVal) return minVal; if (value > maxVal) return maxVal; return value; } - float smoothstep(float edge0, float edge1, float x) + nfixed smoothstep(nfixed edge0, nfixed edge1, nfixed x) { - float t = clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); - return t * t * (3.0f - 2.0f * t); + // float t = clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); + // nfixed t = clamp(fixedDiv(x - edge0, edge1 - edge0), 0, 1 << SPHERE_PREC_SHIFT); + // return t * t * (3.0f - 2.0f * t); + + // Use a faster divide and multiply using Q24.8 numbers instead of Q16.16. + edge0 >>= 8; + edge1 >>= 8; + x >>= 8; + int t = clamp((x - edge0 << 8) / (edge1 - edge0), 0, 1 << 8); // Q24.8 + return (t * t >> 8) * ((3 << 8) - 2 * t); // Result of cubing is Q16.16. } // For generality, the spere uses the segment passed in, not a global. - void drawMe(Segment &seg, bool draw) + void drawMe(Segment &seg, bool draw) { const bool is2D = seg.is2D(); const int gridW = (is2D) ? (int)seg.vWidth() : 1; @@ -7843,12 +7932,21 @@ class MBSphere * and 'y' in the LED array, anti-aliasing the pixels." * Optimize the loop to only working on pixels near the object. Don't do the * whole panel. */ - float edge0 = radius - 0.5f * SPACE_FACTOR; // Soft transition start - float edge1 = radius + 0.5f * SPACE_FACTOR; // Soft transition end - int lowX = floor((x - edge1) * DE_SPACE_FACTOR) - 6; // We don't need to cut it close. - int highX = ceil((x + edge1) * DE_SPACE_FACTOR) + 6; - int lowY = floor((y - edge1) * DE_SPACE_FACTOR) - 6; - int highY = ceil((y + edge1) * DE_SPACE_FACTOR) + 6; + nfixed edge0 = radius - (1 << SPHERE_PREC_SHIFT) / 2; // Soft transition start + nfixed edge1 = radius + (1 << SPHERE_PREC_SHIFT) / 2; // Soft transition end + int lowX = (x - edge1 >> SPHERE_PREC_SHIFT) - 1; // We don't need to cut it too close. + int highX = (x + edge1 >> SPHERE_PREC_SHIFT) + 2; + int lowY = ((y - edge1 >> SPHERE_PREC_SHIFT)) - 1; + int highY = ((y + edge1 >> SPHERE_PREC_SHIFT)) + 2; + + // If completely off the screen, stop it, to avoid an overflow. + if (lowX > gridW || highX < 0 || lowY > gridH || highY < 0) + { + vx = 0; + vy = 0; + } + + // Don't calculate beyond the edges of the LED array. if (lowX < 0) lowX = 0; if (highX > gridW) @@ -7857,23 +7955,30 @@ class MBSphere lowY = 0; if (highY > gridH) highY = gridH; + + /* Loop over a range of pixels on a panel to see how bright the LEDs + * there should be to represent this object. */ for (int lY = lowY; lY < highY; lY++) { for (int lX = lowX; lX < highX; lX++) { // LED pixel center in high-resolution space - float pixelX = (lX + 0.5f) * SPACE_FACTOR; - float pixelY = (lY + 0.5f) * SPACE_FACTOR; + const nfixed halfPixel = 1 << (SPHERE_PREC_SHIFT - 1); + nfixed pixelX = (lX << SPHERE_PREC_SHIFT) + halfPixel; + nfixed pixelY = (lY << SPHERE_PREC_SHIFT) + halfPixel; // Distance from the circle center - float dist = sqrt((pixelX - x) * (pixelX - x) + (pixelY - y) * (pixelY - y)); + nfixed dist = fixedDist(pixelX - x, pixelY - y); // Compute anti-aliasing weight - float alpha = clamp(1.0f - smoothstep(edge0, edge1, dist), 0.0f, 1.0f); + // float alpha = RGBEffect::clamp(1.0f - RGBEffect::smoothstep(FLOAT_IT(edge0), FLOAT_IT(edge1), dist), 0.0f, 1.0f); + nfixed alpha = clamp((1 << SPHERE_PREC_SHIFT) - smoothstep(edge0, edge1, dist), 0, 1 << SPHERE_PREC_SHIFT) + 0; + // nfixed alpha = clamp((1 << SPHERE_PREC_SHIFT) - smoothstep(edge0, edge1, dist), 1 << (SPHERE_PREC_SHIFT - 2), 1 << SPHERE_PREC_SHIFT); + // alpha = 1 << SPHERE_PREC_SHIFT; // Store intensity in LED array (0-1 range) if (draw) { drawColor = sphereColor; - drawColor.nscale8(alpha * 255); + drawColor.nscale8(alpha * 255 >> SPHERE_PREC_SHIFT); } else drawColor = CRGB::Black; @@ -7888,6 +7993,16 @@ class MBSphere } } } + +#if false + // For diagnotistics only. + void print(int instNo) + { + Serial.printf("No. %d, x = %.2f, y = %.2f, vx = %.2f, vy = %.2f, radius = %.2f, density = %.2f, mass = %.2f\n", instNo, + FLOAT_IT(x), FLOAT_IT(y), FLOAT_IT(vx), FLOAT_IT(vy), + FLOAT_IT(radius), FLOAT_IT(_density), FLOAT_IT(mass())); + } +#endif }; // Given 0-255 from SEGMENT.custom2, return in number of 50ms cycles. @@ -7912,9 +8027,29 @@ uint32_t elasticLifetime() return 36000; // 30m case 7: return 72000; // 1hr + default: + return 1200; } } +/* We want of range from 0.1->1->10. + * Thank you Claude.ai. */ +nfixed sliderToSpeed(uint8_t slider) +{ + // Q16.16 quadratic coefficients (calculated from your 3 points) + const int32_t a_q16 = 8; // ~0.000148 in Q16.16 (much smaller!) + const int32_t b_q16 = 300; // ~0.004336 in Q16.16 + const int32_t c_q16 = 6554; // ~0.1 in Q16.16 + + // slider is 0-255 + int64_t x = slider; + + // Calculate ax² + bx + c in Q16.16 + int64_t result = ((int64_t)a_q16 * x * x) + ((int64_t)b_q16 * x) + c_q16; + + return (int32_t)result; +} + uint16_t mode_ElasticCollisions(void) { // by Nicholas Pisarro, Jr. int numSpheres = 1 + (SEGMENT.intensity * 29) / 255; // 1-30 @@ -7934,9 +8069,9 @@ uint16_t mode_ElasticCollisions(void) { // by Nicholas Pisarro, Jr. const int rows = (is2D) ? SEG_H : SEGLEN; // Make a virtual coordinate space that is SPACE_FACTOR times the led array. - const int internalX = SPACE_FACTOR * cols; - const int internalY = SPACE_FACTOR * rows; - const int halfInternalY = internalY >> 1; + const nfixed internalX = cols << SPHERE_PREC_SHIFT; + const nfixed internalY = rows << SPHERE_PREC_SHIFT; + const nfixed halfInternalY = internalY >> 1; // Radius distribution. const int dmTableSize = 20; @@ -7956,20 +8091,24 @@ uint16_t mode_ElasticCollisions(void) { // by Nicholas Pisarro, Jr. if ((SEGMENT.aux0 & SPHERES_DESIRED) == 0) { SEGMENT.aux0 &= SPHERES_DESIRED; - const float complementUniformity = 100 - ((int32_t) SEGMENT.custom1) * 100 / 255; + const int32_t complementUniformity = 100 - ((int32_t) SEGMENT.custom1) * 100 / 255; for (int i = 0; i < numSpheres; ++i) { // Diameter is based on the uniformity. - float radius = 10.0 + skewedRandom(random(100), dmTableSize, dmPercentages) * complementUniformity / 250.0; // 10-50 - radius *= 0.7; - float massFactor = 15.0 / radius * SLOWDOWN_FACTOR; // Big things should move slower to keep momentum down. - float vx = (-50.0 + random(100)) * massFactor / 5.0; // ±10 - float vy = (-50.0 + random(100)) * massFactor * complementUniformity / 500.0; // ±10 + // radius = (250 + skewedRandom(random(100), dmTableSize, dmPercentages) * complementUniformity << SPHERE_PREC_SHIFT) / 250; // 5-25 + nfixed radius = (7 << 16) + ((((uint64_t)skewedRandom(random(100), dmTableSize, dmPercentages) * complementUniformity << 16) / 10000) * (23 << 16) >> 16);// 7-30 + // radius = 30 << SPHERE_PREC_SHIFT; + nfixed massFactor = MBSphere::fixedDiv((11 << SPHERE_PREC_SHIFT), radius); // Big things should move slower to keep momentum down. + nfixed vx = (-50.0 + random(100)) * massFactor / 5.0; // ±10 + nfixed vy = (-50.0 + random(100)) * massFactor * complementUniformity / 500.0; // ±10 + radius /= 10; + vx /= 10; + vy /= 10; if (complementUniformity == 0) // Just one sphere has motion intially, if uniformity = 100%. { if (i == 0) - vx = 5; + vx = 1 << (SPHERE_PREC_SHIFT - 1); // 0.5 else vx = 0; } @@ -7988,8 +8127,8 @@ uint16_t mode_ElasticCollisions(void) { // by Nicholas Pisarro, Jr. { // Give it a random location—closer to the vertical center based on the uniformity. // (Gotcha! WLED random() returns unsigned. It can't go negative.) - float x = random(internalX); - float y = halfInternalY + (((int32_t)(random(internalY)) - halfInternalY) * complementUniformity / 100.0); + nfixed x = random(internalX); + nfixed y = halfInternalY + (((int32_t)(random(internalY)) - halfInternalY) * complementUniformity / 100) & 0xffff0000; if (!is2D) { y = random(internalY); @@ -8031,13 +8170,11 @@ uint16_t mode_ElasticCollisions(void) { // by Nicholas Pisarro, Jr. spheres[i].drawMe(SEGMENT, true); // Move the spheres and check for collisions with the walls. - float a = 0.0002503; // We want of range from 0.1->1->10. - float b = -0.01347; - float c = 0.1; - float speed = SEGMENT.speed; - speed = a * speed * speed + b * speed + c; + // We want of range from 0.1->1->10. + nfixed speed = sliderToSpeed(SEGMENT.speed); for (int i = 0; i < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++i) { + // nfixed fixedSpeed = speed * (1 << SPHERE_PREC_SHIFT); spheres[i].update(speed); // If nearing a regeneration, let the walls fall and the spheres fly off! @@ -8069,7 +8206,7 @@ uint16_t mode_ElasticCollisions(void) { // by Nicholas Pisarro, Jr. return FRAMETIME; } // mode_ElasticCollisions -static const char _data_FXMODE_ELASTICCOLLISIONS[] PROGMEM = "Elastic Collisions@Speed,Count,Uniformity,Lifetime;;!;12;c1=0,sx=120,c2=64"; +static const char _data_FXMODE_ELASTICCOLLISIONS[] PROGMEM = "Elastic Collisions@Speed,Count,Uniformity,Lifetime;;!;12;c1=0,sx=90,c2=64"; // Distortion waves - ldirko From 790a6160cd6cc8e37aa3428b6849d8616c597e9f Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Mon, 22 Sep 2025 18:23:53 -0400 Subject: [PATCH 11/31] It turns out that because of memory alignment, 'XTwinkleLight' was taking 8 bytes. Redefine 'XTwinkleLight' structure to use individual variables, yet still consume 8 bytes-simplifying code. --- wled00/FX.cpp | 53 ++++++++++++++++++++++----------------------------- 1 file changed, 23 insertions(+), 30 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 26ded9ffd1..1e09f91d93 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7481,22 +7481,15 @@ static const char _data_FX_MODE_2DAKEMI[] PROGMEM = "Akemi@Color speed,Dance;Hea // Xmas Twinkle // ///////////////////////// -/* We need to keep data for each twinkle light. - * Except for the color, we smash all other data into a single - * uint32_t to keep memory short. We use time in centiseconds. - * Be careful to not overflow the limited size of these timers. */ +// We need to keep data for each twinkle light. 8 bytes/light typedef struct XTwinkleLight { + int16_t timeToEvent; + int16_t maxCycle; + int16_t retwnkleTime; uint8_t colorIdx; - uint32_t twData; - -// (Be aware of operator precedent when accessing & modifying.) -// (Tried using C++ bit fields, but code broke.) -#define TWINKLE_ON 0x80000000 // 1 bit -#define TIME_TO_EVENT 0x7fe00000 // 10 bits >> 21 -#define TIME_TO_EVENT_SHIFT 21 -#define MAX_CYCLE 0x001ff800 // 10 bits >> 11 -#define MAX_CYCLE_SHIFT 11 -#define T_RETWINKLE 0x000007ff // 11 bits >> 0 + + uint8_t flags; +#define TWINKLE_ON 0x01 } XTwinkleLight; // For creating skewed random numbers toward the shorter end. @@ -7504,7 +7497,6 @@ typedef struct XTwinkleLight { const uint8_t pSize = 20; const uint8_t percentages[pSize] = {12, 11, 10, 10, 6, 6, 5, 5, 3, 3, 1, 1, 1, 1, 1, 1, 2, 3, 3, 15}; const uint8_t slowPercentages[pSize] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 6, 7, 7, 7, 10, 12, 12, 15, 19}; -uint8_t wkgPercentages[pSize]; // Input is 0-100, Ouput is skewed 0-100. // PArray may be any size, but elements must add up to 100. @@ -7557,6 +7549,7 @@ uint16_t mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. // We have two tables, one of 'normal' weights, 1 of slow weights. // use more of the slow percentages in he last quarter of the segment times. + uint8_t wkgPercentages[pSize]; slowWeight = (slowWeight - 0.75) * 4; if (slowWeight < 0) slowWeight = 0.0; @@ -7577,12 +7570,12 @@ uint16_t mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. XTwinkleLight *light = &twinklers[i]; light->colorIdx = random8(); - light->twData = 0; // Everything 0 + light->flags = 0; int cycleTime = skewedRandom(random(100), pSize, wkgPercentages) * maximumTime / 100 + 20; - light->twData |= cycleTime << MAX_CYCLE_SHIFT & MAX_CYCLE; - light->twData |= random(50, cycleTime) << TIME_TO_EVENT_SHIFT & TIME_TO_EVENT; - light->twData |= (random(2, 20) * 100) & T_RETWINKLE; // 2 - 20 seconds 1st time around + light->maxCycle = cycleTime; + light->timeToEvent = random(50, cycleTime); + light->retwnkleTime = random(2, 20) * 100; // 2 - 20 seconds 1st time around } SEGMENT.step = millis(); @@ -7605,34 +7598,34 @@ uint16_t mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. XTwinkleLight *light = &twinklers[i]; // See if we are at the end of twinkle on o off cycle. - int16_t eventTime = ((light->twData & TIME_TO_EVENT) >> TIME_TO_EVENT_SHIFT) - interval; + int16_t eventTime = light->timeToEvent - interval; if (eventTime <= 0) { // Twinkle on cycles are 1/3 length of twinkle off cycles. We're' twinkling after all. - if (light->twData & TWINKLE_ON) - eventTime += random(50, ((light->twData & MAX_CYCLE) >> MAX_CYCLE_SHIFT)); // turn OFF + if (light->flags & TWINKLE_ON) + eventTime += random(50, light->maxCycle); // turn OFF else { // Based on the check box, either use a constant palette index or a new one each time it turns on. if (SEGMENT.check1) - light->colorIdx = random8(); - eventTime += random(10, ((light->twData & MAX_CYCLE) >> MAX_CYCLE_SHIFT) / 3); // turn ON + light->colorIdx = random8(); + eventTime += random(10, light->maxCycle / 3); // turn ON } - light->twData ^= TWINKLE_ON; + light->flags ^= TWINKLE_ON; } // Put the updated event time back. - light->twData = (light->twData & ~TIME_TO_EVENT) | (eventTime << TIME_TO_EVENT_SHIFT & TIME_TO_EVENT); + light->timeToEvent = eventTime; // See if we are at the end of a major cycle, recalculate the max cycle time. - int16_t cycleTime = (light->twData & T_RETWINKLE) - interval; + int16_t cycleTime = light->retwnkleTime - interval; if (cycleTime <= 0) { int maxTime = skewedRandom(random(100), pSize, wkgPercentages) * maximumTime / 100 + 20; - light->twData = (light->twData & ~MAX_CYCLE) | (maxTime << MAX_CYCLE_SHIFT & MAX_CYCLE); + light->maxCycle = maxTime; cycleTime += 2000; // 20 seconds } - light->twData = (light->twData & ~T_RETWINKLE) | (cycleTime & T_RETWINKLE); + light->retwnkleTime = cycleTime; } // Remember the last time as ms. @@ -7647,7 +7640,7 @@ uint16_t mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. { XTwinkleLight *light = &twinklers[i]; - if ((light->twData & TWINKLE_ON) == 0) + if ((light->flags & TWINKLE_ON) == 0) continue; // Compute the offset of the light in the string. From a308a488d678d4b46310b59550835c4756f019e8 Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Tue, 23 Sep 2025 16:35:40 -0400 Subject: [PATCH 12/31] Removed floating point remnant in Xmas Twinkle, use milliseconds instead of centiseconds for time. --- wled00/FX.cpp | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 1e09f91d93..4b55284fd6 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7536,27 +7536,24 @@ void weightPercentages(const uint8_t *arg1, uint16_t mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. uint16_t numTwiklers = SEGLEN * SEGMENT.intensity / 255; - if (numTwiklers <= 1) - numTwiklers = 2; // Divide checks are not cool. + if (numTwiklers <= 0) + numTwiklers = 1; // Divide checks are not cool. // Reinitialize evertying if the number of twinklers has changed. if (numTwiklers != SEGMENT.aux0) SEGMENT.aux0 = 0; // The maximum twinkle time varies based on the time slider - float slowWeight = (255 - SEGMENT.speed) / 255.0; // 0.0 - 1.0 - int32_t maximumTime = (slowWeight * 900.0) + 100.0; // Between 100 & 1000 centiseconds + int32_t slowWeight = (255 - SEGMENT.speed << RAND_PREC_SHIFT) / 255; // 0.0 - 1.0 shifted + int32_t maximumTime = (slowWeight * 9000) + 1000 >> RAND_PREC_SHIFT; // Between 1000 & 10000 milliseconds // We have two tables, one of 'normal' weights, 1 of slow weights. // use more of the slow percentages in he last quarter of the segment times. uint8_t wkgPercentages[pSize]; - slowWeight = (slowWeight - 0.75) * 4; + slowWeight = (slowWeight - /* 0.75 */ 768) * 4; // (0.75 << RAND_PREC_SHIFT) if (slowWeight < 0) - slowWeight = 0.0; - weightPercentages(percentages, slowPercentages, pSize, slowWeight * (1 << RAND_PREC_SHIFT), wkgPercentages); - - // uint8_t flasherDistance = ((255 - SEGMENT.intensity) / 28) +1; //1-10 - // uint16_t numFlashers = (SEGLEN / flasherDistance) +1; + slowWeight = 0; + weightPercentages(percentages, slowPercentages, pSize, slowWeight, wkgPercentages); uint16_t dataSize = sizeof(XTwinkleLight) * numTwiklers; if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed @@ -7571,11 +7568,11 @@ uint16_t mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. light->colorIdx = random8(); light->flags = 0; - int cycleTime = skewedRandom(random(100), pSize, wkgPercentages) * maximumTime / 100 + 20; + int32_t cycleTime = skewedRandom(random(100), pSize, wkgPercentages) * maximumTime / 100 + 200; light->maxCycle = cycleTime; - light->timeToEvent = random(50, cycleTime); - light->retwnkleTime = random(2, 20) * 100; // 2 - 20 seconds 1st time around + light->timeToEvent = random(500, cycleTime); + light->retwnkleTime = random(2, 20) * 1000; // 2 - 20 seconds 1st time around } SEGMENT.step = millis(); @@ -7588,9 +7585,8 @@ uint16_t mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. if (currTime < lastTime) lastTime = 0; - // We're doing our work in centiseconds so we don't overflow our 10 bit counters. // The interval may be zero if the refresh rate is fast enought. - uint32_t interval = (currTime - lastTime) / 10; + uint32_t interval = currTime - lastTime; // Note the time passed to the LEDs, and process any events that occured. for (int i = 0; i < numTwiklers; ++i) @@ -7603,13 +7599,13 @@ uint16_t mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. { // Twinkle on cycles are 1/3 length of twinkle off cycles. We're' twinkling after all. if (light->flags & TWINKLE_ON) - eventTime += random(50, light->maxCycle); // turn OFF + eventTime += random(500, light->maxCycle); // turn OFF else { // Based on the check box, either use a constant palette index or a new one each time it turns on. if (SEGMENT.check1) light->colorIdx = random8(); - eventTime += random(10, light->maxCycle / 3); // turn ON + eventTime += random(100, light->maxCycle / 3); // turn ON } light->flags ^= TWINKLE_ON; @@ -7621,15 +7617,15 @@ uint16_t mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. int16_t cycleTime = light->retwnkleTime - interval; if (cycleTime <= 0) { - int maxTime = skewedRandom(random(100), pSize, wkgPercentages) * maximumTime / 100 + 20; + int maxTime = skewedRandom(random(100), pSize, wkgPercentages) * maximumTime / 100 + 200; light->maxCycle = maxTime; - cycleTime += 2000; // 20 seconds + cycleTime += 20000; // 20 seconds } light->retwnkleTime = cycleTime; } // Remember the last time as ms. - SEGMENT.step += interval * 10; + SEGMENT.step += interval; // Turm off all the LEDS. for (int i = 0; i < SEGLEN; ++i) From 94df6ea452f58fcf0adcb67236af8f6dcde70abb Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Tue, 23 Sep 2025 17:30:36 -0400 Subject: [PATCH 13/31] Document use of SEGMENT 'user' variables by Xmas Twinkle. Make effect much more responsive to speed slider changes. --- wled00/FX.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 4b55284fd6..a321bdc529 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7535,6 +7535,12 @@ void weightPercentages(const uint8_t *arg1, } uint16_t mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. + /* SEGMENT usage: + * aux0 number of twinklers + * aux1 previous SEGMENT.speed + * step last time stamp + * data array of XTwinkleLight structure + */ uint16_t numTwiklers = SEGLEN * SEGMENT.intensity / 255; if (numTwiklers <= 0) numTwiklers = 1; // Divide checks are not cool. @@ -7577,6 +7583,7 @@ uint16_t mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. SEGMENT.step = millis(); SEGMENT.aux0 = numTwiklers; // Initialized. + SEGMENT.aux1 = SEGMENT.speed; // So we don't recalculate reTwinkle time. } // Get the current time, handling overflows. @@ -7613,9 +7620,9 @@ uint16_t mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. // Put the updated event time back. light->timeToEvent = eventTime; - // See if we are at the end of a major cycle, recalculate the max cycle time. + // If we are at the end of a major cycle or the speed has changed, recalculate the max cycle time. int16_t cycleTime = light->retwnkleTime - interval; - if (cycleTime <= 0) + if (cycleTime <= 0 || SEGMENT.aux1 != SEGMENT.speed) { int maxTime = skewedRandom(random(100), pSize, wkgPercentages) * maximumTime / 100 + 200; light->maxCycle = maxTime; @@ -7626,6 +7633,7 @@ uint16_t mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. // Remember the last time as ms. SEGMENT.step += interval; + SEGMENT.aux1 = SEGMENT.speed; // Se we know if this change. // Turm off all the LEDS. for (int i = 0; i < SEGLEN; ++i) From f07c87347e6f5ce8d820611c593e79ccb98b90e5 Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Mon, 6 Oct 2025 08:18:23 -0400 Subject: [PATCH 14/31] Tweaks. --- wled00/FX.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index a321bdc529..f223e26fe5 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7495,8 +7495,8 @@ typedef struct XTwinkleLight { // For creating skewed random numbers toward the shorter end. // The sum of percentages must = 100% const uint8_t pSize = 20; -const uint8_t percentages[pSize] = {12, 11, 10, 10, 6, 6, 5, 5, 3, 3, 1, 1, 1, 1, 1, 1, 2, 3, 3, 15}; -const uint8_t slowPercentages[pSize] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 6, 7, 7, 7, 10, 12, 12, 15, 19}; +const uint8_t percentages[pSize] = {12, 11, 10, 10, 6, 6, 5, 5, 3, 3, 1, 1, 1, 1, 1, 1, 2, 3, 3, 15}; // PROGMEM? +const uint8_t slowPercentages[pSize] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 6, 7, 7, 7, 10, 12, 12, 15, 19}; // PROGMEM? // Input is 0-100, Ouput is skewed 0-100. // PArray may be any size, but elements must add up to 100. @@ -7546,7 +7546,7 @@ uint16_t mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. numTwiklers = 1; // Divide checks are not cool. // Reinitialize evertying if the number of twinklers has changed. - if (numTwiklers != SEGMENT.aux0) + if (numTwiklers != SEGMENT.aux0 || SEGMENT.call == 0) SEGMENT.aux0 = 0; // The maximum twinkle time varies based on the time slider From 416616d5c59bb2613959d4f001149b3a27bb3863 Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Mon, 20 Apr 2026 10:09:58 -0400 Subject: [PATCH 15/31] Put Xmas Twinkle and Elastic collision back after fixed changed conventions. --- wled00/FX.cpp | 732 ++++++++++++++++++++++++++++++++++++++++++++++++++ wled00/FX.h | 4 +- 2 files changed, 735 insertions(+), 1 deletion(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 18d61b2167..750d9e7553 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7746,6 +7746,735 @@ void mode_2DAkemi(void) { static const char _data_FX_MODE_2DAKEMI[] PROGMEM = "Akemi@Color speed,Dance;Head palette,Arms & Legs,Eyes & Mouth;Face palette;2f;si=0"; //beatsin +///////////////////////// +// Xmas Twinkle // +///////////////////////// + +// We need to keep data for each twinkle light. 8 bytes/light +typedef struct XTwinkleLight { + int16_t timeToEvent; + int16_t maxCycle; + int16_t retwnkleTime; + uint8_t colorIdx; + + uint8_t flags; +#define TWINKLE_ON 0x01 +} XTwinkleLight; + +// For creating skewed random numbers toward the shorter end. +// The sum of percentages must = 100% +const uint8_t pSize = 20; +const uint8_t percentages[pSize] = {12, 11, 10, 10, 6, 6, 5, 5, 3, 3, 1, 1, 1, 1, 1, 1, 2, 3, 3, 15}; // PROGMEM? +const uint8_t slowPercentages[pSize] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 6, 7, 7, 7, 10, 12, 12, 15, 19}; // PROGMEM? + +// Input is 0-100, Ouput is skewed 0-100. +// PArray may be any size, but elements must add up to 100. +#define RAND_PREC_SHIFT 10 // Vertual binary point from the right +int32_t skewedRandom( uint8_t rand100, + const uint8_t pArraySize, + const uint8_t *pArray) +{ + int32_t index = 0; + int32_t cumulativePercentage = 0; + + // Find the range in the table based on randomValue. + while (index < pArraySize - 1 && rand100 >= cumulativePercentage + pArray[index]) { + cumulativePercentage += pArray[index]; + index++; + } + + // Calculate linear interpolation + int32_t t = ((rand100 - cumulativePercentage) << RAND_PREC_SHIFT) / pArray[index]; + int32_t result = ((index << RAND_PREC_SHIFT) + t) * 100 / pArraySize >> RAND_PREC_SHIFT; + + return result; +} + +// Take two percentage tables and average them using the weighting factor. +// Both tables and the result must be the same size. +void weightPercentages(const uint8_t *arg1, + const uint8_t *arg2, + const int cnt, + const uint32_t factor, // 0.0-1.0 weight given to arg2 << RAND_PREC_SHIFT + uint8_t *result) +{ + uint32_t arg1Factor = (1 << RAND_PREC_SHIFT) - factor; + for (int i = 0; i < cnt; ++i) + result[i] = arg1[i] * arg1Factor + arg2[i] * factor >> RAND_PREC_SHIFT; +} + +void mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. + /* SEGMENT usage: + * aux0 number of twinklers + * aux1 previous SEGMENT.speed + * step last time stamp + * data array of XTwinkleLight structure + */ + uint16_t numTwiklers = SEGLEN * SEGMENT.intensity / 255; + if (numTwiklers <= 0) + numTwiklers = 1; // Divide checks are not cool. + + // Reinitialize evertying if the number of twinklers has changed. + if (numTwiklers != SEGMENT.aux0 || SEGMENT.call == 0) + SEGMENT.aux0 = 0; + + // The maximum twinkle time varies based on the time slider + int32_t slowWeight = (255 - SEGMENT.speed << RAND_PREC_SHIFT) / 255; // 0.0 - 1.0 shifted + int32_t maximumTime = (slowWeight * 9000) + 1000 >> RAND_PREC_SHIFT; // Between 1000 & 10000 milliseconds + + // We have two tables, one of 'normal' weights, 1 of slow weights. + // use more of the slow percentages in he last quarter of the segment times. + uint8_t wkgPercentages[pSize]; + slowWeight = (slowWeight - /* 0.75 */ 768) * 4; // (0.75 << RAND_PREC_SHIFT) + if (slowWeight < 0) + slowWeight = 0; + weightPercentages(percentages, slowPercentages, pSize, slowWeight, wkgPercentages); + + uint16_t dataSize = sizeof(XTwinkleLight) * numTwiklers; + if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed + XTwinkleLight* twinklers = reinterpret_cast(SEGENV.data); + + // Initialize the twinkle lights. + if (SEGMENT.aux0 == 0) + { + for (int i = 0; i < numTwiklers; ++i) + { + XTwinkleLight *light = &twinklers[i]; + + light->colorIdx = hw_random8(); + light->flags = 0; + int32_t cycleTime = skewedRandom(random(100), pSize, wkgPercentages) * maximumTime / 100 + 200; + + light->maxCycle = cycleTime; + light->timeToEvent = random(500, cycleTime); + light->retwnkleTime = random(2, 20) * 1000; // 2 - 20 seconds 1st time around + } + + SEGMENT.step = millis(); + SEGMENT.aux0 = numTwiklers; // Initialized. + SEGMENT.aux1 = SEGMENT.speed; // So we don't recalculate reTwinkle time. + } + + // Get the current time, handling overflows. + uint32_t lastTime = SEGMENT.step; + uint32_t currTime = millis(); + if (currTime < lastTime) + lastTime = 0; + + // The interval may be zero if the refresh rate is fast enought. + uint32_t interval = currTime - lastTime; + + // Note the time passed to the LEDs, and process any events that occured. + for (int i = 0; i < numTwiklers; ++i) + { + XTwinkleLight *light = &twinklers[i]; + + // See if we are at the end of twinkle on o off cycle. + int16_t eventTime = light->timeToEvent - interval; + if (eventTime <= 0) + { + // Twinkle on cycles are 1/3 length of twinkle off cycles. We're' twinkling after all. + if (light->flags & TWINKLE_ON) + eventTime += random(500, light->maxCycle); // turn OFF + else + { + // Based on the check box, either use a constant palette index or a new one each time it turns on. + if (SEGMENT.check1) + light->colorIdx = hw_random8(); + eventTime += random(100, light->maxCycle / 3); // turn ON + } + + light->flags ^= TWINKLE_ON; + } + // Put the updated event time back. + light->timeToEvent = eventTime; + + // If we are at the end of a major cycle or the speed has changed, recalculate the max cycle time. + int16_t cycleTime = light->retwnkleTime - interval; + if (cycleTime <= 0 || SEGMENT.aux1 != SEGMENT.speed) + { + int maxTime = skewedRandom(random(100), pSize, wkgPercentages) * maximumTime / 100 + 200; + light->maxCycle = maxTime; + cycleTime += 20000; // 20 seconds + } + light->retwnkleTime = cycleTime; + } + + // Remember the last time as ms. + SEGMENT.step += interval; + SEGMENT.aux1 = SEGMENT.speed; // Se we know if this change. + + // Turm off all the LEDS. + for (int i = 0; i < SEGLEN; ++i) + SEGMENT.setPixelColor(i, CRGB::Black); + + // Turn on only those leds that should be. + for (int i = 0; i < numTwiklers; ++i) + { + XTwinkleLight *light = &twinklers[i]; + + if ((light->flags & TWINKLE_ON) == 0) + continue; + + // Compute the offset of the light in the string. + short inset = i * SEGLEN / numTwiklers; + if (inset > SEGLEN) // Safety + break; + + SEGMENT.setPixelColor(inset, ColorFromPalette(SEGPALETTE,light->colorIdx)); + } + + return; +} // mode_XmasTwinkle +static const char _data_FX_MODE_XMASTWINKLE[] PROGMEM = "Xmas Twinkle@Twinkle speed,Density,,,,Color indices vary;;!;012;m12=0"; + +//////////////////////////// +// Elastic Collisions // +//////////////////////////// + +// For print diagnostics, only. + #define FLOAT_IT(x) ((float)(x) / (1 << SPHERE_PREC_SHIFT)) + + /* Note: When you multiply two fixed numbers, the binary point shifts left by the sum of + * binary points. In division the binary point shift right by the difference between + * divident - divisor. */ +#define SPHERE_PREC_SHIFT 16 // Vertual binary point from the right +typedef int32_t nfixed; // These represent fixed point fractional numbers as Q16.16 + +#define SLOWDOWN_FACTOR 0.4 // (Make this a variable?) for very large spheres. +#define BOUNCE_CYCLE_TIME 50 // ms. +#define RESET_CYCLE_TIME 1200 // Number of cycles (60 * 1000 / 50) +#define WALL_COLLAPSE_INTR 125 // Cycles left till regen. + +// --- Portable countLeadingZeros64 for faster SQRT --- +int countLeadingZeros64(uint64_t x) +{ +#if defined(__GNUC__) || defined(__clang__) + return __builtin_clzll(x); +#else + if (x == 0) return 64; + int n = 0; + uint64_t mask = 1ULL << 63; + while ((x & mask) == 0) { + n++; + mask >>= 1; + } + return n; +#endif +} + +class MBSphere +{ + nfixed x, y; // Position + nfixed vx, vy; // Velocity + nfixed radius; // Radius + nfixed _density = (1 << SPHERE_PREC_SHIFT); // Density is 1 for bouncing, other values for gravity + uint8_t colorIdx; +#if false + AbstractList *attrocters; // Null unless this object is affected by gravity. +#endif + + +public: + MBSphere(nfixed radius, nfixed x, nfixed y, nfixed vx, nfixed vy, int8_t color) + : x(x), y(y), vx(vx), vy(vy), radius(radius), colorIdx(color) /*, attrocters(nullptr) */ + { + } + ~MBSphere() { } +#if false + // For effects with gravity. + void addAttractor(MBSphere *sp) + { + if (! attrocters) + attrocters = new List; + + attrocters->add(sp); + } +#endif + nfixed density() { return _density; } + void setDensity(nfixed newD) { _density = newD; } + nfixed mass() { return fixedMult(fixedMult(fixedMult(radius, radius), radius), density()); } + + static nfixed fixedMult(nfixed a, nfixed b) + { + return (int64_t)a * b >> SPHERE_PREC_SHIFT; + } + + static nfixed fixedDiv(nfixed a, nfixed b) + { + return ((int64_t)a << SPHERE_PREC_SHIFT) / b; + } + + static nfixed fixedSqrt(nfixed x) + { + // Promote to 64-bit and scale up for precision + uint64_t n = (uint64_t)x << SPHERE_PREC_SHIFT; // Q16.16 -> Q32.32 + return fixed64Sqrt(n); + } + + // Faster SQRT function curtesy Code Copilot 5. + static nfixed fixed64Sqrt(int64_t n) + { + if (n <= 0) return 0; + + // Initial guess from highest bit. + int lz = 63 - countLeadingZeros64(n); + uint64_t res = 1ULL << (lz / 2); + + // Newton–Raphson refinement (3–4 iterations are plenty) + res = (res + n / res) >> 1; + res = (res + n / res) >> 1; + res = (res + n / res) >> 1; + + // Clamp back to 32-bit Q16.16 + return (nfixed)res; + } + + /* Squaring coordinates can blow out the range of nfixed. + * Work with the 64 bit intermediate result. */ + static nfixed fixedDist(nfixed a, nfixed b) + { + int64_t n = (int64_t)a * a + (int64_t)b * b; + return fixed64Sqrt(n); + } + + // Update the sphere's position and velocity + void update(nfixed dt) { x += fixedMult(vx, dt); y += fixedMult(vy, dt); } + void newLoc(nfixed newX, nfixed newY) { x = newX; y = newY; } + + // Detect if two circles are colliding (simple distance check) + bool areSpheresColliding(MBSphere sp) + { + nfixed dist = fixedDist(sp.x - this->x, sp.y - this->y); + return dist <= this->radius + sp.radius; + } + + /* Make sure two spheres haven't gotten too close. + * Note: There is a pathological case where two spheres + * can crash into each other so hard, that one actually + * ends up insde the other. This function prevents that. */ + void enforceMinDist(MBSphere *sp) + { + nfixed dist = radius + sp->radius; + + nfixed dx = sp->x - x; + nfixed dy = sp->y - y; + nfixed length = fixedDist(dx, dy); + + if (length >= dist || length == 0.0) + return; // Already long enough, or degenerate point + + // Normalize direction + if (length << 1 == 0) + { + // handle gracefully, but this shouldn't happen. + Serial.println("At 0 #1"); + return; + } + nfixed scale = fixedDiv(dist - length, length << 1); + + nfixed offsetX = fixedMult(dx, scale); + nfixed offsetY = fixedMult(dy, scale); + + x -= offsetX; + y -= offsetY; + sp->x += offsetX; + sp->y += offsetY; + } + + // Function to simulate the elastic collision and update velocities + void handleCollision(MBSphere *sp, bool is2D) + { + nfixed m1 = this->mass(); + nfixed m2 = sp->mass(); + + // Calculate the normal and tangent vectors + nfixed nx = sp->x - x; + nfixed ny = sp->y - y; + nfixed dist = fixedDist(nx, ny); + while (dist == 0) { + // handle gracefully + Serial.println("Two objects on top of each other!"); + + x += 1 << (SPHERE_PREC_SHIFT -2); + nx += 1 << (SPHERE_PREC_SHIFT -2); + dist = fixedDist(nx, ny); + } + nx = fixedDiv(nx, dist); + ny = fixedDiv(ny, dist); + + // Tangent is perpendicular to normal + nfixed tx = -ny; + nfixed ty = nx; + + // Use canned values if 1D, otherwise an x velocity creeps in. + if (!is2D) + { + nx = 0; + ny = ((sp->y >= y) ? 1 : -1) << SPHERE_PREC_SHIFT; + tx = -ny; + ty = 0; + } + + // Project velocities onto the normal and tangent + nfixed v1n = fixedMult(vx, nx) + fixedMult(vy, ny); + nfixed v1t = fixedMult(vx, tx) + fixedMult(vy, ty); + nfixed v2n = fixedMult(sp->vx, nx) + fixedMult(sp->vy, ny); + nfixed v2t = fixedMult(sp->vx, tx) + fixedMult(sp->vy, ty); + + // Apply 1D elastic collision for the normal components + nfixed v1n_final = fixedDiv(fixedMult(v1n, m1 - m2) + fixedMult(2 * m2, v2n), m1 + m2); + nfixed v2n_final = fixedDiv(fixedMult(v2n, m2 - m1) + fixedMult(2 * m1, v1n), m1 + m2); + + // Final velocity vectors (tangential velocity remains the same) + vx = fixedMult(v1n_final, nx) + fixedMult(v1t, tx); + vy = fixedMult(v1n_final, ny) + fixedMult(v1t, ty); + sp->vx = fixedMult(v2n_final, nx) + fixedMult(v2t, tx); + sp->vy = fixedMult(v2n_final, ny) + fixedMult(v2t, ty); + } + + // Function to handle wall collisions + void handleWallCollision(nfixed windowWidth, nfixed windowHeight) + { + if (x - radius < 0) { + x = radius; // Keep inside the left wall + vx = -vx; // Reverse x velocity + } else if (x + radius > windowWidth) { + x = windowWidth - radius; // Keep inside the right wall + vx = -vx; // Reverse x velocity + } + + if (y - radius < 0) { + y = radius; // Keep inside the top wall + vy = -vy; // Reverse y velocity + } else if (y + radius > windowHeight) { + y = windowHeight - radius; // Keep inside the bottom wall + vy = -vy; // Reverse y velocity + } + } + +#if false + // Apply the force of gravity with another sphere over the time period in ms. + void applyAttractorGravity(long overTime); + void applyGravity(MBSphere *sp, long overTime); + + // Calculate the initial velocity for a circular orbit. + void initializeOrbit(MBSphere *sp, float dx, float dy); +#endif + + nfixed clamp(nfixed value, nfixed minVal, nfixed maxVal) + { + if (value < minVal) return minVal; + if (value > maxVal) return maxVal; + return value; + } + + nfixed smoothstep(nfixed edge0, nfixed edge1, nfixed x) + { + // float t = clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); + // nfixed t = clamp(fixedDiv(x - edge0, edge1 - edge0), 0, 1 << SPHERE_PREC_SHIFT); + // return t * t * (3.0f - 2.0f * t); + + // Use a faster divide and multiply using Q24.8 numbers instead of Q16.16. + edge0 >>= 8; + edge1 >>= 8; + x >>= 8; + int t = clamp((x - edge0 << 8) / (edge1 - edge0), 0, 1 << 8); // Q24.8 + return (t * t >> 8) * ((3 << 8) - 2 * t); // Result of cubing is Q16.16. + } + + // For generality, the spere uses the segment passed in, not a global. + void drawMe(Segment &seg, bool draw) + { + const bool is2D = seg.is2D(); + const int gridW = (is2D) ? (int)seg.vWidth() : 1; + const int gridH = (is2D) ? (int)seg.vHeight() : seg.vLength(); + + CRGB sphereColor = ColorFromPalette(seg.getCurrentPalette(), colorIdx); + CRGB drawColor; + + /* Thank you Code Copilot: "Using C++, I have a coordinate space that is 10 times + * an LED array. I want to draw a solid circle of diameter 'r' and position 'x' + * and 'y' in the LED array, anti-aliasing the pixels." + * Optimize the loop to only working on pixels near the object. Don't do the + * whole panel. */ + nfixed edge0 = radius - (1 << SPHERE_PREC_SHIFT) / 2; // Soft transition start + nfixed edge1 = radius + (1 << SPHERE_PREC_SHIFT) / 2; // Soft transition end + int lowX = (x - edge1 >> SPHERE_PREC_SHIFT) - 1; // We don't need to cut it too close. + int highX = (x + edge1 >> SPHERE_PREC_SHIFT) + 2; + int lowY = ((y - edge1 >> SPHERE_PREC_SHIFT)) - 1; + int highY = ((y + edge1 >> SPHERE_PREC_SHIFT)) + 2; + + // If completely off the screen, stop it, to avoid an overflow. + if (lowX > gridW || highX < 0 || lowY > gridH || highY < 0) + { + vx = 0; + vy = 0; + } + + // Don't calculate beyond the edges of the LED array. + if (lowX < 0) + lowX = 0; + if (highX > gridW) + highX = gridW; + if (lowY < 0) + lowY = 0; + if (highY > gridH) + highY = gridH; + + /* Loop over a range of pixels on a panel to see how bright the LEDs + * there should be to represent this object. */ + for (int lY = lowY; lY < highY; lY++) { + for (int lX = lowX; lX < highX; lX++) { + // LED pixel center in high-resolution space + const nfixed halfPixel = 1 << (SPHERE_PREC_SHIFT - 1); + nfixed pixelX = (lX << SPHERE_PREC_SHIFT) + halfPixel; + nfixed pixelY = (lY << SPHERE_PREC_SHIFT) + halfPixel; + + // Distance from the circle center + nfixed dist = fixedDist(pixelX - x, pixelY - y); + + // Compute anti-aliasing weight + // float alpha = RGBEffect::clamp(1.0f - RGBEffect::smoothstep(FLOAT_IT(edge0), FLOAT_IT(edge1), dist), 0.0f, 1.0f); + nfixed alpha = clamp((1 << SPHERE_PREC_SHIFT) - smoothstep(edge0, edge1, dist), 0, 1 << SPHERE_PREC_SHIFT) + 0; + // nfixed alpha = clamp((1 << SPHERE_PREC_SHIFT) - smoothstep(edge0, edge1, dist), 1 << (SPHERE_PREC_SHIFT - 2), 1 << SPHERE_PREC_SHIFT); + // alpha = 1 << SPHERE_PREC_SHIFT; + + // Store intensity in LED array (0-1 range) + if (draw) + { + drawColor = sphereColor; + drawColor.nscale8(alpha * 255 >> SPHERE_PREC_SHIFT); + } + else + drawColor = CRGB::Black; + + if (alpha > 0.0) + { + if (is2D) + seg.setPixelColorXY(lX, lY, drawColor); + else + seg.setPixelColor(lY, drawColor); + } + } + } + } + +#if false + // For diagnotistics only. + void print(int instNo) + { + Serial.printf("No. %d, x = %.2f, y = %.2f, vx = %.2f, vy = %.2f, radius = %.2f, density = %.2f, mass = %.2f\n", instNo, + FLOAT_IT(x), FLOAT_IT(y), FLOAT_IT(vx), FLOAT_IT(vy), + FLOAT_IT(radius), FLOAT_IT(_density), FLOAT_IT(mass())); + } +#endif +}; + +// Given 0-255 from SEGMENT.custom2, return in number of 50ms cycles. +uint32_t elasticLifetime() +{ + // 8 categories. + switch (SEGMENT.custom2 >> 5) // /32 + { + case 0: + return 300; // 15s + case 1: + return 600; // 30s + case 2: + return 1200; // 1m + case 3: + return 2400; // 2m + case 4: + return 6000; // 5m + case 5: + return 12000; // 10m + case 6: + return 36000; // 30m + case 7: + return 72000; // 1hr + default: + return 1200; + } +} + +/* We want of range from 0.1->1->10. + * Thank you Claude.ai. */ +nfixed sliderToSpeed(uint8_t slider) +{ + // Q16.16 quadratic coefficients (calculated from your 3 points) + const int32_t a_q16 = 8; // ~0.000148 in Q16.16 (much smaller!) + const int32_t b_q16 = 300; // ~0.004336 in Q16.16 + const int32_t c_q16 = 6554; // ~0.1 in Q16.16 + + // slider is 0-255 + int64_t x = slider; + + // Calculate ax² + bx + c in Q16.16 + int64_t result = ((int64_t)a_q16 * x * x) + ((int64_t)b_q16 * x) + c_q16; + + return (int32_t)result; +} + +void mode_ElasticCollisions(void) { // by Nicholas Pisarro, Jr. + + int numSpheres = 1 + (SEGMENT.intensity * 29) / 255; // 1-30 + + /* + * SEGMENT.aux0.0 = desired number of spheres. + * SEGMENT.aux0.1 = actual number allocated. Might be < aux0.0. + * SEGMENT.step = Next movement intereval + * SEGMENT.aux1 = Next total rebuild as a number of increments. + */ + #define SPHERES_DESIRED 0xff00 + #define SPHERES_DESIRED_SHIFT 8 + #define SPHERES_ALLOCATED 0x00ff + + const bool is2D = strip.isMatrix && SEGMENT.is2D(); + const int cols = (is2D) ? SEG_W : 1; + const int rows = (is2D) ? SEG_H : SEGLEN; + + // Make a virtual coordinate space that is SPACE_FACTOR times the led array. + const nfixed internalX = cols << SPHERE_PREC_SHIFT; + const nfixed internalY = rows << SPHERE_PREC_SHIFT; + const nfixed halfInternalY = internalY >> 1; + + // Radius distribution. + const int dmTableSize = 20; + const uint8_t dmPercentages[20] = {40, 20, 10, 4, 3, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3}; + + // Reinitialize evertying if the number of spheres has changed. + // (We need a separate counter for the number wanted, vs. the number actually initialized.) + if (numSpheres != ((SEGMENT.aux0 & SPHERES_DESIRED) >> SPHERES_DESIRED_SHIFT)) + SEGMENT.aux0 = 0; + + // Point to the sheres. + uint16_t dataSize = sizeof(MBSphere) * numSpheres; + if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed + MBSphere* spheres = reinterpret_cast(SEGENV.data); + + // Initialize the spheres. + if ((SEGMENT.aux0 & SPHERES_DESIRED) == 0) + { + SEGMENT.aux0 &= SPHERES_DESIRED; + const int32_t complementUniformity = 100 - ((int32_t) SEGMENT.custom1) * 100 / 255; + + for (int i = 0; i < numSpheres; ++i) + { + // Diameter is based on the uniformity. + // radius = (250 + skewedRandom(random(100), dmTableSize, dmPercentages) * complementUniformity << SPHERE_PREC_SHIFT) / 250; // 5-25 + nfixed radius = (7 << 16) + ((((uint64_t)skewedRandom(random(100), dmTableSize, dmPercentages) * complementUniformity << 16) / 10000) * (23 << 16) >> 16);// 7-30 + // radius = 30 << SPHERE_PREC_SHIFT; + nfixed massFactor = MBSphere::fixedDiv((11 << SPHERE_PREC_SHIFT), radius); // Big things should move slower to keep momentum down. + nfixed vx = (-50.0 + random(100)) * massFactor / 5.0; // ±10 + nfixed vy = (-50.0 + random(100)) * massFactor * complementUniformity / 500.0; // ±10 + radius /= 10; + vx /= 10; + vy /= 10; + if (complementUniformity == 0) // Just one sphere has motion intially, if uniformity = 100%. + { + if (i == 0) + vx = 1 << (SPHERE_PREC_SHIFT - 1); // 0.5 + else + vx = 0; + } + if (!is2D) + { + vy = vx; + vx = 0; + } + + MBSphere *candidate = new (&spheres[i]) MBSphere(radius, 0, 0, vx, vy, hw_random8()); + + // Make sure the sphere doesn't land on another one. + bool conflicted = false; + int safety = 100; // Don't try a fit too many items. + do + { + // Give it a random location—closer to the vertical center based on the uniformity. + // (Gotcha! WLED random() returns unsigned. It can't go negative.) + nfixed x = random(internalX); + nfixed y = halfInternalY + (((int32_t)(random(internalY)) - halfInternalY) * complementUniformity / 100) & 0xffff0000; + if (!is2D) + { + y = random(internalY); + x = 0; + } + candidate->newLoc(x, y); + + // Make sure it doesn't land on anything else. + conflicted = false; + for (int j = 0; j < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++j) + if (spheres[j].areSpheresColliding(*candidate)) + { + conflicted = true; + break; + } + } while (conflicted && --safety >= 0); + + // Stop, if we were unsuccessful. + if (conflicted) + break; + + ++SEGMENT.aux0; // Increments SPHERES_ALLOCATED + } + + SEGMENT.aux0 = (numSpheres << SPHERES_DESIRED_SHIFT) | (SEGMENT.aux0 & SPHERES_ALLOCATED); + SEGMENT.step = millis() + BOUNCE_CYCLE_TIME; + SEGMENT.aux1 = elasticLifetime(); + } + + // If it is time to do something. + if (millis() > SEGMENT.step) + { + // Turm off all the LEDS. + for (int i = 0; i < SEGLEN; ++i) + SEGMENT.setPixelColor(i, CRGB::Black); + + // Draw the spheres. + for (int i = 0; i < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++i) + spheres[i].drawMe(SEGMENT, true); + + // Move the spheres and check for collisions with the walls. + // We want of range from 0.1->1->10. + nfixed speed = sliderToSpeed(SEGMENT.speed); + for (int i = 0; i < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++i) + { + // nfixed fixedSpeed = speed * (1 << SPHERE_PREC_SHIFT); + spheres[i].update(speed); + + // If nearing a regeneration, let the walls fall and the spheres fly off! + if (SEGMENT.aux1 > WALL_COLLAPSE_INTR) + spheres[i].handleWallCollision(internalX, internalY); + } + + // Check for collisions with other spheres. + for (int i = 0; i < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++i) + for (int j = i + 1; j < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++j) + if (spheres[i].areSpheresColliding(spheres[j])) + { + /* Make sure the two spheres haven't collided so hard that + * one is inside the other. */ + spheres[i].enforceMinDist(spheres + j); + spheres[i].handleCollision(spheres + j, is2D); + } + + // After a while, force a complete recalculation + if (--SEGMENT.aux1 == 0) + { + SEGMENT.aux1 = elasticLifetime(); + SEGMENT.aux0 = 0; + } + + // Remember the last time + SEGMENT.step += BOUNCE_CYCLE_TIME; + } + + return; +} // mode_ElasticCollisions +static const char _data_FXMODE_ELASTICCOLLISIONS[] PROGMEM = "Elastic Collisions@Speed,Count,Uniformity,Lifetime;;!;12;c1=0,sx=90,c2=64"; + + // Distortion waves - ldirko // https://editor.soulmatelights.com/gallery/1089-distorsion-waves // adapted for WLED by @blazoncek, improvements by @dedehai @@ -11104,6 +11833,9 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_PACMAN, &mode_pacman, _data_FX_MODE_PACMAN); addEffect(FX_MODE_SLOW_TRANSITION, &mode_slow_transition, _data_FX_MODE_SLOW_TRANSITION); + addEffect(FX_MODE_XMASTWINKLE, &mode_XmasTwinkle, _data_FX_MODE_XMASTWINKLE); + addEffect(FX_MODE_ELASTICCOLLISIONS, &mode_ElasticCollisions, _data_FXMODE_ELASTICCOLLISIONS); + // --- 1D audio effects --- addEffect(FX_MODE_PIXELS, &mode_pixels, _data_FX_MODE_PIXELS); addEffect(FX_MODE_PIXELWAVE, &mode_pixelwave, _data_FX_MODE_PIXELWAVE); diff --git a/wled00/FX.h b/wled00/FX.h index 9fd3a04d8a..2cdc7d3c58 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -372,7 +372,9 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_PARTICLEGALAXY 217 #define FX_MODE_COLORCLOUDS 218 #define FX_MODE_SLOW_TRANSITION 219 -#define MODE_COUNT 220 +#define FX_MODE_XMASTWINKLE 220 +#define FX_MODE_ELASTICCOLLISIONS 221 +#define MODE_COUNT 222 #define TRANSITION_FADE 0x00 // universal From d92d93f552da767c2085aa326753a2fbfb3683e3 Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Wed, 22 Apr 2026 04:43:04 -0400 Subject: [PATCH 16/31] Install "Simple Twinkle", replacing XMas Twinkle. Breaks aftgr a while. --- wled00/FX.cpp | 239 ++++++++++++++++---------------------------------- 1 file changed, 76 insertions(+), 163 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 750d9e7553..a340fd5c29 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7751,181 +7751,77 @@ static const char _data_FX_MODE_2DAKEMI[] PROGMEM = "Akemi@Color speed,Dance;Hea ///////////////////////// // We need to keep data for each twinkle light. 8 bytes/light -typedef struct XTwinkleLight { - int16_t timeToEvent; - int16_t maxCycle; - int16_t retwnkleTime; +typedef struct { uint8_t colorIdx; - - uint8_t flags; -#define TWINKLE_ON 0x01 -} XTwinkleLight; - -// For creating skewed random numbers toward the shorter end. -// The sum of percentages must = 100% -const uint8_t pSize = 20; -const uint8_t percentages[pSize] = {12, 11, 10, 10, 6, 6, 5, 5, 3, 3, 1, 1, 1, 1, 1, 1, 2, 3, 3, 15}; // PROGMEM? -const uint8_t slowPercentages[pSize] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 6, 7, 7, 7, 10, 12, 12, 15, 19}; // PROGMEM? - -// Input is 0-100, Ouput is skewed 0-100. -// PArray may be any size, but elements must add up to 100. -#define RAND_PREC_SHIFT 10 // Vertual binary point from the right -int32_t skewedRandom( uint8_t rand100, - const uint8_t pArraySize, - const uint8_t *pArray) -{ - int32_t index = 0; - int32_t cumulativePercentage = 0; - - // Find the range in the table based on randomValue. - while (index < pArraySize - 1 && rand100 >= cumulativePercentage + pArray[index]) { - cumulativePercentage += pArray[index]; - index++; - } - - // Calculate linear interpolation - int32_t t = ((rand100 - cumulativePercentage) << RAND_PREC_SHIFT) / pArray[index]; - int32_t result = ((index << RAND_PREC_SHIFT) + t) * 100 / pArraySize >> RAND_PREC_SHIFT; - - return result; -} - -// Take two percentage tables and average them using the weighting factor. -// Both tables and the result must be the same size. -void weightPercentages(const uint8_t *arg1, - const uint8_t *arg2, - const int cnt, - const uint32_t factor, // 0.0-1.0 weight given to arg2 << RAND_PREC_SHIFT - uint8_t *result) -{ - uint32_t arg1Factor = (1 << RAND_PREC_SHIFT) - factor; - for (int i = 0; i < cnt; ++i) - result[i] = arg1[i] * arg1Factor + arg2[i] * factor >> RAND_PREC_SHIFT; -} - -void mode_XmasTwinkle(void) { // by Nicholas Pisarro, Jr. - /* SEGMENT usage: - * aux0 number of twinklers - * aux1 previous SEGMENT.speed - * step last time stamp - * data array of XTwinkleLight structure - */ - uint16_t numTwiklers = SEGLEN * SEGMENT.intensity / 255; - if (numTwiklers <= 0) - numTwiklers = 1; // Divide checks are not cool. - - // Reinitialize evertying if the number of twinklers has changed. - if (numTwiklers != SEGMENT.aux0 || SEGMENT.call == 0) - SEGMENT.aux0 = 0; + struct { + uint32_t isOn : 1; + uint32_t nextEvent : 16; // Time to next state change (centiseconds) + uint32_t unused : 15; // Reserved for future use + } timing; +} TwinkleLight; + +// Simple exponential distribution favoring shorter times +uint16_t skewedTime(uint8_t maxTime) { + uint8_t r = hw_random16(); + // Square the normalized value to skew toward smaller numbers + float normalized = (r / 255.0f); + normalized = normalized * normalized; + return (uint16_t)(20 + normalized * (maxTime - 20)); +} + +void mode_XmasTwinkle(void) { + uint16_t numLights = max(1, (int)SEGLEN * SEGMENT.intensity / 255); + uint16_t dataSize = sizeof(TwinkleLight) * numLights; - // The maximum twinkle time varies based on the time slider - int32_t slowWeight = (255 - SEGMENT.speed << RAND_PREC_SHIFT) / 255; // 0.0 - 1.0 shifted - int32_t maximumTime = (slowWeight * 9000) + 1000 >> RAND_PREC_SHIFT; // Between 1000 & 10000 milliseconds - - // We have two tables, one of 'normal' weights, 1 of slow weights. - // use more of the slow percentages in he last quarter of the segment times. - uint8_t wkgPercentages[pSize]; - slowWeight = (slowWeight - /* 0.75 */ 768) * 4; // (0.75 << RAND_PREC_SHIFT) - if (slowWeight < 0) - slowWeight = 0; - weightPercentages(percentages, slowPercentages, pSize, slowWeight, wkgPercentages); - - uint16_t dataSize = sizeof(XTwinkleLight) * numTwiklers; - if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed - XTwinkleLight* twinklers = reinterpret_cast(SEGENV.data); - - // Initialize the twinkle lights. - if (SEGMENT.aux0 == 0) - { - for (int i = 0; i < numTwiklers; ++i) - { - XTwinkleLight *light = &twinklers[i]; - - light->colorIdx = hw_random8(); - light->flags = 0; - int32_t cycleTime = skewedRandom(random(100), pSize, wkgPercentages) * maximumTime / 100 + 200; - - light->maxCycle = cycleTime; - light->timeToEvent = random(500, cycleTime); - light->retwnkleTime = random(2, 20) * 1000; // 2 - 20 seconds 1st time around + if (!SEGENV.allocateData(dataSize)) return mode_static(); + TwinkleLight* lights = (TwinkleLight*)SEGENV.data; + + uint32_t now = millis() / 10; // centiseconds + uint16_t maxCycleTime = 100 + (255 - SEGMENT.speed) * 3; // 100-865 centiseconds + + // Initialize on first run + if (SEGMENT.aux0 == 0) { + for (int i = 0; i < numLights; i++) { + lights[i].colorIdx = hw_random8(); + lights[i].timing.isOn = 0; + lights[i].timing.nextEvent = now + random(20, 200); } - - SEGMENT.step = millis(); - SEGMENT.aux0 = numTwiklers; // Initialized. - SEGMENT.aux1 = SEGMENT.speed; // So we don't recalculate reTwinkle time. + SEGMENT.aux0 = 1; // Mark as initialized } - - // Get the current time, handling overflows. - uint32_t lastTime = SEGMENT.step; - uint32_t currTime = millis(); - if (currTime < lastTime) - lastTime = 0; - // The interval may be zero if the refresh rate is fast enought. - uint32_t interval = currTime - lastTime; - - // Note the time passed to the LEDs, and process any events that occured. - for (int i = 0; i < numTwiklers; ++i) - { - XTwinkleLight *light = &twinklers[i]; + // Clear all LEDs + for (int i = 0; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, CRGB::Black); + } + + // Update each twinkle light + for (int i = 0; i < numLights; i++) { + TwinkleLight* light = &lights[i]; - // See if we are at the end of twinkle on o off cycle. - int16_t eventTime = light->timeToEvent - interval; - if (eventTime <= 0) - { - // Twinkle on cycles are 1/3 length of twinkle off cycles. We're' twinkling after all. - if (light->flags & TWINKLE_ON) - eventTime += random(500, light->maxCycle); // turn OFF - else - { - // Based on the check box, either use a constant palette index or a new one each time it turns on. - if (SEGMENT.check1) - light->colorIdx = hw_random8(); - eventTime += random(100, light->maxCycle / 3); // turn ON - } + // Check if it's time for state change + if (now >= light->timing.nextEvent) { + light->timing.isOn = !light->timing.isOn; - light->flags ^= TWINKLE_ON; + if (light->timing.isOn) { + // Turning ON - short duration (1/3 of off time) + light->timing.nextEvent = now + skewedTime(maxCycleTime / 3); + if (SEGMENT.check1) light->colorIdx = hw_random8(); // New color each time + } else { + // Turning OFF - longer duration + light->timing.nextEvent = now + skewedTime(maxCycleTime); + } } - // Put the updated event time back. - light->timeToEvent = eventTime; - - // If we are at the end of a major cycle or the speed has changed, recalculate the max cycle time. - int16_t cycleTime = light->retwnkleTime - interval; - if (cycleTime <= 0 || SEGMENT.aux1 != SEGMENT.speed) - { - int maxTime = skewedRandom(random(100), pSize, wkgPercentages) * maximumTime / 100 + 200; - light->maxCycle = maxTime; - cycleTime += 20000; // 20 seconds + + // Light the LED if on + if (light->timing.isOn) { + uint16_t pos = (i * SEGLEN) / numLights; + SEGMENT.setPixelColor(pos, ColorFromPalette(SEGPALETTE, light->colorIdx)); } - light->retwnkleTime = cycleTime; } - - // Remember the last time as ms. - SEGMENT.step += interval; - SEGMENT.aux1 = SEGMENT.speed; // Se we know if this change. - - // Turm off all the LEDS. - for (int i = 0; i < SEGLEN; ++i) - SEGMENT.setPixelColor(i, CRGB::Black); - // Turn on only those leds that should be. - for (int i = 0; i < numTwiklers; ++i) - { - XTwinkleLight *light = &twinklers[i]; - - if ((light->flags & TWINKLE_ON) == 0) - continue; - - // Compute the offset of the light in the string. - short inset = i * SEGLEN / numTwiklers; - if (inset > SEGLEN) // Safety - break; - - SEGMENT.setPixelColor(inset, ColorFromPalette(SEGPALETTE,light->colorIdx)); - } - return; -} // mode_XmasTwinkle +} // mode_XmasTwinkle + static const char _data_FX_MODE_XMASTWINKLE[] PROGMEM = "Xmas Twinkle@Twinkle speed,Density,,,,Color indices vary;;!;012;m12=0"; //////////////////////////// @@ -7946,6 +7842,23 @@ typedef int32_t nfixed; // These represent fixed point f #define RESET_CYCLE_TIME 1200 // Number of cycles (60 * 1000 / 50) #define WALL_COLLAPSE_INTR 125 // Cycles left till regen. +// Input is 0-100, Ouput is skewed 0-100. +// PArray may be any size, but elements must add up to 100. +#define RAND_PREC_SHIFT 10 // Vertual binary point from the right +int32_t skewedRandom( uint8_t rand100, + const uint8_t pArraySize, + const uint8_t *pArray) +{ + int32_t index = 0; + int32_t cumulativePercentage = 0; + + // Find the range in the table based on randomValue. + while (index < pArraySize - 1 && rand100 >= cumulativePercentage + pArray[index]) { + cumulativePercentage += pArray[index]; + index++; + } + } + // --- Portable countLeadingZeros64 for faster SQRT --- int countLeadingZeros64(uint64_t x) { From 4e2b603df435181f90215d6396496019f59cc076 Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Wed, 22 Apr 2026 05:09:50 -0400 Subject: [PATCH 17/31] Put in authorship comment. Didn't quite move all of 'skewedRandom' to Elastic Collisions. --- wled00/FX.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index a340fd5c29..f84bc6ad57 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7750,6 +7750,8 @@ static const char _data_FX_MODE_2DAKEMI[] PROGMEM = "Akemi@Color speed,Dance;Hea // Xmas Twinkle // ///////////////////////// +// Originally by Nick Pisarro, Jr. This version by DedeHai. + // We need to keep data for each twinkle light. 8 bytes/light typedef struct { uint8_t colorIdx; @@ -7857,6 +7859,12 @@ int32_t skewedRandom( uint8_t rand100, cumulativePercentage += pArray[index]; index++; } + + // Calculate linear interpolation + int32_t t = ((rand100 - cumulativePercentage) << RAND_PREC_SHIFT) / pArray[index]; + int32_t result = ((index << RAND_PREC_SHIFT) + t) * 100 / pArraySize >> RAND_PREC_SHIFT; + + return result; } // --- Portable countLeadingZeros64 for faster SQRT --- From beff6f4ca7d813479bcfa54333c3796fc39c8f4e Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Wed, 22 Apr 2026 06:31:53 -0400 Subject: [PATCH 18/31] Fix bug in new Xmas Twinkle, by storing the interval time, not the absolute time in light->timing.nextEvent. --- wled00/FX.cpp | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index f84bc6ad57..242831d09f 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7750,7 +7750,7 @@ static const char _data_FX_MODE_2DAKEMI[] PROGMEM = "Akemi@Color speed,Dance;Hea // Xmas Twinkle // ///////////////////////// -// Originally by Nick Pisarro, Jr. This version by DedeHai. +// Originally by Nick Pisarro, Jr. This version by DedeHai, but updatged by Nick. // We need to keep data for each twinkle light. 8 bytes/light typedef struct { @@ -7772,23 +7772,38 @@ uint16_t skewedTime(uint8_t maxTime) { } void mode_XmasTwinkle(void) { + /* SEGMENT usage: + * aux0 number of twinklers + * aux1 previous SEGMENT.speed + * step last time stamp + * data array of XTwinkleLight structure + */ + uint16_t numLights = max(1, (int)SEGLEN * SEGMENT.intensity / 255); uint16_t dataSize = sizeof(TwinkleLight) * numLights; if (!SEGENV.allocateData(dataSize)) return mode_static(); TwinkleLight* lights = (TwinkleLight*)SEGENV.data; + + // Get the current time, handling overflows. + uint32_t lastTime = SEGMENT.step; + uint32_t currTime = millis(); + if (currTime < lastTime) + lastTime = 0; + + // The interval may be zero if the refresh rate is fast enough. + uint32_t interval = (currTime - lastTime) / 10; // In centiseconds - uint32_t now = millis() / 10; // centiseconds uint16_t maxCycleTime = 100 + (255 - SEGMENT.speed) * 3; // 100-865 centiseconds // Initialize on first run - if (SEGMENT.aux0 == 0) { + if (SEGMENT.aux0 != numLights) { for (int i = 0; i < numLights; i++) { lights[i].colorIdx = hw_random8(); lights[i].timing.isOn = 0; - lights[i].timing.nextEvent = now + random(20, 200); + lights[i].timing.nextEvent = random(20, 200); } - SEGMENT.aux0 = 1; // Mark as initialized + SEGMENT.aux0 = numLights; // Mark as initialized } // Clear all LEDs @@ -7801,18 +7816,21 @@ void mode_XmasTwinkle(void) { TwinkleLight* light = &lights[i]; // Check if it's time for state change - if (now >= light->timing.nextEvent) { + int16_t eventTime = light->timing.nextEvent - interval; + if (eventTime <= 0) { light->timing.isOn = !light->timing.isOn; if (light->timing.isOn) { // Turning ON - short duration (1/3 of off time) - light->timing.nextEvent = now + skewedTime(maxCycleTime / 3); + eventTime = skewedTime(maxCycleTime / 3); if (SEGMENT.check1) light->colorIdx = hw_random8(); // New color each time } else { // Turning OFF - longer duration - light->timing.nextEvent = now + skewedTime(maxCycleTime); + eventTime = skewedTime(maxCycleTime); } } + // Put the updated event time back. + light->timing.nextEvent = eventTime; // Light the LED if on if (light->timing.isOn) { @@ -7820,6 +7838,10 @@ void mode_XmasTwinkle(void) { SEGMENT.setPixelColor(pos, ColorFromPalette(SEGPALETTE, light->colorIdx)); } } + + // Remember the last time as ms. + SEGMENT.step += interval * 10; // Back to ms. + SEGMENT.aux1 = SEGMENT.speed; // Se we know if this change. return; } // mode_XmasTwinkle From 80e81828e5f572c594872a27d743c1830541f862 Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Wed, 22 Apr 2026 16:28:41 -0400 Subject: [PATCH 19/31] Convert to using milliseconds from centiseconds. Fix bug in 'skewedTime' parameter was defined as uint8_t instead of uint16_5, so it was being truncated. --- wled00/FX.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 242831d09f..3b194bd273 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7757,18 +7757,18 @@ typedef struct { uint8_t colorIdx; struct { uint32_t isOn : 1; - uint32_t nextEvent : 16; // Time to next state change (centiseconds) + uint32_t nextEvent : 16; // Time left to next state change (ms.) uint32_t unused : 15; // Reserved for future use } timing; } TwinkleLight; -// Simple exponential distribution favoring shorter times -uint16_t skewedTime(uint8_t maxTime) { +// Simple exponential distribution favoring shorter times. +uint16_t skewedTime(uint16_t maxTime) { uint8_t r = hw_random16(); // Square the normalized value to skew toward smaller numbers float normalized = (r / 255.0f); normalized = normalized * normalized; - return (uint16_t)(20 + normalized * (maxTime - 20)); + return (uint16_t)(200 + normalized * (maxTime - 200)); } void mode_XmasTwinkle(void) { @@ -7792,16 +7792,16 @@ void mode_XmasTwinkle(void) { lastTime = 0; // The interval may be zero if the refresh rate is fast enough. - uint32_t interval = (currTime - lastTime) / 10; // In centiseconds + uint32_t interval = currTime - lastTime; - uint16_t maxCycleTime = 100 + (255 - SEGMENT.speed) * 3; // 100-865 centiseconds + uint16_t maxCycleTime = 1000 + (255 - SEGMENT.speed) * 30; // 1000-8,650 ms. // Initialize on first run if (SEGMENT.aux0 != numLights) { for (int i = 0; i < numLights; i++) { lights[i].colorIdx = hw_random8(); - lights[i].timing.isOn = 0; - lights[i].timing.nextEvent = random(20, 200); + lights[i].timing.isOn = false; + lights[i].timing.nextEvent = skewedTime(maxCycleTime); } SEGMENT.aux0 = numLights; // Mark as initialized } @@ -7840,8 +7840,8 @@ void mode_XmasTwinkle(void) { } // Remember the last time as ms. - SEGMENT.step += interval * 10; // Back to ms. - SEGMENT.aux1 = SEGMENT.speed; // Se we know if this change. + SEGMENT.step += interval; + SEGMENT.aux1 = SEGMENT.speed; // So we know if this change. return; } // mode_XmasTwinkle From 99025dd0e2d98388af057f9ead0b2cc2da546453 Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Thu, 30 Apr 2026 08:17:08 -0400 Subject: [PATCH 20/31] Made Elastic Collision a Usermod. --- .../elastic_collisions/Elastic_Collisions.cpp | 588 ++++++++++++++++++ usermods/elastic_collisions/README.md | 35 ++ usermods/elastic_collisions/library.json | 4 + wled00/FX.cpp | 571 ----------------- wled00/FX.h | 3 +- 5 files changed, 628 insertions(+), 573 deletions(-) create mode 100644 usermods/elastic_collisions/Elastic_Collisions.cpp create mode 100644 usermods/elastic_collisions/README.md create mode 100644 usermods/elastic_collisions/library.json diff --git a/usermods/elastic_collisions/Elastic_Collisions.cpp b/usermods/elastic_collisions/Elastic_Collisions.cpp new file mode 100644 index 0000000000..4f3b84137d --- /dev/null +++ b/usermods/elastic_collisions/Elastic_Collisions.cpp @@ -0,0 +1,588 @@ +#include "wled.h" + +#define FX_FALLBACK_STATIC { SEGMENT.fill(SEGCOLOR(0)); return; } + +//////////////////////////// +// Elastic Collisions // +//////////////////////////// + +// For print diagnostics, only. + #define FLOAT_IT(x) ((float)(x) / (1 << SPHERE_PREC_SHIFT)) + + /* Note: When you multiply two fixed numbers, the binary point shifts left by the sum of + * binary points. In division the binary point shift right by the difference between + * divident - divisor. */ +#define SPHERE_PREC_SHIFT 16 // Vertual binary point from the right +typedef int32_t nfixed; // These represent fixed point fractional numbers as Q16.16 + +#define SLOWDOWN_FACTOR 0.4 // (Make this a variable?) for very large spheres. +#define BOUNCE_CYCLE_TIME 50 // ms. +#define RESET_CYCLE_TIME 1200 // Number of cycles (60 * 1000 / 50) +#define WALL_COLLAPSE_INTR 125 // Cycles left till regen. + +// Input is 0-100, Ouput is skewed 0-100. +// PArray may be any size, but elements must add up to 100. +#define RAND_PREC_SHIFT 10 // Vertual binary point from the right +int32_t skewedRandom( uint8_t rand100, + const uint8_t pArraySize, + const uint8_t *pArray) +{ + int32_t index = 0; + int32_t cumulativePercentage = 0; + + // Find the range in the table based on randomValue. + while (index < pArraySize - 1 && rand100 >= cumulativePercentage + pArray[index]) { + cumulativePercentage += pArray[index]; + index++; + } + + // Calculate linear interpolation + int32_t t = ((rand100 - cumulativePercentage) << RAND_PREC_SHIFT) / pArray[index]; + int32_t result = ((index << RAND_PREC_SHIFT) + t) * 100 / pArraySize >> RAND_PREC_SHIFT; + + return result; + } + +// --- Portable countLeadingZeros64 for faster SQRT --- +int countLeadingZeros64(uint64_t x) +{ +#if defined(__GNUC__) || defined(__clang__) + return __builtin_clzll(x); +#else + if (x == 0) return 64; + int n = 0; + uint64_t mask = 1ULL << 63; + while ((x & mask) == 0) { + n++; + mask >>= 1; + } + return n; +#endif +} + +class MBSphere +{ + nfixed x, y; // Position + nfixed vx, vy; // Velocity + nfixed radius; // Radius + nfixed _density = (1 << SPHERE_PREC_SHIFT); // Density is 1 for bouncing, other values for gravity + uint8_t colorIdx; +#if false + AbstractList *attrocters; // Null unless this object is affected by gravity. +#endif + + +public: + MBSphere(nfixed radius, nfixed x, nfixed y, nfixed vx, nfixed vy, int8_t color) + : x(x), y(y), vx(vx), vy(vy), radius(radius), colorIdx(color) /*, attrocters(nullptr) */ + { + } + ~MBSphere() { } +#if false + // For effects with gravity. + void addAttractor(MBSphere *sp) + { + if (! attrocters) + attrocters = new List; + + attrocters->add(sp); + } +#endif + nfixed density() { return _density; } + void setDensity(nfixed newD) { _density = newD; } + nfixed mass() { return fixedMult(fixedMult(fixedMult(radius, radius), radius), density()); } + + static nfixed fixedMult(nfixed a, nfixed b) + { + return (int64_t)a * b >> SPHERE_PREC_SHIFT; + } + + static nfixed fixedDiv(nfixed a, nfixed b) + { + return ((int64_t)a << SPHERE_PREC_SHIFT) / b; + } + + static nfixed fixedSqrt(nfixed x) + { + // Promote to 64-bit and scale up for precision + uint64_t n = (uint64_t)x << SPHERE_PREC_SHIFT; // Q16.16 -> Q32.32 + return fixed64Sqrt(n); + } + + // Faster SQRT function curtesy Code Copilot 5. + static nfixed fixed64Sqrt(int64_t n) + { + if (n <= 0) return 0; + + // Initial guess from highest bit. + int lz = 63 - countLeadingZeros64(n); + uint64_t res = 1ULL << (lz / 2); + + // Newton–Raphson refinement (3–4 iterations are plenty) + res = (res + n / res) >> 1; + res = (res + n / res) >> 1; + res = (res + n / res) >> 1; + + // Clamp back to 32-bit Q16.16 + return (nfixed)res; + } + + /* Squaring coordinates can blow out the range of nfixed. + * Work with the 64 bit intermediate result. */ + static nfixed fixedDist(nfixed a, nfixed b) + { + int64_t n = (int64_t)a * a + (int64_t)b * b; + return fixed64Sqrt(n); + } + + // Update the sphere's position and velocity + void update(nfixed dt) { x += fixedMult(vx, dt); y += fixedMult(vy, dt); } + void newLoc(nfixed newX, nfixed newY) { x = newX; y = newY; } + + // Detect if two circles are colliding (simple distance check) + bool areSpheresColliding(MBSphere sp) + { + nfixed dist = fixedDist(sp.x - this->x, sp.y - this->y); + return dist <= this->radius + sp.radius; + } + + /* Make sure two spheres haven't gotten too close. + * Note: There is a pathological case where two spheres + * can crash into each other so hard, that one actually + * ends up insde the other. This function prevents that. */ + void enforceMinDist(MBSphere *sp) + { + nfixed dist = radius + sp->radius; + + nfixed dx = sp->x - x; + nfixed dy = sp->y - y; + nfixed length = fixedDist(dx, dy); + + if (length >= dist || length == 0.0) + return; // Already long enough, or degenerate point + + // Normalize direction + if (length << 1 == 0) + { + // handle gracefully, but this shouldn't happen. + Serial.println("At 0 #1"); + return; + } + nfixed scale = fixedDiv(dist - length, length << 1); + + nfixed offsetX = fixedMult(dx, scale); + nfixed offsetY = fixedMult(dy, scale); + + x -= offsetX; + y -= offsetY; + sp->x += offsetX; + sp->y += offsetY; + } + + // Function to simulate the elastic collision and update velocities + void handleCollision(MBSphere *sp, bool is2D) + { + nfixed m1 = this->mass(); + nfixed m2 = sp->mass(); + + // Calculate the normal and tangent vectors + nfixed nx = sp->x - x; + nfixed ny = sp->y - y; + nfixed dist = fixedDist(nx, ny); + while (dist == 0) { + // handle gracefully + Serial.println("Two objects on top of each other!"); + + x += 1 << (SPHERE_PREC_SHIFT -2); + nx += 1 << (SPHERE_PREC_SHIFT -2); + dist = fixedDist(nx, ny); + } + nx = fixedDiv(nx, dist); + ny = fixedDiv(ny, dist); + + // Tangent is perpendicular to normal + nfixed tx = -ny; + nfixed ty = nx; + + // Use canned values if 1D, otherwise an x velocity creeps in. + if (!is2D) + { + nx = 0; + ny = ((sp->y >= y) ? 1 : -1) << SPHERE_PREC_SHIFT; + tx = -ny; + ty = 0; + } + + // Project velocities onto the normal and tangent + nfixed v1n = fixedMult(vx, nx) + fixedMult(vy, ny); + nfixed v1t = fixedMult(vx, tx) + fixedMult(vy, ty); + nfixed v2n = fixedMult(sp->vx, nx) + fixedMult(sp->vy, ny); + nfixed v2t = fixedMult(sp->vx, tx) + fixedMult(sp->vy, ty); + + // Apply 1D elastic collision for the normal components + nfixed v1n_final = fixedDiv(fixedMult(v1n, m1 - m2) + fixedMult(2 * m2, v2n), m1 + m2); + nfixed v2n_final = fixedDiv(fixedMult(v2n, m2 - m1) + fixedMult(2 * m1, v1n), m1 + m2); + + // Final velocity vectors (tangential velocity remains the same) + vx = fixedMult(v1n_final, nx) + fixedMult(v1t, tx); + vy = fixedMult(v1n_final, ny) + fixedMult(v1t, ty); + sp->vx = fixedMult(v2n_final, nx) + fixedMult(v2t, tx); + sp->vy = fixedMult(v2n_final, ny) + fixedMult(v2t, ty); + } + + // Function to handle wall collisions + void handleWallCollision(nfixed windowWidth, nfixed windowHeight) + { + if (x - radius < 0) { + x = radius; // Keep inside the left wall + vx = -vx; // Reverse x velocity + } else if (x + radius > windowWidth) { + x = windowWidth - radius; // Keep inside the right wall + vx = -vx; // Reverse x velocity + } + + if (y - radius < 0) { + y = radius; // Keep inside the top wall + vy = -vy; // Reverse y velocity + } else if (y + radius > windowHeight) { + y = windowHeight - radius; // Keep inside the bottom wall + vy = -vy; // Reverse y velocity + } + } + +#if false + // Apply the force of gravity with another sphere over the time period in ms. + void applyAttractorGravity(long overTime); + void applyGravity(MBSphere *sp, long overTime); + + // Calculate the initial velocity for a circular orbit. + void initializeOrbit(MBSphere *sp, float dx, float dy); +#endif + + nfixed clamp(nfixed value, nfixed minVal, nfixed maxVal) + { + if (value < minVal) return minVal; + if (value > maxVal) return maxVal; + return value; + } + + nfixed smoothstep(nfixed edge0, nfixed edge1, nfixed x) + { + // float t = clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); + // nfixed t = clamp(fixedDiv(x - edge0, edge1 - edge0), 0, 1 << SPHERE_PREC_SHIFT); + // return t * t * (3.0f - 2.0f * t); + + // Use a faster divide and multiply using Q24.8 numbers instead of Q16.16. + edge0 >>= 8; + edge1 >>= 8; + x >>= 8; + int t = clamp((x - edge0 << 8) / (edge1 - edge0), 0, 1 << 8); // Q24.8 + return (t * t >> 8) * ((3 << 8) - 2 * t); // Result of cubing is Q16.16. + } + + // For generality, the spere uses the segment passed in, not a global. + void drawMe(Segment &seg, bool draw) + { + const bool is2D = seg.is2D(); + const int gridW = (is2D) ? (int)seg.vWidth() : 1; + const int gridH = (is2D) ? (int)seg.vHeight() : seg.vLength(); + + CRGB sphereColor = ColorFromPalette(seg.getCurrentPalette(), colorIdx); + CRGB drawColor; + + /* Thank you Code Copilot: "Using C++, I have a coordinate space that is 10 times + * an LED array. I want to draw a solid circle of diameter 'r' and position 'x' + * and 'y' in the LED array, anti-aliasing the pixels." + * Optimize the loop to only working on pixels near the object. Don't do the + * whole panel. */ + nfixed edge0 = radius - (1 << SPHERE_PREC_SHIFT) / 2; // Soft transition start + nfixed edge1 = radius + (1 << SPHERE_PREC_SHIFT) / 2; // Soft transition end + int lowX = (x - edge1 >> SPHERE_PREC_SHIFT) - 1; // We don't need to cut it too close. + int highX = (x + edge1 >> SPHERE_PREC_SHIFT) + 2; + int lowY = ((y - edge1 >> SPHERE_PREC_SHIFT)) - 1; + int highY = ((y + edge1 >> SPHERE_PREC_SHIFT)) + 2; + + // If completely off the screen, stop it, to avoid an overflow. + if (lowX > gridW || highX < 0 || lowY > gridH || highY < 0) + { + vx = 0; + vy = 0; + } + + // Don't calculate beyond the edges of the LED array. + if (lowX < 0) + lowX = 0; + if (highX > gridW) + highX = gridW; + if (lowY < 0) + lowY = 0; + if (highY > gridH) + highY = gridH; + + /* Loop over a range of pixels on a panel to see how bright the LEDs + * there should be to represent this object. */ + for (int lY = lowY; lY < highY; lY++) { + for (int lX = lowX; lX < highX; lX++) { + // LED pixel center in high-resolution space + const nfixed halfPixel = 1 << (SPHERE_PREC_SHIFT - 1); + nfixed pixelX = (lX << SPHERE_PREC_SHIFT) + halfPixel; + nfixed pixelY = (lY << SPHERE_PREC_SHIFT) + halfPixel; + + // Distance from the circle center + nfixed dist = fixedDist(pixelX - x, pixelY - y); + + // Compute anti-aliasing weight + // float alpha = RGBEffect::clamp(1.0f - RGBEffect::smoothstep(FLOAT_IT(edge0), FLOAT_IT(edge1), dist), 0.0f, 1.0f); + nfixed alpha = clamp((1 << SPHERE_PREC_SHIFT) - smoothstep(edge0, edge1, dist), 0, 1 << SPHERE_PREC_SHIFT) + 0; + // nfixed alpha = clamp((1 << SPHERE_PREC_SHIFT) - smoothstep(edge0, edge1, dist), 1 << (SPHERE_PREC_SHIFT - 2), 1 << SPHERE_PREC_SHIFT); + // alpha = 1 << SPHERE_PREC_SHIFT; + + // Store intensity in LED array (0-1 range) + if (draw) + { + drawColor = sphereColor; + drawColor.nscale8(alpha * 255 >> SPHERE_PREC_SHIFT); + } + else + drawColor = CRGB::Black; + + if (alpha > 0.0) + { + if (is2D) + seg.setPixelColorXY(lX, lY, drawColor); + else + seg.setPixelColor(lY, drawColor); + } + } + } + } + +#if false + // For diagnotistics only. + void print(int instNo) + { + Serial.printf("No. %d, x = %.2f, y = %.2f, vx = %.2f, vy = %.2f, radius = %.2f, density = %.2f, mass = %.2f\n", instNo, + FLOAT_IT(x), FLOAT_IT(y), FLOAT_IT(vx), FLOAT_IT(vy), + FLOAT_IT(radius), FLOAT_IT(_density), FLOAT_IT(mass())); + } +#endif +}; + +// Given 0-255 from SEGMENT.custom2, return in number of 50ms cycles. +uint32_t elasticLifetime() +{ + // 8 categories. + switch (SEGMENT.custom2 >> 5) // /32 + { + case 0: + return 300; // 15s + case 1: + return 600; // 30s + case 2: + return 1200; // 1m + case 3: + return 2400; // 2m + case 4: + return 6000; // 5m + case 5: + return 12000; // 10m + case 6: + return 36000; // 30m + case 7: + return 72000; // 1hr + default: + return 1200; + } +} + +/* We want of range from 0.1->1->10. + * Thank you Claude.ai. */ +nfixed sliderToSpeed(uint8_t slider) +{ + // Q16.16 quadratic coefficients (calculated from your 3 points) + const int32_t a_q16 = 8; // ~0.000148 in Q16.16 (much smaller!) + const int32_t b_q16 = 300; // ~0.004336 in Q16.16 + const int32_t c_q16 = 6554; // ~0.1 in Q16.16 + + // slider is 0-255 + int64_t x = slider; + + // Calculate ax² + bx + c in Q16.16 + int64_t result = ((int64_t)a_q16 * x * x) + ((int64_t)b_q16 * x) + c_q16; + + return (int32_t)result; +} + +void mode_ElasticCollisions(void) { // by Nicholas Pisarro, Jr. + + int numSpheres = 1 + (SEGMENT.intensity * 29) / 255; // 1-30 + + /* + * SEGMENT.aux0.0 = desired number of spheres. + * SEGMENT.aux0.1 = actual number allocated. Might be < aux0.0. + * SEGMENT.step = Next movement intereval + * SEGMENT.aux1 = Next total rebuild as a number of increments. + */ + #define SPHERES_DESIRED 0xff00 + #define SPHERES_DESIRED_SHIFT 8 + #define SPHERES_ALLOCATED 0x00ff + + const bool is2D = strip.isMatrix && SEGMENT.is2D(); + const int cols = (is2D) ? SEG_W : 1; + const int rows = (is2D) ? SEG_H : SEGLEN; + + // Make a virtual coordinate space that is SPACE_FACTOR times the led array. + const nfixed internalX = cols << SPHERE_PREC_SHIFT; + const nfixed internalY = rows << SPHERE_PREC_SHIFT; + const nfixed halfInternalY = internalY >> 1; + + // Radius distribution. + const int dmTableSize = 20; + const uint8_t dmPercentages[20] = {40, 20, 10, 4, 3, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3}; + + // Reinitialize evertying if the number of spheres has changed. + // (We need a separate counter for the number wanted, vs. the number actually initialized.) + if (numSpheres != ((SEGMENT.aux0 & SPHERES_DESIRED) >> SPHERES_DESIRED_SHIFT)) + SEGMENT.aux0 = 0; + + // Point to the sheres. + uint16_t dataSize = sizeof(MBSphere) * numSpheres; + if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed + MBSphere* spheres = reinterpret_cast(SEGENV.data); + + // Initialize the spheres. + if ((SEGMENT.aux0 & SPHERES_DESIRED) == 0) + { + SEGMENT.aux0 &= SPHERES_DESIRED; + const int32_t complementUniformity = 100 - ((int32_t) SEGMENT.custom1) * 100 / 255; + + for (int i = 0; i < numSpheres; ++i) + { + // Diameter is based on the uniformity. + // radius = (250 + skewedRandom(random(100), dmTableSize, dmPercentages) * complementUniformity << SPHERE_PREC_SHIFT) / 250; // 5-25 + nfixed radius = (7 << 16) + ((((uint64_t)skewedRandom(random(100), dmTableSize, dmPercentages) * complementUniformity << 16) / 10000) * (23 << 16) >> 16);// 7-30 + // radius = 30 << SPHERE_PREC_SHIFT; + nfixed massFactor = MBSphere::fixedDiv((11 << SPHERE_PREC_SHIFT), radius); // Big things should move slower to keep momentum down. + nfixed vx = (-50.0 + random(100)) * massFactor / 5.0; // ±10 + nfixed vy = (-50.0 + random(100)) * massFactor * complementUniformity / 500.0; // ±10 + radius /= 10; + vx /= 10; + vy /= 10; + if (complementUniformity == 0) // Just one sphere has motion intially, if uniformity = 100%. + { + if (i == 0) + vx = 1 << (SPHERE_PREC_SHIFT - 1); // 0.5 + else + vx = 0; + } + if (!is2D) + { + vy = vx; + vx = 0; + } + + MBSphere *candidate = new (&spheres[i]) MBSphere(radius, 0, 0, vx, vy, hw_random8()); + + // Make sure the sphere doesn't land on another one. + bool conflicted = false; + int safety = 100; // Don't try a fit too many items. + do + { + // Give it a random location—closer to the vertical center based on the uniformity. + // (Gotcha! WLED random() returns unsigned. It can't go negative.) + nfixed x = random(internalX); + nfixed y = halfInternalY + (((int32_t)(random(internalY)) - halfInternalY) * complementUniformity / 100) & 0xffff0000; + if (!is2D) + { + y = random(internalY); + x = 0; + } + candidate->newLoc(x, y); + + // Make sure it doesn't land on anything else. + conflicted = false; + for (int j = 0; j < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++j) + if (spheres[j].areSpheresColliding(*candidate)) + { + conflicted = true; + break; + } + } while (conflicted && --safety >= 0); + + // Stop, if we were unsuccessful. + if (conflicted) + break; + + ++SEGMENT.aux0; // Increments SPHERES_ALLOCATED + } + + SEGMENT.aux0 = (numSpheres << SPHERES_DESIRED_SHIFT) | (SEGMENT.aux0 & SPHERES_ALLOCATED); + SEGMENT.step = millis() + BOUNCE_CYCLE_TIME; + SEGMENT.aux1 = elasticLifetime(); + } + + // If it is time to do something. + if (millis() > SEGMENT.step) + { + // Turm off all the LEDS. + for (int i = 0; i < SEGLEN; ++i) + SEGMENT.setPixelColor(i, CRGB::Black); + + // Draw the spheres. + for (int i = 0; i < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++i) + spheres[i].drawMe(SEGMENT, true); + + // Move the spheres and check for collisions with the walls. + // We want of range from 0.1->1->10. + nfixed speed = sliderToSpeed(SEGMENT.speed); + for (int i = 0; i < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++i) + { + // nfixed fixedSpeed = speed * (1 << SPHERE_PREC_SHIFT); + spheres[i].update(speed); + + // If nearing a regeneration, let the walls fall and the spheres fly off! + if (SEGMENT.aux1 > WALL_COLLAPSE_INTR) + spheres[i].handleWallCollision(internalX, internalY); + } + + // Check for collisions with other spheres. + for (int i = 0; i < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++i) + for (int j = i + 1; j < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++j) + if (spheres[i].areSpheresColliding(spheres[j])) + { + /* Make sure the two spheres haven't collided so hard that + * one is inside the other. */ + spheres[i].enforceMinDist(spheres + j); + spheres[i].handleCollision(spheres + j, is2D); + } + + // After a while, force a complete recalculation + if (--SEGMENT.aux1 == 0) + { + SEGMENT.aux1 = elasticLifetime(); + SEGMENT.aux0 = 0; + } + + // Remember the last time + SEGMENT.step += BOUNCE_CYCLE_TIME; + } + + return; +} // mode_ElasticCollisions +static const char _data_FXMODE_ELASTICCOLLISIONS[] PROGMEM = "Elastic Collisions@Speed,Count,Uniformity,Lifetime;;!;12;c1=0,sx=90,c2=64"; + +///////////////////// +// UserMod Class // +///////////////////// + +class ElasticCollisionsUsermod : public Usermod { + public: + void setup() override { + strip.addEffect(255, &mode_ElasticCollisions, _data_FXMODE_ELASTICCOLLISIONS); + } + + void loop() override {} +}; + +static ElasticCollisionsUsermod elastic_collisions; +REGISTER_USERMOD(elastic_collisions); diff --git a/usermods/elastic_collisions/README.md b/usermods/elastic_collisions/README.md new file mode 100644 index 0000000000..5bcbfd5e76 --- /dev/null +++ b/usermods/elastic_collisions/README.md @@ -0,0 +1,35 @@ +## Description + +**Elastic Collisions** simulates balls randomly hitting each other and bouncing elastically. Balls also bounce off the edges of a display. You can control; their Speed (velocity), Number of balls; 1-30, Uniformity 0-255, and regeneration time 15 seconds to 1 hour. Ball colors are random indices into the current palette. + +Balls have a mass that is the cube of their diameter. Collisions conserve their energy and momentum as per the laws of physics. + +A Uniformity of 255 is special: The balls are initialized with equal mass in a row and only one moving. When it collides with another ball, all momentum is transferred to the next ball and it stops, much like the swinging ball puzzle in "Professor T". + +a few seconds before regenerating a new set, the wall sides "collapse" and the balls drift off the display. + +It works very well on both 2D and 1D displays. + +## Installation + +To activate the usermod, add the following line to your platformio_override.ini +```ini +custom_usermods = elastic_collisions +``` +Or if you are already using a usermod, append elastic_collisions to the list +```ini +custom_usermods = audioreactive elastic_collisions +``` + +You should now see "Elastic Collisions" appear in your effect list. + +## Note + +When you save an effect in *Presets*, it is saved as an ordinal effect number called "fx". These are fixed for standard effects, but may have a different value for effects that are usermods and how many you've included. So if you change your included usermods, this value may have to be revised in your presets. + +## Parameters + +1. **Speed** The average initial velocity of balls over a wide ranage. +2. **Count** 1-30 balls +3. **Uniformity** 0-100%. 100% uniformity is special as discussed above. +4. **Lifetime** Regenration time from 15 seconds to 1 hour. \ No newline at end of file diff --git a/usermods/elastic_collisions/library.json b/usermods/elastic_collisions/library.json new file mode 100644 index 0000000000..40c9a421a5 --- /dev/null +++ b/usermods/elastic_collisions/library.json @@ -0,0 +1,4 @@ +{ + "name": "Elastic Collisions", + "build": { "libArchive": false } +} \ No newline at end of file diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 3b194bd273..d230beca45 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7848,575 +7848,6 @@ void mode_XmasTwinkle(void) { static const char _data_FX_MODE_XMASTWINKLE[] PROGMEM = "Xmas Twinkle@Twinkle speed,Density,,,,Color indices vary;;!;012;m12=0"; -//////////////////////////// -// Elastic Collisions // -//////////////////////////// - -// For print diagnostics, only. - #define FLOAT_IT(x) ((float)(x) / (1 << SPHERE_PREC_SHIFT)) - - /* Note: When you multiply two fixed numbers, the binary point shifts left by the sum of - * binary points. In division the binary point shift right by the difference between - * divident - divisor. */ -#define SPHERE_PREC_SHIFT 16 // Vertual binary point from the right -typedef int32_t nfixed; // These represent fixed point fractional numbers as Q16.16 - -#define SLOWDOWN_FACTOR 0.4 // (Make this a variable?) for very large spheres. -#define BOUNCE_CYCLE_TIME 50 // ms. -#define RESET_CYCLE_TIME 1200 // Number of cycles (60 * 1000 / 50) -#define WALL_COLLAPSE_INTR 125 // Cycles left till regen. - -// Input is 0-100, Ouput is skewed 0-100. -// PArray may be any size, but elements must add up to 100. -#define RAND_PREC_SHIFT 10 // Vertual binary point from the right -int32_t skewedRandom( uint8_t rand100, - const uint8_t pArraySize, - const uint8_t *pArray) -{ - int32_t index = 0; - int32_t cumulativePercentage = 0; - - // Find the range in the table based on randomValue. - while (index < pArraySize - 1 && rand100 >= cumulativePercentage + pArray[index]) { - cumulativePercentage += pArray[index]; - index++; - } - - // Calculate linear interpolation - int32_t t = ((rand100 - cumulativePercentage) << RAND_PREC_SHIFT) / pArray[index]; - int32_t result = ((index << RAND_PREC_SHIFT) + t) * 100 / pArraySize >> RAND_PREC_SHIFT; - - return result; - } - -// --- Portable countLeadingZeros64 for faster SQRT --- -int countLeadingZeros64(uint64_t x) -{ -#if defined(__GNUC__) || defined(__clang__) - return __builtin_clzll(x); -#else - if (x == 0) return 64; - int n = 0; - uint64_t mask = 1ULL << 63; - while ((x & mask) == 0) { - n++; - mask >>= 1; - } - return n; -#endif -} - -class MBSphere -{ - nfixed x, y; // Position - nfixed vx, vy; // Velocity - nfixed radius; // Radius - nfixed _density = (1 << SPHERE_PREC_SHIFT); // Density is 1 for bouncing, other values for gravity - uint8_t colorIdx; -#if false - AbstractList *attrocters; // Null unless this object is affected by gravity. -#endif - - -public: - MBSphere(nfixed radius, nfixed x, nfixed y, nfixed vx, nfixed vy, int8_t color) - : x(x), y(y), vx(vx), vy(vy), radius(radius), colorIdx(color) /*, attrocters(nullptr) */ - { - } - ~MBSphere() { } -#if false - // For effects with gravity. - void addAttractor(MBSphere *sp) - { - if (! attrocters) - attrocters = new List; - - attrocters->add(sp); - } -#endif - nfixed density() { return _density; } - void setDensity(nfixed newD) { _density = newD; } - nfixed mass() { return fixedMult(fixedMult(fixedMult(radius, radius), radius), density()); } - - static nfixed fixedMult(nfixed a, nfixed b) - { - return (int64_t)a * b >> SPHERE_PREC_SHIFT; - } - - static nfixed fixedDiv(nfixed a, nfixed b) - { - return ((int64_t)a << SPHERE_PREC_SHIFT) / b; - } - - static nfixed fixedSqrt(nfixed x) - { - // Promote to 64-bit and scale up for precision - uint64_t n = (uint64_t)x << SPHERE_PREC_SHIFT; // Q16.16 -> Q32.32 - return fixed64Sqrt(n); - } - - // Faster SQRT function curtesy Code Copilot 5. - static nfixed fixed64Sqrt(int64_t n) - { - if (n <= 0) return 0; - - // Initial guess from highest bit. - int lz = 63 - countLeadingZeros64(n); - uint64_t res = 1ULL << (lz / 2); - - // Newton–Raphson refinement (3–4 iterations are plenty) - res = (res + n / res) >> 1; - res = (res + n / res) >> 1; - res = (res + n / res) >> 1; - - // Clamp back to 32-bit Q16.16 - return (nfixed)res; - } - - /* Squaring coordinates can blow out the range of nfixed. - * Work with the 64 bit intermediate result. */ - static nfixed fixedDist(nfixed a, nfixed b) - { - int64_t n = (int64_t)a * a + (int64_t)b * b; - return fixed64Sqrt(n); - } - - // Update the sphere's position and velocity - void update(nfixed dt) { x += fixedMult(vx, dt); y += fixedMult(vy, dt); } - void newLoc(nfixed newX, nfixed newY) { x = newX; y = newY; } - - // Detect if two circles are colliding (simple distance check) - bool areSpheresColliding(MBSphere sp) - { - nfixed dist = fixedDist(sp.x - this->x, sp.y - this->y); - return dist <= this->radius + sp.radius; - } - - /* Make sure two spheres haven't gotten too close. - * Note: There is a pathological case where two spheres - * can crash into each other so hard, that one actually - * ends up insde the other. This function prevents that. */ - void enforceMinDist(MBSphere *sp) - { - nfixed dist = radius + sp->radius; - - nfixed dx = sp->x - x; - nfixed dy = sp->y - y; - nfixed length = fixedDist(dx, dy); - - if (length >= dist || length == 0.0) - return; // Already long enough, or degenerate point - - // Normalize direction - if (length << 1 == 0) - { - // handle gracefully, but this shouldn't happen. - Serial.println("At 0 #1"); - return; - } - nfixed scale = fixedDiv(dist - length, length << 1); - - nfixed offsetX = fixedMult(dx, scale); - nfixed offsetY = fixedMult(dy, scale); - - x -= offsetX; - y -= offsetY; - sp->x += offsetX; - sp->y += offsetY; - } - - // Function to simulate the elastic collision and update velocities - void handleCollision(MBSphere *sp, bool is2D) - { - nfixed m1 = this->mass(); - nfixed m2 = sp->mass(); - - // Calculate the normal and tangent vectors - nfixed nx = sp->x - x; - nfixed ny = sp->y - y; - nfixed dist = fixedDist(nx, ny); - while (dist == 0) { - // handle gracefully - Serial.println("Two objects on top of each other!"); - - x += 1 << (SPHERE_PREC_SHIFT -2); - nx += 1 << (SPHERE_PREC_SHIFT -2); - dist = fixedDist(nx, ny); - } - nx = fixedDiv(nx, dist); - ny = fixedDiv(ny, dist); - - // Tangent is perpendicular to normal - nfixed tx = -ny; - nfixed ty = nx; - - // Use canned values if 1D, otherwise an x velocity creeps in. - if (!is2D) - { - nx = 0; - ny = ((sp->y >= y) ? 1 : -1) << SPHERE_PREC_SHIFT; - tx = -ny; - ty = 0; - } - - // Project velocities onto the normal and tangent - nfixed v1n = fixedMult(vx, nx) + fixedMult(vy, ny); - nfixed v1t = fixedMult(vx, tx) + fixedMult(vy, ty); - nfixed v2n = fixedMult(sp->vx, nx) + fixedMult(sp->vy, ny); - nfixed v2t = fixedMult(sp->vx, tx) + fixedMult(sp->vy, ty); - - // Apply 1D elastic collision for the normal components - nfixed v1n_final = fixedDiv(fixedMult(v1n, m1 - m2) + fixedMult(2 * m2, v2n), m1 + m2); - nfixed v2n_final = fixedDiv(fixedMult(v2n, m2 - m1) + fixedMult(2 * m1, v1n), m1 + m2); - - // Final velocity vectors (tangential velocity remains the same) - vx = fixedMult(v1n_final, nx) + fixedMult(v1t, tx); - vy = fixedMult(v1n_final, ny) + fixedMult(v1t, ty); - sp->vx = fixedMult(v2n_final, nx) + fixedMult(v2t, tx); - sp->vy = fixedMult(v2n_final, ny) + fixedMult(v2t, ty); - } - - // Function to handle wall collisions - void handleWallCollision(nfixed windowWidth, nfixed windowHeight) - { - if (x - radius < 0) { - x = radius; // Keep inside the left wall - vx = -vx; // Reverse x velocity - } else if (x + radius > windowWidth) { - x = windowWidth - radius; // Keep inside the right wall - vx = -vx; // Reverse x velocity - } - - if (y - radius < 0) { - y = radius; // Keep inside the top wall - vy = -vy; // Reverse y velocity - } else if (y + radius > windowHeight) { - y = windowHeight - radius; // Keep inside the bottom wall - vy = -vy; // Reverse y velocity - } - } - -#if false - // Apply the force of gravity with another sphere over the time period in ms. - void applyAttractorGravity(long overTime); - void applyGravity(MBSphere *sp, long overTime); - - // Calculate the initial velocity for a circular orbit. - void initializeOrbit(MBSphere *sp, float dx, float dy); -#endif - - nfixed clamp(nfixed value, nfixed minVal, nfixed maxVal) - { - if (value < minVal) return minVal; - if (value > maxVal) return maxVal; - return value; - } - - nfixed smoothstep(nfixed edge0, nfixed edge1, nfixed x) - { - // float t = clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); - // nfixed t = clamp(fixedDiv(x - edge0, edge1 - edge0), 0, 1 << SPHERE_PREC_SHIFT); - // return t * t * (3.0f - 2.0f * t); - - // Use a faster divide and multiply using Q24.8 numbers instead of Q16.16. - edge0 >>= 8; - edge1 >>= 8; - x >>= 8; - int t = clamp((x - edge0 << 8) / (edge1 - edge0), 0, 1 << 8); // Q24.8 - return (t * t >> 8) * ((3 << 8) - 2 * t); // Result of cubing is Q16.16. - } - - // For generality, the spere uses the segment passed in, not a global. - void drawMe(Segment &seg, bool draw) - { - const bool is2D = seg.is2D(); - const int gridW = (is2D) ? (int)seg.vWidth() : 1; - const int gridH = (is2D) ? (int)seg.vHeight() : seg.vLength(); - - CRGB sphereColor = ColorFromPalette(seg.getCurrentPalette(), colorIdx); - CRGB drawColor; - - /* Thank you Code Copilot: "Using C++, I have a coordinate space that is 10 times - * an LED array. I want to draw a solid circle of diameter 'r' and position 'x' - * and 'y' in the LED array, anti-aliasing the pixels." - * Optimize the loop to only working on pixels near the object. Don't do the - * whole panel. */ - nfixed edge0 = radius - (1 << SPHERE_PREC_SHIFT) / 2; // Soft transition start - nfixed edge1 = radius + (1 << SPHERE_PREC_SHIFT) / 2; // Soft transition end - int lowX = (x - edge1 >> SPHERE_PREC_SHIFT) - 1; // We don't need to cut it too close. - int highX = (x + edge1 >> SPHERE_PREC_SHIFT) + 2; - int lowY = ((y - edge1 >> SPHERE_PREC_SHIFT)) - 1; - int highY = ((y + edge1 >> SPHERE_PREC_SHIFT)) + 2; - - // If completely off the screen, stop it, to avoid an overflow. - if (lowX > gridW || highX < 0 || lowY > gridH || highY < 0) - { - vx = 0; - vy = 0; - } - - // Don't calculate beyond the edges of the LED array. - if (lowX < 0) - lowX = 0; - if (highX > gridW) - highX = gridW; - if (lowY < 0) - lowY = 0; - if (highY > gridH) - highY = gridH; - - /* Loop over a range of pixels on a panel to see how bright the LEDs - * there should be to represent this object. */ - for (int lY = lowY; lY < highY; lY++) { - for (int lX = lowX; lX < highX; lX++) { - // LED pixel center in high-resolution space - const nfixed halfPixel = 1 << (SPHERE_PREC_SHIFT - 1); - nfixed pixelX = (lX << SPHERE_PREC_SHIFT) + halfPixel; - nfixed pixelY = (lY << SPHERE_PREC_SHIFT) + halfPixel; - - // Distance from the circle center - nfixed dist = fixedDist(pixelX - x, pixelY - y); - - // Compute anti-aliasing weight - // float alpha = RGBEffect::clamp(1.0f - RGBEffect::smoothstep(FLOAT_IT(edge0), FLOAT_IT(edge1), dist), 0.0f, 1.0f); - nfixed alpha = clamp((1 << SPHERE_PREC_SHIFT) - smoothstep(edge0, edge1, dist), 0, 1 << SPHERE_PREC_SHIFT) + 0; - // nfixed alpha = clamp((1 << SPHERE_PREC_SHIFT) - smoothstep(edge0, edge1, dist), 1 << (SPHERE_PREC_SHIFT - 2), 1 << SPHERE_PREC_SHIFT); - // alpha = 1 << SPHERE_PREC_SHIFT; - - // Store intensity in LED array (0-1 range) - if (draw) - { - drawColor = sphereColor; - drawColor.nscale8(alpha * 255 >> SPHERE_PREC_SHIFT); - } - else - drawColor = CRGB::Black; - - if (alpha > 0.0) - { - if (is2D) - seg.setPixelColorXY(lX, lY, drawColor); - else - seg.setPixelColor(lY, drawColor); - } - } - } - } - -#if false - // For diagnotistics only. - void print(int instNo) - { - Serial.printf("No. %d, x = %.2f, y = %.2f, vx = %.2f, vy = %.2f, radius = %.2f, density = %.2f, mass = %.2f\n", instNo, - FLOAT_IT(x), FLOAT_IT(y), FLOAT_IT(vx), FLOAT_IT(vy), - FLOAT_IT(radius), FLOAT_IT(_density), FLOAT_IT(mass())); - } -#endif -}; - -// Given 0-255 from SEGMENT.custom2, return in number of 50ms cycles. -uint32_t elasticLifetime() -{ - // 8 categories. - switch (SEGMENT.custom2 >> 5) // /32 - { - case 0: - return 300; // 15s - case 1: - return 600; // 30s - case 2: - return 1200; // 1m - case 3: - return 2400; // 2m - case 4: - return 6000; // 5m - case 5: - return 12000; // 10m - case 6: - return 36000; // 30m - case 7: - return 72000; // 1hr - default: - return 1200; - } -} - -/* We want of range from 0.1->1->10. - * Thank you Claude.ai. */ -nfixed sliderToSpeed(uint8_t slider) -{ - // Q16.16 quadratic coefficients (calculated from your 3 points) - const int32_t a_q16 = 8; // ~0.000148 in Q16.16 (much smaller!) - const int32_t b_q16 = 300; // ~0.004336 in Q16.16 - const int32_t c_q16 = 6554; // ~0.1 in Q16.16 - - // slider is 0-255 - int64_t x = slider; - - // Calculate ax² + bx + c in Q16.16 - int64_t result = ((int64_t)a_q16 * x * x) + ((int64_t)b_q16 * x) + c_q16; - - return (int32_t)result; -} - -void mode_ElasticCollisions(void) { // by Nicholas Pisarro, Jr. - - int numSpheres = 1 + (SEGMENT.intensity * 29) / 255; // 1-30 - - /* - * SEGMENT.aux0.0 = desired number of spheres. - * SEGMENT.aux0.1 = actual number allocated. Might be < aux0.0. - * SEGMENT.step = Next movement intereval - * SEGMENT.aux1 = Next total rebuild as a number of increments. - */ - #define SPHERES_DESIRED 0xff00 - #define SPHERES_DESIRED_SHIFT 8 - #define SPHERES_ALLOCATED 0x00ff - - const bool is2D = strip.isMatrix && SEGMENT.is2D(); - const int cols = (is2D) ? SEG_W : 1; - const int rows = (is2D) ? SEG_H : SEGLEN; - - // Make a virtual coordinate space that is SPACE_FACTOR times the led array. - const nfixed internalX = cols << SPHERE_PREC_SHIFT; - const nfixed internalY = rows << SPHERE_PREC_SHIFT; - const nfixed halfInternalY = internalY >> 1; - - // Radius distribution. - const int dmTableSize = 20; - const uint8_t dmPercentages[20] = {40, 20, 10, 4, 3, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3}; - - // Reinitialize evertying if the number of spheres has changed. - // (We need a separate counter for the number wanted, vs. the number actually initialized.) - if (numSpheres != ((SEGMENT.aux0 & SPHERES_DESIRED) >> SPHERES_DESIRED_SHIFT)) - SEGMENT.aux0 = 0; - - // Point to the sheres. - uint16_t dataSize = sizeof(MBSphere) * numSpheres; - if (!SEGENV.allocateData(dataSize)) FX_FALLBACK_STATIC; //allocation failed - MBSphere* spheres = reinterpret_cast(SEGENV.data); - - // Initialize the spheres. - if ((SEGMENT.aux0 & SPHERES_DESIRED) == 0) - { - SEGMENT.aux0 &= SPHERES_DESIRED; - const int32_t complementUniformity = 100 - ((int32_t) SEGMENT.custom1) * 100 / 255; - - for (int i = 0; i < numSpheres; ++i) - { - // Diameter is based on the uniformity. - // radius = (250 + skewedRandom(random(100), dmTableSize, dmPercentages) * complementUniformity << SPHERE_PREC_SHIFT) / 250; // 5-25 - nfixed radius = (7 << 16) + ((((uint64_t)skewedRandom(random(100), dmTableSize, dmPercentages) * complementUniformity << 16) / 10000) * (23 << 16) >> 16);// 7-30 - // radius = 30 << SPHERE_PREC_SHIFT; - nfixed massFactor = MBSphere::fixedDiv((11 << SPHERE_PREC_SHIFT), radius); // Big things should move slower to keep momentum down. - nfixed vx = (-50.0 + random(100)) * massFactor / 5.0; // ±10 - nfixed vy = (-50.0 + random(100)) * massFactor * complementUniformity / 500.0; // ±10 - radius /= 10; - vx /= 10; - vy /= 10; - if (complementUniformity == 0) // Just one sphere has motion intially, if uniformity = 100%. - { - if (i == 0) - vx = 1 << (SPHERE_PREC_SHIFT - 1); // 0.5 - else - vx = 0; - } - if (!is2D) - { - vy = vx; - vx = 0; - } - - MBSphere *candidate = new (&spheres[i]) MBSphere(radius, 0, 0, vx, vy, hw_random8()); - - // Make sure the sphere doesn't land on another one. - bool conflicted = false; - int safety = 100; // Don't try a fit too many items. - do - { - // Give it a random location—closer to the vertical center based on the uniformity. - // (Gotcha! WLED random() returns unsigned. It can't go negative.) - nfixed x = random(internalX); - nfixed y = halfInternalY + (((int32_t)(random(internalY)) - halfInternalY) * complementUniformity / 100) & 0xffff0000; - if (!is2D) - { - y = random(internalY); - x = 0; - } - candidate->newLoc(x, y); - - // Make sure it doesn't land on anything else. - conflicted = false; - for (int j = 0; j < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++j) - if (spheres[j].areSpheresColliding(*candidate)) - { - conflicted = true; - break; - } - } while (conflicted && --safety >= 0); - - // Stop, if we were unsuccessful. - if (conflicted) - break; - - ++SEGMENT.aux0; // Increments SPHERES_ALLOCATED - } - - SEGMENT.aux0 = (numSpheres << SPHERES_DESIRED_SHIFT) | (SEGMENT.aux0 & SPHERES_ALLOCATED); - SEGMENT.step = millis() + BOUNCE_CYCLE_TIME; - SEGMENT.aux1 = elasticLifetime(); - } - - // If it is time to do something. - if (millis() > SEGMENT.step) - { - // Turm off all the LEDS. - for (int i = 0; i < SEGLEN; ++i) - SEGMENT.setPixelColor(i, CRGB::Black); - - // Draw the spheres. - for (int i = 0; i < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++i) - spheres[i].drawMe(SEGMENT, true); - - // Move the spheres and check for collisions with the walls. - // We want of range from 0.1->1->10. - nfixed speed = sliderToSpeed(SEGMENT.speed); - for (int i = 0; i < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++i) - { - // nfixed fixedSpeed = speed * (1 << SPHERE_PREC_SHIFT); - spheres[i].update(speed); - - // If nearing a regeneration, let the walls fall and the spheres fly off! - if (SEGMENT.aux1 > WALL_COLLAPSE_INTR) - spheres[i].handleWallCollision(internalX, internalY); - } - - // Check for collisions with other spheres. - for (int i = 0; i < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++i) - for (int j = i + 1; j < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++j) - if (spheres[i].areSpheresColliding(spheres[j])) - { - /* Make sure the two spheres haven't collided so hard that - * one is inside the other. */ - spheres[i].enforceMinDist(spheres + j); - spheres[i].handleCollision(spheres + j, is2D); - } - - // After a while, force a complete recalculation - if (--SEGMENT.aux1 == 0) - { - SEGMENT.aux1 = elasticLifetime(); - SEGMENT.aux0 = 0; - } - - // Remember the last time - SEGMENT.step += BOUNCE_CYCLE_TIME; - } - - return; -} // mode_ElasticCollisions -static const char _data_FXMODE_ELASTICCOLLISIONS[] PROGMEM = "Elastic Collisions@Speed,Count,Uniformity,Lifetime;;!;12;c1=0,sx=90,c2=64"; - // Distortion waves - ldirko // https://editor.soulmatelights.com/gallery/1089-distorsion-waves @@ -11775,9 +11206,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_DYNAMIC_SMOOTH, &mode_dynamic_smooth, _data_FX_MODE_DYNAMIC_SMOOTH); addEffect(FX_MODE_PACMAN, &mode_pacman, _data_FX_MODE_PACMAN); addEffect(FX_MODE_SLOW_TRANSITION, &mode_slow_transition, _data_FX_MODE_SLOW_TRANSITION); - addEffect(FX_MODE_XMASTWINKLE, &mode_XmasTwinkle, _data_FX_MODE_XMASTWINKLE); - addEffect(FX_MODE_ELASTICCOLLISIONS, &mode_ElasticCollisions, _data_FXMODE_ELASTICCOLLISIONS); // --- 1D audio effects --- addEffect(FX_MODE_PIXELS, &mode_pixels, _data_FX_MODE_PIXELS); diff --git a/wled00/FX.h b/wled00/FX.h index 2cdc7d3c58..27cc851e7e 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -373,8 +373,7 @@ extern byte realtimeMode; // used in getMappedPixelIndex() #define FX_MODE_COLORCLOUDS 218 #define FX_MODE_SLOW_TRANSITION 219 #define FX_MODE_XMASTWINKLE 220 -#define FX_MODE_ELASTICCOLLISIONS 221 -#define MODE_COUNT 222 +#define MODE_COUNT 221 #define TRANSITION_FADE 0x00 // universal From 4fa8d96b1aeaa51878caacf5f491503032796c7f Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Fri, 1 May 2026 15:07:50 -0400 Subject: [PATCH 21/31] Convert 'skwedTime()' to fixed point. --- wled00/FX.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index d230beca45..acbb200f27 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7762,13 +7762,13 @@ typedef struct { } timing; } TwinkleLight; -// Simple exponential distribution favoring shorter times. -uint16_t skewedTime(uint16_t maxTime) { - uint8_t r = hw_random16(); - // Square the normalized value to skew toward smaller numbers - float normalized = (r / 255.0f); - normalized = normalized * normalized; - return (uint16_t)(200 + normalized * (maxTime - 200)); +// Square a normalized value to skew toward smaller numbers +int16_t skewedTime(uint16_t maxTime) { + // Do things in the proper order so fixed arithmatic works. + uint32_t rSqrd = hw_random8(); // 0-255 + rSqrd *= rSqrd; // 0-65,025 + uint32_t normalized = rSqrd >> 8; // 0-254, i.e. (0.0-1.0 << 8) + return (uint16_t)(200 + (normalized * (maxTime - 200) >> 8)); } void mode_XmasTwinkle(void) { From dbcfac8ca3a9dd816d8ff9c12e33074602256748 Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Sat, 2 May 2026 07:11:35 -0400 Subject: [PATCH 22/31] Preparation for having long and short term cycle times. --- wled00/FX.cpp | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index acbb200f27..a2878d9732 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7750,25 +7750,24 @@ static const char _data_FX_MODE_2DAKEMI[] PROGMEM = "Akemi@Color speed,Dance;Hea // Xmas Twinkle // ///////////////////////// -// Originally by Nick Pisarro, Jr. This version by DedeHai, but updatged by Nick. +// Originally by Nick Pisarro, Jr. This version by DedeHai, and updatged by Nick. // We need to keep data for each twinkle light. 8 bytes/light typedef struct { - uint8_t colorIdx; - struct { - uint32_t isOn : 1; - uint32_t nextEvent : 16; // Time left to next state change (ms.) - uint32_t unused : 15; // Reserved for future use - } timing; + uint32_t nextEvent : 16; // Time left to next state change (ms.) + uint32_t maxCycle : 16; // Working maximum cycle time + uint32_t retwnkleTime : 16; // Time left til we recalculate 'maxCycle' + uint32_t colorIdx : 8; // May be fixed or change with each flash + uint32_t isOn : 1; } TwinkleLight; // Square a normalized value to skew toward smaller numbers -int16_t skewedTime(uint16_t maxTime) { +int16_t skewedTime(uint16_t maxTime, uint16_t minTime = 200) { // Do things in the proper order so fixed arithmatic works. uint32_t rSqrd = hw_random8(); // 0-255 rSqrd *= rSqrd; // 0-65,025 uint32_t normalized = rSqrd >> 8; // 0-254, i.e. (0.0-1.0 << 8) - return (uint16_t)(200 + (normalized * (maxTime - 200) >> 8)); + return (uint16_t)(minTime + (normalized * (maxTime - minTime) >> 8)); } void mode_XmasTwinkle(void) { @@ -7800,11 +7799,18 @@ void mode_XmasTwinkle(void) { if (SEGMENT.aux0 != numLights) { for (int i = 0; i < numLights; i++) { lights[i].colorIdx = hw_random8(); - lights[i].timing.isOn = false; - lights[i].timing.nextEvent = skewedTime(maxCycleTime); + lights[i].isOn = false; + lights[i].nextEvent = skewedTime(maxCycleTime); + lights[i].maxCycle = maxCycleTime; } SEGMENT.aux0 = numLights; // Mark as initialized } + + // Otherwise, update maxCylce for all the lights to reflect a new speed. + else if (SEGMENT.speed != SEGMENT.aux1) { + for (int i = 0; i < numLights; i++) + lights[i].maxCycle = maxCycleTime; + } // Clear all LEDs for (int i = 0; i < SEGLEN; i++) { @@ -7816,24 +7822,24 @@ void mode_XmasTwinkle(void) { TwinkleLight* light = &lights[i]; // Check if it's time for state change - int16_t eventTime = light->timing.nextEvent - interval; + int16_t eventTime = light->nextEvent - interval; if (eventTime <= 0) { - light->timing.isOn = !light->timing.isOn; + light->isOn = !light->isOn; - if (light->timing.isOn) { + if (light->isOn) { // Turning ON - short duration (1/3 of off time) - eventTime = skewedTime(maxCycleTime / 3); + eventTime = skewedTime(lights[i].maxCycle / 3); if (SEGMENT.check1) light->colorIdx = hw_random8(); // New color each time } else { // Turning OFF - longer duration - eventTime = skewedTime(maxCycleTime); + eventTime = skewedTime(lights[i].maxCycle); } } // Put the updated event time back. - light->timing.nextEvent = eventTime; + light->nextEvent = eventTime; // Light the LED if on - if (light->timing.isOn) { + if (light->isOn) { uint16_t pos = (i * SEGLEN) / numLights; SEGMENT.setPixelColor(pos, ColorFromPalette(SEGPALETTE, light->colorIdx)); } From ee42cbf20888cb0280ac6f138c3998bd03995a01 Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Sun, 3 May 2026 03:55:04 -0400 Subject: [PATCH 23/31] Xmas twinkle now has a long term and short term cycle. --- wled00/FX.cpp | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index a2878d9732..a1edee8c8d 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7770,6 +7770,14 @@ int16_t skewedTime(uint16_t maxTime, uint16_t minTime = 200) { return (uint16_t)(minTime + (normalized * (maxTime - minTime) >> 8)); } +// Based on the speed, bias the maxtime toward faster or slower times. +int16_t skewedMax() +{ + int32_t slowWeight = SEGMENT.speed; // 0.0 - 1.0 as fixed Q24.8 + // ">> 9, below divides by 2 and converts Q24.8 to Q32.0. + return (skewedTime(8800, 0) * slowWeight + (8800 - skewedTime(8800, 0)) * (256 - slowWeight) >> 9) + 200; +} + void mode_XmasTwinkle(void) { /* SEGMENT usage: * aux0 number of twinklers @@ -7793,24 +7801,17 @@ void mode_XmasTwinkle(void) { // The interval may be zero if the refresh rate is fast enough. uint32_t interval = currTime - lastTime; - uint16_t maxCycleTime = 1000 + (255 - SEGMENT.speed) * 30; // 1000-8,650 ms. - // Initialize on first run if (SEGMENT.aux0 != numLights) { for (int i = 0; i < numLights; i++) { lights[i].colorIdx = hw_random8(); lights[i].isOn = false; - lights[i].nextEvent = skewedTime(maxCycleTime); - lights[i].maxCycle = maxCycleTime; + lights[i].maxCycle = skewedMax(); + lights[i].nextEvent = skewedTime(lights[i].maxCycle); + lights[i].retwnkleTime = random(2, 20) * 1000; // 2 - 20 seconds 1st time around } SEGMENT.aux0 = numLights; // Mark as initialized } - - // Otherwise, update maxCylce for all the lights to reflect a new speed. - else if (SEGMENT.speed != SEGMENT.aux1) { - for (int i = 0; i < numLights; i++) - lights[i].maxCycle = maxCycleTime; - } // Clear all LEDs for (int i = 0; i < SEGLEN; i++) { @@ -7843,6 +7844,16 @@ void mode_XmasTwinkle(void) { uint16_t pos = (i * SEGLEN) / numLights; SEGMENT.setPixelColor(pos, ColorFromPalette(SEGPALETTE, light->colorIdx)); } + + // If we are at the end of a major cycle or the speed has changed, recalculate the max cycle time. + int16_t cycleTime = light->retwnkleTime - interval; + if (cycleTime <= 0 || SEGMENT.aux1 != SEGMENT.speed) + { + light->maxCycle = skewedMax(); + if (cycleTime <= 0) + cycleTime += 20000; // +20 seconds + } + light->retwnkleTime = cycleTime; } // Remember the last time as ms. From b82a886aefaa16770803ae423a14aa3861ebc290 Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Sun, 3 May 2026 07:49:18 -0400 Subject: [PATCH 24/31] Fix source indentation to match VSCode C++ conventions. Put 'dmPercentages' in PROGMEM. --- .../elastic_collisions/Elastic_Collisions.cpp | 460 ++++++++---------- 1 file changed, 206 insertions(+), 254 deletions(-) diff --git a/usermods/elastic_collisions/Elastic_Collisions.cpp b/usermods/elastic_collisions/Elastic_Collisions.cpp index 4f3b84137d..6859ea1910 100644 --- a/usermods/elastic_collisions/Elastic_Collisions.cpp +++ b/usermods/elastic_collisions/Elastic_Collisions.cpp @@ -6,14 +6,16 @@ // Elastic Collisions // //////////////////////////// +// This effect uses fixed point arithmetic, generally of the form Q16.16 + // For print diagnostics, only. - #define FLOAT_IT(x) ((float)(x) / (1 << SPHERE_PREC_SHIFT)) +#define FLOAT_IT(x) ((float)(x) / (1 << SPHERE_PREC_SHIFT)) - /* Note: When you multiply two fixed numbers, the binary point shifts left by the sum of +/* Note: When you multiply two fixed numbers, the binary point shifts left by the sum of * binary points. In division the binary point shift right by the difference between * divident - divisor. */ -#define SPHERE_PREC_SHIFT 16 // Vertual binary point from the right -typedef int32_t nfixed; // These represent fixed point fractional numbers as Q16.16 +#define SPHERE_PREC_SHIFT 16 // Vertual binary point from the right +typedef int32_t nfixed; // These represent fixed point fractional numbers as Q16.16 #define SLOWDOWN_FACTOR 0.4 // (Make this a variable?) for very large spheres. #define BOUNCE_CYCLE_TIME 50 // ms. @@ -25,123 +27,103 @@ typedef int32_t nfixed; // These represent fixed point f #define RAND_PREC_SHIFT 10 // Vertual binary point from the right int32_t skewedRandom( uint8_t rand100, const uint8_t pArraySize, - const uint8_t *pArray) -{ - int32_t index = 0; - int32_t cumulativePercentage = 0; - - // Find the range in the table based on randomValue. - while (index < pArraySize - 1 && rand100 >= cumulativePercentage + pArray[index]) { - cumulativePercentage += pArray[index]; - index++; - } + const uint8_t *pArray) { + int32_t index = 0; + int32_t cumulativePercentage = 0; + + // Find the range in the table based on randomValue. + while (index < pArraySize - 1 && rand100 >= cumulativePercentage + pArray[index]) { + cumulativePercentage += pArray[index]; + index++; + } - // Calculate linear interpolation - int32_t t = ((rand100 - cumulativePercentage) << RAND_PREC_SHIFT) / pArray[index]; - int32_t result = ((index << RAND_PREC_SHIFT) + t) * 100 / pArraySize >> RAND_PREC_SHIFT; + // Calculate linear interpolation + int32_t t = ((rand100 - cumulativePercentage) << RAND_PREC_SHIFT) / pArray[index]; + int32_t result = ((index << RAND_PREC_SHIFT) + t) * 100 / pArraySize >> RAND_PREC_SHIFT; - return result; - } + return result; +} // --- Portable countLeadingZeros64 for faster SQRT --- int countLeadingZeros64(uint64_t x) { #if defined(__GNUC__) || defined(__clang__) - return __builtin_clzll(x); + return __builtin_clzll(x); #else - if (x == 0) return 64; - int n = 0; - uint64_t mask = 1ULL << 63; - while ((x & mask) == 0) { - n++; - mask >>= 1; - } - return n; + if (x == 0) return 64; + int n = 0; + uint64_t mask = 1ULL << 63; + while ((x & mask) == 0) { + n++; + mask >>= 1; + } + return n; #endif } class MBSphere { - nfixed x, y; // Position - nfixed vx, vy; // Velocity - nfixed radius; // Radius - nfixed _density = (1 << SPHERE_PREC_SHIFT); // Density is 1 for bouncing, other values for gravity - uint8_t colorIdx; -#if false - AbstractList *attrocters; // Null unless this object is affected by gravity. -#endif - + nfixed x, y; // Position + nfixed vx, vy; // Velocity + nfixed radius; // Radius + nfixed _density = (1 << SPHERE_PREC_SHIFT); // Density is 1 for bouncing, other values for gravity + uint8_t colorIdx; public: - MBSphere(nfixed radius, nfixed x, nfixed y, nfixed vx, nfixed vy, int8_t color) - : x(x), y(y), vx(vx), vy(vy), radius(radius), colorIdx(color) /*, attrocters(nullptr) */ - { - } - ~MBSphere() { } -#if false - // For effects with gravity. - void addAttractor(MBSphere *sp) - { - if (! attrocters) - attrocters = new List; - - attrocters->add(sp); - } -#endif - nfixed density() { return _density; } - void setDensity(nfixed newD) { _density = newD; } - nfixed mass() { return fixedMult(fixedMult(fixedMult(radius, radius), radius), density()); } + MBSphere(nfixed radius, nfixed x, nfixed y, nfixed vx, nfixed vy, int8_t color) + : x(x), y(y), vx(vx), vy(vy), radius(radius), colorIdx(color) /*, attrocters(nullptr) */ + { + } + ~MBSphere() { } - static nfixed fixedMult(nfixed a, nfixed b) - { - return (int64_t)a * b >> SPHERE_PREC_SHIFT; - } + nfixed density() { return _density; } + void setDensity(nfixed newD) { _density = newD; } + nfixed mass() { return fixedMult(fixedMult(fixedMult(radius, radius), radius), density()); } - static nfixed fixedDiv(nfixed a, nfixed b) - { - return ((int64_t)a << SPHERE_PREC_SHIFT) / b; - } + static nfixed fixedMult(nfixed a, nfixed b) { + return (int64_t)a * b >> SPHERE_PREC_SHIFT; + } - static nfixed fixedSqrt(nfixed x) - { - // Promote to 64-bit and scale up for precision - uint64_t n = (uint64_t)x << SPHERE_PREC_SHIFT; // Q16.16 -> Q32.32 - return fixed64Sqrt(n); - } + static nfixed fixedDiv(nfixed a, nfixed b) { + return ((int64_t)a << SPHERE_PREC_SHIFT) / b; + } - // Faster SQRT function curtesy Code Copilot 5. - static nfixed fixed64Sqrt(int64_t n) - { - if (n <= 0) return 0; + static nfixed fixedSqrt(nfixed x) { + // Promote to 64-bit and scale up for precision + uint64_t n = (uint64_t)x << SPHERE_PREC_SHIFT; // Q16.16 -> Q32.32 + return fixed64Sqrt(n); + } - // Initial guess from highest bit. - int lz = 63 - countLeadingZeros64(n); - uint64_t res = 1ULL << (lz / 2); + // Faster SQRT function curtesy Code Copilot 5. + static nfixed fixed64Sqrt(int64_t n) { + if (n <= 0) return 0; - // Newton–Raphson refinement (3–4 iterations are plenty) - res = (res + n / res) >> 1; - res = (res + n / res) >> 1; - res = (res + n / res) >> 1; + // Initial guess from highest bit. + int lz = 63 - countLeadingZeros64(n); + uint64_t res = 1ULL << (lz / 2); - // Clamp back to 32-bit Q16.16 - return (nfixed)res; - } + // Newton–Raphson refinement (3–4 iterations are plenty) + res = (res + n / res) >> 1; + res = (res + n / res) >> 1; + res = (res + n / res) >> 1; - /* Squaring coordinates can blow out the range of nfixed. - * Work with the 64 bit intermediate result. */ - static nfixed fixedDist(nfixed a, nfixed b) - { - int64_t n = (int64_t)a * a + (int64_t)b * b; - return fixed64Sqrt(n); - } + // Clamp back to 32-bit Q16.16 + return (nfixed)res; + } - // Update the sphere's position and velocity - void update(nfixed dt) { x += fixedMult(vx, dt); y += fixedMult(vy, dt); } - void newLoc(nfixed newX, nfixed newY) { x = newX; y = newY; } + /* Squaring coordinates can blow out the range of nfixed. + * Work with the 64 bit intermediate result. */ + static nfixed fixedDist(nfixed a, nfixed b) { + int64_t n = (int64_t)a * a + (int64_t)b * b; + return fixed64Sqrt(n); + } + + // Update the sphere's position and velocity + void update(nfixed dt) { x += fixedMult(vx, dt); y += fixedMult(vy, dt); } + void newLoc(nfixed newX, nfixed newY) { x = newX; y = newY; } - // Detect if two circles are colliding (simple distance check) - bool areSpheresColliding(MBSphere sp) - { + // Detect if two circles are colliding (simple distance check) + bool areSpheresColliding(MBSphere sp) { nfixed dist = fixedDist(sp.x - this->x, sp.y - this->y); return dist <= this->radius + sp.radius; } @@ -150,38 +132,35 @@ class MBSphere * Note: There is a pathological case where two spheres * can crash into each other so hard, that one actually * ends up insde the other. This function prevents that. */ - void enforceMinDist(MBSphere *sp) - { - nfixed dist = radius + sp->radius; + void enforceMinDist(MBSphere *sp) { + nfixed dist = radius + sp->radius; - nfixed dx = sp->x - x; - nfixed dy = sp->y - y; - nfixed length = fixedDist(dx, dy); + nfixed dx = sp->x - x; + nfixed dy = sp->y - y; + nfixed length = fixedDist(dx, dy); - if (length >= dist || length == 0.0) - return; // Already long enough, or degenerate point + if (length >= dist || length == 0.0) + return; // Already long enough, or degenerate point - // Normalize direction - if (length << 1 == 0) - { - // handle gracefully, but this shouldn't happen. - Serial.println("At 0 #1"); - return; - } - nfixed scale = fixedDiv(dist - length, length << 1); + // Normalize direction + if (length << 1 == 0) { + // handle gracefully, but this shouldn't happen. + Serial.println("At 0 #1"); + return; + } + nfixed scale = fixedDiv(dist - length, length << 1); - nfixed offsetX = fixedMult(dx, scale); - nfixed offsetY = fixedMult(dy, scale); + nfixed offsetX = fixedMult(dx, scale); + nfixed offsetY = fixedMult(dy, scale); - x -= offsetX; - y -= offsetY; - sp->x += offsetX; - sp->y += offsetY; + x -= offsetX; + y -= offsetY; + sp->x += offsetX; + sp->y += offsetY; } // Function to simulate the elastic collision and update velocities - void handleCollision(MBSphere *sp, bool is2D) - { + void handleCollision(MBSphere *sp, bool is2D) { nfixed m1 = this->mass(); nfixed m2 = sp->mass(); @@ -190,12 +169,12 @@ class MBSphere nfixed ny = sp->y - y; nfixed dist = fixedDist(nx, ny); while (dist == 0) { - // handle gracefully - Serial.println("Two objects on top of each other!"); + // handle gracefully + // Serial.println("Two objects on top of each other!"); - x += 1 << (SPHERE_PREC_SHIFT -2); - nx += 1 << (SPHERE_PREC_SHIFT -2); - dist = fixedDist(nx, ny); + x += 1 << (SPHERE_PREC_SHIFT -2); + nx += 1 << (SPHERE_PREC_SHIFT -2); + dist = fixedDist(nx, ny); } nx = fixedDiv(nx, dist); ny = fixedDiv(ny, dist); @@ -205,12 +184,11 @@ class MBSphere nfixed ty = nx; // Use canned values if 1D, otherwise an x velocity creeps in. - if (!is2D) - { - nx = 0; - ny = ((sp->y >= y) ? 1 : -1) << SPHERE_PREC_SHIFT; - tx = -ny; - ty = 0; + if (!is2D) { + nx = 0; + ny = ((sp->y >= y) ? 1 : -1) << SPHERE_PREC_SHIFT; + tx = -ny; + ty = 0; } // Project velocities onto the normal and tangent @@ -230,59 +208,46 @@ class MBSphere sp->vy = fixedMult(v2n_final, ny) + fixedMult(v2t, ty); } - // Function to handle wall collisions - void handleWallCollision(nfixed windowWidth, nfixed windowHeight) - { + // Function to handle wall collisions + void handleWallCollision(nfixed windowWidth, nfixed windowHeight) { if (x - radius < 0) { - x = radius; // Keep inside the left wall - vx = -vx; // Reverse x velocity + x = radius; // Keep inside the left wall + vx = -vx; // Reverse x velocity } else if (x + radius > windowWidth) { - x = windowWidth - radius; // Keep inside the right wall - vx = -vx; // Reverse x velocity + x = windowWidth - radius; // Keep inside the right wall + vx = -vx; // Reverse x velocity } if (y - radius < 0) { - y = radius; // Keep inside the top wall - vy = -vy; // Reverse y velocity + y = radius; // Keep inside the top wall + vy = -vy; // Reverse y velocity } else if (y + radius > windowHeight) { - y = windowHeight - radius; // Keep inside the bottom wall - vy = -vy; // Reverse y velocity + y = windowHeight - radius; // Keep inside the bottom wall + vy = -vy; // Reverse y velocity } } -#if false - // Apply the force of gravity with another sphere over the time period in ms. - void applyAttractorGravity(long overTime); - void applyGravity(MBSphere *sp, long overTime); - - // Calculate the initial velocity for a circular orbit. - void initializeOrbit(MBSphere *sp, float dx, float dy); -#endif - - nfixed clamp(nfixed value, nfixed minVal, nfixed maxVal) - { - if (value < minVal) return minVal; - if (value > maxVal) return maxVal; - return value; + nfixed clamp(nfixed value, nfixed minVal, nfixed maxVal) { + if (value < minVal) return minVal; + if (value > maxVal) return maxVal; + return value; } - nfixed smoothstep(nfixed edge0, nfixed edge1, nfixed x) - { - // float t = clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); - // nfixed t = clamp(fixedDiv(x - edge0, edge1 - edge0), 0, 1 << SPHERE_PREC_SHIFT); - // return t * t * (3.0f - 2.0f * t); - - // Use a faster divide and multiply using Q24.8 numbers instead of Q16.16. - edge0 >>= 8; - edge1 >>= 8; - x >>= 8; - int t = clamp((x - edge0 << 8) / (edge1 - edge0), 0, 1 << 8); // Q24.8 - return (t * t >> 8) * ((3 << 8) - 2 * t); // Result of cubing is Q16.16. + nfixed smoothstep(nfixed edge0, nfixed edge1, nfixed x) { + // float t = clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); + // nfixed t = clamp(fixedDiv(x - edge0, edge1 - edge0), 0, 1 << SPHERE_PREC_SHIFT); + // return t * t * (3.0f - 2.0f * t); + + // Use a faster divide and multiply using Q24.8 numbers instead of Q16.16. + edge0 >>= 8; + edge1 >>= 8; + x >>= 8; + int t = clamp((x - edge0 << 8) / (edge1 - edge0), 0, 1 << 8); // Q24.8 + return (t * t >> 8) * ((3 << 8) - 2 * t); // Result of cubing is Q16.16. } // For generality, the spere uses the segment passed in, not a global. - void drawMe(Segment &seg, bool draw) - { + void drawMe(Segment &seg, bool draw) { const bool is2D = seg.is2D(); const int gridW = (is2D) ? (int)seg.vWidth() : 1; const int gridH = (is2D) ? (int)seg.vHeight() : seg.vLength(); @@ -369,8 +334,7 @@ class MBSphere }; // Given 0-255 from SEGMENT.custom2, return in number of 50ms cycles. -uint32_t elasticLifetime() -{ +uint32_t elasticLifetime() { // 8 categories. switch (SEGMENT.custom2 >> 5) // /32 { @@ -397,8 +361,7 @@ uint32_t elasticLifetime() /* We want of range from 0.1->1->10. * Thank you Claude.ai. */ -nfixed sliderToSpeed(uint8_t slider) -{ +nfixed sliderToSpeed(uint8_t slider) { // Q16.16 quadratic coefficients (calculated from your 3 points) const int32_t a_q16 = 8; // ~0.000148 in Q16.16 (much smaller!) const int32_t b_q16 = 300; // ~0.004336 in Q16.16 @@ -438,7 +401,7 @@ void mode_ElasticCollisions(void) { // by Nicholas Pisarro, Jr. // Radius distribution. const int dmTableSize = 20; - const uint8_t dmPercentages[20] = {40, 20, 10, 4, 3, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3}; + const uint8_t PROGMEM dmPercentages[20] = {40, 20, 10, 4, 3, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3}; // Reinitialize evertying if the number of spheres has changed. // (We need a separate counter for the number wanted, vs. the number actually initialized.) @@ -451,69 +414,62 @@ void mode_ElasticCollisions(void) { // by Nicholas Pisarro, Jr. MBSphere* spheres = reinterpret_cast(SEGENV.data); // Initialize the spheres. - if ((SEGMENT.aux0 & SPHERES_DESIRED) == 0) - { + if ((SEGMENT.aux0 & SPHERES_DESIRED) == 0) { SEGMENT.aux0 &= SPHERES_DESIRED; const int32_t complementUniformity = 100 - ((int32_t) SEGMENT.custom1) * 100 / 255; - for (int i = 0; i < numSpheres; ++i) - { - // Diameter is based on the uniformity. - // radius = (250 + skewedRandom(random(100), dmTableSize, dmPercentages) * complementUniformity << SPHERE_PREC_SHIFT) / 250; // 5-25 - nfixed radius = (7 << 16) + ((((uint64_t)skewedRandom(random(100), dmTableSize, dmPercentages) * complementUniformity << 16) / 10000) * (23 << 16) >> 16);// 7-30 - // radius = 30 << SPHERE_PREC_SHIFT; - nfixed massFactor = MBSphere::fixedDiv((11 << SPHERE_PREC_SHIFT), radius); // Big things should move slower to keep momentum down. - nfixed vx = (-50.0 + random(100)) * massFactor / 5.0; // ±10 - nfixed vy = (-50.0 + random(100)) * massFactor * complementUniformity / 500.0; // ±10 - radius /= 10; - vx /= 10; - vy /= 10; - if (complementUniformity == 0) // Just one sphere has motion intially, if uniformity = 100%. - { - if (i == 0) - vx = 1 << (SPHERE_PREC_SHIFT - 1); // 0.5 - else - vx = 0; - } - if (!is2D) - { - vy = vx; - vx = 0; + for (int i = 0; i < numSpheres; ++i) { + // Diameter is based on the uniformity. + // radius = (250 + skewedRandom(random(100), dmTableSize, dmPercentages) * complementUniformity << SPHERE_PREC_SHIFT) / 250; // 5-25 + nfixed radius = (7 << 16) + ((((uint64_t)skewedRandom(random(100), dmTableSize, dmPercentages) * complementUniformity << 16) / 10000) * (23 << 16) >> 16);// 7-30 + // radius = 30 << SPHERE_PREC_SHIFT; + nfixed massFactor = MBSphere::fixedDiv((11 << SPHERE_PREC_SHIFT), radius); // Big things should move slower to keep momentum down. + nfixed vx = (-50.0 + random(100)) * massFactor / 5.0; // ±10 + nfixed vy = (-50.0 + random(100)) * massFactor * complementUniformity / 500.0; // ±10 + radius /= 10; + vx /= 10; + vy /= 10; + if (complementUniformity == 0) { // Just one sphere has motion intially, if uniformity = 100%. + if (i == 0) + vx = 1 << (SPHERE_PREC_SHIFT - 1); // 0.5 + else + vx = 0; + } + if (!is2D) { + vy = vx; + vx = 0; + } + + MBSphere *candidate = new (&spheres[i]) MBSphere(radius, 0, 0, vx, vy, hw_random8()); + + // Make sure the sphere doesn't land on another one. + bool conflicted = false; + int safety = 100; // Don't try a fit too many items. + do { + // Give it a random location—closer to the vertical center based on the uniformity. + // (Gotcha! WLED random() returns unsigned. It can't go negative.) + nfixed x = random(internalX); + nfixed y = halfInternalY + (((int32_t)(random(internalY)) - halfInternalY) * complementUniformity / 100) & 0xffff0000; + if (!is2D) { + y = random(internalY); + x = 0; } + candidate->newLoc(x, y); - MBSphere *candidate = new (&spheres[i]) MBSphere(radius, 0, 0, vx, vy, hw_random8()); - - // Make sure the sphere doesn't land on another one. - bool conflicted = false; - int safety = 100; // Don't try a fit too many items. - do - { - // Give it a random location—closer to the vertical center based on the uniformity. - // (Gotcha! WLED random() returns unsigned. It can't go negative.) - nfixed x = random(internalX); - nfixed y = halfInternalY + (((int32_t)(random(internalY)) - halfInternalY) * complementUniformity / 100) & 0xffff0000; - if (!is2D) - { - y = random(internalY); - x = 0; - } - candidate->newLoc(x, y); - - // Make sure it doesn't land on anything else. - conflicted = false; - for (int j = 0; j < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++j) - if (spheres[j].areSpheresColliding(*candidate)) - { - conflicted = true; - break; - } - } while (conflicted && --safety >= 0); - - // Stop, if we were unsuccessful. - if (conflicted) + // Make sure it doesn't land on anything else. + conflicted = false; + for (int j = 0; j < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++j) + if (spheres[j].areSpheresColliding(*candidate)) { + conflicted = true; break; - - ++SEGMENT.aux0; // Increments SPHERES_ALLOCATED + } + } while (conflicted && --safety >= 0); + + // Stop, if we were unsuccessful. + if (conflicted) + break; + + ++SEGMENT.aux0; // Increments SPHERES_ALLOCATED } SEGMENT.aux0 = (numSpheres << SPHERES_DESIRED_SHIFT) | (SEGMENT.aux0 & SPHERES_ALLOCATED); @@ -522,8 +478,7 @@ void mode_ElasticCollisions(void) { // by Nicholas Pisarro, Jr. } // If it is time to do something. - if (millis() > SEGMENT.step) - { + if (millis() > SEGMENT.step) { // Turm off all the LEDS. for (int i = 0; i < SEGLEN; ++i) SEGMENT.setPixelColor(i, CRGB::Black); @@ -535,33 +490,30 @@ void mode_ElasticCollisions(void) { // by Nicholas Pisarro, Jr. // Move the spheres and check for collisions with the walls. // We want of range from 0.1->1->10. nfixed speed = sliderToSpeed(SEGMENT.speed); - for (int i = 0; i < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++i) - { - // nfixed fixedSpeed = speed * (1 << SPHERE_PREC_SHIFT); - spheres[i].update(speed); + for (int i = 0; i < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++i) { + // nfixed fixedSpeed = speed * (1 << SPHERE_PREC_SHIFT); + spheres[i].update(speed); - // If nearing a regeneration, let the walls fall and the spheres fly off! - if (SEGMENT.aux1 > WALL_COLLAPSE_INTR) - spheres[i].handleWallCollision(internalX, internalY); + // If nearing a regeneration, let the walls fall and the spheres fly off! + if (SEGMENT.aux1 > WALL_COLLAPSE_INTR) + spheres[i].handleWallCollision(internalX, internalY); } // Check for collisions with other spheres. for (int i = 0; i < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++i) for (int j = i + 1; j < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++j) - if (spheres[i].areSpheresColliding(spheres[j])) - { - /* Make sure the two spheres haven't collided so hard that - * one is inside the other. */ - spheres[i].enforceMinDist(spheres + j); - spheres[i].handleCollision(spheres + j, is2D); - } + if (spheres[i].areSpheresColliding(spheres[j])) { + /* Make sure the two spheres haven't collided so hard that + * one is inside the other. */ + spheres[i].enforceMinDist(spheres + j); + spheres[i].handleCollision(spheres + j, is2D); + } - // After a while, force a complete recalculation - if (--SEGMENT.aux1 == 0) - { - SEGMENT.aux1 = elasticLifetime(); - SEGMENT.aux0 = 0; - } + // After a while, force a complete recalculation + if (--SEGMENT.aux1 == 0) { + SEGMENT.aux1 = elasticLifetime(); + SEGMENT.aux0 = 0; + } // Remember the last time SEGMENT.step += BOUNCE_CYCLE_TIME; From 71bd8d93f2ed297a5522e03ebaee163e4b4a7208 Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Mon, 4 May 2026 07:42:34 -0400 Subject: [PATCH 25/31] Added an "Avg. Duty Cycle" slider. Fixed a bug in accessing 'maxCycle'. --- wled00/FX.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index a1edee8c8d..abff53bc40 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7829,11 +7829,17 @@ void mode_XmasTwinkle(void) { if (light->isOn) { // Turning ON - short duration (1/3 of off time) - eventTime = skewedTime(lights[i].maxCycle / 3); + uint32_t wkgMaxCycle = light->maxCycle; + if (SEGMENT.custom1 < 128) // If the Duty Cycle < 50%. + wkgMaxCycle = wkgMaxCycle * SEGMENT.custom1 >> 7; // Q24.8 -> Q32.0 * 2 + eventTime = skewedTime(wkgMaxCycle); if (SEGMENT.check1) light->colorIdx = hw_random8(); // New color each time } else { // Turning OFF - longer duration - eventTime = skewedTime(lights[i].maxCycle); + uint32_t wkgMaxCycle = light->maxCycle; + if (SEGMENT.custom1 >= 128) // If the Duty Cycle < 50%. + wkgMaxCycle = wkgMaxCycle * (256 - (uint16_t)SEGMENT.custom1) >> 7; // Q24.8 -> Q32.0 * 2 + eventTime = skewedTime(wkgMaxCycle); } } // Put the updated event time back. @@ -7863,7 +7869,7 @@ void mode_XmasTwinkle(void) { return; } // mode_XmasTwinkle -static const char _data_FX_MODE_XMASTWINKLE[] PROGMEM = "Xmas Twinkle@Twinkle speed,Density,,,,Color indices vary;;!;012;m12=0"; +static const char _data_FX_MODE_XMASTWINKLE[] PROGMEM = "Xmas Twinkle@Twinkle speed,Density,Avg. Duty Cycle,,,Color indices vary;;!;012;c1=43,m12=0"; // Distortion waves - ldirko From 88607ec396082bebf32d9faed82033a2cbfb8f10 Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Tue, 5 May 2026 08:30:24 -0400 Subject: [PATCH 26/31] Implemented changes to Elastic Collisions recommended by CodeRabbit with some corrections. --- .../elastic_collisions/Elastic_Collisions.cpp | 16 ++++++++-------- usermods/elastic_collisions/README.md | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/usermods/elastic_collisions/Elastic_Collisions.cpp b/usermods/elastic_collisions/Elastic_Collisions.cpp index 6859ea1910..dd9a72b44c 100644 --- a/usermods/elastic_collisions/Elastic_Collisions.cpp +++ b/usermods/elastic_collisions/Elastic_Collisions.cpp @@ -145,7 +145,7 @@ class MBSphere // Normalize direction if (length << 1 == 0) { // handle gracefully, but this shouldn't happen. - Serial.println("At 0 #1"); + DEBUG_PRINTLN("At 0 #1"); return; } nfixed scale = fixedDiv(dist - length, length << 1); @@ -170,7 +170,7 @@ class MBSphere nfixed dist = fixedDist(nx, ny); while (dist == 0) { // handle gracefully - // Serial.println("Two objects on top of each other!"); + // DEBUG_PRINTLN("Two objects on top of each other!"); x += 1 << (SPHERE_PREC_SHIFT -2); nx += 1 << (SPHERE_PREC_SHIFT -2); @@ -272,6 +272,7 @@ class MBSphere { vx = 0; vy = 0; + return; } // Don't calculate beyond the edges of the LED array. @@ -401,7 +402,7 @@ void mode_ElasticCollisions(void) { // by Nicholas Pisarro, Jr. // Radius distribution. const int dmTableSize = 20; - const uint8_t PROGMEM dmPercentages[20] = {40, 20, 10, 4, 3, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3}; + static const uint8_t PROGMEM dmPercentages[20] = {40, 20, 10, 4, 3, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 3}; // Reinitialize evertying if the number of spheres has changed. // (We need a separate counter for the number wanted, vs. the number actually initialized.) @@ -424,8 +425,8 @@ void mode_ElasticCollisions(void) { // by Nicholas Pisarro, Jr. nfixed radius = (7 << 16) + ((((uint64_t)skewedRandom(random(100), dmTableSize, dmPercentages) * complementUniformity << 16) / 10000) * (23 << 16) >> 16);// 7-30 // radius = 30 << SPHERE_PREC_SHIFT; nfixed massFactor = MBSphere::fixedDiv((11 << SPHERE_PREC_SHIFT), radius); // Big things should move slower to keep momentum down. - nfixed vx = (-50.0 + random(100)) * massFactor / 5.0; // ±10 - nfixed vy = (-50.0 + random(100)) * massFactor * complementUniformity / 500.0; // ±10 + nfixed vx = (-50 + (nfixed)hw_random(100)) * massFactor / 5; // ±10 + nfixed vy = (-50 + (nfixed)hw_random(100)) * massFactor * complementUniformity / 500; // ±10 radius /= 10; vx /= 10; vy /= 10; @@ -479,9 +480,8 @@ void mode_ElasticCollisions(void) { // by Nicholas Pisarro, Jr. // If it is time to do something. if (millis() > SEGMENT.step) { - // Turm off all the LEDS. - for (int i = 0; i < SEGLEN; ++i) - SEGMENT.setPixelColor(i, CRGB::Black); + // Turn off all the LEDS. + SEGMENT.fill(BLACK); // Draw the spheres. for (int i = 0; i < (SEGMENT.aux0 & SPHERES_ALLOCATED); ++i) diff --git a/usermods/elastic_collisions/README.md b/usermods/elastic_collisions/README.md index 5bcbfd5e76..218c2ede37 100644 --- a/usermods/elastic_collisions/README.md +++ b/usermods/elastic_collisions/README.md @@ -29,7 +29,7 @@ When you save an effect in *Presets*, it is saved as an ordinal effect number ca ## Parameters -1. **Speed** The average initial velocity of balls over a wide ranage. +1. **Speed** The average initial velocity of balls over a wide range. 2. **Count** 1-30 balls 3. **Uniformity** 0-100%. 100% uniformity is special as discussed above. -4. **Lifetime** Regenration time from 15 seconds to 1 hour. \ No newline at end of file +4. **Lifetime** Regeneration time from 15 seconds to 1 hour. \ No newline at end of file From 5b90294a8da91de5314cc715af0d146b825d6a0c Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Wed, 6 May 2026 05:27:04 -0400 Subject: [PATCH 27/31] Address more issues by CodeRabbit: Put "// AI:" tags around A.I. code. Fixed potential overflow. Use better LED erase. --- usermods/elastic_collisions/Elastic_Collisions.cpp | 8 ++++++-- wled00/FX.cpp | 4 +--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/usermods/elastic_collisions/Elastic_Collisions.cpp b/usermods/elastic_collisions/Elastic_Collisions.cpp index dd9a72b44c..e139052c46 100644 --- a/usermods/elastic_collisions/Elastic_Collisions.cpp +++ b/usermods/elastic_collisions/Elastic_Collisions.cpp @@ -95,6 +95,7 @@ class MBSphere } // Faster SQRT function curtesy Code Copilot 5. + // AI: below section was generated by an AI static nfixed fixed64Sqrt(int64_t n) { if (n <= 0) return 0; @@ -110,6 +111,7 @@ class MBSphere // Clamp back to 32-bit Q16.16 return (nfixed)res; } + // AI: end /* Squaring coordinates can blow out the range of nfixed. * Work with the 64 bit intermediate result. */ @@ -335,7 +337,7 @@ class MBSphere }; // Given 0-255 from SEGMENT.custom2, return in number of 50ms cycles. -uint32_t elasticLifetime() { +uint16_t elasticLifetime() { // 8 categories. switch (SEGMENT.custom2 >> 5) // /32 { @@ -354,7 +356,7 @@ uint32_t elasticLifetime() { case 6: return 36000; // 30m case 7: - return 72000; // 1hr + return 65535; // not quite 1 hr. default: return 1200; } @@ -363,6 +365,7 @@ uint32_t elasticLifetime() { /* We want of range from 0.1->1->10. * Thank you Claude.ai. */ nfixed sliderToSpeed(uint8_t slider) { + // AI: below section was generated by an AI // Q16.16 quadratic coefficients (calculated from your 3 points) const int32_t a_q16 = 8; // ~0.000148 in Q16.16 (much smaller!) const int32_t b_q16 = 300; // ~0.004336 in Q16.16 @@ -373,6 +376,7 @@ nfixed sliderToSpeed(uint8_t slider) { // Calculate ax² + bx + c in Q16.16 int64_t result = ((int64_t)a_q16 * x * x) + ((int64_t)b_q16 * x) + c_q16; + // AI: end return (int32_t)result; } diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 9143ce1d17..7ad15c3c5a 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7817,9 +7817,7 @@ void mode_XmasTwinkle(void) { } // Clear all LEDs - for (int i = 0; i < SEGLEN; i++) { - SEGMENT.setPixelColor(i, CRGB::Black); - } + SEGMENT.fill(BLACK); // Update each twinkle light for (int i = 0; i < numLights; i++) { From b70e6dcf50f0fcad96e15b88c9d72f9bc5257a2b Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Wed, 6 May 2026 17:19:14 -0400 Subject: [PATCH 28/31] Couple of code tweaks; accidental f.p. conversions, inconsequential reformatting. --- usermods/elastic_collisions/Elastic_Collisions.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/usermods/elastic_collisions/Elastic_Collisions.cpp b/usermods/elastic_collisions/Elastic_Collisions.cpp index e139052c46..3ecc7c1a7c 100644 --- a/usermods/elastic_collisions/Elastic_Collisions.cpp +++ b/usermods/elastic_collisions/Elastic_Collisions.cpp @@ -141,7 +141,7 @@ class MBSphere nfixed dy = sp->y - y; nfixed length = fixedDist(dx, dy); - if (length >= dist || length == 0.0) + if (length >= dist || length == 0) return; // Already long enough, or degenerate point // Normalize direction @@ -306,16 +306,14 @@ class MBSphere // alpha = 1 << SPHERE_PREC_SHIFT; // Store intensity in LED array (0-1 range) - if (draw) - { + if (draw) { drawColor = sphereColor; drawColor.nscale8(alpha * 255 >> SPHERE_PREC_SHIFT); } else drawColor = CRGB::Black; - if (alpha > 0.0) - { + if (alpha > 0) { if (is2D) seg.setPixelColorXY(lX, lY, drawColor); else From 85e6bc59e789e8586ae15ab5776c717529f217f6 Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Thu, 7 May 2026 02:27:51 -0400 Subject: [PATCH 29/31] Better initialization test. Properly initialize Xmas Twinkle timer. --- usermods/elastic_collisions/Elastic_Collisions.cpp | 2 +- wled00/FX.cpp | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/usermods/elastic_collisions/Elastic_Collisions.cpp b/usermods/elastic_collisions/Elastic_Collisions.cpp index 3ecc7c1a7c..c1311024ac 100644 --- a/usermods/elastic_collisions/Elastic_Collisions.cpp +++ b/usermods/elastic_collisions/Elastic_Collisions.cpp @@ -408,7 +408,7 @@ void mode_ElasticCollisions(void) { // by Nicholas Pisarro, Jr. // Reinitialize evertying if the number of spheres has changed. // (We need a separate counter for the number wanted, vs. the number actually initialized.) - if (numSpheres != ((SEGMENT.aux0 & SPHERES_DESIRED) >> SPHERES_DESIRED_SHIFT)) + if (! SEGMENT.call || numSpheres != ((SEGMENT.aux0 & SPHERES_DESIRED) >> SPHERES_DESIRED_SHIFT)) SEGMENT.aux0 = 0; // Point to the sheres. diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 7ad15c3c5a..56b773ff4d 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7799,13 +7799,13 @@ void mode_XmasTwinkle(void) { uint32_t lastTime = SEGMENT.step; uint32_t currTime = millis(); if (currTime < lastTime) - lastTime = 0; + SEGMENT.step = lastTime = 0; // The interval may be zero if the refresh rate is fast enough. uint32_t interval = currTime - lastTime; // Initialize on first run - if (SEGMENT.aux0 != numLights) { + if (! SEGMENT.call || SEGMENT.aux0 != numLights) { for (int i = 0; i < numLights; i++) { lights[i].colorIdx = hw_random8(); lights[i].isOn = false; @@ -7814,6 +7814,8 @@ void mode_XmasTwinkle(void) { lights[i].retwnkleTime = random(2, 20) * 1000; // 2 - 20 seconds 1st time around } SEGMENT.aux0 = numLights; // Mark as initialized + SEGMENT.step = currTime; + interval = 0; } // Clear all LEDs From 32d5a2dc469e14ed4323e91868417e273c3132d3 Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Wed, 20 May 2026 08:23:55 -0400 Subject: [PATCH 30/31] Fixed various issues brought up by coderabbitai. --- .../elastic_collisions/Elastic_Collisions.cpp | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/usermods/elastic_collisions/Elastic_Collisions.cpp b/usermods/elastic_collisions/Elastic_Collisions.cpp index c1311024ac..2926329a28 100644 --- a/usermods/elastic_collisions/Elastic_Collisions.cpp +++ b/usermods/elastic_collisions/Elastic_Collisions.cpp @@ -24,28 +24,30 @@ typedef int32_t nfixed; // These represent fixed point f // Input is 0-100, Ouput is skewed 0-100. // PArray may be any size, but elements must add up to 100. +// PArray is assumed to be 'static PROGMEM'. #define RAND_PREC_SHIFT 10 // Vertual binary point from the right -int32_t skewedRandom( uint8_t rand100, +static int32_t skewedRandom( uint8_t rand100, const uint8_t pArraySize, const uint8_t *pArray) { int32_t index = 0; int32_t cumulativePercentage = 0; // Find the range in the table based on randomValue. - while (index < pArraySize - 1 && rand100 >= cumulativePercentage + pArray[index]) { - cumulativePercentage += pArray[index]; + while (index < pArraySize - 1 && rand100 >= cumulativePercentage + pgm_read_byte_near(pArray + index)) { + cumulativePercentage += pgm_read_byte_near(pArray + index); index++; } // Calculate linear interpolation - int32_t t = ((rand100 - cumulativePercentage) << RAND_PREC_SHIFT) / pArray[index]; + const uint8_t bucket = pgm_read_byte_near(pArray + index); + int32_t t = ((rand100 - cumulativePercentage) << RAND_PREC_SHIFT) / bucket; int32_t result = ((index << RAND_PREC_SHIFT) + t) * 100 / pArraySize >> RAND_PREC_SHIFT; return result; } // --- Portable countLeadingZeros64 for faster SQRT --- -int countLeadingZeros64(uint64_t x) +static int countLeadingZeros64(uint64_t x) { #if defined(__GNUC__) || defined(__clang__) return __builtin_clzll(x); @@ -90,6 +92,7 @@ class MBSphere static nfixed fixedSqrt(nfixed x) { // Promote to 64-bit and scale up for precision + if (x <= 0) return 0; uint64_t n = (uint64_t)x << SPHERE_PREC_SHIFT; // Q16.16 -> Q32.32 return fixed64Sqrt(n); } @@ -97,7 +100,7 @@ class MBSphere // Faster SQRT function curtesy Code Copilot 5. // AI: below section was generated by an AI static nfixed fixed64Sqrt(int64_t n) { - if (n <= 0) return 0; + if (n == 0) return 0; // Initial guess from highest bit. int lz = 63 - countLeadingZeros64(n); @@ -125,10 +128,10 @@ class MBSphere void newLoc(nfixed newX, nfixed newY) { x = newX; y = newY; } // Detect if two circles are colliding (simple distance check) - bool areSpheresColliding(MBSphere sp) { + bool areSpheresColliding(const MBSphere &sp) const { nfixed dist = fixedDist(sp.x - this->x, sp.y - this->y); return dist <= this->radius + sp.radius; - } + } /* Make sure two spheres haven't gotten too close. * Note: There is a pathological case where two spheres @@ -300,10 +303,7 @@ class MBSphere nfixed dist = fixedDist(pixelX - x, pixelY - y); // Compute anti-aliasing weight - // float alpha = RGBEffect::clamp(1.0f - RGBEffect::smoothstep(FLOAT_IT(edge0), FLOAT_IT(edge1), dist), 0.0f, 1.0f); nfixed alpha = clamp((1 << SPHERE_PREC_SHIFT) - smoothstep(edge0, edge1, dist), 0, 1 << SPHERE_PREC_SHIFT) + 0; - // nfixed alpha = clamp((1 << SPHERE_PREC_SHIFT) - smoothstep(edge0, edge1, dist), 1 << (SPHERE_PREC_SHIFT - 2), 1 << SPHERE_PREC_SHIFT); - // alpha = 1 << SPHERE_PREC_SHIFT; // Store intensity in LED array (0-1 range) if (draw) { @@ -335,7 +335,7 @@ class MBSphere }; // Given 0-255 from SEGMENT.custom2, return in number of 50ms cycles. -uint16_t elasticLifetime() { +static uint16_t elasticLifetime() { // 8 categories. switch (SEGMENT.custom2 >> 5) // /32 { @@ -362,7 +362,7 @@ uint16_t elasticLifetime() { /* We want of range from 0.1->1->10. * Thank you Claude.ai. */ -nfixed sliderToSpeed(uint8_t slider) { +static nfixed sliderToSpeed(uint8_t slider) { // AI: below section was generated by an AI // Q16.16 quadratic coefficients (calculated from your 3 points) const int32_t a_q16 = 8; // ~0.000148 in Q16.16 (much smaller!) From 2092b686d32f03900aa3d78db824b69ac8f80847 Mon Sep 17 00:00:00 2001 From: "Nicholas Pisarro, Jr." Date: Thu, 21 May 2026 04:36:00 -0400 Subject: [PATCH 31/31] CodeRabbitAI was concerned about possible divide checks. I put in some safety checks. --- usermods/elastic_collisions/Elastic_Collisions.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/usermods/elastic_collisions/Elastic_Collisions.cpp b/usermods/elastic_collisions/Elastic_Collisions.cpp index 2926329a28..24f365c1a8 100644 --- a/usermods/elastic_collisions/Elastic_Collisions.cpp +++ b/usermods/elastic_collisions/Elastic_Collisions.cpp @@ -87,6 +87,8 @@ class MBSphere } static nfixed fixedDiv(nfixed a, nfixed b) { + if (a == b) // (unlikely) safety + return 0; return ((int64_t)a << SPHERE_PREC_SHIFT) / b; } @@ -247,6 +249,8 @@ class MBSphere edge0 >>= 8; edge1 >>= 8; x >>= 8; + if (edge1 == edge0) + return (x >= edge1) ? (1 << 16) : 0; // (unlikely) degenerate case: hard edge int t = clamp((x - edge0 << 8) / (edge1 - edge0), 0, 1 << 8); // Q24.8 return (t * t >> 8) * ((3 << 8) - 2 * t); // Result of cubing is Q16.16. }