One of the best features of Photoshop is the layer blending modes. They allow you to create spectacular effects with little to no effort. I’ve been taking a closer look into how Photoshop handles the blending of layers (especially the math side of it).
Each of the blending functions (modes) takes two inputs (a top layer, top in the math, and a bottom layer, bottom in the math) and yields an output (or product, r in the math).
Below are the layers I used to test:
You’ll notice a little variance between the output (the gradients do not match up perfectly). I hard coded the rainbow gradient and the black to white gradient with code (I’m still not sure why I chose this approach) so there is a small amount of variance.
Some of the functions isolate individual channels (r = 0xff, g = 0xff and b = 0xff) while others deal with the color a whole (rgb = 0xffffff). I will specify which functions use just the channels and which use the whole color integer.
Some of the functions require the channel values to be normalized—meaning that rather than being a value from 0-255, it must be a number between 0-1. Here is a great normalizing function:
|
1 2 3 |
public static double normalize ( int col ) { return col / 255.0; } |
It’s worth mentioning that the following equations are not spot on with Adobe’s (since I do not have the source code for Photoshop xD) but they at least seem to get the job done. For those who are just here for the math or the code here is a table summarizing all the functions:
Edits or paints each pixel to make it the result color. This is the default mode. (Normal mode is called Threshold when you’re working with a bitmapped or indexed-color image.)1
So this is the normal blending mode. It’s pretty boring and self explanatory (if you really need an explanation, see me in the comments).
r = top
|
1 2 3 |
public static int normal ( int top, int bottom ) { return top; } |
Edits or paints each pixel to make it the result color. However, the result color is a random replacement of the pixels with the base color or the blend color, depending on the opacity at any pixel location.
This will be covered… eventually.
Looks at the color information in each channel and selects the base or blend color—whichever is darker—as the result color. Pixels lighter than the blend color are replaced, and pixels darker than the blend color do not change.
Returns the darker (smaller) of the two colors.
if top > bottom
r = bottom
else
r = top
|
1 2 3 4 5 |
public static int darken ( int top, int bottom ) { return ( top > bottom ) ? bottom : top; } |
Looks at the color information in each channel and multiplies the base color by the blend color. The result color is always a darker color. Multiplying any color with black produces black. Multiplying any color with white leaves the color unchanged. When you’re painting with a color other than black or white, successive strokes with a painting tool produce progressively darker colors. The effect is similar to drawing on the image with multiple marking pens.

