平滑的画布线和曲线起点 [英] Smooth canvas line and on curser start point

查看:129
本文介绍了平滑的画布线和曲线起点的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试创建一个canvas web应用程序,并有两个主要问题。我很想让笔工具绘制更流畅。其次,每次清除草图并再次绘制时,线条将从与curser / mouse不同的点开始。



这里是我的绘图工具的javascript: / p>

  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 detachedage = function(){
dragging = false;
}

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

这是我如何清除草图:

  // JavaScript文档

//绑定事件处理程序以清除按钮
document.getElementById('clear')。addEventListener ',function(){
context.clearRect(0,0,canvas.width,canvas.height);
},false);

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



感谢您的帮助!

解决方案

首先是在错误的地方开始的行的问题。您忘记完成创建的路径。你有 beginPath moveTo ,但你把它挂起来。



平滑



线平滑是许多专业绘图应用程序处理各种解决方案的问题的一个非常复杂的事情。似乎没有一个同意的方法。大问题是..你如何平滑线,但不破坏所需的线?



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



strong>降低线路复杂度



第一步,降低线路复杂度。采样鼠标给了许多点。所以我需要减少分数,但不会丢失任何细节。



我使用 Ramer-Douglas-Peucker 算法。它很快,并且很好地减少了一条线的复杂性(点数)。下面你可以找到我的算法的实现。这不是最好的,因为它可以做一些优化。您最有可能在其他语言中找到它,并将其移植到javascript中。



它使用递归函数来减少基于线段之间的长度和角度的复杂性。在其核心是两个线段的点积,它是确定两个线段之间的角度的快速方式。

  //基于
的行简化// Ramer-Douglas- Peucker算法
// referance https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
// points:are和数组数组,由[ [x,y],[x,y],...,[x,y]]
// length:以像素为单位,是实际距离的平方。
//返回与输入参数点形式相同的点数组。
var simplifyLineRDP = function(points,length){
var simplify = function(start,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 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 =
maxDist = dist;
}
}

if(maxDist> length){//继续简化,而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;
}

使用贝塞尔曲线平滑 b
$ b

接下来的平滑。因为线已经被简化,如果合理快速,然后比较许多线之间的角度并且如果角度低于所需的阈值则创建贝塞尔曲线。



下面是例子我怎么做。虽然这不适合原始行,它只是关心平滑。它再次是一个黑客我的部分,而不是基于任何尝试和测试的算法。我有另一个bezier适合,但是这是太慢的示例。



基本上,它通过线段,计算两个段之间的角度,如果角度低于阈值,则其沿着两条线段的切线添加贝塞尔控制点,根据是否平滑两个连续点来做出二阶或三阶贝塞尔。这是一个更复杂的算法的剥离版本,所以请放心。

  //这是我自己的平滑方法blindman`s smoother 
//它创建一组贝塞尔控制点,第二阶或第三阶
//贝塞尔曲线。
// points:点列表[[x,y],[x,y],...,[x,y]]
// cornerThres:之间的线。
//当角度小于cornerThres时,平滑。
// match:如果为true,那么控制点将被平衡。
//函数将复制点
//返回[[x,y],[x,y,bx,by],[x,y,b1x,b1y,b2x,b2y ],.....]与x和y线点
// bx,控制点的二阶贝塞尔和b1x,b1y,b2x,b2y控制
//指向三阶贝塞尔。这些根据需要混合。测试
//每个点数组的长度来计算出哪个bezier(如果有的话)。

var smoothLine = function(points,cornerThres,match){//如果线的角度小于thres,则在点处添加bezier控制点
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){//获取产品
// dist1,dist2,nx1,nx2,ny1,ny2是长度和法线, b //标准化两个向量
dist1 = Math.sqrt(x * x + y * y); // get length
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;
}
return Math.acos(nx1 * nx2 + ny1 * ny2); // dot product
}
newPoints = []; // array for new points
aLen = points.length;
if(aLen< = 2){//如果行太短,则没有内容
for(i = 0; i newPoints.push([points [i] [0],points [i] [1]]);
}
return newPoints;
}
p1 = points [0];
endP = points [aLen-1];
i = 0; //从第二个poitn开始如果行未关闭
closed = false;
len = Math.hypot(p1 [0] - endP [0],p1 [1] -endP [1]);
if(len< Math.SQRT2){//端点是相同的。在坐标空间中加入它们
endP = p1;
i = 0; //从第一点开始如果行关闭
p1 = points [aLen-2];
closed = true;
}
newPoints.push([points [i] [0],points [i] [1]])
for(; 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] ]));
if(dist1!== 0){// dist1和dist2来自点函数
if(angle< cornerThres * 3.14){//如果线之间的角度小则折弯它
if(match){
dist1 = Math.min(dist1,dist2);
dist2 = dist1;
}
//使用沿着线的两个归一化向量来创建切线向量
x =(nx1 + nx2)/ 2;
y =(ny1 + ny2)/ 2;
len = Math.sqrt(x * x + y * y); // normalizing 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([//使用新的贝塞尔控制点创建新点。
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){//如果关闭然后复制第一个点到最后。
p1 = [];
for(i = 0; 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])$ ​​b $ b for(i = 0; i p = line [i];
p1 = line [i + 1]
if(p.length === 2){// linear
ctx.lineTo(p [0],p [1])$ ​​b $ b} 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])$ ​​b $ b}
ctx.stroke();
}

因此,使用这些来平滑线。只需在绘制时捕获鼠标点。当完成后,依次向两个函数发送点。删除绘制的线并将其替换为新线。这是一个有点在钢笔和平滑的结果之间的滞后,但两个功能有很大的改进的余地。



为了把所有在一起我已经添加下面的代码段。左上角的两个条控制平滑和细节。底部栏控制上述第一个功能,顶部控制平滑(bezier),看到的红色越亮,线条越平滑,细节减少越多。



按钮清除或只是重新启动。



对不起,这是比我预期的更多的工作,所以评论是有点稀疏。我将根据时间允许来改进注释。



  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算法的行简化//参考https://en.wikipedia。 org / wiki / Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm //点是由[[x,y],[x,y],...,[x,y]]组成的数组数组// length以像素为单位,是实际距离的平方//返回与输入参数形式相同的点的数组points.var simplifyLineRDP = function(points,length){var simplify = function(start,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  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> 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]];简化(0,end); newLine.push(points [end]); return newLine;} //这是我自己的平滑方法//它创建一组bezier控制点(二阶或三阶)//贝塞尔曲线//点:点列表// cornerThres:何时平滑拐角并表示线之间的角度。 //当角度小于角度时Thres then smooth.// match:如果为true那么控制点将被平衡//函数将复制pointsvar smoothLine = function(points,cornerThres,match){//如果线具有小于角度的角度,则在点处添加贝塞尔控制点。变量p1,p2,p3,dist1,dist2,x,y,endP,len,angle,i,newPoints,aLen,closed,bal,cont1,nx1, ny1,ny2,np;函数dot(x,y,xx,yy){// get do product // dist1,dist2,nx1,nx2,ny1,ny2 are the length and normals and used outside function dist1 = Math.sqrt x * x + y * y)。 // get length 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; } return Math.acos(nx1 * nx2 + ny1 * ny2); // dot product} newPoints = []; //新点的数组aLen = points.length; if(aLen <= 2){//对于(i = 0; i  

  .canC {width:1000px; height:500px;}  

 < canvas class =canC id =canVwidth = 1000 height = 500>< / canvas>  


I am attempting to create a canvas web application and having two main problems. I would love to make the pen tool draw more smoothly. Secondly, each time I clear the sketch and begin to draw again the line begins in a different point to the curser/mouse.

Here is javascript for my drawing tool:

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

and this is how I am clearing the sketch:

// JavaScript Document

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

A live preview can be seen at: http://www.sarahemily.net/canvas/

THANKS FOR YOUR HELP!

解决方案

First the problem of the line starting in the wrong spot. You are forgetting to finish the path you create. You have beginPath, and moveTo but you leave it hanging. You need to call stroke once when the mouse button is up.

Smoothing.

Line smoothing is a very complicated thing to do with many professional drawing apps tackling the problem with a variety of solutions. There does not seem to be one agreed upon method. The big problem is.. How do you smooth a line but not destroy the desired line? and How do you do it quickly????

Here I present a two stage process.

Reduce the line complexity

Step one, reduce the line complexity. Sampling the mouse gives way to many points. So I need to reduce the number of points, but not lose any details.

I use the Ramer–Douglas–Peucker algorithm. It's quick and does a good job of reducing the complexity (number of points) of a line. Below you can find my implementation of the algorithm. It's not the best as it could do with some optimisation. You could most likely find it in some other language and port it to javascript.

It uses a recursive function to reduce complexity based on length and angle between line segments. At its core is the dot product of two line segments, it is a quick way of determining the angle between the two segments. See the supplied link above for more details.

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

Smoothing using bezier curves

Next the smoothing. As the line has been simplified it if reasonably quick to then compare the angles between the many lines and create a bezier if the angle is below a required threshold.

Below is a example of how I do it. Though this will not fit the original line it is just concerned with smoothing. It is again a bit of a hack on my part and not based on any tried and tested algorithm. I have another one that does a bezier fit but that is too slow for the example.

Basicly it steps through the line segments and calculates the angle between two segments, if the angle is below the threshold it then adds bezier control points along the tangent of the two line segments, making either 2nd order or 3rd order beziers depending on whether two consecutive points are smoothed. This is a stripped down version of a much more complicated algorithm so excuse the mess.

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

As I did not put that much thought into ease of use you will have to use the following function to render the resulting line.

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

So to use these to smooth a line. Simply capture the mouse points as you draw. When the done, then send the points to both functions in turn. Erase the drawn line and replace it with the new line. The is a bit of a lag between pen up and the smoothed result, but there is plenty of room for improvement in both functions.

To put it all together I have added a snippet below. The two bars at the top left control the smoothing and detail. The bottom bar controls the first function described above and the top controls the smoothing (bezier) the more red you see the smoother the lines and greater the detail reduction.

Middle mouse button clears or just restart.

Sorry, this was more work than I expected so the comments are a little sparse. I will improve the comments as time permits..

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>

这篇关于平滑的画布线和曲线起点的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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