- Published on
Colors and Gradients
Colors Fundamentals
Digitally, people talk about colors in terms of RGB, or hex codes. For the scope of this article, we refer to colors in terms of their RGB ratios. We can also convert hex colors to RGB using the following algorithm:
function hexColorToFloatColor(hex){
return [
parseInt(hex.substring(0,2), 16) / 255,
parseInt(hex.substring(2,4), 16) / 255,
parseInt(hex.substring(4,6), 16) / 255
];
}
The above image is actually the color of our website background.
The tuple in RGB corresponds to black, and corresponds to white. Basically, the values of each can range from . Now that results in a BIGGGG number of colors. Literally colors, and we can't tell most of the colors apart by an eye. That's part of the reason why color gradients work- the seamless blending from one color to another.
On a screen, a pixel looks somewhat like this. The variation in the intensity of the red, green, and blue creates variation in colors. We use these variations to make our said "gradients".
Gradients
"They come in all shapes and sizes"
Yes, they really do.
Axial gradients
Radial gradients
And, there are conic ones, random ones, and a lot more.
An Algorithm to Generate Linear Gradients
The process of gradually shifting from one color to the other is called linear interpolation.
It's also called 'lerp', if you remember that term from the bezier article.
To visualise gradients, we think in terms of coordinates in the RGB plane instead of the XYZ coordinates. Basically, think of a gradient as a path between 2 colors. but, there isn't just one path between 2 colors, as in a normal XYZ plane. (We will talk about the smoothest path in a later section.)
Consider the following gradient:
Simple Linear Gradient
Here, we use a pure red, and a pure blue. A color could be represented in its tuples that take values between 0 to 255. However, just to normalise, let's take the values to be between 1 and 0. Red would be (1.0, 0.0, 0.0) and blue would be (0.0, 0.0, 1.0)
To define a function called 'lerp', let's define a couple of things:
Colors
An array with the tuples of both the colors as follows, wherein corresponds to the intensity of red of the th color we choose for the extreme ends of the gradients.
function lerp(colors, value){
return [
colors[0][0] + (colors[1][0] - colors[0][0]) * value,
colors[0][1] + (colors[1][1] - colors[0][1]) * value,
colors[0][2] + (colors[1][2] - colors[0][2]) * value
];
}
In the example we considered, the gradient is px wide, so I just need find values between and where goes from to .
What we expect is that at value we get color , at we get color and at we get etc. You might be wondering why the middle value is 0. Well, that's because it corresponds to the intensity of Green in the colors. In pure red, and pure blue, there's no green! Hence, it doesn't even vary.
What's happening is we find the distance between the points in terms of each component . So along the red axis the difference is , and we just multiply by how far along we are plus the starting value. Blue is the reverse, it's the same length but we're moving in the reverse direction and we start at instead of . The sign is important to know if we are increasing or decreasing.
If this was a less simple color we might have length shorter than for the component but we're still just finding the fraction of the length that the current point represents.
Multi Stop Gradient
One can also make gradients with multi stops, wherein the progression isn't hardcoded to fit only 2 values.
Here, let's go beyond colors. Just for fun. Let's consider 4-dimensional points. (Just to generalise the idea of lerps.
Here's an extremely unoptimised algorithm to find values of pixels for the gradients. If you want to actually draw the gradient you might instead precompute the stop calculations and iterate over the pixels here rather than call this function for each pixel.
function linearGradient(stops, value) {
const stopLength = 1 / (stops.length - 1);
const valueRatio = value / stopLength;
const stopIndex = Math.floor(valueRatio);
if (stopIndex === (stops.length - 1)) {
return stops[stops.length - 1];
}
const stopFraction = valueRatio % 1;
return lerp(stops[stopIndex], stops[stopIndex + 1], stopFraction);
}
function lerp(pointA, pointB, normalValue) {
return [
pointA[0] + (pointB[0] - pointA[0]) * normalValue,
pointA[1] + (pointB[1] - pointA[1]) * normalValue,
pointA[2] + (pointB[2] - pointA[2]) * normalValue,
pointA[3] + (pointB[3] - pointA[3]) * normalValue,
];
}
We're not just hardcoding two values this time. The result indicates how far along the entire colour journey we are. To identify the colour at the value location, we must first determine which two colours we are sandwiched between. StopIndex, which is the value divided by the length between each stop, indicates this. We must floor it in order to obtain the integer index.
The first colour will be this, and the last colour will be stopIndex + 1. The only thing that's left is a new value between the two spots. Taking the modulo gives us the fractional bit of a float. Then it's the same as the -stop problem, but with a fractional bit instead of the entire number.
We can make a loooot of different types of gradients with better optimisations. However, let's talk about one last thing. SMOOTH CURVES. Like, sharp ugly gradients are just sad. In the following gradient, you can see bands and separations.
The gradient can be smoothened to the following gradient, which is much more homogenous, has lesser width of dull boring colors.
The points that define a spline are known as "Control Points". One of the features of the Catmull-Rom spline is that the specified curve will pass through all of the control points - this is not true of all types of splines.
Catmull-Rom is selected as it means that the input color stops will also appear on the final curve that we trace through 3D color space.
In order to solve these issues, we can:
- Use a more perceptually uniform color space such as CIELAB (L* a* b* ).
- Use Catmull-Rom spline interpolation to smoothly transition from one 3D Lab* vector to the next.
In the L* a* b* color space, L* indicates lightness and a* and b* are chromaticity coordinates. a* and b* are color directions: +a* is the red axis, -a' is the green axis, +b* is the yellow axis and b* is the blue axis.
Resources to explore
Perceptually Smooth Multi-Color Linear Gradients, Matt DesLauriers
Linear Color Gradients from Scratch, ndesmic
Introduction to Catmull-Rom Splines