As the name implies, it multiplies the two colors together. The result is never brighter than the original color.
r = top × bottom
The math on this one assumes that the individual channels have been normalized (changed from a value between 0-255 to a number between 0-1). If normalized Rwould then have to be multiplied by 255 to be used in code.
There are a couple of options with this one, you could demand that the colors be sent to the function as normalized values, normalize them within the function or just divide by 255 (0xff).
|
1 2 3 |
public static int multiply ( int top, int bottom ) { return top * bottom / 0xff; } |
|
1 2 3 |
public static int multiply ( int top, int bottom ) { return normalize( top ) * normalize( bottom ) * 0xff; } |
Personally I think the second function is a bit too redundant.
Looks at the color information in each channel and darkens the base color to reflect the blend color by increasing the contrast between the two. Blending with white produces no change.
The resulting color is a darkening of the top layer to reflect the bottom color by increasing the contrast. As you can see in the preceding image, white in the top layer does not affect the bottom color (the image looks like the bottom layer toward the bottom).
r = 1 – (1 – bottom) / top
|
1 2 3 4 5 |
public static int channelColorBurn ( int top, int bottom ) { if ( top == 0 ) return 0; // We don't want to divide by zero int col = (int) ( 255 * ( 1 - ( 1 - normalize( bottom ) ) / normalize( top ) ) ); return ( col < 0 ) ? 0 : col; } |
Looks at the color information in each channel and darkens the base color to reflect the blend color by decreasing the brightness. Blending with white produces no change.
This takes the sum of both layers and subtracts the color white (0xff or 1 if normalized).
r = top + bottom – 1
|
1 2 3 |
public static int linearBurn ( int top, int bottom ) { return ( top + bottom < 255 ) ? 0 : top + bottom - 255; } |
Compares the total of all channel values for the blend and base color and displays the lower value color. Darker Color does not produce a third color, which can result from the Darken blend, because it chooses the lowest channel values from both the base and the blend color to create the result color.
My current implementation of this is buggy.
Looks at the color information in each channel and selects the base or blend color—whichever is lighter—as the result color. Pixels darker than the blend color are replaced, and pixels lighter than the blend color do not change.
Lighten returns the lighter (closer to 0xff) of the two colors (opposite of darken).
if top > bottom
r = top
else
r = bottom
|
1 2 3 |
public static int lighten ( int top, int bottom ) { return ( top > bottom ) ? top : bottom; } |
Looks at each channel’s color information and multiplies the inverse of the blend and base colors. The result color is always a lighter color. Screening with black leaves the color unchanged. Screening with white produces white. The effect is similar to projecting multiple photographic slides on top of each other.
Multiplies the inverse of both layers
r = (1 – top) * (1 – bottom)
|
1 2 3 |
public static int screen ( int blend, int base ) { return (int) Math.round( 255 * ( ( 1 - normalize( base ) * ( 1 - normalize( blend ) ) ) ) ); } |
Looks at the color information in each channel and brightens the base color to reflect the blend color by decreasing contrast between the two. Blending with black produces no change.
r = bottom / (1 – top)
|
1 2 3 4 5 |
public static int colorDodge ( int top, int bottom ) { if ( top == 0xff ) return 0xff; int col = (int) Math.round( ( normalize( bottom ) / ( 1 - normalize( top ) ) ) * 255 ); return ( col > 0xff ) ? 0xff : col; } |
Looks at the color information in each channel and brightens the base color to reflect the blend color by increasing the brightness. Blending with black produces no change.
r = top + bottom
|
1 2 3 4 5 6 7 |
public static int add ( int top, int bottom ) { return ( top + bottom > 255 ) ? 0xff : top + bottom; } public static int linearDodge ( int top, int bottom ) { return add( top, bottom ); } |
Compares the total of all channel values for the blend and base color and displays the higher value color. Lighter Color does not produce a third color, which can result from the Lighten blend, because it chooses the highest channel values from both the base and blend color to create the result color.
This, like Darker Color, is buggy.
Multiplies or screens the colors, depending on the base color. Patterns or colors overlay the existing pixels while preserving the highlights and shadows of the base color. The base color is not replaced, but mixed with the blend color to reflect the lightness or darkness of the original color.
if bottom < 1/2
r = 2 × top × bottom
else
r = 1 – 2 ×(1 – top) × (1 – bottom)
|
1 2 3 4 5 6 |
public static int overlay ( int top, int bottom ) { // NOTE: this formula seem to lose some of the fidelity. :( return ( bottom < 128 ) ? (int) ( 2 * top * bottom / 255 ) : (int) ( 255 * ( 1 - 2 * ( 1 - normalize( top ) ) * ( 1 - normalize( bottom ) ) ) ); } |
Darkens or lightens the colors, depending on the blend color. The effect is similar to shining a diffused spotlight on the image. If the blend color (light source) is lighter than 50% gray, the image is lightened as if it were dodged. If the blend color is darker than 50% gray, the image is darkened as if it were burned in. Painting with pure black or white produces a distinctly darker or lighter area, but does not result in pure black or white.
r = (1 – top) * top * bottom + top * (1 – (1 – top) * (1 – bottom))
This one is more complicated that most, so I normalized the layers first and then put them into the return statement.
|
1 2 3 4 5 |
public static int channelSoftLight( int top, int bottom ) { double a = normalize( top ); double b = normalize( bottom ); return (int) ( Math.round( 255 * ( ( 1 - a ) * a * b + a * ( 1 - ( 1 - a ) * ( 1 - b ) ) ) ) ); } |
Multiplies or screens the colors, depending on the blend color. The effect is similar to shining a harsh spotlight on the image. If the blend color (light source) is lighter than 50% gray, the image is lightened, as if it were screened. This is useful for adding highlights to an image. If the blend color is darker than 50% gray, the image is darkened, as if it were multiplied. This is useful for adding shadows to an image. Painting with pure black or white results in pure black or white.
if bottom < 1/2
r = 2 × top × bottom
else
r = 1 – 2 × (1 – top) × (1 – bottom)
|
1 2 3 4 5 6 7 8 9 |
public static int hardLight ( int top, int bottom ) { double a = normalize( top ); double b = normalize( bottom ); if ( b < 0.5 ) { return (int) Math.round( 255 * 2 * a * b ); } else { return (int) Math.round( 255 * ( 1 - 2 * ( 1 - a ) * ( 1 - b ) ) ); } } |
Burns or dodges the colors by increasing or decreasing the contrast, depending on the blend color. If the blend color (light source) is lighter than 50% gray, the image is lightened by decreasing the contrast. If the blend color is darker than 50% gray, the image is darkened by increasing the contrast.
if top < 1/2
r = 1 – (1 – bottom) / 2 * top
else
r = bottom / (1 – 2 * (top – 1/2))
|
1 2 3 4 5 6 7 |
public static int vividLight ( int top, int bottom ) { if ( top < 128 ) { return colorBurn( 2 * top, bottom ); } else { return colorDodge( 2 * ( top - 128 ) , bottom ); } } |
Burns or dodges the colors by decreasing or increasing the brightness, depending on the blend color. If the blend color (light source) is lighter than 50% gray, the image is lightened by increasing the brightness. If the blend color is darker than 50% gray, the image is darkened by decreasing the brightness.
|
1 2 3 4 5 6 7 |
public static int linearLight ( int top, int bottom ) { if ( top < 128 ) { return linearBurn( 2 * top, bottom ); } else { return linearDodge( 2 * ( top - 128 ), bottom ); } } |
Replaces the colors, depending on the blend color. If the blend color (light source) is lighter than 50% gray, pixels darker than the blend color are replaced, and pixels lighter than the blend color do not change. If the blend color is darker than 50% gray, pixels lighter than the blend color are replaced, and pixels darker than the blend color do not change. This is useful for adding special effects to an image.
|
1 2 3 4 5 |
public static int pinLight ( int top, int bottom ) { return ( top < 128 ) ? darken( 2 * top, bottom ) : lighten( 2 * ( top - 128 ), bottom ); } |
Adds the red, green and blue channel values of the blend color to the RGB values of the base color. If the resulting sum for a channel is 255 or greater, it receives a value of 255; if less than 255, a value of 0. Therefore, all blended pixels have red, green, and blue channel values of either 0 or 255. This changes all pixels to primary additive colors (red, green, or blue), white, or black.
|
1 2 3 |
public static int hardMix ( int blend, int base ) { return ( blend < 255 - base ) ? 0 : 255; } |
Looks at the color information in each channel and subtracts either the blend color from the base color or the base color from the blend color, depending on which has the greater brightness value. Blending with white inverts the base color values; blending with black produces no change.
r = |top – bottom|
|
1 2 3 |
public static int difference ( int top, int bottom ) { return Math.abs( top - bottom ); } |
Creates an effect similar to but lower in contrast than the Difference mode. Blending with white inverts the base color values. Blending with black produces no change.
r = top + bottom – 2 × top × bottom
|
1 2 3 |
public static int exclusion( int top, int bottom ) { return (int) Math.round( top + bottom - 2 * top * bottom / 255.0 ); } |
Looks at the color information in each channel and subtracts the blend color from the base color. In 8- and 16-bit images, any resulting negative values are clipped to zero.
Looks at the color information in each channel and divides the blend color from the base color.
Hi!
Thanks for this post! I was looking for how the overlay works and the explanation published by Adobe didn’t really helped me out!
You saved me a lot of time!
I was looking for this kind of layouts.You helped me a lot. I will recommend it to all my friends. You might be interested in http://www.youtube.com/user/GuruBix Also are you arranging any seminars on layouts.When will be the your next seminar
I post pretty infrequently. I will be doing a post on blurring in the near future.
Wonderful Kevin!… just a help by me: Darker Color is a comparison of the GRAY value of the entire pixel in each layer and it wins the darker gray, so its relative color… same for Lighter Color but obviously inverse behaviour and choice… See you soon!
Very nicely laid out reference.
Only problem I see is that you’re missing a factor in the Screen mixing calculation.
Assuming 1 is white, if you Screen mix a white layer over another white layer, the math as you’ve listed it above will yield 0 (black). In reality that’s not the case.
The whole equation should be, I believe, for Screen:
r = 1 – ( (1 – top) * (1 – bottom) )
I see what you mean. I’ll do some testing and let you know what I find out.