在<canvas>元素上实现流畅的草图和绘画

36

我尝试使用canvas创建一个绘图区域。在绘制曲线时,我发现线条看起来不太平滑,并且我的算法中线条的粗细也会变化,这样看起来很糟糕,因为大小跳跃太大,还可以看到大小的变化。我在stackoverflow上找到了这个链接:https://dev59.com/g2445IYBdhLWcg3wBlyJ,但这是针对本地iPhone应用程序的,我无法理解。

这是我的当前JS代码。 这是它在jsFiddle上运行

var xStart,
xEnd,
yStart,
yEnd,
paint,
ctx;
$(document).ready(function (){

   ctx = $('canvas')[0].getContext("2d");
   ctx.strokeStyle = '#000';
   ctx.lineJoin="round";
   ctx.lineCap="round";
   ctx.lineWidth = 1;


   $('canvas').bind('mousedown mousemove mouseup mouseleave touchstart touchmove touchend', function(e){
        var orig = e.originalEvent;

        if(e.type == 'mousedown'){
            e.preventDefault(); e.stopPropagation();

            xStart = e.clientX - $(this).offset().left;
            yStart = e.clientY - $(this).offset().top;
            xEnd = xStart;
            yEnd = yStart;

            paint = true;
            draw(e.type);

        }else if(e.type == 'mousemove'){
            if(paint==true){
                xEnd = e.clientX - $(this).offset().left;
                yEnd = e.clientY - $(this).offset().top;


               lineThickness = 1 + Math.sqrt((xStart - xEnd) *(xStart-xEnd) + (yStart - yEnd) * (yStart-yEnd))/5;

               if(lineThickness > 10){
                    lineThickness = 10;   
               }

                ctx.lineWidth = lineThickness;
                draw(e.type);
            }
        }else if(e.type == 'mouseup'){
            paint = false;
        }else if(e.type == 'mouseleave'){
            paint = false;
        }else if(e.type == 'touchstart'){
            if(orig.touches.length == 1){
                e.preventDefault(); e.stopPropagation();

                xStart = orig.changedTouches[0].pageX - $(this).offset().left;
                yStart = orig.changedTouches[0].pageY - $(this).offset().top;
                xEnd = xStart;
                yEnd = yStart; 

                paint = true;
                draw(e.type);
            }
        }else if(e.type == 'touchmove'){
            if(orig.touches.length == 1){
                if(paint==true){
                    xEnd = orig.changedTouches[0].pageX - $(this).offset().left;
                    yEnd = orig.changedTouches[0].pageY - $(this).offset().top;


                            lineThickness = 1 + Math.sqrt((xStart - xEnd) *(xStart-xEnd) + (yStart - yEnd) * (yStart-yEnd))/6;
                       if(lineThickness > 10){
                          lineThickness = 10;   
                       }


                      ctx.lineWidth = lineThickness;


                    draw(e.type);
                }
            }
        }else if(e.type == 'touchend'){
            paint = false;
        }

      });
    });


    function draw(event){

    if(event == 'mousedown'){
        ctx.beginPath();
        ctx.moveTo(xStart, yStart);
        ctx.lineTo(xEnd, yEnd);
        ctx.stroke();
    }else if(event == 'mousemove'){
        ctx.beginPath();
        ctx.moveTo(xStart, yStart);
        ctx.lineTo(xEnd, yEnd);
        ctx.stroke();
    }else if(event == 'touchstart'){
        ctx.beginPath();
        ctx.moveTo(xStart, yStart);
        ctx.lineTo(xEnd, yEnd);
        ctx.stroke();
    }else if(event == 'touchmove'){
        ctx.beginPath();
        ctx.moveTo(xStart, yStart);
        ctx.lineTo(xEnd, yEnd);
        ctx.stroke();
    }
    xStart = xEnd;
    yStart = yEnd;                  
}

先预先感谢大家。

这是您绘制时当前状态的外观。 current (jagged) implementation

......而这就是我想要实现的效果:

smooth brushstrokes

7个回答

25

我之前做过类似的东西,并将其转化为jQuery插件。

