A Simple Touchscreen Sketchpad using Javascript and HTML5

April 21, 2014 at 8:13 pm
touchscreen

From an original image by Sean MacEntee

With the release of Windows 8, and the huge popularity of iPads and Android tablets, touchscreen is being built into more and more traditional PCs.

However, seeing dedicated touchscreen features on websites is still comparatively rare.

Here we’ll show you how to use some basic HTML and Javascript to add a simple touch-enabled sketchpad to a page.

We’re not trying to build a full-featured touchscreen app here – just a simple, very lightweight sketchpad, but it should be enough to show you some of the basic ideas behind touch support on the web.

If you are thinking of making a full HTML 5 touchscreen site or app, you might want to check out something like jQuery Mobile, however it’s worth going through the pure Javascript version here to get an understanding of the interactions between the HTML 5 canvas, and the mouse and touchscreen functions.

Compatibility note: We going to use the most common “Touch Events” approach here which is supported by all browsers on iOS and Android. Unfortunately, this won’t work directly on Windows 8 touchscreen when using Internet Explorer, however, it can be adapted relatively easily using the alternative “Pointer Events” model.

See the “Supported browsers” note towards the end of the article for more information.

Blank paper

First, we need something to draw on. Let’s start with a web-page with some text, and an area beside it for sketching. We can use the HTML 5 canvas tag to create our sketchpad area.

Edit the HTML for your page, add the canvas tag with an id name of your choice, and change the dimensions to fit your layout.

<canvas id="sketchpad" width="400" height="300"></canvas>

This creates our blank canvas. Now let’s try to make it into a working sketchpad. We can do this using the mouse first, then add touchscreen support later.

So what happens in a sketchpad?

When the cursor/pointer is over the sketchpad, and a mouse button pressed, then we want to draw something at that location.

Since our drawing action will take place when the mouse button is pressed down, we can put all of our code into a function that’s called when this happens.

We need two things:

  • A function to draw a dot (filled circle) at the specified location on the canvas
  • A way to call this function when the mouse button is being held down, and give it the current position of the mouse

Let’s try to do the drawing function first.

We need to write some small Javascript code to do this.

<script type="text/javascript">
    // Get the specific canvas element from the HTML document
    var canvas = document.getElementById('sketchpad');

    // If the browser supports the canvas tag, get the 2d drawing context for this canvas
    if (canvas.getContext)
        var ctx = canvas.getContext('2d');

    // Draws a dot at a specific position on the supplied canvas name
    // Parameters are: A canvas context, the x position, the y position
    function drawDot(ctx,x,y) {
        // Let's use black by setting RGB values to 0, and 255 alpha (completely opaque)
        r=0; g=0; b=0; a=255;

        // Select a fill style
        ctx.fillStyle = "rgba("+r+","+g+","+b+","+(a/255)+")";
        // Draw a filled circle
        ctx.beginPath();
        ctx.arc(x, y, size, 0, Math.PI*2, true);
        ctx.closePath();
        ctx.fill();
    }
</script>

Now we can draw on the canvas at a location of our choice, using the drawDot function.

When to Draw

When do we want to draw a dot on the sketchpad?

There are two cases (assuming the mouse is positioned over the sketchpad):

  • Immediately after the mouse button is pressed
  • Immediately after moving the mouse with the button held down

There is a slight complication here, since we can’t directly check if the mouse button is being held down using Javascript.

We have to keep track of when the button is pressed and released ourselves, in order to determine whether it’s currently being held down.

Tracking the mouse

track-mouse

Image credit: Sonja Trpovski

 

In fact, we have 3 useful triggers or “events” to help us keep track of the mouse:

  • mousedown event – Activated once whenever the (left) mouse button is pressed.
  • mouseup event – Activated once whenever the (left) mouse button is released.
  • mousemove event – Activated each time that the mouse is being moved, regardless of the button state.

So, we will need to keep track of the mouse button status by using the mouseup and mousedown triggers.

Additionally, we can use mousemove to do something useful when the mouse is being moved.

What happens with these events normally?

