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.
What's the one thing every developer wants? More screens! Enhance your coding experience with an external monitor to increase screen real estate.
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!
Hey! This is great tutorial, but I am having troubles to find last part… Is it out already?
Nope, its not out yet, but hopefully will be soon!
The problem is that I’m not able to combine all those tools in one script and determine, which tool I’m currently using, and draw with it. Maybe you could take look at my script, and help me out 🙂
http://jsfiddle.net/UK3sh/
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!
I fixed it, no problem 😉
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
http://jsfiddle.net/39Hak/
i posted it here.
Can you please provide complete source code with adding text on canvas part
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.
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..
Nice catch! I added this at line 84 to fix the issue (inside the
mouseup
event of tmp_canvas) –ctx.globalCompositeOperation = 'source-over';
.Cheers!
Hi Rishabh, thanks for the reply.. I tried to implement paint app , can be found http://cssdeck.com/labs/uoill26x . But facing problems adding text to canvas. Any help would be appreciated.
Thanks in advance
your text tool is not working if position of canvas is changed.. I have placed my fiddle on http://cssdeck.com/labs/uoill26x .. please suggest how to make add text part to work smoothly..
@ Mahendra , DId you Find any work arroud on Text – tool not Working issue, Because i am also using your http://cssdeck.com/labs/uoill26x Code. it has also one issue on “Eraser”. Once it use, After it user can’t draw other Shapes.
@Mahendra , i have Resolved . All The issues.:D
mahendra do you have the full source code of this tutorial?
thanks buddy
How did you solve the erase issue?
@mihir, I don´t find the failure… Can you help out with your solution?
Hi mihir/Mahendra, Could you please post here the resolution steps for following issues
1) Other tools are not working once we use “Eraser” tool.
2) “Text tool” is not working.
Thanks
Vinay
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
is there like a copyright or anything to use this code? I want to use this on my website. is that okay?
Yeah feel free to use it. If you want to credit that’s cool, if not don’t worry! 🙂
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.
Sorry buddy, haven’t found time to complete the last part. Hopefully it’ll be done sometime soon.
Can I know if this has been already done and if so what is the link/URL to that.
Thanks!!
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/
Thank you very much for compiling this wonderful list!
Rishabh,
How the same example like drawing rectangle can be implemented on mobile device with touchstart, touchmove events.
how about implementing a polygon method to draw polygon of n sides ?
Good Idea! I’ll give it a shot when I find some time.
One question though, why ur text tool not working on chrome…it work just fine on firefox
Hello Rishabh…have you finished the final project for the tutorial you made?
Thank you!
Nope sorry, haven’t got time yet.
When its ready can you send it to my email please? [email protected]
Thanks Man!
A reader (Andrii Votiakov) tried to unify all the tools. Here’s the code – http://cssdeck.com/labs/2hkvnxnu – hope that’ll help someone!
Hi Rishabh,
Thank you for the great tutorial.
For the Text Tool issue, I move the mouse up event into double click. So when user finish typing the double click on temporary canvas to hide text area. It solve my issues on Chrome. Hope it helps
tmp_canvas.addEventListener(‘dblclick’, function() {
//I am checking which tool that currently selected
if (theCanvasAttr.get(“tool”) == ‘text’){
//Emptying Textarea and hide it
theCanvasAttr.get(“textAreaTool”).style.display = ‘none’;
theCanvasAttr.get(“textAreaTool”).value = ”;
}
}, false);
Thank you so much for this! In this series or posts, you pulled together some great things that really helped fill-in the gaps in my knowledge! I appreciate all your hard work and writing!!