
Evan S. answered 12/16/21
Computer Science and Math Tutor
Hey Gabe,
I'd be happy to help you set up custom color stops in an RGB-defined gradient using Python!
I am not sure which graphics python module you are using, but to tackle this problem I will walk you through TWO different ways to achieve this result using a flexible graphics module called Pycairo.
Step 0: Installation of Graphics Module (I use Pycairo)
On most systems (I am using Windows) this is installed most easily through the pip package manager:
pip install pycairo
After executing that command in your Powershell / Terminal, your system should have Pycairo installed and ready to use. To use Pycairo in Python, simply type in your gradient.py file:
import cairo
Option 1: Use Pycairo's built-in LinearGradient class to create a user-defined RGB Gradient
I will present 2 options here, this first one being the more efficient from a practical point of view. This is because a LinearGradient in Pycairo just needs two color stops added as key RGB references from which an algorithm fills in the 'color space' that represents a gradient moving between those colors. That way, you are still getting a 2D RGB linear gradient (i.e. composed of lines drawn across the screen), but you do not have to manually calculate the intermediate RGB values between the 2 desired color stops.
The code below will ask the user for the desired dimensions of the window, as well as the Red, Green, and Blue values for two color stops that define the gradient.
#!/usr/bin/env python
import cairo
# Set up surface (the projected image backend e.g. .jpg) and context (object used for drawing)
print("_"*16,"\nCANVAS SET-UP\n","_"*16, sep='')
print(">> Enter Canvas Dimensions:")
WIDTH = int(input("\tWidth: ") or "256")
HEIGHT = int(input("\tHeight: ") or "256")
print(">> Specify first and last gradient stop colors:")
print(" >> Format: #R# #G# #B# (e.g. 255 255 255)")
stop0 = [int(x) for x in input("\tStop0: ").split()]
stop1 = [int(x) for x in input("\tStop1: ").split()]
# Pass color format (standard RGB24, 8 bits per channel here) & user-input dimensions
# to image surface backend
surface = cairo.ImageSurface(cairo.FORMAT_RGB24, WIDTH, HEIGHT)
# Set up Context for drawing on canvas
ctx = cairo.Context(surface)
ctx.scale(WIDTH, HEIGHT) # Normalizing the canvas (both dimensions are 0.0-1.0 now)
# Create gradient Pattern and add color stops at offset 0 and 1
pat = cairo.LinearGradient(0.0, 0.0, 0.0, 1.0)
pat.add_color_stop_rgb(1, stop1[0]/255, stop1[1]/255, stop1[2]/255) # One stop
pat.add_color_stop_rgb(0, stop0[0]/255, stop0[1]/255, stop0[2]/255) # Another stop
ctx.rectangle(0, 0, 1, 1) # Rectangle(x0, y0, x1, y1)
ctx.set_source(pat)
ctx.fill()
surface.write_to_png("ex_manual0.png") # Output to PNG
EXAMPLE RESULT:
Option 2: Use Pycairo's Context.line_to() to manually define the color of each horizontal line in the RGB gradient
So this option is admittedly more manual and slow, but if you desire to interpolate the linear color shift in a gradient on your own, all you have to do, like you said in the original post, is draw horizontal lines with R, G, and B values that move color-wise from that of the first color stop to the color of the last stop as we move down the screen. Although you may have to mess around with Context.set_line_width(width) to get the result that looks best to you, this gives a manual definition of a gradient between 2 custom color stops.
#!/usr/bin/env python
import cairo
# Set up surface (the projected image backend e.g. .jpg) and context (object used for drawing)
print("_"*16,"\nCANVAS SET-UP\n","_"*16, sep='')
print(">> Enter Canvas Dimensions:")
WIDTH = int(input("\tWidth: ") or "256")
HEIGHT = int(input("\tHeight: ") or "256")
print(">> Specify first and last gradient stop colors:")
print(" >> Format: #R# #G# #B# (e.g. 255 255 255)")
stop0 = [int(x) for x in input("\tStop0: ").split()]
stop1 = [int(x) for x in input("\tStop1: ").split()]
# Pass color format (standard RGB24, 8 bits per channel here) & user-input dimensions
# to image surface backend
surface = cairo.ImageSurface(cairo.FORMAT_RGB24, WIDTH, HEIGHT)
# Set up Context for drawing on canvas
ctx = cairo.Context(surface)
ctx.scale(WIDTH, HEIGHT) # Normalizing the canvas (both dimensions are 0.0-1.0 now)
ctx.set_line_width(0.1)
LW = ctx.get_line_width()
n_lines = int(HEIGHT / LW)
ctx.move_to(0, 1/HEIGHT)
for stroke in range(0, n_lines):
ctx.move_to(0, (stroke * LW + 1)/HEIGHT)
delta_r = (stroke / n_lines) * abs(stop0[0] - stop1[0])
delta_g = (stroke / n_lines) * abs(stop0[1] - stop1[1])
delta_b = (stroke / n_lines) * abs(stop0[2] - stop1[2])
color = [255, 255, 255]
color[0] =(stop0[0] + delta_r) if stop0[0] < stop1[0] else (stop0[0] - delta_r)
color[1] =(stop0[1] + delta_g) if stop0[1] < stop1[1] else (stop0[1] - delta_g)
color[2] =(stop0[2] + delta_r) if stop0[2] < stop1[2] else (stop0[2] - delta_r)
#print("color:", color)
ctx.set_source_rgb(color[0]/255, color[1]/255, color[2]/255)
ctx.line_to(1.0, (stroke * LW + 1)/HEIGHT)
ctx.stroke()
surface.write_to_png("ex_LINES1.png") # Output to PNG
The first part of the code is the same as above, however, we do not apply a LinearGradient 'Pattern' to fill the 'Context' (the object used for drawing in PyCairo). Rather, we calculate the number of lines it will take to fill the screen's height, and iterate through each of those horizontal line counts. As we do this, we simply set up 3 linear equations moving a color from y0 = R0, G0, B0 to y1 = R1, G1, B1 as we draw each line. We take the number of total lines, n_lines, and the current stroke number, stroke, and multiply the quotient stroke / n_lines by the absolute difference in R, G, and B values individually. This will give you the amount your R, G, or B component needs to be adjusted for the given stroke or line. Just change each of the R, G, and B values by this delta_r delta_g and delta_b from the above calculation, and tell the PyCairo Context to draw a line of this custom color.
EXAMPLE RESULT:
Feel free to reach out with further questions about the implementation here or any other details.
Best,
Evan S.