Different Tools for Our Sketching Application

It is important for our painting application to have different tools like:

  • Line tool to draw straight lines.
  • Rectangle tool to draw rectangles.
  • Circle tool to draw circles.
  • Eraser tool for erasing purposes.
  • Color tool to choose colors.
  • … and more

We have already built our pencil tool in previous parts. In this part, we’ll focus on creating the aforementioned tools.

Line Tool

It is super simple to implement this. First we’ll need to save the start x/y positions of the mouse in the mousedown event.

var start_mouse = {x: 0, y: 0};
// ...
tmp_canvas.addEventListener('mousedown', function(e) {
	tmp_canvas.addEventListener('mousemove', onPaint, false);

	mouse.x = typeof e.offsetX !== 'undefined' ? e.offsetX : e.layerX;
	mouse.y = typeof e.offsetY !== 'undefined' ? e.offsetY : e.layerY;

	start_mouse.x = mouse.x;
	start_mouse.y = mouse.y;

	onPaint();
}, false);

Then, we’ll just need to use the basic canvas methods to draw the line:

var onPaint = function() {

	// Tmp canvas is always cleared up before drawing.
	tmp_ctx.clearRect(0, 0, tmp_canvas.width, tmp_canvas.height);

	tmp_ctx.beginPath();
	tmp_ctx.moveTo(start_mouse.x, start_mouse.y);
	tmp_ctx.lineTo(mouse.x, mouse.y);
	tmp_ctx.stroke();
	tmp_ctx.closePath();

};

Rectangle Tool

Now, since we have already done the Line tool, doing rectangle tool is super easy. All we need to do is replace the Line drawing canvas method calls with strokeRect.

var x = Math.min(mouse.x, start_mouse.x);
var y = Math.min(mouse.y, start_mouse.y);
var width = Math.abs(mouse.x - start_mouse.x);
var height = Math.abs(mouse.y - start_mouse.y);
tmp_ctx.strokeRect(x, y, width, height);

To get the x and y position for the new drawing, we just need to take the minimum of the x/y positions between the current mouse position and that when the drawing started, i.e., during the mousedown event.

Getting the width and height is also easy, just need to get the absolute value of the difference between the same x/y position of both the states.


Circle Tool

Using the arc method, circles can also be drawn.

var onPaint = function() {

	// Tmp canvas is always cleared up before drawing.
	tmp_ctx.clearRect(0, 0, tmp_canvas.width, tmp_canvas.height);

	var x = (mouse.x + start_mouse.x) / 2;
	var y = (mouse.y + start_mouse.y) / 2;

	var radius = Math.max(
		Math.abs(mouse.x - start_mouse.x),
		Math.abs(mouse.y - start_mouse.y)
	) / 2;

	tmp_ctx.beginPath();
	tmp_ctx.arc(x, y, radius, 0, Math.PI*2, false);
	// tmp_ctx.arc(x, y, 5, 0, Math.PI*2, false);
	tmp_ctx.stroke();
	tmp_ctx.closePath();

};

Ellipse Tool

The Javascript Canvas API does not really provide us with an easy to use method to draw ellipses. So we’ll have to nail down our own approach.

There’s one where you can draw a circle with the arc method and then scale it. Unfortunately, it’s hard to get the shape controlled with mouse activity (moves) on the canvas. This makes it obvious for us to think that the solution has to include multiple quadraticCurveTo or bezierCurveTo.

This SO thread has proven to be quite handy on this topic. I think we can definitely use the chosen answer ourselves to draw ovals ourselves and resize/rescale the on mouse movements.

function drawEllipse(ctx, x, y, w, h) {
  var kappa = .5522848;
      ox = (w / 2) * kappa, // control point offset horizontal
      oy = (h / 2) * kappa, // control point offset vertical
      xe = x + w,           // x-end
      ye = y + h,           // y-end
      xm = x + w / 2,       // x-middle
      ym = y + h / 2;       // y-middle

  ctx.beginPath();
  ctx.moveTo(x, ym);
  ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);
  ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);
  ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);
  ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);
  ctx.closePath();
  ctx.stroke();
}

Try drawing some ellipses now.


Here’s another quick experiment I made that uses the same code that we just discussed to draw ellipse (you can click and drag to create new ovals). It plots the control points that could be helpful for some of you in comprehending the logic.


Andrew Staroscik has also written a piece of code that doesn’t uses any curve method to draw an oval on canvas. Instead it calculates the exact points of the ellipse using a neat little formula and plots them using lineTo. This gives us a ton of control over the ellipse as we can specify the exact X/Y center, X radius, Y radius, angle of rotation, start angle and end angle. Computationally, it’s a bit slow.

