We're not going to go wild with it, there just isn't time, but we'll be seeing how easy it is to do things like rotating, resizing, translating and even subtle color manipulation. Don't kid yourselves that we'll finish up with an equal to Photoshop, although that is in theory possible, but considering we're working within the confines of nothing more complex than a browser, personally I still think it's pretty remarkable.
This tutorial includes a screencast available for Tuts+ Premium members.
What you'll need for this tutorial
To produce a working version of the demo locally you'll need to use a Webkit-based browser such as Safari or Chrome, or Opera. The demo will work in Firefox, but it will need to be run through a web-server for most of the functionality to work. Don't even think about using IE; only version 9 even approaches support for the canvas element, and to be honest, I wouldn't even trust IE9 to render the code and functionality correctly.
Getting started
The underlying HTML is really quite trivial; all we need for the structure of the editor are the following basic elements:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Canvas Image Editor</title> <link rel="stylesheet" href="image-editor.css"> <link rel="stylesheet" href="ui-lightness/jquery-ui-1.8.7.custom.css"> </head> <body> <div id="imageEditor"> <section id="editorContainer"> <canvas id="editor" width="480" height="480"></canvas> </section> <section id="toolbar"> <a id="save" href="#" title="Save">Save</a> <a id="rotateL" href="#" title="Rotate left">Rotate Left</a> <a id="rotateR" href="#" title="Rotate right">Rotate Right</a> <a id="resize" href="#" title="Resize">Resize</a> <a id="greyscale" href="#" title="Convert to grayscale">B&W</a> <a id="sepia" href="#" title="Convert to sepia tone">Sepia</a> </section> </div> <script src="jquery-1.4.4.min.js"></script> <script src="jquery-ui-1.8.7.custom.min.js"></script> <script> </script> </body> </html>
Save the page as image-editor.html. Aside from the standard HTML elements making up the skeleton of the page we've got a custom style sheet, which we'll add in just a moment, and a style sheet provided by jQuery UI. At the bottom of the page, just before the closing </body> tag, we've got a reference to jQuery (current version at the time of writing is 1.4.4), a reference to jQuery UI (current version 1.8.7) and an empty script tag into which we'll put the code that gives the editor its functionality.
The jQuery UI components we'll use in this example are resizable and dialog, and the theme is ui-lightness.
The visible elements on the page are quite basic; we have an outer containing <div> element, within which reside two <section> elements. The first contains the <canvas> element that we'll use to manipulate our image. The second contains a toolbar of buttons that will be used to do the manipulations. From the id attributes given to each button it should be fairly obvious what each button does.
Adding the styles
Like the HTML, the CSS used is extremely simple, and consists of the following:
#imageEditor { width:482px; margin:auto; padding:20px; border:1px solid # 4b4b4b; -moz-border-radius:8px; -webkit-border-radius:8px; border-radius:8px; background-color:#ababab; } #editorContainer { display:block; width:480px; height:480px; } #editor { display:block; margin:0 20px 20px 0; border:1px solid # 4b4b4b; } #toolbar { display:block; margin:20px 0 0; } #toolbar a { margin-right:10px; outline:none; color:#4b4b4b; } #resizer { border:2px dashed #000; } #tip { padding:5px; margin:0; border:1px solid #000; -moz-border-radius:3px; -webkit-border-radius:3px; border-radius:3px; position:absolute; background-color:#fff; background-color:rgba(255,255,255,.3); -moz-box-shadow:1px 1px 1px rgba(0,0,0,0.5); -webkit-box-shadow:1px 1px 1px rgba(0,0,0,0.5); box-shadow:1px 1px 1px rgba(0,0,0,0.5); }
Save this as image-editor.css in the same directory as the HTML page. There's nothing truly remarkable here, mostly the styles layout the editor and its constituent elements in the manner illustrated in the below screenshot:
Full Screencast
The fun part
All that's left to do is add the code that makes the editor work. Start by adding the below code to the empty <script> element at the bottom of the page:
(function($){ //get canvas and context var editor = document.getElementById("editor"), context = editor.getContext("2d"), //create/load image image = $("<img/>", { src: "img/graffiti.png", load: function() { context.drawImage(this, 0, 0); } }), //more code to follow here... })(jQuery);
First of all, we're placing all of our code into a closure and aliasing the jQuery object as the $ character. I realise that jQuery automatically aliases itself to the $ character for us, however, there are situations where this can cause conflicts with other code that may be in use when our code gets implemented by others.
This is also the recommended way of creating plugins, so simply doing this every time jQuery is used makes it much quicker and easier to go back and turn code into a plugin if necessary. I simply find it more convenient to write all of my jQuery code within a closure like this. It has other benefits as well, such as removing a dependency on global variables – all the variables we create will be safely hidden from other code outside of the closure in most situations.
To work with the canvas we need to get a reference to its context. This is an object representing the surface of the canvas upon which most of the methods for working with the canvas are called. A couple of methods can be called on the canvas object directly, but most are called on the context object.
Getting the context is a two-step process; we first get a reference to the <canvas> element itself using the standard JavaScript document.getElementById() method, we then call the getContext() method on the canvas (this is one such method that may be directly called on the canvas element, and for good reason!), specifying 2d as the context. At the present time, 2d is the only context that widely exists, although in the future we may be able to look forward to working with 3d contexts.
Both the canvas and context objects are stored in top-level variables (the equivalent of global variables were we not working within a closure) so that any functions we define can make use of them.
Following these two variables we create another, called image. We use jQuery here to quickly and effortlessly create a new image element. We set the src of an image (a sample, royalty-free image is included with the code download) so that we have something to work with, and add an onload event handler that simply paints the image onto the canvas one the image has loaded. It is important when working with the canvas to ensure that any images used are fully loaded before they are added to the canvas.
The image is painted to the canvas using the drawImage() method, which is called on the context object. This method takes three arguments (it may optionally take more, as we shall see later in the example); these arguments are the image to use (referred to using the this keyword), the x position on the canvas to start drawing the image, and the y position on the canvas to start drawing the image. The sample image is the exact size of the canvas in this example, and for simplicity is completely square.
We're now ready to add some more code at this stage. There's a trailing comma after the image we created, so the next bit of code we add will also be a variable.
Adding toolbar functionality
Next we need to add the some code to handle the toolbar buttons. We could just add a series of click handlers, one for each button that we wish to add. Although easy to do, it wouldn't be hugely efficient and wouldn't scale incredibly well.
Ideally we want a single master function that handles a click on any button and invokes the correct function. Fortunately, this is also very easy. Add the following code directly after the image we just created:
//toolbar functions tools = { }; $("#toolbar").children().click(function(e) { e.preventDefault(); //call the relevant function tools[this.id].call(this); });
It's as simple as that. Let's cover what this code does. The last top-level variable we create is called tools and it contains an empty (at this stage) object. The functions for each individual button will all be put into this object, as we'll see shortly.
We then add a single click handler, attached to all children of the element with the id toolbar (one of the <section> elements in the underlying HTML).
Within this click-handling function we first stop the default behaviour of the browser, which would be to follow the link (this prevents the unwanted 'jump-to-top' behaviour sometimes exhibited when using standard <a> elements as buttons).
Lastly we apply the JavaScript call() method to the function contained in the property of the tools object that matches the id attribute of the link that was clicked. As all we need from the link that was clicked is its id attribute, we can use the standard this keyword without wrapping it in a jQuery object. The call() method requires that the this keyword is also passed to it as an argument.
Now, if we add a function to the tools object under the property save, whenever we click the toolbar button with the id save, the function will be called. So all we need to do now is add a function for each of our toolbar buttons.
Saving the edited image
We'll start with the save function as it's pretty small and easy. Add the following code to the tools object:
//output to <img> save: function() { var saveDialog = $("<div>").appendTo("body"); $("<img/>", { src: editor.toDataURL() }).appendTo(saveDialog); saveDialog.dialog({ resizable: false, modal: true, title: "Right-click and choose 'Save Image As'", width: editor.width + 35 }); },
The first thing our function does is create a new <div> element and append it to the <body> of the page. This element will be used in conjunction with jQuery UI's dialog component to produce a modal dialog box.
Next we create a new image element; this time we set it's src to a base-64 encoded representation of everything contained within the canvas. This action is performed using the toDataURL() method, which incidentally, is another method called directly on the canvas element. Once created, the image is appended to the <div> element created in the previous step.
Finally, we initialise the dialog component; this done using the dialog() method, a method added by jQuery UI. An object literal is passed to the dialog() method which allows us to set its different configuration options. In this instance we configure the dialog so that it is not resizable and so that it is modal (an overlay will be applied to the rest of the page while the dialog is open). We also set the title of the dialog to a string giving instructions on what to do to save the object, and make the dialog big enough to contain the image by setting its width to the width of the canvas element plus 35 pixels.
Rotation
A common feature of image editors is the ability to rotate an element, and using built-in canvas functionality, it's pretty easy to implement this in our editor. Add the following functions after the save function:
rotate: function(conf) { //save current image before rotating $("<img/>", { src: editor.toDataURL(), load: function() { //rotate canvas context.clearRect(0, 0, editor.width, editor.height); context.translate(conf.x, conf.y); context.rotate(conf.r); //redraw saved image context.drawImage(this, 0, 0); } }); }, rotateL: function() { var conf = { x: 0, y: editor.height, r: -90 * Math.PI / 180 }; tools.rotate(conf); }, rotateR: function() { var conf = { x: editor.width, y: 0, r: 90 * Math.PI / 180 }; tools.rotate(conf); },
So we've added three new functions; a master rotate function, and then a function each for the rotate left and rotate right toolbar buttons. The rotateL() and rotateR() functions each just build a custom configuration object and then call the master rotate() function, passing in the configuration object. It is the master rotate() function that actually performs the rotation. An important point to note is that it is the canvas itself we are rotating, not the image on the canvas.
The configuration object we create in the rotateL() and rotateR() functions is very simple; it contains three properties – x, y and r. The properties x and y refer to the amount of translation that is required and r is the amount of rotation. Each click on a rotate button will rotate the image by plus or minus 90 degrees.
In the rotateL() function we need to translate the canvas the same distance as the height of the canvas on the vertical axis to ensure the image remains visible once the canvas has been rotated. Rotation to the 'left' is classed as negative rotation as it is anti-clockwise. When calculating the angle of rotation we can't use degrees, we need to convert 90 degrees (or -90 degrees in this case) into radians. This is easy and requires the following formula:
The number of degrees multiplied by Pi divided by 180
The rotateR() function is just as straight-forward, this time we translate the canvas on the horizontal axis instead of the vertical axis and use a positive number for rotation in a clockwise direction.
In the master rotate() function we again need to grab a copy of the current image on the canvas, which we do by creating a new image element with jQuery and setting its src to the result of the toDataURL() method again. This time we do want to draw the image back to the canvas once the canvas has been rotated so we add an onload event handler to the image.
Once the image has loaded we first clear the canvas using the clearRect() method. This method takes four arguments; the first two arguments are the x and y coordinates to begin clearing. The thrid and fourth are the size of the area to clear. We want to clear the whole canvas so we start at 0,0 (the top-left corner of the canvas) and clear the entire width and height of the canvas.
Once we have cleared the canvas we translate it (move it essentially) using the translate() transformation method. We will translate it either the full height, of the full width of the canvas depending on whether the rotateL() or rotateR() function initiated the rotation. Translating the canvas is necessary because by default rotation occurs around the bottom right of the canvas, not in the middle as you would expect.
We can then rotate the canvas and then redraw the image back to the canvas. Even though we are drawing the same image back to the canvas, the canvas has been rotated so the image will automatically be rotated as well. Again, we can refer to the image to pass to the drawImage() method as this because we are inside the load handler for the image and jQuery ensures this refers to the image. The next screenshot shows the image rotated to the left once:
Resizing the image
Resizing the image is our most complex interaction, and the function required to do it is quite large, even though resizing the canvas itself is quite trivial. Add the following function to the tools object after the rotation functions we just looked at:
resize: function() { //create resizable over canvas var coords = $(editor).offset(), resizer = $("<div>", { id: "resizer" }).css({ position: "absolute", left: coords.left, top: coords.top, width: editor.width - 1, height: editor.height - 1 }).appendTo("body"); var resizeWidth = null, resizeHeight = null, xpos = editor.offsetLeft + 5, ypos = editor.offsetTop + 5; resizer.resizable({ aspectRatio: true, maxWidth: editor.width - 1, maxHeight: editor.height - 1, resize: function(e, ui) { resizeWidth = Math.round(ui.size.width); resizeHeight = Math.round(ui.size.height); //tooltip to show new size var string = "New width: " + resizeWidth + "px,<br />new height: " + resizeHeight + "px"; if ($("#tip").length) { $("#tip").html(string); } else { var tip = $("<p></p>", { id: "tip", html: string }).css({ left: xpos, top: ypos }).appendTo("body"); } }, stop: function(e, ui) { //confirm resize, then do it var confirmDialog = $("<div></div>", { html: "Image will be resized to " + resizeWidth + "px wide, and " + resizeHeight + "px high.<br />Proceed?" }); //init confirm dialog confirmDialog.dialog({ resizable: false, modal: true, title: "Confirm resize?", buttons: { Cancel: function() { //tidy up $(this).dialog("close"); resizer.remove(); $("#tip").remove(); }, Yes: function() { //tidy up $(this).dialog("close"); resizer.remove(); $("#tip").remove(); $("<img/>", { src: editor.toDataURL(), load: function() { //remove old image context.clearRect(0, 0, editor.width, editor.height); //resize canvas editor.width = resizeWidth; editor.height = resizeHeight; //redraw saved image context.drawImage(this, 0, 0, resizeWidth, resizeHeight); } }); } } }); } }); },
So let's see the steps required by our resize function. First we need some way of indicating to the user what size the image will be resized to, and the user requires some way of setting the new size. We then want to confirm whether the user wishes to apply the new size, and then if so, apply the new size. The new function does all of the things, let's see how.
First we get the coordinates on the page of the canvas element using the jQuery offset() method, so that we know where to position the resizable element. We then create a new <div> element, which will become the resizable, and give it an id of resize so that we can easily refer to it. We also set some style properties of the new element in order to position it over the canvas. Once these styles are set we append it to the <body> of the page. The resizable will appear as a dotted border around the inside of the canvas, as shown below:
Next we initialise two more variables, but set their values to null for the time being. These will be used to store the width and height that the resizable is changed to when it is resized. These variables will be populated and used later in the function. The xpos and ypos variables are used to position a tooltip that we'll create in a moment. They position the tooltip 5 pixels from the left edge and top edge of the canvas.
Next we initialise the resizable using jQuery UI's resizable() method. We configure the resizable to have its aspect ratio locked; our image is square, so we want it to remain square whatever size it is resized to. We also ensure the image can't be increased in size so that the quality of the image is retained. If the image were enlarged instead of being shrunk, it would become blocky. The maxWidth and maxHeight configuration options ensure the image can only be made smaller. Both are set to the current width and height of the canvas respectively.
The resizable component has some custom event handlers that we can assign functions to which will be executed when these custom events are fired. We use two of these event handlers for this example; resize, which will be fired every time the resizable is resized, and stop which is fired once the resizable has finished being resized.
The resize function can receive two arguments; the first is an event object, which we don't need to use in this example, but which must be declared in order to make use of the second argument, which we do need. The second argument is an object which contains useful information about the resizable, including the size it has been changed to. The first thing we do in this function is assign the new size of the resizable to our resizeWidth and resizeHeight variables using properties of the ui object that the function receives.
The last thing this function does is create the tooltip that tells the user how big the resizable currently is. If this tooltip already exists we don't need to recreate it and can just set its inner text to a string showing the current size of the resizable. If the tooltip does not already exist, such as the first time the resizable is resized, we create it from scratch. The tooltip is positioned using the xpos and ypos that we created earlier. The string is built from scratch each time the resizable changes size. The tooltip will appear like this:
The stop function, which is executed once when the resize interaction ends, first creates a new dialog element which checks that the visitor wants to apply the new size to the canvas. Once again the dialog is configured so that it isn't resizable itself and so that it is modal. We also add some buttons to this dialog. The first is a cancel button, which allows the visitor to back out of the resize operation without applying the new size. All we do here is some housekeeping, removing the dialog, the resizable, and the tooltip.
We also create a Yes button that applies the new size. We also do the same housekeeping tasks here, but then to apply the new size we create a new image once more in the same way that we have done several times already. Setting the new size of the canvas is extremely easy; we just supply the values to which the resizable was changed to the width and height properties of the canvas element. Here is the dialog, which appears the same as the save dialog from earlier, just with different content:
Changing the rgb values of individual pixels
I said earlier that we have complete pixel-level control of the contents of the canvas element, so our final two toolbar button functions will do exactly that. The first function is used to convert the image to greyscale, the second changes the image to sepia tone. We'll look at the greyscale function first:
greyscale: function() { //get image data var imgData = context.getImageData(0, 0, editor.width, editor.height), pxData = imgData.data, length = pxData.length; for(var x = 0; x < length; x+=4) { //convert to grayscale var r = pxData[x], g = pxData[x + 1], b = pxData[x + 2], grey = r * .3 + g * .59 + b * .11; pxData[x] = grey; pxData[x + 1] = grey; pxData[x + 2] = grey; } //paint grayscale image back context.putImageData(imgData, 0, 0); },
The first thing this function needs to do is to get the current pixel data of the canvas. We use the getImageData() method to do this. This method accepts four arguments, which are the same as the clearRect() method that we looked at earlier – the first two arguments are the x and y positions to start collecting data from and the third and fourth are the width and height of the area to get. We want the whole canvas so we start at 0,0 (top-left) and continue for the width and height of the canvas. The resulting object returned by the method is stored in the imgData variable.
The object stored in this variable has a property called data; within this property is an array containing the r g b and alpha values for every single pixel in the canvas, so the first item in the array will contain the r value of the first pixel, the second item contains the g value of the first pixel, the third contains the b value of the first pixel, and the fourth item contains the alpha value of the fist pixel. This array is incredibly large; a 480 by 480 pixel image contains 230400 pixels and we have four items for every single pixel. This makes the array a total of 921600 items in length! This length is also stored for use in the for loop that we define next.
The for loop is a little different than usual for loops. Remember, the array can be organised into discrete blocks of 4 items, where each item in a single block refers to the individual rgba components, so we loop through the array four items at a time. On each iteration we get each pixel component and then use the formula r * .3 + g * .59 + b * .11 to convert each one to greyscale. We then save the converted pixel component back to its original array item.
Once we have looped over the entire array, we can write the array contents back to the canvas, replacing each original pixel with its new greyscale counterpart using the putImageData() method, which simply does the opposite of getImageData().
The sepia tone function is identical to the greyscale function except that we use a different formula to convert each r g b component to sepia tone:
sepia: function() { //get image data var imgData = context.getImageData(0, 0, editor.width, editor.height), pxData = imgData.data, length = pxData.length; for(var x = 0; x < length; x+=4) { //convert to grayscale var r = pxData[x], g = pxData[x + 1], b = pxData[x + 2], sepiaR = r * .393 + g * .769 + b * .189, sepiaG = r * .349 + g * .686 + b * .168, sepiaB = r * .272 + g * .534 + b * .131; pxData[x] = sepiaR; pxData[x + 1] = sepiaG; pxData[x + 2] = sepiaB; } //paint sepia image back context.putImageData(imgData, 0, 0); }
Here's a shot of the image once it has been converted to sepia tone:
Conclusion
In this tutorial we've seen how the canvas can be turned into a powerful image editor that gives us some of the functionality of the basic, application-based image editors with which we are familiar. This example could easily be extended to add other features, such as cropping, and drawing with a pencil tool, although I'll leave the addition of this functionality to you. Thanks for reading.
Comments