如果这正是你需要的,可以看看这里,我会发一篇更详细的答案并从我的档案中挖出简化的jQuery版本:

http://jsfiddle.net/95tft/

编辑

好的,抱歉昨天没能回复:

最初上述代码是从Mr Doob的“harmony”sketcher fork而来的,链接如下: http://mrdoob.com/projects/harmony/#ribbon

(我认为这是最好的解决方案)。 但是我把它拆开重建了另一个项目。我对自己的插件进行了一些修改,以使其更加容易使用,链接在这里:

http://jsfiddle.net/dh3bj/

你唯一可能想要更改的是将其更改为mousedown/mouseup工作,这也应该很容易。还要查看插件底部的设置,可以通过调整刷子大小、颜色、alpha (rgba)等来获得所需的效果。

希望这有所帮助。


是的,这绝对是正确的方向。感谢您挖掘出您的代码,我将等待您详细的答案。 - ryuutatsuo
@ryuutatsuo 上面添加了。如果需要更多信息,请告诉我。 - Alex
如果您在mrdoob.com上点击“关于”,其中一个链接是“源代码”,它会带您到https://github.com/mrdoob/harmony。 - Dale

12

看看这段代码:

http://jsfiddle.net/aMmVQ/

我的做法是,在鼠标按下时开始一个新的点列表,然后对于每个mousemove事件,我都会向列表中添加一个点。一旦我收集到足够的点(大约6个),我就开始绘制二次曲线,其中控制点是当前点和下一个点的平均值。

drawPoints 是实现这个魔法的关键部分:

function drawPoints(ctx, points) {
    // draw a basic circle instead
    if (points.length < 6) {
        var b = points[0];
        ctx.beginPath(), ctx.arc(b.x, b.y, ctx.lineWidth / 2, 0, Math.PI * 2, !0), ctx.closePath(), ctx.fill();
        return
    }
    ctx.beginPath(), ctx.moveTo(points[0].x, points[0].y);
    // draw a bunch of quadratics, using the average of two points as the control point
    for (i = 1; i < points.length - 2; i++) {
        var c = (points[i].x + points[i + 1].x) / 2,
            d = (points[i].y + points[i + 1].y) / 2;
        ctx.quadraticCurveTo(points[i].x, points[i].y, c, d)
    }
    ctx.quadraticCurveTo(points[i].x, points[i].y, points[i + 1].x, points[i + 1].y), ctx.stroke()
}

你的代码在创建平滑线条方面表现得很出色,但是如何改变线条粗细(即ctx.lineWidth)呢?我对你的方法有一个问题,那就是你先绘制所有的点,然后再结束stroke()。要改变线条粗细,似乎需要将beginPath()、moveTo()、quadraticCurveTo()和stroke()都放在循环内部,但是当我这样做时,它就变得有些奇怪了。 - ryuutatsuo
记录一下,您发布的图像并没有改变厚度,只是在两端改变了不透明度。 - Simon Sarris
“内存画布”是用来做什么的? - Dois

6

我能获取这个的源代码吗?Croquis没有文档,我需要找到如何在每次鼠标移动时绘制一个圆形。 - Arslan Ali

2

2
看起来你需要在画布上使用一些画笔。很难说你需要什么样的画笔,但是已经有许多JS库实现了画笔技术。
例如,你看过这些库吗? 在网络上,您可以找到许多刷子实现在Mr. Doob的Harmony项目中。例如,在github上有stringyHarmony-Brushes项目。

这是来自上面的jsFiddle 链接的代码,你会发现它非常粗糙,线条不平滑,而且你肯定可以看出在描边中线条粗细的变化。 - ryuutatsuo

2
建议使用一系列贝塞尔曲线来呈现,这些曲线将围绕着要填充的曲线。(即以ctx.fill结尾) 还有很多工作要做,但希望这可以帮助。 我改编了一个漂亮的贝塞尔曲线演示应用程序,并将其添加到了你的代码中。 http://jsfiddle.net/d3zFU/1/
/*
 * Canvas curves example
 *
 * By Craig Buckler,        http://twitter.com/craigbuckler
 * of OptimalWorks.net        http://optimalworks.net/
 * for SitePoint.com        http://sitepoint.com/
 *
 * Refer to:
 * http://blogs.sitepoint.com/html5-canvas-draw-quadratic-curves/
 * http://blogs.sitepoint.com/html5-canvas-draw-bezier-curves/
 *
 * This code can be used without restriction.
 */

