rpi-ws281x / rpi-ws281x-python

Python library wrapping for the rpi-ws281x library
BSD 2-Clause "Simplified" License
319 stars 102 forks source link

Explain how rainbowCycle works? #40

Open emdeex opened 4 years ago

emdeex commented 4 years ago

I'm loving playing with rainbowCycle, and its working fine.... but can anybody explain (for dummies) exactly what's going on with the formulas in the code?

I see it iterates through each neopixel, and uses the wheel function to set the color... but can you breakdown the formula like at strip.setPixelColor(i, wheel((int(i * 256 / strip.numPixels()) + j) & 255))? And how do the wait_ms and iterations variables interact with it?

def rainbowCycle(strip, wait_ms=20, iterations=5):
    """Draw rainbow that uniformly distributes itself across all pixels."""
    for j in range(256 * iterations):
        for i in range(strip.numPixels()):
            strip.setPixelColor(i, wheel(
                (int(i * 256 / strip.numPixels()) + j) & 255))
        strip.show()
        time.sleep(wait_ms / 1000.0)
Gadgetoid commented 3 years ago

This is probably the worst code you could learn from, since it's fairly contrived, ported from what I assume was code originally written for very limited microcontrollers, and thus completely disconnected from how any normal Python code might work.

The majority of noteworthy code lives in the wheel function, which demonstrates a crude method of turning a number from 0-255 into an RGB colour value roughly approximating an imaginary position in a "rainbow".

def wheel(pos):
    """Generate rainbow colors across 0-255 positions."""
    if pos < 85:
        return Color(pos * 3, 255 - pos * 3, 0)
    elif pos < 170:
        pos -= 85
        return Color(255 - pos * 3, 0, pos * 3)
    else:
        pos -= 170
        return Color(0, pos * 3, 255 - pos * 3)

This is crude because colour theory already gives us the HSV (Hue, Saturation, Value) colourspace where H or "Hue" maps roughly to what you might consider a colour in a rainbow. I wrote something about this years ago which you might find useful- https://learn.pimoroni.com//tutorial/unicorn-hat/making-rainbows-with-unicorn-hat

Python has a colorsys library that makes converting from RGB to HSV and back again pretty trivial. On a microcontroller the code above might be significantly more efficient, but on desktop Python there is no earthly reason why you'd use code as contrived as that above, in lieu of just doing colourspace conversions.

Broadly any "formula" for lighting a single LED should only ever accept the input values: "LED Index" and "Time" and only ever output one colour value. A rainbow would thus be produced either physically across the LED strip by driving the "Hue" (and thus the RGB value) from the "LED Index" or temporally over a period of time by deriving the "Hue" from "Time".

Things like the rainbow example in our Plasma library do this: https://github.com/pimoroni/plasma/blob/master/examples/rainbow.py

And a very extreme example from Unicorn HAT HD shows a range of functions which use just the X/Y coordinate from a square RGB display, and the "step" (which is just time, effectively) to create detailed demonscene style effects: https://github.com/pimoroni/unicorn-hat-hd/blob/master/examples/demo.py

This is extremely rough and untested and includes no strip setup, but the concise, Python way of doing rainbowCycle would look something like this:

import colorsys
import time

NUM_PIXELS = 10

def hue_to_rgb(h):
    """Convert a hue into an r, g, b color"""
    return [int(c * 255) for c in colorsys.hsv_to_rgb(h, 1.0, 1.0)]

while True:
    t = time.time() / 100  # Get the current timestep

    # Iterate through each pixel
    for i in range(NUM_PIXELS):
        # Spread the Hue range over the pixels, but also cycle it over time
        r, g, b = hue_to_rgb(t + i / NUM_PIXELS)
        strip.setPixelColor(i, Color(r, g, b))

    strip.show()
    time.sleep(1.0 / 60)  # Aim for ~60FPS