To illustrate some of the basics of photo manipulation with canvas I'll use a short function I've called process that takes three arguments and is defined below.

When calling process, the first argument needs to be a handle for the source image you want to edit (a 24-bit RGB image). The second argument should be a handle for the canvas element on which to draw the final output; we'll give it dimensions that match the source image inside the process function. These handles for the input <img> and output <canvas> can be either vanilla JavaScript DOM selections (using querySelector, querySelectorAll, getElementById, getElementsByClassName or getElementsByTagName) or jQuery alternatives. If a selection consists of an array (or NodeList) of DOM elements (i.e. if it wasn't created using querySelector or getElementById) then only the first element, index 0, will be used.

The third argument to process should be a pixel-editing function, such as one of those described in detail in the examples below.

var process = function(source, canvas, func){
		
   //If the source/canvas selection contains > 1 element then select only the 1st
   //This allows for the use of jQuery selections or document.querySelectorAll
   if(source.length){source = source[0];}
   if(canvas.length){canvas = canvas[0];}
			
   //Get width and height attributes of source image and set canvas to match	
   var width = source.clientWidth;
   var height = source.clientHeight;	
   canvas.setAttribute("width",width);
   canvas.setAttribute("height",height);
		
   //Get the canvas context and copy over the source pixels
   var context = canvas.getContext("2d");
   context.drawImage(source,0,0);
		
   //Get all the canvas data
   var imageData = context.getImageData(0,0,width,height);
   var pixelData = imageData.data;
		
   //Each pixel is made up of four consecutive values in the pixels array (r, g, b, alpha) 
   var nPixels = pixelData.length/4;

   //Loop over each pixel, edit the data
   for(var i=0; i<nPixels; i++){
      var rPos = 4*i; //position of red channel value of ith pixel
      var gPos = rPos + 1; //position of green channel value of ith pixel
      var bPos = rPos + 2; //position of blue channel value of ith pixel
      var processed = func(pixelData[rPos], pixelData[gPos], pixelData[bPos]); 
      pixelData[rPos] = processed[0]; //new red-channel value
      pixelData[gPos] = processed[1]; //new green-channel value
      pixelData[bPos] = processed[2]; //new blue-channel value
   }
		
   //apply alterations to the canvas
   context.putImageData(imageData,0,0);
		
};

The code sets the width and height of the <canvas> element to that of the source element; this will actually clear any visual content that was already on the canvas(!). After that the canvas context is obtained, the source image is copied across to the canvas, and the image data is extracted: The call to getImageData returns an object that contains width and height variables (that we don't need here) and an array called data. The latter is a Uint8ClampedArray array holding all the red, green, blue and alpha (i.e. transparency) values of the image. This is encoded in a specific way: The first member of the array is the value for the red channel of the first pixel (in the top-left of the image). The second member is the green value for the same pixel. The third and fourth members of the array are the blue and alpha channel values, respectively. Then the fifth member is the value for the red channel of the second pixel (one pixel in from the top left) and so on across the row and then down. As such, the total number of pixels can be seen to be the length of this array divided by 4; the next step is to loop over each pixel and change the image data.

As noted, the func argument is the pixel manipulation function you supply. It is called once in each iteration of the for loop to modify the data for the corresponding pixel. It is passed the red, green and blue channel values of the current pixel and should return modified values for these as a three-element array (in the order red, green, blue). I chose not to modify the alpha data at all, but it shouldn't be a difficult task to extend the process function so that this too can be altered.

Finally, the changes are applied to the visible canvas using the call to putImageData.

The examples below use the process function to implement a number of fairly common photo-manipulation techniques. For three of the examples pairs of histograms are also shown. In most cases these illustrate the distribution of red, green and blue channel values from the corresponding image directly above (source <img> on the left, destination <canvas> on the right). For the grayscale canvas example, red, green and blue channels match so only one is shown (colored gray). Traditionally, histograms like these are used to assist photographers and photo-editors judge color balance. Here, however, their primary purpose is to help illustrate the effect the function being applied has on the underlying data. You can zoom in and out on a histogram by hovering over it and scrolling the mouse wheel. Alternatively, drag a rectangle to zoom in. When zoomed in, you can pan in any direction using the mouse while holding the shift key on the keyboard.

1) Invert

One of the most basic things you can do to a digital image is to invert each color channel to get a negative. (Use the drop-down menu to switch the source image on the left.)

With 255 being the maximum value for each channel (8-bits per channel allows each value to range from 0 to 28-1 = 255), the function passed to process and invoked for every pixel is incredibly simple:

var invert = function(red, green, blue){
   return [255-red, 255-green, 255-blue];	
};
	

2) Convert to Grayscale

A common image processing task is the conversion of a full-color image to a grayscale one. There are multiple options for doing this, as described in this excellent article by Tanner Helland. The example below implements many of those listed by Helland. The list below gives brief details of each method (see the article just mentioned for a more in-depth discussion). For each named method (in bold) we set the output red, green and blue values of a pixel equal to the…:

 

To implement this I used the code below which also covers the next example. Rather than passing the grayscale function to the process function, one invokes it with the name (as a string) of the relevant conversion method (and without defining a threshold). The returned function is then passed as the third argument of the process function.

var grayscale = function(method, threshold){
				 
   var grayFunc;				

   switch(method){
      case "luminance":
         grayFunc = function(r,g,b){return Math.round(r*0.299 + g*0.587 + b*0.114);};
         break; 
	  case "average":
         grayFunc = function(r,g,b){return Math.round(r + g + b)/3;};
         break;
      case "minimum":
         grayFunc = function(r,g,b){return Math.min(r,g,b);};
         break;				
      case "maximum":
         grayFunc = function(r,g,b){return Math.max(r,g,b);};
         break;
      case "red":
         grayFunc = function(r){return r;};
         break;
      case "green":
         grayFunc = function(r,g){return g;};
         break;
      case "blue":
         grayFunc = function(r,g,b){return b;};
         break;	
   }

   var out;
   
   if(typeof threshold !== "number"){ //grayscale image
      out = function(red, green, blue){
         var grayVal = grayFunc(red, green, blue);
         return [grayVal, grayVal, grayVal];
      };	
   } else{
      out = function(red, green, blue){ //black and white image
         var grayVal = grayFunc(red, green, blue);
         var bw = grayVal≥threshold ? 255 : 0;
         return [bw, bw, bw];	
      };
   }

   return out; 
};
	

3) Convert to Black and White

This example uses the code above but with a defined threshold in the call to grayscale to create a purely black and white image. In short, a representative grayscale value is found for each color pixel based on any of the grayscale conversion methods mentioned above. If the grayscale value for a pixel is less than the threshold then it is turned black on the canvas, otherwise it's turned white.

The threshold slider scale in the example below goes from 1 to 255. This means dragging the slider scale to the left end will turn all pixels white that aren't black after grayscale conversion. Dragging the slider to the right end means the reverse: all pixels that aren't white after grayscale conversion will be turned black.

4) Shift Color Channels

We could write a function that adds a single number to each channel-value of each pixel. If the number is positive the effect is to lighten the corresponding image. If the number is negative then the image will be darkened. (Having said that, it's worth noting that linear changes across all RGB channel values don't correspond to linear changes in perceived brightness.)

This is doable. But there's no actual requirement that we shift all channels in sync. We could add more to the red channel than the green and the blue. Or we could subtract from the red channel while increasing the blue and keeping the green channel the same. Or any of the (255×2+1)3 = 133,432,831 permutations. (Due to clamping, discussed below, some of the results of the different permutations may be indistinguishable.)

Again the pixel-wise code needed to implement this is straightforward:

var shift = function(redShift, greenShift, blueShift){
   return function(red, green, blue){
      return [red+redShift, green+greenShift, blue+blueShift];
   };
};

The outer function, shift, takes three values, one for each of the adjustments to be made to the separate channels. It is then the function that shift returns that can be passed to the process function and called for every pixel.

One thing this inner function doesn't do is check whether each new channel value falls in the required range of 0 to 255. There is actually no requirement to do this because the data is automatically clamped to this range when passed back in to the Uint8ClampedArray pixelData in the process function. (Even IE10, which uses a deprecated CanvasPixelArray object instead of a Uint8ClampedArray seems to do this correctly.)

The extent of clamping can be seen in the canvas histograms. For example, with the Sunset image visible, drag the Blue slider slightly to the left of the zero point. The blue curve doesn't change shape, it's just shifted a bit. However, pull the slider a larger distance to the left or right and the curve will shoot up at one end as the data is clamped. The original distribution can be restored by dragging the slider back to the center. This works because process always redraws the canvas image from the source image (even if you don't see it) before applying the shift function.

5) Quantize Colors

Color quantization is the process of transforming an image from one that contains many colors to one that contains much fewer. Today color quantization is most notably used for the creation of GIFs (and 8-bit PNG files) which are restricted to (at most) 256 24-bit colors.

