Stretchy Rainbows
When I first installed our Theater LED Lighting, I set it to mostly default to a rainbow effect available on the ESPHome. It gives a pleasing effect that looks like this.
This is described by the following relationship:
\[ h=\frac{n}{N} + \frac{t}{P} \% 1 \]
where
\( h \) is the hue of the color in the HSV color space,
\( n \) is the index of pixel,
\( N \) is the number of pixels,
\( t \) is the time,
\( P \) is the period of the oscillation, and
\( \% \) is the modulo operator (which just makes the term \( \frac{t}{P} \% 1 \) go from 0 to 1 and then wrap around again when the time extends beyond the period)
After a few months though, this effect became less visually interesting. I started trying to figure out how to make this effect a bit more dynamic. The ESP8266 isn't a particularly powerful device, so the first thing worth exploring was a feature that allows streaming pixel sequences to the device. I tried E1.31, but there is a known bug with E1.31 implementation on the ESP8266, so I pivoted to a library implementing DDP for the ESP8266. With this, it's possible to individually control the LEDs on an ESPHome via a network API. I made a rust application and threw it on my server, and got a stretchy rainbow!
The equation is a bit of a mess, but this gives the illusion of unpredictability. It never seems the same twice. The formula is:
\[ \LARGE h=\frac{\cos\Bigg(2\pi\Big((\frac{n}{N}+\frac{\sin(2\pi\frac{t}{P_1})+1}{2}) \% 1\Big)^2\Bigg)+1}{2}+\frac{t}{P_2} \% 1 \]
where
\( h \) is the hue of the color in the HSV color space,
\( n \) is the index of pixel,
\( N \) is the number of pixels,
\( t \) is the time,
\( P_1 \) is the period of the oscillation for the rocking part of the motion,
\( P_2 \) is the period of the oscillation for the forward part of the motion,
\( \% \) is the modulo operator (which, again, is used to make a couple of terms go from 0 to 1 and then wrap again when the period ends)
So, why does this work? There are only a few things going on, so we can break the equation into pieces.
First, there's \( \large \frac{n}{N}+\frac{\sin(2\pi\frac{t}{P_1}+1)}{2} \). The first term, \( \frac{n}{N} \) starts the process by wrapping the entire rainbow around the pixel rectangle, starting at 0 and ending at 1, which, in the HSV space, just wraps back around to 0. The second term, \( \large \frac{\sin(2\pi\frac{t}{P_1})+1}{2} \), offsets this wrap with a sine wave that oscillates between 0 and 1 over time, and causes the wrapped rainbow to rock back and forth. Taking the modulo of this result \( \large \Big(\frac{n}{N}+\frac{\sin(2\pi\frac{t}{P_1}+1)}{2}\Big) \% 1 \) makes sure this value wraps around from 1 back to 0 when the elapsed time goes beyond the period just like the HSV space does.
Now, we want to stretch the rainbow out. To do this, we square the result from the last step and then use it as the argument for a cosine function, which gives:
\[ \frac{\cos\Bigg(2\pi\Big((\frac{n}{N}+\frac{\sin(2\pi\frac{t}{P_1})+1}{2}) \% 1\Big)^2\Bigg)+1}{2} \]
Squaring the value is the key to stretching it such that the transition from 0 to 1 is slower for values closer to 0 and faster for values closer to 1. This is translated into angular space with \( 2\pi \) and this is passed into the cosine function to add additional oscillation and avoid an abrupt change when 1 wraps around to 0. The result is a wave that oscillates between 0 and 1 twice around the perimeter of the rectangle, and its wavelength starts very long and gets progressively shorter.
Lastly, we add the final term from before, \( \frac{t}{P_2} \% 1 \). This not only slowly pushes the wave along the rectangle so that the rocking motion doesn't just rock back and forth between the same two locations, but it also has the effect of slowly changing which color is stretched out.
This effect in combination with a few fade out transitions that run randomly when the theater is put into viewing mode in Home Assistant makes for a fresh feeling every time.

Comments ()