【问题标题】:Flattening a CGPath扁平化 CGPath
【发布时间】:2013-09-17 19:12:04
【问题描述】:

简单地说,我正在寻找可以在 iOS 上使用的与 NSBezierPath 的 -bezierPathByFlatteningPath 等效的东西。这是直接处理 CGPath 的函数还是 UIBezierPath 上的方法对我来说并不重要,因为这两者可以很容易地来回转换。 CGPath ReferenceUIBezierPath Class Reference 都不表示存在任何此类函数或方法。

另外:我知道 CGPath 的 CGPathApply 函数,但我缺乏时间和技能集来通过迭代 CGPathApplierFunction 中的路径元素来实现我自己的展平算法。我正在寻找一个现有的解决方案——应用程序函数、UIBezierPath 上的类别等。肯定存在。

【问题讨论】:

  • 使用UIBezierPath,设置path.flatness = 1 是否能实现您的目标?
  • 很遗憾,不,因为作为绘图属性,flatness 只影响曲线的渲染方式,而不影响曲线的存储方式。
  • 嗯...在这种情况下,您最好的选择可能是使用CGPathApply 遍历所有点并使用适当的函数构建新路径以获得平坦效果。
  • 这是正确的想法,但这正是我希望避免做的事情。 This person 几年前在 Apple 邮件列表中提出了几乎完全相同的问题,但遗憾的是在 this 之后不再收到回复。老实说,我可能会放弃导致我首先需要执行此操作的计划,但为了所有将“flatten cgpath”作为建议的 Google 搜索的人,如果有人有答案,那仍然很棒到这个。

标签: ios uikit core-graphics


【解决方案1】:

Erica Sadun 提供了一组有用的函数来处理 UIBezierPath 和 CGPathRef。

此代码用于this book

她没有提供 CGPathRef 展平的实现,但可以使用以下函数轻松完成: https://github.com/erica/iOS-Drawing/blob/master/C05/Quartz%20Book%20Pack/Bezier/BezierFunctions.m

特别是,这些函数将有助于离散非线性贝塞尔线段:

float CubicBezier(float t, float start, float c1, float c2, float end)
float QuadBezier(float t, float start, float c1, float end)
CGPoint CubicBezierPoint(CGFloat t, CGPoint start, CGPoint c1, CGPoint c2, CGPoint end);
CGPoint QuadBezierPoint(CGFloat t, CGPoint start, CGPoint c1, CGPoint end);

所以基本上,初始化一个空的 CGMutablePathRef,并且对于原始路径中的每个 CGPath 元素,如果它是线性的则复制它,或者根据贝塞尔线段的度数对其进行离散化。

您可能还想应用Ramer–Douglas–Peucker algorithm 来删除不必要的点。

您也可以直接使用:- (NSArray *) interpolatedPathPoints,它返回一个点的 NSArray,可用于构建路径的近似值。该算法是幼稚的,因此您必须简化这种情况下的结果,例如,三次贝塞尔路径将是线性的(如果控制点对齐);和以前一样,Ramer-Douglas-Peucker 算法可以完成这项工作。

这是实际离散化的样子。代码不是自包含的,您必须使用所有依赖项。

