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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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/14] 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