Flattening a CGPath

血红的双手。 提交于 2019-12-03 16:39:46

Erica Sadun provides a set of useful functions to deal with UIBezierPath and CGPathRef.

This code is used in this book.

She does not provide an implementation of a CGPathRef flattening, but it can be easily done using functions that can be found here: https://github.com/erica/iOS-Drawing/blob/master/C05/Quartz%20Book%20Pack/Bezier/BezierFunctions.m

Particulary, these functions will help discretize the non linear Bezier segments:

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);

So basically, initialize an empty CGMutablePathRef, and for each CGPath element in the original path, either copy it if it's linear, or discretize it according to the degree of the Bezier segment.

You may also want to apply the Ramer–Douglas–Peucker algorithm to remove unnecessary points.

You could also directly use: - (NSArray *) interpolatedPathPoints which returns an NSArray of points which can be used to build an approximation of the path. The algorithm is naive, so you have to simplify the result in the case, e.g , where a cubic Bezier path would be linear (if the control points are aligned); just as before, Ramer–Douglas–Peucker algorithm does the job.

Here is what the actual discretization looks like. Code is not self contained, you'll have to use all dependencies.

- (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;
}

Code belongs to Erica Sadun. See here for the complete implementation: https://github.com/erica/iOS-Drawing

Rob Napier also wrote about Bezier curves in iOS 6 Pushing the limits, Chapter 26 Fancy Text Layout. He was not trying to flatten a full UIBezierPath, only one cubic Bezier path defined with four points, but really that's exactly the same thing (discretizing a Bezier path) Also, you may find this article interesting: http://robnapier.net/faster-bezier

What you're basically looking to do is transform the path to replace all the curves and quadCurves to lines. You can do this pretty painlessly with CGPathApply and wrap it in a category on 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

The effect can be seen below, where green is the original path, red is the path generated by this method, and yellow is the path using setFlatness:MAXFLOAT

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!