New Website

I've made a new website, as lynx.io is dead. You can find it here: http://macr.ae/—it's similar in format to lynx.io, but has better articles, and they're all written by me.

JavaScript disabled

While it will still mostly work, a lot of this site's functionality relies on JavaScript - please enable it for the best experience.

Making a Simple Drawing Application using RaphaëlJS

Raphaël is a library for working with vector graphics in JavaScript. It uses SVG, and boasts support for pretty much all browsers - Firefox 3.0+, Safari 3.0+, Chrome 5.0+, Opera 9.5+ and Internet Explorer 6.0+. Every object created is also a DOM object, meaning that you can treat them as such, adding event handlers the normal way.

In this tutorial, I'll be showing you how you can create a simple drawing application using Raphaël. It will just support basic line drawings, and you will be able to change the width and colour of the line. It will support all the browsers mentioned above, as I'm using jQuery to do stuff like event handling.

All the examples in this article are live, and using Raphaël. I'd advise against looking at the source for this article to see how I have done stuff, as I have changed it a bit so that I can get it into the page without it looking funny. This article seems to be having some issues with Firefox. If the examples don't work, try it in Chrome.

This article has been translated to Serbo-Croation by Anja Skrba.

Step one: setting up Raphaël

To set up Raphaël, put the following HTML onto your page (for non-HTML users, you'll want to add type attributes to the script tags):

<div id="canvas"></div>
<script src="raphael-min.js"></script>
<script>
    var canvas = document.getElementById('canvas'),
        paper = new Raphael(canvas, 500, 700);
</script>

Add the following to your stylesheet:

#canvas svg {
    height: 500px;
    width: 700px;
    margin: 0 auto;
    margin-top: 50px;
    border: 1px black solid;
}

We're storing the canvas variable for later use. Feel free to adjust the styling as you wish. That will just look like this, and won't do anything:

As you can see, it is fairly boring. You do have the paper variable that Raphaël uses though, so with a little more code you can do stuff.

You can test whether it is working by adding the following to the script:

paper.circle(350, 250, 100);

That will have created a circle in the center of the canvas with a radius of 100px. It should look like this:

Step two: Drawing lines

This step involves two things. The first is detecting the movement of the mouse (and whether it is down), and the second is drawing the line from that. Both are fairly easy to achieve, so I'll give you all of the code and then explain it:

var canvas = document.getElementById('canvas'),
    paper = new Raphael(canvas, 500, 700),
    mousedown = false,
    lastX, lastY, path, pathString;

$(canvas).mousedown(function (e) {
    mousedown = true;

    var x = e.offsetX,
        y = e.offsetY;

    pathString = 'M' + x + ' ' + y + 'l0 0';
    path = paper.path(pathString);

    lastX = x;
    lastY = y;
});
$(document).mouseup(function () {
    mousedown = false;
});

$(canvas).mousemove(function (e) {
    if (!mousedown) {
        return;
    }

    var x = e.offsetX,
        y = e.offsetY;

    pathString += 'l' + (x - lastX) + ' ' + (y - lastY);
    path.attr('path', pathString);

    lastX = x;
    lastY = y;
});

This does a few more things than the previous code sample. First, it declares some more variable which we will be using throughout the code. Then, it adds a listener to the mousedown event on the canvas, and a listener to the mouseup event on the document. The listener on mousedown will set the initial x and y values of the mouse (because the input that Raphaël accepts wants points relative to the previous point, not relative to the top left of the canvas like with the initial point) and start off the path, creating the path and pathString variables which we will use on the mousemove event. The mousedown event will finally set two variables, lastX and lastY. the reason that we set these is so that we can work out the relative coordinates for the mousemove event.

In the mouseup event, we just set the mousedown variable to false. The reason that we use this variable is so that in the mousemove event handler we can tell whether the mouse is down or not - we don't want to draw the line when the mouse isn't pressed. The reason that we're adding the event listener to the document and not the canvas is that we want it to stop drawing wherever the mouse is: otherwise if we click down on the canvas, drag the mouse to another place on the document that isn't the canvas, and then let go of the mouse, it will still draw. This is a mistake that a lot of Flash games make, and it isn't one that we want to make.

In the mousemove event listener, we first check that the mousedown variable is set to true, and stop if it isn't. We then find out the coordinates that we want the line to go to relative to the previous point, and update the path. We then reset the lastX and lastY variables to be the new x and y values.

Your canvas should now look and behave like this:

Magic. As you can see, it only supports 1px black lines so far, but we will be changing that in the next step…

Step Three: Changing the width of the line

Changing the width of the line is fairly easy. Again, this step involves two things: accepting the user input, and changing the width of the line from that information we have just been given.

We're going to use the path.attr method to change the thickness of the line. It works like this:

path = paper.path(pathString);
path.attr({
    'stroke-linecap': 'round',
    'stroke-linejoin': 'round',
    'stroke-width': 7 // Width in pixels
});

stroke-width sets the width of the line itself, and stoke-linecap and stroke-linejoin tells Raphaël that we want the corners and ends to be rounded (it looks odd if you don't; give it a try).

The other part of this step is to allow the user to change the width without going into the console or inspector and changing a variable. For this, we're just going to use the number keys on the keyboard. This will only allow us from 1px to 9px, but this is only a demo - if you're going to use this, you can build upon this (say, pressing any of the number keys opens a popup so that you can type your number and then press enter). We can do this using the following code:

$(document).keydown(function (e) {
    if (e.keyCode > 48 && e.keyCode < 58) {
        width = e.keyCode - 48;
    }
});

The keyCode of 1 is 49, to the keyCode of 9 which is 57. As they're numeric, it is safe to just take 48 from their value to get the width (although not too semantic, this should probably actually have a comment). We also have to declare the width variable at the top of the code with the other variables, or we will lose the value (or if you don't have strict mode enabled, it'll become global; even worse!).

Now, we have the following:

Note that on this page, pressing the number keys will change the width for every example on the page (except for the first three, which don't support width). Either refresh the page or just press 1 to go back to the original width.

Step three: Changing the colour of the line

Finally, we reach the last step. In this step, we add functionality to allow the user to change the colour of the line. This involves the same kind of things as before, but is a lot trickier as we're not just listening for keyboard events to accept user inputs, we're creating some coloured boxes for them to click. Whenever I write "colour" in this section, I do mean "color", of course.

Changing the colour of the line itself is fairly easy: we just need a colour variable at the top of the function (defaulting to "black", or whatever) and a 'stroke' attribute on the .attr call we added in the previous step.

Now we just have to get the information from the user (what colour they want). To do this, we will make some coloured boxes to the left of the canvas. Replace the HTML with the following:

<div id="canvaswrapper">
    <div id="canvascolours">
        <div data-colour="black"></div>
        <div data-colour="red"></div>
        <div data-colour="orange"></div>
        <div data-colour="yellow"></div>
        <div data-colour="green"></div>
        <div data-colour="blue"></div>
        <div data-colour="indigo"></div>
        <div data-colour="violet"></div>
        <div data-colour="white"></div>
    </div>
    <div id="canvas"></div>
</div>

We're adding nine colours: the colours of the rainbow plus black and white. When adding or changing colours, you can use any colour string that CSS would accept (string, hex, rgb, rgba, whatever).

Replace the CSS with the following:

#canvas svg {
    width: 700px;
    height: 500px;
    float: left;
    border: 1px black solid;
}

#canvaswrapper {
    overflow: auto;
    width: 730px;
    margin: 0 auto;
}

#canvascolours {
    width: 26px;
    float: left;
}

#canvascolours [data-colour] {
    width: 18px;
    height: 18px;
    margin-bottom: 10px;
    cursor: pointer;
    border: 1px black solid;
}

For those of you not familiar with the syntax, [data-colour] selects elements with a data-colour attribute ([data-colour="red"] would refer to elements where the data-colour attribute is equal to "red").

I'm not setting the background colour in that CSS as I should, as attr(data-colour) was throwing an error in the browser I was using. We are instead using JavaScript, but that isn't a huge problem as the application won't work if it is disabled anyway. Add the following JavaScript to what is there already:

$('#canvascolours [data-colour]').each(function () {
    var $this = $(this),
        divColour = $this.data('colour');

    // Change the background colour of the box
    $this.css('background-color', divColour);

    // Add the event listener
    $this.click(function () {
        colour = divColour;
    });
});

That cycles through all the colour boxes we added just now, and sets the background colour and adds an event handler for the click event so that when the div is clicked, the colour variable is updated so that the colour of the next line is changed.

Our final application looks like this:

The final code

var canvas = document.getElementById('canvas'),
    paper = new Raphael(canvas, 500, 700),
    colour = 'black',
    mousedown = false,
    width = 1,
    lastX, lastY, path, pathString;

$(canvas).mousedown(function (e) {
    mousedown = true;

    var x = e.offsetX,
        y = e.offsetY;

    pathString = 'M' + x + ' ' + y + 'l0 0';
    path = paper.path(pathString);
    path.attr({
        'stroke': colour,
        'stroke-linecap': 'round',
        'stroke-linejoin': 'round',
        'stroke-width': width
    });

    lastX = x;
    lastY = y;
});
$(document).mouseup(function () {
    mousedown = false;
});

$(canvas).mousemove(function (e) {
    if (!mousedown) {
        return;
    }

    var x = e.offsetX,
        y = e.offsetY;

    pathString += 'l' + (x - lastX) + ' ' + (y - lastY);
    path.attr('path', pathString);

    lastX = x;
    lastY = y;
});

