Image Blend modes and HTML Canvas
Image Blend modes are the methods used to determine how 2 image layers are blended (mixed) to each other. As the digital images are stored in pixel, which are represented by numerical values, there are a large number of possible ways for blending based on the mathematical functions. With the help of Canvas API, now we can easily retrieve the images, export all the pixels on the image, apply blending effect, calculate to get the new blended pixels, export and display the new image on the web or save to server.
In this post, I will demonstrate some basic steps for applying Image Blend modes using HTML canvas API.
The Logic
Wikipedia already listed some popular blending methods here Blend Modes.
Assume that we have two image layers, top layer and bottom layer. For each loop, a is the value of a color channel in the underlying layer and b is that of the corresponding channel of the upper layer, we got the function for calculating the new pixel ƒ(a, b).
The steps for generating the new blended image from the two images are
- First, retrieve those two images
- Create two separate canvases with the same size and draw those two images into the corresponding canvas.
- Get all the pixel data from the two canvases.
- Loop through each pixel, apply the blending function to create a new pixel and store it inside an array
- Create a new canvas with the same size, use the blended pixel data to draw the blended image on that new canvas
- Export the canvas to image, file to save to server or to local computer.
Note: I mentioned that we need to create two canvases with the same size. However, that is just to make it easy for the demonstration purpose. You can still generate two canvases with different size, but you will need to change the calculation function a bit.
Retrieve the Images
Firstly, you need to fetch the images from outside to the memory for processing. The images can be either from the internet, a data URI string or even from local file (if the File API is supported in your browser).
var bottomImage, topImage;
bottomImage.onload = function() {
topImage.onload = function(){
// finish loading two images here, call the function to process the data
drawImagesOnCanvas(bottomImage, topImage);
};
topImage.src = 'http://example.com/img2.png';
};
bottomImage.src = 'http://example.com/img1.png';
In the above example, I used the normal callback to load the images so it looks a bit ugly. Try to eliminate the pyramid by yourself :D
Instead of loading images from an URL, you can also load them from a data url string, for example
img1.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAkIAAADICAYAAAAEE46XAAAgAElEQVR4Xu2dXZBU1bXH17zFWxUrkreBqqhgBb0aNXGIL1dQo';
If your browser supports File API, you can also load the image from the files on local computer using URL.createObjectURL. See this page for example URL.createObjectURL.
Draw Images and Get Pixel Data
Now, we will create two canvas elements using the normal document.createElement function. You only need to create the two canvases and they are not necessarily be appended to the web page to work properly (actually you shouldn’t append them).
function drawImagesOnCanvas(bottomImage, topImage){
// create canvases
var bottomCanvas = document.createElement('canvas');
var topCanvas = document.createElement('canvas');
// set the size for them, for example 500x500 pixels
bottomCanvas.width = 500;
bottomCanvas.height = 500;
topCanvas.width = 500;
topCanvas.height = 500;
// more will be presented later
...
}
Next, draw the images that we have loaded before to those canvases. In order to
do that, you need to get the 2d context from the canvases. After finishing
drawing, get the image data (represent the pixels information of the canvas)
using the getImageData
method of the 2d context.
function drawImagesOnCanvas(bottomImage, topImage){
// continue from the above
...
// get the 2d context to draw
var bottomCtx = bottomCanvas.getContext('2d');
var topCtx = topCanvas.getContext('2d');
// draw the image to top and bottom canvas, from the position x, y (0, 0) and
// with the size 500, 500 pixels
bottomCtx.drawImage(bottomImage, 0, 0, 500, 500);
topCtx.drawImage(topImage, 0, 0, 500, 500);
// get the pixel data of the 2 canvas, from the position x, y (0, 0) and
// with the size 500, 500 pixels
var bottomImageData = bottomCtx.getImageData(0, 0, 500, 500);
var topImageData = topCtx.getImageData(0, 0, 500, 500);
// apply blending, will be discussed in the next section
applyBlending(bottomImageData, topImageData);
}
You can read this page for more information about the parameter passed to
drawImage
and getImageData
function.
Apply Blending method
Now, you need to create a new canvas for processing the new image. Again, you don’t necessarily need to append this canvas to the document’s body.
function applyBlending(bottomImageData, topImageData) {
// create the canvas
var canvas = document.createElement('canvas');
canvas.width = 500;
canvas.height = 500;
var ctx = canvas.getContext('2d');
// will be more
...
}
Next, loop through the pixel data and apply the desired blending function to generate the new pixel data and then draw it on on the new canvas. The pixel data array is a little special. In this example, our image size is 500x500 pixel, 250,000 pixels in total. However, the pixel data array will have 250,000 x 4 elements. This is because each pixel is represented by 4 value: (R, G, B, A), 4 continuous elements in the pixel data array.
The demonstration uses the multiply blending method with the formula
ƒ(a, b) = a*b
. That means the new R value is (Ra * Rb) / 255
. The same logic
will be applied for other color value.
function applyBlending(bottomImageData, topImageData) {
// continue from the above
...
// get the pixel data as array
var bottomData = bottomImageData.data;
var topData = topImageData.data;
// loop each pixel data, calculate the new pixel value and assign it directly
// to the topData (to save memory)
// if you want to keep the original data, don't do this. instead create a new
// image data object
for(var i = 0; i < topData.length; i += 4) {
topData[i] = topData[i] * bottomData[i] / 255;
topData[i+1] = topData[i+1] * bottomData[i+1] / 255;
topData[i+2] = topData[i+2] * bottomData[i+2] / 255;
}
// draw it on the canvas with the size 500, 500
ctx.putImageData(topImageData, 500, 500);
// export image, discussed in the next part
exportImage(canvas);
}
Export the Blended Image
You have successfully apply Blend mode using HTML Canvas (multiply for specific in this case). However, the canvas is not visible yet and you still cannot see the result. The final step is to transform the hidden canvas to a visible item. There are many ways of doing this, select the one that is most suitable for you
function exportImage(canvas) {
// append it to the document's body to see
document.body.appendChild(canvas);
// or export the base64 data url string
var dataUrl = canvas.toDataURL();
// on modern browser, you can just print the data url, open the console and
// click on it, the browser will generate the image for you
console.log(dataUrl);
// or create a new image element from the data url
var img = document.createElement('img');
img.setAttribute('src', dataUrl);
img.onload = function(){
document.body.appendChild(img);
};
// if you don't want to submit the base64 string and convert it back
// to image on the server, you can convert the data url to a file and then
// submit to server (if your browser supports File API)
var binary = atob(dataUrl.split(',')[1]);
var array = [];
for(var i = 0; i < binary.length; i++) {
array.push(binary.charCodeAt(i));
}
var file = new Blob([new Uint8Array(array)], {type: 'image/png'});
// use the FormData to submit it (with jquery or XMLHttpRequest)
}
Demo
This is created using the above example. Here are the two original images.


After applying blending method for these 2 images, I got this image. Note that I used multiply blend mode for this example, so any transparent pixel on either top or bottom will produce the corresponding transparent pixel on the final image (since all the RGB value is 0, and anything times 0 is 0). That’s why you see the below image got transparent at the two sides

Finally, I use this overlay image with transprent center region (the region inside the bag)…

… and draw it over the blended image, I got this one, notice the top left of the bag and text in the middle of the bag
