diff --git a/wled00/FX.cpp b/wled00/FX.cpp index f96deabe91..5b1f3515e2 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -5381,6 +5381,117 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: } // mode_2Dgameoflife() static const char _data_FX_MODE_2DGAMEOFLIFE[] PROGMEM = "Game Of Life@!,Color Mutation ☾,Blur ☾,,,All Colors ☾,Overlay BG ☾,Wrap ☾;!,!;!;2;sx=56,ix=2,c1=128,o1=0,o2=0,o3=1"; +///////////////////////// +// 2D SnowFall // +///////////////////////// + +uint16_t mode_2DSnowFall(void) { // By: Brandon Butler + // Uses Game of Life style bit array to track snow/particles + if (!strip.isMatrix) return mode_static(); // Not a 2D set-up + const uint16_t cols = SEGMENT.virtualWidth(); + const uint16_t rows = SEGMENT.virtualHeight(); + const size_t dataSize = (SEGMENT.length() + 7) / 8; // Round up to nearest byte + + if (!SEGENV.allocateData(dataSize)) return mode_static(); // Allocation failed + byte *grid = reinterpret_cast(SEGENV.data); + + bool overlay = SEGMENT.check2; // Overlay is inverted. Only draws non-snow. Layer 1 controls snow color + uint32_t bgColor = SEGCOLOR(1); + + if (SEGENV.call == 0) { + SEGMENT.setUpLeds(); + SEGMENT.fill(bgColor); + SEGENV.aux0 = 0; // Overflow value + memset(grid, 0, dataSize); + } + + // Draw non snow for inverted overlay + if (overlay) { + for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) { + if (!getBitValue(grid, y * cols + x)) SEGMENT.setPixelColorXY(x, y, bgColor); + } + } + + uint8_t speed = map(SEGMENT.speed, 0, 255, 0, 60); // Updates per second + if (!speed || strip.now - SEGENV.step < 1000 / speed) return FRAMETIME; // Not enough time passed + + uint8_t blur = map(SEGMENT.custom2, 0, 255, 255, 0); + uint8_t sway = SEGMENT.custom3; + + // Despawn snow + bool overflow = SEGENV.aux0 && SEGMENT.check3; + int despawnChance = SEGMENT.custom1 == 255 ? 256 : map(SEGMENT.custom1, 0, 255, 0, 100); // 255 goes to 256, allows always despawn + int lastY = rows - 1; + for (int x = 0; x < cols; x++) { + if (overflow || random8() < despawnChance) setBitValue(grid, lastY * cols + x, 0); + if (overlay || getBitValue(grid, lastY * cols + x)) continue; // Skip drawing if inverted overlay or snow + SEGMENT.blendPixelColorXY(x, lastY, bgColor, blur); + } + if (SEGENV.aux0) --SEGENV.aux0; // Decrease overflow + + // Precompute shuffled indices, helps randomize snow movement + uint16_t shuffledIndices[cols]; + for (int i = 0; i < cols; i++) shuffledIndices[i] = i; + std::random_shuffle(shuffledIndices, shuffledIndices + cols); + + // Update snow, loop from 2nd bottom row to top with precomputed random order + for (int y = rows - 2; y >= 0; y--) { + for (int i = 0; i < cols; i++) { + int x = shuffledIndices[i]; + + int pos = XY(x, y); + + uint32_t xyColor = SEGMENT.getPixelColorXY(x, y); // Limit getPixelColorXY calls + if (!getBitValue(grid, pos)) { // No snow, fade if needed and skip + if (!overlay && blur) SEGMENT.setPixelColorXY(x, y, color_blend(xyColor, bgColor, blur)); + continue; + } + + int newX = x, newY = y + 1; + int newPos = XY(newX, newY); + // Open Position Booleans + bool down = !getBitValue(grid, newPos); + bool downLeft = x > 0 && !getBitValue(grid, newPos - 1); + bool downRight = x < cols - 1 && !getBitValue(grid, newPos + 1); + + if (!down) { + if (downLeft && downRight) newX = random8(2) ? x - 1 : x + 1; + else if (downLeft) newX = x - 1; + else if (downRight) newX = x + 1; + else newY = y; // Snow is stuck + } + else if (sway && random8(30) < sway) { // Sway falling snow if horizontal and diagonal directions are open + if (x % 2 == 1 && downLeft && !getBitValue(grid, pos - 1)) newX = x - 1; // Odd Columns Move Left + else if (x % 2 == 0 && downRight && !getBitValue(grid, pos + 1)) newX = x + 1; // Even Columns Move Right + } + + if (newY != y || newX != x) { // Snow moved + setBitValue(grid, pos, 0); // Clear old + setBitValue(grid, XY(newX, newY), 1); // Set new + if (!overlay) SEGMENT.setPixelColorXY(x, y, color_blend(xyColor, bgColor, blur)); // Fade old + } + if (!overlay) SEGMENT.setPixelColorXY(newX, newY, xyColor); // Draw new / redraw stuck + } + } + + // Spawn snow + int spawnChance = map(SEGMENT.intensity, 0, 255, 0, 100); + for (int x = 0; x < cols; x++) { // y = 0 + if (random8() >= spawnChance) continue; + if (getBitValue(grid, x)) {SEGENV.aux0 = rows; continue;} // Snow exists, overflowing + + setBitValue(grid, x, 1); // Spawn snow + if (overlay) continue; // Skip drawing if inverted overlay + + if (SEGMENT.check1) SEGMENT.setPixelColorXY(x, 0, ColorFromPalette(SEGPALETTE, random8())); // Use palette + else {int c = random8(120,200); SEGMENT.setPixelColorXY(x, 0, c, c, c);} // Use snow color + } + + SEGENV.step = strip.now; + return FRAMETIME; +} // mode_2DSnowFall() +static const char _data_FX_MODE_2DSNOWFALL[] PROGMEM = "Snow Fall ☾@!,Spawn Rate,Despawn Rate,Blur,Sway Chance,Use Palette,Inverted Overlay,Prevent Overflow,;!,!;!;2;sx=128,ix=16,c1=17,c2=0,c3=0,o1=0,o2=0,o3=1"; + ///////////////////////// // 2D Hiphotic // ///////////////////////// @@ -8777,6 +8888,7 @@ void WS2812FX::setupEffectData() { addEffect(FX_MODE_2DSOAP, &mode_2Dsoap, _data_FX_MODE_2DSOAP); addEffect(FX_MODE_2DOCTOPUS, &mode_2Doctopus, _data_FX_MODE_2DOCTOPUS); addEffect(FX_MODE_2DWAVINGCELL, &mode_2Dwavingcell, _data_FX_MODE_2DWAVINGCELL); + addEffect(FX_MODE_2DSNOWFALL, &mode_2DSnowFall, _data_FX_MODE_2DSNOWFALL); addEffect(FX_MODE_2DAKEMI, &mode_2DAkemi, _data_FX_MODE_2DAKEMI); // audio diff --git a/wled00/FX.h b/wled00/FX.h index 04eabe70c9..5266e2edc5 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -351,8 +351,9 @@ bool strip_uses_global_leds(void) __attribute__((pure)); // WLEDMM implemented // #define FX_MODE_PALETTE_AR 193 // WLED-SR audioreactive palette #define FX_MODE_FIREWORKS_AR 194 // WLED-SR audioreactive fireworks #define FX_MODE_GEQLASER 195 // WLED-MM GEQ Laser -#define FX_MODE_2DPAINTBRUSH 196 // WLED-MM GEQ Laser -#define MODE_COUNT 197 +#define FX_MODE_2DPAINTBRUSH 196 // WLED-MM Paintbrush +#define FX_MODE_2DSNOWFALL 197 // WLED-MM Snowfall +#define MODE_COUNT 198 typedef enum mapping1D2D { M12_Pixels = 0,