就像很多三角函数一样,这比看起来容易。
首先为一个圆实现这个,这要简单得多,因为距离和角度始终保持不变:
-
根据你想要气球上的尖刺将圆的圆周分成任意多的点。
-
选择其中任何一点作为起点。
-
计算它相对于气球中心的位置,然后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:。在 Quartz 中,使用CGAffineTransformMakeScale。
然后,将此转换应用于您的路径。对于 NSBezierPath,发送 transformUsingAffineTransform: 并传递 NSAffineTransform 对象。对于 UIBezierPath,发送 applyTransform: 并传递 CGAffineTransform 结构。对于 CGPath,请使用 CGPathCreateCopyByTransformingPath。在这里将路径直接绘制到上下文中是行不通的,因为您无法在不扭曲笔画的情况下转换路径。
由您决定是否应该在转换路径之前或之后添加尾部。