If you want to dig further, then here’s another algorithm to draw conics that’s probably better and faster. If you come up with something interesting with this, then do share in the comments!

Eraser Tool

There are quite a few ways to create an eraser tool, depending upon your needs.

Pencil Tool

You can use the pencil tool to fill white colour. We have already discussed how to make the pencil tool before, just need to use a strokeStyle of white with it.

Using globalCompositeOperation for Erasing

The globalCompositeOperation property can act as an excellent eraser tool. Check out the chart for the different values and what they do at MDN.

The destination-out operation is a perfect fit in our case. Basically, when using this operation, anything that we draw on the canvas, makes those portions transparent and you can only see the portions on the canvas that do not overlap with the drawing done after setting this operation. It is important to note that, this time, we won’t draw on the temporary canvas, but on the main canvas because all the drawing exists on the main one. If we erase on the temporary canvas, we cannot erase anything off the main one. Quite obvious, right ?

So what’s the advantage of this method ? Well, using a pencil tool, you basically end up drawing white lines. This is kind of more proper, as you are erasing things, i.e., replacing parts of the drawing with transparent lines. So, just incase if you have some background image on the parent of the canvas element, it’ll show up on erasing using this approach, but if you draw white lines, they won’t show up.

Demo Time! Refresh the demo if you do not see an image loaded.


We had to repeat quite a bit of code to setup event handlers on the main canvas this time. With a framework like jQuery, you can always reduce your code.

Apart from adding extra events, the other key modification we have made is, added this piece of code to achieve the erasing effect:

ctx.globalCompositeOperation = 'destination-out';
ctx.fillStyle = 'rgba(0,0,0,1)';
ctx.strokeStyle = 'rgba(0,0,0,1)';
ctx.lineWidth = 5;

Also, in this case, we shouldn’t be using clearRect for whatever purpose, as that would clear all data from canvas and end up leaving nothing. If you remember, during paint, we clearRect the temporary canvas, not the main one.

Having Multiple Layers

If you want further more control over your erasing operations, like say you want to erase anything drawn on the kungfu panda image that we drew over the canvas, but not that image itself, then you’ll need to have multiple canvas for different layers and do all the managing yourself. You have to be super careful in that case as things could get quite a bit complex as well as frustrating.

Spray Tool

As the name says, a spray tool will spray the chosen color on the canvas. So how do we go about coding this tool ? Let’s see.

First of all, when a mousedown event is triggered, all you need to do is plot pixels in random spots in a given circular boundary. The mouse.x and mouse.y are going to be the center of that circlular area on which the spray is going to occur.

Now given that we have the center of the conic area where the spray is going to happen, we need to find out random x/y offsets to quickly plot pixels. Since we need both negative and positive numbers that we could add on mouse.x and mouse.y to get our random points, this recalls for trigonometry functions sine and cosine.

If you remember or know well enough, when you pass any angle from 0 to 360 degree (2 Pi radians) to the sine and cosine functions, you get values between -1 and 1. You can multiply a bigger number to those to increase the magnitude and use for any purpose you want to. For example, in our case we’ll use that to plot our points for the spraying.

var getRandomOffset = function(radius) {
	var random_angle = Math.random() * (2*Math.PI);
	var random_radius = Math.random() * radius;
	
	// console.log(random_angle, random_radius, Math.cos(random_angle), Math.sin(random_angle));
	
	return {
		x: Math.cos(random_angle) * random_radius,
		y: Math.sin(random_angle) * random_radius
	};
};

Multiplying Math.random() with 2 Pi radians is a quick way to get a random angle. When you multiply X (2 Pi in our case) with say 0.6 (returned by Math.random()) you’re basically getting 60% of X – which is fine to deal with in our context. It’s the same with multiplying the same with our spray area’s radius.

Using Math.cos/sin we get a random number between -1 and 1 and increase their magnitude by multiplying with our random radius. If you give it a second thought, this is actually basic trigonometry. This is a real cool way to generate our random pixel data.

Finally all we need to do is plot them using fillRect.

var generateSprayParticles = function() {
	// Particle count, or, density
	var density = 50;
	
	for (var i = 0; i < density; i++) {
		var offset = getRandomOffset(10);
		
		var x = mouse.x + offset.x;
		var y = mouse.y + offset.y;
		
		tmp_ctx.fillRect(x, y, 1, 1);
	}
};

You can tweak the density at your own wish. It’ll control the particle count in your spray.

This was pretty straightforward, but we’re still not done. Usually, if you keep your mouse (or fingers in real life) pressed without moving, then the spraycan will continue to spray. We need to achieve that now. As usual that’s not hard either.