(function() {

var canvas, ctx, code, point, style, drag = null, dPoint;

// define initial points
function Init(quadratic) {

    point = {
        p1: { x:100, y:250 },
        p2: { x:400, y:250 }
    };

    if (quadratic) {
        point.cp1 = { x: 250, y: 100 };
    }
    else {
        point.cp1 = { x: 150, y: 100 };
        point.cp2 = { x: 350, y: 100 };
    }

    // default styles
    style = {
        curve:    { width: 6, color: "#333" },
        cpline:    { width: 1, color: "#C00" },
        point: { radius: 10, width: 2, color: "#900", fill: "rgba(200,200,200,0.5)", arc1: 0, arc2: 2 * Math.PI }
    }

    // line style defaults
    ctx.lineCap = "round";
    ctx.lineJoin = "round";

    // event handlers
    canvas.onmousedown = DragStart;
    canvas.onmousemove = Dragging;
    canvas.onmouseup = canvas.onmouseout = DragEnd;

    DrawCanvas();
}


// draw canvas
function DrawCanvas() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    // control lines
    ctx.lineWidth = style.cpline.width;
    ctx.strokeStyle = style.cpline.color;
    ctx.fillStyle = style.cpline.color;
    ctx.beginPath();
    ctx.moveTo(point.p1.x, point.p1.y);
    ctx.lineTo(point.cp1.x, point.cp1.y);
    if (point.cp2) {
        ctx.moveTo(point.p2.x, point.p2.y);
        ctx.lineTo(point.cp2.x, point.cp2.y);
    }
    else {
        ctx.lineTo(point.p2.x, point.p2.y);
    }
    ctx.stroke();

    // curve
ctx.lineWidth = 1 ; //style.curve.width;
    ctx.strokeStyle = style.curve.color;
    ctx.beginPath();
    ctx.moveTo(point.p1.x, point.p1.y);
    if (point.cp2) {
        ctx.bezierCurveTo(point.cp1.x, point.cp1.y, point.cp2.x, point.cp2.y, point.p2.x, point.p2.y);
        ctx.bezierCurveTo(point.cp2.x, point.cp2.y+12, point.cp1.x, point.cp1.y+12, point.p1.x, point.p1.y);

    }
    else {
        ctx.quadraticCurveTo(point.cp1.x, point.cp1.y, point.p2.x, point.p2.y);
    }
//ctx.stroke();
ctx.fill();

    // control points
    for (var p in point) {
        ctx.lineWidth = style.point.width;
        ctx.strokeStyle = style.point.color;
        ctx.fillStyle = style.point.fill;
        ctx.beginPath();
        ctx.arc(point[p].x, point[p].y, style.point.radius, style.point.arc1, style.point.arc2, true);
        ctx.fill();
        ctx.stroke();
    }

    ShowCode();
}


// show canvas code
function ShowCode() {
    if (code) {
        code.firstChild.nodeValue =
            "canvas = document.getElementById(\"canvas\");\n"+
            "ctx = canvas.getContext(\"2d\")\n"+
            "ctx.lineWidth = " + style.curve.width +
            ";\nctx.strokeStyle = \"" + style.curve.color +
            "\";\nctx.beginPath();\n" +
            "ctx.moveTo(" + point.p1.x + ", " + point.p1.y +");\n" +
            (point.cp2 ?
                "ctx.bezierCurveTo("+point.cp1.x+", "+point.cp1.y+", "+point.cp2.x+", "+point.cp2.y+", "+point.p2.x+", "+point.p2.y+");" :
                "ctx.quadraticCurveTo("+point.cp1.x+", "+point.cp1.y+", "+point.p2.x+", "+point.p2.y+");"
            ) +
            "\nctx.stroke();"
        ;
    }
}


// start dragging
function DragStart(e) {
    e = MousePos(e);
    var dx, dy;
    for (var p in point) {
        dx = point[p].x - e.x;
        dy = point[p].y - e.y;
        if ((dx * dx) + (dy * dy) < style.point.radius * style.point.radius) {
            drag = p;
            dPoint = e;
            canvas.style.cursor = "move";
            return;
        }
    }
}


// dragging
function Dragging(e) {
    if (drag) {
        e = MousePos(e);
        point[drag].x += e.x - dPoint.x;
        point[drag].y += e.y - dPoint.y;
        dPoint = e;
        DrawCanvas();
    }
}


// end dragging
function DragEnd(e) {
    drag = null;
    canvas.style.cursor = "default";
    DrawCanvas();
}


// event parser
function MousePos(event) {
    event = (event ? event : window.event);
    return {
        x: event.pageX - canvas.offsetLeft,
        y: event.pageY - canvas.offsetTop
    }
}


// start
canvas = document.getElementById("canvas");
code = document.getElementById("code");
if (canvas.getContext) {
    ctx = canvas.getContext("2d");
    Init(canvas.className == "quadratic");
}

})();


-1

重要提示!

我收集了一些必要的部件,并在此附上所有内容!

<canvas id="paint_board" width="500" height="800" style="border: 1px solid;"></canvas>

<script>
var el = document.getElementById('paint_board');
// rect gets the cavas left top value in browser
var rect = el.getBoundingClientRect();
var ctx = el.getContext('2d');
    ctx.lineJoin = ctx.lineCap = 'round';
    ctx.lineWidth = 1;
var isDrawing, pen_type=1;
var screenWidth=500, screenHeight=500;  //set canvas width and height
var strokes=20;                         //how many strokes to draw
var color = [0, 0, 0]; // color val RGB 0-255, 0-255, 0-255
var painters = [], unpainters = [], timers = [];
var brushPressure=1; // brush Opacity
var easing = 0.7; // kind of "how loopy" higher= bigger loops
var refreshRate = 30; // set this higher if performace is an issue directly affects easing
var mouseX = screenWidth / 2, mouseY = screenHeight / 2;
var testinterval;

pen_init();
function pen_init(){
    for(var i = 0; i < strokes; i++) {
        var ease = Math.random() * 0.05 + easing;
        painters.push({
            dx : screenWidth / 2,
            dy : screenHeight / 2,
            ax : 0,
            ay : 0,
            div : 0.1,
            ease : ease
        });
    }
    testinterval = setInterval(update, refreshRate);
    function update() {
        var i;
        ctx.strokeStyle = "rgba(" + color[0] + ", " + color[1] + ", " + color[2] + ", " + brushPressure + ")";
        for( i = 0; i < painters.length; i++) {
            ctx.beginPath();
            var dx = painters[i].dx;
            var dy = painters[i].dy;
            ctx.moveTo(dx, dy);
            var dx1 = painters[i].ax = (painters[i].ax + (painters[i].dx - mouseX) * painters[i].div) * painters[i].ease;
            painters[i].dx -= dx1;
            var dx2 = painters[i].dx;
            var dy1 = painters[i].ay = (painters[i].ay + (painters[i].dy - mouseY) * painters[i].div) * painters[i].ease;
            painters[i].dy -= dy1;
            var dy2 = painters[i].dy;
            ctx.lineTo(dx2, dy2);
            ctx.stroke();
        }
    }
}
el.onmousedown = function(e) {
    isDrawing = true;
    mouseX = e.clientX+window.scrollX-rect.left;
    mouseY = e.clientY+window.scrollY-rect.top;
    var i = 0, paintersLen = painters.length;
    for(i; i < paintersLen; i++) {
                painters[i].dx = mouseX;
                painters[i].dy = mouseY;
    }
};
el.onmousemove = function(e) {
    if (!isDrawing) return;
    mouseX = e.clientX+window.scrollX-rect.left;
    mouseY = e.clientY+window.scrollY-rect.top;
};
el.onmouseup = function() {
    isDrawing = false;
};
</script>

只需复制全部内容并粘贴到您的代码中。别忘了点击投票!


仅仅发布代码并不是一个恰当的回答。请解释一下。 - GaboBrandX

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