Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML5

HTML 5 Canvas - A Simple Paint Program

4.83/5 (5 votes)
1 Apr 2012CPOL5 min read 54.7K   1.3K  
A voyage of discovery into HTML5's cool Canvas object

Introduction

Here is a simple and fun introduction to the HTML5 canvas and how your users can interact with it.

Image 1

Code

Background 

HTML5 presents a whole new and exciting set of capabilities to the browser. The newest generation of browsers that support HTML5 support the canvas object. With some simple JavaScript, you can provide visitors to your website the ability to modify images.

The intention of this project was a simple proof of concept that allows an image to be loaded to a canvas and modified by the user. With some simple additions, this could be extended to save the new image to the server or the user's local computer.

Using the code  

The entire project is written in client-side JavaScript with some jQuery to provide some shortcuts. If you're not familiar with jQuery, it can be easily replaced with classic JavaScript.

A working sample is available here. Your browser must support HTML5 to work (Tested on IE9+ and FireFox 10+). Otherwise you'll get strange (if any) results.

Start with a blank HTML page. If you use jQuery, remember to include it.

 We'll start with a CanvasManager object. This JavaScript class provides encapsulation of the canvas and its helper methods.  Don't worry if this is a lot of code to digest at once. I'll detail it below.

JavaScript
//
// The CanvasManager class
//
      
     function CanvasManager(canvasId) {
            this.canvas = $('#' + canvasId);
            this.imgColor = [255, 255, 255];
            this.buttonDown = false;
            this.lastPos = [0, 0];
            this.mode;

            this.canvas.mouseup(function () {
                canvasMgr.buttonDown = false;
            });

            this.canvas.mouseout(function () {
                canvasMgr.buttonDown = false;
            });

            this.canvas.mousedown(function () {
                if (!canvasMgr.mode)
                    return;

                if (arguments.length > 0) {
                    var arg = arguments[0];
                    var ctx = canvasMgr.canvas.get(0).getContext("2d");
                    var offsets = getOffsets(arg);

                    if (canvasMgr.mode == 'dropper') {                        
                        var imgData = ctx.getImageData(offsets.offsetX, offsets.offsetY, 1, 1);

                        canvasMgr.imgColor[0] = imgData.data[0];
                        canvasMgr.imgColor[1] = imgData.data[1];
                        canvasMgr.imgColor[2] = imgData.data[2];

                        $('#dvSwatch').css('background-color', 'rgb(' + imgData.data[0] +
                            ',' + imgData.data[1] +
                            ',' + imgData.data[2] + ')');
                    }
                    else if (canvasMgr.mode == 'paint') {
                        canvasMgr.buttonDown = true;
                    }
                    else if (canvasMgr.mode == 'text') {
                        canvasMgr.lastPos[0] = offsets.offsetX;
                        canvasMgr.lastPos[1] = offsets.offsetY;
                    }
                }
            });

            this.canvas.mousemove(function (e) {
                var arg = e;
                var ctx = canvasMgr.canvas.get(0).getContext("2d");
                if (canvasMgr.mode == 'paint') {
                    if (canvasMgr.buttonDown) { //mousebutton down

                        var offsets = getOffsets(e);
                        var imgData = ctx.getImageData(offsets.offsetX - 5, offsets.offsetY - 5, 10, 10);

                        for (i = 0; i < imgData.width * imgData.height * 4; i += 4) {
                            imgData.data[i] = canvasMgr.imgColor[0];
                            imgData.data[i + 1] = canvasMgr.imgColor[1];
                            imgData.data[i + 2] = canvasMgr.imgColor[2];
                            imgData.data[i + 3] = 255;
                        }

                        ctx.putImageData(imgData, offsets.offsetX - 5, offsets.offsetY - 5);
                    }
                }
            });

            $(window).keypress(function (e) {
                if (canvasMgr.mode == 'text') {
                    var canvasDOM = canvasMgr.canvas.get(0);
                    var ctx = canvasDOM.getContext("2d");
                    ctx.font = "20pt Arial";
                    ctx.textBaseline = "bottom";
                    ctx.textAlign = "left";
                    ctx.fillStyle = 'rgb(' + canvasMgr.imgColor[0] +
                            ',' + canvasMgr.imgColor[1] +
                            ',' + canvasMgr.imgColor[2] + ')';

                    ctx.fillText(String.fromCharCode(e.which), canvasMgr.lastPos[0], canvasMgr.lastPos[1]);
                    var textMTX = ctx.measureText(String.fromCharCode(e.which));
                    canvasMgr.lastPos[0] += textMTX.width + 1;
                }
            });
        }

        CanvasManager.prototype.setMode = function (mode) {
            this.mode = mode;
            var cur;
            switch (this.mode) {
                case 'text':
                    cur = 'text';
                    break;
                case 'dropper':
                    cur = 'pointer'
                    break;
                case 'paint':
                    cur = 'crosshair';
                    break;
                default:
                    cur = 'default';
                    break;

            }
            var localCanvas = this.canvas;
            
            this.canvas.mouseover(function () {
                localCanvas.css('cursor', cur);
            });
        }

        CanvasManager.prototype.drawImage = function (image, x, y, w, h) {
            var canvas = this.canvas.get(0);
            var ctx = canvas.getContext("2d");
            ctx.drawImage(image, x, y, w, h);
        }

The class supports drawing, setting the color and text. The rest wraps events and canvas helpers.

Let's look at the constructor:

JavaScript
function CanvasManager(canvasId) {
     this.canvas = $('#' + canvasId); 
     this.imgColor = [255, 255, 255];
     this.buttonDown = false;
     this.lastPos = [0, 0];
     this.mode = '';
     ...

The canvas member is actually the jQuery wrapper of the DOM object.

The imgColor member stores the color the user selected. The default color is white (#FFFFFF).

For drawing, I needed to remember the mouse button state (pressed or not pressed). The buttonDown boolean member handles that.

The lastPos member remembers the last mouse position. It is used for drawing text.

Finally but most importantly, the mode member represents what the canvas' mode is (paint, set color or text).

Next in the constructor we assign event handlers to the canvas. We'll use the mousemove event to since it is the most complex canvas action.

JavaScript
this.canvas.mousemove(function (e) {
    var arg = e;
    var ctx = canvasMgr.canvas.get(0).getContext("2d");
    if (canvasMgr.mode == 'paint') {
        if (canvasMgr.buttonDown) { //mousebutton down

            var offsets = getOffsets(e);
            var imgData = ctx.getImageData(offsets.offsetX - 5, offsets.offsetY - 5, 10, 10);

            for (i = 0; i < imgData.width * imgData.height * 4; i += 4) {
                imgData.data[i] = canvasMgr.imgColor[0];
                imgData.data[i + 1] = canvasMgr.imgColor[1];
                imgData.data[i + 2] = canvasMgr.imgColor[2];
                imgData.data[i + 3] = 255;
            }

            ctx.putImageData(imgData, offsets.offsetX - 5, offsets.offsetY - 5);
        }
    }
});

Here's what you should note about painting to the canvas:

We reference the canvasManager's canvas object and we use the jQuery get() method to get at the DOM canvas object (not the jQuery wrapper).  

We check the canvas' mode. If the mode is set to "paint" then we proceed.

Next we check to see if the mouse's button state is "down". This is set in the mousedown event handler. Next, we are ready to draw. I have a helper function getOffsets() to get the mouse location. We need a helper because IE uses OffsetX/OffsetY and Mozilla uses LayerX/LayerY. The method is shown at the bottom of this article.

To paint a section of the canvas, you need to get the chunk of the canvas you want to paint on, then change it. The Context.getImageData() method returns an array of pixels from the drawing area.

Each pixel takes up four elements of the array; red=0, green=1, blue=2 and alpha=3. So we loop through the array stepping by four elements each time. With each pixel, I set the color attributes from the CanvasManager's imgColor member. Finally, write your chunk of canvas back to the main canvas in the Context.putImageData() method.

Drawing text to the canvas  

JavaScript
$(window).keypress(function (e) {
          if (canvasMgr.mode == 'text') {
               var canvasDOM = canvasMgr.canvas.get(0);
               var ctx = canvasDOM.getContext("2d");
               ctx.font = "20pt Arial";
               ctx.textBaseline = "bottom";
               ctx.textAlign = "left";
               ctx.fillStyle = 'rgb(' + canvasMgr.imgColor[0] +
                            ',' + canvasMgr.imgColor[1] +
                            ',' + canvasMgr.imgColor[2] + ')';

               ctx.fillText(String.fromCharCode(e.which), canvasMgr.lastPos[0], canvasMgr.lastPos[1]);
               var textMTX = ctx.measureText(String.fromCharCode(e.which));
               canvasMgr.lastPos[0] += textMTX.width + 1;
           }
       });

Drawing text to the canvas is trickier because the canvas object doesn't seem to accept keypress or keydown events. We need to hook the DOM window object for that.

Once again, we get the canvas' Context object to perform any graphics actions on the canvas.

Note that I am using the CanvasManager's mode property to make sure we're in "text" mode.

The Context object supports these properties for rendering text; font, textBaseline, textAlign and fillStyle. Basic CSS syntax is used to set these properties. 

To draw text, simply call the Context.fillText() method. The fillText() call provides the ASCII code converted to a string (String.fromCharCode(e.which)) and the X & Y from the CanvasManager's lastPos value.

In order to set the position of the next character, use the Context.MeasureText() to get the length of the previous character and increase the lastPos value for the next character.

Because of cross-browser incompatibility, I had to write a utility function that returns the X and Y coordinates of the mouse location. Here it is. Note, I've gotten funny behavior from Firefox. IE and Chrome did just fine.  

JavaScript
function getOffsets(evt) {
    var offsetX, offsetY;
    if (typeof evt.offsetX != 'undefined') {
        offsetX = evt.offsetX;
        offsetY = evt.offsetY;
    }
    else if (typeof evt.clientX != 'undefined') {
        offsetX = evt.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
        offsetY = evt.clientY + document.body.scrollTop + document.documentElement.scrollTop;
        offsetX -= canvasMgr.canvas.get(0).offsetLeft;
        offsetY -= canvasMgr.canvas.get(0).offsetTop;
    }
    return { 'offsetX': offsetX, 'offsetY': offsetY };
}

Finally, the code that wires up your canvas to controls is simple buttons that set the CanvasManager's mode.

HTML
<div style="width: 600px; height: 25px; overflow: hidden;">
    <input type="button" id="btnText" value="Text" onclick="canvasMgr.setMode('text');" />            
    <input type="button" id="btnPaint" value="Paint" onclick="canvasMgr.setMode('paint');" />
    <input type="button" id="btnDropper" value="Set Color" onclick="canvasMgr.setMode('dropper');" />
    <div style="border: 1pt solid blue; height: 10px; display: inline;" id="dvSwatch">    </div>
</div>

Points of Interest 

  • The canvas object prefers the PNG image file format. I believe the thinking is that the PNG is the wave of the future. It supports JPEG, but only to humor you old-school developers.
  • For this sample, the canvas is 500X500px. Your image should have the same dimensions.
  • If you learned about drawing from the Windows world, you will find the HTML5 canvas very easy to pick up. You could probably use the Win32 drawing API reference to get the basics of the canvas down. The Context is a first cousin with the Graphics object.
  • As any web developer soon discovers, the differences in browsers can cause headaches, premature baldness, etc. Finding the mouse coordinates took a little digging. Just remember, the Internet already figured it out for you. Bing is your friend. 
  • jQuery is the best thing to happen to web development. One of the best, yet under-appreciated benefits of jQuery is that someone already sweated out the cross-browser issues and covered it under a friendly API.
  • The keypress event should include code that filters out non-printing characters. Enter and Backspace will not render text but let the browser do what it wants to.
  • If you found this article interesting, please post a comment. It would help my delicate ego.

History 

Initially published 3/20/2012  

Updated 4/1/2012 to correct finding coordinates for FireFox.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)