Creating an HTML5 Paint App

HTML5 Paint App
There are several apps available today that allow one to draw freely on your touch screen smartphone. Be it to take notes, annotate photographs, capture signatures digitally or to play games like the popular OMGPOP Draw Something App.

In this article we’ll explore a way to achieve this using the HTML5 canvas element and a little JavaScript.


HTML5 Paint App Demo


The first step is to create our html page and include the canvas tag in the page’s body. The height and width need to be set on the height and width attributes of the canvas tag and not in CSS. Any CSS sizing of the canvas element merely stretches the canvas and does not size it properly.

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
            <style type="text/css">
            ...
            /*Included Later*/
            ...
            </style>
    </head>
    <body>
        <canvas id="canvas" width="500px" height="500px"> Your browser does not support the HTML5 "canvas" tag. </canvas>
    </body>
</html>

Next, we need to add event listeners for the mouse/touch events on the canvas that we want to use to paint.

To keep things organized, I chose to wrap my canvas in an object and called it DrawingArea. The contructor for the object accepts the canvas’ ID as it’s only parameter.

var DrawingArea = function(elementId) {
    this.canvas = document.getElementById(elementId);
    this.context = this.canvas.getContext("2d");
    this.penDown = false;
    this.lineStarted = false;
    this.touchMode = !!(navigator.userAgent.toLowerCase().match(/(android|iphone|ipod|ipad|blackberry)/));
    ...
}

The DrawingArea object is then given references to it’s canvas element and a 2d context for that canvas. The penDown and lineStarted variables will be used in drawing as demonstrated later. Lastly, the touchMode boolean variable indicates whether or not the app is currently executing within an Android WebView or on an iPad etc.. by doing a RegEx test on the useragent string.

As the mousedown & touchstart events are only fired once and not continuously while the cursor or one’s finger is dragged across the canvas, we will need to target the mousemove & touchmove events to draw our lines.

We’ll use touchstart & mousedown to indicate the start of a new line and touchend & mouseup to end it.

As we’d like this app to run in a web browser as well as an Android app, we can simplify matters by placing the appropriate event name into a placeholder variable as follows:

var DrawingArea = function(elementId) {
    ...
    this.touchMode = !!(navigator.userAgent.toLowerCase().match(/(android|iphone|ipod|ipad|blackberry)/));
    this.downEvent = this.touchMode ? 'touchstart' : 'mousedown';
    this.moveEvent = this.touchMode ? 'touchmove' : 'mousemove';
    this.upEvent = this.touchMode ? 'touchend' : 'mouseup';
    ...
}

Next we’ll add an EventListener for our downEvent in which we will set our DrawingArea’s penDown property to true. The call to e.preventDefault() is required for compatibility with older versions of Android.

var DrawingArea = function(elementId) {
    ...
    this.onDownEvent = function(e){
        e.preventDefault();
        this.penDown = true;
    }      
    this.canvas.addEventListener(this.downEvent, this.onDownEvent.bind(this), false);
    ...
}

I’ve used this.onDownEvent.bind(this) to ensure that references to the this keyword are correctly resolved no matter the context of the calling code.
See: Function.prototype.bind – developer.mozilla.org

The onMoveEvent is where our actual lines will be drawn. Our first order of business is to determine whether or not pen is to paper (and not just hovering over it) by checking the value of the this.penDown boolean flag. if this.penDown is false we simply exit the function.

Next, the coordinates of the mouse/touch event are calculated relative to the top left hand corner of the canvas, via the getTouchPos() or getMousePos() depending on the touchMode as determined earlier. These funcitons will be discussed in the next section.

var DrawingArea = function(elementId) {
    ...
    this.onMoveEvent = function(e){
        e.preventDefault();
        if (!this.penDown)
            return;
        var pos = this.touchMode ? this.getTouchPos(e) : this.getMousePos(e);

        if (!this.lineStarted) {
            this.context.beginPath();
            this.context.moveTo(pos.x, pos.y);
            this.lineStarted = true;
        } else {
            this.context.lineTo(pos.x, pos.y);
            this.context.stroke();
        }
    }
    this.canvas.addEventListener(this.moveEvent, this.onMoveEvent.bind(this), false);
    ...
}

Following the retrieval of the pen’s position we can start drawing our line.
The first time this function is called in any stroke, this.lineStarted will be equal to false, indicating that a new line must be started.

