diff --git a/usermods/elastic_collisions/Elastic_Collisions.cpp b/usermods/elastic_collisions/Elastic_Collisions.cpp new file mode 100644 index 0000000000..c1311024ac --- /dev/null +++ b/usermods/elastic_collisions/Elastic_Collisions.cpp @@ -0,0 +1,542 @@ +#include "wled.h" + +#define FX_FALLBACK_STATIC { SEGMENT.fill(SEGCOLOR(0)); return; } + +//////////////////////////// +// 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)) + +/* 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; + +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() { } + + 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. + // AI: below section was generated by an AI + 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; + } + // AI: end + + /* 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) + return; // Already long enough, or degenerate point + + // Normalize direction + if (length << 1 == 0) { + // handle gracefully, but this shouldn't happen. + DEBUG_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 + // DEBUG_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 + } + } + + 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; + return; + } + + // 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) { + 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. +uint16_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 65535; // not quite 1 hr. + default: + return 1200; + } +} + +/* 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 + 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; + // AI: end + + 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; + 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.) + if (! SEGMENT.call || 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 + (nfixed)hw_random(100)) * massFactor / 5; // ±10 + nfixed vy = (-50 + (nfixed)hw_random(100)) * massFactor * complementUniformity / 500; // ±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) { + // Turn off all the LEDS. + SEGMENT.fill(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..218c2ede37 --- /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 range. +2. **Count** 1-30 balls +3. **Uniformity** 0-100%. 100% uniformity is special as discussed above. +4. **Lifetime** Regeneration 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 bd42d1a9eb..56b773ff4d 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7749,6 +7749,132 @@ 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 // +///////////////////////// + +// 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 { + 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, 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)(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 + * 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) + 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.call || SEGMENT.aux0 != numLights) { + for (int i = 0; i < numLights; i++) { + lights[i].colorIdx = hw_random8(); + lights[i].isOn = false; + 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 + SEGMENT.step = currTime; + interval = 0; + } + + // Clear all LEDs + SEGMENT.fill(BLACK); + + // Update each twinkle light + for (int i = 0; i < numLights; i++) { + TwinkleLight* light = &lights[i]; + + // Check if it's time for state change + int16_t eventTime = light->nextEvent - interval; + if (eventTime <= 0) { + light->isOn = !light->isOn; + + if (light->isOn) { + // Turning ON - short duration (1/3 of off time) + 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 + 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. + light->nextEvent = eventTime; + + // Light the LED if on + if (light->isOn) { + 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. + SEGMENT.step += interval; + SEGMENT.aux1 = SEGMENT.speed; // So we know if this change. + + return; +} // mode_XmasTwinkle + +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 // https://editor.soulmatelights.com/gallery/1089-distorsion-waves // adapted for WLED by @blazoncek, improvements by @dedehai @@ -11107,6 +11233,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); // --- 1D audio effects --- addEffect(FX_MODE_PIXELS, &mode_pixels, _data_FX_MODE_PIXELS); diff --git a/wled00/FX.h b/wled00/FX.h index 9fd3a04d8a..27cc851e7e 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -372,7 +372,8 @@ 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 MODE_COUNT 221 #define TRANSITION_FADE 0x00 // universal