[英]How to calculate bezier curve control points that avoid objects?
具体来说,我正在使用javascript在canvas中工作。
基本上,我的对象有我想要避免的边界,但仍然围绕着贝塞尔曲线。 但是,我甚至不确定从哪里开始编写一个可以移动控制点以避免碰撞的算法。
问题出现在下图中,即使您不熟悉音乐符号,问题仍然应该是相当清楚的。 曲线的点是红点
此外,我可以访问每个音符的边界框,其中包括词干。
所以很自然地,必须在边界框和曲线之间检测到碰撞(这里的某个方向会很好,但我一直在浏览并看到有相当数量的信息)。 但是在检测到碰撞后会发生什么? 计算控制点位置以制作看起来更像的东西会发生什么:
最初的问题是一个广泛的问题 - 甚至可能更广泛的问题,因为有许多不同的情景需要考虑制定“一个适合所有人的解决方案”。 这是一个完整的项目。 因此,我将为您提供一个可以构建的解决方案的基础 - 它不是一个完整的解决方案(但接近一个......)。 我在最后添加了一些关于添加的建议。
此解决方案的基本步骤是:
将笔记分为两组,左侧和右侧。
然后,控制点基于从第一个(结束)点到该组中任何其他音符的最大角度,以及到第二个组中任何点的最后一个结束点。
然后将来自两组的所得角度加倍(最大90°)并用作计算控制点(基本上是点旋转)的基础。 可以使用张力值进一步修剪距离。
角度,倍增,距离,张力和填充偏移将允许微调以获得最佳的总体结果。 可能存在需要额外条件检查的特殊情况,但这不在此范围内(它不是完整的密钥就绪解决方案,但为进一步工作提供了良好的基础)。
来自该流程的几个快照:
示例中的主要代码分为两部分,两部分解析每一半以找到最大角度和距离。 这可以合并为一个循环,并且除了从左到中之外还有从右到中的第二个迭代器,但为了简单起见并更好地理解发生了什么,我将它们分成两个循环(并引入了一个bug在下半场 - 请注意。我会把它留作练习):
var dist1 = 0, // final distance and angles for the control points
dist2 = 0,
a1 = 0,
a2 = 0;
// get min angle from the half first points
for(i = 2; i < len * 0.5 - 2; i += 2) {
var dx = notes[i ] - notes[0], // diff between end point and
dy = notes[i+1] - notes[1], // current point.
dist = Math.sqrt(dx*dx + dy*dy), // get distance
a = Math.atan2(dy, dx); // get angle
if (a < a1) { // if less (neg) then update finals
a1 = a;
dist1 = dist;
}
}
if (a1 < -0.5 * Math.PI) a1 = -0.5 * Math.PI; // limit to 90 deg.
与下半部分相同,但在这里我们翻转角度,以便通过比较当前点与终点而不是与当前点相比的终点来更容易处理。 循环完成后,我们将其翻转180°:
// get min angle from the half last points
for(i = len * 0.5; i < len - 2; i += 2) {
var dx = notes[len-2] - notes[i],
dy = notes[len-1] - notes[i+1],
dist = Math.sqrt(dx*dx + dy*dy),
a = Math.atan2(dy, dx);
if (a > a2) {
a2 = a;
if (dist2 < dist) dist2 = dist; //bug here*
}
}
a2 -= Math.PI; // flip 180 deg.
if (a2 > -0.5 * Math.PI) a2 = -0.5 * Math.PI; // limit to 90 deg.
(错误是即使较短的距离点具有更大的角度,也会使用最长的距离 - 我现在将其视为一个例子。它可以通过反转迭代来修复。)。
我发现的关系很好,是地板和点之间的角度差两倍:
var da1 = Math.abs(a1); // get angle diff
var da2 = a2 < 0 ? Math.PI + a2 : Math.abs(a2);
a1 -= da1*2; // double the diff
a2 += da2*2;
现在我们可以简单地计算控制点并使用张力值来微调结果:
var t = 0.8, // tension
cp1x = notes[0] + dist1 * t * Math.cos(a1),
cp1y = notes[1] + dist1 * t * Math.sin(a1),
cp2x = notes[len-2] + dist2 * t * Math.cos(a2),
cp2y = notes[len-1] + dist2 * t * Math.sin(a2);
瞧:
ctx.moveTo(notes[0], notes[1]);
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, notes[len-2], notes[len-1]);
ctx.stroke();
要创建曲线,可以通过以下操作简单地添加逐渐变细的渐变:
在添加第一条贝塞尔曲线之后,不是抚摸路径,而是以微小的角度偏移调整控制点。 然后通过添加另一条从右到左的Bezier曲线继续路径,最后填充它( fill()
将关闭隐含的路径):
// first path from left to right
ctx.beginPath();
ctx.moveTo(notes[0], notes[1]); // start point
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, notes[len-2], notes[len-1]);
// taper going from right to left
var taper = 0.15; // angle offset
cp1x = notes[0] + dist1*t*Math.cos(a1-taper);
cp1y = notes[1] + dist1*t*Math.sin(a1-taper);
cp2x = notes[len-2] + dist2*t*Math.cos(a2+taper);
cp2y = notes[len-1] + dist2*t*Math.sin(a2+taper);
// note the order of the control points
ctx.bezierCurveTo(cp2x, cp2y, cp1x, cp1y, notes[0], notes[1]);
ctx.fill(); // close and fill
建议的改进:
希望这可以帮助!
如果您打开使用非Bezier方法,则以下可以在音符杆上方给出近似曲线。
该解决方案包括4个步骤:
这是一个原型解决方案,所以我没有针对所有可能的组合进行测试。 但它应该给你一个良好的起点和基础继续。
第一步很简单,收集代表音符杆顶部的点 - 对于演示,我使用以下点集合,稍微代表你在帖子中的图像。 它们按x,y顺序排列:
var notes = [60,40, 100,35, 140,30, 180,25, 220,45, 260,25, 300,25, 340,45];
这将表示如下:
然后我创建了一个简单的多遍算法,可以滤除相同斜率上的倾角和点。 算法中的步骤如下:
anotherPass
(真)它将继续,或直到最初设置的最大通过次数 skip
标志,该点就会复制到另一个数组 skip
标志,因此不会复制下一个点(当前中间点) skip
标志被设置。 skip
标志,它还将设置anotherPass
标志。 核心功能如下:
while(anotherPass && max) {
skip = anotherPass = false;
for(i = 0; i < notes.length - 2; i += 2) {
if (!skip) curve.push(notes[i], notes[i+1]);
skip = false;
// if this to next points goes downward
// AND the next and the following up we have a dip
if (notes[i+3] >= notes[i+1] && notes[i+5] <= notes[i+3]) {
skip = anotherPass = true;
}
// if slope from this to next point =
// slope from next and following skip
else if (notes[i+2] - notes[i] === notes[i+4] - notes[i+2] &&
notes[i+3] - notes[i+1] === notes[i+5] - notes[i+3]) {
skip = anotherPass = true;
}
}
curve.push(notes[notes.length-2], notes[notes.length-1]);
max--;
if (anotherPass && max) {
notes = curve;
curve = [];
}
}
第一次传递的结果是在偏移y轴上的所有点之后 - 注意忽略倾斜音符:
在完成所有必要的传递之后,最终的点数组将表示为:
剩下的唯一步骤是使曲线平滑。 为此,我使用了我自己的基数样条实现(在MIT下许可,可以在这里找到 ),它采用x,y点和平滑的数组,它根据张力值添加插值点。
它不会产生完美的曲线,但结果将是:
有一些方法可以改善我没有解决过的视觉效果,但如果您觉得有必要,我会留给您做。 其中可能是:
这个算法是为这个答案创建的,所以它显然没有经过适当的测试。 可能会有特殊情况和组合将其抛弃,但我认为这是一个良好的开端。
已知的弱点:
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.