We need to execute our painting operation at regular intervals inside our mousedown event.

// Inside our onmousedown event listener
sprayIntervalID = setInterval(onPaint, 50);

The variable sprayIntervalID is defined outside the mousedown event listener so that it can be accessed from our mouseup event listener to cancel the setInterval!

// Inside mouseup event listener
clearInterval(sprayIntervalID);

Wohoo! and we’re done with our ever-awesome spraying tool.


Color Chooser

A color chooser or a pallette is also going to be simple to integrate. All you’ll need to do is, get the color from the UI interaction and set it to strokeStyle or fillStyle depending upon the requirements.

Line Width Choosers

Setting different line widths from a list of options is similar to setting the color. Just get the line width value from UI interaction and set the value to lineWidth property on the canvas context.

Text Tool

You know what a text tool is, right ? One that you select and drag on the painting canvas to create a temporary box inside which you can type text. Finally, when you’re done with typing you click somewhere outside the temporary box and the written text gets added to the canvas. We’ll build that now!

The basic idea is to have a textarea as that temporary box that is positioned absolutely. This is how we create it dynamically.

var textarea = document.createElement('textarea');
textarea.id = 'text_tool';
sketch.appendChild(textarea);

Little bit of CSS:

#text_tool {
	position: absolute;
	border: 1px dashed black;
	outline: 0;
	display: none;
	
	font: 10px Verdana;
	overflow: hidden;
	white-space: nowrap;
}

We’ve been attaching onPaint on mouse move, this is how our new version of this method will look like:

var onPaint = function() {
	
	// Tmp canvas is always cleared up before drawing.
	tmp_ctx.clearRect(0, 0, tmp_canvas.width, tmp_canvas.height);
	
	var x = Math.min(mouse.x, start_mouse.x);
	var y = Math.min(mouse.y, start_mouse.y);
	var width = Math.abs(mouse.x - start_mouse.x);
	var height = Math.abs(mouse.y - start_mouse.y);
	
	textarea.style.left = x + 'px';
	textarea.style.top = y + 'px';
	textarea.style.width = width + 'px';
	textarea.style.height = height + 'px';
	
	textarea.style.display = 'block';
};

Now, as soon as you click (mousedown) and drag (mousemove), you’ll see a textarea appear with dashed black border, positioned absolutely. You can type anything inside it. But hey, how do we put the typed text inside the canvas when a click is triggered outside the textarea ?

Not to worry. All it requires is a few pieces of tricks wired up together. First we’ll split all the lines of our text into an array.

var lines = textarea.value.split('\n');
var processed_lines = [];

Then we iterate over each line and then over each character and add them to a div that we dynamically create.

for (var i = 0; i < lines.length; i++) {
	var chars = lines[i].length;
	
	for (var j = 0; j < chars; j++) {
		var text_node = document.createTextNode(lines[i][j]);
		tmp_txt_ctn.appendChild(text_node);
		
		// Since tmp_txt_ctn is not taking any space
		// in layout due to display: none, we gotta
		// make it take some space, while keeping it
		// hidden/invisible and then get dimensions
		tmp_txt_ctn.style.position   = 'absolute';
		tmp_txt_ctn.style.visibility = 'hidden';
		tmp_txt_ctn.style.display    = 'block';
		
		var width = tmp_txt_ctn.offsetWidth;
		var height = tmp_txt_ctn.offsetHeight;
		
		tmp_txt_ctn.style.position   = '';
		tmp_txt_ctn.style.visibility = '';
		tmp_txt_ctn.style.display    = 'none';
		
		// Logix
		// console.log(width, parseInt(textarea.style.width));
		if (width > parseInt(textarea.style.width)) {
			break;
		}
	}
	
	processed_lines.push(tmp_txt_ctn.textContent);
	tmp_txt_ctn.innerHTML = '';
}

Creation of the div and adding it to the DOM is initially done like this:

// Text tool's text container for calculating
// lines/chars
var tmp_txt_ctn = document.createElement('div');
tmp_txt_ctn.style.display = 'none';
sketch.appendChild(tmp_txt_ctn);

So what is that logic inside the inner for loop ? Actually, if you type a piece of line inside the textarea that is more than the visual width of it, the normal expected behaviour would be the trim the part that extends off the right edge. For that purpose, we iterate over each line and character of the text typed, and then add each character to a invisible div that we dynamically create and inject into the DOM. As soon as the div’s width is more than the textarea, this means that’s the part of line we need to show up in our painting and trim the excess characters.