- (NSArray *) interpolatedPathPoints
{
    NSMutableArray *points = [NSMutableArray array];
    BezierElement *current = nil;
    int overkill = 3;
    for (BezierElement *element in self.elements)
    {
        switch (element.elementType)
        {
            case kCGPathElementMoveToPoint:
            case kCGPathElementAddLineToPoint:
                [points addObject:[NSValue valueWithCGPoint:element.point]];
                current = element;
                break;
            case kCGPathElementCloseSubpath:
                current = nil;
                break;
            case kCGPathElementAddCurveToPoint:
            {
                for (int i = 1; i < NUMBER_OF_BEZIER_SAMPLES * overkill; i++)
                {
                    CGFloat percent = (CGFloat) i / (CGFloat) (NUMBER_OF_BEZIER_SAMPLES * overkill);
                    CGPoint p = CubicBezierPoint(percent, current.point, element.controlPoint1, element.controlPoint2, element.point);
                    [points addObject:[NSValue valueWithCGPoint:p]];
                }
                [points addObject:[NSValue valueWithCGPoint:element.point]];
                current = element;
                break;
            }
            case kCGPathElementAddQuadCurveToPoint:
            {
                for (int i = 1; i < NUMBER_OF_BEZIER_SAMPLES * overkill; i++)
                {
                    CGFloat percent = (CGFloat) i / (CGFloat) (NUMBER_OF_BEZIER_SAMPLES * overkill);
                    CGPoint p = QuadBezierPoint(percent, current.point, element.controlPoint1, element.point);
                    [points addObject:[NSValue valueWithCGPoint:p]];
                }
                [points addObject:[NSValue valueWithCGPoint:element.point]];
                current = element;
                break;
            }
        }
    }
    return points;
}

代码属于 Erica Sadun。完整实现见这里:https://github.com/erica/iOS-Drawing

Rob Napier 还在 iOS 6 Pushing the limits 第 26 章中写过有关贝塞尔曲线的文章 花式文本布局。他并没有试图展平一个完整的 UIBezierPath,只是一个用四个点定义的三次 Bezier 路径,但实际上这完全一样(离散化 Bezier 路径) 另外,您可能会觉得这篇文章很有趣:http://robnapier.net/faster-bezier

【讨论】:

  • 这虽然仍然不够理想(理想的是an Apple-supplied API),但在回答原始问题方面做得更好,因为它解决了非线性路径段的离散化问题,所以我已经把复选标记移到这里。
【解决方案2】:

您基本上要做的是将路径转换为将所有曲线和四边形曲线替换为线。您可以使用CGPathApply 轻松完成此操作,并将其包装在UIBezierPath 上的一个类别中:

#import <CoreGraphics/CoreGraphics.h>

@interface UIBezierPath (Flatten)
- (UIBezierPath *)bezierPathByFlatteningPath;
@end

void __flattenBezierPath(void *target, const CGPathElement *element) {
    UIBezierPath *newPath = (__bridge UIBezierPath *)(target);

    switch (element->type) {
        case kCGPathElementMoveToPoint:
            [newPath moveToPoint:element->points[0]];
            break;

        case kCGPathElementAddLineToPoint:
            [newPath addLineToPoint:element->points[0]];
            break;

        case kCGPathElementCloseSubpath:
            [newPath closePath];
            break;

        case kCGPathElementAddCurveToPoint:
            [newPath addLineToPoint:element->points[2]];
            break;

        case kCGPathElementAddQuadCurveToPoint:
            [newPath addLineToPoint:element->points[1]];
            break;
    }

}

@implementation UIBezierPath (Flatten)

- (UIBezierPath *)bezierPathByFlatteningPath
{
    UIBezierPath *newPath = [UIBezierPath bezierPath];
    CGPathApply(self.CGPath, (__bridge void *)(newPath), __flattenBezierPath);
    return newPath;
}

@end

效果如下图,其中绿色为原始路径,红色为该方法生成的路径,黄色为使用setFlatness:MAXFLOAT的路径

【讨论】:

  • 这是一个很好的答案,但是您还没有提供一种方法来更改近似值的粒度,这可以使用NSBezierPathsetDefaultFlatness: 方法来完成。这也以相同的方式处理曲线的所有部分,当它应该将曲线的较陡部分分成更多部分以获得更好的近似时。很抱歉放弃这个问题——我自己已经从这个问题上继续前进了,但为了更好地衡量,我已经提交了一个雷达 (openradar.me/15217701) 并将其标记为现在的答案。 :)
  • 你应该将二次和三次贝塞尔曲线离散成一系列线性段。这不是一个展平函数。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-04-23
  • 2016-04-02
  • 2018-05-24
  • 2017-10-31
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多