Using HTML5 canvas, Drag and Drop and File API to offer The Cure

By using a combination of the <canvas> element and the File API we could put together a little service that offers “The Cure” for many people.

The cure for what, you might ask? Baldness, of course. πŸ™‚

I’m going to go through the basics here to show how you can drag an image from your desktop into the web browser, drop it into a <canvas> element and then start drawing on it; to be more specific, draw something that could, for instance, be hair on the picture of a bald guy. When you are done, you also have the option to save what you have accomplished in the canvas.

The end result

You can see/try out this example in The Cure demo page. I’ve also put together a little video showing this in action, offered below through the HTML5 <video> element and the vid.ly video service:

Web browser support disclaimer

Before we begin, let me state that this will only work fully as intended in Firefox and Google Chrome (not Safari). All the canvas things should work fine in Safari, Opera and IE9, but the Drag and Drop combined with the File API is the things that won’t.

First steps

To begin with, we create a canvas element in a page and get some scripting references:

HTML

    <canvas id="my-canvas" width="700" height="600">I am canvas</canvas>

JavaScript

var canvas = document.getElementById("my-canvas"),
	context = canvas.getContext("2d"),
	img = document.createElement("img"),
	mouseDown = false,
	brushColor = "rgb(0, 0, 0)",
	hasText = true,
	clearCanvas = function () {
		if (hasText) {
			context.clearRect(0, 0, canvas.width, canvas.height);
			hasText = false;
		}
	};

This is basically just about getting a reference to the canvas element, its 2d-context we will be drawing on and some extra variables for detecting what has happened on the canvas, brush color and such. The img variable is a container image for future loading use.

Instructions and loading an image

    // Adding instructions
	context.fillText("Drop an image onto the canvas", 240, 200);
	context.fillText("Click a spot to set as brush color", 240, 220);
	
	// Image for loading	
	img.addEventListener("load", function () {
		clearCanvas();
		context.drawImage(img, 0, 0);
	}, false);

What we do here is draw some text in the canvas element to show initial instructions, and also applying a loading event to a container image we will be using to load dropped images on the canvas and then draw it into the canvas once it has loaded.

Drag and drop

// To enable drag and drop
canvas.addEventListener("dragover", function (evt) {
	evt.preventDefault();
}, false);

// Handle dropped image file - only Firefox and Google Chrome
canvas.addEventListener("drop", function (evt) {
	var files = evt.dataTransfer.files;
	if (files.length > 0) {
		var file = files[0];
		if (typeof FileReader !== "undefined" && file.type.indexOf("image") != -1) {
			var reader = new FileReader();
			// Note: addEventListener doesn't work in Google Chrome for this event
			reader.onload = function (evt) {
				img.src = evt.target.result;
			};
			reader.readAsDataURL(file);
		}
	}
	evt.preventDefault();
}, false);    

First, we need to apply the dragover event to the canvas and cancel its default behavior; yes, folks, that’s how the Drag and Drop specification and implementation works, to make the drop event to be triggered…

The second part is handling a dropped image: it gets a reference to the dropped file, and then utilizes the FileReader object to directly read out the dropped image as code (i.e. as a data URL, a base64-encoded representation of the image). When it has finished loading the dropped image into the FileReader it loads that image as a src for the image container we created before.

Drawing on the canvas

So, now when we have the basic things set up, Drag and Drop in place and drawing an image into the canvas, it’s time to add drawing features.

// Detect mousedown
canvas.addEventListener("mousedown", function (evt) {
	clearCanvas();
	mouseDown = true;
	context.beginPath();
}, false);

// Detect mouseup
canvas.addEventListener("mouseup", function (evt) {
	mouseDown = false;
	var colors = context.getImageData(evt.layerX, evt.layerY, 1, 1).data;
	brushColor = "rgb(" + colors[0] + ", " + colors[1] + ", " + colors[2] + ")";
}, false);

// Draw, if mouse button is pressed
canvas.addEventListener("mousemove", function (evt) {
	if (mouseDown) {
		context.strokeStyle = brushColor;								
		context.lineWidth = 20;
		context.lineJoin = "round";
		context.lineTo(evt.layerX+1, evt.layerY+1);
		context.stroke();
	}
}, false);    

First we detect if the mouse is being pressed and make sure that the drawing will begin a new path, i.e. not closing the drawing for a potential previous path. We have a mouseup event, and the interesting thing to mention there is a little extra side-feature: if you click somewhere, we will use the getImageData method on the canvas context to detect the color of the canvas coordinates where the mouse button was released. Here we just read out the RGB color and then set that as the new brush color for our drawing.

