Share your Esphome light effects

So I have a whole bunch of devices running esphome, with just the standard light effects listed on the site.
Has anyone made any cool custom effects?
If so please share - it would be really nice to build up a thread of all the custom effects people have made, especially for this time of year when lots of people are using esphome devices for cool Christmas light setups :stuck_out_tongue_winking_eye:

I’ve looked at wled which has some lovely effects, but the board never seemed reliable running that.

6 Likes

I have the following 3 I’ve collected. Honestly though, I highly recommend taking a look at WLED again. It now has HA integration and it has more effects and customization (for lighting) than esphome ever will. I wish there were a way to marry the two but since microcontrollers are like $2, I don’t lose too much sleep over having some dedicated just to lighting.
I have been thinking about running two controllers next to each other and establishing a serial link between the two so I can maintain the automation at the edge that esphome enables with the lighting effects that WLED enables and it function even if HA or wifi are down. Haven’t messed with it yet though.

- addressable_lambda:
  name: "Christmas RedGreen (Static)"
  lambda: |-

    for (int i = 1; i <  it.size(); i+=2) {
    it[i] = light::ESPColor(255, 0, 18);
    }
    for (int i = 0; i <  it.size(); i+=2) {
    it[i] = light::ESPColor(0, 179, 44);
    }
  # from reddit user thedoctor___
  # https://www.reddit.com/r/homeassistant/comments/bua3u8/esphome_what_custom_addressable_lambda_effects/


- addressable_lambda:
  name: "Bluez"
  lambda: |-

    for (int i = 0; i <  it.size(); i+=10) {
    it[i] = light::ESPColor(255, 255, 255);
    }

    for (int i = 1; i <  it.size(); i+=10) {
    it[i] = light::ESPColor(255, 255, 255);
    }

    for (int i = 2; i <  it.size(); i+=10) {
    it[i] = light::ESPColor(238, 0, 255);
    }

    for (int i = 3; i <  it.size(); i+=10) {
    it[i] = light::ESPColor(238, 0, 255);
    }

    for (int i = 4; i <  it.size(); i+=10) {
    it[i] = light::ESPColor(255, 157, 0);
    }

    for (int i = 5; i <  it.size(); i+=10) {
    it[i] = light::ESPColor(255, 157, 0);
    }

    for (int i = 6; i <  it.size(); i+=10) {
    it[i] = light::ESPColor(0, 28, 209);
    }

    for (int i = 7; i <  it.size(); i+=10) {
    it[i] = light::ESPColor(0, 28, 209);
    }

    for (int i = 8; i <  it.size(); i+=10) {
    it[i] = light::ESPColor(183, 255, 0);
    }

    for (int i = 9; i <  it.size(); i+=10) {
    it[i] = light::ESPColor(183, 255, 0);
    }
  # from reddit user thedoctor___
  # https://www.reddit.com/r/homeassistant/comments/bua3u8/esphome_what_custom_addressable_lambda_effects/

- addressable_lambda:
  name: "Fire"
  update_interval: 15ms
  lambda: |-
    int Cooling = 55;
    int Sparking = 110;
    static byte heat[188];
    int cooldown;

    // Step 1.  Cool down every cell a little
    for( int i = 0; i < it.size(); i++) {
      cooldown = random(0, ((Cooling * 10) / it.size()) + 2);

      if(cooldown>heat[i]) {
        heat[i]=0;
      } else {
        heat[i]=heat[i]-cooldown;
      }
    }

    // Step 2.  Heat from each cell drifts 'up' and diffuses a little
    for( int k= it.size() - 1; k >= 2; k--) {
      heat[k] = (heat[k - 1] + heat[k - 2] + heat[k - 2]) / 3;
    }

    // Step 3.  Randomly ignite new 'sparks' near the bottom
    if( random(255) < Sparking ) {
      int y = random(7);
      heat[y] = heat[y] + random(160,255);
    }

    // Step 4.  Convert heat to LED colors
    for( int Pixel = 0; Pixel < it.size(); Pixel++) {
      // Scale 'heat' down from 0-255 to 0-191
      byte t192 = round((heat[Pixel]/255.0)*191);

      // calculate ramp up from
      byte heatramp = t192 & 0x3F; // 0..63
      heatramp <<= 2; // scale up to 0..252

      // figure out which third of the spectrum we're in:
      //this is where you can reverse the effect by switching the commented out lines in all 3 places.
      if( t192 > 0x80) {                     // hottest
        //it[it.size() - Pixel - 1] = ESPColor(255, 255, heatramp);
        it[Pixel] = ESPColor(255, 255, heatramp);
      } else if( t192 > 0x40 ) {             // middle
        //it[it.size() - Pixel - 1] = ESPColor(255, heatramp, 0);
        it[Pixel] = ESPColor(255, heatramp, 0);
      } else {                               // coolest
        //it[it.size() - Pixel - 1] = ESPColor(heatramp, 0, 0);
        it[Pixel] = ESPColor(heatramp, 0, 0);
      }
    }

-J

2 Likes

I wrote this quickly last night using @JayElDubya’s Red/Green as a base

      - addressable_lambda:
          name: "Static Rainbow"
          lambda: |-
           for (int i = 1; i < it.size(); i+=7) {
               it[i] = light::ESPColor(148, 0, 211);
            }
           for (int i = 0; i < it.size(); i+=7) {
               it[i] = light::ESPColor(75, 0, 130);
            }
           for (int i = 1; i < it.size(); i+=7) {
               it[i] = light::ESPColor(0, 0, 255);
            }
           for (int i = 0; i < it.size(); i+=7) {
               it[i] = light::ESPColor(0, 255, 0);
            }
           for (int i = 1; i < it.size(); i+=7) {
               it[i] = light::ESPColor(255, 255, 0);
            }
           for (int i = 0; i < it.size(); i+=7) {
               it[i] = light::ESPColor(255, 127, 0);
            }
           for (int i = 0; i < it.size(); i+=7) {
               it[i] = light::ESPColor(255, 0, 0);
            }

It just simply makes a static rainbow on my christmas tree. I use the Rainbow animation and the wife wanted one that didn’t move at all.

Just used a google for rainbow RGB to get the color values but they work well enough for me.

Thanks both, will be giving these a try out this evening :slight_smile:

Createt by @dashdrum and works great:

- addressable_lambda:
    name: Blue Scan
    update_interval: 25ms
    lambda:


      static int step = 0;
      static int direction = 1;

      if(initial_run){
        step = 0;
      }


      it[step] = ESPColor(0,0,255);
      if(step >0 && step < it.size()){
        it[step + (direction * -1)] = ESPColor::BLACK;
      }

      step = step + direction;

      if(step >= it.size() || step < 0){
        direction = direction * -1;
        step = step + (direction * 2);
      }
