【问题标题】:Using a Bezier Curve to draw a spiral使用贝塞尔曲线绘制螺旋
【发布时间】:2012-07-07 20:04:38
【问题描述】:

这是针对 iPad 应用程序的,但它本质上是一道数学题。

我需要绘制一个不同(单调递增)线宽的圆弧。在曲线的开始处,它会有一个起始厚度(比如 2 点),然后厚度会平滑增加,直到弧线的末端达到最大厚度(比如 12 点)。

我认为最好的方法是创建一个 UIBezierPath 并填充形状。我的第一次尝试是使用两个圆弧(带有偏移中心),在 90° 以内效果很好,但圆弧通常在 90° 到 180° 之间,所以这种方法不会切割它。

我目前的方法是使用贝塞尔四边形或三次曲线制作一个轻微的螺旋形(一个从圆弧略微增大,一个略微缩小)。问题是我应该把控制点放在哪里,这样圆弧的偏差(也就是形状“厚度”)就是我想要的值。

约束:

  • 形状必须能够以任意角度开始和结束(彼此相差 180° 以内)
  • 形状的“厚度”(与圆的偏差)必须以给定值开始和结束
  • “厚度”必须单调增加(不能变大再变小)
  • 它必须看起来光滑,不能有任何急弯

我也愿意接受其他解决方案。

【问题讨论】:

    标签: ios math bezier uibezierpath spiral


    【解决方案1】:

    我的方法只是构建 2 个圆弧并填充中间的区域。棘手的一点是找出这些弧的中心和半径。如果厚度不太大,看起来还不错。 (剪切和粘贴并自行决定是否满足您的需求。)可以通过使用剪切路径来改进。

    - (void)drawRect:(CGRect)rect
    {
      CGContextRef context = UIGraphicsGetCurrentContext();
    
      CGMutablePathRef path = CGPathCreateMutable();
    
      // As appropriate for iOS, the code below assumes a coordinate system with
      // the x-axis pointing to the right and the y-axis pointing down (flipped from the standard Cartesian convention).
      // Therefore, 0 degrees = East, 90 degrees = South, 180 degrees = West,
      // -90 degrees = 270 degrees = North (once again, flipped from the standard Cartesian convention).
      CGFloat startingAngle = 90.0;  // South
      CGFloat endingAngle = -45.0;   // North-East
      BOOL weGoFromTheStartingAngleToTheEndingAngleInACounterClockwiseDirection = YES;  // change this to NO if necessary
    
      CGFloat startingThickness = 2.0;
      CGFloat endingThickness = 12.0;
    
      CGPoint center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds));
      CGFloat meanRadius = 0.9 * fminf(self.bounds.size.width / 2.0, self.bounds.size.height / 2.0);
    
      // the parameters above should be supplied by the user
      // the parameters below are derived from the parameters supplied above
    
      CGFloat deltaAngle = fabsf(endingAngle - startingAngle);
    
      // projectedEndingThickness is the ending thickness we would have if the two arcs
      // subtended an angle of 180 degrees at their respective centers instead of deltaAngle
      CGFloat projectedEndingThickness = startingThickness + (endingThickness - startingThickness) * (180.0 / deltaAngle);
    
      CGFloat centerOffset = (projectedEndingThickness - startingThickness) / 4.0;
      CGPoint centerForInnerArc = CGPointMake(center.x + centerOffset * cos(startingAngle * M_PI / 180.0),
                                              center.y + centerOffset * sin(startingAngle * M_PI / 180.0));
      CGPoint centerForOuterArc = CGPointMake(center.x - centerOffset * cos(startingAngle * M_PI / 180.0),
                                              center.y - centerOffset * sin(startingAngle * M_PI / 180.0));
    
      CGFloat radiusForInnerArc = meanRadius - (startingThickness + projectedEndingThickness) / 4.0;
      CGFloat radiusForOuterArc = meanRadius + (startingThickness + projectedEndingThickness) / 4.0;
    
      CGPathAddArc(path,
                   NULL,
                   centerForInnerArc.x,
                   centerForInnerArc.y,
                   radiusForInnerArc,
                   endingAngle * (M_PI / 180.0),
                   startingAngle * (M_PI / 180.0),
                   !weGoFromTheStartingAngleToTheEndingAngleInACounterClockwiseDirection
                   );
    
      CGPathAddArc(path,
                   NULL,
                   centerForOuterArc.x,
                   centerForOuterArc.y,
                   radiusForOuterArc,
                   startingAngle * (M_PI / 180.0),
                   endingAngle * (M_PI / 180.0),
                   weGoFromTheStartingAngleToTheEndingAngleInACounterClockwiseDirection
                   );
    
      CGContextAddPath(context, path);
    
      CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
      CGContextFillPath(context);
    
      CGPathRelease(path);  
    }
    

    【讨论】:

    • 这看起来真的很棒!你为我节省了很多工作。这比我正在研究的方法(求解螺旋的贝塞尔多项式方程)简单得多。我让它在 90° 的倍数下工作,但任意角度会很痛苦。这好多了……
    • @JonHull 很高兴你喜欢它。我刚刚意识到我已经隐含地假设endingThickness >= startingThickness 但您应该能够轻松地安排您的输入参数以满足此条件。如果不是,可能会出现projectedEndingThickness 为负数的情况,然后我就无法确定代数了。它可能仍然有效,但我还没有测试过。
    • 哦,干得好兄弟,,,,你是一个真正的救生员?,,,谢谢??
    【解决方案2】:

    一种解决方案是手动生成折线。这很简单,但它的缺点是如果控件以高分辨率显示,则必须按比例增加生成的点数。我对 iOS 的了解还不够,无法为您提供 iOS/ObjC 示例代码,但这里有一些类似于 python 的伪代码:

    # lower: the starting angle
    # upper: the ending angle
    # radius: the radius of the circle
    
    # we'll fill these with polar coordinates and transform later
    innerSidePoints = []
    outerSidePoints = []
    
    widthStep = maxWidth / (upper - lower)
    width = 0
    
    # could use a finer step if needed
    for angle in range(lower, upper):
        innerSidePoints.append(angle, radius - (width / 2))
        outerSidePoints.append(angle, radius + (width / 2))
        width += widthStep
    
    # now we have to flip one of the arrays and join them to make
    # a continuous path.  We could have built one of the arrays backwards
    # from the beginning to avoid this.
    
    outerSidePoints.reverse()
    allPoints = innerSidePoints + outerSidePoints # array concatenation
    
    xyPoints = polarToRectangular(allPoints) # if needed
    

    【讨论】:

    • 感谢伪代码。如果我找不到使用贝塞尔曲线或圆弧的解决方案,这将是我的备份。
    猜你喜欢
    • 2017-05-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-26
    • 1970-01-01
    • 2014-09-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多