The this.context.beginPath() function is called to indicate to the canvas context that a path should be started. this.context.moveTo(pos.x, pos.y) is then called to move the pen to the coordinates of the line’s starting point on the canvas without drawing anything yet. We then set this.lineStarted = true so that subsequent calls to the onMoveEvent do not repeat this step.

Instead, after the line has been started, subsequent calls to our onMoveEvent event handler will call this.context.lineTo(pos.x, pos.y) to indicate to the canvas context that a line should be drawn to the current coordinates and this.context.stroke() to paint the path using the stroke function.

Finally, we need to reset the penDown and lineStarted variables when the left mouse button is released or one’s finger raised off of the touch screen.

var DrawingArea = function(elementId) {
    ...
    this.onUpEvent = function(e) {
            e.preventDefault();
            this.penDown = false;
            this.lineStarted = false;
        }
    this.canvas.addEventListener(this.upEvent, this.onUpEvent.bind(this), false);
    ...
}

To do this I’ve added an event handler for the mouseup & touchend events as above.

Calculating Mouse/Touch Event Coordinates on the Canvas

As promised, the methods for determining the coordinates at which a touch/mouse event was fired relative to the top, left of the canvas follow. The event arguments supplied to our event handlers in each case contain properties that contain the coordinates relative to the top left of the entire document. The trick is to simply subtract the top/left measurement of the canvas itself from the supplied coordinate to calculate the coordinate relative to the canvas as follows.

var DrawingArea = function(elementId) {
    ...
    this.getMousePos = function(e) {
            var rect = canvas.getBoundingClientRect();
            return {
                x : e.clientX - rect.left,
                y : e.clientY - rect.top
            };
        }

    this.getTouchPos = function(e) {
        var rect = this.canvas.getBoundingClientRect();
        var touch = e.targetTouches[0];
        return {
                x : touch.pageX - rect.left,
                y : touch.pageY - rect.top
        };
    }
    ...
}

I want my canvas to fill the entire screen, and so the following function, onScreenSizeChanged() can be called to re-size the canvas to match the screen height and width. To make use of it, the HTML body element should be updated as follows <body onresize="drawingArea.onScreenSizeChanged()"> or an EventListener added programaticaly. This also takes care of smartphone/tablet orientation changes.

var DrawingArea = function(elementId) {
    ...
        this.onScreenSizeChanged = function () {
            if ((this.canvas.width != window.innerWidth ||
                this.canvas.height != window.innerHeight) &&
                window.innerWidth > 0 &&
                window.innerHeight > 0) {
                    this.canvas.width = (window.innerWidth);
                    this.canvas.height = (window.innerHeight);
            }
        }
        this.onScreenSizeChanged();//Set the initial canvas size
}

Changing Color & Line Width

To allow users to select the color and line width we can add an input for each of type “color” and “number” respectively.
HTML5 compatible browsers, like Google Chrome will display a color picker for <input type="color" .... Most modern browsers should render a numbers-only text box at the very least for <input type="number" ....
I’ve also added a button to clear the canvas.

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
            <style type="text/css">
            ...
            /*Included Later*/
            ...
            </style>
    </head>
    <body onresize="drawingArea.onScreenSizeChanged()">
        <canvas id="canvas" width="500px" height="500px"> Your browser does not support the HTML5 "canvas" tag. </canvas>
        <div id="controls" class="hidden">
            <div class="control-group">
                <label for="color-input">Color</label>
                <input id="color-input" class="input" type="color" value="#000000"></input>
            </div>
            <div id="color-buttons" class="control-group">
            </div>
            <div class="control-group">
                <label for="brush-size">Brush Size</label>
                <input id="brush-size" class="input" type="number" min="1" value="1"></input>
            </div>
            <div class="control-group">
                <a id="clear-canvas">Clear Canvas</a>
            </div>
        </div>
        <div id="controls-opener">
            <svg width="30px" height="30px">
                <g id="open-controls-button">              
                    <line x1="6" y1="6" x2="24" y2="6" style="stroke-width: 3; stroke: white;" />
                    <line x1="6" y1="12" x2="24" y2="12" style="stroke-width: 3; stroke: white;" />
                    <line x1="6" y1="18" x2="24" y2="18" style="stroke-width: 3; stroke: white;" />
                    <line x1="6" y1="24" x2="24" y2="24" style="stroke-width: 3; stroke: white;" />
                </g>
            </svg>
        </div>
    </body>
</html>

As WebView on my Android phone does not yet support the <input type="color" ... element. I will also create a scrollable list of buttons for some popular colors in the <div id="color-buttons" ... div.

I have opted to create another JavaScript object simply called “Controls” to manage my UI.

var Controls = function (drawingArea) {
    var _self = this;
    this.drawingArea = drawingArea;
    this.element = document.getElementById('controls');
    this.touchMode = this.drawingArea.touchMode;
    ...
}

The Controls object is constructed by passing it a reference to the DrawingArea. a function scoped variable var _self = this is declared to simplify making references to the Controls object in an EventListener a little later. The DrawingArea passed as a parameter is assigned to this.drawingArea, a reference to the controls container is stored for fast access, and the drawingArea’s touchMode property is copied.

var Controls = function (drawingArea) {
    ...
    this.controlsOpener = document.getElementById('controls-opener');
    this.onControlsOpenerClicked = function (e) {
        e.preventDefault();
        this.toggleControlsTray();
    }
    this.controlsOpener.addEventListener(this.drawingArea.upEvent, this.onControlsOpenerClicked.bind(this), false);
    ...
}

Next, the controlsOpener is initialized. This is the little square button that is responsible for toggling the visibility of the controls menu. the onControlsOpenerClicked() event simply calls the this.toggleControlsTray() function that is yet to be defined.

