user10183964
user10183964

Reputation: 41

Fading between 2 colors in Curses (Python)

I'm trying to fade the background color of a Curses subwindow between 2 arbitrary RGB values passed to the setupColor() function in my code.

In some cases, the code behaves as expected and can satisfactorily fade between the colors, but most of the time it performs strangely.

setupColor((0,0,0),(0,255,55))

This will fade the subwindow between black and aqua, and works nicely.

However, if I try to fade between say, yellow and purple, like so:

setupColor((255,200,0),(200,0,200))

This will cycle between the 2 colors for the first few cycles, but seems to lose sync and eventually produces noticeably different colors then those passed to the function.

As I based the original code on the Arduino LED Fade sketch, I searched to see if anyone had tried to do something similar with physical RGB LED's, which turned up this thread: c++ Fade between colors? (Arduino).

The solution posted on the thread seems like it would be ideal for my needs, but I don't know C++ or JavaScript well enough to be able to follow, understand and port the code to Python.

Is it possible to adapt my code in order for the colors to fade properly, or is it worth scrapping it and starting over from scratch?

import curses,time
from curses import wrapper

def setupColor((red,green,blue),(red1,green1,blue1)):

    global color1,color2                                   #initialise globals for use later on in the program.
    global incrFloatRed,incrFloatGreen,incrFloatBlue
    global minRed,minGreen,minBlue,maxRed,maxGreen,maxBlue

    stepNumber = 60.00                 # number of steps within each color transition.

    color1 = (red,green,blue)         # clone the input tuples for use later on...
    color2 = (red1,green1,blue1)


    differenceRed = red - red1          # subtract each channel of color1 from color2,
    differenceGreen = green - green1    # this will return either a positive or negative float.
    differenceBlue = blue - blue1


    incrFloatRed = differenceRed / stepNumber        # divide the difference between the 2 colors by the 
    incrFloatGreen = differenceGreen / stepNumber    # step rate to obtain the color increments.
    incrFloatBlue = differenceBlue / stepNumber

    if red > red1:                            # if the red channel value of the 1st color is greater than 
            incrFloatRed = -incrFloatRed      # that of the 2nd, invert the increment (to allow
            maxRed = red                      # color subtraction), then set the top end of the range as 
            minRed = red1                     # red 1st channel and the bottom as red 2nd channel.
    else:                                     # Else, perform the inverse operation.
            incrFloatRed = abs(incrFloatRed)
            maxRed = red1
            minRed = red

    if green > green1:
            incrFloatGreen = -incrFloatGreen
            maxGreen = green
            minGreen = green1
    elif green < green1:
            incrFloatGreen = abs(incrFloatGreen)        
            maxGreen = green1
            minGreen = green

    if blue > blue1:
            incrFloatBlue = -incrFloatBlue
            maxBlue = blue
            minBlue = blue1
    else:
            incrFloatBlue = abs(incrFloatBlue)
            maxBlue = blue1
            minBlue = blue



