It’s hard to identify universally accepted rules in web design, but if there is one that the whole community agrees upon, it’s that you should always separate your content from its style.
On almost every webpage, data is output as HTML and style rules are applied using CSS. Nobody mixes data and style in the same file anymore…
…Except you. And me. In fact we all do it, everyday. Almost every image you see online has been cropped, twisted, styled, toned and resized to the point that it’s impossible to separate the original data from the subsequent styling.
Wouldn’t it be great if there was a way to apply style information to images, without permanently affecting the data, just like we do with text? Today, we’re going to build a simple jQuery plugin to do just that.
WHAT’S THE PROBLEM?
Millions of images across the web have been manipulated in applications like Photoshop to fit a style — one of the current trends is the vintage look inspired by mobile apps like Instagram and Hipstamatic. But what happens when the trend ends, as all trends inevitably do? We’re left scratching around for our original .tiff files so we can restyle them. That’s a particularly large problem for blogs, where a single site might be hosting thousands of images.
More importantly, separating data from style means the same data can be presented in different ways depending on context. A single image file can be cropped, re-toned or sharpened for one purpose, and enlarged, blurred or desaturated for another.
The ubiquitous nature of CSS is testament to web design’s core-philosophy of separating style from content. But until recently, there were very few options when it came to separating image style from image content.
Amongst the new structure, the new tags and the AV capabilities a tantalizing glimpse of the future crept into the HTML5 specification: a drawing option for JavaScript named Canvas. Despite its recent arrival and relatively few features, canvas is already an extremely powerful option for styling images.
BEFORE WE START
Unlike a lot of new technologies you can use canvas today: Chrome 19+, Safari 5.1+ Firefox 3.6+, Opera 12+, iOS Safari 3.2+, Android 2.1+ and even IE 9+ have full support. (Opera mini 5+ lets the side down with only partial support.)
Note however that Chrome’s security model is going to throw an error if you try to test locally, so if you prefer to use Chrome you’ll need to test this project online.
If you prefer your learning to be of the copy and paste variety, you can download the source code for each of the three steps at the start of each section.
BUILDING A JQUERY PLUGIN
The first thing we’re going to need is an image to manipulate. I’ve picked this great image from Shutterstock.
SETTING UP AN HTML5 PAGE
Our next job is to set up a basic HTML5 page, we’re going to add an instance of our image, then we’re going to give the image the class popart, this is so we can refer to it using jQuery.
We need to import the jQuery library from Google and our jQuery plugin (which we haven’t written yet but will be called jquery.popart.js). We’re referring to the plugin as ‘popart’ because it fits nicely with the defaults we’ll set up, but you could actually call it anything.
Lastly we need to write a small jQuery script in the head of the document to select any image with the class popart and apply the popart() method to that image.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Canvas demo</title>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script type="text/javascript" src="jquery.popart.js"></script>
<script type="text/javascript">
$(document).ready(function(){
$("img.popart").popart();
});
</script>
</head>
<body>
<img class="popart" src="image.jpg" width="600" height="800" alt="Cowgirl" />
</body>
</html>
SETTING UP THE JQUERY PLUGIN
Now we have our HTML page setup we need to set up the jQuery plugin that will style our image.
Why are we writing a jQuery plugin? Because plugin format is simply a convenient way to apply the style (and one day, remove it again).
We’re not going to cover the finer points of jQuery plugin development because that isn’t the primary focus of this article, but if you’d like to read more you can’t start in a better place than the jQuery documentation on writing plugins.
Here’s what your jquery.popart.js file should look like:
(function($)
{
$.fn.popart = function(p)
{
var defaults = {
};
var params = $.extend(defaults, p);
return this.each(function()
{
});
};
})(jQuery)
Save the file as jquery.popart.js in the same folder as your .html file.
Normally you’d save the plugin code in a different folder named js or scripts or something similar, we’re not doing so here for the sake of simplicity but if you prefer, save it in its own directory, just remember to update the reference to the file in your html page.
STEP 1: CROPPING SIZE USING CANVAS
We’re going to cover several basic canvas concepts, the first is cropping the size of your image.
It won’t always be best-practice to crop an image with canvas: you’re loading more data than if you crop the image in something like Photoshop. However, there are lots of occasions when cropping dynamically is worthwhile. Imagine you have a single image that you want to reuse in different aspect ratios throughout your site, in those circumstances loading a single image from the cache and cropping it dynamically is preferable in every way to loading multiple images cropped in Photoshop.
The additions to the jquery.popart.js file that will crop the image are highlighted in bold:
(function($)
{
$.fn.popart = function(p)
{
var defaults = {
<strong>ratio:1</strong>
};
var params = $.extend(defaults, p);
return this.each(function()
{
<strong>$(this).load(function(){ var canvas = document.createElement("canvas"); if(canvas.getContext) { var image = new Image(); image.src = this.src; canvas.width = image.width; canvas.height = image.width * params.ratio; var context = canvas.getContext('2d'); context.drawImage(image, 0, 0); this.src = canvas.toDataURL(); $(this) .unbind('load') .attr('width', canvas.width) .attr('height', canvas.height); } });</strong>
});
};
})(jQuery)
The first thing we’ve done is add a ratio parameter to the defaults object. The way parameters work in jQuery plugins is to use the $.extend() method to merge an object (in this case our defaults object) with an object passed to the plugin (in this case represented by the p variable). We’re not passing in any values yet, so the values of the defaults object will be used.
We’re specifying a ratio to allow us to control the aspect ratio of the image. An aspect ratio of greater than 1 will produce a portrait format, a value of less than 1 will produce a landscape format and 1 will produce a square image.
The return this.each(function() { } block runs for every image we apply the plugin to.
The first thing we need to do is use the load method to wait for the image to load, we can’t manipulate an image until it’s loaded.
Next, we create a canvas element and test to see if that canvas element has a method called getContext. If that test returns false, then canvas isn’t supported, we only want to proceed if we know we’re using a browser that is compatible.
Next we create a new Image instance and we set its source to match the source of the image we’re styling.
Then we set the width and height of the canvas element, using the image’s width and the image’s width multiplied by the ratio parameter.
Next we retrieve the context from the canvas instance. The getContext(‘2d’) method is a complex method that hasn’t been fully utilized in the current version of canvas, but expect it to figure heavily in future revisions. For our purposes, all you need to know is we’re specifying a 2D image with a width and height (but no depth) and the origin of the image is the top left corner.
Setting the src of the image using the canvas’ toDataURL() method simply replaces the original src of our image with the data we’ve been manipulating.
Lastly, we perform two vital functions: we use jQuery’s unbind method to remove the load function (which prevents the script running twice when new data is entered in the image, as it would in certain browsers) then we correct the width and height attributes of our image so that they match the new pixel dimensions we’ve just set.
Here’s the result:
I think you’ll agree that that is an awful lot of work for such a small change. However, what we have achieved is setting up our files with all the major elements necessary to get a lot more ambitious with our styling in step 2.
STEP 2: ALTERING COLOR WITH CANVAS
Now that we have our canvas set up, tweaking the color couldn’t be simpler.
Every single pixel in a bitmap image is made up of four channels (red, green, blue and alpha) with values ranging from 0 to 255. If we can access that data, and manipulate it, we can change the bitmap in any way we see fit. This is essentially how Photoshop filters operate, but we’re going to do it live in the browser!
First we want to add a new parameter named color, which is an object with properties representing red, green and blue channels. We’re going to specify a value to multiply the existing r, g and b values by. So for example, if a pixel has a red channel value of 184 and we set the r property to 1.1 the red channel for that pixel will be modified to 202.
var defaults = {
ratio:1<strong>, color:{r:1.5, g:1.3, b:1.1}</strong>
};
The rest of the changes to the script lie between retrieving the context and setting the image src:
context.drawImage(image, 0, 0);
<strong>var pixels = context.getImageData(0, 0, canvas.width, canvas.height); for(var currentValue = 0; currentValue < pixels.width * pixels.height * 4; currentValue += 4) { var r = pixels.data[currentValue]; var g = pixels.data[currentValue + 1]; var b = pixels.data[currentValue + 2]; r = r * params.color.r; g = g * params.color.g; b = b * params.color.b; pixels.data[currentValue] = r; pixels.data[currentValue + 1] = g; pixels.data[currentValue + 2] = b; } context.putImageData(pixels, 0, 0, 0, 0, pixels.width, pixels.height);</strong>
this.src = canvas.toDataURL();
In order to manipulate the image’s color we need to access its pixels, so the first thing we do is grab the image data out of the context variable. (The parameters of the getImageData are x, y, width and height, we want to cover the whole image so we start in the top left and find the full size of the canvas.)
What follows is a key principle of working with pixel-level data in canvas: the data we have just pulled is a single array made up of groups of four values. The first value is the red channel of the first pixel, the second is the green value, the third is the blue and the fourth is the alpha channel. The fifth value is the red channel of the second pixel and so on.
Remember that arrays start at zero, so the value at 0 is the first pixel’s red channel, the value at 41 is the tenth pixel’s green channel etc.
To find the total number of pixels in an image we multiply the width by the height, to find the total number of entries in the array we multiply the result by 4.
In this for loop we start at 0, which is the first pixel’s red channel value and we jump in steps of 4. So that every iteration of the loop the currentValue is equal to a pixel’s red channel. We can then find the green and blue channels by adding 1 and 2 to the currentValue.
Note that it’s entirely possible to loop though every value in the array but this is a processor intensive loop, so by moving in steps of 4, we cut the number of loops by 75%.
Once we’ve found each channel we multiply them by the corresponding properties in the color object, then we reinsert the values into the array.
Finally, outside the for loop (so that it runs once) we slot the pixel data back into the context instance.
Save the file and refresh the browser and our pin-up is fit for the 60s:
STEP 3: ADDING SOME CURVES
A key characteristic of 60s photos is their shape. Standardization hadn’t taken hold back in the day and photographers often printed and framed photographs in a manner reminiscent of society portrait painting from earlier centuries.
We could crop our image to an interesting shape by running through the array and tweaking the alpha channel (the fourth value) but that’s a whole other article.
For now, we’re going to use a CSS3 property, set by jQuery to round up the corners into a circle.
First, add a new parameter to specify how much curve to add to the corners:
var defaults = {
ratio:1,
color:{r:1.5, g:1.3, b:1.1}<strong>,
curve:1.2</strong>
};
border-radius is a very well supported CSS3 property but it does have marginally less support that canvas, so we need to add it three times, once with the webkit prefix, once with the mozilla and lastly, as the W3C intended.
We’re setting the value after the width and height attributes, by finding half the length of the width and multiplying that by the curve parameter:
$(this)
.unbind('load')
.attr('width', canvas.width)
.attr('height', canvas.height)
<strong>.css("-webkit-border-radius", (canvas.width / 2) * params.curve + "px") .css("-moz-border-radius", (canvas.width / 2) * params.curve + "px") .css("border-radius", (canvas.width / 2) * params.curve + "px");</strong>
And here’s the final result:
MODIFYING THE EFFECT VIA PARAMETERS
Throughout this article I’ve complicated the issue by insisting on storing values in the parameters.
The reason I did so, was to make the plugin a little more flexible. Now that we’ve done so, you can modify the effect the plugin has by passing different values to the plugin in the script in your HTML page, like this:
$("img.popart").popart({ratio:1.5});
Or this:
$("img.popart").popart({color:{r:2.4, g:1.9, b:-0.3}});
Or this:
$("img.popart").popart(ratio:0.5, curve:0.7);
CONCLUSION
Canvas is still in its infancy, but the simple techniques we’ve covered in this article and the pixel-level control that you have make it a very powerful tool right now.
The ability to dynamically change the rgba of any pixel in an image means that literally any bitmap effect can be achieved, live in the browser.
We’re all convinced of the need to separate style from content, but should that apply to images as well as text? Will canvas ever replace the need for Photoshop? Let us know what you think in the comments.