3

How would I fade from one colour into another in pygame? I want to slowly change the colour of a circle from green to blue to purple to pink to red to orange to yellow to green. How would I do that? At the moment, I'm using

def colour():
    switcher = {
        0: 0x2FD596,
        1: 0x2FC3D5,
        2: 0x2F6BD5,
        3: 0x432FD5,
        4: 0x702FD5,
        5: 0xBC2FD5,
        6: 0xD52F91,
        7: 0xD52F43,
        8: 0xD57F2F,
        9: 0xD5D52F,
        10: 0x64D52F,
        11: 0x2FD557,
    }
    return switcher.get(round((datetime.datetime.now() - starting_time).total_seconds()%11))

but that has really big steps in between the colours and looks clunky.

qff
  • 5,524
  • 3
  • 37
  • 62
Jacques Amsel
  • 1,061
  • 2
  • 14
  • 30
  • 3
    When you get to a resolution, please remember to up-vote useful things and accept your favourite answer (even if you have to write it yourself), so Stack Overflow can properly archive the question. – Prune Aug 22 '18 at 19:58

7 Answers7

7

The key is to simply calculate how much you have to change each channel (a,r,g and b) each step. Pygame's Color class is quite handy, since it allows iteration over each channel and it's flexible in it's input, so you could just change e.g. 'blue' to 0x2FD596 in the below example and it will still run.

Here's the simple, running example:

import pygame
import itertools

pygame.init()

screen = pygame.display.set_mode((800, 600))

colors = itertools.cycle(['green', 'blue', 'purple', 'pink', 'red', 'orange'])

clock = pygame.time.Clock()

base_color = next(colors)
next_color = next(colors)
current_color = base_color

FPS = 60
change_every_x_seconds = 3.
number_of_steps = change_every_x_seconds * FPS
step = 1

font = pygame.font.SysFont('Arial', 50)

running = True
while running:

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    text = font.render('fading {a} to {b}'.format(a=base_color, b=next_color), True, pygame.color.Color('black'))

    step += 1
    if step < number_of_steps:
        # (y-x)/number_of_steps calculates the amount of change per step required to 
        # fade one channel of the old color to the new color
        # We multiply it with the current step counter
        current_color = [x + (((y-x)/number_of_steps)*step) for x, y in zip(pygame.color.Color(base_color), pygame.color.Color(next_color))]
    else:
        step = 1
        base_color = next_color
        next_color = next(colors)

    screen.fill(pygame.color.Color('white'))
    pygame.draw.circle(screen, current_color, screen.get_rect().center, 100)
    screen.blit(text, (230, 100))
    pygame.display.update()
    clock.tick(FPS)

enter image description here


If you don't want to be dependent on the framerate but rather use a time based approach, you could change the code to:

...
change_every_x_milliseconds = 3000.
step = 0

running = True
while running:

    ...

    if step < change_every_x_milliseconds:
        current_color = [x + (((y-x)/change_every_x_milliseconds)*step) for x, y in zip(pygame.color.Color(base_color), pygame.color.Color(next_color))]
    else:
        ...
    ...

    pygame.display.update()
    step += clock.tick(60)
sloth
  • 99,095
  • 21
  • 171
  • 219
3

You could go between all the values from one colour to the next by converting it into an int, increasing the number, and converting it back to hex. Then you just loop until you reach the next value with something like so:

value1 = 0xff00ff
value2 = 0xffffff
increment = 1 # amount to decrease or increase the hex value by
while value1 != value2:
    if value1 > value2:
        if int(value1)-increment < int(value2): # failsafe if the increment is greater than 1 and it skips being the value
            value1 = value2
        else:
            value1 = hex(int(value1)-increment)
    else:
        if int(value1)+increment > int(value2):
            value1 = value2
        else:
            value1 = hex(int(value1)+increment)
    code_to_change_colour(value1)

See the edit by Prune for a much more elegant implementation of this. Note that code_to_change_colour(value1) should be changed to however you change the colour in your program. The increment will let you change how many colours are skipped. Obviously this code would need to be edited into a manner it can be used easily: e.g a function like def fade(value1, value2).


Edit from @Prune -- because code doesn't work well in comments.

Note that most of what you've written is "merely" loop control. You have known start and stop values and a fixed increment. This suggests a for loop rather than a while. Consider this:

value1 = int(0xff00ff)
value2 = int(0xffffff)
increment = 1 if value1 < value2 else -1