Nothing happens – We will need to add event handlers to tell the browser to do something when that specific event is triggered.

Event handlers

What each handler will do is simply call a function of our choice immediately after the event is triggered. So we should have one function for each event.

Let’s call them sketchpad_mouseDown, sketchpad_mouseUp and sketchpad_mouseMove.

We’ll also use our own getMousePos function to get the current co-ordinates of the mouse pointer when it’s moved.

<script type="text/javascript">
    // Define some variables to keep track of the mouse status 
    var mouseX,mouseY,mouseDown=0;

    function sketchpad_mouseDown() {
        mouseDown=1;
        drawDot(ctx,mouseX,mouseY);
    }

    function sketchpad_mouseUp() {
        mouseDown=0;
    }

    function sketchpad_mouseMove(e) { 
        // Update the mouse co-ordinates when moved
        getMousePos(e);

        // Draw a pixel if the mouse button is currently being pressed 
        if (mouseDown==1) { 
            drawDot(ctx,mouseX,mouseY); 
        }
    }

    // Get the current mouse position relative to the top-left of the canvas
    function getMousePos(e) {
        if (!e)
         var e = event;

        if (e.offsetX) {
            mouseX = e.offsetX;
            mouseY = e.offsetY;
        }
        else if (e.layerX) {
            mouseX = e.layerX;
            mouseY = e.layerY;
        }
    }
</script>

So now we have our 3 event handlers each performing a function –

  • sketchpad_mouseDown – registers a mouse click and also draws a pixel at the current position.
  • sketchpad_mouseUp – simply de-registers the mouse click, so we know that the button is no longer being pressed.
  • sketchpad_mouseMove – checks to see if the mouse button is currently down, and if so, draws something at the current location.
  • getMousePos – updates our mouseX and mouseY variables with the current position of the mouse.

 

All we have to do now is attach our event handlers to the events themselves.

canvas.addEventListener('mousedown', sketchpad_mouseDown, false);
canvas.addEventListener('mousemove', sketchpad_mouseMove, false);
window.addEventListener('mouseup', sketchpad_mouseUp, false);

 

You might notice that we are using addEventListener with the canvas element in the first two cases, and then with “window” for the mouseup event.

This is because for mouse downs and mouse moves, we’re only interested when these happen over our sketchpad canvas.

However, someone could click the mouse button on the canvas, then keep it held down, but move the mouse to another part of the web page. If they then release the mouse button, we won’t see this event happening if we’re just looking at the canvas, and our mouse button tracking can become confused, and not register the button being released.

In order to avoid this, we will listen in the entire window for mouse up events, so that we can track them correctly even if they happen outside of our sketchpad box.

Note: You can also use “document.body” instead of window, however since our example is being displayed in this post within an iframe, using “window” should capture the mouseup event outside of the iframe, whereas using “document.body” won’t.

Putting it all together

Here’s what our mouse-based sketchpad looks like right now (try it!):

(Look further down this article for the full touchscreen version)


… and here’s the complete web-page code for the mouse-based version:

sketchpad-mouse.html

<html>
<head>
<title>Sketchpad</title>

<script type="text/javascript">
    // Variables for referencing the canvas and 2dcanvas context
    var canvas,ctx;

    // Variables to keep track of the mouse position and left-button status 
    var mouseX,mouseY,mouseDown=0;

    // Draws a dot at a specific position on the supplied canvas name
    // Parameters are: A canvas context, the x position, the y position, the size of the dot
    function drawDot(ctx,x,y,size) {
        // Let's use black by setting RGB values to 0, and 255 alpha (completely opaque)
        r=0; g=0; b=0; a=255;

        // Select a fill style
        ctx.fillStyle = "rgba("+r+","+g+","+b+","+(a/255)+")";

        // Draw a filled circle
        ctx.beginPath();
        ctx.arc(x, y, size, 0, Math.PI*2, true); 
        ctx.closePath();
        ctx.fill();
    } 

    // Clear the canvas context using the canvas width and height
    function clearCanvas(canvas,ctx) {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
    }

    // Keep track of the mouse button being pressed and draw a dot at current location
    function sketchpad_mouseDown() {
        mouseDown=1;
        drawDot(ctx,mouseX,mouseY,12);
    }

    // Keep track of the mouse button being released
    function sketchpad_mouseUp() {
        mouseDown=0;
    }

    // Keep track of the mouse position and draw a dot if mouse button is currently pressed
    function sketchpad_mouseMove(e) { 
        // Update the mouse co-ordinates when moved
        getMousePos(e);

        // Draw a dot if the mouse button is currently being pressed
        if (mouseDown==1) {
            drawDot(ctx,mouseX,mouseY,12);
        }
    }

    // Get the current mouse position relative to the top-left of the canvas
    function getMousePos(e) {
        if (!e)
            var e = event;

        if (e.offsetX) {
            mouseX = e.offsetX;
            mouseY = e.offsetY;
        }
        else if (e.layerX) {
            mouseX = e.layerX;
            mouseY = e.layerY;
        }
     }


    // Set-up the canvas and add our event handlers after the page has loaded
    function init() {
        // Get the specific canvas element from the HTML document
        canvas = document.getElementById('sketchpad');

        // If the browser supports the canvas tag, get the 2d drawing context for this canvas
        if (canvas.getContext)
            ctx = canvas.getContext('2d');

        // Check that we have a valid context to draw on/with before adding event handlers
        if (ctx) {
            canvas.addEventListener('mousedown', sketchpad_mouseDown, false);
            canvas.addEventListener('mousemove', sketchpad_mouseMove, false);
            window.addEventListener('mouseup', sketchpad_mouseUp, false);
        }
    }
</script>

<style>
/* Some CSS styling */
#sketchpadapp {
    /* Prevent nearby text being highlighted when accidentally dragging mouse outside confines of the canvas */
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
}
.leftside {
    float:left;
    width:220px;
    height:285px;
    background-color:#def;
    padding:10px;
    border-radius:4px;
}
.rightside {
    float:left;
    margin-left:10px;
}
#sketchpad {
    float:left;
    border:2px solid #888;
    border-radius:4px;
    position:relative; /* Necessary for correct mouse co-ords in Firefox */
}
</style>
</head>

<body onload="init()">
    <div id="sketchpadapp">
        <div class="leftside">
             Simple mouse-based HTML5 canvas sketchpad.<br/><br/>
             Draw something by holding down the mouse button or using single clicks.<br/><br/>
             On a touchscreen, tapping the area will register as a single mouse click.<br/><br/>

             <input type="submit" value="Clear Sketchpad" onclick="clearCanvas(canvas,ctx);">
        </div>
        <div class="rightside">
            <canvas id="sketchpad" height="300" width="400">
            </canvas>
        </div>
    </div>
</body>
</html>

Note that we have moved the initialisation of the canvas context and event handlers into a separate “init()” function that is called just after the page has finished loading (using “<body onload..>”).

We’ve also added a very simple “Clear” button which calls a function with a single line of code to clear the canvas.

Prevent nearby text being accidentally selected

You might notice that we’ve used this block of styling in the CSS of the div surrounding the sketchpad:

-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;

This is due to the fact that when you have a large area on the screen that involves dragging the mouse across, the mouse cursor can sometimes stray outside of this area.

When this happens, because the mouse button is being held down, it’s interpreted by the web browser as a request to select areas of text. This produces a distracting flash of highlighting on the page.

To prevent this happening, we can just use the above user-select statements in the relevant CSS area to prevent text surrounding the sketchpad from being “selectable”.

Adding Touchscreen support

plasma-ball

Image Credit: Melanie Davies

If you just want to play around with a touchscreen interface, it might seem like a lot of effort adding all of the mouse support above.

However, the touchscreen code is very similar to the mouse-based sketchpad code, so it’s useful to understand how the mouse version works before looking at touchscreen. Also, in most cases you’ll want to have a functioning mouse version working as a fallback.

 

Touch events

If you try the current mouse-based version on an iPad, it’s not very appealing. Moving your finger over the sketchpad scrolls it, and you can just about make a blot appear if you press on a single area.

This is because we’re not yet listening to the specific events that are being triggered when the canvas area is touched.

We are getting some limited interaction with a single tap on the sketchpad – this is because the tap is also being interpreted as a mouseclick, and so it will (eventually) fire the mousedown event handler.

However, there is a 300ms delay when using the mouse events like this on a touchscreen device.

This is because the device is waiting to see if the single-tap is actually a double-tap. When we add our touch event handlers, we won’t need to worry about this quirk, since the touch events are triggered instantly after touching the screen.

Just like the mouse version, we have access to at least 3 useful touch-based events:

  • touchstart event – Activated whenever a touch is registered on the associated area.
  • touchend event – Activated whenever a touch is released from the associated area.
  • touchmove event – Activated whenever a touch movement is detected in the associated area.

 

Let’s look at the code for dealing with these events.

We’re going to keep this code separate from the mouse-handling code, which leads to some bloat, but gives a bit more flexibility in cases where you need to perform a specific action only in response to a touch event.

Also, the code is a little simpler, since we don’t need to keep track of anything like the mouse button status – when a touchmove event is fired, we can already assume the equivalent of “the mouse button is being pressed” is happening.

We also don’t really need to do anything with the touchend event in this case, however you might need to add it in a more complex application to perform an event when a finger is lifted off the screen – for example to update some values. In this case, just copy the format of the touchstart event handler.

Note also that we’re just going to deal with non-multitouch support for the moment.

If you are working with a complex multitouch site or potential HTML app, you might want to look into something like hammer.js.

Here are the functions that we’re going to trigger on touch events.

<script type="text/javascript">
    // Define some variables to keep track of the touch position
    var touchX,touchY;

    function sketchpad_touchStart() {
        getTouchPos();
        drawDot(ctx,touchX,touchY,12);

        // Prevents an additional mousedown event being triggered
        event.preventDefault();
    }

    function sketchpad_touchMove(e) { 
        // Update the touch co-ordinates
        getTouchPos(e);

        // During a touchmove event, unlike a mousemove event, we don't need to check if the touch is engaged, since there will always be contact with the screen by definition.
        drawDot(ctx,touchX,touchY,12); 

        // Prevent a scrolling action as a result of this touchmove triggering.
        event.preventDefault();
    }

    // Get the touch position relative to the top-left of the canvas
    // When we get the raw values of pageX and pageY below, they take into account the scrolling on the page
    // but not the position relative to our target div. We'll adjust them using "target.offsetLeft" and
    // "target.offsetTop" to get the correct values in relation to the top left of the canvas.
    function getTouchPos(e) {
        if (!e)
            var e = event;

        if (e.touches) {
            if (e.touches.length == 1) { // Only deal with one finger
                var touch = e.touches[0]; // Get the information for finger #1
                touchX=touch.pageX-touch.target.offsetLeft;
                touchY=touch.pageY-touch.target.offsetTop;
            }
        }
    }
</script>

Prevent screen scrolling on touch-enabled elements

A very important part of adding touch support to a page is working out which elements should be immune to scrolling the page by swiping up or down.

There are some ergonomic considerations here – you don’t want to confuse the user by having large swathes of the screen un-scrollable. The key is to have intuitive, reasonably sized areas that perform some action based on touch, that don’t also scroll the page.

To prevent scrolling on individual page elements, we need to call the Javascript preventDefault() function at the end of our touchmove handler. You can see this in the sketchpad_touchMove function above.

Remember that we still have to attach our touch handlers above to the correct elements on the page.

We can do this as follows:

canvas.addEventListener('touchstart', sketchpad_touchStart, false);
canvas.addEventListener('touchmove', sketchpad_touchMove, false);

 

Note that we’re only attaching the event listeners to the canvas element.

If you decide to attach these listeners to a larger part of the page, or even the entire document, you would probably need to be careful in managing use of preventDefault() to stop swipe-scrolling – for example, you might only trigger it for certain touch co-ordinates or boundaries.

Also, even though we’re only capturing touchmove in relation to our canvas element, it will continue firing even if you drag outside of the canvas area, so be aware that the touch co-ordinates you get at this stage can be outside the bounds of the canvas.