Since the div is set to display: none;, we change it’s styling (alter values of position, visibility and display properties) to make it occupy some space in the layout and get it’s width/height from the offsetWidth/Height properties. Neat trick! Now you also know how to get the width and height of an invisible element that does not take any space in the layout, i.e., set to display: none;.

Each “processed” (trimmed) line is added to the processed_lines array that we’ll now use to fill our canvas with the text.

var ta_comp_style = getComputedStyle(textarea);
var fs = ta_comp_style.getPropertyValue('font-size');
var ff = ta_comp_style.getPropertyValue('font-family');

tmp_ctx.font = fs + ' ' + ff;
tmp_ctx.textBaseline = 'top';

for (var n = 0; n < processed_lines.length; n++) {
	var processed_line = processed_lines[n];
	
	tmp_ctx.fillText(
		processed_line,
		parseInt(textarea.style.left),
		parseInt(textarea.style.top) + n*parseInt(fs)
	);
}

Pretty easy piece of code. You can add bold or italic to tmp_ctx.font if you would like to, based on some dropdowns in the UI where the user can select. Underline texts are a little hard to achieve. The canvas API doesn’t directly supports it yet. The only way I can think of now is to smartly use lineTo() with stroke().

If you note, we got the font size and font family from the textarea’s styles. Usually, you’d want to build dropdown UI elements for that where the user can make appropriate choices.

Finally, we’ll just draw our tmp canvas on the main canvas. We’ll also hide and empty our textarea.

// Writing down to real canvas now
ctx.drawImage(tmp_canvas, 0, 0);
// Clearing tmp canvas
tmp_ctx.clearRect(0, 0, tmp_canvas.width, tmp_canvas.height);

// clearInterval(sprayIntervalID);
textarea.style.display = 'none';
textarea.value = '';

Here’s the full code:

	tmp_canvas.addEventListener('mouseup', function() {
		tmp_canvas.removeEventListener('mousemove', onPaint, false);
		
		var lines = textarea.value.split('\n');
		var processed_lines = [];
		
		for (var i = 0; i < lines.length; i++) {
			var chars = lines[i].length;
			
			for (var j = 0; j < chars; j++) {
				var text_node = document.createTextNode(lines[i][j]);
				tmp_txt_ctn.appendChild(text_node);
				
				// Since tmp_txt_ctn is not taking any space
				// in layout due to display: none, we gotta
				// make it take some space, while keeping it
				// hidden/invisible and then get dimensions
				tmp_txt_ctn.style.position   = 'absolute';
				tmp_txt_ctn.style.visibility = 'hidden';
				tmp_txt_ctn.style.display    = 'block';
				
				var width = tmp_txt_ctn.offsetWidth;
				var height = tmp_txt_ctn.offsetHeight;
				
				tmp_txt_ctn.style.position   = '';
				tmp_txt_ctn.style.visibility = '';
				tmp_txt_ctn.style.display    = 'none';
				
				// Logix
				// console.log(width, parseInt(textarea.style.width));
				if (width > parseInt(textarea.style.width)) {
					break;
				}
			}
			
			processed_lines.push(tmp_txt_ctn.textContent);
			tmp_txt_ctn.innerHTML = '';
		}
		
		var ta_comp_style = getComputedStyle(textarea);
		var fs = ta_comp_style.getPropertyValue('font-size');
		var ff = ta_comp_style.getPropertyValue('font-family');
		
		tmp_ctx.font = fs + ' ' + ff;
		tmp_ctx.textBaseline = 'top';
		
		for (var n = 0; n < processed_lines.length; n++) {
			var processed_line = processed_lines[n];
			
			tmp_ctx.fillText(
				processed_line,
				parseInt(textarea.style.left),
				parseInt(textarea.style.top) + n*parseInt(fs)
			);
		}
		
		// Writing down to real canvas now
		ctx.drawImage(tmp_canvas, 0, 0);
		// Clearing tmp canvas
		tmp_ctx.clearRect(0, 0, tmp_canvas.width, tmp_canvas.height);
		
		// clearInterval(sprayIntervalID);
		textarea.style.display = 'none';
		textarea.value = '';
	}, false);

Here’s your favourite demo:


Try changing the font size and font family of textarea in the CSS tab and see how the changes are reflected in the text tool’s usage too!

Just a little pointer, you can also do this with SVG, which is an interesting solution to the problem.

<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'>
    <foreignObject width='100%' height='100%'>
        
        <div xmlns='http://www.w3.org/1999/xhtml' style='font-size:13px;font-family:Verdana'>
            {text}
        </div>
        
    </foreignObject>
</svg>​

