36

Question

How do I draw free (using my mouse / fingers) on a canvas element like you can do it in paint with a pencil?

About this question

There are a lot of questions that want to achieve free hand drawing on canvas:

So I thought it would be a good idea to make a reference question, where every answer is community wiki and contains a explanation for exactly one JavaScript library / pure JavaScript how to do paint on canvas.

Structure of answers

The answers should be community wiki and use the following template:

## [Name of library](Link to project page)
### Simple example
    A basic, complete example. That means it has to contain HTML 
    and JavaScript. You can start with this:

    <!DOCTYPE html>
    <html>
      <head>
        <title>Simple example</title>
        <script type='text/javascript' src='http://cdnjs.com/[your library]'></script>
        <style type='text/css'>
            #sheet {
                border:1px solid black;
            }
        </style>
        <script type='text/javascript'>
            window.onload=function(){
                // TODO: Adjust
            }
        </script>
      </head>
      <body>
        <canvas id="sheet" width="400" height="400"></canvas>
      </body>
    </html>

    If possible, this example should work with both, mouse and touch events.

[JSFiddle](Link to code on jsfiddle.net)

This solution works with:

<!-- Please test it the following way: Write "Hello World"
  Problems that you test this way are:
   * Does it work at all?
   * Are lines separated?
   * Does it get slow when you write too much?
-->

* Desktop computers:
  * [Browser + Version list]
* Touch devices:
  * [Browser + Version list] on [Device name]

### Import / Export
Some explanations how to import / export user drawn images.

### Line smoothing
Explanations about how to manipulate the line the user draws. 
This can include:
  * Bézier curves
  * Controlling thickness of lines
Community
  • 1
  • 1
Martin Thoma
  • 124,992
  • 159
  • 614
  • 958

7 Answers7

19

Fabric.js

<!DOCTYPE html>
<html>
  <head>
    <title>Simple example</title>
    <script type='text/javascript' src='http://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.4.0/fabric.min.js'></script>
    <style type='text/css'>
        #sheet {
            border:1px solid black;
        }
    </style>
    <script type='text/javascript'>
        window.onload=function(){
            var canvas = new fabric.Canvas('sheet');
            canvas.isDrawingMode = true;
            canvas.freeDrawingBrush.width = 5;
            canvas.freeDrawingBrush.color = "#ff0000";
        }
    </script>
  </head>
  <body>
    <canvas id="sheet" width="400" height="400"></canvas>
  </body>
</html>

JSFiddle - Demo

  • The width of the lines can be controlled with canvas.freeDrawingBrush.width.
  • The color of the lines can be controlled with canvas.freeDrawingBrush.color.

This solution works with:

  • Desktop computers:
    • Chrome 33
    • Firefox 28
  • Touch devices:
    • Chrome 34 on Nexus 4
    • Opera 20 on Nexus 4
    • Firefox 28 on Nexus 4

Import / Export

Is only possible by serializing the complete canvas, see Tutorial

Line smoothing

Is done automatically and it seems not to be possible to deactivate it.

seokgyu
  • 147
  • 1
  • 11
Martin Thoma
  • 124,992
  • 159
  • 614
  • 958
  • In JSFiddle the shape i drew kind of "settled" on mouseup. The whole shape of one stroke moved few pixels to left and/or down. Win7 Firefox 28 – timbernasley Apr 23 '14 at 12:44
  • In fact you can, each time you draw a path a 'path:created' event occurs, the following: canvas.on('path:created', function(path) { JSON_path = JSON.stringify(path.path.toJSON()) }); will retrieve the JSON string of the last drawn path, when this string arrives to another canvas, which already contains other paths, all you have to do is to retrieve the JSON string of the entire canvas, to add the last path, then to reload that JSON to the canvas: canvas_JSON = canvas.toJSON(); canvas_JSON.objects.push(JSON.parse(JSON_path)); canvas.loadFromJSON(canvas_JSON); – ubugnu Jul 02 '16 at 13:21
15

Plain JavaScript

Simple example

<!DOCTYPE html>
<html>
  <head>
    <title>Simple example</title>
    <style type='text/css'>
        #sheet {
            border:1px solid black;
        }
    </style>
  </head>
  <body>
    <canvas id="sheet" width="400" height="400"></canvas>
    <script type='text/javascript'>
/*jslint browser:true */
"use strict";
var context = document.getElementById('sheet').getContext("2d");
var canvas = document.getElementById('sheet');
context = canvas.getContext("2d");
context.strokeStyle = "#ff0000";
context.lineJoin = "round";
context.lineWidth = 5;

var clickX = [];
var clickY = [];
var clickDrag = [];
var paint;

/**
 * Add information where the user clicked at.
 * @param {number} x
 * @param {number} y
 * @return {boolean} dragging
 */
function addClick(x, y, dragging) {
    clickX.push(x);
    clickY.push(y);
    clickDrag.push(dragging);
}

/**
 * Redraw the complete canvas.
 */
function redraw() {
    // Clears the canvas
    context.clearRect(0, 0, context.canvas.width, context.canvas.height);

    for (var i = 0; i < clickX.length; i += 1) {
        if (!clickDrag[i] && i == 0) {
            context.beginPath();
            context.moveTo(clickX[i], clickY[i]);
            context.stroke();
        } else if (!clickDrag[i] && i > 0) {
            context.closePath();

            context.beginPath();
            context.moveTo(clickX[i], clickY[i]);
            context.stroke();
        } else {
            context.lineTo(clickX[i], clickY[i]);
            context.stroke();
        }
    }
}

/**
 * Draw the newly added point.
 * @return {void}
 */
function drawNew() {
    var i = clickX.length - 1
    if (!clickDrag[i]) {
        if (clickX.length == 0) {
            context.beginPath();
            context.moveTo(clickX[i], clickY[i]);
            context.stroke();
        } else {
            context.closePath();

            context.beginPath();
            context.moveTo(clickX[i], clickY[i]);
            context.stroke();
        }
    } else {
        context.lineTo(clickX[i], clickY[i]);
        context.stroke();
    }
}

function mouseDownEventHandler(e) {
    paint = true;
    var x = e.pageX - canvas.offsetLeft;
    var y = e.pageY - canvas.offsetTop;
    if (paint) {
        addClick(x, y, false);
        drawNew();
    }
}

function touchstartEventHandler(e) {
    paint = true;
    if (paint) {
        addClick(e.touches[0].pageX - canvas.offsetLeft, e.touches[0].pageY - canvas.offsetTop, false);
        drawNew();
    }
}

function mouseUpEventHandler(e) {
    context.closePath();
    paint = false;
}

function mouseMoveEventHandler(e) {
    var x = e.pageX - canvas.offsetLeft;
    var y = e.pageY - canvas.offsetTop;
    if (paint) {
        addClick(x, y, true);
        drawNew();
    }
}

function touchMoveEventHandler(e) {
    if (paint) {
        addClick(e.touches[0].pageX - canvas.offsetLeft, e.touches[0].pageY - canvas.offsetTop, true);
        drawNew();
    }
}

function setUpHandler(isMouseandNotTouch, detectEvent) {
    removeRaceHandlers();
    if (isMouseandNotTouch) {
        canvas.addEventListener('mouseup', mouseUpEventHandler);
        canvas.addEventListener('mousemove', mouseMoveEventHandler);
        canvas.addEventListener('mousedown', mouseDownEventHandler);
        mouseDownEventHandler(detectEvent);
    } else {
        canvas.addEventListener('touchstart', touchstartEventHandler);
        canvas.addEventListener('touchmove', touchMoveEventHandler);
        canvas.addEventListener('touchend', mouseUpEventHandler);
        touchstartEventHandler(detectEvent);
    }
}

function mouseWins(e) {
    setUpHandler(true, e);
}

function touchWins(e) {
    setUpHandler(false, e);
}

function removeRaceHandlers() {
    canvas.removeEventListener('mousedown', mouseWins);
    canvas.removeEventListener('touchstart', touchWins);
}

canvas.addEventListener('mousedown', mouseWins);
canvas.addEventListener('touchstart', touchWins);
    </script>
  </body>
</html>

JSFiddle

  • The width of the lines can be controlled with context.lineWidth.
  • The color of the lines can be controlled with strokeStyle.

This solution works with:

  • Desktop computers:
    • Chrome 33
    • Firefox 28
  • Touch devices:
    • Firefox 28 on Nexus 4

It does not work with

  • Touch devices:
    • Chrome 34 / Opera 20 on Nexus 4 (see issue)

Import / Export

Importing and exporting the image can be done by importing / exporting clickX, clickY and clickDrag.

Line smoothing

Can eventually be done by replacing lineTo() with bezierCurveTo()

Martin Thoma
  • 124,992
  • 159
  • 614
  • 958
  • 1
    Don't use two arrays for x and y, just one or browser will have to do two array lookups. Store either as pairs [x1, y1, x2, y2, ...] or object literals [{x:x1, y:y1}, {x:x2, y:y2}...]. Don't use Bezier for line-smoothing but a cardinal spline. The latter will actually go through the points and allows tension value, while Bezier won't. My 2 cents.. :) –  Apr 22 '14 at 23:20
  • why don't we add event listeners to mouse/touch events right away? why does the `setUpHandler` function exist? – Adham Zahran Mar 27 '20 at 13:50
4

Plain JS - ES6

Simple example

Plain Javascript example above has some serious issues: it does not reflect the comments objections, the paint state is redundant, events are not unhooked properly, the redraw() function is not used, it can be simplified a lot and it doesn't work with modern syntax. The fix is here:

var canvas = document.getElementById('sheet'), g = canvas.getContext("2d");
g.strokeStyle = "hsl(208, 100%, 43%)";
g.lineJoin = "round";
g.lineWidth = 1;
g.filter = "blur(1px)";

const
relPos = pt => [pt.pageX - canvas.offsetLeft, pt.pageY - canvas.offsetTop],
drawStart = pt => { with(g) { beginPath(); moveTo.apply(g, pt); stroke(); }},
drawMove = pt => { with(g) { lineTo.apply(g, pt); stroke(); }},

pointerDown = e => drawStart(relPos(e.touches ? e.touches[0] : e)),
pointerMove = e => drawMove(relPos(e.touches ? e.touches[0] : e)),

draw = (method, move, stop) => e => {
    if(method=="add") pointerDown(e);
    canvas[method+"EventListener"](move, pointerMove);
    canvas[method+"EventListener"](stop, g.closePath);
};

canvas.addEventListener("mousedown", draw("add","mousemove","mouseup"));
canvas.addEventListener("touchstart", draw("add","touchmove","touchend"));
canvas.addEventListener("mouseup", draw("remove","mousemove","mouseup"));
canvas.addEventListener("touchend", draw("remove","touchmove","touchend"));
<canvas id="sheet" width="400" height="400" style="border: 1px solid black"></canvas>
  • Support It should work everywhere today. I could be further simplified by pointer events, but Safari lacks support for it as of 2021.

Import / Export

For import, use g.drawImage()

g.drawImage(img, 0, 0);

For export, see canvas.toBlob()

function save(blob) {
  var fd = new FormData();
  fd.append("myFile", blob);
  // handle formData to your desire here
}
canvas.toBlob(save,'image/jpeg');

Line smoothing

For antialiasing, See blur() from SVG filters; if you import, don't forget to apply it AFTER the image is imported

context.filter = "blur(1px)";
Jan Turoň
  • 31,451
  • 23
  • 125
  • 169
  • What is the cause of the "blurring" effect that starts at the tip and travels along the last ~3 seconds of line drawn? Commenting out the g.filter line reduces it to ~1 second but does not remove it. – sgfit Apr 23 '22 at 15:29
  • @sgfit it sounds like OpenGL issues in the (browser?) engine; I'd try some OpenGL tests/benchmarks – Jan Turoň May 09 '22 at 07:28
3

Paper.js

Simple example

<!DOCTYPE html>
<html>
<head>
    <title>Paper.js example</title>
    <script type='text/javascript' src='http://paperjs.org/assets/js/paper.js'></script>
    <style type='text/css'>
        #sheet {
            border:1px solid black;
        }
    </style>
</head>
<body>
    <script type="text/paperscript" canvas="sheet">
        var path;

        function onMouseDown(event) {
            // If we produced a path before, deselect it:
            if (path) {
                path.selected = false;
            }

            // Create a new path and set its stroke color to black:
            path = new Path({
                segments: [event.point],
                strokeColor: 'black',
                strokeWidth: 3
            });
        }

        // While the user drags the mouse, points are added to the path
        // at the position of the mouse:
        function onMouseDrag(event) {
            path.add(event.point);
        }

        // When the mouse is released, we simplify the path:
        function onMouseUp(event) {
            path.simplify();
        }
    </script>

    <canvas id="sheet" width="400" height="400"></canvas>
</body>
</html>

JSFiddle

  • The width of the lines can be controlled with strokeWidth.
  • The color of the lines can be controlled with strokeColor.

This solution works with:

  • Desktop computers:
    • Chrome 33

Import / Export

?

Line smoothing

Line smoothing can be done by adjusting path.simplify();.