Note that we’re using the most general form of retrieving the current “touch list” by using e.touches in getTouchPos().

You could also use something like e.targetTouches or e.changedTouches, which can give a slightly different list. This is more important for multi-touch applications though, compared to single-touch support.

Complete version

Now we have full touchscreen and mouse support:


… and here’s the complete code for the final version:

sketchpad-touch.html

<html>
<head>
<title>Sketchpad</title>

<script type="text/javascript">
    // Variables for referencing the canvas and 2dcanvas context
    var canvas,ctx;

    // Variables to keep track of the mouse position and left-button status 
    var mouseX,mouseY,mouseDown=0;

    // Variables to keep track of the touch position
    var touchX,touchY;

    // Draws a dot at a specific position on the supplied canvas name
    // Parameters are: A canvas context, the x position, the y position, the size of the dot
    function drawDot(ctx,x,y,size) {
        // Let's use black by setting RGB values to 0, and 255 alpha (completely opaque)
        r=0; g=0; b=0; a=255;

        // Select a fill style
        ctx.fillStyle = "rgba("+r+","+g+","+b+","+(a/255)+")";

        // Draw a filled circle
        ctx.beginPath();
        ctx.arc(x, y, size, 0, Math.PI*2, true); 
        ctx.closePath();
        ctx.fill();
    } 

    // Clear the canvas context using the canvas width and height
    function clearCanvas(canvas,ctx) {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
    }

    // Keep track of the mouse button being pressed and draw a dot at current location
    function sketchpad_mouseDown() {
        mouseDown=1;
        drawDot(ctx,mouseX,mouseY,12);
    }

    // Keep track of the mouse button being released
    function sketchpad_mouseUp() {
        mouseDown=0;
    }

    // Keep track of the mouse position and draw a dot if mouse button is currently pressed
    function sketchpad_mouseMove(e) { 
        // Update the mouse co-ordinates when moved
        getMousePos(e);

        // Draw a dot if the mouse button is currently being pressed
        if (mouseDown==1) {
            drawDot(ctx,mouseX,mouseY,12);
        }
    }

    // Get the current mouse position relative to the top-left of the canvas
    function getMousePos(e) {
        if (!e)
            var e = event;

        if (e.offsetX) {
            mouseX = e.offsetX;
            mouseY = e.offsetY;
        }
        else if (e.layerX) {
            mouseX = e.layerX;
            mouseY = e.layerY;
        }
     }

    // Draw something when a touch start is detected
    function sketchpad_touchStart() {
        // Update the touch co-ordinates
        getTouchPos();

        drawDot(ctx,touchX,touchY,12);

        // Prevents an additional mousedown event being triggered
        event.preventDefault();
    }

    // Draw something and prevent the default scrolling when touch movement is detected
    function sketchpad_touchMove(e) { 
        // Update the touch co-ordinates
        getTouchPos(e);

        // During a touchmove event, unlike a mousemove event, we don't need to check if the touch is engaged, since there will always be contact with the screen by definition.
        drawDot(ctx,touchX,touchY,12); 

        // Prevent a scrolling action as a result of this touchmove triggering.
        event.preventDefault();
    }

    // Get the touch position relative to the top-left of the canvas
    // When we get the raw values of pageX and pageY below, they take into account the scrolling on the page
    // but not the position relative to our target div. We'll adjust them using "target.offsetLeft" and
    // "target.offsetTop" to get the correct values in relation to the top left of the canvas.
    function getTouchPos(e) {
        if (!e)
            var e = event;

        if(e.touches) {
            if (e.touches.length == 1) { // Only deal with one finger
                var touch = e.touches[0]; // Get the information for finger #1
                touchX=touch.pageX-touch.target.offsetLeft;
                touchY=touch.pageY-touch.target.offsetTop;
            }
        }
    }


    // Set-up the canvas and add our event handlers after the page has loaded
    function init() {
        // Get the specific canvas element from the HTML document
        canvas = document.getElementById('sketchpad');

        // If the browser supports the canvas tag, get the 2d drawing context for this canvas
        if (canvas.getContext)
            ctx = canvas.getContext('2d');

        // Check that we have a valid context to draw on/with before adding event handlers
        if (ctx) {
            // React to mouse events on the canvas, and mouseup on the entire document
            canvas.addEventListener('mousedown', sketchpad_mouseDown, false);
            canvas.addEventListener('mousemove', sketchpad_mouseMove, false);
            window.addEventListener('mouseup', sketchpad_mouseUp, false);

            // React to touch events on the canvas
            canvas.addEventListener('touchstart', sketchpad_touchStart, false);
            canvas.addEventListener('touchmove', sketchpad_touchMove, false);
        }
    }