2 Likes

and this is mine:

      - addressable_lambda:
          name: snowflack
          update_interval: 43ms
          lambda:
    
            static int step = 0;
            
            static int startstepa = 0;
            static int startpositiona = 0;
            static int endpositiona = 0;
            static int directiona = 0;
            
            static int startstepb = 0;
            static int startpositionb = 0;
            static int endpositionb = 0;
            static int directionb = 0;
            
            
            if(initial_run){
              it.all() = ESPColor(0, 0, 0);
              step = 0;
              
              startstepa = 7;
              startpositiona = 79;
              endpositiona = 0;
              directiona = -1;
              
              startstepb = 55;
              startpositionb = 259;
              endpositionb = 180;
              directionb = -1;
            

            }

   
    
            if(step >= startstepa-3+(directiona*3) && step <= startstepa+(endpositiona-startpositiona)*directiona-3+(directiona*3)){
              it[startpositiona+((step-startstepa)*directiona)-3+(directiona*3)] = ESPColor(77, 54, 32);
            }
            if(step >= startstepa-3+(directiona*2) && step <= startstepa+(endpositiona-startpositiona)*directiona-3+(directiona*2)){
              it[startpositiona+((step-startstepa)*directiona)-3+(directiona*2)] = ESPColor(255, 181, 108);
            }
            if(step >= startstepa-3+(directiona*1) && step <= startstepa+(endpositiona-startpositiona)*directiona-3+(directiona*1)){
              it[startpositiona+((step-startstepa)*directiona)-3+(directiona*1)] = ESPColor(179, 127, 76);
            }
            if(step >= startstepa-3 && step <= startstepa+(endpositiona-startpositiona)*directiona-3){
              it[startpositiona+((step-startstepa)*directiona)-3] = ESPColor(77, 54, 32);
            }
            if(step >= startstepa-3-(directiona*1) && step <= startstepa+(endpositiona-startpositiona)*directiona-3-(directiona*1)){
              it[startpositiona+((step-startstepa)*directiona)-3-(directiona*1)] = ESPColor(51, 36, 22);
            }
            if(step >= startstepa-3-(directiona*2) && step <= startstepa+(endpositiona-startpositiona)*directiona-3-(directiona*2)){
              it[startpositiona+((step-startstepa)*directiona)-3-(directiona*2)] = ESPColor(26, 18, 11);
            }
            if(step >= startstepa-3-(directiona*3) && step <= startstepa+(endpositiona-startpositiona)*directiona-3-(directiona*3)){
              it[startpositiona+((step-startstepa)*directiona)-3-(directiona*3)] = ESPColor(0, 0, 0);
            }



            if(step >= startstepb-3+(directionb*3) && step <= startstepb+(endpositionb-startpositionb)*directionb-3+(directionb*3)){
              it[startpositionb+((step-startstepb)*directionb)-3+(directionb*3)] = ESPColor(77, 54, 32);
            }
            if(step >= startstepb-3+(directionb*2) && step <= startstepb+(endpositionb-startpositionb)*directionb-3+(directionb*2)){
              it[startpositionb+((step-startstepb)*directionb)-3+(directionb*2)] = ESPColor(255, 181, 108);
            }
            if(step >= startstepb-3+(directionb*1) && step <= startstepb+(endpositionb-startpositionb)*directionb-3+(directionb*1)){
              it[startpositionb+((step-startstepb)*directionb)-3+(directionb*1)] = ESPColor(179, 127, 76);
            }
            if(step >= startstepb-3 && step <= startstepb+(endpositionb-startpositionb)*directionb-3){
              it[startpositionb+((step-startstepb)*directionb)-3] = ESPColor(77, 54, 32);
            }
            if(step >= startstepb-3-(directionb*1) && step <= startstepb+(endpositionb-startpositionb)*directionb-3-(directionb*1)){
              it[startpositionb+((step-startstepb)*directionb)-3-(directionb*1)] = ESPColor(51, 36, 22);
            }
            if(step >= startstepb-3-(directionb*2) && step <= startstepb+(endpositionb-startpositionb)*directionb-3-(directionb*2)){
              it[startpositionb+((step-startstepb)*directionb)-3-(directionb*2)] = ESPColor(26, 18, 11);
            }
            if(step >= startstepb-3-(directionb*3) && step <= startstepb+(endpositionb-startpositionb)*directionb-3-(directionb*3)){
              it[startpositionb+((step-startstepb)*directionb)-3-(directionb*3)] = ESPColor(0, 0, 0);
            }


            step = step + 1;

            if(step >= 200 || step < 0){
              step = 0;
            }

You have to edit this section:

startstepa = 7; -> start time in the loop
startpositiona = 79; -> start position
endpositiona = 0; -> end position
directiona = -1; -> direction

just, if you like to scans/flake on the same time:
startstepb = 55;
startpositionb = 259;
endpositionb = 180;
directionb = -1;

if(step >= 200 || step < 0){ -> 200 = loop count
step = 0;
}

This is a simple one, but is a pleasing effect on an Xmas tree:

- addressable_lambda:
    name: Gold Glitter
    update_interval: 18ms
    lambda:
      static int state = 0;

      if (initial_run){
        state = 0;

        it.all() = ESPColor(218,165,32);

        ESP_LOGD("custom", "Gold Glitter");
      } else {

        it.all() = ESPColor(218,165,32);

        if(state==0){
          int i = rand() % it.size();
          it[i] = ESPColor::WHITE;
          state += 1;
        } else {
          state += 1;
          state = state % 10;
        }
      }
1 Like

Hey there, even tho its a bit late and not christmas related I thought might share this :grinning:. It’s what imo is an improved rainbow effect.