def main(stdscr):
    global incrFloatRed,incrFloatGreen,incrFloatBlue

    setupColor((0,0,0),(0,255,255))

    red = color1[0]                #red,green and blue are the variables used to control the fade.
    green = color1[1]              #To begin with, they is set to the colors contained in the first 
    blue = color1[2]               #tuple that is passed to setupColor()



    label = stdscr.subwin(10,50,1,4)  # create a subwindow, draw a box around it, then add the string
    label.box()                       # "Hello, World!" to the subwindow at position row 1, column 1.
    label.addstr(1,1,"Hello, World!")
    curses.curs_set(0)                # Disable cursor visibility


    while True:




            red = red + incrFloatRed           # color = colorValue + colorIncrement
            green = green + incrFloatGreen
            blue = blue + incrFloatBlue

            if red <= minRed or red >= maxRed:   # if color is less than the start of the color range or
                    incrFloatRed = -incrFloatRed # greater than end of the color range, invert the color increment

            if green <= minGreen or green >= maxGreen:
                    incrFloatGreen = -incrFloatGreen

            if blue <= minBlue or blue >= maxBlue:
                    incrFloatBlue = -incrFloatBlue
                                                      # curses.init_color takes a tuple of rgb values as it's argument,
            cursesRed = int(int(red) / 0.255)         # but uses funky 1000-point intensity values, instead of the usual
            cursesGreen = int(int(green) / 0.255)     # 255. e.g. rgb(1000,0,0) for full intensity red, instead of
            cursesBlue = int(int(blue) / 0.255)       #           rgb(255,0,0).
                                                      # To convert between them, divide the integer of the color value float 
                                                      # by 0.255, then obtain the integer of the resulting float. 



            if cursesRed >=1000:                      # Sometimes a color value is set to greater
                    cursesRed = 1000                  # than 1k or less than 0. When a negative value or a value greater 
            if cursesGreen >=1000:                    # than 1k is passed to curses.init_color(), it will return ERR and
                    cursesGreen = 999                 # Curses will crash. 
            if cursesBlue >=1000:                     
                    cursesBlue = 999

            if cursesRed <=0:
                    cursesRed = 0
            if cursesGreen <=0:
                    cursesGreen = 0
            if cursesBlue <=0:
                    cursesBlue = 0


            curses.init_color(1,cursesRed,cursesGreen,cursesBlue) # reassign Curses color (1) to the RGB1K color of the current step... 

            curses.init_pair(1,255,1)                             # then create a color pair with the dynamic value (1) as 
                                                                  # the BG color, and white (255) as the FG. 



            label.bkgd(curses.color_pair(1)) # set the background of the label subwindow to the current color pair.. 
            label.refresh()                  # then refresh, so that we can see the change.

            time.sleep(0.02)                 # Take a little snooze, then do it all again. 


wrapper(main)

Upvotes: 3

Views: 1154

Answers (1)

user10183964
user10183964

Reputation: 41

I figured this one out myself in the end, it was a relatively simple (but hacky and inelegant) fix. I printed the output of each of the color channels (variables red,green and blue in the code above) at this point in the program:

if red <= minRed or red >= maxRed:   
    outFile.write("Red : " + str(int(red)) + "|Green : " + str(int(green)) + "|Blue : " + str(int(blue)) + "\n")
    incrFloatRed = -incrFloatRed 

This is the output for the first three cycles of the program:

Red : 199|Green : 3  |Blue : 196
Red : 255|Green : 193|Blue : 0

Red : 199|Green : 9  |Blue : 196
Red : 255|Green : 186|Blue : 0

Red : 199|Green : 16 |Blue : 196
Red : 255|Green : 179|Blue : 0

As you can see, the green channel gradually drifts out of sync compared to the others, and the values for red and blue are slightly different to those passed to the setupColor() function.

The color value inaccuracy can be fixed by using if statements to directly set the values:

while True:
        red = red + incrFloatRed           # color = colorValue + colorIncrement
        green = green + incrFloatGreen
        blue = blue + incrFloatBlue

        ##### Add the if statements after this ####

        if red < minRed:
            red = minRed
        if red > maxRed:
            red = maxRed

        if blue < minBlue:
            blue = minBlue
        if blue > maxBlue:
            blue = maxBlue

        if green < minGreen:
            green = minGreen
        if green > maxGreen:
            green = maxGreen

And the timing/synchronization issue can be fixed by using a single if statement to change the color fade direction. Instead of using a statement for each color like this...

        if green <= minGreen or green >= maxGreen:
            incrFloatGreen = -incrFloatGreen 

...set all of them at once like this:

        if green <= minGreen or green >=maxGreen:
            incrFloatRed = -incrFloatRed 
            incrFloatGreen = -incrFloatGreen
            incrFloatBlue = -incrFloatBlue   

The eagle eyed will have probably spotted that if both maxGreen and minGreen are both set to 0 (e.g, rgb(230,0,100) and rgb(100,0,200)), nothing will happen. If you change green to a different color, it'll work fine.

I doubt that it would be hard to add some logic to detect which color channels would work, but considering that this can be avoided simply by passing 1 instead of 0, I've not bothered.

Another good move (efficiency wise) would be to dump all of the fade values into an array, and set the colors based on that, instead of going through the overhead of calculating the values each time.

Upvotes: 1

Related Questions