如何使用JavaScript在画布上手绘?

37

问题

如何在画布元素上自由地(使用鼠标/手指)绘制,就像在画图中用铅笔一样?

关于此问题

有很多问题想在画布上实现自由绘制:

因此我认为制作一个参考问题是个好主意,每个答案都是社区 wiki,并包含一个 JavaScript 库或纯 JavaScript 如何在画布上绘制的解释。

答案结构

答案应该是社区 wiki,并使用以下模板:

## [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
7个回答

18

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 - 演示

  • 通过canvas.freeDrawingBrush.width可以控制线条的宽度。
  • 通过canvas.freeDrawingBrush.color可以控制线条的颜色。

此解决方案适用于:

  • 台式电脑:
    • Chrome 33
    • Firefox 28
  • 触摸设备:
    • Nexus 4 上的 Chrome 34
    • Nexus 4 上的 Opera 20
    • Nexus 4 上的 Firefox 28

导入/导出

只能通过序列化完整的画布来实现,请参见教程

线条平滑处理

自动完成,似乎无法停用。


在JSFiddle中,我绘制的形状在鼠标松开后有点“定格”了。整个一笔的形状向左和/或向下移动了几个像素。Win7 Firefox 28。 - timbernasley
实际上你可以,在每次绘制路径时,都会出现一个“path:created”事件,如下所示:canvas.on('path:created', function(path) { JSON_path = JSON.stringify(path.path.toJSON()) });将检索到最后绘制的路径的JSON字符串,当这个字符串到达另一个画布时,该画布已经包含了其他路径,你所要做的就是检索整个画布的JSON字符串,添加最后的路径,然后重新加载JSON到画布中:canvas_JSON = canvas.toJSON(); canvas_JSON.objects.push(JSON.parse(JSON_path)); canvas.loadFromJSON(canvas_JSON); - ubugnu

15

纯 JavaScript

简单示例

<!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

  • 使用 context.lineWidth 控制线条宽度。
  • 使用 strokeStyle 控制线条颜色。

本解决方案适用于:

  • 桌面电脑:
    • Chrome 33
    • Firefox 28
  • 触摸设备:
    • Nexus 4 上的 Firefox 28

本解决方案不适用于:

  • 触摸设备:
    • Nexus 4 上的 Chrome 34 / Opera 20(请参见问题

导入/导出

通过导入/导出 clickXclickYclickDrag 来实现图像的导入和导出。

平滑线条

可以通过将 lineTo() 替换为bezierCurveTo() 来实现线条平滑。


1
不要使用x和y的两个数组,只需一个或浏览器将不得不进行两个数组查找。将其存储为对[x1,y1,x2,y2,...]或对象文字[{x:x1,y:y1},{x:x2,y:y2} ...]。不要使用Bezier进行线平滑处理,而是使用基数样条。后者实际上会通过点并允许张力值,而Bezier则不会。我的意见.. :) - user1693593
为什么我们不直接将事件监听器添加到鼠标/触摸事件中?为什么需要存在 setUpHandler 函数? - Adham Zahran

4

纯JS - ES6

简单示例

上面的纯Javascript示例存在一些严重问题:它没有反映出评论中的异议,paint状态是冗余的,事件没有正确地解除绑定,redraw()函数没有被使用,它可以被大大简化,并且它不能与现代语法一起使用。修复方案如下:

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>

  • 支持 目前应该在任何地方都可以工作。可以通过指针事件进一步简化,但截至2021年,Safari尚不支持。

导入/导出

对于导入,请使用g.drawImage()

g.drawImage(img, 0, 0);

如需导出,请参见 canvas.toBlob()

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

线条平滑处理

对于反锯齿,可使用SVG滤镜中的blur()方法;如果导入,请务必在图片导入后应用它。

context.filter = "blur(1px)";

“模糊”效果从线条的末端开始并沿着最后约3秒钟的路径移动,这是什么原因?注释掉g.filter行可以将其减少到约1秒钟,但无法消除它。 - sgfit
@sgfit 看起来是浏览器引擎中的OpenGL问题。我建议尝试一些OpenGL测试/基准测试。 - Jan Turoň

3

Paper.js

简单示例

<!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

  • 使用strokeWidth可以控制线的宽度。
  • 使用strokeColor可以控制线的颜色。

此解决方案适用于:

  • 桌面电脑:
    • Chrome 33

导入/导出

线条平滑处理

通过调整path.simplify();可以实现线条平滑处理。


平滑处理很酷,但是在画布外发生了一个鼠标抬起事件的错误,是否也应该监听mouseleave事件? - vp_arth
@vp_arth 你为什么认为 onMouseUp 存在 Bug?在 Chrome 上(桌面版),即使我在画布内开始绘制并在外部停止绘制,它也能正常工作。 - Martin Thoma
在鼠标松开并离开画布后,它会继续绘制。 - vp_arth
@vp_arth 不是针对我。你能告诉我你使用的是哪个浏览器吗? - Martin Thoma
@vp_arth 好的...奇怪。当你添加mouseleave时,它对你有用吗? - Martin Thoma
变量c = document.getElementById('sheet'); c.addEventListener('mouseleave',function(e){ var evt = document.createEvent("MouseEvents"); evt.initEvent("mouseup", true, true); c.dispatchEvent(evt); }); 这段代码为我解决了问题...但我相信,在paperjs的代码中可以更容易的解决它 :) - vp_arth

3

EaselJS

简单示例

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>

演示

文档中有趣的部分包括:

该解决方案适用于:

  • 台式电脑:
    • Chrome 33
    • Firefox 28
  • 触摸设备:
    • Nexus 4 上的 Chrome 34 / Firefox 28 / Opera 20

导入/导出

线条平滑


2

在这里,试试我的自由画布和橡皮。

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>


似乎对于点不起作用。比如写“i”或“j”。 - Martin Thoma
嗨,moose,绘图功能在鼠标移动时才能生效,因此必须移动鼠标才能显示绘图。但是你也可以将绘图添加到mousedown处理程序中,这样当你按下鼠标时,会绘制一个点,你知道我的意思吗?需要进行一些修改。 - Richard
不支持触摸事件。 - jor

0

(免责声明:本人编写了这个库)

Scrawl.js

简单示例

<!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

此解决方案适用于:

  • IE、Chrome、Firefox、Opera(桌面版)的最新版本
  • (未在移动/触摸设备上进行测试)

添加触摸支持

  • (尝试添加专用的触摸库,如Hammer.js?)

导入/导出

线平滑和其他精灵操作

  • 线数据在内部保存为SVGTiny Path.d值 - 任何可以接受该格式的线数据并对其进行平滑处理的算法都可以使用
  • 线属性 - 厚度、颜色、定位、旋转等 - 可以设置和动画化。

在 js fiddle 上,当你绘制时,画布会跟随鼠标移动,在使用 Firefox v.28.0 的 OsX 上也是如此。 - clami219
@clami219:Win7和Firefox v. 28.0也是一样的。 - timbernasley
糟糕!我昨晚发布了Scrawl的3.1版本 - 需要更改弹跳中的一行代码来修复它。现在应该一切正常了。 - KaliedaRik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接