There are a range of methods for selecting appropriate color palettes from which to recreate an image. From simply selecting the most commonly occurring colors, to applying the median cut algorithm or using octrees. For expediency, however, I'm going to demonstrate something much less useful (but I hope still interesting): using pre-defined palettes. With pre-defined palettes the results will generally be poor but the JavaScript implementation is straightforward. In the present case you can chose between four palettes:

The code for this example is shown below. As with the previous three examples, an outer function is used to generate an inner function that is passed to process. As well as creating the appropriate palette array, the outer function also creates a cache object to store results for future invocations of the inner function. This is particularly useful for the large web-safe palette.

When the inner function is called it checks whether cache already contains a key representing the current input red, green and blue values. If it does then the corresponding property holds the appropriate index in to the color palette and the output red, green and blue values can be swiftly returned. If it doesn't then the function must loop over all the colors in the palette to find the closest one. In this case the relevant metric is the Euclidean distance in RGB coordinates (or the square of that distance to be precise, since the final step of calculating the square root is an unnecessary computation). Once again it should be noted that this is not a perceptual color space and so this routine produces at best an approximate guess at what we might think of as the nearest palette color to input color.

var quantize = function(pal){
		
   var palette;
	
   switch(pal){
		
      case "simple":
         palette = [
            [0,0,0], [255,255,255], [255,0,0], [255,255,0],
            [0,255,0], [0,255,255], [0,0,255], [255,0,255]
         ];
         break;
			
      case "html3":
         palette = [
            [0,0,0], [128,128,128], [192,192,192], [255,255,255],
            [128,0,0], [255,0,0], [128,128,0], [255,255,0],
            [0,128,0], [0,255,0], [0,128,128], [0,255,255],
            [0,0,128], [0,0,255], [128,0,128], [255,0,255]
         ];
         break;
			
      case "websafe":
         (function(){
            var colVals = [0, 51, 102, 153, 204, 255];
            var nColVals = colVals.length;
            palette = [];
            for(var i=0; i<nColVals; i++){
               for(var j=0; j<nColVals; j++){
                  for(var k=0; k<nColVals; k++){
                     palette.push([colVals[i],colVals[j],colVals[k]]);
                  }
               }
            }
         })();
         break;
			
      case "pastel1":
         palette = [
            [251,180,174], [179,205,227], [204,235,197], [222,203,228],
            [254,217,166], [255,255,204], [229,216,189], [253,218,236],
            [242,242,242], [0,0,0], [255,255,255]
         ];
         break;				

   }
	
   var nCols = palette.length;
	
   var distFunc = function(col1, col2){
      var rsq = Math.pow(col1[0]-col2[0],2);
      var gsq = Math.pow(col1[1]-col2[1],2);
      var bsq = Math.pow(col1[2]-col2[2],2);
      return rsq + gsq + bsq;
   };
	
   var cache = {};
	

   return function(red, green, blue){
		
      var col1 = [red, green, blue];
      var colString = red + "," + green + "," + blue;
      var chosen = cache[colString];
		
      if(chosen === undefined){
         var minDistance = Infinity;
         for(var i=0; i<nCols; i++){
            var col2 = palette[i];
            var distance = distFunc(col1,col2);
            if(distance<minDistance){
               minDistance = distance;
               chosen = i;
            }	
         }
         cache[colString] = chosen;
      }

      return palette[chosen];
				
   };
		
};

Limitations

I hope you find all the examples used here relatively intuitive and see that the barrier for simple image processing using JavaScript and <canvas> is fairly low. As is almost always the case with JavaScript, there are a few "quirky" issues that are worth a quick mention.

Obviously, since it's a newer addition, not every single browser supports the <canvas> element. However, support is now very good across all major browsers. (If you still need to support Internet Explorer 8 and earlier you may have more difficulty.)

Using <canvas> elements in responsive web pages can also be a bit fiddly; as previously noted, changing the elements width and/or height attributes clears the canvas! You must also make sure the source image is fully loaded before trying to copy it across to the canvas. If it isn't all the "copied" pixels will be "transparent black".

I deliberately picked image-processing examples where one can look at pixels independently of each other (aside from referencing the cache in this last example for efficiency). More complex procedures that are not restricted in this manner are certainly plausible. In the case of color quantization, for example, one could create dynamic palettes based on image data and use dithering to improve the appearance of the final image.

Try our jQuery HTML5 controls for your web apps and take immediate advantage of their stunning data visualization capabilities. Download Free Trial now!