lambda: |-
            uint8_t led_change = 24; //(higher is more change) the difference in hue for each led down the strip
            float speed = 7; //(lower is faster) the speed the first led colour changes at (therefore affecting all)
            
            if (initial_run) {
              it.all() = Color(0, 0, 0);
            }
            
            unsigned long time = millis() / speed;
            int repetitions = time / 1529;
            uint16_t hue = time - (1529 * repetitions);
            
            for (int i = 0; i < it.size(); i++) {
              if (hue >= 0 && hue < 255) {
                uint8_t green = hue;
                it[i] = Color(255, green, 0);
              } else if (hue >= 255 && hue < 510) {
                uint8_t red = hue - 255;
                it[i] = Color((255 - red), 255, 0);
              } else if (hue >= 510 && hue < 765) {
                uint8_t blue = hue - 510;
                it[i] = Color(0, 255, blue);
              } else if (hue >= 765 && hue < 1020) {
                uint8_t green = hue - 765;
                it[i] = Color(0, (255 - green), 255);
              } else if (hue >= 1020 && hue < 1275) {
                uint8_t red = hue - 1020;
                it[i] = Color(red, 0, 255);
              } else if (hue >= 1275 && hue < 1530) {
                uint8_t blue = hue - 1275;
                it[i] = Color(255, 0, (255 - blue));
              }
              hue+=led_change;
              if (hue >= 1530) {
                hue-=1530;
              }
            }

Changing the variable on the lines with comments will change aspects of the rainbow.

The default rainbow effect I found to be too dull in the mixed led colours and only bright on the single led colours. Imo this better showcases the mixed colours like yellow, cyan and pink. It’s also brighter which is nice.

1 Like

Hey guys,

Trying to add the christmas lights posted above but getting YAML Syntax errors. Can someone advise whats wrong?

This is just my full effects list for easy readability.

    effects:
       - lambda:
           name: Breathing Red
           update_interval: 16s #Finetune to your liking with the transition lenght below
           lambda: |-
            #define Color1 1.0, 0.0, 0.0 //These are the colors defined, feel free to change or extend the list
                                         //if you extend the list, dont forget to add them in the switch loop below
                                         //and remember to adjust the reset counter at the bottom
            static int state = 0;
            static int color = 1;
            auto call = id(printer_lamp).turn_on(); //put the id for your light in here
            call.set_transition_length(15000);
            if (state == 0) 
            {
             call.set_brightness(0.01);
             
            }
             else if (state == 1)
            {
              call.set_rgb(Color1); 
              call.set_brightness(1.0);
            }
             
             state ++;
             if (state == 2){
             state = 0;
             }
             call.perform();
        
        - addressable_lambda:
           name: "Christmas RedGreen (Static)"
           lambda: |-

             for (int i = 1; i <  it.size(); i+=2) {
             it[i] = light::ESPColor(255, 0, 18);
             }
             for (int i = 0; i <  it.size(); i+=2) {
             it[i] = light::ESPColor(0, 179, 44);

Hi all,

I have converted a really great looking fastled based fireplace effect to lambda:

I’m using it with a home made 21*15 addressable led matrix made of a WS2811 addressable led strip controlled by an ESP32 WROOM. The matrix is installed inside an old cole stove in the living room and I’m very satisfied with the results.

My C++ knowledge is very rusty (basing myself on what I recall of my C++ courses in school 20 years ago), but I have attempted to rewrite it without the need of additional helpers and using a single function.

Configurable through te relevant consts:

  • Matrix rows and columns
  • Amount of flares and intensity
  • Colors

It goes without saying all credits go to the original author:

light:
  - platform: fastled_clockless
    chipset: WS2811
    pin: GPIO13
    num_leds: 315
    rgb_order: GRB
    id: ${ha_id}
    name: ${ha_name}
    effects:
      - addressable_lambda:
          name: Fire
          update_interval: 70ms
          lambda: |-
            const bool colmajor = false;
            const bool mattop = true;
            const bool matleft = true;
            const bool zigzag = true;
            const uint16_t rows = 15;
            const uint16_t cols = 21;
            const uint16_t offsetx = 0;
            const uint16_t offsety = 0;
            const uint8_t maxflare = 3;
            const uint8_t flarerows = 7;
            const uint8_t flarechance = 30;
            const uint8_t flaredecay = 14;
            const uint32_t colors[] = {0x000000,0x100000,0x300000,0x600000,0x800000,0xA00000,0xC02000,0xC04000,0xC06000,0xC08000,0x807080};
            const uint8_t NCOLORS = (sizeof(colors)/sizeof(colors[0]));

            static uint8_t nflare = 0;
            static uint32_t flare[maxflare];
            static uint8_t pix[rows][cols];
            static bool needsinit = true;
            static long t = 0;
            
            uint16_t b, d, i, j, k, l, n, x, y, z;
            uint16_t phy_w = cols;
            uint16_t phy_h = rows;
            uint16_t phy_x = 0;
            uint16_t phy_y = 0;
            
            
            if ( needsinit == true ) {
              needsinit = false;
              for ( i=0; i<rows; ++i ) {
                for ( j=0; j<cols; ++j ) {
                  if ( i == 0 ) pix[i][j] = NCOLORS - 1;
                  else pix[i][j] = 0;
                }
              }
            }
            
            // First, move all existing heat points up the display and fade
            for ( i=rows-1; i>0; --i ) {
              for ( j=0; j<cols; ++j ) {
                uint8_t n = 0;
                if ( pix[i-1][j] > 0 )
                  n = pix[i-1][j] - 1;
                pix[i][j] = n;
              }
            }
          
            // Heat the bottom row
            for ( j=0; j<cols; ++j ) {
              i = pix[0][j];
              if ( i > 0 ) {
                pix[0][j] = random(NCOLORS-6, NCOLORS-2);
              }
            }

            // Update existing flares
            for ( i=0; i<nflare; ++i ) {
              x = flare[i] & 0xff;
              y = (flare[i] >> 8) & 0xff;
              z = (flare[i] >> 16) & 0xff;
              b = z * 10 / flaredecay + 1;
              for ( k=(y-b); k<(y+b); ++k ) {
                for ( int l=(x-b); l<(x+b); ++l ) {
                  if ( k >=0 && l >= 0 && k < rows && l < cols ) {
                    d = ( flaredecay * sqrt16((x-l)*(x-l) + (y-k)*(y-k)) + 5 ) / 10;
                    n = 0;
                    if ( z > d ) n = z - d;
                    if ( n > pix[k][l] ) { // can only get brighter
                      pix[k][l] = n;
                    }
                  }
                }
              }
              if ( z > 1 ) {
                flare[i] = (flare[i] & 0xffff) | ((z-1)<<16);
              } else {
                // This flare is out
                for ( j=i+1; j<nflare; ++j ) {
                  flare[j-1] = flare[j];
                }
                --nflare;
              }
            }
            // New Flare
            if ( nflare < maxflare && random(1,101) <= flarechance ) {
              x = random(0, cols);
              y = random(0, flarerows);
              z = NCOLORS - 1;
              b = z * 10 / flaredecay + 1;
              flare[nflare++] = (z<<16) | (y<<8) | (x&0xff);
              for ( k=(y-b); k<(y+b); ++k ) {
                for ( int l=(x-b); l<(x+b); ++l ) {
                  if ( k >=0 && l >= 0 && k < rows && l < cols ) {
                    d = ( flaredecay * sqrt16((x-l)*(x-l) + (y-k)*(y-k)) + 5 ) / 10;
                    n = 0;
                    if ( z > d ) n = z - d;
                    if ( n > pix[k][l] ) { // can only get brighter
                      pix[k][l] = n;
                    }
                  }
                }
              }
            }
            // Draw
            if ( colmajor == true ) {
              phy_w = rows;
              phy_h = cols;
            }
            for ( uint16_t row=0; row<rows; ++row ) {
              for ( uint16_t col=0; col<cols; ++col ) {
                if ( colmajor == true ) {
                    phy_x = offsetx + (uint16_t) row;
                    phy_y = offsety + (uint16_t) col;
                } else {
                    phy_x = offsetx + (uint16_t) col;
                    phy_y = offsety + (uint16_t) row;
                }
                if ( matleft == true && zigzag == true ) {
                  if ( ( phy_y & 1 ) == 1 ) {
                    phy_x = phy_w - phy_x - 1;
                  }
                } else if ( matleft == false && zigzag == true ) {
                  if ( ( phy_y & 1 ) == 0 ) {
                    phy_x = phy_w - phy_x - 1;
                  }
                } else if ( matleft == false ) {
                  phy_x = phy_w - phy_x - 1;
                }
                if ( mattop == true && colmajor == true ) {
                  phy_x = phy_w - phy_x - 1;
                } else if (mattop) {
                  phy_y = phy_h - phy_y - 1;
                }
                it[phy_x + phy_y * phy_w] = ESPColor(colors[pix[row][col]]);
              }
            }
11 Likes

I know this thread is a bit older, but I was wondering if anyone had a nice list of effects that could be used with old fashioned Analogue RGB strips. I looked at the ones here, and they are all for addressable LED’s.
I have a couple of RGBW analogue strips I’d like to make some effects for

3 Likes

hello i would like to create a light scene on esphome with neopixelbus like this:

Someone can help me please?

I have tried converting the StormCloud effect to adressable_lambda code. This is my take on it:

      - addressable_lambda:
          name: "Lightning Animation"
          update_interval: 5ms

          lambda: |-
            static float speed_multiplier = 0.003;
            static uint32_t lightning_end = 0;
            static uint32_t lightning_wait = 0;
            static uint32_t lightning_modifier = 0;
            for(uint8_t led_index = 0; led_index < it.size(); led_index++) {
              uint32_t elapsed = millis() + (float(led_index) * 650);  // MOVEMENT_MODIFIER

              // Calculate blue color component
              uint8_t b = uint8_t((pow(sin((elapsed * speed_multiplier) / 4.0), 3.0) + 1.0) * 80.0 + 90.0);

              uint8_t r, g;

              // Check if lightning is active
              if (lightning_end == 0) {
                // No lightning - normal animation
                r = uint8_t((sin((elapsed * speed_multiplier) / 5.0) + 1.0) * 40.0);
                g = uint8_t(pow(sin(((elapsed * speed_multiplier) / 8.0) + PI / 2.0), 4.0) * 50.0);

                // Lightning initiation condition
                if (millis() >= lightning_wait && random_uint32() % 1500 == 1) {
                  lightning_end = millis() + (rand() % (500 - 100) + 100);  // Random duration between 100 and 500ms
                  lightning_modifier = rand() % (35 - 20) + 20;             // Random speed modifier between 20 and 35
                }
              } else {
                // Lightning is active - brighter white-ish light
                r = g = uint8_t((sin(elapsed / lightning_modifier) + 1.0) * 60);

                if (millis() >= lightning_end) {
                  lightning_end = 0;
                  lightning_wait = millis() + 3000;  // Lightning cooldown
                }
              }

              // Set the LED color
              it[led_index] = esphome::Color(r, g, b);
            }

The original code goes over each pixel one-by-one while I do it in a for-loop. If I do it one-by-one within the lambda function the update is too slow.
By doing so, I think I changed the lightning effect to work a bit differently. Still, it looks impressing and should work.

Having said that, can anyone help me converting the Pacifica-code from FastLED to ESPHome Adressable Lambda?

1 Like

Here are a couple of Christmas themed effects. These are done with GPT in Cursor. ā€œRed-Green Fireā€ is a rewrite of ā€œFireā€ in this topic, thank you @JayElDubya ! Enjoy!

- addressable_lambda:
          name: "Red-Green Rainbow"
          update_interval: 50ms
          lambda: |-
            static float phase = 0.0;
            const float phase_increment = 0.1;

            for (int i = 0; i < it.size(); i++) {
              float position = (float)i / it.size();
              float red_intensity = (sin(position * 2.0 * 3.14159 + phase) + 1.0) / 2.0;
              float green_intensity = (sin(position * 2.0 * 3.14159 + phase + 3.14159) + 1.0) / 2.0;

              it[i] = esphome::Color(
                (int)(red_intensity * 255),
                (int)(green_intensity * 255),
                0
              );
            }

            phase += phase_increment;
            if (phase >= 2.0 * 3.14159) {
              phase -= 2.0 * 3.14159;
            }
- addressable_lambda:
          name: "Red-Green Twinkle"
          update_interval: 100ms
          lambda: |-
            for (int i = 0; i < it.size(); i++) {
              if (rand() % 10 == 0) {  // Randomly select some LEDs to twinkle
                auto current_color = it[i].get();
                if (current_color.red == 255 && current_color.green == 0 && current_color.blue == 0) {
                  it[i] = esphome::Color(0, 255, 0);  // Change from red to green
                } else {
                  it[i] = esphome::Color(255, 0, 0);  // Change from green to red
                }
              }
            }
- addressable_lambda:
          name: "Red-Green Fire"
          update_interval: 15ms
          lambda: |-
            int Cooling = 55;
            int Sparking = 110;
            static byte heat[188];
            int cooldown;

            // Step 1.  Cool down every cell a little
            for (int i = 0; i < it.size(); i++) {
              cooldown = random(0, ((Cooling * 10) / it.size()) + 2);

              if (cooldown > heat[i]) {
                heat[i] = 0;
              } else {
                heat[i] = heat[i] - cooldown;
              }
            }

            // Step 2.  Heat from each cell drifts 'up' and diffuses a little
            for (int k = it.size() - 1; k >= 2; k--) {
              heat[k] = (heat[k - 1] + heat[k - 2] + heat[k - 2]) / 3;
            }

            // Step 3.  Randomly ignite new 'sparks' near the bottom
            if (random(255) < Sparking) {
              int y = random(7);
              heat[y] = heat[y] + random(160, 255);
            }

            // Step 4.  Convert heat to LED colors with slightly more red in the hottest section
            for (int Pixel = 0; Pixel < it.size(); Pixel++) {
              // Scale 'heat' down from 0-255 to 0-191
              byte t192 = round((heat[Pixel] / 255.0) * 191);

              // calculate ramp up from
              byte heatramp = t192 & 0x3F; // 0..63
              heatramp <<= 2; // scale up to 0..252

              // figure out which third of the spectrum we're in:
              if (t192 > 0x80) { // hottest
                it[Pixel] = esphome::Color(255, heatramp * 0.9, 0); // Slightly more red
              } else if (t192 > 0x40) { // middle
                it[Pixel] = esphome::Color(heatramp, 255, 0);
              } else { // coolest
                it[Pixel] = esphome::Color(0, heatramp, 0);
              }
            }
1 Like

I took the first two effects @Gitstro posted above and added a third that cycles between them. I also added controls to customize their behavior (speed, intervals).

See GitHub - jgruen/ESPHome-custom-addressable-lambdas: custom LED lighting effects for ESPHome.

Using packages and substitutions, configuration is a good bit simpler:

packages:
  custom_addressable_lambdas: !include custom_addressable_lambdas.include.yaml

# under your light...
light:
  - platform: neopixelbus  # or whatever you are using
    # ...
    effects:
      # other effects here
      - addressable_lambda:
          name: "Red-Green Rainbow"
          update_interval: ${addressable_lambda_update_interval_ms}ms
          lambda: |-
            red_green_rainbow(it, current_color, initial_run);
      - addressable_lambda:
          name: "Red-Green Twinkle"
          update_interval: ${addressable_lambda_update_interval_ms}ms
          lambda: |-
            red_green_twinkle(it, current_color, initial_run);
      - addressable_lambda:
          name: "Red-Green Rainbow/Twinkle Cycle"
          update_interval: ${addressable_lambda_update_interval_ms}ms
          lambda: |-
            red_green_rainbow_twinkle_cycle(it, current_color, initial_run);

See the ESPHome-custom-addressable-lambdas/README.md at main Ā· jgruen/ESPHome-custom-addressable-lambdas Ā· GitHub for more details…!

1 Like

Great update on these!

I made a new firework effect with the help of copilot. It has 3 different explosion effects that are picked randomly. It tested this on a 800led strip around my Christmas tree. Looks quite good. Can be probably optimized a lot by someone who knows how to actually code

      - addressable_lambda:
          name: "Firework Effect"
          lambda: |-
            // State machine variables
            static int state = 0; // 0: idle, 1: launching, 2: old explosion, 3: new dynamic explosion, 4: sparkly glitter effect
            static float position_f = 0; // Floating point position for smoother control
            static int direction = 1; // 1: left to right, -1: right to left
            static int peak = 0; // Random peak position
            static float speed = 0;
            static float move_speed = 10.0; //  speed of the firework
            static int explosion_step = 0;

            // Fade all LEDs slightly
            for (int i = 0; i < it.size(); i++) {
              auto color = it[i].get();
              color.red = color.red * 3 / 5;   // Reduce brightness to 60%
              color.green = color.green * 3 / 5;
              color.blue = color.blue * 3 / 5;
              it[i] = color;
            }

            // State machine logic
            switch (state) {
              case 0: // Idle, prepare to launch
                if (rand() % 20 == 0) { // Random chance to start a new firework
                  direction = (rand() % 2 == 0) ? 1 : -1;
                  position_f = (direction == 1) ? 0 : (it.size() - 1);
                  peak = rand() % (it.size() / 2) + it.size() / 4; // Random peak between 25% and 75% of the strip
                  speed = move_speed; 
                  state = 1;
                }
                break;

              case 1: // Launching
                {
                  int start = static_cast<int>(position_f);
                  position_f += direction * speed; // Update position
                  int end = static_cast<int>(position_f);

                  // Set the firework's trail
                  if (start > end) std::swap(start, end);
                  for (int i = start; i <= end; i++) {
                    if (i >= 0 && i < it.size()) {
                      it[i] = esphome::Color(128, 128, 128); // Less bright trail
                    }
                  }

                  // Slow down as it approaches the peak
                  if ((direction == 1 && position_f >= peak) || (direction == -1 && position_f <= peak)) {
                    speed *= 0.8; // Gradually reduce speed
                    if (speed < 0.1) { // If the firework is nearly stopped
                      // Randomly pick either old explosion, new dynamic explosion, or sparkly glitter effect
                      state = (rand() % 3 == 0) ? 2 : (rand() % 2 == 0) ? 3 : 4;
                      explosion_step = 0;
                    }
                  }
                }
                break;

              case 2: // New explosion effect
                {
                  static bool explosion_initialized2 = false;
                  static esphome::Color colors[50];
                  static int offsets[50];
                  static int total_particles = 50;
                  float fade = 1.0f - (explosion_step * 0.02f); // Faster fade rate
                  if (fade < 0.0f) fade = 0.0f;

                  // Initialize particles once per explosion
                  if (!explosion_initialized2) {
                    // Create an array of random colors
                    for (int i = 0; i < total_particles; i++) {
                      colors[i] = esphome::Color(rand() % 255, rand() % 255, rand() % 255);
                      offsets[i] = 0;
                    }
                    explosion_initialized2 = true;

                    // Create a bright white flash
                    for (int i = -5; i <= 5; i++) {
                      int index = static_cast<int>(position_f) + i;
                      if (index >= 0 && index < it.size()) {
                        it[index] = esphome::Color::WHITE;
                      }
                    }
                  } else {
                    int center = static_cast<int>(position_f);
                    // Update and display each particle
                    for (int i = 0; i < total_particles; i++) {
                      for (int j = -1; j <= 1; j++) { // Each particle is 3 LEDs wide
                        int pos_left = center - offsets[i] + j;
                        int pos_right = center + offsets[i] + j;
                        if (pos_left >= 0 && pos_left < it.size()) {
                          // Set the color from the array
                          auto c = colors[i];
                          // Scale color by fade
                          c.red   = (uint8_t)(c.red   * fade);
                          c.green = (uint8_t)(c.green * fade);
                          c.blue  = (uint8_t)(c.blue  * fade);

                          it[pos_left] = c;
                        }
                        if (pos_right >= 0 && pos_right < it.size()) {
                          // Set the color from the array
                          auto c = colors[i];
                          // Scale color by fade
                          c.red   = (uint8_t)(c.red   * fade);
                          c.green = (uint8_t)(c.green * fade);
                          c.blue  = (uint8_t)(c.blue  * fade);

                          it[pos_right] = c;
                        }
                      }
                      // Move outward faster
                      offsets[i] += 3; // Faster propagation
                    }

                    // Create gaps between particles and make particles disappear
                    for (int i = 0; i < total_particles; i++) {
                      for (int j = -2; j <= 2; j++) { // Each particle is 5 LEDs wide
                        int pos_left = center - offsets[i] + j;
                        int pos_right = center + offsets[i] + j;
                        if (pos_left >= 0 && pos_left < it.size()) {
                          if (rand() % 2 == 0) {
                            it[pos_left] = esphome::Color(0, 0, 0); // Turn off some LEDs to create gaps
                          }
                        }
                        if (pos_right >= 0 && pos_right < it.size()) {
                          if (rand() % 2 == 0) {
                            it[pos_right] = esphome::Color(0, 0, 0); // Turn off some LEDs to create gaps
                          }
                        }
                      }
                    }
                  }

                  explosion_step++;
                  // End explosion if all particles are out of range or enough steps pass
                  if (explosion_step > (int)(it.size() / 1.5) || fade <= 0.0f) {
                    state = 0;
                    explosion_initialized2 = false;
                  }
                }
                break;

              case 3: // New dynamic explosion effect
                {
                  static bool explosion_initialized3 = false;
                  static int offsets[30];
                  static int total_particles = 0;
                  float fade = 1.0f - (explosion_step * 0.02f); // Faster fade rate
                  if (fade < 0.0f) fade = 0.0f;

                  // Initialize particles once per explosion
                  if (!explosion_initialized3) {
                    total_particles = 50 + (rand() % 51); // 50..100
                    for (int i = 0; i < total_particles; i++) {
                      // Each offset can be positive or negative
                      offsets[i] = (rand() % 10) * ((rand() % 2) ? 1 : -1);
                    }
                    explosion_initialized3 = true;
                  }

                  int center = static_cast<int>(position_f);
                  // Update and display each particle
                  for (int i = 0; i < total_particles; i++) {
                    int pos = center + offsets[i];
                    if (pos >= 0 && pos < it.size()) {
                      // Calculate a fade factor (higher steps => more fade)
                      auto c = esphome::Color(rand() % 255 + 128, rand() % 255 + 128, rand() % 255 + 128); // Brighter random colors
                      // Scale color by fade
                      c.red   = (uint8_t)(c.red   * fade);
                      c.green = (uint8_t)(c.green * fade);
                      c.blue  = (uint8_t)(c.blue  * fade);

                      // Fluctuate brightness
                      float brightness_factor = 0.5 + 0.5 * sin(explosion_step * 0.1);
                      c.red   = (uint8_t)(c.red   * brightness_factor);
                      c.green = (uint8_t)(c.green * brightness_factor);
                      c.blue  = (uint8_t)(c.blue  * brightness_factor);

                      it[pos] = c;
                    }
                    // Move outward faster
                    if (offsets[i] < 0) offsets[i] -= 2; // Faster propagation
                    else offsets[i] += 2;
                  }

                  explosion_step++;
                  // End explosion if all particles are out of range or enough steps pass
                  if (explosion_step > (int)(it.size() / 2) || fade <= 0.0f) {
                    state = 0;
                    explosion_initialized3 = false;
                  }
                }
                break;

              case 4: // Sparkly glitter effect
                {
                  int center = static_cast<int>(position_f);
                  int num_sparkles = 30 + rand() % 21; // Random number of sparkles between 30 and 50
                  for (int i = 0; i < num_sparkles; i++) {
                    int index = center + (rand() % (2 * explosion_step + 1)) - explosion_step;
                    if (index >= 0 && index < it.size()) { // Ensure index is within bounds
                      it[index] = esphome::Color(rand() % 255, rand() % 255, rand() % 255); // Random colors
                    }
                  }
                  explosion_step++;
                  if (explosion_step > 50) { // Duration of the sparkly glitter effect
                    state = 0; // Return to idle
                  }
                }
                break;
            }

In case anyone’s curious, the new Voice Preview Edition has a couple lambda effects for its addressable LED ring, starting around line 580 of the ESPHome YAML file.

1 Like

I came here looking for light effects but didn’t find what I wanted, so I used an LLM to create some. These are wave related addressable effects, and they dynamically change color based on what the strip is set to using id(light_id).current_values.

Disclaimer: These are generated using AI with my guidance/correction. They all work, but might not be written the greatest.

For all effects, you can change update_interval for speed, x for the width of the wave and y for the spacing between waves. You must update ā€œlight_idā€ with the ID of your light (and it must be defined if it isn’t already).

A simple repeating wave. The wave starts at max brightness and has a fading tail. Has the color that the strip is set to.

      - addressable_lambda:
          name: "Wave"
          update_interval: 40ms
          lambda: |-
            static float wave_position = 0.0;
            const int x = 10;  // Width of the wave peak
            const int y = 20;  // Spacing between wave peaks
            const float wave_speed = 0.5;  // Speed of the wave progression

            wave_position += wave_speed;
            if (wave_position >= (x + y)) {
              wave_position -= (x + y);
            }

            // Fetch the globally set color and use it
            auto call = id(light_id).current_values;
            Color selected_color(call.get_red() * 255, call.get_green() * 255, call.get_blue() * 255);

            for (int i = 0; i < it.size(); i++) {
              // Calculate brightness for the wave
              float position_in_wave = fmod(i + wave_position, x + y);
              float brightness = 0.0;
              if (position_in_wave < x) {
                brightness = 1.0 - (position_in_wave / x);
              }

              // Scale the selected color by the calculated brightness
              uint8_t r = std::min(255, static_cast<int>(selected_color.red * brightness));
              uint8_t g = std::min(255, static_cast<int>(selected_color.green * brightness));
              uint8_t b = std::min(255, static_cast<int>(selected_color.blue * brightness));

              it[i] = Color(r, g, b);
            }

Same as above, but the wave is faded on both ends (like a hump)

      - addressable_lambda
         name: "Wave Fade"
          update_interval: 40ms
          lambda: |-
            static float wave_position = 0.0;
            const int x = 10;  // Width of the wave peak
            const int y = 20;  // Spacing between wave peaks
            const float wave_speed = 0.5;  // Speed of the wave progression

            wave_position += wave_speed;
            if (wave_position >= (x + y)) {
              wave_position -= (x + y);
            }

            // Fetch the globally set color and use it
            auto call = id(light_id).current_values;
            Color selected_color(call.get_red() * 255, call.get_green() * 255, call.get_blue() * 255);

            for (int i = 0; i < it.size(); i++) {
              // Calculate brightness for the wave
              float position_in_wave = fmod(i + wave_position, x + y);
              float brightness = 0.0;
              if (position_in_wave < x) {
                brightness = (1.0 - fabs((position_in_wave / x) * 2.0 - 1.0)); // Linear fade in and out
              }

              // Scale the selected color by the calculated brightness
              uint8_t r = std::min(255, static_cast<int>(selected_color.red * brightness));
              uint8_t g = std::min(255, static_cast<int>(selected_color.green * brightness));
              uint8_t b = std::min(255, static_cast<int>(selected_color.blue * brightness));

              it[i] = Color(r, g, b);
            }

Similar to the first, but there is a second wave traveling of the opposite direction. The color of the second wave is the compliment (180 degrees hue rotated) of the first, and when they collide, the colors are added.

      - addressable_lambda:
          name: "Wave Comp"
          update_interval: 75ms
          lambda: |-
            static float wave_position = 0.0;
            const int x = 10;  // Width of the wave peak
            const int y = 20;  // Spacing between wave peaks
            const float wave_speed = 0.5;  // Speed of the wave progression

            wave_position += wave_speed;
            if (wave_position >= (x + y)) {
              wave_position -= (x + y);
            }

            // Fetch the globally set color and use it
            auto call = id(light_id).current_values;
            Color rgb1(call.get_red() * 255, call.get_green() * 255, call.get_blue() * 255);

            // Calculate the complementary color
            Color rgb2(255 - rgb1.red, 255 - rgb1.green, 255 - rgb1.blue);

            wave_position += wave_speed;
            if (wave_position >= (x + y)) {
              wave_position -= (x + y);
            }

            for (int i = 0; i < it.size(); i++) {
              // Calculate brightness for the forward wave
              float forward_position_in_wave = fmod(i + wave_position, x + y);
              float forward_brightness = 0.0;
              if (forward_position_in_wave < x) {
                forward_brightness = 1.0 - (forward_position_in_wave / x);
              }

              // Calculate brightness for the backward wave
              float backward_position_in_wave = fmod(it.size() - 1 - i + wave_position, x + y);
              float backward_brightness = 0.0;
              if (backward_position_in_wave < x) {
                backward_brightness = 1.0 - (backward_position_in_wave / x);
              }

              // Add the two colors and clamp the result
              uint8_t r = std::min(255, static_cast<int>((rgb1.red   * forward_brightness) + (rgb2.red   * backward_brightness)));
              uint8_t g = std::min(255, static_cast<int>((rgb1.green * forward_brightness) + (rgb2.green * backward_brightness)));
              uint8_t b = std::min(255, static_cast<int>((rgb1.blue  * forward_brightness) + (rgb2.blue  * backward_brightness)));

              it[i] = Color(r, g, b);
            }

Same as above but the waves are faded on both ends (like humps)

      - addressable_lambda:
          name: "Wave Comp Fade"
          update_interval: 75ms
          lambda: |-
            static float wave_position = 0.0;
            const int x = 10;  // Width of the wave peak
            const int y = 20;  // Spacing between wave peaks
            const float wave_speed = 0.5;  // Speed of the wave progression

            wave_position += wave_speed;
            if (wave_position >= (x + y)) {
              wave_position -= (x + y);
            }

            // Fetch the globally set color and use it
            auto call = id(light_id).current_values;
            Color rgb1(call.get_red() * 255, call.get_green() * 255, call.get_blue() * 255);

            // Calculate the complementary color
            Color rgb2(255 - rgb1.red, 255 - rgb1.green, 255 - rgb1.blue);

            for (int i = 0; i < it.size(); i++) {
              // Calculate brightness for the forward wave
              float forward_position_in_wave = fmod(i + wave_position, x + y);
              float forward_brightness = 0.0;
              if (forward_position_in_wave < x) {
                forward_brightness = (1.0 - fabs((forward_position_in_wave / x) * 2.0 - 1.0)); // Linear fade in/out
              }

              // Calculate brightness for the backward wave
              float backward_position_in_wave = fmod(it.size() - 1 - i + wave_position, x + y);
              float backward_brightness = 0.0;
              if (backward_position_in_wave < x) {
                backward_brightness = (1.0 - fabs((backward_position_in_wave / x) * 2.0 - 1.0)); // Linear fade in/out
              }

              // Add the two colors and clamp the result
              uint8_t r = std::min(255, static_cast<int>((rgb1.red   * forward_brightness) + (rgb2.red   * backward_brightness)));
              uint8_t g = std::min(255, static_cast<int>((rgb1.green * forward_brightness) + (rgb2.green * backward_brightness)));
              uint8_t b = std::min(255, static_cast<int>((rgb1.blue  * forward_brightness) + (rgb2.blue  * backward_brightness)));

              it[i] = Color(r, g, b);
            }

Same as the above two, but the second color is 120 degrees rotated. (Or you can change the number in the code too).

      - addressable_lambda:
          name: "Wave Rotate"
          update_interval: 75ms
          lambda: |-
            static float wave_position = 0.0;
            const int x = 10;  // Width of the wave peak
            const int y = 20;  // Spacing between wave peaks
            const float wave_speed = 0.5;  // Speed of the wave progression

            wave_position += wave_speed;
            if (wave_position >= (x + y)) {
              wave_position -= (x + y);
            }

            // Fetch the globally set color and use it
            auto call = id(fairy).current_values;
            Color rgb1(call.get_red() * 255, call.get_green() * 255, call.get_blue() * 255);

            // Manually rotate the hue by 120 degrees
            auto rotate_hue = [](Color color, float angle) -> Color {
              float r = color.red / 255.0;
              float g = color.green / 255.0;
              float b = color.blue / 255.0;

              // Convert RGB to the maximum of R, G, B
              float max_val = std::max({r, g, b});
              float min_val = std::min({r, g, b});
              float delta = max_val - min_val;

              float hue = 0.0;
              if (delta > 0.0) {
                if (max_val == r) {
                  hue = (g - b) / delta;
                } else if (max_val == g) {
                  hue = 2.0 + (b - r) / delta;
                } else {
                  hue = 4.0 + (r - g) / delta;
                }
                hue /= 6.0;
                if (hue < 0) hue += 1.0;
              }

              // Rotate the hue by the specified angle
              hue = fmod(hue + angle / 360.0, 1.0);

              // Convert back to RGB
              float p, q, t, h1, r_out, g_out, b_out;
              if (delta == 0) {
                r_out = g_out = b_out = max_val;
              } else {
                h1 = hue * 6;
                p = max_val * (1.0 - delta);
                q = max_val * (1.0 - delta * h1);
                t = max_val * (1.0 - delta * (1.0 - h1));

                if (h1 < 1) {
                  r_out = max_val;
                  g_out = t;
                  b_out = p;
                } else if (h1 < 2) {
                  r_out = q;
                  g_out = max_val;
                  b_out = p;
                } else if (h1 < 3) {
                  r_out = p;
                  g_out = max_val;
                  b_out = t;
                } else if (h1 < 4) {
                  r_out = p;
                  g_out = q;
                  b_out = max_val;
                } else if (h1 < 5) {
                  r_out = t;
                  g_out = p;
                  b_out = max_val;
                } else {
                  r_out = max_val;
                  g_out = p;
                  b_out = q;
                }
              }
              return Color(r_out * 255, g_out * 255, b_out * 255);
            };

            // Rotate the selected color by 120 degrees
            Color rgb2 = rotate_hue(rgb1, 120.0);

            wave_position += wave_speed;
            if (wave_position >= (x + y)) {
              wave_position -= (x + y);
            }

            for (int i = 0; i < it.size(); i++) {
              // Calculate brightness for the forward wave
              float forward_position_in_wave = fmod(i + wave_position, x + y);
              float forward_brightness = 0.0;
              if (forward_position_in_wave < x) {
                forward_brightness = 1.0 - (forward_position_in_wave / x);
              }

              // Calculate brightness for the backward wave
              float backward_position_in_wave = fmod(it.size() - 1 - i + wave_position, x + y);
              float backward_brightness = 0.0;
              if (backward_position_in_wave < x) {
                backward_brightness = 1.0 - (backward_position_in_wave / x);
              }

              // Add the two colors and clamp the result
              uint8_t r = std::min(255, static_cast<int>((rgb1.red   * forward_brightness) + (rgb2.red   * backward_brightness)));
              uint8_t g = std::min(255, static_cast<int>((rgb1.green * forward_brightness) + (rgb2.green * backward_brightness)));
              uint8_t b = std::min(255, static_cast<int>((rgb1.blue  * forward_brightness) + (rgb2.blue  * backward_brightness)));

              it[i] = Color(r, g, b);
            }

Same as above not the waves are faded on both sides (like humps)

      - addressable_lambda:
          name: "Wave Rotate Fade"
          update_interval: 75ms
          lambda: |-
            static float wave_position = 0.0;
            const int x = 10;  // Width of the wave peak
            const int y = 20;  // Spacing between wave peaks
            const float wave_speed = 0.5;  // Speed of the wave progression

            wave_position += wave_speed;
            if (wave_position >= (x + y)) {
              wave_position -= (x + y);
            }

            // Fetch the globally set color and use it
            auto call = id(fairy).current_values;
            Color rgb1(call.get_red() * 255, call.get_green() * 255, call.get_blue() * 255);

            // Manually rotate the hue by 120 degrees
            auto rotate_hue = [](Color color, float angle) -> Color {
              float r = color.red / 255.0;
              float g = color.green / 255.0;
              float b = color.blue / 255.0;

              // Convert RGB to the maximum of R, G, B
              float max_val = std::max({r, g, b});
              float min_val = std::min({r, g, b});
              float delta = max_val - min_val;

              float hue = 0.0;
              if (delta > 0.0) {
                if (max_val == r) {
                  hue = (g - b) / delta;
                } else if (max_val == g) {
                  hue = 2.0 + (b - r) / delta;
                } else {
                  hue = 4.0 + (r - g) / delta;
                }
                hue /= 6.0;
                if (hue < 0) hue += 1.0;
              }

              // Rotate the hue by the specified angle
              hue = fmod(hue + angle / 360.0, 1.0);

              // Convert back to RGB
              float p, q, t, h1, r_out, g_out, b_out;
              if (delta == 0) {
                r_out = g_out = b_out = max_val;
              } else {
                h1 = hue * 6;
                p = max_val * (1.0 - delta);
                q = max_val * (1.0 - delta * h1);
                t = max_val * (1.0 - delta * (1.0 - h1));

                if (h1 < 1) {
                  r_out = max_val;
                  g_out = t;
                  b_out = p;
                } else if (h1 < 2) {
                  r_out = q;
                  g_out = max_val;
                  b_out = p;
                } else if (h1 < 3) {
                  r_out = p;
                  g_out = max_val;
                  b_out = t;
                } else if (h1 < 4) {
                  r_out = p;
                  g_out = q;
                  b_out = max_val;
                } else if (h1 < 5) {
                  r_out = t;
                  g_out = p;
                  b_out = max_val;
                } else {
                  r_out = max_val;
                  g_out = p;
                  b_out = q;
                }
              }
              return Color(r_out * 255, g_out * 255, b_out * 255);
            };

            // Rotate the selected color by 120 degrees
            Color rgb2 = rotate_hue(rgb1, 120.0);

            wave_position += wave_speed;
            if (wave_position >= (x + y)) {
              wave_position -= (x + y);
            }

            for (int i = 0; i < it.size(); i++) {
              // Calculate brightness for the forward wave
              float forward_position_in_wave = fmod(i + wave_position, x + y);
              float forward_brightness = 0.0;
              if (forward_position_in_wave < x) {
                forward_brightness = (1.0 - fabs((forward_position_in_wave / x) * 2.0 - 1.0)); // Linear fade in/out
              }

              // Calculate brightness for the backward wave
              float backward_position_in_wave = fmod(it.size() - 1 - i + wave_position, x + y);
              float backward_brightness = 0.0;
              if (backward_position_in_wave < x) {
                backward_brightness = (1.0 - fabs((backward_position_in_wave / x) * 2.0 - 1.0)); // Linear fade in/out
              }

              // Add the two colors and clamp the result
              uint8_t r = std::min(255, static_cast<int>((rgb1.red   * forward_brightness) + (rgb2.red   * backward_brightness)));
              uint8_t g = std::min(255, static_cast<int>((rgb1.green * forward_brightness) + (rgb2.green * backward_brightness)));
              uint8_t b = std::min(255, static_cast<int>((rgb1.blue  * forward_brightness) + (rgb2.blue  * backward_brightness)));

              it[i] = Color(r, g, b);
            }