Build an Image Editor With EaselJS, jQuery, and the HTML5 File API

As HTML5 becomes more popular, more of the major browsers begin to support its APIs. Today, using the Canvas and File APIs, we can create a full-blown picture editor, with features on par with some desktop applications. For this, we will use the EaselJS library. It uses a syntax similar to AS3, so it will be easy to understand both for Flash and JavaScript programmers.

Final Result Preview

Let's take a look at the final result we will be working towards:

Click to try the demo

Play around with it to get a feel for what it's capable of. You might even want to download the full source code and take a look around before digging in to this tutorial.

Introduction

Because of the amount of code in this tutorial, I'm going to go through each files and explain every part in turn, rather than guiding you through re-building it from scratch. I will try to comment everything as much as I can, and I believe you will understand everything.

Step 1: The Style

I will start in an unusual way, from the CSS files. First create the style.css file:

On the first line we change the font and disable outlines around elements. Next there are just style definitions: ul#mainmenu is the main menu element, div#overlay is the shading under all dialogs, and ul#layers is the Layers panel that will be displayed on the right side of the canvas. Next we define the style for tool buttons, and finally we have a fragment of the jQuery-UI's style, because we will need this part for the layer cropping dialog.

Next comes the print.css file which contains only two lines to hide everything apart from the canvas when printing the image (this style is applied only when you print the page, because of its declaration in the HTML file).

The first line hides all elements inside the body section, and the second line makes only the canvas visible (and also aligned to the top-left corner). This is because when someone wants to print the image they usually don't want to print the interface.

Step 2: The HTML Structure

You should have a basic idea of the interface from looking at the CSS files above. Now create the index.html file and enter the following lines:

Next we construct the whole UI structure. Just remember: every div with a class of dialog is just a dialog for the user to input data needed to perform some operation on image. If you run this code in your browser now you will notice a few 404 errors in the console and the that the menu will not work, but we will fix that when we create the ui.js file.

Step 3: Main Application Object

It's a good practice to wrap all your application related functions and variables inside a single object, to prevent them from being overriden by external libraries or even your own scripts. Our object will look like this:

In app.stage we will hold a reference to the Stage object for our application. If you've coded anything in ActionScript, think of this Stage as being like AS3's. It has a display list which is drawn to the canvas element on each update. The app.canvas is variable with reference to canvas element inside our html document. We will use it to create the Stage and to resize it along with the window.

The app.layers array will hold all image layers, and app.tool contains value of actually selected tool. The app.callbacks will hold every event callback we will need to specify (e.g. clicking menu button), app.renameLayer holds the number of the actually renamed layer, and app.undoBuffer and app.redoBuffer are arrays to hold backed-up app.layers state to make undo and redo functions work.

You will also need to add these four lines before the app definition (they are just tool ID constants):

const
TOOL_MOVE = 0,
TOOL_SELECT = 1,
TOOL_TEXT = 2;

Step 4: Useful Methods

Now, we will define the methods of this object. First add the following refreshLayers() and sortLayers() methods:

Please note that inside an object you need to declare variables and functions with ":" instead of "=".

The sortLayers() method is called when the user drags layers within the Layers panel;refreshLayers() is called very often, because it recreates the app.stage and adds all layers to the stage once again, setting their properties and applying event callbacks. These callbacks enables you to move layers and edit text on text layers. This is a very important function as it also adds all layers to the Layers panel in the UI and disables tool buttons in the menu (if there are no layers) and enables them as well (when there is at least one layer).

Before the refreshLayers(), insert another set of helper functions (remember to add a comma after the last one!):

An active layer is the one to which you are applying all operations (tranformations, adding filters, etc.). You can activate a layer by clicking on it on the Layers panel or using the Select tool on the canvas.

As you can see the activateLayer() method parameter can be either a Bitmap or a number. If it is a Bitmap - the EaselJS object for images - then its active property is set to true, and if it is a number then the layer on this position in the app.layers array is activated. getActiveLayer() just returns the layer which is active and getActiveLayerN() returns the position of the active layer within the app.layers array.

The last bunch of methods in this object should be insterted directly after app.redoBuffer declaration and before the ones you put there earlier:

As you shoud notice when reading app.refreshLayers(), the toString() method of app.layers is overridden by the code which prepares stringified version of all layers inside. Of course it would be a waste of memory to keep all the layer information there, so only the values that can be changed by application are backed up.