</script>

<style>
/* Some CSS styling */
#sketchpadapp {
    /* Prevent nearby text being highlighted when accidentally dragging mouse outside confines of the canvas */
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    -khtml-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
}
.leftside {
    float:left;
    width:220px;
    height:285px;
    background-color:#def;
    padding:10px;
    border-radius:4px;
}
.rightside {
    float:left;
    margin-left:10px;
}
#sketchpad {
    float:left;
    height:300px;
    width:400px;
    border:2px solid #888;
    border-radius:4px;
    position:relative; /* Necessary for correct mouse co-ords in Firefox */
}
#clearbutton {
    font-size: 15px;
    padding: 10px;
    -webkit-appearance: none;
    background: #eee;
    border: 1px solid #888;
}
</style>
</head>

<body onload="init()">
    <div id="sketchpadapp">
        <div class="leftside">
             Touchscreen and mouse support HTML5 canvas sketchpad.<br/><br/>
             Draw something by tapping or dragging.<br/><br/>
             Works on iOS, Android and desktop/laptop touchscreens using Chrome/Firefox/Safari.<br/><br/>
             <input type="submit" value="Clear Sketchpad" id="clearbutton" onclick="clearCanvas(canvas,ctx);">
        </div>
        <div class="rightside">
            <canvas id="sketchpad" height="300" width="400">
            </canvas>
        </div>
    </div>
</body>
</html>

Touchscreen browsers try to emulate mouse events

It’s worth clarifying an additional point at this stage – in order to try to get maximum compatibility with existing websites and their mouse event handling, browsers on touchscreen devices such as Mobile Safari will try their best to emulate a mouse when you tap on any page element that is responsive to mouse events, such as a link, or a div or canvas listening for mousedown, mouseup etc.

So unless we prevent this behaviour, we might get a double-acknowledgement when tapping on the sketchpad for example – the first event trigger will be a touchstart, followed shortly after by a mousedown.

To stop this happening, we simply use the preventDefault() call when we’re finished with our touchstart, in order to stop any further events from being triggered due to that specific touch action.

You might run into some bugs with this on older Android browsers, however, especially if you’re also using more complex interactions with touchend, so it may be worth fully testing this behaviour on a touchscreen device.

Also, only use preventDefault() in this context when you’re sure that you have completed whatever the action the user wanted.

If you listened for touchstart on the entire document page for example, and issued a preventDefault() regardless of where/what the user touched, you may find that they are no longer able to tap on links on the page, since you’ve stopped the further issuing of the “emulated” mousedown or mouseclick on the link.

So just use preventDefault() when you’re sure that you’ve covered all the potential user interaction within the touch events associated with that element.

Supported browsers/devices

The HTML 5 canvas element works in all modern browsers, including mobile/tablet. With Internet Explorer, it’s supported from version 9 upwards. The touch events (touchstart, touchmove..) work on all mobile/tablet browsers. The situation with desktop browsers is a little more involved.

IE has an alternative system called Pointer Events that is supported from IE 10 upwards. The problem here is that other browsers have yet to add support, but it’s expected to happen throughout 2014 and onward.

Firefox supports touch events, but due to a bug filed in bugzilla, they have been turned off by default in the past few releases. They can be re-enabled in the browser. Further details are here.

It’s still worth using the Touch events version now however, since it supports all current and legacy Android and iPad users for example, as well as Chromebook and other touchscreen users on desktops/laptops.

