如何使用JavaScript HTML5canvas通过N点绘制平滑曲线?

对于绘图应用程序,我将鼠标移动坐标保存到一个数组,然后用lineTo绘制它们。 结果线不平滑。 我怎样才能在所有聚集的点之间产生一条曲线?

我已经search了,但我只find了3个绘制线的函数:对于2个采样点,只需使用lineTo。 对于3个采样点quadraticCurveTo,对于4个采样点,bezierCurveTo。

(我试图在数组中每4个点绘制一个bezierCurveTo,但是这会导致每4个采样点扭曲,而不是连续的平滑曲线。)

如何编写一个函数来绘制5个采样点以上的平滑曲线?

将后续采样点与不相交的“curveTo”types函数结合在一起的问题是,曲线相交的地方不平滑。 这是因为两条曲线共享一个终点,但受到完全不相交的控制点的影响。 一种解决scheme是“曲线化”下两个后续采样点之间的中点。 使用这些新的插值点连接曲线给出了终点处的平滑过渡(一次迭代的终点成为下一次迭代的控制点 )。换句话说,两条不连续的曲线现在有更多的共同点。

这个解决scheme是从“Foundation ActionScript 3.0animation:让事情移动”一书中提炼出来的。 第95页 – 渲染技术:创build多条曲线。

