基本上,我有一些对象,它们具有要避免但仍然被贝塞尔曲线包围的边界。 但是,我甚至不知道从哪里开始编写算法以移动控制点以避免碰撞。
问题在下面的图像中,即使您不熟悉音乐符号,问题也应该相当明显。 曲线的点是红点。
此外,我可以访问每个音符的边界框,其中包括茎。
因此,必须在边界框和曲线之间检测到碰撞(在这里提供一些方向会很好,但我一直在浏览并看到有大量关于此的信息)。 但是,在检测到碰撞后会发生什么? 必须发生什么才能计算控制点位置以使其看起来更像:
最初这个问题是一个广泛的问题 - 甚至对于SO来说也可能过于广泛,因为有许多不同的情况需要考虑以制定“一种适合所有情况的解决方案”。这本身就是一个完整的项目。因此,我将提供一个基础解决方案,您可以在此基础上构建 - 它不是一个完整的解决方案(但接近于一个解决方案..)。我在末尾添加了一些建议。
此解决方案的基本步骤如下:
将笔记分组为左侧部分和右侧部分。
然后,控制点基于第一个(结束)点的最大角度和该组其他任何笔记的距离,以及最后一个结束点到第二组中的任何点的距离。
然后将两个组的结果角度加倍(最大值为90°),并用作计算控制点的基础(基本上是点旋转)。可以使用张力值进一步修剪距离。
角度、加倍、距离、张力和边距偏移量将允许微调以获得最佳的整体结果。可能会有一些特殊情况需要额外的条件检查,但这超出了此处覆盖的范围(它不会是一个完整的键准备解决方案,但提供了一个良好的基础,可进一步工作)。
过程中的几个快照:
示例中的主要代码分为两个部分,两个循环解析每半部分以查找最大角度和距离。这可以合并为单个循环,并引入第二个迭代器从右到中间进行迭代,除了从左到中间的迭代器外,但为了简单起见并更好地理解发生了什么,我将其拆分为两个循环(并在第二半部分引入了一个错误 - 请注意,我将其留作练习)。
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.
(这个 bug 是使用了最长距离,即使一个角度更大的点有较短的距离 - 我现在将其保留作为示例。可以通过反转迭代来修复它。)
我发现有效的关系是地板和点之间的角度差乘以二:
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();
为了使曲线更美观,可以通过以下简单步骤添加锥形效果:
在添加第一个贝塞尔曲线后,不要描边路径,而是将控制点稍微偏移一定角度。然后继续路径,从右到左添加另一个贝塞尔曲线,最后填充它(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
(true),它就会继续,或者直到最初设置的最大通过数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 = [];
}
}