给定起点和终点控制点、曲线长度和张力位置确定二次贝塞尔曲线控制点

问题描述 投票:0回答:2

我正在编写一个程序,供用户使用二次贝塞尔曲线快速近似弯曲或直的可弯曲管道的形状。他们将输入起点和终点并提供管道的长度。打个比方,有人在一根已知长度的绳索的起点和终点放桩。

为了确定曲线的形状,他们将在显示的管道/绳索形状的边缘单击一个点,在该点从起点/终点“弯曲最远”。这在下图中用红叉表示。我显示的红叉不在管道上,表示用户允许/可能在此选择中出错。他们似乎不太可能选择恰好在曲线上的点。

一旦用户选择了这一点,结果将显示为 SVG 二次贝塞尔曲线:https://www.w3.org/TR/SVG/paths.html#PathDataQuadraticBezierCommands

鉴于起点和终点是二次方的两个控制点。假设起点/终点和长度准确,我想计算二次贝塞尔曲线的第三个控制点。换句话说,红点应该在第三个控制点的方向上“拉紧”曲线。

我想我可以最初计算第三个控制点,假设用户的点击(红叉)实际上是在二次曲线上使用这个算法

给出定义曲线的三个控制点,然后我可以使用这个方程计算曲线的长度。

如果曲线的长度在误差容限范围内,我就完成了。如果不是,我可以迭代移动第三个控制点,重新计算长度,重复直到控制点将长度置于所需的公差范围内。

如何在保持曲线形状的同时调整第三个控制点的 x,y 位置?

或者整体上有更好的解决方案吗?

algorithm svg bezier
2个回答
1
投票

对于二次曲线,你可以通过找到起点和终点的切线的交点来找到那个红叉。现在,你没有那些,但你有近似切线,因为靠近起点和终点,点和星形/趋势之间的线大约等于它的切线

但是,如果我们对您显示的形状这样做,我们可以看到这非常不是二次贝塞尔曲线:

bgimg.onload = () => {
  cvs.width = 400;
  cvs.height = 350;
  const ctx = cvs.getContext(`2d`);
  ctx.drawImage(bgimg, 0, 0, 400, 350);
  ctx.lineWidth = 2;
  ctx.strokeStyle = `red`;
  
  // quadratic
  ctx.beginPath();
  ctx.moveTo(58,275);
  ctx.quadraticCurveTo(104, 92, 320, 50);
  ctx.moveTo(104, 92);
  ctx.arc(104, 92, 3, 0, 2*Math.PI);
  ctx.stroke(); 
};
.hidden {
  display: none;
}
canvas {
  border:1px solid black;
}
<canvas id="cvs"></canvas>
<img id="bgimg" class="hidden" src="https://i.stack.imgur.com/ZcqMD.png">

当然,您可以使用二次曲线的曲线与基线比率来找到不同的控制点,以便曲线的中间与您的管道重叠:

bgimg.onload = () => {
  cvs.width = 400;
  cvs.height = 350;
  const ctx = cvs.getContext(`2d`);
  ctx.drawImage(bgimg, 0, 0, 400, 350);
  ctx.lineWidth = 2;
  ctx.strokeStyle = `red`;

  // point "c" at the baseline midpoint
  ctx.beginPath();
  ctx.arc(177, 172, 3, 0, 2*Math.PI);  
  ctx.moveTo(58,275);
  ctx.lineTo(320, 50);
  ctx.moveTo(177,172);
  ctx.lineTo(20,0);
  // point "b" is on our curve
  ctx.moveTo(128,118);
  ctx.arc(128, 118, 3, 0, 2*Math.PI);  
  // point "a" is at b - (c-b)
  const ax = 128 - (177 - 128);
  const ay = 118 - (172 - 118);
  ctx.moveTo(ax, ay);
  ctx.arc(ax, ay, 3, 0, 2*Math.PI);
  ctx.moveTo(58,275);
  ctx.quadraticCurveTo(ax, ay, 320,50)
  ctx.stroke(); 
};
.hidden {
  display: none;
}
canvas {
  border:1px solid black;
}
<canvas id="cvs"></canvas>
<img id="bgimg" class="hidden" src="https://i.stack.imgur.com/ZcqMD.png">

但是它会过度弯曲,所以你可能不得不在这里使用三次贝塞尔曲线,因为你正在使用的形状不是二次的。


0
投票

我想对用户设置十字的位置提出不同的解释。正如 Pomax 指出的那样,您绘制的曲线绝不是二次贝塞尔曲线。但是,也许凭直觉,十字位于端点切线相交处附近的一个点。我认为这是一个容易掌握的概念:画十字以指示管端切线应指向的位置。

使用三次贝塞尔曲线相对容易实现,因为两个控制点必须位于连接端点和目标点的相应线上。将它们移近或远离该点,直到长度接近给定值。

很明显,十字的位置是有一个可以放置的限制的,否则控制点就得超出目标点。但与您的方法不同,用户可以通过选择一个为管道长度留下更多“空间”的点来纠正该错误——这可以直观地完成。

const pipeLength = 160;

const bezier = document.querySelector('#bezier');
const start = [0, 0];
const end = [100, 100];
const target = [90, 10];

let curveLength = Math.hypot(target[0] - start[0], target[1] - start[1]) +
                  Math.hypot(target[0] - end[0], target[1] - end[1]);
let part = 1, low = 0, high = 1, i= 0;

while ((i++ < 20) && Math.abs(curveLength - pipeLength) > 1) {
  const control1 = [
    target[0] * part + start[0] * (1 - part),
    target[1] * part + start[1] * (1 - part)
  ];
  const control2 = [
    target[0] * part + end[0] * (1 - part),
    target[1] * part + end[1] * (1 - part)
  ];
  
  bezier.setAttribute('d', 'M ' + [...start, 'C', ...control1, ...control2, ...end].join(' '));
  
  curveLength = bezier.getTotalLength();
  
  if (curveLength > pipeLength) {
    high = part;
  } else {
    low = part;
  }
  part = (high + low) / 2;
}
<svg viewBox="-10 -10 120 120" height = "100vh">
  <path d="M80,10h20M90,0v20" stroke="red" />
  <path d="M0,0 90,10 100,100" fill="none" stroke="red" stroke-dasharray="3 3" />
  <path id="bezier" d="M0,0 C 90,10 90,10 100,100" fill="none" stroke="blue" />
</svg>

© www.soinside.com 2019 - 2024. All rights reserved.