As more non-mobile users become touchscreen enabled, pressure will undoubtedly grow on browser developers to get everything working smoothly on the desktop too.

Upgrades

Once you’ve got to grips with the basic framework for the sketchpad, it’s easy to add features!


Change the pen size

Replace every instance of “12” in called to the DrawDot() function with a variable of your own.

You can then add images or buttons to the page which change the value of this variable when clicked.


Change the colours

Following the same method as changing the pen size, simply modify DrawDot() to use your own RGBA (red,green,blue,alpha/opacity) values.

Change these values based on some interaction with an on-screen widget (or use a counter to give a funky rainbow effect?).


Draw connected lines instead of dots

You might have noticed that if you move the mouse or touch position too fast, the rendering can’t keep up and you get some spaces between dots.

An alternative is to create a DrawLine function, similar to DrawDot, except that it keeps track of the last x,y position as well as the current one.

Now instead of using ctx.arc to draw a circle, use ctx.moveTo to position the start of the line at the old co-ordinates, then ctx.lineTo to draw the line to the new (current) co-ordinates (example here).

This changes the look and feel of the sketchpad, so it may or may not be an improvement on the “dot” method.

Update: Here is a working example of the sketchpad using lines (source code is here).

Note that we’ve added lastX and lastY variables, and set them to -1 at the start. These will be our starting positions when we draw a line, but since we don’t know where the first line will start from, we will use the -1 value to indicate that the current position is invalid, or unknown. There is no special significance to using -1, it could be any other co-ordinate value that won’t appear in normal use.

At the start of our new DrawLine function, we check if lastX is invalid (set to -1).

This will be the case the first time a line is drawn, or if the “pen” (mouse or finger) is not touching the canvas. If so, we will just set lastX and lastY to the current position, then the next time we call drawLine, we will be able to correctly draw a line with two valid sets of co-ordinates.

This introduces a slight bit of added complexity, since we need to invalidate the lastX and lastY co-ordinates any time the pen is lifted from the canvas. Otherwise, we would continue drawing a line from the last position, even when someone has lifted the “pen” and decided to draw in a different part of the canvas.

The solution is simply to set lastX and lastY to -1 in the mouseUp function, and also in a new touchEnd function:

function sketchpad_mouseUp() {
    mouseDown=0;

    // Reset lastX and lastY to -1 to indicate that they are now invalid, since we have lifted the "pen"
    lastX=-1;
    lastY=-1;
}

function sketchpad_touchEnd() {
    // Reset lastX and lastY to -1 to indicate that they are now invalid, since we have lifted the "pen"
    lastX=-1;
    lastY=-1;
}

…and also add the new touchEnd event listener to our init() function:

            canvas.addEventListener('touchend', sketchpad_touchEnd, false);

Line style

One final point we need to look at when drawing lines is setting the line “cap” style. This is simply how the end of the line looks like, but if it’s not changed from the default setting of “butt”, the lines can look disjointed when drawing, since not enough pixels are drawn to connect adjacent lines at different angles.

We can do this in the drawLine() function as follows:

        ctx.lineCap = "round";

You can try commenting out this line to see what happens without it. You can also use “ctx.lineJoin” to change how it looks when two lines join, but it’s not relevant to our sketchpad since we are only drawing a single line each time between two co-ordinates, instead of a group of lines at once.


Want to let users submit their pictures after drawing them?

Try using the canvas toDataURL() function. This will convert the entire canvas area into a base64 encoded .PNG image.

You can then use this data in a hidden form field with a submit button to post the encoded image to your server. You’ll also need to strip out some of the extraneous formatting and convert back from base64 to a binary image on the server side.

Check out the answer here using PHP by user568021.

Also see the image saving demo code below, which implements most of the steps to get the image on to the server – you would just need to modify the PHP script to save it (using e.g. the PHP function file_put_contents() ) instead of sending it back to the user.


I just want to save the Sketchpad as an image?

This is surprisingly a little more difficult than it first appears. There are a few different ways of saving the sketchpad, but they all start with converting the sketchpad data to an image format such as PNG first.

