就像很多三角学一样,这比看起来要容易。
首先为一个圆实现这个,这要简单得多,因为距离和角度始终保持不变:
将圆的圆周分成任意多的点,就像你想要气球上的尖峰一样。
选择其中任何一点作为起点。
计算它相对于气球中心的位置,然后计算moveto
它。
从每个尖峰点,计算下一个尖峰点,然后找到两者之间的正中间的点。
使用该中点作为arcto
. 这看起来像CGContextAddArcToPoint(context, midPoint.x, midPoint.y, nextSpikePoint.x, nextSpikePoint.y, radius)
.
(radius
这里是两个尖峰之间的山谷深度。您可以对其进行硬编码,使其用户可编辑,使其取决于气球大小,使用从起始尖峰点到中点的距离,或将最后一个相乘一个接一个。)
弧线将曲线置于每对尖峰之间。我相信弧线总是逆时针的,所以你可以改变你绕气球的方式——以逆时针顺序或顺时针顺序取点——来改变你是画一个呼喊气球还是一个思想气球。
清洗,冲洗,重复,直到您回到您在步骤 2 中开始的峰值点。
关闭路径,将尾部绘制为单独的子路径,对它们进行描边,然后将它们都填充。(描边会留下重叠的线条;填充覆盖这些线条。)
第 1 步是一种非步骤:你真正要做的就是决定你想要多少分。您可以硬编码一些点开始,然后使其可变,可能取决于气球的周长。
要从圆周推导出它,您需要记住尖峰点之间的一定距离(特别是如果您想要一定的半径,例如,使用点之间距离的一半作为半径)。您将圆周除以该距离,然后以一种或另一种方式绕圆(或选择您的起点,以便其余部分被气球的尾巴覆盖)。
计算圆的周长
这是众所周知的:
circ = radius² × π
(或者,更著名和更简洁地说,“πr²”。)
两点之间的距离
问问毕达哥拉斯吧!
您在这里所做的是将两个点视为直角三角形的角点,并计算该三角形的斜边。我不会重复这个定理,因为在 C 数学库中有一个方便的函数:
#include <tgmath.h>
CGSize size = {
fabs(nextPoint.x - currentPoint.x),
fabs(nextPoint.y - currentPoint.y)
};
distance = hypot(size.width, size.height);
(fabs
是绝对值函数。如果它的参数是负数,它返回它的否定 - 相同的数字,但正数。如果它的参数是正数,它返回它不变。)
寻找距离中间的点
要找到中点,您要做的就是找到从当前点到下一个点的距离(如上),然后找到相同的角度。
想象一个围绕该中点的圆圈(一个小得多的圆圈)。在它的圆周上触摸它的是它两侧的两个尖峰点:当前点和下一个点。这两个点的距离(半径)相同,角度不同(实际上,完全相反的角度,因为我们的中点在一条直线上)。
所以,现在将那个假想的圆圈移动到当前点上。现在,它以当前点为中心,中点在其圆周上——半径相同,方向相反(角度)。
查找中点相对于当前点的位置的方法与查找每个尖峰点相对于气球中心的位置的方法相同。
计算每个点在圆上的位置
从气球的中心开始。
每个点都与气球中心成一定角度和一定距离。给定一个圆形气球,它们都处于相同的距离,即圆的半径。
首先,角度。每两个尖峰点之间的角度为 2π(一个完整圆中的弧度数)除以尖峰数。计算一次该角度,然后在您通过循环时将其乘以每个尖峰点的索引。i
从 0 到点数;i+1
是你的下一点。
接下来是半径。用三角法求半径的方法是建立直角三角形,其斜边就是那个距离。测量斜边,你就知道了距离。
不过小问题:上次我们计算斜边时,我们有一个宽度和高度来给它。这一次,我们没有。
这就是余弦和正弦的用途!
余弦和正弦函数取一个角度(以弧度为单位),并为您提供该假想直角三角形的宽度和高度(分别)。然后,您只需将它们传递给hypot
.
一个皱纹:他们不采取半径。他们根据单位圆(半径 1)返回结果。这很容易克服:只需将结果乘以所需的半径即可。
所以我们的点沿圆周查找代码如下所示:
static CGPoint pointFromPolar(CGFloat theta, CGFloat radius) {
return (CGPoint){
cos(theta) * radius,
sin(theta) * radius
};
}
您将使用相同的方法找到两个尖峰点之间的中点:通过它们之间的假想线的角度,以及该线距离的一半。将结果数字添加到当前点以获得中点。
寻找中点:替代方法
找到两个尖峰点之间中点的另一种方法是将点之间的角度的一半简单地添加到当前点的角度(从气球中心),然后找到该角度在相同半径处的结束位置。
该点实际上将沿着气球圆圈的圆周(而不是在一条直线上)。它是一个或另一个并不重要。使用任何可行且最容易实现的方法。
现在,如何为椭圆做所有这些
所以这是一个普通的圈子。棘手的部分是将其推广到任何椭圆。
(因为所有正方形都是长方形,所以所有圆形都是椭圆形的——对于倒数也是如此:因为并非所有长方形都是正方形,所以并非所有椭圆都是圆形。)
现在,我可以做所有的数学运算并编写一个测试应用程序并计算出如何为椭圆做上述所有事情,但我建议您改为让仿射变换为您完成工作。
注意:不要在您的上下文中应用转换。将其应用于路径。
您需要做的就是创建一个非均匀缩放的仿射变换,即宽度大于高度,反之亦然。在 Cocoa 中,您可以创建一个 NSAffineTransform 并发送它scaleXBy:yBy:
。在石英中,使用CGAffineTransformMakeScale
.
然后,将此转换应用于您的路径。对于 NSBezierPath,发送它transformUsingAffineTransform:
并传递 NSAffineTransform 对象。对于 UIBezierPath,发送它applyTransform:
并传递CGAffineTransform
结构。对于 CGPath,使用CGPathCreateCopyByTransformingPath
. 将路径直接绘制到上下文中在这里不起作用,因为您无法在不扭曲笔画的情况下转换路径。
我将由您决定是否应该在转换路径之前或之后添加尾部。