Using the foreignObject element, you can embed non-SVG document, inside an SVG document. This basically means what we just did, i.e., render XHTML inside our SVG XML.

Although even Internet Explorer (latest version is IE10 at the moment) does not support the foreignObject element.

Apart from these 2 approaches, there’s a drawWindow method available on the canvas context that would super easily help us make this tool quickly. Unfortunately, it’s Chrome only.

Undo and Redo Tools

For Undoing and Redoing Operations the simplest way is to convert the canvas to an image using ctx.toDataURL() after each operation and store them in an array. Then you just need to take out the array element by the appropriate index and draw that on the canvas. Something like this:

var img_data = undo_arr[index];
var undo_img = new Image();
undo_img.src = img_data.toString();

ctx.drawImage(undo_img, 0, 0);

This approach is pretty easy but can get eat up lots of memory with lots of images stored in the array. So you might want to take another approach by figuring out proper arguments for each step and store them as JSONs in the array. Then use the array to generate strokes/drawings for each step. This approach is definitely a lot more complex.

You could even take a hybrid approach smartly!

Up Next

That was a looong read. Hope it didn’t get too boring and proves to be useful. Now you also realize how helpful the temporary canvas is ? We kept on drawing on it (and clearing it) in most cases and eventually move its content to the main canvas.

In the next and final post, we’ll put together all our tools into a neatly designed paint app!

Share:

33 thoughts on “Different Tools for Our Sketching Application

        1. Rishabh Post author

          Well, you defined onPaint multiple times hence it won’t work.

          I tried to work on your code, cleared it up a bit (still it’s not the most optimized) – http://cssdeck.com/labs/jhgvzloe

          I hope to complete the final part (full blown paint app) and put the source code on github (and maybe even launch it as a separate website/app) soon.

          Hope that helps!

          Reply
  1. albanyeye

    Hi
    this is awesome.
    I already have a HTML paint app that is working well. I wont to have the option to be able to type in the canvas too.
    Essentially I want to select the type tool instead of the crayon tool and then start typing.
    How would I do that? Any help would be greatly appreciated.
    Thanks

    Reply
    1. Rishabh Post author

      I’ll try to do that asap and put code on github. Although I’m sure taking the bits from the post and assembling yourself shouldn’t be too hard.

      Reply
  2. Mahendra

    Nice Tutorial.. thanks ..!!
    I am currently working on a generic library for drawing.. I have almost completed but facing some problems.. after erasing, when i click on pencil at your demo on http://cssdeck.com/labs/jwvajze4/0 , its not drawing on canvas but erasing other part..
    Can you please suggest how to tackle this…
    Thanks in advance..

    Reply
    1. Rishabh Post author

      Nice catch! I added this at line 84 to fix the issue (inside the mouseup event of tmp_canvas) – ctx.globalCompositeOperation = 'source-over';.

      Cheers!

      Reply
  3. Jay

    Hey Rishabh , can you mail complete source code because i have tried but the code is not working. It is humble request i need this code in my final year project and i have very short time to complete the project, so please mail it , thanks in advance

    Reply
  4. Siddhant

    is there like a copyright or anything to use this code? I want to use this on my website. is that okay?

    Reply
  5. Raghu

    You mention in the last line that “In the next and final post, we’ll put together all our tools into a neatly designed paint app!” Can I please know if this has been already done and if so what is the link/URL to that.

    Reply
  6. maxmas

    Here are some sample, marked ★ is very wonderful.

    HTML5-PaintApp
    https://github.com/iamshanedoyle/HTML5-PaintApp

    Small example of a html5 paint app
    https://github.com/gonzalocasaseca/paint

    Paint-like webapplication written in Javascript, HTML5
    https://github.com/tiste/paint

    justPaint
    https://github.com/maximuze/justPaint

    fractal-paint
    https://github.com/sirXemic/fractal-paint
    Online little tool to create fractal-ish images from simple drawings.

    ★Paint-board
    http://gospodarets.com/developments/paint-board/
    Paint-board on HTML5 canvas (canvas, drag-and-drop, localstorage, js file API, AJAX)

    CanvasWebPaint
    http://github.com/boringmachine/CanvasWebPaint
    Paint app using HTML5, jQuery and PHP.

    doodlePaint5
    https://github.com/designoidgames/doodlePaint5
    Painting program for the Pokki platform, HTML5 and JS

    ★Painter
    https://github.com/yanhaijing/Painter
    demo: http://yanhaijing.github.io/Painter/

    Reply
  7. Sachin

    Rishabh,
    How the same example like drawing rectangle can be implemented on mobile device with touchstart, touchmove events.

    Reply

Leave a Reply

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

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>