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

时间:2014-04-06 08:47:49

标签: javascript canvas

问题

如何在画布元素上自由绘制(使用我的鼠标/手指),就像你可以用铅笔画画一样?

关于这个问题

有很多问题需要在画布上实现自由手绘:

所以我认为提出一个参考问题是一个好主意,其中每个答案都是社区维基,并且包含对一个JavaScript库/纯JavaScript如何在画布上绘画的解释。

答案结构

答案应该是社区维基并使用以下模板:

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

答案 0 :(得分:8)

普通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(请参阅issue

导入/导出

导入和导出图像可以通过导入/导出clickXclickYclickDrag来完成。

线条平滑

最终可以将lineTo()替换为bezierCurveTo()

来完成

答案 1 :(得分:8)

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

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

此解决方案适用于:

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

导入/导出

只有通过序列化完整画布才能实现,请参阅Tutorial

线条平滑

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

答案 2 :(得分:2)

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>

Demo

文档中有趣的部分是:

此解决方案适用于:

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

导入/导出

线条平滑

答案 3 :(得分:1)

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();来完成线条平滑。

答案 4 :(得分:1)

在这里,尝试我的画布免费绘图和擦除。

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

&#13;
&#13;
//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
}
&#13;
canvas {
    cursor: crosshair;
    border: 1px solid #000000;
}
&#13;
<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>
&#13;
&#13;
&#13;

答案 5 :(得分:1)

普通 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>

  • 支持 今天它应该可以在任何地方工作。我可以通过 pointer events 进一步简化,但 Safari 到 2021 年不再支持它。

导入/导出

对于导入,使用 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 filters 中的 blur();如果您导入,请不要忘记在导入图像后应用它

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

答案 6 :(得分: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值 - 任何可以采用该格式的行数据并使其平滑的算法都可以正常工作
  • 线条属性 - 厚度,颜色,定位,旋转等 - 可以设置和设置动画。