With the mousemove event, if the mouse button is being held down, we draw a 20 pixels wide rounded line with the chosen brush color where the mouse currently is.

Saving the canvas content

When you are happy with the creation you have made in the canvas, we have a dynamically generated button that will open the canvas content in a new window/tab (depending on your web browser’s default behavior), just as a data URL in the PNG format.

The entire code

This is what the entire code looks like:

(function () {
	var canvas = document.getElementById("my-canvas"),
		context = canvas.getContext("2d"),
		img = document.createElement("img"),
		mouseDown = false,
		brushColor = "rgb(0, 0, 0)",
		hasText = true,
		clearCanvas = function () {
			if (hasText) {
				context.clearRect(0, 0, canvas.width, canvas.height);
				hasText = false;
			}
		};
		
	// Adding instructions
	context.fillText("Drop an image onto the canvas", 240, 200);
	context.fillText("Click a spot to set as brush color", 240, 220);
	
	// Image for loading	
	img.addEventListener("load", function () {
		clearCanvas();
		context.drawImage(img, 0, 0);
	}, false);
	
	// Detect mousedown
	canvas.addEventListener("mousedown", function (evt) {
		clearCanvas();
		mouseDown = true;
		context.beginPath();
	}, false);
	
	// Detect mouseup
	canvas.addEventListener("mouseup", function (evt) {
		mouseDown = false;
		var colors = context.getImageData(evt.layerX, evt.layerY, 1, 1).data;
		brushColor = "rgb(" + colors[0] + ", " + colors[1] + ", " + colors[2] + ")";
	}, false);
	
	// Draw, if mouse button is pressed
	canvas.addEventListener("mousemove", function (evt) {
		if (mouseDown) {
			context.strokeStyle = brushColor;								
			context.lineWidth = 20;
			context.lineJoin = "round";
			context.lineTo(evt.layerX+1, evt.layerY+1);
			context.stroke();
		}
	}, false);

	// To enable drag and drop
	canvas.addEventListener("dragover", function (evt) {
		evt.preventDefault();
	}, false);

	// Handle dropped image file - only Firefox and Google Chrome
	canvas.addEventListener("drop", function (evt) {
		var files = evt.dataTransfer.files;
		if (files.length > 0) {
			var file = files[0];
			if (typeof FileReader !== "undefined" && file.type.indexOf("image") != -1) {
				var reader = new FileReader();
				// Note: addEventListener doesn't work in Google Chrome for this event
				reader.onload = function (evt) {
					img.src = evt.target.result;
				};
				reader.readAsDataURL(file);
			}
		}
		evt.preventDefault();
	}, false);
	
	// Save image
	var saveImage = document.createElement("button");
	saveImage.innerHTML = "Save canvas";
	saveImage.addEventListener("click", function (evt) {
		window.open(canvas.toDataURL("image/png"));
		evt.preventDefault();
	}, false);
	document.getElementById("main-content").appendChild(saveImage);
})();

Combining APIs

This is just an example of how you can combine a number of HTML5 APIs to interact with the user’s desktop/file system, read out file information and offering drawing and saving in the web browser. Now go code something nice. πŸ™‚