Martin Thoma
  • 124,992
  • 159
  • 614
  • 958
  • cool smoothing, there is a bug with mouseup event outside a canvas, should be mouseleave listened also? – vp_arth Apr 06 '14 at 16:37
  • @vp_arth Why do you think there is a bug with `onMouseUp`? It works fine for me on Chrome (desktop) even if I start drawing inside and stop outside of the canvas. – Martin Thoma Apr 06 '14 at 16:40
  • after mouseup outside and returning to canvas, it continues drawing – vp_arth Apr 06 '14 at 16:50
  • @vp_arth Not for me. Could you please tell me which browser you use? – Martin Thoma Apr 06 '14 at 16:55
  • @vp_arth Ok ... strange. Does for you it work when you add mouseleave? – Martin Thoma Apr 06 '14 at 17:00
  • `var c = document.getElementById('sheet'); c.addEventListener('mouseleave', function(e){ var evt = document.createEvent("MouseEvents"); evt.initEvent("mouseup", true, true); c.dispatchEvent(evt); })` This code fixes issue for me... but I sure, it can be fixed easier inside paperjs code :) – vp_arth Apr 06 '14 at 17:23
3

EaselJs

Simple example

A basic, complete example. That means it has to contain HTML 
and JavaScript. You can start with this:

<!DOCTYPE html>
<html>
<head>
    <title>EaselJS example</title>

    <script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/EaselJS/0.7.1/easeljs.min.js"></script>

    <script>
        var canvas, stage;
        var drawingCanvas;
        var oldPt;
        var oldMidPt;
        var color;
        var stroke;
        var index;

        function init() {
            if (window.top != window) {
                document.getElementById("header").style.display = "none";
            }
            canvas = document.getElementById("sheet");
            index = 0;

            //check to see if we are running in a browser with touch support
            stage = new createjs.Stage(canvas);
            stage.autoClear = false;
            stage.enableDOMEvents(true);

            createjs.Touch.enable(stage);
            createjs.Ticker.setFPS(24);

            drawingCanvas = new createjs.Shape();

            stage.addEventListener("stagemousedown", handleMouseDown);
            stage.addEventListener("stagemouseup", handleMouseUp);

            stage.addChild(drawingCanvas);
            stage.update();
        }

        function stop() {}

        function handleMouseDown(event) {
            color = "#ff0000";
            stroke = 5;
            oldPt = new createjs.Point(stage.mouseX, stage.mouseY);
            oldMidPt = oldPt;
            stage.addEventListener("stagemousemove" , handleMouseMove);
        }

        function handleMouseMove(event) {
            var midPt = new createjs.Point(oldPt.x + stage.mouseX>>1, oldPt.y+stage.mouseY>>1);

            drawingCanvas.graphics.clear().setStrokeStyle(stroke, 'round', 'round').beginStroke(color).moveTo(midPt.x, midPt.y).curveTo(oldPt.x, oldPt.y, oldMidPt.x, oldMidPt.y);

            oldPt.x = stage.mouseX;
            oldPt.y = stage.mouseY;

            oldMidPt.x = midPt.x;
            oldMidPt.y = midPt.y;

            stage.update();
        }

        function handleMouseUp(event) {
            stage.removeEventListener("stagemousemove" , handleMouseMove);
        }

    </script>
</head>
<body onload="init();">
    <canvas id="sheet" width="400" height="400"></canvas>
</body>
</html>

Demo

The interesting parts in the documentation are:

This solution works with:

  • Desktop computers:
    • Chrome 33
    • Firefox 28
  • Touch devices:
    • Chrome 34 / Firefox 28 / Opera 20 on Nexus 4

Import / Export

?

Line smoothing

?

Avatar
  • 14,622
  • 9
  • 119
  • 198
Martin Thoma
  • 124,992
  • 159
  • 614
  • 958
2

Here, try my canvas free drawing and erase.

https://jsfiddle.net/richardcwc/d2gxjdva/

//Canvas
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
//Variables
var canvasx = $(canvas).offset().left;
var canvasy = $(canvas).offset().top;
var last_mousex = last_mousey = 0;
var mousex = mousey = 0;
var mousedown = false;
var tooltype = 'draw';

//Mousedown
$(canvas).on('mousedown', function(e) {
    last_mousex = mousex = parseInt(e.clientX-canvasx);
 last_mousey = mousey = parseInt(e.clientY-canvasy);
    mousedown = true;
});

//Mouseup
$(canvas).on('mouseup', function(e) {
    mousedown = false;
});