$(document).keydown(function (e) {
    if (e.keyCode > 48 && e.keyCode < 58) {
        width = e.keyCode - 48;
    }
});

$('#canvascolours [data-colour]').each(function () {
    var $this = $(this),
        divColour = $this.data('colour');

    // Change the background colour of the box
    $this.css('background-color', divColour);

    // Add the event listener
    $this.click(function () {
        colour = divColour;
    });
});

If you use this code, I'd appreciate a link back to this to this article. If you do anything interesting with it, make sure you share it in the comments!

About Callum Macrae:

Callum Macrae is the founder of lynx.io and a JavaScript developer from the United Kingdom. He is currently writing his first book, to be published by O'Reilly Media.

You can view more articles by this author here.

Tags: raphaëljs javascript drawing

Comments

Dom says:

All the canvases are blank for me :(

Callum Macrae says:

@Dom: What browser are you using, and are there any errors in the console? It worked in all the browsers I tested it in.

Dan Towner says:

They're not all blank for me (one has a circle, and the other has the colour box palette on the left), but none of them respond interactively. Firefox 3.6.7, on Fedora 13. No messages in the console either.

Callum Macrae says:

It looks to be a Raphaël problem, and It's happening in the latest version, too. I'm not really sure what I can do about it.

ps3 says:

Last screen did not work in firefox 15.0.1 but did work in safari

Narretz says:

Cool tutorial!

However, drawing doesn't work in Firefox 15. It works in Chrome 21.0.1180.89 m though.

Bill says:

Sorry - Not working in my FF15 or Chrome or IE9.

Looks a very interesting article, if you can work out what has messed it up!

;-)

Bill

Djules says:

Same problem for me, does not work in FF15.0.

Nice tutorial anymore ! ;)

Sssure says:

works fine in chrome 21.0.1180.89 running on linux. Great tutorial.

Callum Macrae says:

I added a note to the article about it not working in Firefox. Still not sure why it isn't, though :-(

SomeDude says:

Well, I did test this functionality too at the Windows XP machine and it is does work an newest Chrome (21.0....) and doesn't work at Firefox 16. It is definitely surprise me and showing simple thing - browser comparability.

Rob says:

You may want to check out this link.. http://bugs.jquery.com/ticket/8523... You're using offsetX and offsetY which don't work in firefox. Use pageX and pageY instead and subtract the location of your div $('#canvas5').position() to get the correct mouse position.

Callum Macrae says:

Dammit. Thanks, I'll fix it later!

Matt says:

Very cool, thanks for the hard work!

Once fixed for FF compatibility, could it be possible to export the result to an image file?

Callum Macrae says:

It is possible, but it isn't very easy. You wouldn't be able to export it as an image directly, you would have to save each path, and then reconstruct it on the client side.

To do it, you would have to add a "paths" array to the variable declarations. Then in the mouseup event, push pathString, colour and width to the array. You'll end up with something like this:

[
    {color: 'black', width: 1, path: 'M169,148L169,148'},
    {color: 'black', width: 1, path: 'M169,121L169,121L169,121L167,120L160,118L137,117L132,117L123,117L113,120L106,124L96,131L89,139L85,148L84,155L84,160L84,166L87,173L92,179L102,186L120,197L135,204L163,212L190,218L207,220L222,221L231,221L235,220L240,218L243,215L245,212L249,206L253,199L255,192L256,181L256,170L255,160L249,148L242,138L232,129L220,122L212,118L200,114L188,111L180,110L166,108L154,107L146,107L140,107L136,107L135,107L134,107'},
    {color: 'black', width: 1, path: 'M428,143L428,143'},
    {color: 'black', width: 1, path: 'M433,103L433,103L433,103L432,103L431,103L430,103L405,106L393,111L388,115L382,124L377,131L374,138L371,169L374,192L383,207L412,230L433,236L516,243L566,236L579,230L589,214L589,205L587,186L581,177L571,167L537,140L502,119L486,113L468,109L460,108L450,107L442,107L439,107L424,106L421,106'},
    {color: 'black', width: 1, path: 'M123,302L123,302L123,303L123,306L123,312L126,334L128,344L132,354L135,364L141,376L151,392L153,398L171,417L184,427L196,433L236,449L266,455L279,458L304,461L317,462L342,465L352,466L384,466L405,466L459,461L474,454L478,452L493,433L500,423L503,416L510,397L512,386L516,368L517,359L517,351L517,349L518,344L519,337L521,332L522,325L523,324L523,322L522,322L521,322L517,322L511,323L503,323L492,324L479,325L467,326L420,327L405,327L386,327L352,327L343,327L321,327L306,327L290,327L276,327L259,327L246,327L219,326L207,326L200,325L191,324L158,319L152,318L149,317L145,317L141,316L139,316L138,316L136,314L135,314L134,311L134,310L131,309L130,308L129,307L127,307L127,306L126,306L125,306L123,306'}
]

You can then reconstruct that server-side and save that (using one of many image generation libraries), or save it JSON, and send that to the client as-is to be generated there.

Hope that helps!

Ashraf Sabry says:

Greetings,

Thank you for this article. I've used your code in making a simple scribbling page for one of my clients. Oh, and I haven't noticed the CC licence until after I delivered the work.

I had to make some changes to make it work on Firefox and IE. First, I set the canvas div to float:left, for its borders to be aligned with the main SVG element borders, then I wrote some code to normalize offsetX and offsetY (required in mousemove and mousedown events). var pos = $("#canvas").offset(); e.offsetX = e.pageX - pos.left; e.offsetY = e.pageY - pos.top; This is because Firefox and IE sometimes set event.target to an SVG path element not to the SVG canvas, and this causes drawn lines to spike.

mar-tino says:

This code works in FF, Chrome, Opera, IE9: ` <script type="text/javascript" charset="utf-8"> $(function($){
var canvas = $('#canvas'), paper = new Raphael("canvas", 700, 500), colour = 'black', mousedown = false, width = 1, lastX, lastY, path, pathString;

            canvas.mousedown(function (e) {
                mousedown = true;

                var x = e.pageX - $(this).offset().left;
                var y = e.pageY - $(this).offset().top;

                pathString = 'M' + x + ' ' + y + 'l0 0';
                path = paper.path(pathString);
                path.attr({
                    'stroke': colour,
                    'stroke-linecap': 'round',
                    'stroke-linejoin': 'round',
                    'stroke-width': width
                });

                lastX = x;
                lastY = y;
            });
            $(document).mouseup(function () {
                mousedown = false;
            });

            canvas.mousemove(function (e) {
                if (!mousedown) {
                    return;
                }

                var x = e.pageX - $(this).offset().left;
                var y = e.pageY - $(this).offset().top;

                pathString += 'l' + (x - lastX) + ' ' + (y - lastY);
                path.attr('path', pathString);

                lastX = x;
                lastY = y;
            });

            $(document).keydown(function (e) {
                if (e.keyCode &gt; 48 &amp;&amp; e.keyCode &lt; 58) {
                    width = e.keyCode - 48;
                }
            });

            $('#canvascolours [data-colour]').each(function () {
                var $this = $(this),
                    divColour = $this.data('colour');

                // Change the background colour of the box
                $this.css('background-color', divColour);

                // Add the event listener
                $this.click(function () {
                    colour = divColour;
                });
            });
        });
    &lt;/script&gt;
    &lt;style&gt;
        #canvas{
            width: 700px;
            height: 500px;
            float: left;
            border: 1px black solid;
        }

        #canvaswrapper {
            overflow: hidden;
            width: 745px;
            margin: 0 auto;
        }

        #canvascolours {
            width: 26px;
            float: left;
        }

        #canvascolours [data-colour] {
            width: 18px;
            height: 18px;
            margin-bottom: 10px;
            cursor: pointer;
            border: 1px black solid;
        }
    &lt;/style&gt;

`

Sonu says:

Hi - Can you share an example on how to create rectangle, circle and text and lastly clear the canvas (not from the DOM) to redraw items?

Thanks.

p.s - All the code works flawlessly. Awesome.

says:

Add comment

 

You can use markdown in comments (press "m" for a cheatsheet).

Enable JavaScript to post a comment

Markdown Cheat Sheet

# This is an <h1> tag
## This an <h2> tag
###### This is an <h6> tag

Inline markup: _this text is italic_, **this is bold**, and `code()`.

[Link text](link URL "Optional title")
[Google](http://google.com/ "Google!")

![Alt text](image URL)

![This is a fish](images/fish.jpg)

1. Ordered list item 1
2. Ordered list item 2

* Unordered list item 1
* Unordered list item 2
* Item 2a
* Item 2b

And some code:

// Code is indented by one tab
echo 'Hello world!';

Horizontal rules are done using four or more hyphens:

----

> This is a blockquote

This is an <h1> tag

This an <h2> tag

This is an <h6> tag

Inline markup: this text is italic, this is bold, and code().

Link text Google

This is a fish

  1. Ordered list item 1
  2. Ordered list item 2
  • Unordered list item 1
  • Unordered list item 2
    • Item 2a
    • Item 2b

And some code:

// Code is indented by one tab
echo 'Hello world!';

Horizontal rules are done using four or more hyphens:


This is a blockquote

Toggle MarkDown / HTML (t), full reference or close this