繁体   English   中英

平滑的画布线和光标起点

[英]Smooth canvas line and on curser start point

我正在尝试创建一个画布Web应用程序,但有两个主要问题。 我希望使钢笔工具绘制更流畅。 其次,每次我清除草图并再次开始绘制时,线条从光标/鼠标的不同点开始。

这是我的绘图工具的javascript:

var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');

var radius = 10;
var dragging = false;

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

context.lineWidth = radius*2;

var putPoint = function(e){
if(dragging){
context.lineTo(e.clientX, e.clientY);
context.stroke();
context.beginPath();
context.arc(e.clientX, e.clientY, radius, 0, Math.PI*2);
context.fill();
context.beginPath();
context.moveTo(e.clientX, e.clientY);
}}

var engage = function(){
dragging = true;
}

var disengage = function(){
dragging = false;
}

canvas.addEventListener('mousedown', engage);
canvas.addEventListener('mousemove', putPoint);
canvas.addEventListener('mouseup', disengage);

这就是我清除草图的方式:

// JavaScript Document

  // bind event handler to clear button
  document.getElementById('clear').addEventListener('click', function() {
    context.clearRect(0, 0, canvas.width, canvas.height);
  }, false);

可以在以下位置查看实时预览: http : //www.sarahemily.net/canvas/

谢谢你的帮助!

首先,生产线的问题始于错误的位置。 您忘记完成创建的路径。 您具有beginPathmoveTo但是将其挂起。 你需要调用stroke一旦当鼠标按键时。

平滑。

使用许多专业的绘图应用程序通过各种解决方案来解决问题,线条平滑是一件非常复杂的事情。 似乎没有一种商定的方法。 最大的问题是..如何平滑线条而不破坏所需的线条? 和你如何迅速做到????

在这里,我介绍一个两个阶段的过程。

降低生产线复杂度

第一步,降低生产线复杂度。 对鼠标进行采样可以让很多点。 因此,我需要减少分数,但不要丢失任何细节。

我使用了Ramer–Douglas–Peucker算法。 它很快并且在降低线的复杂度(点数)方面做得很好。 您可以在下面找到我对算法的实现。 这不是最好的,因为可以进行一些优化。 您很可能会以其他某种语言找到它并将其移植到javascript。

它使用递归函数根据线段之间的长度和角度来降低复杂度。 它的核心是两个线段的点积,这是确定两个线段之间角度的快速方法。 有关更多详细信息,请参见上面提供的链接。

// 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) { // recursive 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) {
               // dot product
                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 The blindman`s smoother
// It creates a set of bezier control points either 2nd order or third order 
// bezier curves.
// points: list of points [[x,y],[x,y],...,[x,y]]
// 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
// returns [[x,y],[x,y,bx,by],[x,y,b1x,b1y,b2x,b2y],.....] with x and y line points
// bx,by control points for 2nd order bezier and b1x,b1y,b2x,b2y the control
// points for 3rd order bezier. These are mixed as needed. Test the length of
// each point array to work out which bezier if any to use.

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;    
}

由于我没有对易用性进行过多考虑,因此您将不得不使用以下函数来渲染结果行。

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();
}

因此,使用这些来平滑线条。 绘制时只需捕获鼠标点。 完成后,依次将点发送到两个功能。 擦除绘制的线条,并用新的线条替换它。 这在笔向上和平滑结果之间有些滞后,但是在两种功能上都有很大的改进空间。

综上所述,我在下面添加了一个代码段。 左上角的两个条控制平滑度和细节。 底部的条控制上述的第一个功能,顶部的条控制平滑(贝塞尔曲线),您看到的红色越多,线条越平滑,细节减少越多。

鼠标中键将清除或仅重新启动。

抱歉,这项工作比我预期的要多,因此评论很少。 如果时间允许,我将改善评论。

 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 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 = 1; ctx.fillStyle = "red"; ctx.fillRect(3,3,lineSmooth.angle*200,10); ctx.fillRect(3,14,lineSmooth.lengthMin*10,10); ctx.textAlign = "left"; ctx.textBaseline = "top"; ctx.fillStyle = "#5F2" ctx.strokeRect(3,3,200,10); ctx.fillText("Smooth",5,2) ctx.strokeRect(3,14,200,10); ctx.fillText("Detail",5,13); }else{ canvas.style.cursor = "crosshair"; } if(!input){ ctx.lineWidth = 3; 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;} 
 <canvas class="canC" id="canV" width=1000 height=500></canvas> 

我希望使钢笔工具绘制更流畅

使用可以使用二次曲线代替直线:

ctx.quadraticCurveTo(cpx, cpy, x, y);

例如: http//www.w3schools.com/tags/canvas_quadraticcurveto.asp

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM