diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 0bface88..26b795ef 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -5228,8 +5228,276 @@ static const char _data_FX_MODE_2DFRIZZLES[] PROGMEM = "Frizzles@X frequency,Y f /////////////////////////////////////////// -// 2D Cellular Automata Game of life // +// 2D Cellular Automata Game of Life // /////////////////////////////////////////// +typedef struct Cell { + uint8_t currentStatus : 1, currentNeighborCount : 4, oscillatorCheck : 1, spaceshipCheck : 1, unused : 1; // current data + repeated detection + uint8_t futureStatus : 1, futureNeighborCount : 4, edgeCell : 1, superDead : 1, hasColor : 1; // future data + opimizations +} Cell; + +class GameOfLifeGrid { + private: + Cell* cells; + const int cols, rows, maxIndex; + const int nOffsets[8] = {-cols-1, -cols, -cols+1, -1, 1, cols-1, cols, cols+1}; // Neighbor offsets + const int offsetX[8] = {-1, 0, 1, -1, 1, -1, 0, 1}; + const int offsetY[8] = {-1, -1, -1, 0, 0, 1, 1, 1}; + public: + GameOfLifeGrid(Cell* data, int c, int r) : cells(data), cols(c), rows(r), maxIndex(r * c) {} + void getNeighborIndexes(unsigned neighbors[9], unsigned cIndex, unsigned x, unsigned y, bool wrap) { + bool edgeCell = cells[cIndex].edgeCell; + unsigned neighborCount = 0; + for (unsigned i = 0; i < 8; ++i) { + unsigned nIndex = cIndex + nOffsets[i]; + if (edgeCell) { + int nX = x + offsetX[i], nY = y + offsetY[i]; + if (nX < 0) {if (!wrap) continue; nIndex += cols;} + else if (nX >= cols) {if (!wrap) continue; nIndex -= cols;} + if (nY < 0) {if (!wrap) continue; nIndex += maxIndex;} + else if (nY >= rows) {if (!wrap) continue; nIndex -= maxIndex;} + } + neighbors[++neighborCount] = nIndex; + } + neighbors[0] = neighborCount; + } + void setCell(unsigned cIndex, unsigned x, unsigned y, bool alive, bool wrap) { + Cell* cell = &cells[cIndex]; + cell->futureStatus = alive; + if (alive == cell->currentStatus) return; // No change + unsigned neighbors[9]; + getNeighborIndexes(neighbors, cIndex, x, y, wrap); + int val = alive ? 1 : -1; + for (unsigned i = 1; i <= neighbors[0]; ++i) cells[neighbors[i]].futureNeighborCount += val; + } + void recalculateEdgeNeighbors(bool wrap) { + unsigned cIndex = 0; + for (unsigned y = 0; y < rows; ++y) for (unsigned x = 0; x < cols; ++x, ++cIndex) { + Cell* cell = &cells[cIndex]; + if (cell->edgeCell) { + cell->futureNeighborCount = 0; + cell->currentNeighborCount = 0; + cell->superDead = 0; + + unsigned neighbors[9]; + getNeighborIndexes(neighbors, cIndex, x, y, wrap); + + for (unsigned i = 1; i <= neighbors[0]; ++i) { + if (cells[neighbors[i]].currentStatus) { ++cell->futureNeighborCount; ++cell->currentNeighborCount; } + } + } + } + } + void shiftFutureToCurrent() { + for (unsigned i = 0; i < maxIndex; ++i) { + cells[i].currentStatus = cells[i].futureStatus; + cells[i].currentNeighborCount = cells[i].futureNeighborCount; + } + } +}; + +uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://natureofcode.com/book/chapter-7-cellular-automata/ + // and https://github.com/DougHaber/nlife-color , Modified By: Brandon Butler + 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() * sizeof(Cell); // Cell = 2 bytes + const size_t totalSize = dataSize + 5; // 5 bytes for prevRows, prevCols, prevPalette, prevWrap, soloGlider + + if (!SEGENV.allocateData(totalSize)) return mode_static(); //allocation failed + uint8_t *prevRows = reinterpret_cast(SEGENV.data); + uint8_t *prevCols = reinterpret_cast(SEGENV.data + 1); + uint8_t *prevPalette = reinterpret_cast(SEGENV.data + 2); + bool *prevWrap = reinterpret_cast (SEGENV.data + 3); + bool *soloGlider = reinterpret_cast (SEGENV.data + 4); + Cell *cells = reinterpret_cast (SEGENV.data + 5); + + uint16_t& generation = SEGENV.aux0; //Rename SEGENV/SEGMENT variables for readability + uint16_t& gliderLength = SEGENV.aux1; + bool allColors = SEGMENT.check1; + bool overlayBG = SEGMENT.check2; + bool wrap = SEGMENT.check3; + bool bgBlendMode = SEGMENT.custom1 > 220 && !overlayBG; // if blur is high and not overlaying, use bg blend mode + byte blur = overlayBG ? 255 : bgBlendMode ? map2(SEGMENT.custom1 - 220, 0, 35, 255, 128) : map2(SEGMENT.custom1, 0, 220, 255, 10); + uint32_t bgColor = SEGCOLOR(1); + + GameOfLifeGrid grid(cells, cols, rows); + + // If rows or cols change due to mirror/transpose, edges and neighbor counts need to be recalculated. Just reset the game. + bool setup = SEGENV.call == 0 || rows != *prevRows || cols != *prevCols; + + if (setup) { + SEGMENT.setUpLeds(); + SEGMENT.fill(bgColor); // to make sure that segment buffer and physical leds are aligned initially + SEGENV.step = 0; + *prevRows = rows; + *prevCols = cols; + + // Calculate glider length LCM(rows,cols)*4 once + uint8_t a = rows; + uint8_t b = cols; + while (b) { + uint8_t t = b; + b = a % b; + a = t; + } + gliderLength = cols * rows / a * 4; + } + + if (abs(long(strip.now) - long(SEGENV.step)) > 2000) SEGENV.step = 0; // Timebase jump fix + bool paused = SEGENV.step > strip.now; + + // Setup New Game of Life + if ((!paused && generation == 0) || setup) { + SEGENV.step = strip.now + 1250; // show initial state for 1.25 seconds + paused = true; + generation = 1; + *prevWrap = wrap; + *prevPalette = SEGMENT.palette; + + //Setup Grid + memset(cells, 0, dataSize); + #if !ESP32 + random16_set_seed(strip.now>>2); //seed the random generator + #else + const uint32_t chance = UINT32_MAX * 0.32; + #endif + unsigned cIndex = 0; + for (unsigned y = 0; y < rows; ++y) for (unsigned x = 0; x < cols; ++x, ++cIndex) { + if (x == 0 || x == cols - 1 || y == 0 || y == rows - 1) cells[cIndex].edgeCell = 1; + #if ESP32 + if (esp_random() < chance) grid.setCell(cIndex, x, y, true, wrap); // ~32% chance of being alive + #else + if (random16(100) < 32) grid.setCell(cIndex, x, y, true, wrap); + #endif + else { cells[cIndex].hasColor = 1; cells[cIndex].superDead = 1; } + } + grid.shiftFutureToCurrent(); // Shift future states to current states + } + + bool palChanged = SEGMENT.palette != *prevPalette && !allColors; + if (palChanged) *prevPalette = SEGMENT.palette; + + // Enter redraw loop if not updating or palette changed. + if (palChanged || paused || (SEGMENT.speed != 255 && strip.now - SEGENV.step < 1000 / map2(SEGMENT.speed,0,254,1,60))) { //(1 - 60) updates/sec 255 is uncapped + // Redraw if paused (remove blur), palette changed, overlaying background if not max speed (avoid flicker) + // Generation 1 draws alive cells randomly and fades dead cells + bool newGame = generation == 1; + if (paused || palChanged || overlayBG) { + unsigned cIndex = 0; + for (unsigned y = 0; y < rows; ++y) for (unsigned x = 0; x < cols; ++x, ++cIndex) { + Cell& cell = cells[cIndex]; + if (!newGame && cell.superDead) continue; // Skip super dead cells unless new game + bool alive = cell.currentStatus; + uint32_t cellColor = SEGMENT.getPixelColorXY(x,y); + if (alive) { + if ((!cell.hasColor && !random(10)) || palChanged) { + uint32_t randomColor = allColors ? random16() * random16() : SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 0); + SEGMENT.setPixelColorXY(x,y, randomColor); // Palette changed or needs initial color + cells[cIndex].hasColor = 1; + } + else if (overlayBG && cell.hasColor) SEGMENT.setPixelColorXY(x,y, cellColor); // Redraw alive cells for overlayBG + } // Dead + else if (paused && !overlayBG) { + uint32_t blended = color_blend(cellColor, bgColor, bgBlendMode ? 16 : blur); + if (blended == cellColor) blended = bgColor; // color_blend fix + if ((bgBlendMode && newGame) || !bgBlendMode) SEGMENT.setPixelColorXY(x, y, blended); // Blur dead cells when paused + } + } + } + return FRAMETIME; + } + + uint32_t color = allColors ? random16() * random16() : SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 0); // Backup color + if (generation <= 8) blur = 255 - (((generation-1) * (255 - blur)) >> 3); // Ramp up blur for first 8 generations + + //Update Game of Life + bool disableWrap = !wrap || generation % 1500 == 0 || *soloGlider; // Disable wrap every 1500 generations to prevent undetected repeats + if (*prevWrap != !disableWrap) { grid.recalculateEdgeNeighbors(!disableWrap); *prevWrap = !disableWrap; } + // Repeat detection + unsigned aliveCount = 0; // Detects empty grids and solo gliders (for smaller grids) + bool updateOscillator = generation % 16 == 0; + bool updateSpaceship = gliderLength && generation % gliderLength == 0; + bool repeatingOscillator = true, repeatingSpaceship = true; + + //Loop through all cells. Apply rules, setPixel + unsigned cIndex = 0; + for (unsigned y = 0; y < rows; ++y) for (unsigned x = 0; x < cols; ++x, ++cIndex) { + Cell& cell = cells[cIndex]; + if (repeatingOscillator && cell.oscillatorCheck != cell.currentStatus) repeatingOscillator = false; + if (repeatingSpaceship && cell.spaceshipCheck != cell.currentStatus) repeatingSpaceship = false; + if (updateOscillator) cell.oscillatorCheck = cell.currentStatus; + if (updateSpaceship) cell.spaceshipCheck = cell.currentStatus; + + unsigned neighbors = cell.currentNeighborCount; + if (cell.superDead && neighbors != 3) continue; // Skip super dead cells (bgColor dead cells) + + bool cellValue = cell.currentStatus; + uint32_t cellColor = SEGMENT.getPixelColorXY(x, y); + + if (cellValue) { + ++aliveCount; + if (cellColor != bgColor) color = cellColor; // Update last seen color + if (neighbors < 2 || neighbors > 3) { + // Loneliness or Overpopulation + grid.setCell(cIndex, x, y, false, !disableWrap); + if (!overlayBG) SEGMENT.setPixelColorXY(x,y, blur == 255 ? bgColor : color_blend(cellColor, bgColor, blur)); + if (blur == 255) cell.superDead = 1; + } + else SEGMENT.setPixelColorXY(x, y, cellColor == bgColor ? color : cellColor); // Redraw alive + } + else if (neighbors == 3 && !cellValue) { + // Reproduction + grid.setCell(cIndex, x, y, true, !disableWrap); + cell.superDead = 0; + uint32_t birthColor = color; + if (random8() < SEGMENT.intensity) birthColor = allColors ? random16() * random16() : SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 0); + else { + // Get Colors + uint32_t nColors[8]; + unsigned colorCount = 0; + unsigned neighbors[9]; + grid.getNeighborIndexes(neighbors, cIndex, x, y, !disableWrap); + + for (unsigned i = 1; i <= neighbors[0]; ++i) { + unsigned nIndex = neighbors[i]; + if (cells[nIndex].futureStatus) { + uint32_t nColor = SEGMENT.getPixelColorXY(nIndex % cols, nIndex / cols); + if (nColor == bgColor) continue; + nColors[colorCount++] = nColor; + } + } + if (colorCount) { birthColor = nColors[random8(colorCount)]; color = birthColor; } + } + SEGMENT.setPixelColorXY(x,y, birthColor); + } + else { // Already dead + if (blur != 255 && !overlayBG && !bgBlendMode) { + uint32_t blended = color_blend(cellColor, bgColor, blur); // color_blend doesn't always converge to bgColor (this fix needed for fast fps with custom bgColor) + if (blended == cellColor) { blended = bgColor; cell.superDead = 1; } + SEGMENT.setPixelColorXY(x, y, blended); + } + } + } + + grid.shiftFutureToCurrent(); + + if (aliveCount == 5) *soloGlider = true; else *soloGlider = false; + if (repeatingOscillator || repeatingSpaceship || !aliveCount) { + generation = 0; // reset on next call + SEGENV.step += 1000; // pause final generation for 1 second + return FRAMETIME; + } + + ++generation; + SEGENV.step = strip.now; + return FRAMETIME; +} // 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 // +///////////////////////// static bool getBitValue(const uint8_t* byteArray, size_t n) { size_t byteIndex = n / 8; size_t bitIndex = n % 8; @@ -5244,199 +5512,8 @@ static void setBitValue(uint8_t* byteArray, size_t n, bool value) { else byteArray[byteIndex] &= ~(1 << bitIndex); } - -uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://natureofcode.com/book/chapter-7-cellular-automata/ - // and https://github.com/DougHaber/nlife-color , Modified By: Brandon Butler - 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 - const size_t detectionSize = sizeof(uint16_t) * 3 + 1; // 2 CRCs, gliderLength, soloGlider boolean - const size_t totalSize = dataSize * 2 + detectionSize + sizeof(uint8_t); // detectionSize + prevPalette - - if (!SEGENV.allocateData(totalSize)) return mode_static(); //allocation failed - byte *cells = reinterpret_cast(SEGENV.data); - byte *futureCells = reinterpret_cast(SEGENV.data + dataSize); - uint16_t *gliderLength = reinterpret_cast(SEGENV.data + dataSize * 2); - uint16_t *oscillatorCRC = reinterpret_cast(SEGENV.data + dataSize * 2 + sizeof(uint16_t)); - uint16_t *spaceshipCRC = reinterpret_cast(SEGENV.data + dataSize * 2 + sizeof(uint16_t) * 2); - bool *soloGlider = reinterpret_cast(SEGENV.data + dataSize * 2 + sizeof(uint16_t) * 3); - uint8_t *prevPalette = reinterpret_cast(SEGENV.data + dataSize * 2 + detectionSize); - - uint16_t &generation = SEGENV.aux0; //Rename SEGENV/SEGMENT variables for readability - bool allColors = SEGMENT.check1; - bool overlayBG = SEGMENT.check2; - bool wrap = SEGMENT.check3; - bool bgBlendMode = SEGMENT.custom1 > 220 && !overlayBG; // if blur is high and not overlaying, use bg blend mode - byte blur = bgBlendMode ? map2(SEGMENT.custom1 - 220, 0, 35, 255, 128) : map2(SEGMENT.custom1, 0, 255, 255, 0); - uint32_t bgColor = SEGCOLOR(1); - uint32_t color = allColors ? random16() * random16() : SEGMENT.color_from_palette(0, false, PALETTE_SOLID_WRAP, 0); - - if (SEGENV.call == 0) { - SEGMENT.setUpLeds(); - SEGMENT.fill(BLACK); // to make sure that segment buffer and physical leds are aligned initially - SEGENV.step = 0; - } - // fix SEGENV.step in case that timebase jumps - if (abs(long(strip.now) - long(SEGENV.step)) > 2000) SEGENV.step = 0; - - // Setup New Game of Life - if ((SEGENV.call == 0 || generation == 0) && SEGENV.step < strip.now) { - SEGENV.step = strip.now + 1250; // show initial state for 1.25 seconds - generation = 1; - *prevPalette = SEGMENT.palette; - random16_set_seed(strip.now>>2); //seed the random generator - //Setup Grid - memset(cells, 0, dataSize); - for (unsigned x = 0; x < cols; x++) for (unsigned y = 0; y < rows; y++) { - if (random8(100) < 32) { // ~32% chance of being alive - setBitValue(cells, y * cols + x, true); - if (overlayBG) SEGMENT.setPixelColorXY(x,y, allColors ? random16() * random16() : SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 0)); - else SEGMENT.setPixelColorXY(x,y, bgColor); // Initial color set in redraw loop - } - } - memcpy(futureCells, cells, dataSize); - - //Set CRCs - uint16_t crc = crc16((const unsigned char*)cells, dataSize); - *oscillatorCRC = crc; - *spaceshipCRC = crc; - - //Calculate glider length LCM(rows,cols)*4 - uint8_t a = rows; - uint8_t b = cols; - while (b) { - uint8_t t = b; - b = a % b; - a = t; - } - *gliderLength = cols * rows / a * 4; - } - - bool blurDead = SEGENV.step > strip.now && blur !=255 && !bgBlendMode && !overlayBG; - bool palChanged = SEGMENT.palette != *prevPalette && !allColors; - bool newGame = generation == 1; - if (palChanged) *prevPalette = SEGMENT.palette; - - // Redraw Loop - // Redraw if paused (remove blur), palette changed, overlaying background (avoid flicker) - // Generation 1 draws alive cells randomly and fades dead cells - if (blurDead || newGame || palChanged || overlayBG) { - for (unsigned x = 0; x < cols; x++) for (unsigned y = 0; y < rows; y++) { - unsigned cIndex = y * cols + x; - uint32_t cellColor = SEGMENT.getPixelColorXY(x,y); - bool alive = getBitValue(cells, cIndex); - bool aliveBgColor = (!overlayBG && alive && newGame && cellColor == bgColor ); - - if ( alive && (palChanged || (aliveBgColor && !random(12)))) { // Palette change or spawn initial colors randomly - uint32_t randomColor = allColors ? random16() * random16() : SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 0); - SEGMENT.setPixelColorXY(x,y, randomColor); // Recolor alive cells - } - else if ( alive && overlayBG && !aliveBgColor) SEGMENT.setPixelColorXY(x,y, cellColor); // Redraw alive cells for overlayBG - if (!alive && palChanged && !overlayBG) SEGMENT.setPixelColorXY(x,y, bgColor); // Remove blurred cells from previous palette - else if (!alive && blurDead) SEGMENT.setPixelColorXY(x,y, color_blend(cellColor, bgColor, blur));// Blur dead cells (paused) - else if (!alive && !overlayBG && generation == 1) SEGMENT.setPixelColorXY(x,y, color_blend(cellColor, bgColor, 16)); // Fade dead cells on generation 1 - } - } - - if (!SEGMENT.speed || SEGENV.step > strip.now || (SEGMENT.speed != 255 && strip.now - SEGENV.step < 1000 / map2(SEGMENT.speed,0,254,0,60))) return FRAMETIME; //(0 - 60) updates/sec 255 is uncapped - - //Update Game of Life - unsigned aliveCount = 0; // Detects dead grids and solo gliders - bool disableWrap = !wrap || (generation % 1500 == 0 || *soloGlider); // Disable wrap every 1500 generations to prevent undetected repeats - //Loop through all cells. Count neighbors, apply rules, setPixel - for (unsigned x = 0; x < cols; x++) for (unsigned y = 0; y < rows; y++) { - unsigned cIndex = y * cols + x; - bool cellValue = getBitValue(cells, cIndex); - uint32_t cellColor = SEGMENT.getPixelColorXY(x, y); - if (cellValue) aliveCount++; - - unsigned neighbors = 0, colorCount = 0; - unsigned neighborIndexes[3]; - - // Count neighbors and store indexes, get neighbor colors later if needed - for (int i = -1; i <= 1; i++) for (int j = -1; j <= 1; j++) { // Iterate through all neighbors - if (i == 0 && j == 0) continue; // Ignore self - if (i == 1 && j == 0 && !cellValue && !neighbors) break; // Cell can't be born with no neighbors and 2 remaining checks - int nX = x + i; - int nY = y + j; - if (nX < 0) {if (disableWrap) continue; nX = cols - 1;} - else if (nX >= cols) {if (disableWrap) continue; nX = 0;} - if (nY < 0) {if (disableWrap) continue; nY = rows - 1;} - else if (nY >= rows) {if (disableWrap) continue; nY = 0;} - - unsigned nIndex = nY * cols + nX; // Neighbor cell index - if (getBitValue(cells, nIndex)) { - ++neighbors; - if (neighbors > 3) break; // Cell dies, stop neighbor loop - neighborIndexes[neighbors - 1] = nIndex; // Store alive neighbor index - } - } - - if (!cellValue && neighbors != 3 && cellColor == bgColor) continue; // Skip dead cells with no neighbors and no color - - // Rules of Life - if (cellValue && (neighbors < 2 || neighbors > 3)) { - // Loneliness or Overpopulation - setBitValue(futureCells, cIndex, false); - if (!overlayBG) SEGMENT.setPixelColorXY(x,y, color_blend(cellColor, bgColor, blur)); - } - else if (neighbors == 3 && !cellValue) { - // Reproduction - // Get Colors - uint32_t nColors[3]; - for (int i = 0; i < 3; i++) { - unsigned nIndex = neighborIndexes[i]; - if (!getBitValue(futureCells, nIndex)) continue; // Parent just died, color lost or blended - uint32_t nColor = SEGMENT.getPixelColorXY(nIndex % cols, nIndex / cols); - if (nColor == bgColor) continue; - color = nColor; // Update last seen color - nColors[colorCount++] = nColor; - - } - setBitValue(futureCells, cIndex, true); - uint32_t birthColor = colorCount ? nColors[random8(colorCount)] : color; // Uses last seen color if no surviving neighbors - // Mutate color chance - if (random8() < SEGMENT.intensity) birthColor = allColors ? random16() * random16() : SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 0); - SEGMENT.setPixelColorXY(x,y, birthColor); - } - else { // Blur dead cells and redraw alive cells - if (cellValue) SEGMENT.setPixelColorXY(x, y, cellColor == bgColor ? color : cellColor); // Redraw alive, fixes fading cells - else if (blur != 255 && !overlayBG && !bgBlendMode) SEGMENT.setPixelColorXY(x, y, color_blend(cellColor, bgColor, blur)); - } - - } - //update cell values - memcpy(cells, futureCells, dataSize); - - // Get current crc value - uint16_t crc = crc16((const unsigned char*)cells, dataSize); - - bool repetition = false; - if (!aliveCount || crc == *oscillatorCRC || crc == *spaceshipCRC) repetition = true; //check if cell changed this gen and compare previous stored crc values - if (repetition) { - generation = 0; // reset on next call - SEGENV.step += 1000; // pause final generation for 1 second - return FRAMETIME; - } - // Update CRC values - if (generation % 16 == 0) *oscillatorCRC = crc; - if (*gliderLength && generation % *gliderLength == 0) *spaceshipCRC = crc; - if (aliveCount == 5) *soloGlider = true; else *soloGlider = false; - - generation++; - SEGENV.step = strip.now; - return FRAMETIME; -} // 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 + // Uses 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();