The difficulty is in sending this image to the browser as a download. If you’re OK about first sending the image to a script on a server, then this is the best way for the end-user (apart from a small delay while uploading and downloading the image.

Here is a demo page, and here is the HTML source.

Here is the source for the PHP script (“save-image.php”) we will use.

This is what happens:

  • A “Save Image” button is added to the Sketchpad page. This button is part of a HTML form.
  • When clicked, a function is called, let’s call it saveImageRemote().
  • This function converts the canvas data into an image (PNG) format using “toDataURL”.
  • The image data is added to a hidden variable in the form (“save_remote_data”) and the form is submitted.
  • A PHP script receives the form data, and converts it into a binary image. Before sending the image back to the browser, it adds Content headers to specify that it’s a downloadable file.
  • The browser receives the data back from the script and starts downloading the image.

Why do we have to go through the strange steps of uploading the image to a server just to immediately download it again?

It’s because we can’t easily force the browser to accept an image download, unless that download comes from a remote server.

There are ways of forcing the image data into the current browser page (for example, using “window.location.href=image” in the answers here),  however it doesn’t work very well – there is no .PNG file extension added, and the end-user can’t simply click on the file to open it.

By sending our image data to a remote script, we can add the content headers that tell the browser that it should download the file, and specify other data such as filename and filetype. The disadvantage of this approach is that it doesn’t work offline, and there may be a slight delay in uploading/downloading for large sketchpad images.

Note that iOS / Mobile Safari won’t process image downloads at all – they simply display within the browser, and the end-user has to long-tap to save them regardless.

I need to be able to save it offline

Looking at the previous example, we can also add a “View Image” button which displays the Sketchpad as a plain image in a new browser tab.

Alternatively, you can provide a thumbnail image of the sketchpad that updates every time the mouseup or touchend events are triggered. We do this in the example above using the “updateImageLocal()” function.

The user can then long tap or right-click on this thumbnail to save the image (the image will be saved as the full size version).


I would like to have more than one Sketchpad on the screen

There are a number of ways of doing this. One way is to create separate Javascript objects for each sketchpad (along with addition HTML canvas elements). It’s more complicated, but doesn’t require much extra code. We will need to get rid of the global canvas and ctx variables since they only allow for a single canvas and canvas context to be defined.

We will need to move these into a new ‘sketchpad()‘ function. We will also need to move the event handlers in here.

Check out a working example here, and adjusted source code here.

In our new init() function, we’re creating one or more sketchpads using our new sketchpad() function. This will mean that each separate sketchpad gets it’s own canvas and context variables.

To make this work properly, we need to preface any variables and functions that need to be kept local to either sketch1 or sketch2 (in our example), with “this.“.

this” is simply a reference to either sketch1 or sketch2. When we create sketch1 with the line sketch1 = new sketchpad(‘sketchpad1’) then any reference to “this” is equivalent to referencing the sketch1 object. When we create sketch2, “this” is a reference to sketch2.

This means that each set of context and canvas values don’t get shared or mixed up between sketch1 and sketch2.

The last modification needed is to add “.bind(this)” when we’re adding our event listeners. This ensures that the functions that are called when the event triggers, also use the correct value of “this“.

In other words, when we refer to “this.ctx” in the drawDot() line in sketchpad_touchStart(), it should use the same value of “this” as in the main “sketchpad()” function, and therefore refer to the correct canvas context.

The “.bind(this)” is used because when an event is triggered, we don’t normally easily have access to whatever the value of “this” is between where the event handler was added, and the function that gets called when it triggers. See here for more details. Bind is a relatively new addition to Javascript that is available in IE9 and any new browsers.

We can also now easily clear a specific canvas by simply using “sketch1.clearCanvas();” for example. Since it’s simply a normal function and not a browser event, we don’t need to deal with the bind keyword.


The canvas sketchpad should give you a good basis for understanding interactions between mouse events, touchscreen events and the canvas element, even if you decide to use an alternative framework later.

 

Did you find it useful or informative?

Were you able to use some of the ideas in projects of your own?

Let us know in the comments section below.