for current in range(value1, value2, increment):
    code_to_change_colour(hex(value1))

value1 = value2        
Code Enjoyer
  • 681
  • 4
  • 18
2

If you'd prefer just calculating the colors (without using any surfaces), you can do this:

First, you need to determine how long you want the dissolve to take. You also need to store the original and final colors. Last, calculate the blend. I would create a class for this:

import pygame
import time

class color_blend:
    def __init__(self, start_color, end_color, duration=1000):
        self.start_color = pygame.Color(start_color.r, start_color.g, start_color.b)
        self.current_color = pygame.Color(start_color.r, start_color.g, start_color.b)
        self.end_color = end_color
        self.duration = float(duration)
        self.start_time = color_blend.millis()

    # Return current time in ms
    @staticmethod
    def millis():
        return (int)(round(time.time() * 1000))

    # Blend any 2 colors
    # 0 <= amount <= 1 (0 is all initial_color, 1 is all final_color)
    @staticmethod
    def blend_colors(initial_color, final_color, amount):
        # Calc how much to add or subtract from start color
        r_diff = (final_color.r - initial_color.r) * amount
        g_diff = (final_color.g - initial_color.g) * amount
        b_diff = (final_color.b - initial_color.b) * amount

        # Create and return new color
        return pygame.Color((int)(round(initial_color.r + r_diff)),
                            (int)(round(initial_color.g + g_diff)),
                            (int)(round(initial_color.b + b_diff)))

    def get_next_color(self):
        # Elapsed time in ms
        elapsed_ms = color_blend.millis() - self.start_time

        # Calculate percentage done (0 <= pcnt_done <= 1)
        pcnt_done = min(1.0, elapsed_ms / self.duration)

        # Store new color
        self.current_color = color_blend.blend_colors(self.start_color, self.end_color, pcnt_done)
        return self.current_color

    def is_finished(self):
        return self.current_color == self.end_color

# Blend red to green in .3 seconds
c = color_blend(pygame.Color(255, 0, 0), pygame.Color(0, 255, 0), 300)
while not c.is_finished():
    print(c.get_next_color())

You can easily modify this to do non-linear blending. For example, in blend_colors: amount = math.sin(amount * math.pi)

(I'm no Pygame expert - there may already be a built-in function for this.)

001
  • 13,291
  • 5
  • 35
  • 66
1

Set your foreground surface to the old color, over a background of the new one. Use set_alpha() to perform the fade. Once you're entirely on the new color, make that surface the foreground and make a new background of your third color. Repeat as desired.

This question and other references to "fade" and set_alpha() should allow you to finish the job.

Is that enough to get you moving?

Prune
  • 76,765
  • 14
  • 60
  • 81
1

I hesitated to post an answer because I came up with almost the same answer as Sloth's, but I'd just like to mention linear interpolation (short lerp, also called mix in OpenGL/GLSL). It's usually used to blend between two colors, but unfortunately pygame's Color class doesn't have a lerp method, so you have to define your own lerp function and use a list comprehension to interpolate the RGBA values.

Here's the lerp function from Wikipedia ported to Python (t is the weight and has to be between 0 and 1):

def lerp(v0, v1, t):
    return (1 - t) * v0 + t * v1

Now you can lerp the RGBA values of two colors with a list comprehension.

color = [lerp(v0, v1, t) for v0, v1 in zip(color1, color2)]

E.g.:

>>> [lerp(v0, v1, .5) for v0, v1 in zip((0, 0, 0), (255, 255, 255))]
[127.5, 127.5, 127.5]
>>> [lerp(v0, v1, .25) for v0, v1 in zip((0, 0, 0), (255, 255, 255))]
[63.75, 63.75, 63.75]

If you don't need the alpha channel, you can also use pygame's Vector3 class which has a lerp method for your colors, then you'd just have to write: color = color1.lerp(color2, t).

import itertools
import pygame as pg
from pygame.math import Vector3


pg.init()
screen = pg.display.set_mode((640, 480))
clock = pg.time.Clock()

color_cycle = itertools.cycle([
    Vector3(255, 0, 0),
    Vector3(0, 255, 0),
    Vector3(255, 255, 0),
    Vector3(0, 0, 255),
    ])

color1 = next(color_cycle)
color2 = next(color_cycle)
color = color1
start_time = pg.time.get_ticks()
time_interval = 2000  # milliseconds
t = 0

done = False
while not done:
    for event in pg.event.get():
        if event.type == pg.QUIT:
            done = True

    now = pg.time.get_ticks()
    t = (now - start_time) / time_interval

    if t > 1:
        t = 0
        start_time = now
        color1, color2 = color2, next(color_cycle)

    color = color1.lerp(color2, t)  # Lerp the two colors.

    screen.fill(color)
    pg.display.flip()
    clock.tick(60)
skrx
  • 19,980
  • 5
  • 34
  • 48
0

I had a similar problem not to long ago, I went the crazy route of learning a bit of Calculus to do it R = X, G = Y, B = Z

then wrote a program to calculate the difference between R0 and R1 (X0 and X1) etc.

#Nick Poling
#Fade Test V2

import random
#import mpmath
#import sympy
import time
import pygame
import shelve
import sys
from pygame.locals import *

#Always Initialize
pygame.init()

#sympy contains function line_3d?
#top level?
#formated string?

#Change Name Displayed in Corner
MY_FONT = 'freesansbold.ttf'
FONTSIZE_1 = 20
pygame.display.set_caption('Fade Test V2')
GAME_NAME = pygame.font.Font(MY_FONT, FONTSIZE_1)

#Boards
GAME_SCREEN_WIDTH = 900
GAME_SCREEN_HEIGHT = 600

Main_Game_Loop = 1

def get_close_out():
    #Save_Game = get_Save_Game()
    pygame.quit()
    sys.exit()

def get_Save_Game():
    #Save
    global Main_Game_Loop
    global GAME_SCREEN_COLOR
    global GAME_SCREEN_WIDTH
    global GAME_SCREEN_HEIGHT
    saveGameShelfFile = shelve.open('Fade Test V1')
    saveGameShelfFile['GAME_SCREEN_WIDTH'] = GAME_SCREEN_WIDTH
    saveGameShelfFile['GAME_SCREEN_HEIGHT'] = GAME_SCREEN_HEIGHT

def get_Load_Game():
    global Main_Game_Loop
    global GAME_SCREEN_COLOR
    global GAME_SCREEN_WIDTH
    global GAME_SCREEN_HEIGHT
    try:
        #Load
        saveGameShelfFile = shelve.open('Fade Test V1')
        GAME_SCREEN_WIDTH = saveGameShelfFile['GAME_SCREEN_WIDTH']
        GAME_SCREEN_HEIGHT = saveGameShelfFile['GAME_SCREEN_HEIGHT']
    except:
        #Save
        #Save_Game = get_Save_Game()
        #Load
        saveGameShelfFile = shelve.open('Fade Test V1')
        GAME_SCREEN_WIDTH = saveGameShelfFile['GAME_SCREEN_WIDTH']
        GAME_SCREEN_HEIGHT = saveGameShelfFile['GAME_SCREEN_HEIGHT']
    GAME_SCREEN = pygame.display.set_mode((GAME_SCREEN_WIDTH, GAME_SCREEN_HEIGHT),pygame.RESIZABLE)
    #By putting the GAME_SCREEN here you can make the resize doable with the press of a button
    #Does need to be un "#" to work when press "L"

def get_Colors():
    #Colors
    global BLACK
    global WHITE
    BLACK = [0, 0, 0]
    WHITE = [255,255,255]

def get_Main_Game():
    global Main_Game_Loop
    global GAME_SCREEN_COLOR
    global GAME_SCREEN_WIDTH
    global GAME_SCREEN_HEIGHT
    global BLACK
    global WHITE
    global Point_1
    global Point_2
    global Vector_1
    global Vector_2
    Colors = get_Colors()
    GAME_SCREEN_COLOR = BLACK
    #Load_Game = get_Load_Game()
    GAME_SCREEN = pygame.display.set_mode((GAME_SCREEN_WIDTH, GAME_SCREEN_HEIGHT),pygame.RESIZABLE)
    while Main_Game_Loop == 1:
        GAME_SCREEN.fill(GAME_SCREEN_COLOR)
        Equation_Of_Lines_in_3D_Space = get_Equation_Of_Lines_in_3D_Space()
        for t in range(0,255):
            XYZ_1 = [Point_1[0] + (Vector_1[0] * t), Point_1[1] + (Vector_1[1] * t), Point_1[2] + (Vector_1[2] * t)]
            XYZ_2 = [Point_2[0] + (Vector_2[0] * t), Point_2[1] + (Vector_2[1] * t), Point_2[2] + (Vector_2[2] * t)]
            GAME_SCREEN_COLOR = XYZ_1
            GAME_SCREEN.fill(GAME_SCREEN_COLOR)
            ticks = pygame.time.delay(5)
            pygame.display.update()
            for event in pygame.event.get():
                if event.type == QUIT:
                    close_out = get_close_out()
                elif event.type == pygame.VIDEORESIZE:
                    # There's some code to add back window content here.
                    surface = pygame.display.set_mode((event.w, event.h),pygame.RESIZABLE)
                    GAME_SCREEN_HEIGHT = event.h
                    GAME_SCREEN_WIDTH = event.w
        pygame.display.update()

def get_Equation_Of_Lines_in_3D_Space():
    global Point_1
    global Point_2
    global BLACK
    global WHITE
    global Vector_1
    global Vector_2
    global LCM_X1
    global LCM_X2
    global LCM_Y1
    global LCM_Y2
    global LCM_Z1
    global LCM_Z2
    Point_1 = BLACK
    Point_2 = WHITE
    Vector_1 = []
    Vector_2 = []
    LCM_X1 = []
    LCM_X2 = []
    LCM_Y1 = [] 
    LCM_Y2 = [] 
    LCM_Z1 = []
    LCM_Z2 = []
    for i in range(0,3):
        #
        Delta_XYZ_1 = Point_2[i] - Point_1[i]
        Vector_1.append(Delta_XYZ_1)
        Delta_XYZ_2 = Point_1[i] - Point_2[i]
        Vector_2.append(Delta_XYZ_2)
    factors = get_factors()

def get_factors():
    global num_1
    global num_2
    global Vector_1
    global Vector_2
    global LCM_XYZ_1
    global LCM_XYZ_2
    for i in range(1,7):
        if i == 1:
            num_1 = Vector_1[0]
            num_2 = 1
        elif i == 2:
            num_1 = Vector_2[0]
            num_2 = 2
        elif i == 3:
            num_1 = Vector_1[1]
            num_2 = 3
        elif i == 4:
            num_1 = Vector_2[1]
            num_2 = 4
        elif i == 5:
            num_1 = Vector_1[2]
            num_2 = 5
        elif i == 6:
            num_1 = Vector_2[2]
            num_2 = 6
        get_largest_and_lowest_common_factors(num_1)
    get_LCM_XYZ()
    Vector_1[0] = Vector_1[0] / LCM_XYZ_1[0]
    Vector_1[1] = Vector_1[1] / LCM_XYZ_1[0]
    Vector_1[2] = Vector_1[2] / LCM_XYZ_1[0]
    #
    Vector_2[0] = Vector_2[0] / LCM_XYZ_2[0]
    Vector_2[1] = Vector_2[1] / LCM_XYZ_2[0]
    Vector_2[2] = Vector_2[2] / LCM_XYZ_2[0]

def get_largest_and_lowest_common_factors(x):
    global num_1
    global num_2
    global Vector_1
    global Vector_2
    global LCM_X1
    global LCM_X2
    global LCM_Y1
    global LCM_Y2
    global LCM_Z1
    global LCM_Z2
    #This function takes a number and puts its factor into a list
    for i in range(1, x + 1) or range(-x, x - 1, -1):
        try:
            if x % i == 0:
                if num_1 == Vector_1[0] and num_2 == 1:
                    LCM_X1.append(i)
                elif num_1 == Vector_1[1] and num_2 == 3:
                    LCM_Y1.append(i)
                elif num_1 == Vector_1[2] and num_2 == 5:
                    LCM_Z1.append(i)
                elif num_1 == Vector_2[0] and num_2 == 2:
                    LCM_X2.append(i)
                elif num_1 == Vector_2[1] and num_2 == 4:
                    LCM_Y2.append(i)
                elif num_1 == Vector_2[2] and num_2 == 6:
                    LCM_Z2.append(i)
        except  ZeroDivisionError:
            return 0

def get_LCM_XYZ():
    global LCM_X1
    global LCM_Y1
    global LCM_Z1
    global LCM_X2
    global LCM_Y2
    global LCM_Z2
    global LCM_XYZ_1
    global LCM_XYZ_2
    #If 1 is 0
    check_1 = 0
    check_2 = 0
    check_3 = 0
    for i in range(0,3):
        if i == 0:
            if LCM_X1 == [] and LCM_X2 == []:
                check_1 = 1
        elif i == 1:
            if LCM_Y1 == [] and LCM_Y2 == []:
                check_2 = 2
        elif i == 2:
            if LCM_Z1 == [] and LCM_Z2 == []:
                check_3 = 3
    F_check = check_1 + check_2 + check_3
    if F_check == 1:
        LCM_X1.extend(LCM_Y1)
        LCM_X2.extend(LCM_Y2)
    elif F_check == 2:
        LCM_Y1.extend(LCM_X1)
        LCM_Y2.extend(LCM_X2)
    elif F_check == 3:
        if check_2 == 0:
            LCM_Z1.extend(LCM_Y1)
            LCM_Z2.extend(LCM_Y2)
        elif check_2 != 0:
            LCM_X1.extend(LCM_Z1)
            LCM_X2.extend(LCM_Z2)
            LCM_Y1.extend(LCM_Z1)
            LCM_Y2.extend(LCM_Z2)
    elif F_check == 4:
        LCM_X1.extend(LCM_Y1)
        LCM_X2.extend(LCM_Y2)
        LCM_Z1.extend(LCM_Y1)
        LCM_Z2.extend(LCM_Y2)
    elif F_check == 5:
        LCM_Y1.extend(LCM_X1)
        LCM_Y2.extend(LCM_X2)
        LCM_Z1.extend(LCM_X1)
        LCM_Z2.extend(LCM_X2)
    elif F_check == 6:
        LCM_X1.append(1)
        LCM_X2.append(1)
        LCM_Y1.append(1)
        LCM_Y2.append(1)
        LCM_Z1.append(1)
        LCM_Z2.append(1)
    LCM_X1 = set(LCM_X1)
    LCM_Y1 = set(LCM_Y1)
    LCM_Z1 = set(LCM_Z1)
    LCM_X2 = set(LCM_X2)
    LCM_Y2 = set(LCM_Y2)
    LCM_Z2 = set(LCM_Z2)
    LCM_XYZ_1 = list(set.intersection(LCM_X1,LCM_Y1,LCM_Z1))
    LCM_XYZ_2 = list(set.intersection(LCM_X2,LCM_Y2,LCM_Z2))
    LCM_XYZ_1.sort(reverse = True)
    LCM_XYZ_2.sort(reverse = True)


Main_Game = get_Main_Game()
0

Pygame provides the pygame.Color object. The object can construct a color from various arguments (e.g. RGBA color channels, hexadecimal numbers, strings, ...).
It also offers the handy method lerp, that can interpolate 2 colors:

Returns a Color which is a linear interpolation between self and the given Color in RGBA space

Use the pygame.Color object and the lerp method to interpolate a color form a list of colors:

def lerp_color(colors, value):
    fract, index = math.modf(value)
    color1 = pygame.Color(colors[int(index) % len(colors)])
    color2 = pygame.Color(colors[int(index + 1) % len(colors)])
    return color1.lerp(color2, fract)

The interpolation value must change over time. Use pygame.time.get_ticks to get the current time in milliseconds:

colors = ["green", "blue", "purple", "pink", "red", "orange", "yellow"]
value = (pygame.time.get_ticks() - start_time) / 1000
current_color = lerp_color(colors, value)

See also Color - Lerp


Minimal example:

import pygame, math

def lerp_color(colors, value):
    fract, index = math.modf(value)
    color1 = pygame.Color(colors[int(index) % len(colors)])
    color2 = pygame.Color(colors[int(index + 1) % len(colors)])
    return color1.lerp(color2, fract)

pygame.init()
window = pygame.display.set_mode((400, 300))
clock = pygame.time.Clock()
start_time = pygame.time.get_ticks()
run = True
while run:
    clock.tick(60)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False
        if event.type == pygame.KEYDOWN:
            start_time = pygame.time.get_ticks()

    colors = ["green", "blue", "purple", "pink", "red", "orange", "yellow"]
    value = (pygame.time.get_ticks() - start_time) / 1000
    current_color = lerp_color(colors, value)

    window.fill((255, 255, 255))
    pygame.draw.circle(window, current_color, window.get_rect().center, 100)
    pygame.display.flip()

pygame.quit()
exit()
Rabbid76
  • 202,892
  • 27
  • 131
  • 174