31 Comments

  • Robert,
    Thanks for sharing this. I have done some work along the same lines – Instant Sprite, an in browser CSS sprite generator (code). I have also wrapped up the File Reading into a small library that allows you to access files via the standard <input type=”file” /> change event or from a drop event, and lets you process files as groups, filter by content types, etc: filereader.js

  • Robert Nyman says:

    Brian,

    Thanks for the tip.

  • crossaxx says:

    nice work…i am no coder but i want to be able to move the images i drag to the canvas,can it be done with few lines of code?

  • Robert Nyman says:

    crossaxx,

    Well, no, it’ll take a little more than a few lines of code.

  • Maria says:

    hello
    this code is what i was looking for..thank you πŸ˜‰
    but I’ve problem running it offline…your online demo works fine but when i create my own index the image won’t load but i can draw…i also tried to download the whole demo and i got the same problem…it detects that i am moving the image on the canvas and it writes “+Move” but the image won’t show after dragging it to the canvas but still i can draw
    any suggestions? is it because i am running it offline?
    note that i tried it on Firefox 4 and chrome 10
    your help is really appreciated
    here is an image of whats going on http://img64.imageshack.us/i/mariammm.png/
    thanks πŸ™‚

  • Robert Nyman says:

    Maria,

    No, it works offline for me too. Do you get any JavaScript errors or something?

  • Maria says:

    no javascript errors..its freaking me out!
    could you please send the working index to wiswis911@live.com?
    i am sure i did every thing right but I’ve got nothing else to try except your index that worked offline
    Thanks again πŸ™‚

  • Robert Nyman says:

    Maria,

    Please just take the source from http://robertnyman.com/html5/canvas/the-cure.html or save that page in the web browser (File > Save Page As). There are no other dependencies, it’s all there.

    I just saved it and ran it locally, and it worked the same way.
    Good luck!

  • Maria says:

    already tried that…there must be something wrong with my system, thanks anyway

  • ray says:

    hello
    how about if i wan’t to drag and image from the same page to the canvas or from another browser window?
    thx

  • Robert Nyman says:

    Maria,

    Sorry about that.

    ray,

    That should in general work fine, too. It depends om what data is exposed and the operating system you use.

  • CHUNG says:

    Hello.
    Are questions to be asked.
    “addEventListener doesn’t work in Google Chrome for this event”
    But to work in Chrome do you know what to do?
    Please let me know ..

  • Robert Nyman says:

    CHUNG,

    The demo works fine in Google Chrome for me. Which version do you have and which event doesn’t work?

  • CHUNG says:

    hi..

    Drag and drop a file on the canvas does not…

    help me…

  • CHUNG says:

    Drag and drop the file does not look at the canvas…

    sorry, i can’t speak english well..

  • Robert Nyman says:

    CHUNG,

    I’m sorry, I really can’t help. For me it works in Google Chrome to drag and drop – you need to give me more information then.

  • sm1112 says:

    I haven’t coded in javascript for a while and I needed something to revisit and brush up…

    Thanks for sharing!

  • Robert Nyman says:

    sm1112,

    Glad you liked it!

  • Chris says:

    Maria, Chrome cannot read the image data natively on your computer. You must either run your code from localhost, or upload your code to your webspace and test it that way. Not sure why the author couldn’t explain this to you

  • Robert Nyman says:

    Chris,

    Maybe because it wasn’t clear that she was running it as a local file compared to localhost? She also mentioned that it didn’t work in Firefox, but running local files work fine in Firefox, not just in Google Chrome.

  • Monica says:

    Hi….
    I’m just learning….
    I’m wondering if it is possible to create a maze over an existing image using canvas? Any info would be greatly appreciated. Thanks for all that you share here—-awesome!!!

  • Robert Nyman says:

    Monica,

    It is definitely possible, but it would take some code to put it together.

  • Dan says:

    Here is the example running at jsfiddle.

    http://jsfiddle.net/dansalmo/eyyMJ/

  • Robert Nyman says:

    Dan,

    Thanks, but the link doesn’t work.

  • Michelle says:

    Just chiming in for people who couldn’t run this locally. I’ve run into the same problem before, where drag and drop doesn’t work by opening the html file in the browser. As another commenter said, you need to run a local server. On a Mac, this is how I did it.

    Open up Terminal
    Navigate to the folder (do this by typing “cd ” then dragging the folder onto Terminal for the path), hit Enter
    then type: “python -m SimpleHTTPServer” hit Enter
    Then it should say something like: Serving HTTP on 0.0.0.0 port 8000 …
    Now you have a server running, here it’s on port 8000, but it can be different for you.

    In your browser, you can then use the site by navigating to: http://localhost:8000/myfile.html
    where “8000” is the port number

    I hope that helps.
    Great tutorial, btw. I’m about to use it in a project!

  • Robert Nyman says:

    Michelle,

    Thanks, and also thanks for the tip for people!

  • Kevin Robles says:

    is it the same with this http://www.customink.com/lab

    wherein from list you can click a image then it will show to the canvas and you can move it freely within the set bounds? and you can save it?

  • Robert Nyman says:

    Kevin,

    Well, it’s similar. More of a coding exercise, really.

  • Timwillhack says:

    Hey Robert,

    Thanks for the concise example. When I run it in chrome the painbrush is offset incorrectly.

    Should line 45 be:
    context.lineTo(evt.offsetX+1, evt.offsetY+1);

    Rather than:
    context.lineTo(evt.layerX+1, evt.layerY+1);

    • Robert Nyman says:

      So, the post is three and a half years old, so can’t really say what code will still be working across the board. But layerX works in the other web browsers, but WebKit/Blink rendering engines. So for that, you need to do a workaround and some code branching (googling layerX will show you people’s problems with it and alternatives).

  • Milan says:

    hi, thanks for this example, hopefully you are still around πŸ™‚
    if you can remember this old code… I found it highly usable, but I don’t know how could I load image from url (drop it from another site), code works only for files dropped from my computer. If you have any ideas how to do this, please drop me email…Thanks in advance for any hint!

Leave a Reply to Robert Nyman Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.