具体来说,我正在使用 javascript 在画布上工作。
基本上,我的对象有我想避免的边界,但仍然用贝塞尔曲线环绕。但是,我什至不确定从哪里开始编写一个可以移动控制点以避免碰撞的算法。
问题在下图中,即使你不熟悉乐谱,问题应该还是很清楚的。曲线的点是红点
此外,我可以访问每个音符的边界框,其中包括词干。
所以自然地,必须在边界框和曲线之间检测到碰撞(这里有一些方向会很好,但我一直在浏览,发现有很多关于这方面的信息)。但是在检测到碰撞后会发生什么?计算控制点位置以使某些东西看起来更像:
具体来说,我正在使用 javascript 在画布上工作。
基本上,我的对象有我想避免的边界,但仍然用贝塞尔曲线环绕。但是,我什至不确定从哪里开始编写一个可以移动控制点以避免碰撞的算法。
问题在下图中,即使你不熟悉乐谱,问题应该还是很清楚的。曲线的点是红点
此外,我可以访问每个音符的边界框,其中包括词干。
所以自然地,必须在边界框和曲线之间检测到碰撞(这里有一些方向会很好,但我一直在浏览,发现有很多关于这方面的信息)。但是在检测到碰撞后会发生什么?计算控制点位置以使某些东西看起来更像:
最初,这个问题是一个广泛的问题 - 甚至对于 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.
(错误是即使距离较短的点具有更大的角度,也会使用最长的距离 - 我现在就让它成为一个例子。它可以通过反转迭代来修复。)。
我发现效果很好的关系是地板和点之间的角度差乘以 2:
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
建议改进:
希望这可以帮助!
如果您愿意使用非贝塞尔方法,那么以下可以给出音符上方的近似曲线。
该解决方案包括 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 轴上的所有点之后 - 请注意,浸渍音符被忽略:
在运行所有必要的传递之后,最终点数组将表示为:
剩下的唯一步骤是平滑曲线。为此,我使用了自己的基数样条实现(在麻省理工学院获得许可,可以在此处找到),它采用带有 x、y 点的数组并根据张力值添加插值点对其进行平滑处理。
它不会产生完美的曲线,但结果是:
有一些方法可以改善我没有提到的视觉效果,但如果你觉得有必要,我会留给你去做。其中可能包括:
该算法是为这个答案创建的,所以它显然没有经过适当的测试。可能会有特殊情况和组合将其扔掉,但我认为这是一个好的开始。
已知弱点: