如何使用 javascript HTML5画布绘制平滑曲线通过 N 个点?

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

我在谷歌上搜索了一下,但是我只找到了3个绘制线条的函数: 对于2个样本点,只需使用 lineTo。对于3个样本点 quadraticCurveTo,对于4个样本点 bezierCurveTo

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

我如何写一个函数来画一个光滑的曲线与5个样本点和以上?

149002 次浏览

将后续的样本点与不相交的“ curveTo”类型函数连接起来的问题是,曲线相交的地方不是光滑的。这是因为两条曲线共享一个终点,但是受到完全不相交的控制点的影响。一种解决方案是在接下来的两个样本点之间“曲线”到中点。使用这些新的插值点将曲线连接起来,在终点处得到了平滑的转换(一次迭代的终点变成了下一次迭代的 管制站)换句话说,这两条脱节的曲线现在有了更多的共同点。

这个解决方案摘自《基础动作脚本3.0动画: 让事物移动》一书。P. 95-渲染技术: 创建多个曲线。

注意: 这个解决方案实际上并没有画出每一个点,这是我问题的标题(相反,它通过样本点近似曲线,但从来没有通过样本点) ,但是对于我的目的(一个绘图应用程序) ,它对我来说已经足够好了,而且在视觉上你不能分辨出差异。有 的解决方案,通过所有的样本点,但它是更复杂的(见 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);

有点晚了,不过我要声明。

你可以通过使用 基数样条曲线(又名规范样条)来绘制通过点的平滑曲线来实现平滑线。

我把这个函数设计成了画布-它被分成了三个函数来增加通用性。主要包装函式如下:

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>

结果是:

Example pix

您可以轻松地扩展画布,这样您就可以这样调用它:

ctx.drawCurve(myPoints);

在 javascript 中添加以下内容:

if (CanvasRenderingContext2D != 'undefined') {
CanvasRenderingContext2D.prototype.drawCurve =
function(pts, tension, isClosed, numOfSegments, showPoints) {
drawCurve(this, pts, tension, isClosed, numOfSegments, showPoints)}
}

您可以在 NPM (npm i cardinal-spline-js)或 GitLab上找到更优化的版本。

作为 丹尼尔 · 霍华德指出,罗布斯宾塞描述了你想在 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(p.x, p.y);
};
      

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+","+p.y;
};
      

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>

为了补充 K3N 的基数样条方法,或许也为了解决 T.J。 Crowder 对曲线在误导位置“倾斜”的担忧,我在 getCurvePoints()函数中插入了以下代码,就在 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;
}

这有效地在每一对连续的点之间创建一个(不可见的)边界框,并确保曲线停留在这个边界框内。如果曲线上的一个点在两个点的上/下/左/右,它就改变它的位置在盒子里。这里使用了中点,但是这个可以改进,也许可以使用线性插值。

我决定添加,而不是张贴我的解决方案到另一个职位。 下面是我构建的解决方案,可能不是完美的,但到目前为止输出是好的。

重点: 它将通过所有的点!

如果你有任何想法,到 让它变得更好,请分享给我。谢谢。

下面是之前和之后的比较:

enter image description here

将此代码保存为 HTML 以进行测试。

    <!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 (b.y-a.y)/(b.x-a.x);
}
    

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>

第一个答案不会通过所有的点。这个图将精确地通过所有的点,并且将是一条完美曲线,其中的点为[{ 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_x2 = (x_mid + points[i+1].x) / 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);
}

我发现这个很管用

function drawCurve(points, tension) {
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);


var t = (tension != null) ? tension : 1;
for (var i = 0; i < points.length - 1; i++) {
var p0 = (i > 0) ? points[i - 1] : points[0];
var p1 = points[i];
var p2 = points[i + 1];
var p3 = (i != points.length - 2) ? points[i + 2] : p2;


var cp1x = p1.x + (p2.x - p0.x) / 6 * t;
var cp1y = p1.y + (p2.y - p0.y) / 6 * t;


var cp2x = p2.x - (p3.x - p1.x) / 6 * t;
var cp2y = p2.y - (p3.y - p1.y) / 6 * t;


ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, p2.x, p2.y);
}
ctx.stroke();
}

令人难以置信的迟到,但是受到了 Homan 精彩的简单答案的启发,请允许我发布一个更一般的解决方案(一般意义上说,Homan 的解决方案在少于3个顶点的点数组上崩溃) :

function smooth(ctx, points)
{
if(points == undefined || points.length == 0)
{
return true;
}
if(points.length == 1)
{
ctx.moveTo(points[0].x, points[0].y);
ctx.lineTo(points[0].x, points[0].y);
return true;
}
if(points.length == 2)
{
ctx.moveTo(points[0].x, points[0].y);
ctx.lineTo(points[1].x, points[1].y);
return true;
}
ctx.moveTo(points[0].x, points[0].y);
for (var 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);
}
ctx.quadraticCurveTo(points[i].x, points[i].y, points[i+1].x, points[i+1].y);
}

如果你想通过 n 个点确定曲线方程,那么下面的代码会给出 n-1次多项式的系数,并将这些系数保存到 coefficients[]阵列(从常数项开始)。X 坐标不必按顺序排列。这是一个 拉格朗日插值法的例子。

var xPoints=[2,4,3,6,7,10]; //example coordinates
var yPoints=[2,5,-2,0,2,8];
var coefficients=[];
for (var m=0; m<xPoints.length; m++) coefficients[m]=0;
for (var m=0; m<xPoints.length; m++) {
var newCoefficients=[];
for (var nc=0; nc<xPoints.length; nc++) newCoefficients[nc]=0;
if (m>0) {
newCoefficients[0]=-xPoints[0]/(xPoints[m]-xPoints[0]);
newCoefficients[1]=1/(xPoints[m]-xPoints[0]);
} else {
newCoefficients[0]=-xPoints[1]/(xPoints[m]-xPoints[1]);
newCoefficients[1]=1/(xPoints[m]-xPoints[1]);
}
var startIndex=1;
if (m==0) startIndex=2;
for (var n=startIndex; n<xPoints.length; n++) {
if (m==n) continue;
for (var nc=xPoints.length-1; nc>=1; nc--) {
newCoefficients[nc]=newCoefficients[nc]*(-xPoints[n]/(xPoints[m]-xPoints[n]))+newCoefficients[nc-1]/(xPoints[m]-xPoints[n]);
}
newCoefficients[0]=newCoefficients[0]*(-xPoints[n]/(xPoints[m]-xPoints[n]));
}
for (var nc=0; nc<xPoints.length; nc++) coefficients[nc]+=yPoints[m]*newCoefficients[nc];
}

这个代码对我来说是完美的:

this.context.beginPath();
this.context.moveTo(data[0].x, data[0].y);
for (let i = 1; i < data.length; i++) {
this.context.bezierCurveTo(
data[i - 1].x + (data[i].x - data[i - 1].x) / 2,
data[i - 1].y,
data[i - 1].x + (data[i].x - data[i - 1].x) / 2,
data[i].y,
data[i].x,
data[i].y);
}

你有正确的平滑线和正确的端点 注意! (y = “画布高度”-y) ;

对原来问题的略有不同的回答;

如果有人想画一个形状:

  • 用一系列的观点来描述
  • 这条线在点处有一条小曲线
  • 这条线并不一定要通过 通过的点(也就是说稍微通过它们的“内部”)

那么希望我的下面的功能可以帮助

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


const drawPointsWithCurvedCorners = (points, ctx) => {
for (let n = 0; n <= points.length - 1; n++) {
let pointA = points[n];
let pointB = points[(n + 1) % points.length];
let pointC = points[(n + 2) % points.length];


const midPointAB = {
x: pointA.x + (pointB.x - pointA.x) / 2,
y: pointA.y + (pointB.y - pointA.y) / 2,
};
const midPointBC = {
x: pointB.x + (pointC.x - pointB.x) / 2,
y: pointB.y + (pointC.y - pointB.y) / 2,
};
ctx.moveTo(midPointAB.x, midPointAB.y);
ctx.arcTo(
pointB.x,
pointB.y,
midPointBC.x,
midPointBC.y,
radii[pointB.r]
);
ctx.lineTo(midPointBC.x, midPointBC.y);
}
};


const shapeWidth = 200;
const shapeHeight = 150;


const topInsetDepth = 35;
const topInsetSideWidth = 20;
const topInsetHorizOffset = shapeWidth * 0.25;


const radii = {
small: 15,
large: 30,
};


const points = [
{
// TOP-LEFT
x: 0,
y: 0,
r: "large",
},
{
x: topInsetHorizOffset,
y: 0,
r: "small",
},
{
x: topInsetHorizOffset + topInsetSideWidth,
y: topInsetDepth,
r: "small",
},
{
x: shapeWidth - (topInsetHorizOffset + topInsetSideWidth),
y: topInsetDepth,
r: "small",
},
{
x: shapeWidth - topInsetHorizOffset,
y: 0,
r: "small",
},
{
// TOP-RIGHT
x: shapeWidth,
y: 0,
r: "large",
},
{
// BOTTOM-RIGHT
x: shapeWidth,
y: shapeHeight,
r: "large",
},
{
// BOTTOM-LEFT
x: 0,
y: shapeHeight,
r: "large",
},
];


// ACTUAL DRAWING OF POINTS
ctx.beginPath();
drawPointsWithCurvedCorners(points, ctx);
ctx.stroke();
</script>
</body>


</html>

你好

我欣赏 user1693593的解决方案: 埃尔米特多项式似乎是控制将要绘制的内容的最佳方式,从数学的角度来看也是最令人满意的。 这个话题似乎已经结束了很长一段时间,但可能一些像我这样的后来者仍然对它感兴趣。 我一直在寻找一个免费的交互式绘图工具,它可以让我存储曲线并在其他任何地方重用它,但是在网络上没有找到这样的东西: 所以我用自己的方式创建了它,来自 user1693593提到的维基百科源代码。 在这里很难解释它是如何工作的,要知道它是否值得,最好的方法是看看 https://sites.google.com/view/divertissements/accueil/splines