注意:这个解决scheme实际上并没有通过每一个点,这是我的问题的标题(而不是通过样本点近似曲线,但从来没有通过样本点),但为了我的目的(绘图应用程序),这对我来说已经足够好了,而且在视觉上你无法区分这一点。 有一个解决scheme可以通过所有的样本点,但是要复杂得多(见http://www.cartogrammar.com/blog/actionscript-curves-update/

以下是逼近方法的绘图代码:

// move to the first point ctx.moveTo(points[0].x, points[0].y); for (i = 1; i < points.length - 2; i ++) { var xc = (points[i].x + points[i + 1].x) / 2; var yc = (points[i].y + points[i + 1].y) / 2; ctx.quadraticCurveTo(points[i].x, points[i].y, xc, yc); } // curve through the last two points ctx.quadraticCurveTo(points[i].x, points[i].y, points[i+1].x,points[i+1].y); 

有点晚了,但为了logging。

通过使用基数样条 (也称为规范样条)绘制通过点的平滑曲线,可以获得平滑的线条。

我为canvas做了这个function – 它被分成三个function来增加多function性。 主包装函数如下所示:

 function drawCurve(ctx, ptsa, tension, isClosed, numOfSegments, showPoints) { showPoints = showPoints ? showPoints : false; ctx.beginPath(); drawLines(ctx, getCurvePoints(ptsa, tension, isClosed, numOfSegments)); if (showPoints) { ctx.stroke(); ctx.beginPath(); for(var i=0;i<ptsa.length-1;i+=2) ctx.rect(ptsa[i] - 2, ptsa[i+1] - 2, 4, 4); } } 

绘制一个曲线有一个数组,其x,y点的顺序为: x1,y1, x2,y2, ...xn,yn

像这样使用它:

 var myPoints = [10,10, 40,30, 100,10]; //minimum two points var tension = 1; drawCurve(ctx, myPoints); //default tension=0.5 drawCurve(ctx, myPoints, tension); 

上面的函数调用两个子函数,一个计算平滑点。 这将返回一个具有新点的数组 – 这是计算平滑点的核心函数:

 function getCurvePoints(pts, tension, isClosed, numOfSegments) { // use input value if provided, or use a default value tension = (typeof tension != 'undefined') ? tension : 0.5; isClosed = isClosed ? isClosed : false; numOfSegments = numOfSegments ? numOfSegments : 16; var _pts = [], res = [], // clone array x, y, // our x,y coords t1x, t2x, t1y, t2y, // tension vectors c1, c2, c3, c4, // cardinal points st, t, i; // steps based on num. of segments // clone array so we don't change the original // _pts = pts.slice(0); // The algorithm require a previous and next point to the actual point array. // Check if we will draw closed or open curve. // If closed, copy end points to beginning and first points to end // If open, duplicate first points to befinning, end points to end if (isClosed) { _pts.unshift(pts[pts.length - 1]); _pts.unshift(pts[pts.length - 2]); _pts.unshift(pts[pts.length - 1]); _pts.unshift(pts[pts.length - 2]); _pts.push(pts[0]); _pts.push(pts[1]); } else { _pts.unshift(pts[1]); //copy 1. point and insert at beginning _pts.unshift(pts[0]); _pts.push(pts[pts.length - 2]); //copy last point and append _pts.push(pts[pts.length - 1]); } // ok, lets start.. // 1. loop goes through point array // 2. loop goes through each segment between the 2 pts + 1e point before and after for (i=2; i < (_pts.length - 4); i+=2) { for (t=0; t <= numOfSegments; t++) { // calc tension vectors t1x = (_pts[i+2] - _pts[i-2]) * tension; t2x = (_pts[i+4] - _pts[i]) * tension; t1y = (_pts[i+3] - _pts[i-1]) * tension; t2y = (_pts[i+5] - _pts[i+1]) * tension; // calc step st = t / numOfSegments; // calc cardinals c1 = 2 * Math.pow(st, 3) - 3 * Math.pow(st, 2) + 1; c2 = -(2 * Math.pow(st, 3)) + 3 * Math.pow(st, 2); c3 = Math.pow(st, 3) - 2 * Math.pow(st, 2) + st; c4 = Math.pow(st, 3) - Math.pow(st, 2); // calc x and y cords with common control vectors x = c1 * _pts[i] + c2 * _pts[i+2] + c3 * t1x + c4 * t2x; y = c1 * _pts[i+1] + c2 * _pts[i+3] + c3 * t1y + c4 * t2y; //store points in array res.push(x); res.push(y); } } return res; } 

实际上将点画成一条平滑的曲线(或者只要有一个x,y数组):

 function drawLines(ctx, pts) { ctx.moveTo(pts[0], pts[1]); for(i=2;i<pts.length-1;i+=2) ctx.lineTo(pts[i], pts[i+1]); } 
 var ctx = document.getElementById("c").getContext("2d"); function drawCurve(ctx, ptsa, tension, isClosed, numOfSegments, showPoints) { ctx.beginPath(); drawLines(ctx, getCurvePoints(ptsa, tension, isClosed, numOfSegments)); if (showPoints) { ctx.beginPath(); for(var i=0;i<ptsa.length-1;i+=2) ctx.rect(ptsa[i] - 2, ptsa[i+1] - 2, 4, 4); } ctx.stroke(); } var myPoints = [10,10, 40,30, 100,10, 200, 100, 200, 50, 250, 120]; //minimum two points var tension = 1; drawCurve(ctx, myPoints); //default tension=0.5 drawCurve(ctx, myPoints, tension); function getCurvePoints(pts, tension, isClosed, numOfSegments) { // use input value if provided, or use a default value tension = (typeof tension != 'undefined') ? tension : 0.5; isClosed = isClosed ? isClosed : false; numOfSegments = numOfSegments ? numOfSegments : 16; var _pts = [], res = [], // clone array x, y, // our x,y coords t1x, t2x, t1y, t2y, // tension vectors c1, c2, c3, c4, // cardinal points st, t, i; // steps based on num. of segments // clone array so we don't change the original // _pts = pts.slice(0); // The algorithm require a previous and next point to the actual point array. // Check if we will draw closed or open curve. // If closed, copy end points to beginning and first points to end // If open, duplicate first points to befinning, end points to end if (isClosed) { _pts.unshift(pts[pts.length - 1]); _pts.unshift(pts[pts.length - 2]); _pts.unshift(pts[pts.length - 1]); _pts.unshift(pts[pts.length - 2]); _pts.push(pts[0]); _pts.push(pts[1]); } else { _pts.unshift(pts[1]); //copy 1. point and insert at beginning _pts.unshift(pts[0]); _pts.push(pts[pts.length - 2]); //copy last point and append _pts.push(pts[pts.length - 1]); } // ok, lets start.. // 1. loop goes through point array // 2. loop goes through each segment between the 2 pts + 1e point before and after for (i=2; i < (_pts.length - 4); i+=2) { for (t=0; t <= numOfSegments; t++) { // calc tension vectors t1x = (_pts[i+2] - _pts[i-2]) * tension; t2x = (_pts[i+4] - _pts[i]) * tension; t1y = (_pts[i+3] - _pts[i-1]) * tension; t2y = (_pts[i+5] - _pts[i+1]) * tension; // calc step st = t / numOfSegments; // calc cardinals c1 = 2 * Math.pow(st, 3) - 3 * Math.pow(st, 2) + 1; c2 = -(2 * Math.pow(st, 3)) + 3 * Math.pow(st, 2); c3 = Math.pow(st, 3) - 2 * Math.pow(st, 2) + st; c4 = Math.pow(st, 3) - Math.pow(st, 2); // calc x and y cords with common control vectors x = c1 * _pts[i] + c2 * _pts[i+2] + c3 * t1x + c4 * t2x; y = c1 * _pts[i+1] + c2 * _pts[i+3] + c3 * t1y + c4 * t2y; //store points in array res.push(x); res.push(y); } } return res; } function drawLines(ctx, pts) { ctx.moveTo(pts[0], pts[1]); for(i=2;i<pts.length-1;i+=2) ctx.lineTo(pts[i], pts[i+1]); } 
 canvas { border: 1px solid red; } 
 <canvas id="c"><canvas> 

第一个答案不会通过所有的观点。 这个graphics将精确地通过所有的点,并将是一个完美的曲线,点的点为[{x:,y:}] n这样的点。

 var points = [{x:1,y:1},{x:2,y:3},{x:3,y:4},{x:4,y:2},{x:5,y:6}] //took 5 example points ctx.moveTo((points[0].x), points[0].y); for(var i = 0; i < points.length-1; i ++) { var x_mid = (points[i].x + points[i+1].x) / 2; var y_mid = (points[i].y + points[i+1].y) / 2; var cp_x1 = (x_mid + points[i].x) / 2; var cp_y1 = (y_mid + points[i].y) / 2; var cp_x2 = (x_mid + points[i+1].x) / 2; var cp_y2 = (y_mid + points[i+1].y) / 2; ctx.quadraticCurveTo(cp_x1,points[i].y ,x_mid, y_mid); ctx.quadraticCurveTo(cp_x2,points[i+1].y ,points[i+1].x,points[i+1].y); } 

给KineticJS一个尝试 – 你可以用一系列的点来定义样条。 这是一个例子:

旧url: http : //www.html5canvastutorials.com/kineticjs/html5-canvas-kineticjs-spline-tutorial/

查看存档url: https ://web.archive.org/web/20141204030628/http: //www.html5canvastutorials.com/kineticjs/html5-canvas-kineticjs-spline-tutorial/

正如Daniel Howard所指出的那样 ,Rob Spencer在http://scaledinnovation.com/analytics/splines/aboutSplines.html描述了你想要的东西。;

这是一个交互式演示: http : //jsbin.com/ApitIxo/2/

在这里,它是一个片段,以防jsbin失效。

 <!DOCTYPE html> <html> <head> <meta charset=utf-8 /> <title>Demo smooth connection</title> </head> <body> <div id="display"> Click to build a smooth path. (See Rob Spencer's <a href="http://scaledinnovation.com/analytics/splines/aboutSplines.html">article</a>) <br><label><input type="checkbox" id="showPoints" checked> Show points</label> <br><label><input type="checkbox" id="showControlLines" checked> Show control lines</label> <br> <label> <input type="range" id="tension" min="-1" max="2" step=".1" value=".5" > Tension <span id="tensionvalue">(0.5)</span> </label> <div id="mouse"></div> </div> <canvas id="canvas"></canvas> <style> html { position: relative; height: 100%; width: 100%; } body { position: absolute; left: 0; right: 0; top: 0; bottom: 0; } canvas { outline: 1px solid red; } #display { position: fixed; margin: 8px; background: white; z-index: 1; } </style> <script> function update() { $("tensionvalue").innerHTML="("+$("tension").value+")"; drawSplines(); } $("showPoints").onchange = $("showControlLines").onchange = $("tension").onchange = update; // utility function function $(id){ return document.getElementById(id); } var canvas=$("canvas"), ctx=canvas.getContext("2d"); function setCanvasSize() { canvas.width = parseInt(window.getComputedStyle(document.body).width); canvas.height = parseInt(window.getComputedStyle(document.body).height); } window.onload = window.onresize = setCanvasSize(); function mousePositionOnCanvas(e) { var el=e.target, c=el; var scaleX = c.width/c.offsetWidth || 1; var scaleY = c.height/c.offsetHeight || 1; if (!isNaN(e.offsetX)) return { x:e.offsetX*scaleX, y:e.offsetY*scaleY }; var x=e.pageX, y=e.pageY; do { x -= el.offsetLeft; y -= el.offsetTop; el = el.offsetParent; } while (el); return { x: x*scaleX, y: y*scaleY }; } canvas.onclick = function(e){ var p = mousePositionOnCanvas(e); addSplinePoint(px, py); }; function drawPoint(x,y,color){ ctx.save(); ctx.fillStyle=color; ctx.beginPath(); ctx.arc(x,y,3,0,2*Math.PI); ctx.fill() ctx.restore(); } canvas.onmousemove = function(e) { var p = mousePositionOnCanvas(e); $("mouse").innerHTML = p.x+","+py; }; var pts=[]; // a list of x and ys // given an array of x,y's, return distance between any two, // note that i and j are indexes to the points, not directly into the array. function dista(arr, i, j) { return Math.sqrt(Math.pow(arr[2*i]-arr[2*j], 2) + Math.pow(arr[2*i+1]-arr[2*j+1], 2)); } // return vector from i to j where i and j are indexes pointing into an array of points. function va(arr, i, j){ return [arr[2*j]-arr[2*i], arr[2*j+1]-arr[2*i+1]] } function ctlpts(x1,y1,x2,y2,x3,y3) { var t = $("tension").value; var v = va(arguments, 0, 2); var d01 = dista(arguments, 0, 1); var d12 = dista(arguments, 1, 2); var d012 = d01 + d12; return [x2 - v[0] * t * d01 / d012, y2 - v[1] * t * d01 / d012, x2 + v[0] * t * d12 / d012, y2 + v[1] * t * d12 / d012 ]; } function addSplinePoint(x, y){ pts.push(x); pts.push(y); drawSplines(); } function drawSplines() { clear(); cps = []; // There will be two control points for each "middle" point, 1 ... len-2e for (var i = 0; i < pts.length - 2; i += 1) { cps = cps.concat(ctlpts(pts[2*i], pts[2*i+1], pts[2*i+2], pts[2*i+3], pts[2*i+4], pts[2*i+5])); } if ($("showControlLines").checked) drawControlPoints(cps); if ($("showPoints").checked) drawPoints(pts); drawCurvedPath(cps, pts); } function drawControlPoints(cps) { for (var i = 0; i < cps.length; i += 4) { showPt(cps[i], cps[i+1], "pink"); showPt(cps[i+2], cps[i+3], "pink"); drawLine(cps[i], cps[i+1], cps[i+2], cps[i+3], "pink"); } } function drawPoints(pts) { for (var i = 0; i < pts.length; i += 2) { showPt(pts[i], pts[i+1], "black"); } } function drawCurvedPath(cps, pts){ var len = pts.length / 2; // number of points if (len < 2) return; if (len == 2) { ctx.beginPath(); ctx.moveTo(pts[0], pts[1]); ctx.lineTo(pts[2], pts[3]); ctx.stroke(); } else { ctx.beginPath(); ctx.moveTo(pts[0], pts[1]); // from point 0 to point 1 is a quadratic ctx.quadraticCurveTo(cps[0], cps[1], pts[2], pts[3]); // for all middle points, connect with bezier for (var i = 2; i < len-1; i += 1) { // console.log("to", pts[2*i], pts[2*i+1]); ctx.bezierCurveTo( cps[(2*(i-1)-1)*2], cps[(2*(i-1)-1)*2+1], cps[(2*(i-1))*2], cps[(2*(i-1))*2+1], pts[i*2], pts[i*2+1]); } ctx.quadraticCurveTo( cps[(2*(i-1)-1)*2], cps[(2*(i-1)-1)*2+1], pts[i*2], pts[i*2+1]); ctx.stroke(); } } function clear() { ctx.save(); // use alpha to fade out ctx.fillStyle = "rgba(255,255,255,.7)"; // clear screen ctx.fillRect(0,0,canvas.width,canvas.height); ctx.restore(); } function showPt(x,y,fillStyle) { ctx.save(); ctx.beginPath(); if (fillStyle) { ctx.fillStyle = fillStyle; } ctx.arc(x, y, 5, 0, 2*Math.PI); ctx.fill(); ctx.restore(); } function drawLine(x1, y1, x2, y2, strokeStyle){ ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); if (strokeStyle) { ctx.save(); ctx.strokeStyle = strokeStyle; ctx.stroke(); ctx.restore(); } else { ctx.save(); ctx.strokeStyle = "pink"; ctx.stroke(); ctx.restore(); } } </script> </body> </html> 

我决定添加,而不是将我的解决scheme发布到另一篇文章。 以下是我构build的解决scheme,可能并不完美,但到目前为止输出效果还是不错的。

重要的是:它会通过所有的点!

如果您有任何想法,为了让它更好 ,请分享给我。 谢谢。

以下是之前的比较:

在这里输入图像描述

将此代码保存到HTML以testing它。

 <!DOCTYPE html> <html> <body> <canvas id="myCanvas" width="1200" height="700" style="border:1px solid #d3d3d3;">Your browser does not support the HTML5 canvas tag.</canvas> <script> var cv = document.getElementById("myCanvas"); var ctx = cv.getContext("2d"); function gradient(a, b) { return (by-ay)/(bx-ax); } function bzCurve(points, f, t) { //f = 0, will be straight line //t suppose to be 1, but changing the value can control the smoothness too if (typeof(f) == 'undefined') f = 0.3; if (typeof(t) == 'undefined') t = 0.6; ctx.beginPath(); ctx.moveTo(points[0].x, points[0].y); var m = 0; var dx1 = 0; var dy1 = 0; var preP = points[0]; for (var i = 1; i < points.length; i++) { var curP = points[i]; nexP = points[i + 1]; if (nexP) { m = gradient(preP, nexP); dx2 = (nexP.x - curP.x) * -f; dy2 = dx2 * m * t; } else { dx2 = 0; dy2 = 0; } ctx.bezierCurveTo(preP.x - dx1, preP.y - dy1, curP.x + dx2, curP.y + dy2, curP.x, curP.y); dx1 = dx2; dy1 = dy2; preP = curP; } ctx.stroke(); } // Generate random data var lines = []; var X = 10; var t = 40; //to control width of X for (var i = 0; i < 100; i++ ) { Y = Math.floor((Math.random() * 300) + 50); p = { x: X, y: Y }; lines.push(p); X = X + t; } //draw straight line ctx.beginPath(); ctx.setLineDash([5]); ctx.lineWidth = 1; bzCurve(lines, 0, 1); //draw smooth line ctx.setLineDash([0]); ctx.lineWidth = 2; ctx.strokeStyle = "blue"; bzCurve(lines, 0.3, 1); </script> </body> </html> 

为了添加到K3N的基本样条方法,并且可能解决了TJ Crowder关于曲线在导致误解的地方的“倾斜”问题,我在getCurvePoints()函数中插入了下面的代码,就在res.push(x);之前res.push(x);

 if ((y < _pts[i+1] && y < _pts[i+3]) || (y > _pts[i+1] && y > _pts[i+3])) { y = (_pts[i+1] + _pts[i+3]) / 2; } if ((x < _pts[i] && x < _pts[i+2]) || (x > _pts[i] && x > _pts[i+2])) { x = (_pts[i] + _pts[i+2]) / 2; } 

这有效地创build了每一对连续点之间的(不可见的)边界框,并确保曲线保持在这个边界框内 – 即。 如果曲线上的一个点位于两个点的上/下/左/右,则将其位置改变为方框内的位置。 在这里使用中点,但是可以改进,也许使用线性插值。