var Controls = function (drawingArea) {
    ...
    this.colorInput = document.getElementById('color-input');
    this.colorInput.value = this.drawingArea.context.strokeStyle;
    this.onColorInputChanged = function () {
        this.drawingArea.context.strokeStyle = this.colorInput.value;
    }
    this.colorInput.addEventListener("change", this.onColorInputChanged.bind(this));

The onchange event for the <input type="color" ... element sets the canvas context’s strokeStyle property to the new value of the color picker control.context.strokeStyle can be set to any valid color descriptor string including '#ff0000', 'rgb(255,0,0)' and 'red'. Once this assignment has been made the color that is painted onto the canvas has effectively changed.

Next we’ll create the color-buttons control for browsers that do not yet support the <input type="color" ... element. First, I have declared an array of color objects as follows (I haven’t included the entire list that i used here as it’s a long one, just enough to reveal the format that I used).

var Controls = function (drawingArea) {
    ...
    var colors = [{ name: "Black", color: "#000000" },
          { name: "Gunmetal", color: "#2C3539" },
          { name: "Midnight", color: "#2B1B17" },
          { name: "Charcoal", color: "#34282C" },
          { name: "Dark Slate Gray", color: "#25383C" }, ...];

    this.colorButtonList = document.getElementById('color-buttons');
    for (i = 0; i < colors.length; i++) {
        var button = document.createElement('a');
        button.setAttribute('href', '#');
        //button.innerHTML = colors[i].name;
        button.color = colors[i].color;
        button.onclick = function (e) {
            _self.drawingArea.context.strokeStyle = this.color;
            _self.colorInput.value = this.color;
        }
        button.style.backgroundColor = colors[i].color;
        this.colorButtonList.appendChild(button);
    }
    ...
}

After declaring the colors and a reference to the button container is made, a for loop is used to run through the colors declared. For each color in the loop a button is created. The button’s background color set to the color in question. An onClick EventListener is defined that sets the canvas context’s current color, and finally the button is appended to the DOM as a child node of the ‘color-buttons’ div.

var Controls = function (drawingArea) {
    ...
    this.brushSizeSelector = document.getElementById('brush-size');
    this.brushSizeSelector.value = this.drawingArea.context.lineWidth;
    this.onBrushSizeSelectorChanged = function () {
        this.drawingArea.context.lineWidth = this.brushSizeSelector.value;
    }
    this.brushSizeSelector.addEventListener("change", this.onBrushSizeSelectorChanged.bind(this));
    ...
}

The brushSizeSelector control is created in a similar way to the colorInput control. The difference being that the value of the <input type="number" ... element is assigned to the canvas context’s lineWidth property after which the width of lines drawn by the app on the canvas is effectively changed.

The click event for the clearCanvasButton control begins by determining the dimensions of the canvas by calling canvas.getBoundingClientRect(). These dimensions are used in the following call to context.clearRect(x, y, width, height) where x & y are the top left coordinates of the area to clear and width & height are the size of the area to clear.

var Controls = function (drawingArea) {
    ...
    this.clearCanvasButton = document.getElementById('clear-canvas');
    this.onClearCanvasButtonClicked = function (e) {
        var rect = this.drawingArea.canvas.getBoundingClientRect();
        this.drawingArea.context.clearRect(0, 0, rect.width, rect.height);
    }
    this.clearCanvasButton.addEventListener("click", this.onClearCanvasButtonClicked.bind(this));
    ...
}

The .hide() and .show() functions simply add/remove the ‘hidden’ CSS class to the <div id="controls" ... element. the hidden class simply sets display:none;. the controlsOpener button is moved all the way over to the left when the controls menu is hidden and next to it when it is displayed.

.toggleControlsTray() will call .hide() or .show() depending on the menu’s current visibility and is used as the click event for the controlsOpener button.

var Controls = function (drawingArea) {
    ...
    this.hide = function () {
        this.element.classList.add('hidden');
        this.visible = false;
        this.controlsOpener.style.left = '0px';
    }

    this.show = function () {
        this.element.classList.remove('hidden');
        this.visible = true;
        this.controlsOpener.style.left = this.element.getBoundingClientRect().width + 'px';
    }

    this.toggleControlsTray = function () {
        if (this.visible) {
            this.hide();
        } else {
            this.show();
        }
    }

    this.show();
}

With both the DrawingArea and Controls UI objects defined, they are instantiated as follows:

    var drawingArea = new DrawingArea('canvas');
    var controls = new Controls(drawingArea);

CSS

That only leaves the CSS which I have defined as follows:

body
{
    margin: 0px;
    padding: 0px;
    border: 0;
    overflow: hidden;
    font-family: Arial, sans-serif;
}
#canvas
{
    position: absolute;
    top: 0px;
    left: 0px;
    border: 0;
    margin: 0;
    padding: 0;
}
#controls
{
    position: absolute;
    display: block;
    top: 0px;
    left: 0px;
    height: 100%;
    width: 220px;
    border: 0;
    margin: 0;
    padding: 0;
    background-color: #333;
    opacity: .75;
    border-right: 2px solid #333;
}
.hidden
{
    display: none !important;
}
#controls label
{
    display: block;
    width: inherit;
    margin: 15px 5px 0 5px;
    font-size: 1.4rem;
    text-transform: uppercase;
    font-weight: bold;
    color: white;
}
#controls input, #controls select
{
    width: 200px;
    border: 5px solid #FFF;
    margin: 5px;
    font-size: 1rem;
}
.control-group
{
    border-bottom: 1px solid #333;
}
#color-buttons
{
    background-color: #CCC;
    height: 200px;
    width: inherit;
    overflow-y: scroll;
}
#color-buttons a
{
    width: 30px;
    height: 30px;
    margin: 5px;
    display: inline-block;
    border: 5px solid #FFF;
}
#clear-canvas
{
    display: block;
    border: 5px solid #FFF;
    margin: 5px;
    padding: 3px;
    text-align: center;
    color: white;
}
#controls-opener
{
    position: absolute;
    display: block;
    top: 0px;
    left: 0px;
    height: 30px;
    width: 30px;
    border: 0;
    margin: 0;
    padding: 0;
    background-color: #333;
    opacity: 0.75;
}
  • Hanzen Lim

    Greate tutorial. Thanks for sharing!!!

  • Guest

    Great tutorial. Thanks for sharing!!!
    but.. how do we get it to work with touch screens?

    • SheldonNeilson

      Hi. I’ve only tested this on my Android smartphone but it’s working fine on that in the browser and embedded in an HTML5 app.

      This should cater for most modern browsers as is but there are always special cases. If you’re having trouble you could strip out the section that attempts to determine whether to use mouse or touch events and just use touch events if it’s only going to be used on a touch screen. Alternatively modify the user agent string regex to ensure that you’re getting a match with your device. If that doesn’t work, you’d have to make sure that the events that are raised by by the devices browser match those that are used here and modify the event listener bit accordingly.

  • Amber Webster

    Thank you so much! This is great!

  • C Beth Holtzman

    This is great but is there a way to add a typing feature or drag and drop?