/** * Copyright (c) 2015-present, Horcrux. * All rights reserved. * * This source code is licensed under the MIT-style license found in the * LICENSE file in the root directory of this source tree. */ /* Erica Sadun, http://ericasadun.com iPhone Developer's Cookbook, 6.x Edition BSD License, Use at your own risk */ #import "UIBezierPath-Points.h" #define POINTSTRING(_CGPOINT_) (NSStringFromCGPoint(_CGPOINT_)) #define VALUE(_INDEX_) [NSValue valueWithCGPoint:points[_INDEX_]] #define POINT(_INDEX_) [(NSValue *)[points objectAtIndex:_INDEX_] CGPointValue] // Return distance between two points static float distance (CGPoint p1, CGPoint p2) { float dx = p2.x - p1.x; float dy = p2.y - p1.y; return sqrt(dx*dx + dy*dy); } @implementation UIBezierPath (Points) void getPointsFromBezier(void *info, const CGPathElement *element) { NSMutableArray *bezierPoints = (__bridge NSMutableArray *)info; CGPathElementType type = element->type; CGPoint *points = element->points; if (type != kCGPathElementCloseSubpath) { if ((type == kCGPathElementAddLineToPoint) || (type == kCGPathElementMoveToPoint)) [bezierPoints addObject:VALUE(0)]; else if (type == kCGPathElementAddQuadCurveToPoint) [bezierPoints addObject:VALUE(1)]; else if (type == kCGPathElementAddCurveToPoint) [bezierPoints addObject:VALUE(2)]; } } - (NSArray *)points { NSMutableArray *points = [NSMutableArray array]; CGPathApply(self.CGPath, (__bridge void *)points, getPointsFromBezier); return points; } // Return a Bezier path buit with the supplied points + (UIBezierPath *) pathWithPoints: (NSArray *) points { UIBezierPath *path = [UIBezierPath bezierPath]; if (points.count == 0) return path; [path moveToPoint:POINT(0)]; for (int i = 1; i < points.count; i++) [path addLineToPoint:POINT(i)]; return path; } - (CGFloat) length { NSArray *points = self.points; float totalPointLength = 0.0f; for (int i = 1; i < points.count; i++) totalPointLength += distance(POINT(i), POINT(i-1)); return totalPointLength; } - (NSArray *) pointPercentArray { // Use total length to calculate the percent of path consumed at each control point NSArray *points = self.points; int pointCount = points.count; float totalPointLength = self.length; float distanceTravelled = 0.0f; NSMutableArray *pointPercentArray = [NSMutableArray array]; [pointPercentArray addObject:@(0.0)]; for (int i = 1; i < pointCount; i++) { distanceTravelled += distance(POINT(i), POINT(i-1)); [pointPercentArray addObject:@(distanceTravelled / totalPointLength)]; } // Add a final item just to stop with. Probably not needed. [pointPercentArray addObject:[NSNumber numberWithFloat:1.1f]]; // 110% return pointPercentArray; } - (CGPoint) pointAtPercent: (CGFloat) percent withSlope: (CGPoint *) slope { NSArray *points = self.points; NSArray *percentArray = self.pointPercentArray; CFIndex lastPointIndex = points.count - 1; if (!points.count) { return CGPointZero; } // Check for 0% and 100% if (percent <= 0.0f) { return POINT(0); } if (percent >= 1.0f) { return POINT(lastPointIndex); } // Find a corresponding pair of points in the path CFIndex index = 1; while ((index < percentArray.count) && (percent > ((NSNumber *)percentArray[index]).floatValue)) { index++; } // This should not happen. if (index > lastPointIndex) { return POINT(lastPointIndex); } // Calculate the intermediate distance between the two points CGPoint point1 = POINT(index -1); CGPoint point2 = POINT(index); float percent1 = [[percentArray objectAtIndex:index - 1] floatValue]; float percent2 = [[percentArray objectAtIndex:index] floatValue]; float percentOffset = (percent - percent1) / (percent2 - percent1); float dx = point2.x - point1.x; float dy = point2.y - point1.y; // Store dy, dx for retrieving arctan if (slope) { *slope = CGPointMake(dx, dy); } // Calculate new point CGFloat newX = point1.x + (percentOffset * dx); CGFloat newY = point1.y + (percentOffset * dy); CGPoint targetPoint = CGPointMake(newX, newY); return targetPoint; } void getBezierElements(void *info, const CGPathElement *element) { NSMutableArray *bezierElements = (__bridge NSMutableArray *)info; CGPathElementType type = element->type; CGPoint *points = element->points; switch (type) { case kCGPathElementCloseSubpath: [bezierElements addObject:@[@(type)]]; break; case kCGPathElementMoveToPoint: case kCGPathElementAddLineToPoint: [bezierElements addObject:@[@(type), VALUE(0)]]; break; case kCGPathElementAddQuadCurveToPoint: [bezierElements addObject:@[@(type), VALUE(0), VALUE(1)]]; break; case kCGPathElementAddCurveToPoint: [bezierElements addObject:@[@(type), VALUE(0), VALUE(1), VALUE(2)]]; break; } } - (NSArray *) bezierElements { NSMutableArray *elements = [NSMutableArray array]; CGPathApply(self.CGPath, (__bridge void *)elements, getBezierElements); return elements; } + (UIBezierPath *) pathWithElements: (NSArray *) elements { UIBezierPath *path = [UIBezierPath bezierPath]; if (elements.count == 0) return path; for (NSArray *points in elements) { if (!points.count) continue; CGPathElementType elementType = [points[0] integerValue]; switch (elementType) { case kCGPathElementCloseSubpath: [path closePath]; break; case kCGPathElementMoveToPoint: if (points.count == 2) [path moveToPoint:POINT(1)]; break; case kCGPathElementAddLineToPoint: if (points.count == 2) [path addLineToPoint:POINT(1)]; break; case kCGPathElementAddQuadCurveToPoint: if (points.count == 3) [path addQuadCurveToPoint:POINT(2) controlPoint:POINT(1)]; break; case kCGPathElementAddCurveToPoint: if (points.count == 4) [path addCurveToPoint:POINT(3) controlPoint1:POINT(1) controlPoint2:POINT(2)]; break; } } return path; } @end