//Mousemove
$(canvas).on('mousemove', function(e) {
    mousex = parseInt(e.clientX-canvasx);
    mousey = parseInt(e.clientY-canvasy);
    if(mousedown) {
        ctx.beginPath();
        if(tooltype=='draw') {
            ctx.globalCompositeOperation = 'source-over';
            ctx.strokeStyle = 'black';
            ctx.lineWidth = 3;
        } else {
            ctx.globalCompositeOperation = 'destination-out';
            ctx.lineWidth = 10;
        }
        ctx.moveTo(last_mousex,last_mousey);
        ctx.lineTo(mousex,mousey);
        ctx.lineJoin = ctx.lineCap = 'round';
        ctx.stroke();
    }
    last_mousex = mousex;
    last_mousey = mousey;
    //Output
    $('#output').html('current: '+mousex+', '+mousey+'<br/>last: '+last_mousex+', '+last_mousey+'<br/>mousedown: '+mousedown);
});

//Use draw|erase
use_tool = function(tool) {
    tooltype = tool; //update
}
canvas {
    cursor: crosshair;
    border: 1px solid #000000;
}
<canvas id="canvas" width="800" height="500"></canvas>
<input type="button" value="draw" onclick="use_tool('draw');" />
<input type="button" value="erase" onclick="use_tool('erase');" />
<div id="output"></div>
Richard
  • 922
  • 11
  • 11
  • It seems not to work for dots. Like writing an "i" or "j". – Martin Thoma Nov 13 '15 at 12:00
  • Hi moose, the drawing works on mousemove, so the mouse has to move for the drawing to show. But you can also add the drawing onto the mousedown handler, so when you mousedown, a dot will be draw, do you know what I mean? Some modification will be needed. – Richard Nov 15 '15 at 22:51
  • Touch events are not supported. – jor Dec 02 '19 at 10:38
0

(Disclaimer: I wrote this library)

Scrawl.js

Simple example

<!DOCTYPE html>
<html>
    <head>
        <title>Simple example</title>
        <style type='text/css'>
            #sheet {border:1px solid black;}
        </style>
    </head>
    <body>
        <canvas id="sheet" width="400" height="400"></canvas>
        <script src="http://scrawl.rikweb.org.uk/js/scrawlCore-min.js"></script>
        <script>
            var mycode = function(){
                //define variables
                var myPad = scrawl.pad.sheet, 
                    myCanvas = scrawl.canvas.sheet,
                    sX, sY, here,
                    drawing = false, 
                    currentSprite = false,
                    startDrawing,
                    endDrawing;

                //event listeners
                startDrawing = function(e){
                    drawing = true;
                    currentSprite = scrawl.newShape({
                        start:          here,
                        lineCap:        'round',
                        lineJoin:       'round',
                        method:         'draw',
                        lineWidth:      4,
                        strokeStyle:    'red',
                        data:           'l0,0 ',
                    });
                    sX = here.x;
                    sY = here.y;
                    if(e){
                        e.stopPropagation();
                        e.preventDefault();
                    }
                };
                myCanvas.addEventListener('mousedown', startDrawing, false);

                endDrawing = function(e){
                    if(currentSprite){
                        currentSprite = false;
                    }
                    drawing = false;
                    if(e){
                        e.stopPropagation();
                        e.preventDefault();
                    }
                };
                myCanvas.addEventListener('mouseup', endDrawing, false);

                //animation object
                scrawl.newAnimation({
                    fn: function(){
                        //get current mouse position
                        here = myPad.getMouse();
                        if(here.active){
                            if(drawing){
                                if(here.x !== sX || here.y !== sY){
                                    //extend the line
                                    currentSprite.set({
                                        data: currentSprite.data+' '+(here.x - sX)+','+(here.y - sY),
                                        });
                                    sX = here.x;
                                    sY = here.y;
                                }
                            }
                        }
                        else{
                            //stop drawing if mouse leaves canvas area
                            if(currentSprite){
                                endDrawing();
                            }
                        }
                        //update display
                        scrawl.render();
                    },
                });
            };

            //Scrawl is modular - load additional modules
            scrawl.loadModules({
                path: 'js/',
                modules: ['animation', 'shape'],            
                callback: function(){
                    window.addEventListener('load', function(){
                        scrawl.init();      //start Scrawl
                        mycode();           //run code
                    }, false);
                },
            });
        </script>
    </body>
</html>

JSFiddle

This solution works with:

  • recent versions of IE, Chrome, Firefox, Opera (desktop)
  • (not tested on mobile/touch devices)

Adding touch support

  • (try adding a dedicated touch library like Hammer.js?)

Import / Export

Line smoothing and other sprite manipulations

  • line data is saved internally as an SVGTiny Path.d value - any algorithm that can take line data in that format and smooth it should work
  • line attributes - thickness, color, positioning, rotation, etc - can be set, and animated.
KaliedaRik
  • 358
  • 3
  • 6