如何使用html canvas和javascript绘制一条平滑的连续线 [英] How to draw a smooth continuous line with mouse using html canvas and javascript

查看:309
本文介绍了如何使用html canvas和javascript绘制一条平滑的连续线的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我试图用html5 canvas和普通的javascript创建一个简单的绘制/绘制程序。我已经可以正常工作了,但是当绘制和移动鼠标的速度太快时,线路会断开,我最终会出现一连串的点 - 我如何才能使这个平滑的实线?



建议将非常感谢!我对JS非常陌生,所以代码示例会非常有用,请提前致谢。



当前的JS是:

< pre-class =snippet-code-js lang-js prettyprint-override> var canvas,ctxvar mouseX,mouseY,mouseDown = 0function draw(ctx,x,y,size){ctx.fillStyle = #000000ctx.beginPath()ctx.arc(x,y,size,0,Math.PI * 2,true)ctx.closePath()ctx.fill()}函数clearCanvas(canvas,ctx){ctx.clearRect ()函数onMouseUp(){mouseDown = 0}函数onMouseMove(e){getMousePos(){mouseDown = 1 draw(ctx,mouseX,mouseY,2) e)if(mouseDown == 1){draw(ctx,mouseX,mouseY,2)}} function getMousePos(e){if(!e)var e = event if(e.offsetX){mouseX = e.offsetX mouseY = e.offsetY}否则if(e.layerX){mouseX = e.layerX mouseY = e.layerY}} fu nction init(){canvas = document.getElementById('sketchpad')ctx = canvas.getContext('2d')canvas.addEventListener('mousedown',onMouseDown,false)canvas.addEventListener('mousemove',onMouseMove,false)window .addEventListener('mouseup',onMouseUp,false)} init();

 < canvas id =sketchpadwidth =500height =500>< / canvas>  

解决方案

用鼠标绘制平滑曲线。



悲伤地如果你想保持真正的艺术家预定线,这并不容易。

它涉及记录整个鼠标笔画。当中风完成时,将点数减少到细节限制(由艺术家设置),然后对其余点应用贝塞尔平滑功能。



可以完成因为笔画被画出来,但是对于一些设备来说,如果线条变得很长,这可能变得太多。由于线条细节减少会在显示平滑线条时看到所有点,有些人不喜欢它随着线条变长而稍微变化的方式。



演示



下面的代码演示了一个我认为有用的解决方案。


  • 使用左键平滑绘制一个按钮释放。

  • 使用右侧按钮绘制实时平滑(蓝线)。

  • 鼠标中键单击清除。



使用顶部的两个滑块设置平滑量和细节量。左键单击以拖出笔划,显示原始线。当鼠标被释放后,线条会被简化,平滑并添加到背景图片中。



  var canvas = document.getElementById(canV); var ctx = canvas.getContext(2d); // mouse stuffvar mouse = {x:0,y:0,buttonLastRaw:0,//用户修改的值buttonRaw:0,buttons:[1,2,4,6 ,5,3],//用于设置和清除按钮原始位的掩码;}; function mouseMove(event){mouse.x = event.offsetX; mouse.y = event.offsetY; if(mouse.x === undefined){mouse.x = event.clientX; mouse.y = event.clientY;} if(event.type ===mousedown){mouse.buttonRaw | = mouse.buttons [event.which-1]; } else if(event.type ===mouseup){mouse.buttonRaw& = mouse.buttons [event.which + 2]; } else if(event.type ===mouseout){mouse.buttonRaw = 0; mouse.over = false; } else if(event.type ===mouseover){mouse.over = true; } event.preventDefault();} canvas.addEventListener('mousemove',mouseMove); canvas.addEventListener('mousedown',mouseMove); canvas.addEventListener('mouseup',mouseMove); canvas.addEventListener('mouseout',mouseMove); canvas.addEventListener('mouseover',mouseMove); canvas.addEventListener(contextmenu,function(e){e.preventDefault();},false); //基于// Ramer-Douglas-Peucker算法的线简化// referance https://en.wikipedia。 org / wiki / Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm // points是由[[x,y],[x,y],...,[x,y]]组成的数组数组//长度以像素为单位,并且是实际距离的平方.//返回与输入参数相同形式的点的数组points.var simplLineRDP = function(points,length){var simplify = function(start,end) {// recursize简化点从开始到结束var maxDist,index,i,xx,yy,dx,dy,ddx,ddy,p1,p2,p,t,dist,dist1; p1 = points [start]; p2 =点[结束]; xx = p1 [0]; yy = p1 [1]; ddx = p2 [0]  -  xx; ddy = p2 [1]  -  yy; dist1 =(ddx * ddx + ddy * ddy); maxDist =长度; for(var i = start + 1; i  1){dx = p [0] -p2 [0]; dy = p [1]  -  p2 [1]; } else if(t> 0){dx = p [0]  - (xx + ddx * t); dy = p [1]  - (yy + ddy * t); } else {dx = p [0]  -  xx; dy = p [1]  -  yy; }} else {dx = p [0]  -  xx; dy = p [1]  -  yy; } dist = dx * dx + dy * dy if(dist> maxDist){index = i; maxDist = dist; }} if(maxDist> length){//继续简化,而maxDist>如果(索引 - 开始> 1){简化(开始,索引); } newLine.push(points [index]); if(end  -  index> 1){simplify(index,end); }}} var end = points.length  -  1; var newLine = [points [0]];简化(0,结束); newLine.push(点[END]); return newLine;} //这是我自己的平滑方法//它创建一组贝塞尔控制点,二阶或三阶// bezier曲线// points:点列表// cornerThres:何时平滑拐角并表示线与线之间的角度。 //当角度小于角落时,则光滑//匹配:如果为真,则控制点将被平衡。//函数将复制pointsvar smoothLine = function(points,cornerThres,match){//如果线条的角度小于这些角度,则在点上增加贝塞尔控制点。var p1,p2,p3,dist1,dist2,x,y,endP,len,angle,i,newPoints,aLen,closed,bal,cont1,nx1,nx2, ny1,ny2,np;函数dot(x,y,xx,yy){//获得do乘积// dist1,dist2,nx1,nx2,ny1,ny2是长度和法线,并用于外部函数//规范化两个向量dist1 = Math.sqrt x * x + y * y); //获得长度if(dist1> 0){// normalize nx1 = x / dist1; ny1 = y / dist1; } else {nx1 = 1; //需要有东西,所以这会做得和任何事情一样ny1 = 0; } dist2 = Math.sqrt(xx * xx + yy * yy); if(dist2> 0){nx2 = xx / dist2; ny2 = yy / dist2; } else {nx2 = 1; ny2 = 0; }返回Math.acos(nx1 * nx2 + ny1 * ny2); // dot product} newPoints = []; //新点数组aLen = points.length;如果(aLen <= 2){//对于(i = 0; i< aLen; i ++){//确保点被复制newPoints.push([points [i] [0],点[I] [1]]); } return newPoints; } p1 = points [0]; endP = points [aLen-1]; i = 0; //如果行未关闭,则从第二个poitn开始关闭= false; len = Math.hypot(p1 [0]  -  endP [0],p1 [1] -endP [1]);如果(len  

.canC {宽度:1000px;高度:500像素; border:1px black solid;}

 < canvas class = canCid =canVwidth = 1000 height = 500>< / canvas>  

I'm attempting to create a simple draw/paint programme using html5 canvas and plain javascript. I've got it working ok, but when drawing and moving the mouse too fast the line disconnects and I just end up with a line of dots - how can I make this a smooth continuous line?

Advice would be much appreciated! I'm quite new to JS so code examples would be really useful, thanks in advance.

Current JS is:

var canvas, ctx
var mouseX, mouseY, mouseDown = 0

function draw(ctx,x,y,size) {
  ctx.fillStyle = "#000000"
  ctx.beginPath()
  ctx.arc(x, y, size, 0, Math.PI*2, true)
  ctx.closePath()
  ctx.fill()
}

function clearCanvas(canvas,ctx) {
  ctx.clearRect(0, 0, canvas.width, canvas.height)
}

function onMouseDown() {
  mouseDown = 1
  draw(ctx, mouseX, mouseY, 2)
}

function onMouseUp() {
  mouseDown = 0
}

function onMouseMove(e) {
  getMousePos(e)
  if (mouseDown == 1) {
      draw(ctx, mouseX, mouseY, 2)
  }
}

function getMousePos(e) {
  if (!e)
      var e = event
  if (e.offsetX) {
      mouseX = e.offsetX
      mouseY = e.offsetY
  }
  else if (e.layerX) {
      mouseX = e.layerX
      mouseY = e.layerY
  }
 }

function init() {
    canvas = document.getElementById('sketchpad')
    ctx = canvas.getContext('2d')
    canvas.addEventListener('mousedown', onMouseDown, false)
    canvas.addEventListener('mousemove', onMouseMove, false)
    window.addEventListener('mouseup', onMouseUp, false)
}
init();

<canvas id="sketchpad" width="500" height="500"></canvas>

解决方案

Drawing a smooth curve with the mouse.

Sadly it is not that easy if you wish to stay true to the artists intended line.

It involves recording the whole mouse stroke. When the stroke is complete, reduce the number of points to the detail limit (set by artist) then apply a bezier smoothing function on the remaining points.

It can be done as the stroke is drawn but for some devices this can become too much if the line becomes very long. As the line detail reduction looks at all points when showing the smoothed line live some people dont like the way it slightly changes as the line gets longer.

Demo

The code below demonstrates a solution I have found useful.

  • Use the left button to draw with smoothing done one button release.
  • Use the right button to draw with live smoothing (blue line).
  • Middle mouse button click to clear.

Use the two sliders at the top to set the amount of smoothing, and the amount of detail. Left click to drag out a stroke, the raw line is shown. When the mouse is released the line is then simplified, smoothed, and added to the background image.

var canvas = document.getElementById("canV"); 
var ctx = canvas.getContext("2d");


// mouse stuff
var mouse = {
    x:0,
    y:0,
    buttonLastRaw:0, // user modified value 
    buttonRaw:0,
    buttons:[1,2,4,6,5,3], // masks for setting and clearing button raw bits;
};
function mouseMove(event){
    mouse.x = event.offsetX;  mouse.y = event.offsetY; 
    if(mouse.x === undefined){ mouse.x = event.clientX;  mouse.y = event.clientY;}    
    if(event.type === "mousedown"){ mouse.buttonRaw |= mouse.buttons[event.which-1];
    }else if(event.type === "mouseup"){mouse.buttonRaw &= mouse.buttons[event.which+2];
    }else if(event.type === "mouseout"){ mouse.buttonRaw = 0; mouse.over = false;
    }else if(event.type === "mouseover"){ mouse.over = true; }
    event.preventDefault();
}
canvas.addEventListener('mousemove',mouseMove);
canvas.addEventListener('mousedown',mouseMove);
canvas.addEventListener('mouseup'  ,mouseMove); 
canvas.addEventListener('mouseout'  ,mouseMove); 
canvas.addEventListener('mouseover'  ,mouseMove); 
canvas.addEventListener("contextmenu", function(e){ e.preventDefault();}, false);


// Line simplification based on
// the Ramer–Douglas–Peucker algorithm
// referance https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
// points are and array of arrays consisting of [[x,y],[x,y],...,[x,y]]
// length is in pixels and is the square of the actual distance.
// returns array of points of the same form as the input argument points.
var simplifyLineRDP = function(points, length) {
    var simplify = function(start, end) { // recursize simplifies points from start to end
        var maxDist, index, i, xx , yy, dx, dy, ddx, ddy, p1, p2, p, t, dist, dist1;
        p1 = points[start];
        p2 = points[end];   
        xx = p1[0];
        yy = p1[1];
        ddx = p2[0] - xx;
        ddy = p2[1] - yy;
        dist1 = (ddx * ddx + ddy * ddy);
        maxDist = length;
        for (var i = start + 1; i < end; i++) {
            p = points[i];
            if (ddx !== 0 || ddy !== 0) {
                t = ((p[0] - xx) * ddx + (p[1] - yy) * ddy) / dist1;
                if (t > 1) {
                    dx = p[0] - p2[0];
                    dy = p[1] - p2[1];
                } else 
                if (t > 0) {
                    dx = p[0] - (xx + ddx * t);
                    dy = p[1] - (yy + ddy * t);
                } else {
                    dx = p[0] - xx;
                    dy = p[1] - yy;
                }
            }else{
                dx = p[0] - xx;
                dy = p[1] - yy;
            }
            dist = dx * dx + dy * dy 
            if (dist > maxDist) {
                index = i;
                maxDist = dist;
            }
        }

        if (maxDist > length) { // continue simplification while maxDist > length
            if (index - start > 1){
                simplify(start, index);
            }
            newLine.push(points[index]);
            if (end - index > 1){
                simplify(index, end);
            }
        }
    }    
    var end = points.length - 1;
    var newLine = [points[0]];
    simplify(0, end);
    newLine.push(points[end]);
    return newLine;
}



// This is my own smoothing method 
// It creates a set of bezier control points either 2nd order or third order 
// bezier curves.
// points: list of points
// cornerThres: when to smooth corners and represents the angle between to lines. 
//     When the angle is smaller than the cornerThres then smooth.
// match: if true then the control points will be balanced.
// Function will make a copy of the points

var smoothLine = function(points,cornerThres,match){  // adds bezier control points at points if lines have angle less than thres
    var  p1, p2, p3, dist1, dist2, x, y, endP, len, angle, i, newPoints, aLen, closed, bal, cont1, nx1, nx2, ny1, ny2, np;
    function dot(x, y, xx, yy) {  // get do product
        // dist1,dist2,nx1,nx2,ny1,ny2 are the length and  normals and used outside function
        // normalise both vectors
        dist1 = Math.sqrt(x * x + y * y); // get length
        if (dist1  > 0) {  // normalise
            nx1 = x / dist1 ;
            ny1 = y / dist1 ;
        }else {
            nx1 = 1;  // need to have something so this will do as good as anything
            ny1 = 0;
        }
        dist2  = Math.sqrt(xx * xx + yy * yy);
        if (dist2  > 0) {
            nx2 = xx / dist2;
            ny2 = yy / dist2;
        }else {
            nx2 = 1;
            ny2 = 0;
        }
       return Math.acos(nx1 * nx2 + ny1 * ny2 ); // dot product
    }
    newPoints = []; // array for new points
    aLen = points.length;
    if(aLen <= 2){  // nothing to if line too short
        for(i = 0; i < aLen; i ++){  // ensure that the points are copied          
            newPoints.push([points[i][0],points[i][1]]);
        }
        return newPoints;
    }
    p1 = points[0];
    endP =points[aLen-1];
    i = 0;  // start from second poitn if line not closed
    closed = false;
    len = Math.hypot(p1[0]- endP[0], p1[1]-endP[1]);
    if(len < Math.SQRT2){  // end points are the same. Join them in coordinate space
        endP =  p1;
        i = 0;             // start from first point if line closed
        p1 = points[aLen-2];
        closed = true;
    }       
    newPoints.push([points[i][0],points[i][1]])
    for(; i < aLen-1; i++){
        p2 = points[i];
        p3 = points[i + 1];
        angle = Math.abs(dot(p2[0] - p1[0], p2[1] - p1[1], p3[0] - p2[0], p3[1] - p2[1]));
        if(dist1 !== 0){  // dist1 and dist2 come from dot function
            if( angle < cornerThres*3.14){ // bend it if angle between lines is small
                  if(match){
                      dist1 = Math.min(dist1,dist2);
                      dist2 = dist1;
                  }
                  // use the two normalized vectors along the lines to create the tangent vector
                  x = (nx1 + nx2) / 2;  
                  y = (ny1 + ny2) / 2;
                  len = Math.sqrt(x * x + y * y);  // normalise the tangent
                  if(len === 0){
                      newPoints.push([p2[0],p2[1]]);                                  
                  }else{
                      x /= len;
                      y /= len;
                      if(newPoints.length > 0){
                          var np = newPoints[newPoints.length-1];
                          np.push(p2[0]-x*dist1*0.25);
                          np.push(p2[1]-y*dist1*0.25);
                      }
                      newPoints.push([  // create the new point with the new bezier control points.
                            p2[0],
                            p2[1],
                            p2[0]+x*dist2*0.25,
                            p2[1]+y*dist2*0.25
                      ]);
                  }
            }else{
                newPoints.push([p2[0],p2[1]]);            
            }
        }
        p1 = p2;
    }  
    if(closed){ // if closed then copy first point to last.
        p1 = [];
        for(i = 0; i < newPoints[0].length; i++){
            p1.push(newPoints[0][i]);
        }
        newPoints.push(p1);
    }else{
        newPoints.push([points[points.length-1][0],points[points.length-1][1]]);      
    }
    return newPoints;    
}

// creates a drawable image
var createImage = function(w,h){
    var image = document.createElement("canvas");
    image.width = w;
    image.height =h; 
    image.ctx = image.getContext("2d"); 
    return image;
}  

// draws the smoothed line with bezier control points.
var drawSmoothedLine = function(line){
    var i,p;
    ctx.beginPath()
    ctx.moveTo(line[0][0],line[0][1])
    for(i = 0; i < line.length-1; i++){
       p = line[i];
       p1 = line[i+1]
       if(p.length === 2){ // linear 
            ctx.lineTo(p[0],p[1])
       }else
       if(p.length === 4){ // bezier 2nd order
           ctx.quadraticCurveTo(p[2],p[3],p1[0],p1[1]);
       }else{              // bezier 3rd order
           ctx.bezierCurveTo(p[2],p[3],p[4],p[5],p1[0],p1[1]);
       }
    }
    if(p.length === 2){
        ctx.lineTo(p1[0],p1[1])
    }
    ctx.stroke();
}

// smoothing settings
var liveSmooth;
var lineSmooth = {};
lineSmooth.lengthMin = 8;  // square of the pixel length
lineSmooth.angle = 0.8;      // angle threshold
lineSmooth.match = false;  // not working.
// back buffer to save the canvas allowing the new line to be erased
var backBuffer = createImage(canvas.width,canvas.height);
var currentLine = [];
mouse.lastButtonRaw = 0;  // add mouse last incase not there
ctx.lineWidth = 3;
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.strokeStyle = "black";
ctx.clearRect(0,0,canvas.width,canvas.height);
var drawing = false;  // if drawing
var input = false;  // if menu input
var smoothIt = false;  // flag to allow feedback that smoothing is happening as it takes some time.
function draw(){
    // if not drawing test for menu interaction and draw the menus
    if(!drawing){      
        if(mouse.x < 203 && mouse.y < 24){
            if(mouse.y < 13){
                if(mouse.buttonRaw === 1){
                    ctx.clearRect(3,3,200,10);
                    lineSmooth.angle = (mouse.x-3)/200;
                    input = true;
                }
            }else
            if(mouse.buttonRaw === 1){
                ctx.clearRect(3,14,200,10);
                lineSmooth.lengthMin = (mouse.x-3)/10;
                input = true;
            }
                
            canvas.style.cursor = "pointer";
        }else{
            canvas.style.cursor = "crosshair";
            
        }
        if(mouse.buttonRaw === 0 && input){
            input = false;
            mouse.lastButtonRaw = 0;
        }
        ctx.lineWidth = 0.5;
        ctx.fillStyle = "red";
        ctx.clearRect(3,3,200,10);
        ctx.clearRect(3,14,200,10);
        ctx.fillRect(3,3,lineSmooth.angle*200,10);
        ctx.fillRect(3,14,lineSmooth.lengthMin*10,10);

        ctx.textAlign = "left";
        ctx.textBaseline = "top";
        ctx.fillStyle = "#000"
        ctx.strokeRect(3,3,200,10);
        ctx.fillText("Smooth "+(lineSmooth.angle * (180 / Math.PI)).toFixed(0)+"deg",5,2)
        ctx.strokeRect(3,14,200,10);
        ctx.fillText("Detail "+lineSmooth.lengthMin.toFixed(0) + "pixels",5,13);

    }else{
        canvas.style.cursor = "crosshair"; 
    }
    if(!input){
         ctx.lineWidth = 3;
        if(mouse.buttonRaw === 4 && mouse.lastButtonRaw === 0){
            currentLine = [];
            drawing  = true;

            backBuffer.ctx.clearRect(0,0,canvas.width,canvas.height);
            backBuffer.ctx.drawImage(canvas,0,0);
            currentLine.push([mouse.x,mouse.y])
        }else
        if(mouse.buttonRaw === 4){
            var lp = currentLine[currentLine.length-1]; // get last point
            // dont record point if no movement
            if(mouse.x !== lp[0] || mouse.y !== lp[1] ){
                currentLine.push([mouse.x,mouse.y]);
                ctx.beginPath();
                ctx.moveTo(lp[0],lp[1])
                ctx.lineTo(mouse.x,mouse.y);
                ctx.stroke();
                liveSmooth = smoothLine(
                    simplifyLineRDP(
                        currentLine,
                        lineSmooth.lengthMin
                    ),
                    lineSmooth.angle,
                    lineSmooth.match
                );
                ctx.clearRect(0,0,canvas.width,canvas.height);
               ctx.drawImage(backBuffer,0,0);
                ctx.strokeStyle = "Blue";
                drawSmoothedLine(liveSmooth );
                ctx.strokeStyle = "black";
            }
        }else
        if(mouse.buttonRaw === 0 && mouse.lastButtonRaw === 4){
            ctx.textAlign = "center"
            ctx.fillStyle = "red"
            ctx.fillText("Smoothing...",canvas.width/2,canvas.height/5);
            smoothIt = true;
        }else
        if(smoothIt){
            smoothIt = false;
            
            var newLine = smoothLine(
                simplifyLineRDP(
                    currentLine,
                    lineSmooth.lengthMin
                ),
                lineSmooth.angle,
                lineSmooth.match
            );
            ctx.clearRect(0,0,canvas.width,canvas.height);
            ctx.drawImage(backBuffer,0,0);
            drawSmoothedLine(newLine);
            drawing  = false;
            
        }


        if(mouse.buttonRaw === 1 && mouse.lastButtonRaw === 0){
            currentLine = [];
            drawing  = true;

            backBuffer.ctx.clearRect(0,0,canvas.width,canvas.height);
            backBuffer.ctx.drawImage(canvas,0,0);
            currentLine.push([mouse.x,mouse.y])
        }else
        if(mouse.buttonRaw === 1){
            var lp = currentLine[currentLine.length-1]; // get last point
            // dont record point if no movement
            if(mouse.x !== lp[0] || mouse.y !== lp[1] ){
                currentLine.push([mouse.x,mouse.y]);
                ctx.beginPath();
                ctx.moveTo(lp[0],lp[1])
                ctx.lineTo(mouse.x,mouse.y);
                ctx.stroke();
            }
        }else
        if(mouse.buttonRaw === 0 && mouse.lastButtonRaw === 1){
            ctx.textAlign = "center"
            ctx.fillStyle = "red"
            ctx.fillText("Smoothing...",canvas.width/2,canvas.height/5);
            smoothIt = true;
        }else
        if(smoothIt){
            smoothIt = false;
            
            var newLine = smoothLine(
                simplifyLineRDP(
                    currentLine,
                    lineSmooth.lengthMin
                ),
                lineSmooth.angle,
                lineSmooth.match
            );
            ctx.clearRect(0,0,canvas.width,canvas.height);
            ctx.drawImage(backBuffer,0,0);
            drawSmoothedLine(newLine);
            drawing  = false;
            
        }
    }
    // middle button clear
    if(mouse.buttonRaw === 2){
        ctx.clearRect(0,0,canvas.width,canvas.height);
    }
    mouse.lastButtonRaw = mouse.buttonRaw;

    requestAnimationFrame(draw);

}

draw();

.canC { width:1000px;  height:500px; border:1px black solid;}

<canvas class="canC" id="canV" width=1000 height=500></canvas>

这篇关于如何使用html canvas和javascript绘制一条平滑的连续线的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