From 7088d52b71c298707bb4348cfb6d28a1adb4e3a9 Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Fri, 29 Dec 2017 21:50:30 +0200 Subject: [PATCH] Refactor and simplify, cache path properties. Remove use of UIBezierPath and categories. --- ios/Elements/RNSVGPath.h | 2 +- ios/Elements/RNSVGPath.m | 256 ++++++++++++++++++++++++- ios/RNSVG.xcodeproj/project.pbxproj | 22 +-- ios/Text/RNSVGTSpan.h | 2 - ios/Text/RNSVGTSpan.m | 32 ++-- ios/Utils/BezierElement.h | 11 +- ios/Utils/BezierElement.m | 43 +---- ios/Utils/RNSVGPathParser.m | 53 +++-- ios/Utils/UIBezierPath+TextRendering.h | 12 -- ios/Utils/UIBezierPath+TextRendering.m | 230 ---------------------- 10 files changed, 311 insertions(+), 352 deletions(-) delete mode 100644 ios/Utils/UIBezierPath+TextRendering.h delete mode 100644 ios/Utils/UIBezierPath+TextRendering.m diff --git a/ios/Elements/RNSVGPath.h b/ios/Elements/RNSVGPath.h index 92461cd1..ea8e2431 100644 --- a/ios/Elements/RNSVGPath.h +++ b/ios/Elements/RNSVGPath.h @@ -14,6 +14,6 @@ @property (nonatomic, strong) RNSVGPathParser *d; -- (NSArray *)getBezierCurves; +- (void)getPathLength:(CGFloat*)length lineCount:(NSInteger*)lineCount lengths:(NSArray* __strong *)lengths lines:(NSArray* __strong *)lines isClosed:(BOOL*)isClosed; @end diff --git a/ios/Elements/RNSVGPath.m b/ios/Elements/RNSVGPath.m index c2ee7815..95eeb199 100644 --- a/ios/Elements/RNSVGPath.m +++ b/ios/Elements/RNSVGPath.m @@ -7,10 +7,256 @@ */ #import "RNSVGPath.h" +#import "BezierElement.h" + +/* Bezier logic from PerformanceBezier */ +/* + + ## License + + Creative Commons License
This work is licensed under a Creative Commons Attribution 3.0 United States License. + + For attribution, please include: + + 1. Mention original author "Adam Wulf for Loose Leaf app" + 2. Link to https://getlooseleaf.com/opensource/ + 3. Link to https://github.com/adamwulf/PerformanceBezier + + */ +static CGFloat idealFlatness = .01; + +/** + * returns the distance between two points + */ +CGFloat distance(CGPoint p1, CGPoint p2) +{ + CGFloat dx = p2.x - p1.x; + CGFloat dy = p2.y - p1.y; + + return sqrt(dx*dx + dy*dy); +} + +/** + * returns the dot product of two coordinates + */ +CGFloat dotProduct(const CGPoint p1, const CGPoint p2) { + return p1.x * p2.x + p1.y * p2.y; +} + +/** + * returns the shortest distance from a point to a line + */ +CGFloat distanceOfPointToLine(CGPoint point, CGPoint start, CGPoint end){ + CGPoint v = CGPointMake(end.x - start.x, end.y - start.y); + CGPoint w = CGPointMake(point.x - start.x, point.y - start.y); + CGFloat c1 = dotProduct(w, v); + CGFloat c2 = dotProduct(v, v); + CGFloat d; + if (c1 <= 0) { + d = distance(point, start); + } + else if (c2 <= c1) { + d = distance(point, end); + } + else { + CGFloat b = c1 / c2; + CGPoint Pb = CGPointMake(start.x + b * v.x, start.y + b * v.y); + d = distance(point, Pb); + } + return d; +} + +/** + * calculate the point on a bezier at time t + * where 0 < t < 1 + */ +CGPoint bezierPointAtT(const CGPoint bez[4], CGFloat t) +{ + CGPoint q; + CGFloat mt = 1 - t; + + CGPoint bez1[4]; + CGPoint bez2[4]; + + q.x = mt * bez[1].x + t * bez[2].x; + q.y = mt * bez[1].y + t * bez[2].y; + bez1[1].x = mt * bez[0].x + t * bez[1].x; + bez1[1].y = mt * bez[0].y + t * bez[1].y; + bez2[2].x = mt * bez[2].x + t * bez[3].x; + bez2[2].y = mt * bez[2].y + t * bez[3].y; + + bez1[2].x = mt * bez1[1].x + t * q.x; + bez1[2].y = mt * bez1[1].y + t * q.y; + bez2[1].x = mt * q.x + t * bez2[2].x; + bez2[1].y = mt * q.y + t * bez2[2].y; + + bez1[3].x = bez2[0].x = mt * bez1[2].x + t * bez2[1].x; + bez1[3].y = bez2[0].y = mt * bez1[2].y + t * bez2[1].y; + + return CGPointMake(bez1[3].x, bez1[3].y); +} + +// Subdivide a Bézier (specific division) +/* + * (c) 2004 Alastair J. Houghton + * All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. The name of the author of this software may not be used to endorse + * or promote products derived from the software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT OWNER BE LIABLE FOR ANY DIRECT, INDIRECT, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ +void subdivideBezierAtT(const CGPoint bez[4], CGPoint bez1[4], CGPoint bez2[4], CGFloat t) +{ + CGPoint q; + CGFloat mt = 1 - t; + + bez1[0].x = bez[0].x; + bez1[0].y = bez[0].y; + bez2[3].x = bez[3].x; + bez2[3].y = bez[3].y; + + q.x = mt * bez[1].x + t * bez[2].x; + q.y = mt * bez[1].y + t * bez[2].y; + bez1[1].x = mt * bez[0].x + t * bez[1].x; + bez1[1].y = mt * bez[0].y + t * bez[1].y; + bez2[2].x = mt * bez[2].x + t * bez[3].x; + bez2[2].y = mt * bez[2].y + t * bez[3].y; + + bez1[2].x = mt * bez1[1].x + t * q.x; + bez1[2].y = mt * bez1[1].y + t * q.y; + bez2[1].x = mt * q.x + t * bez2[2].x; + bez2[1].y = mt * q.y + t * bez2[2].y; + + bez1[3].x = bez2[0].x = mt * bez1[2].x + t * bez2[1].x; + bez1[3].y = bez2[0].y = mt * bez1[2].y + t * bez2[1].y; +} + +void addLine(CGPoint *last, const CGPoint *next, NSMutableArray *lines, CGFloat *length, NSMutableArray *lengths) { + NSArray *line = @[[NSValue valueWithCGPoint:*last], [NSValue valueWithCGPoint:*next]]; + [lines addObject:line]; + *length += distance(*last, *next); + [lengths addObject:[NSNumber numberWithDouble:*length]]; + *last = *next; +} @implementation RNSVGPath { CGPathRef _path; + NSMutableArray *lengths; + NSMutableArray *lines; + NSInteger lineCount; + CGFloat length; + BOOL isClosed; + BOOL cached; +} + +- (void)getPathLength:(CGFloat*)lengthP lineCount:(NSInteger*)lineCountP lengths:(NSArray* __strong *)lengthsP lines:(NSArray* __strong *)linesP isClosed:(BOOL*)isClosedP +{ + if (!cached) { + CGPoint origin = CGPointMake (0.0, 0.0); + CGPoint last = CGPointMake (0.0, 0.0); + lengths = [NSMutableArray array]; + lines = [NSMutableArray array]; + isClosed = NO; + lineCount = 0; + length = 0; + + NSArray *elements = [_d getBezierCurves]; + for (BezierElement *element in elements) { + switch (element.elementType) + { + case kCGPathElementMoveToPoint: + origin = last = element.point; + break; + + case kCGPathElementAddLineToPoint: { + CGPoint next = element.point; + addLine(&last, &next, lines, &length, lengths); + lineCount++; + break; + } + case kCGPathElementAddQuadCurveToPoint: + case kCGPathElementAddCurveToPoint: + { + // handle both curve types gracefully + CGPoint curveTo = element.point; + CGPoint ctrl1 = element.controlPoint1; + CGPoint ctrl2 = element.elementType == kCGPathElementAddQuadCurveToPoint ? ctrl1 : element.controlPoint2; + + // this is the bezier for our current element + CGPoint bezier[4] = { last, ctrl1, ctrl2, curveTo }; + NSValue *arr = [NSValue valueWithBytes:&bezier objCType:@encode(CGPoint[4])]; + NSMutableArray *curves = [NSMutableArray arrayWithObjects:arr, nil]; + + for (NSInteger curveIndex = 0; curveIndex >= 0; curveIndex--) { + CGPoint bez[4]; + [curves[curveIndex] getValue:&bez]; + [curves removeLastObject]; + + // calculate the error rate of the curve vs + // a line segement between the start and end points + CGPoint onCurve = bezierPointAtT(bez, .5); + CGPoint next = bez[3]; + CGFloat error = distanceOfPointToLine(onCurve, last, next); + + // if the error is less than our accepted level of error + // then add a line, else, split the curve in half + if (error <= idealFlatness) { + addLine(&last, &next, lines, &length, lengths); + lineCount++; + } else { + CGPoint bez1[4], bez2[4]; + subdivideBezierAtT(bez, bez1, bez2, .5); + [curves addObject:[NSValue valueWithBytes:&bez2 objCType:@encode(CGPoint[4])]]; + [curves addObject:[NSValue valueWithBytes:&bez1 objCType:@encode(CGPoint[4])]]; + curveIndex += 2; + } + } + last = curveTo; + break; + } + + case kCGPathElementCloseSubpath: { + CGPoint next = origin; + addLine(&last, &next, lines, &length, lengths); + lineCount++; + isClosed = YES; + break; + } + + default: + break; + } + } + cached = YES; + } + *lineCountP = lineCount; + *isClosedP = isClosed; + *lengthsP = lengths; + *lengthP = length; + *linesP = lines; } - (void)setD:(RNSVGPathParser *)d @@ -21,6 +267,9 @@ [self invalidate]; _d = d; + lines = nil; + lengths = nil; + cached = false; CGPathRelease(_path); _path = CGPathRetain([d getPath]); } @@ -30,13 +279,10 @@ return _path; } -- (NSArray *)getBezierCurves -{ - return [_d getBezierCurves]; -} - - (void)dealloc { + lines = nil; + lengths = nil; CGPathRelease(_path); } diff --git a/ios/RNSVG.xcodeproj/project.pbxproj b/ios/RNSVG.xcodeproj/project.pbxproj index a5dca387..88570bcd 100644 --- a/ios/RNSVG.xcodeproj/project.pbxproj +++ b/ios/RNSVG.xcodeproj/project.pbxproj @@ -87,10 +87,7 @@ 9494C5471F4C44DD00D5BCFD /* TextPathSide.m in Sources */ = {isa = PBXBuildFile; fileRef = 9494C5361F4C44DD00D5BCFD /* TextPathSide.m */; }; 9494C5481F4C44DD00D5BCFD /* TextPathSpacing.m in Sources */ = {isa = PBXBuildFile; fileRef = 9494C5371F4C44DD00D5BCFD /* TextPathSpacing.m */; }; 9494C5491F4C44DD00D5BCFD /* TextPathSpacing.m in Sources */ = {isa = PBXBuildFile; fileRef = 9494C5371F4C44DD00D5BCFD /* TextPathSpacing.m */; }; - 94EB93171FF4196100C0B251 /* UIBezierPath+TextRendering.m in Sources */ = {isa = PBXBuildFile; fileRef = 94EB93161FF4196100C0B251 /* UIBezierPath+TextRendering.m */; }; - 94EB93181FF4196100C0B251 /* UIBezierPath+TextRendering.m in Sources */ = {isa = PBXBuildFile; fileRef = 94EB93161FF4196100C0B251 /* UIBezierPath+TextRendering.m */; }; - 94EB936C1FF4916F00C0B251 /* BezierElement.m in Sources */ = {isa = PBXBuildFile; fileRef = 94EB936B1FF4916F00C0B251 /* BezierElement.m */; }; - 94EB936D1FF4916F00C0B251 /* BezierElement.m in Sources */ = {isa = PBXBuildFile; fileRef = 94EB936B1FF4916F00C0B251 /* BezierElement.m */; }; + 94C70B1A1FF6B1C0004DFD49 /* BezierElement.m in Sources */ = {isa = PBXBuildFile; fileRef = 94C70B171FF6B1C0004DFD49 /* BezierElement.m */; }; A361E76E1EB0C33D00646005 /* RNSVGTextManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 10BA0D331CE74E3100887C2B /* RNSVGTextManager.m */; }; A361E76F1EB0C33D00646005 /* RNSVGImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1039D2841CE71EB7001E90A8 /* RNSVGImage.m */; }; A361E7701EB0C33D00646005 /* RNSVGRect.m in Sources */ = {isa = PBXBuildFile; fileRef = 10BA0D471CE74E3D00887C2B /* RNSVGRect.m */; }; @@ -295,11 +292,9 @@ 9494C5351F4C44DD00D5BCFD /* TextPathMidLine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TextPathMidLine.m; path = Text/TextPathMidLine.m; sourceTree = ""; }; 9494C5361F4C44DD00D5BCFD /* TextPathSide.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TextPathSide.m; path = Text/TextPathSide.m; sourceTree = ""; }; 9494C5371F4C44DD00D5BCFD /* TextPathSpacing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TextPathSpacing.m; path = Text/TextPathSpacing.m; sourceTree = ""; }; + 94C70B151FF6B1BF004DFD49 /* BezierElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BezierElement.h; path = Utils/BezierElement.h; sourceTree = ""; }; + 94C70B171FF6B1C0004DFD49 /* BezierElement.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BezierElement.m; path = Utils/BezierElement.m; sourceTree = ""; }; 94DDAC5C1F3D024300EED511 /* libRNSVG-tvOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libRNSVG-tvOS.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 94EB93151FF4196100C0B251 /* UIBezierPath+TextRendering.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "UIBezierPath+TextRendering.h"; path = "Utils/UIBezierPath+TextRendering.h"; sourceTree = ""; }; - 94EB93161FF4196100C0B251 /* UIBezierPath+TextRendering.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "UIBezierPath+TextRendering.m"; path = "Utils/UIBezierPath+TextRendering.m"; sourceTree = ""; }; - 94EB936B1FF4916F00C0B251 /* BezierElement.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = BezierElement.m; path = Utils/BezierElement.m; sourceTree = ""; }; - 94EB93701FF4918D00C0B251 /* BezierElement.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BezierElement.h; path = Utils/BezierElement.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -501,6 +496,8 @@ 1039D29A1CE7212C001E90A8 /* Utils */ = { isa = PBXGroup; children = ( + 94C70B151FF6B1BF004DFD49 /* BezierElement.h */, + 94C70B171FF6B1C0004DFD49 /* BezierElement.m */, 7F69160D1E3703D800DA6EDC /* RNSVGUnits.h */, 10ABC7381D43982B006CCF6E /* RNSVGVBMOS.h */, 10ABC7371D439779006CCF6E /* RNSVGCGFCRule.h */, @@ -514,10 +511,6 @@ 7F9CDAF91E1F809C00E0C805 /* RNSVGPathParser.m */, 1039D29B1CE72177001E90A8 /* RCTConvert+RNSVG.h */, 1039D29C1CE72177001E90A8 /* RCTConvert+RNSVG.m */, - 94EB93151FF4196100C0B251 /* UIBezierPath+TextRendering.h */, - 94EB93161FF4196100C0B251 /* UIBezierPath+TextRendering.m */, - 94EB936B1FF4916F00C0B251 /* BezierElement.m */, - 94EB93701FF4918D00C0B251 /* BezierElement.h */, ); name = Utils; sourceTree = ""; @@ -614,7 +607,6 @@ 10BA0D341CE74E3100887C2B /* RNSVGCircleManager.m in Sources */, 10BEC1BC1D3F66F500FDCB19 /* RNSVGLinearGradient.m in Sources */, 9494C5461F4C44DD00D5BCFD /* TextPathSide.m in Sources */, - 94EB93171FF4196100C0B251 /* UIBezierPath+TextRendering.m in Sources */, 1039D2B01CE72F27001E90A8 /* RNSVGPercentageConverter.m in Sources */, 9494C53C1F4C44DD00D5BCFD /* TextAnchor.m in Sources */, 10BA0D491CE74E3D00887C2B /* RNSVGEllipse.m in Sources */, @@ -649,7 +641,7 @@ 9494C5251F4B605F00D5BCFD /* GlyphContext.m in Sources */, 10BA0D481CE74E3D00887C2B /* RNSVGCircle.m in Sources */, 9494C5401F4C44DD00D5BCFD /* TextLengthAdjust.m in Sources */, - 94EB936C1FF4916F00C0B251 /* BezierElement.m in Sources */, + 94C70B1A1FF6B1C0004DFD49 /* BezierElement.m in Sources */, 10BA0D351CE74E3100887C2B /* RNSVGEllipseManager.m in Sources */, 1039D2A01CE72177001E90A8 /* RCTConvert+RNSVG.m in Sources */, 9494C4FF1F4B5BE800D5BCFD /* FontData.m in Sources */, @@ -683,7 +675,6 @@ A361E7711EB0C33D00646005 /* RNSVGCircleManager.m in Sources */, A361E7721EB0C33D00646005 /* RNSVGLinearGradient.m in Sources */, A361E7731EB0C33D00646005 /* RNSVGPercentageConverter.m in Sources */, - 94EB93181FF4196100C0B251 /* UIBezierPath+TextRendering.m in Sources */, 9494C53F1F4C44DD00D5BCFD /* TextDecoration.m in Sources */, A361E7751EB0C33D00646005 /* RNSVGEllipse.m in Sources */, A361E7761EB0C33D00646005 /* RNSVGPath.m in Sources */, @@ -718,7 +709,6 @@ 9494C53B1F4C44DD00D5BCFD /* FontVariantLigatures.m in Sources */, 9494C5001F4B5BE800D5BCFD /* FontData.m in Sources */, 9494C5491F4C44DD00D5BCFD /* TextPathSpacing.m in Sources */, - 94EB936D1FF4916F00C0B251 /* BezierElement.m in Sources */, A361E78D1EB0C33D00646005 /* RNSVGLineManager.m in Sources */, 9494C53D1F4C44DD00D5BCFD /* TextAnchor.m in Sources */, 9494C5471F4C44DD00D5BCFD /* TextPathSide.m in Sources */, diff --git a/ios/Text/RNSVGTSpan.h b/ios/Text/RNSVGTSpan.h index 0b1c7c9e..9f909070 100644 --- a/ios/Text/RNSVGTSpan.h +++ b/ios/Text/RNSVGTSpan.h @@ -15,8 +15,6 @@ #import "TextPathSpacing.h" #import "TextLengthAdjust.h" #import "AlignmentBaseline.h" -#import "UIBezierPath+TextRendering.h" - @interface RNSVGTSpan : RNSVGText diff --git a/ios/Text/RNSVGTSpan.m b/ios/Text/RNSVGTSpan.m index ed52be91..1d549ccc 100644 --- a/ios/Text/RNSVGTSpan.m +++ b/ios/Text/RNSVGTSpan.m @@ -18,8 +18,8 @@ NSCharacterSet *separators = nil; CGPathRef _cache; CGFloat _pathLength; RNSVGTextPath *textPath; - NSMutableArray *lengths; - NSMutableArray *lines; + NSArray *lengths; + NSArray *lines; NSInteger lineCount; BOOL isClosed; } @@ -788,7 +788,7 @@ NSCharacterSet *separators = nil; int i = 0; CGFloat totalLength = 0; CGFloat prevLength = 0; - + // TODO investigate at what lineCount a binary search is faster while (i < lineCount - 1) { prevLength = totalLength; @@ -799,25 +799,24 @@ NSCharacterSet *separators = nil; break; } }; - + CGFloat length = totalLength - prevLength; CGFloat targetPercent = (midPoint - prevLength) / length; - + NSArray * points = [lines objectAtIndex: i]; CGPoint p1 = [[points objectAtIndex: 0] CGPointValue]; CGPoint p2 = [[points objectAtIndex: 1] CGPointValue]; - + CGPoint slope; CGPoint mid = InterpolateLineSegment(p1, p2, targetPercent, &slope); - + // Calculate the rotation double angle = atan2(slope.y, slope.x); transform = CGAffineTransformConcat(CGAffineTransformMakeTranslation(mid.x, mid.y), transform); transform = CGAffineTransformConcat(CGAffineTransformMakeRotation(angle + r), transform); transform = CGAffineTransformScale(transform, scaledDirection, side); - transform = CGAffineTransformConcat(CGAffineTransformMakeTranslation(-halfWay, dy + baselineShift), transform); - transform = CGAffineTransformConcat(CGAffineTransformMakeTranslation(0, y), transform); + transform = CGAffineTransformConcat(CGAffineTransformMakeTranslation(-halfWay, y + dy + baselineShift), transform); } else { transform = CGAffineTransformMakeTranslation(startPoint, y + dy + baselineShift); transform = CGAffineTransformConcat(CGAffineTransformMakeRotation(r), transform); @@ -839,13 +838,13 @@ CGPoint InterpolateLineSegment(CGPoint p1, CGPoint p2, CGFloat percent, CGPoint { CGFloat dx = p2.x - p1.x; CGFloat dy = p2.y - p1.y; - + if (slope) *slope = CGPointMake(dx, dy); - + CGFloat px = p1.x + dx * percent; CGFloat py = p1.y + dy * percent; - + return CGPointMake(px, py); } @@ -865,16 +864,13 @@ CGFloat getTextAnchorOffset(enum TextAnchor textAnchor, CGFloat width) - (void)setupTextPath:(CGContextRef)context { + lines = nil; + lengths = nil; textPath = nil; [self traverseTextSuperviews:^(__kindof RNSVGText *node) { if ([node class] == [RNSVGTextPath class]) { textPath = (RNSVGTextPath*) node; - RNSVGPath *svgPath = [textPath getPath]; - UIBezierPath *bezierPath = [UIBezierPath bezierPathWithCGPath:[svgPath getPath:nil]]; - - lines = [NSMutableArray array]; - lengths = [NSMutableArray array]; - [bezierPath getTextProperties](&_pathLength, &lineCount, lengths, lines, &isClosed); + [[textPath getPath] getPathLength:&_pathLength lineCount:&lineCount lengths:&lengths lines:&lines isClosed:&isClosed]; return NO; } return YES; diff --git a/ios/Utils/BezierElement.h b/ios/Utils/BezierElement.h index 17af3394..a78c8721 100644 --- a/ios/Utils/BezierElement.h +++ b/ios/Utils/BezierElement.h @@ -1,15 +1,13 @@ /* - - Erica Sadun, http://ericasadun.com - - */ + Erica Sadun, http://ericasadun.com + https://github.com/erica/iOS-Drawing/tree/master/C08/Quartz%20Book%20Pack/Bezier + */ #import #import #define NULLPOINT CGRectNull.origin -#define POINT_IS_NULL(_POINT_) CGPointEqualToPoint(_POINT_, NULLPOINT) @interface BezierElement : NSObject @@ -19,8 +17,5 @@ @property (nonatomic, assign) CGPoint controlPoint1; @property (nonatomic, assign) CGPoint controlPoint2; -// Instance creation -+ (instancetype) elementWithPathElement: (CGPathElement) element; - @end; diff --git a/ios/Utils/BezierElement.m b/ios/Utils/BezierElement.m index ab53465c..73a8eb50 100644 --- a/ios/Utils/BezierElement.m +++ b/ios/Utils/BezierElement.m @@ -1,9 +1,8 @@ /* - - Erica Sadun, http://ericasadun.com - - */ + Erica Sadun, http://ericasadun.com + https://github.com/erica/iOS-Drawing/tree/master/C08/Quartz%20Book%20Pack/Bezier + */ #import "BezierElement.h" @@ -22,41 +21,5 @@ } return self; } - -+ (instancetype) elementWithPathElement: (CGPathElement) element -{ - BezierElement *newElement = [[self alloc] init]; - newElement.elementType = element.type; - - switch (newElement.elementType) - { - case kCGPathElementCloseSubpath: - break; - case kCGPathElementMoveToPoint: - case kCGPathElementAddLineToPoint: - { - newElement.point = element.points[0]; - break; - } - case kCGPathElementAddQuadCurveToPoint: - { - newElement.point = element.points[1]; - newElement.controlPoint1 = element.points[0]; - break; - } - case kCGPathElementAddCurveToPoint: - { - newElement.point = element.points[2]; - newElement.controlPoint1 = element.points[0]; - newElement.controlPoint2 = element.points[1]; - break; - } - default: - break; - } - - return newElement; -} - @end diff --git a/ios/Utils/RNSVGPathParser.m b/ios/Utils/RNSVGPathParser.m index 719d8116..76d78c71 100644 --- a/ios/Utils/RNSVGPathParser.m +++ b/ios/Utils/RNSVGPathParser.m @@ -9,14 +9,14 @@ #import "RNSVGPathParser.h" #import #import "math.h" +#import "BezierElement.h" @implementation RNSVGPathParser { NSString* _d; NSString* _originD; NSRegularExpression* _pathRegularExpression; - NSMutableArray* _bezierCurves; - NSValue *_lastStartPoint; + NSMutableArray* _bezierCurves; float _penX; float _penY; float _penDownX; @@ -159,8 +159,10 @@ _pivotY = _penY = y; CGPathMoveToPoint(path, nil, x, y); - _lastStartPoint = [NSValue valueWithCGPoint: CGPointMake(x, y)]; - [_bezierCurves addObject: @[_lastStartPoint]]; + BezierElement *newElement = [[BezierElement alloc] init]; + newElement.elementType = kCGPathElementMoveToPoint; + newElement.point = CGPointMake(x, y); + [_bezierCurves addObject:newElement]; } - (void)line:(CGMutablePathRef)path x:(float)x y:(float)y @@ -174,8 +176,10 @@ _pivotY = _penY = y; CGPathAddLineToPoint(path, nil, x, y); - NSValue * destination = [NSValue valueWithCGPoint:CGPointMake(x, y)]; - [_bezierCurves addObject: @[destination, destination, destination]]; + BezierElement *newElement = [[BezierElement alloc] init]; + newElement.elementType = kCGPathElementAddLineToPoint; + newElement.point = CGPointMake(x, y); + [_bezierCurves addObject:newElement]; } - (void)curve:(CGMutablePathRef)path c1x:(float)c1x c1y:(float)c1y c2x:(float)c2x c2y:(float)c2y ex:(float)ex ey:(float)ey @@ -202,11 +206,12 @@ _penY = ey; CGPathAddCurveToPoint(path, nil, c1x, c1y, c2x, c2y, ex, ey); - [_bezierCurves addObject: @[ - [NSValue valueWithCGPoint:CGPointMake(c1x, c1y)], - [NSValue valueWithCGPoint:CGPointMake(c2x, c2y)], - [NSValue valueWithCGPoint:CGPointMake(ex, ey)] - ]]; + BezierElement *newElement = [[BezierElement alloc] init]; + newElement.elementType = kCGPathElementAddCurveToPoint; + newElement.controlPoint1 = CGPointMake(c1x, c1y); + newElement.controlPoint2 = CGPointMake(c2x, c2y); + newElement.point = CGPointMake(ex, ey); + [_bezierCurves addObject:newElement]; } - (void)smoothCurve:(CGMutablePathRef)path c1x:(float)c1x c1y:(float)c1y ex:(float)ex ey:(float)ey @@ -367,14 +372,20 @@ float cp2x = x + k * y; float cp2y = y - k * x; - CGPathAddCurveToPoint(path, - nil, - cx + xx * cp1x + yx * cp1y, - cy + xy * cp1x + yy * cp1y, - cx + xx * cp2x + yx * cp2y, - cy + xy * cp2x + yy * cp2y, - cx + xx * x + yx * y, - cy + xy * x + yy * y); + float c1x = cx + xx * cp1x + yx * cp1y; + float c1y = cy + xy * cp1x + yy * cp1y; + float c2x = cx + xx * cp2x + yx * cp2y; + float c2y = cy + xy * cp2x + yy * cp2y; + float ex = cx + xx * x + yx * y; + float ey = cy + xy * x + yy * y; + CGPathAddCurveToPoint(path, nil, c1x, c1y, c2x, c2y, ex, ey); + + BezierElement *newElement = [[BezierElement alloc] init]; + newElement.elementType = kCGPathElementAddCurveToPoint; + newElement.controlPoint1 = CGPointMake(c1x, c1y); + newElement.controlPoint2 = CGPointMake(c2x, c2y); + newElement.point = CGPointMake(ex, ey); + [_bezierCurves addObject:newElement]; } } @@ -385,7 +396,9 @@ _penY = _penDownY; _penDownSet = NO; CGPathCloseSubpath(path); - [_bezierCurves addObject: @[_lastStartPoint, _lastStartPoint, _lastStartPoint]]; + BezierElement *newElement = [[BezierElement alloc] init]; + newElement.elementType = kCGPathElementCloseSubpath; + [_bezierCurves addObject:newElement]; } } diff --git a/ios/Utils/UIBezierPath+TextRendering.h b/ios/Utils/UIBezierPath+TextRendering.h deleted file mode 100644 index 88eadab2..00000000 --- a/ios/Utils/UIBezierPath+TextRendering.h +++ /dev/null @@ -1,12 +0,0 @@ -// -// UIBezierPath+TextRendering.h -// RNSVG -// -// Created by Mikael Sand on 27/12/2017. -// - -#import - -@interface UIBezierPath (TextRendering) -- (void (^)(CGFloat *lengthP, NSInteger *lineCountP, NSMutableArray * lengthsP, NSMutableArray * linesP, BOOL *isClosedP)) getTextProperties; -@end diff --git a/ios/Utils/UIBezierPath+TextRendering.m b/ios/Utils/UIBezierPath+TextRendering.m deleted file mode 100644 index e64f8a0c..00000000 --- a/ios/Utils/UIBezierPath+TextRendering.m +++ /dev/null @@ -1,230 +0,0 @@ -// -// UIBezierPath+TextRendering.m -// RNSVG -// -// Created by Mikael Sand on 27/12/2017. -// - -#import "UIBezierPath+TextRendering.h" -#import "BezierElement.h" - -static CGFloat idealFlatness = .01; - -/** - * returns the distance between two points - */ -CGFloat distance(CGPoint p1, CGPoint p2) -{ - CGFloat dx = p2.x - p1.x; - CGFloat dy = p2.y - p1.y; - - return sqrt(dx*dx + dy*dy); -} - -/** - * returns the dot product of two coordinates - */ -CGFloat dotProduct(const CGPoint p1, const CGPoint p2) { - return p1.x * p2.x + p1.y * p2.y; -} - -/** - * returns the shortest distance from a point to a line - */ -CGFloat distanceOfPointToLine(CGPoint point, CGPoint start, CGPoint end){ - CGPoint v = CGPointMake(end.x - start.x, end.y - start.y); - CGPoint w = CGPointMake(point.x - start.x, point.y - start.y); - CGFloat c1 = dotProduct(w, v); - CGFloat c2 = dotProduct(v, v); - CGFloat d; - if (c1 <= 0) { - d = distance(point, start); - } - else if (c2 <= c1) { - d = distance(point, end); - } - else { - CGFloat b = c1 / c2; - CGPoint Pb = CGPointMake(start.x + b * v.x, start.y + b * v.y); - d = distance(point, Pb); - } - return d; -} - -/** - * calculate the point on a bezier at time t - * where 0 < t < 1 - */ -CGPoint bezierPointAtT(const CGPoint bez[4], CGFloat t) -{ - CGPoint q; - CGFloat mt = 1 - t; - - CGPoint bez1[4]; - CGPoint bez2[4]; - - q.x = mt * bez[1].x + t * bez[2].x; - q.y = mt * bez[1].y + t * bez[2].y; - bez1[1].x = mt * bez[0].x + t * bez[1].x; - bez1[1].y = mt * bez[0].y + t * bez[1].y; - bez2[2].x = mt * bez[2].x + t * bez[3].x; - bez2[2].y = mt * bez[2].y + t * bez[3].y; - - bez1[2].x = mt * bez1[1].x + t * q.x; - bez1[2].y = mt * bez1[1].y + t * q.y; - bez2[1].x = mt * q.x + t * bez2[2].x; - bez2[1].y = mt * q.y + t * bez2[2].y; - - bez1[3].x = bez2[0].x = mt * bez1[2].x + t * bez2[1].x; - bez1[3].y = bez2[0].y = mt * bez1[2].y + t * bez2[1].y; - - return CGPointMake(bez1[3].x, bez1[3].y); -} - -// Subdivide a Bézier (specific division) -void subdivideBezierAtT(const CGPoint bez[4], CGPoint bez1[4], CGPoint bez2[4], CGFloat t) -{ - CGPoint q; - CGFloat mt = 1 - t; - - bez1[0].x = bez[0].x; - bez1[0].y = bez[0].y; - bez2[3].x = bez[3].x; - bez2[3].y = bez[3].y; - - q.x = mt * bez[1].x + t * bez[2].x; - q.y = mt * bez[1].y + t * bez[2].y; - bez1[1].x = mt * bez[0].x + t * bez[1].x; - bez1[1].y = mt * bez[0].y + t * bez[1].y; - bez2[2].x = mt * bez[2].x + t * bez[3].x; - bez2[2].y = mt * bez[2].y + t * bez[3].y; - - bez1[2].x = mt * bez1[1].x + t * q.x; - bez1[2].y = mt * bez1[1].y + t * q.y; - bez2[1].x = mt * q.x + t * bez2[2].x; - bez2[1].y = mt * q.y + t * bez2[2].y; - - bez1[3].x = bez2[0].x = mt * bez1[2].x + t * bez2[1].x; - bez1[3].y = bez2[0].y = mt * bez1[2].y + t * bez2[1].y; -} - -void addLine(CGPoint *last, const CGPoint *next, NSMutableArray *lines, CGFloat *length, NSMutableArray *lengths) { - NSArray *line = @[[NSValue valueWithCGPoint:*last], [NSValue valueWithCGPoint:*next]]; - [lines addObject:line]; - *length += distance(*last, *next); - [lengths addObject:[NSNumber numberWithDouble:*length]]; - *last = *next; -} - -// Convert one element to BezierElement and save to array -void GetBezierElements(void *info, const CGPathElement *element) -{ - NSMutableArray *bezierElements = (__bridge NSMutableArray *)info; - if (element) - [bezierElements addObject:[BezierElement elementWithPathElement:*element]]; -} - -@implementation UIBezierPath (TextRendering) - -// Retrieve array of component elements -- (NSArray *) elements -{ - NSMutableArray *elements = [NSMutableArray array]; - CGPathApply(self.CGPath, (__bridge void *)elements, GetBezierElements); - return elements; -} - -- (void (^)(CGFloat *, NSInteger *, NSMutableArray *, NSMutableArray *, BOOL *)) getTextProperties{ - return ^(CGFloat *lengthP, NSInteger *lineCountP, NSMutableArray * lengths, NSMutableArray * lines, BOOL *isClosedP) { - CGPoint origin = CGPointMake (0.0, 0.0); - CGPoint last = CGPointMake (0.0, 0.0); - NSInteger lineCount = 0; - CGFloat length = 0; - BOOL isClosed = NO; - NSArray * elements = self.elements; - for (BezierElement *element in elements) { - switch (element.elementType) - { - case kCGPathElementMoveToPoint: - origin = last = element.point; - break; - - case kCGPathElementAddLineToPoint: { - CGPoint next = element.point; - addLine(&last, &next, lines, &length, lengths); - lineCount++; - break; - } - - case kCGPathElementAddQuadCurveToPoint: - case kCGPathElementAddCurveToPoint: - { - // handle both curve types gracefully - CGPoint curveTo; - CGPoint ctrl1; - CGPoint ctrl2; - if (element.elementType == kCGPathElementAddQuadCurveToPoint) { - curveTo = element.point; - ctrl1 = element.controlPoint1; - ctrl2 = ctrl1; - } else if (element.elementType == kCGPathElementAddCurveToPoint) { - curveTo = element.point; - ctrl1 = element.controlPoint1; - ctrl2 = element.controlPoint2; - } else { - break; - } - - // this is the bezier for our current element - CGPoint bezier[4] = { last, ctrl1, ctrl2, curveTo }; - NSValue *arr = [NSValue valueWithBytes:&bezier objCType:@encode(CGPoint[4])]; - NSMutableArray *curves = [NSMutableArray arrayWithObjects:arr, nil]; - - NSInteger count = 1; - while (count-- > 0) { - CGPoint bez[4]; - [curves[count] getValue:&bez]; - [curves removeLastObject]; - - // calculate the error rate of the curve vs - // a line segement between the start and end points - CGPoint onCurve = bezierPointAtT(bez, .5); - CGPoint next = bez[3]; - CGFloat error = distanceOfPointToLine(onCurve, last, next); - - // if the error is less than our accepted level of error - // then add a line, else, split the curve in half - if (error <= idealFlatness) { - addLine(&last, &next, lines, &length, lengths); - lineCount++; - } else { - CGPoint bez1[4], bez2[4]; - subdivideBezierAtT(bez, bez1, bez2, .5); - [curves addObject:[NSValue valueWithBytes:&bez2 objCType:@encode(CGPoint[4])]]; - [curves addObject:[NSValue valueWithBytes:&bez1 objCType:@encode(CGPoint[4])]]; - count += 2; - } - } - last = curveTo; - break; - } - - case kCGPathElementCloseSubpath: { - CGPoint next = origin; - addLine(&last, &next, lines, &length, lengths); - lineCount++; - isClosed = YES; - break; - } - - default: - break; - } - } - *lineCountP = lineCount; - *isClosedP = isClosed; - *lengthP = length; - }; -} -@end -