我正在编写一个程序,供用户使用二次贝塞尔曲线快速近似弯曲或直的可弯曲管道的形状。他们将输入起点和终点并提供管道的长度。打个比方,有人在一根已知长度的绳索的起点和终点放桩。
为了确定曲线的形状,他们将在显示的管道/绳索形状的边缘单击一个点,在该点从起点/终点“弯曲最远”。这在下图中用红叉表示。我显示的红叉不在管道上,表示用户允许/可能在此选择中出错。他们似乎不太可能选择恰好在曲线上的点。
一旦用户选择了这一点,结果将显示为 SVG 二次贝塞尔曲线:https://www.w3.org/TR/SVG/paths.html#PathDataQuadraticBezierCommands
鉴于起点和终点是二次方的两个控制点。假设起点/终点和长度准确,我想计算二次贝塞尔曲线的第三个控制点。换句话说,红点应该在第三个控制点的方向上“拉紧”曲线。
我想我可以最初计算第三个控制点,假设用户的点击(红叉)实际上是在二次曲线上使用这个算法。
给出定义曲线的三个控制点,然后我可以使用这个方程计算曲线的长度。
如果曲线的长度在误差容限范围内,我就完成了。如果不是,我可以迭代移动第三个控制点,重新计算长度,重复直到控制点将长度置于所需的公差范围内。
如何在保持曲线形状的同时调整第三个控制点的 x,y 位置?
或者整体上有更好的解决方案吗?
对于二次曲线,你可以通过找到起点和终点的切线的交点来找到那个红叉。现在,你没有那些,但你有近似切线,因为靠近起点和终点,点和星形/趋势之间的线大约等于它的切线
但是,如果我们对您显示的形状这样做,我们可以看到这非常不是二次贝塞尔曲线:
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">
但是它会过度弯曲,所以你可能不得不在这里使用三次贝塞尔曲线,因为你正在使用的形状不是二次的。
我想对用户设置十字的位置提出不同的解释。正如 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>