I would like to learn about the logic of gradients in CSS, in what patterns they can be applied and to what elements.
I assume you know a little bit about how HTML color codes (like #02feca
) work
It's not a requirement, but a little C knowledge will definitely help
24-bit colors in HTML look like this: #012345
. Each pair represents a color channel (r, g, and b)
Delimiter | R | G | B |
---|---|---|---|
# | 01 | 23 | 45 |
Each of these numbers are 8-bit integers (24 / 3 = 8), meaning they range from 00 - FF, or 0 to 28-1, or 0 - 255
The other part of understanding color codes, and RGB colors in general, is to understand what makes what. This is light, not paint, so Yellow and Blue do not make Green. They in fact make White since they are inverse of each other (Yellow = ffff00, blue = 0000ff). In addition, #000000 (nothing) is Black, and #FFFFFF (all channels 100%) is White
Confused? See the table below.
Red | Green | Blue | |
---|---|---|---|
Red | Red | Yellow | Purple |
Green | Yellow | Green | Aqua |
Blue | Purple | Aqua | Blue |
Let's also not forget that lowering a channel makes it darker. So if you have red (#ff0000
), but it's too bright for your taste, you can lower the red channel to something like (#550000
), and you'll have a crimson color. Or let's say you want orange. Well, what's orange? It's a mix of Yellow and Red. And Yellow is a mix of Red and Green. Puzzled? The trick is to take down the green channel so there's more red. Red = #ff0000
, yellow = #ffff00
, orange = #ff7700
. #77ff00
, though, gives you some sort of ugly yellowish green color. And to get brown, you just take a reddish-yellow color and make it super dark. The color code would be something like #332200
, and this is the result.
So now we can start to understand how a gradient might work. Simple, right? Just take each channel of color1, and compare it to color2. If it's less, add to it. If it's greater, subtract.
So let's take a look at an implementation in C. In this case, it's easier to work with structures than trying to coerce a hexidecimal value from a string
I'll briefly explain it line-by-line
#include <stdio.h>
#include <stdlib.h>
typedef struct{
int r;
int g;
int b;
} color;
void c_cpy(color *dest, color *src){
dest->r = src->r;
dest->g = src->g;
dest->b = src->b;
}
color *gradient(color *s1, color *s2){
int c_ptr = 0;
color *out = calloc(sizeof(color), 0x1000000);
c_cpy(&out[c_ptr++], s1);
while(1){
if(s1->r < s2->r) s1->r ++;
if(s1->r > s2->r) s1->r --;
if(s1->g < s2->g) s1->g ++;
if(s1->g > s2->g) s1->g --;
if(s1->b < s2->b) s1->b ++;
if(s1->b > s2->b) s1->b --;
c_cpy(&out[c_ptr++], s1);
if(s1->r == s2->r && s1->b == s2->b && s1->g == s2->g) break;
}
color blank = {-1};
c_cpy(&out[c_ptr++], &blank);
return out;
}
void g_print(color *c){
int i = 0;
while(c[i].r != -1){
printf("#%02X%02X%02X\n", c[i].r, c[i].g, c[i].b);
i++;
}
}
int main(){
color c1 = {0x00,0x00,0x00}; // Equivalent to #000000, or black
color c2 = {0x11,0x22,0x33}; // Equivalent to #112233, or dark blue-green
color *c3 = gradient(&c1, &c2); // Return an array of colors graduating from c1 to c2
g_print(c3);
}
Output: #000000 #010101 #020202 #030303 #040404 #050505 #060606 #070707 #080808 #090909 #0A0A0A #0B0B0B #0C0C0C #0D0D0D #0E0E0E #0F0F0F #101010 #111111 #111212 #111313 #111414 #111515 #111616 #111717 #111818 #111919 #111A1A #111B1B #111C1C #111D1D #111E1E #111F1F #112020 #112121 #112222 #112223 #112224 #112225 #112226 #112227 #112228 #112229 #11222A #11222B #11222C #11222D #11222E #11222F #112230 #112231 #112232 #112233
#include <stdio.h>
#include <stdlib.h>
These lines include two headers from libc (the standard C library): stdio.h
, which holds standard input/output functions, and stdlib.h
, which has supplementary things like random number and memory allocation functions. If you don't understand this, then you're probably not going to understand the rest, so you might as well skip to the closing.
typedef struct{
int r;
int g;
int b;
} color;
This is our color model. Specifically, it's assigning a struct with 3 numbers: r, g, and b, to a datatype named "color". With this, I can make a variable like color c = {255,46,100};
void c_cpy(color *dest, color *src){
dest->r = src->r;
dest->g = src->g;
dest->b = src->b;
}
We're starting to get a bit advanced here. This is a function called c_cpy. Meaning "color copy". It takes color pointers, which can either refer to a single variable, or an array of them. In this case, it's taking two pointers to colors, instead of making copies of them within its own scope. It returns nothing, as indicated by the void. It then proceeds to copy the values of the source to the destination
.Pitfall alert: If using a struct pointer, you reference its members with
struct->member
, but if you have a regular struct, you reference members withstruct.member
. Also, be sure, if your function has a struct pointer, and is passing it to another function, to NOT use&ptr
. If you are passing a normal struct to a function that is supposed to modify it, pass it like sofunction(&struct)
and make sure the function accepts a struct pointervoid function(struct *struct)
. BUT, if you're in function(), and pass the same struct pointer, just pass it likefunction2(struct)
. Pointers may be a simple concept, but they can still kick your ass if you're not careful.
color *gradient(color *s1, color *s2){
int c_ptr = 0;
color *out = calloc(sizeof(color), 0x1000000);
c_cpy(&out[c_ptr++], s1);
while(1){
if(s1->r < s2->r) s1->r ++;
if(s1->r > s2->r) s1->r --;
if(s1->g < s2->g) s1->g ++;
if(s1->g > s2->g) s1->g --;
if(s1->b < s2->b) s1->b ++;
if(s1->b > s2->b) s1->b --;
c_cpy(&out[c_ptr++], s1);
if(s1->r == s2->r && s1->b == s2->b && s1->g == s2->g) break;
}
color blank = {-1};
c_cpy(&out[c_ptr++], &blank);
return out;
}
Oh boy, where do I start? Its name is gradient, it returns a color pointer (in this case, an array of colors) (as indicated by color *
). It accepts two color pointers. s1 is the start point of the gradient, and s2 is the end point.
It then makes an int called c_ptr (color pointer), initializes it to 0, and the makes a very large color pointer called out, set to the size of the amount of all possible colors, plus 1. So that even if you want #000000 to #ffffff, it will be able to pull it off. Why am I doing this rather than, say, color out[0x1000000] = {0};
? Because I need this pointer to not die when the function goes out of scope. That way it can't be overwritten by another process on the system right after I return it.
The next line is a call to c_cpy(). It sends the newest cell from out as the destination, and the argument, s1, as the source. Pay special attention to those ampersands. They're way too easy to screw up.
the next line starts a block. Specifically, an infinite loop. A while loop executes as long as the condition passed to it evaluates to true. Non-zero numbers always evaluate to true.
In this loop, it checks the red channel of the start color against the red channel of the end color. Then the green, Then the blue. This is where the c_cpy function comes in handy. It makes sure that I can modify s1 without indirectly modifying the out array. And finally. It checks if the start color is equal to the end color, and if so, it breaks from the loop.
And finally, the next two lines create a blank color, which has an invalid value for the red channel (-1 is 232-1, or 264-1 on 64-bit systems, which is clearly way too large for a color channel). It then copies this fake color to the next cell of the out array, and this fake color is to mark the end of the array. After that, it returns the out array
void g_print(color *c){
int i = 0;
while(c[i].r != -1){
printf("#%02X%02X%02X\n", c[i].r, c[i].g, c[i].b);
i++;
}
}
This function's a little simpler. It's named g_print, for "gradient print". Returns nothing. Takes a color array as an argument (in C, arrays are internally the same as pointers).
it creates an int, i for iterator, and sets it to 0. The next line is a while loop checking if the red channel of the color located at index i in the c array is not equal to -1. Remember, this marks the end of the array. Just like how the NUL ASCII character marks the end of a null-terminated string.
The while loop is pretty simple. It just prints out the red, green and blue channels in hexadecimal format (%02X
means "Uppercase hexadecimal, padded with zeros to reach a minimum 2 chars in length"). Then it increments i.
int main(){
color c1 = {0x00,0x00,0x00}; // Equivalent to #000000, or black
color c2 = {0x11,0x22,0x33}; // Equivalent to #112233, or dark blue-green
color *c3 = gradient(&c1, &c2); // Return an array of colors graduating from c1 to c2
g_print(c3);
}
and main(). This function is very important. It marks the entry point for the linker (which is the last step of compilation). By convention, it returns int, and usually, that is 0. But sometimes I feel lazy and neglect to put a return 0;
on the last line. Return 0 just tells the system, or application that runs this application, that it exited without error. So in other words, this is ye olde fashioned way of doing exception handling.
The first two lines define our start and end colors, and the third uses the gradient function to make a gradient out of our two colors
The final line uses g_print() to print the gradient we just created.
In addition, here's an example of how higher level languages sometimes make things more complicated than they need to be. Ladies and gentlemen, the equivalent g_print function in JavaScript:
function color(r,g,b){
this.r = r;
this.g = g;
this.b = b;
}
function g_print(c){
var r,g,b,i = 0;
while(c[i] != -1){
// Due to JS's lack of any kind of string formatting function, I have to manually build up the colors full 24-bit value.
// So I have to make sure r, g, and b are within the 0 - 255 range
r = c[i].r % 0x100;
g = c[i].g % 0x100;
b = c[i].b % 0x100;
output = '#';
output += ((r >> 4).toString(16) + (r & 0x0F).toString(16)).toUpperCase();
output += ((g >> 4).toString(16) + (g & 0x0F).toString(16)).toUpperCase();
output += ((b >> 4).toString(16) + (b & 0x0F).toString(16)).toUpperCase();
console.log(output);
i++;
}
}
function main(){
var c = [];
for(var i = 0; i < 200; i++){
c.push(new color( Math.floor(Math.random() * 255), Math.floor(Math.random() * 255), Math.floor(Math.random() * 255) ));
}
c.push(new color(0x11,0x22,0x33));
c.push(-1);
g_print(c);
}
main();
Yeah. And for those who are curious. Here's an explanation of the bitwise operations
>>
is the right-shift operation. a >> b shifts the bits in a right by b spaces. Hexadecimal numbers are 16 bits and 16 = 24, so 4 binary digits fit into 1 hex digit (F16 = 11112). Let's take a look at what happens when we take 4B (01001011) and bitshift it
4B16 >> 410 01001011 >>>>01001011 00000100 416
&
is the bitwise AND. It's pretty self explanatory, 1 & 1 = 1, 1 & 0 = 0, 0 & 0 = 0
. F16 (11112) = 1510.
4B & F 01001011 & 00001111 00001011 B16
As you can see, these isolate the digits so that they can be turned into base 16 representations of themselves. This would all be so much easier if JS just had printf()
The patterns for a gradient are pretty much linear and radial. Linear is self-explanatory. It's kinda what I just went over above. Radial gradients are gradients that radiate out from a center point. Like a circle.
In addition, gradients can also have multiple color stops, so you can make any wacky gradient you desire. Personally, I avoid gradients like the plague. I feel that solid colors look better, and gradients are far too easy to screw up. If it's not perfect, it'll look like crap. Solid colors, transparency, and drop shadows, when combined, make a great UI on their own, but for gradients, I prefer maximum subtlety.
Pretty much anything that can have a colored background can have a gradient. text fields, divs, spans, paragraphs, preformatted blocks, tables, canvases, you name it.
I'm sure you understood at least some of that. And if you understood the whole thing, then congratulations! You now know a lot more about colors.
Permanent Link to this page: http://bradenbest.com/tutorials/get_page.php?path=tuts%2Fother%2F&name=Gradient+Logic
Have an idea for a tutorial? Go to the Suggestion Box