The addUndo() method pushes actual state of the layers to app.undoBuffer array and clears the app.redoBuffer - because when you make an action that can be undone then you cannot redo anything that was undone before that action. loadLayers() takes two arguments (the array from which we should pop the state of app.layers and the array to which we should push the actual state of this variable), and performs parsing of the backed-up app.layers.

As EaselJS's filter example says:

"... filters are only displayed when the display object is cached ..."

This means that you need to call cache() method of the Bitmap to apply the filter. Caching is performed to improve performance - the filter is applied only once and only the filtered bitmap is drawn. EaselJS is caching content in a very clever way - it just copies it to another canvas element which is not added to the document (it's hidden). I mention this because at the end of the loadLayers() method there is an if block which checks whether there are any filters which should be updated on this layer - and if there are, it updates cache or caches element.

Step 5: Initialization

Initialization of the whole application is simple; just insert this after the app declaration:

Ticker is a EaselJS-implemented timer which calls the listener's tick() function to maintain the stable FPS set earlier. This way we can automatically call app.stage.update() to redraw the Stage.

At the beginning (just after the document has loaded) we assign the first canvas element on the page that the $ (jQuery) function finds to app.canvas, then we disable selecting of anything on document (because otherwise when you drag mouse across the canvas there is a effect as if you are selecting text).

We set Ticker's FPS to 30 (you need only 24 frames per second to trick the human eye into thinking it's seeing movement) and set the window as the Ticker's listener.

Step 6: UI Helper Functions

Now it is time to bring our menu and the whole user interface to life. The ui.js file will be composed almost entirely of jQuery functions, so it is really easy to understand. Let's begin with helper functions:

The importFile variable will inform us of whether we are opening a file or importing it.

The names of the showDialog() and hideDialog() functions speak for themselves - though one interesing thing in the hideDialog() function is how it checks whether all dialogs are hidden with the jQuery ':visible' pseudo-class, only to then hide the overlay. In the end it proved to be useless because there is no situation that more than one dialog is on screen, but I left it for your future use; maybe it will come in handy.

Step 7: Resize The Stage

Now we should do something when user resizes the browser window. This is when the window's resize event comes into play. It is fired every time the user resizes the browser window:

First we center all dialogs using jQuery's each() function. This calls the callback for every item which is matched by the selector in the $ function.

Then, we've got to set the canvas's width and height - but not in CSS because this would strech the images inside of the canvas, and we do not want that. The menu height is 37px so we set the canvas' height to the window's height minus 37px. Same for the width, but this time we've got to subtract the width of the Layers panel which is 232px. We're also resizing the menu and Layers panel to fit the window (here we can use CSS).

After that we need to refresh the layers to make sure they are always up-to-date when window is resized. The last thing is to move the crop dialog in case the user resized the window when cropping the layer.

Step 8: Binding All Together

The menu's buttons must be bound to the callbacks specified in app.callbacks, and also we need to bind the keydown event for inputs and click for dialog buttons. The last sentence may sound complicated, but when you see the code it will become clear:

Another thing to remember: when you do anything with jQuery and any HTML elements, do it inside the document.ready callback, because only then can you be sure that all elements you are using are already rendered.

The above code is long, but that's because there are many parts which are similar but differ in parts that do not allow us to wrap it in any helper function. The highlighted part is where we set all callbacks for dialog buttons and inputs.

Next you should look at the live() function we use to bind click events to layer buttons (on the Layer panel) - the live() function adds callback for every element that will match this selector in the future, making it very useful since we generate a new layer list on every app.refreshLayers() call.

The last function here is $(window).resize() which manually fires the resize event of a window. This is why it is important to link scripts in order, because if ui.js were added to the HTML before main.js, the layers would be refreshed before the function definition, which can sometimes lead to unexpected results, making finding the bug even harder.

If you run the application now you will see a nicely working menu and proper resizing of the UI, but still no button will do anything other than throwing errors to the console when you click it.

Step 9: Opening The Files

Now we will use another API from HTML5 specification: the File API. It enables us to open files from the user's computer, but only when he chooses them in the OS's file input field (to prevent web apps from stealing your private data).

Please note that if you will be running this application on local computer you need to setup a local server or add the --allow-file-access-from-files parameter when running Chrome, because opening files from within local web pages is disabled by default.

In the file.js file we will also put functions to save and print the image, so let's start with these four helpers:

The openFile function will be used to open the image and add it to the layers. If we selected 'Open File' from the menu then the old content would be erased, whereas 'Import File' would add new layers to the image. (In the function, if the first parameter is true then we are opening the file, otherwise we are importing it).

openURL opens the file using the same function, but from an external source (and as far as I know Chrome disables access to cross-domain origin pixel data which makes those images pretty useless). Because we cannot save the file to the user's disk we just open another window containing just an image representing the actual Stage; the user can then right-click to save the image.

Printing is achieved by calling window.print(). We can't, of course, print anything without letting the user know, so this function will open the default printing dialog where the user can choose printing preferences.

As you can see the code is very short yet very powerful. In the first (app.callbacks.openFile) callback we check whether the opened file is an image, and stop if it is not. Then we create a new FileReader, set its onload callback to open the file, and invoke the readAsDataURL(file) method which loads the file and outputs the result as a data URL for us to read.

(Also please note that we are clearing undo and redo arrays; we must do this because we can't restore the image if we delete it - the user must manually reselect the file from input.)

Save the file, open the app in your browser, and you can finally do something! Not much, but if you are doing this for the first time it is probably exciting to be able to load some images into the browser, even if you can only move them.

Step 10: Text Layers

Now that you can open and import images you could add some text. There is one really useful thing with the canvas - you define text just like in a CSS font attribute. And EaselJS fully uses that feature.

We will define a Text tool in the tools.js file. Add the following lines:

The editText variable is true when we are changing properties of the existing text layer instead of creating a new one.

The first function is, as usual, a helper function. It checks whether we are editing an existing text layer or adding a new one, then creates a new Text object and adds it to application layers. Because all objects in EaselJS extend the base DisplayObject, we can use both Text and Bitmap in the same way; only their properties differ.

The second function is a callback. First thing we need to do is to check what type of event we are receiving (because we use only one callback both for handling button click and pressing enter inside the input). Then we just call the previous helper.

Step 11: Layer Transformations

Simple layer transformations are indeed really simple with EaselJS. Everything that I will show here can be done just by changing layer (Bitmap or Text) properties.

I'll start by explaining the registration point. The regX and regY properties specify the point from which rotation and position are calculated - it's like a handle. In the main.js file we set this point to the center of the image, to make transforming layers easier. All layer transformation functions will go in the layer.js file.

In this part there are - as usual - helper functions. There are few things I want to say about this piece of code.

First: we need to call the addUndo() function before we start changing anything. Why? Because we want to go back to the state before some operation was performed when we click the undo button.

Also notice the affectImage variable. We will set it to true when we want to affect the whole image; in almost every function there is an if statement checking if we are affecting the whole image, and (if so) returning the result of the appropriate image*() function.

Another bunch of jQuery calls. We also check the event type because we can get those functions called by button or input field. Every function in the above code is much the same: check event type, get input value from dialog and call function.

The callback on the top of this part of the code is bound to inputs that should only get numbers inside them.

Step 12: Transformations: Scale

Let's start by adding this source code; I will explain it later. Put the code below into the image.js file:

We are just looping through all layers setting their scaleX and scaleY properties. But the image would look weird if we only scale layers. We also need to move every layer to make this function work fine.

Step 13: Image Transformations: Rotation

Rotation will be a little harder than scaling. But first is the code; put this also into the image.js file:

We are of course adding to the layer rotation, but the code I highlighted is new. It is based on the equations of rotating the point in the Cartesian coordinate system:

Where Φ is the angle. So the highlighted code is just the translation of the above equations into JavaScript code (plus the conversion from degrees to radians, because trigonometry functions in Math library take radians as parameters, and one radian is exactly pi/180 degrees).

Step 14: Transformations: Skew

Skewing is very similar to rotation, because basically it is rotation but with two different angles for two directions. Take a look at the code:

Of course we can ignore the callbacks - we already know what do they do. We should focus on the first two functions. They are just looping through the layers setting their scaleX or scaleY to the opposite value - this is what we know as flipping: just a negative scale. Also the x or y must be reversed to make the image really look flipped.

This was the last of the image transformations. Now we are going to make something more advanced - filters!

Step 16: Simple Filters: Introduction

I call them simple because we are using filters which are built in to EaselJS: ColorFilter and ColorMatrixFilter. These modify the image pixel by pixel, so with big images and complicated filters you can make the browser lag for a while or even stop completely.

When the filter is applied it splits the image into four channels (red, green, blue and alpha) and for every channel it multiplies each value by the corresponding multiplier and adds the corresponding offset. (Actually, the image is not really split; this is a handy metaphor.)

When this filter is applied, it also (metaphorically) splits image into channels, and then it multiplies each value by each other. For example, the equation for the value of a pixel in the red channel after passing through the filter is:

newRed = (red * rr) + (green * rg) + (blue * rb) + (alpha * ra) + ro;

This is the same for green, blue and alpha also, only with different variables from matrix (gr,gg,gb,ga for green, and so on). This filter is a little more advanced than the ColorFilter, because each color depends on other colors of the pixel.

It does all the work for us: grabs the active layer; if there are no filters then creates a filters array; and adds the filter.

After that we've got to cache the layer for the filter effects to be visible, so we check whether we've already cached this layer (for example when cropping it) and call updateCache() or cache() as appropriate.

Here is the image I'll use to show the effects of the filters:

Step 18: Simple Filters: Brightness

For this effect we will use the ColorFilter, because changing the brightness is just changing all channel values (red, green, blue) in the layer by the same value.

As I mentioned earlier, our helper function is doing everything for us, we only need to create new filter. Here we are creating ColorFilter with the red, green, blue multipliers set to value and alpha set to 1.0 (we do not want the alpha to be touched by this filter).

Below is an example result of this filter:

Step 19: Simple Filters: Colorify

Colorify will make some channels value bigger or smaller to change the overall color of the image, so we will again use the ColorFilter.

Again, the dirty work is handled by applyFilter, and we only focus on creating the filter object. Here we will be using the last four parameters of the ColorFilter constructor. They are added to the channels, so they perfectly fit our needs.

Below is an example result of this filter:

Step 20: Simple Filters: Desaturation

Desaturation is the process of removing the saturation - in simple words, making the image black and white. To do that we need to calculate the luminosity of each pixel and set all colors to this value. The simpliest luminosity equation involves only adding the same amount of all colors, and for that we can use the ColorMatrixFilter:

As I said earlier - take the equal amount of three colors and add them. We again do not touch alpha as it do not contain any color values.

There is no example result image because it is already black and white; the filter has no effect.

Step 21: Convolution Filters

The Convolution Filter is a little more advanced than ColorMatrixFilter. It also uses a matrix, but the convolution matrix represents the multipliers of pixels surrounding actual pixel.

Let's say that we have this example 3x3 convolution matrix (already represented as a JavaScript array):

[
[ 0, 0, 0],
[-1, 1, 0],
[ 0, 0, 0]
]

And (for example) we are looking at a part of the image where the pixels look like this (each number represents the strength of the red color channel; we ignore the rest for simplicity):

[ 00 ] [ 12 ] [ 43 ]
[ 12 ] [ 56 ] [ 62 ]
[ 63 ] [ 67 ] [ 92 ]

With the convolution filter, we are modifying the pixel in the middle (current value: 56). So we start by multiplying every color value around that pixel by its multiplier from the convolution array, and then we add them together. We get the following equation:

So now we set the pixel's new red channel value to 44 - but in a new array of data, because we still need to hold on to the old value of 56 for modifying the other pixels in the image. This means that, when applying the filter, we actually create a copy of the image rather than modifying the existing one in-place.

With a larger matrix, you can see how the formula would get more complex, as each pixel depends on data from more surrounding pixels; for this reason, a bigger matrix requires a longer running time. When running a convolution filter, the browser will usually freeze for a while.

All advanced filters that we will create will depend on this filter, so it will be better if you understand how it works. Again, more information is available here.

Also you have to remember that the sum of all values inside the convolution matrix must be equal to either zero or one - otherwise you will get weird results. To make this simpler we can use the factor and the offset variables. After calculating the pixel value (with the above equation) we multiply the whole value by the factor and add the offset. This simplifies creating, for example, the blur filter (which we'll get to in a minute), where all convolution matrix values are the same.

Sadly there is no ConvolutionFilter implementation in EaselJS, so we have to write one. Following the example of ColorFilter I created this code:

You can skip all the methods except for applyFilter, as they are used by EaselJS to initialise the filter.

applyFilter() is invoked when we apply a filter to the image. First we have to get the image data from canvas, then I use a trick with JSON.parse(JSON.stringify(imageData.data)) - because we want a copy of the image data, and imageData.data object dont have a clone() or slice() method to achieve this, so we use this tricky to completely copy the object and all of its properties.

So every pixel takes four array items - one for each channel. Finally, after iterating through all pixel data, we call putImageData() on the target to save the result.

Step 22: Convolution Filters: Blur

This is the simplest convolution effect, so I decided to let the user set the radius of the filter (which will result in setting the size of the array) to make it more complex. Here is the function we will use:

Why do we set factor to Math.pow(radius * 2, 2)? Because as I said earlier: the sum of all array fields must be equal to zero or one; if we divide them all by their sum we will always get 1.

Below is the result of this filter:

Step 23: Gaussian Blur

This convolution filter is so-called because it uses values from the Gaussian standard distribution (pictured above) inserted into the convolution matrix. To simplify the task we let the user choose only three radius values, because applying the filter for a bigger radius will take too much time (a 3px radius is already a 7 by 7 matrix).

We specify the matrices outside of the function so as not to waste time assigning it every time the user selects it from the menu. In this function we simply choose the specified matrix and apply the filter. You can of course add more radius values if you want to.

Below is the result of this filter:

Step 24: Edge Detection

Edge detection is a technique often used in robots' AI to help them move around, because edge detection leaves only the edges of the image. It is also a really nice effect to use in art.

To achieve this we use approximation of the first values from the Laplace distribution (image above) with b = 1/4. All functions from this point will only have different matrices:

Step 25: Edge Enhance

This filter is similar in effect to the previous, but it enhances the edges without blacking out the rest of the image - making it perfect for artistic use. This is actually the matrix I used to explain to you how convolution filters work:

Step 29: Scripting: Introduction

Allowing user to use some scripting language in your application is very useful feature. It allows user to automate his work, or when he achieves some nice effect he can share it with someone else, and this person will get the same effect too. And because we are writing the whole application in JavaScript - which is a scripting language itself - it is really easy to create such a feature.

Using the eval() function we can run some JavaScript from a string, and this string will be the user's script.

You have probably read that using the eval() function is very bad practice. Of course if you have to use it inside of your code, then this sentence is true, because this disables any cacheing or compiling that modern JavaScript engines use to speed the code up. It also creates another instance of the JavaScript parser, which wastes the memory until it finishes with the code. So avoid using the eval() like this:

In our case everything is ok, because writing your own interpreter would be a massive waste of time and resources (meaning more or bigger files for user to download).

Step 30: Scripting: Secure Eval

Because we are executing user scripts we have to be sure that he does not accidentally destroy the result he worked on because he mistyped function names or layer numbers. That is why he have to make sure that user cannot access any window or app methods directly.

For this reason, we make our function look like this:

scriptExecute = function (code) {
hideDialog('#dialog-executescript');
if ((code.match(/eval\(/g) != null) && (!confirm('You used the eval function inside of your code. This may lead to unexpected effects, do you want to continue?'))) return;
eval(code.replace(/(window\.|app\.)(.*?);/g, ''));
}

(Put this code into the scripts.js file.)

We hide the dialog first because it would otherwise just hang there until the script finishes, and maybe the user wants to see his script at work. Then we call the provided code, but we replace all window and app related calls, so the user can't delete all layers or close the window by mistake.

A little warning here: the user can still do something with these variables if he uses the eval() function in his code - but then we ask him if he really wants to do this.

That is the complete scripting system. Go ahead and check it out by passing some nice code to the 'Execute Script' dialog. Try to use the eval() there to see that it is asking you whether you really want to do this.

Conclusion

As you can see the HTML5 Canvas is a powerful thing. But we only scratched the surface of what can be done with it. We have created a really advanced application - which allows users to load their photos and make some modifications to them, then save and print the edited photo - using pure JavaScript. A few years ago that would have been a joke.

Also you are welcome to expand the application you just created! Add more filters, change the interface, add more useful functions (for example you could add more properties to the layer, maybe a list of all filters with possibility of removing and editing them, or a button to convert the undo buffer into ready script for user to share). Just be creative and maybe you will create some really useful stuff!

Thanks for reading this tutorial, I hope I really taught you something that you will use in some big project. If you need any help in creation of something advanced with HTML5 feel free to ask me on my contact email or by adding a comment to this tutorial. I will answer you as soon as I get your message.

I am a programmer from Poland. I started programming when I was about eight years old, and I have learned many programming languages since then (including Assembler, Basic, C, C#, Delphi, Java, JavaScript and many more). I fell in love with JavaScript from the first sight, and this one became my passion — I am developing both front-end and back-end solutions with it, including desktop software. I also enjoy writing some Delphi application or game from time to time.