From 78c4f2098d2e9a12ed188069b086fbc3128e2d87 Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Thu, 3 Oct 2019 22:17:59 +0300 Subject: [PATCH] feat(ios): implement getTotalLength and getPointAtLength --- .../horcrux/svg/RNSVGRenderableManager.java | 1 - ios/RNSVG.xcodeproj/project.pbxproj | 8 + ios/Text/RNSVGTSpan.m | 67 ++---- ios/Text/RNSVGTextPath.h | 3 - ios/Text/RNSVGTextPath.m | 197 ---------------- ios/Utils/RNSVGPathMeasure.h | 23 ++ ios/Utils/RNSVGPathMeasure.m | 214 ++++++++++++++++++ ios/ViewManagers/RNSVGRenderableManager.m | 72 +++++- 8 files changed, 320 insertions(+), 265 deletions(-) create mode 100644 ios/Utils/RNSVGPathMeasure.h create mode 100644 ios/Utils/RNSVGPathMeasure.m diff --git a/android/src/main/java/com/horcrux/svg/RNSVGRenderableManager.java b/android/src/main/java/com/horcrux/svg/RNSVGRenderableManager.java index ca42e2bef..211bbf093 100644 --- a/android/src/main/java/com/horcrux/svg/RNSVGRenderableManager.java +++ b/android/src/main/java/com/horcrux/svg/RNSVGRenderableManager.java @@ -52,7 +52,6 @@ public void run() { } else { successCallback.invoke(false); } - return; } else { float scale = svg.mScale; src[0] *= scale; diff --git a/ios/RNSVG.xcodeproj/project.pbxproj b/ios/RNSVG.xcodeproj/project.pbxproj index d4e716a63..31b06a719 100644 --- a/ios/RNSVG.xcodeproj/project.pbxproj +++ b/ios/RNSVG.xcodeproj/project.pbxproj @@ -67,6 +67,8 @@ 947F38102148119A00677F2A /* RNSVGMaskManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 947F380E2148119A00677F2A /* RNSVGMaskManager.m */; }; 9482DEFA23460EC800FC486E /* RNSVGContextBrush.m in Sources */ = {isa = PBXBuildFile; fileRef = 9482DEF823460EC700FC486E /* RNSVGContextBrush.m */; }; 9482DEFB23460EC800FC486E /* RNSVGContextBrush.m in Sources */ = {isa = PBXBuildFile; fileRef = 9482DEF823460EC700FC486E /* RNSVGContextBrush.m */; }; + 9482DF02234680A200FC486E /* RNSVGPathMeasure.m in Sources */ = {isa = PBXBuildFile; fileRef = 9482DF00234680A200FC486E /* RNSVGPathMeasure.m */; }; + 9482DF03234680A200FC486E /* RNSVGPathMeasure.m in Sources */ = {isa = PBXBuildFile; fileRef = 9482DF00234680A200FC486E /* RNSVGPathMeasure.m */; }; 9494C4D81F473BA700D5BCFD /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9494C4D71F473BA700D5BCFD /* QuartzCore.framework */; }; 9494C4DA1F473BCB00D5BCFD /* CoreText.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9494C4D91F473BCB00D5BCFD /* CoreText.framework */; }; 9494C4DC1F473BD900D5BCFD /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9494C4DB1F473BD900D5BCFD /* CoreGraphics.framework */; }; @@ -261,6 +263,8 @@ 947F380E2148119A00677F2A /* RNSVGMaskManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNSVGMaskManager.m; sourceTree = ""; }; 9482DEF823460EC700FC486E /* RNSVGContextBrush.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNSVGContextBrush.m; sourceTree = ""; }; 9482DEF923460EC800FC486E /* RNSVGContextBrush.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNSVGContextBrush.h; sourceTree = ""; }; + 9482DF00234680A200FC486E /* RNSVGPathMeasure.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RNSVGPathMeasure.m; path = Utils/RNSVGPathMeasure.m; sourceTree = ""; }; + 9482DF01234680A200FC486E /* RNSVGPathMeasure.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSVGPathMeasure.h; path = Utils/RNSVGPathMeasure.h; sourceTree = ""; }; 9494C4D71F473BA700D5BCFD /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 9494C4D91F473BCB00D5BCFD /* CoreText.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreText.framework; path = System/Library/Frameworks/CoreText.framework; sourceTree = SDKROOT; }; 9494C4DB1F473BD900D5BCFD /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; @@ -482,6 +486,8 @@ 1039D29A1CE7212C001E90A8 /* Utils */ = { isa = PBXGroup; children = ( + 9482DF01234680A200FC486E /* RNSVGPathMeasure.h */, + 9482DF00234680A200FC486E /* RNSVGPathMeasure.m */, 94A178FD2344097B00693CB3 /* RNSVGMarkerPosition.h */, 94A178FE2344097B00693CB3 /* RNSVGMarkerPosition.m */, 94696EE92235A7F200C1D558 /* RNSVGVectorEffect.h */, @@ -608,6 +614,7 @@ 9482DEFA23460EC800FC486E /* RNSVGContextBrush.m in Sources */, 0CF68B071AF0549300FF9E5C /* RNSVGRenderable.m in Sources */, 1039D2891CE71EB7001E90A8 /* RNSVGGroup.m in Sources */, + 9482DF02234680A200FC486E /* RNSVGPathMeasure.m in Sources */, 10ED4A9E1CF0656A0078BC02 /* RNSVGClipPathManager.m in Sources */, 10BEC1C61D3F7BD300FDCB19 /* RNSVGPainter.m in Sources */, 10ED4AA21CF078830078BC02 /* RNSVGNode.m in Sources */, @@ -674,6 +681,7 @@ 9482DEFB23460EC800FC486E /* RNSVGContextBrush.m in Sources */, A361E77D1EB0C33D00646005 /* RNSVGGroup.m in Sources */, A361E77E1EB0C33D00646005 /* RNSVGClipPathManager.m in Sources */, + 9482DF03234680A200FC486E /* RNSVGPathMeasure.m in Sources */, A361E77F1EB0C33D00646005 /* RNSVGPainter.m in Sources */, A361E7801EB0C33D00646005 /* RNSVGNode.m in Sources */, A361E7811EB0C33D00646005 /* RNSVGClipPath.m in Sources */, diff --git a/ios/Text/RNSVGTSpan.m b/ios/Text/RNSVGTSpan.m index 155b1db6f..cda6575d1 100644 --- a/ios/Text/RNSVGTSpan.m +++ b/ios/Text/RNSVGTSpan.m @@ -9,6 +9,7 @@ #import "RNSVGText.h" #import "RNSVGTextPath.h" #import "RNSVGTextProperties.h" +#import "RNSVGPathMeasure.h" #import "RNSVGFontData.h" static NSCharacterSet *RNSVGTSpan_separators = nil; @@ -38,18 +39,14 @@ - (void)drawTextInRect:(CGRect) rect @implementation RNSVGTSpan { CGFloat startOffset; - CGFloat _pathLength; RNSVGTextPath *textPath; - NSArray *lengths; - NSArray *lines; - NSUInteger lineCount; - BOOL isClosed; NSMutableArray *emoji; NSMutableArray *emojiTransform; CGFloat cachedAdvance; CTFontRef fontRef; CGFloat firstX; CGFloat firstY; + RNSVGPathMeasure *measure; } - (id)init @@ -62,6 +59,7 @@ - (id)init emoji = [NSMutableArray arrayWithCapacity:0]; emojiTransform = [NSMutableArray arrayWithCapacity:0]; + measure = [[RNSVGPathMeasure alloc]init]; return self; } @@ -476,7 +474,7 @@ vertical alternates (OpenType feature: vert) must be enabled. int side = 1; CGFloat startOfRendering = 0; - CGFloat endOfRendering = _pathLength; + CGFloat endOfRendering = measure.pathLength; CGFloat fontSize = [gc getFontSize]; //bool sharpMidLine = false; if (hasTextPath) { @@ -541,13 +539,13 @@ For the start (end) value, the text is rendered from the start (end) of the line the path is reached. */ CGFloat absoluteStartOffset = [RNSVGPropHelper fromRelative:textPath.startOffset - relative:_pathLength + relative:measure.pathLength fontSize:fontSize]; offset += absoluteStartOffset; - if (isClosed) { - CGFloat halfPathDistance = _pathLength / 2; + if (measure.isClosed) { + CGFloat halfPathDistance = measure.pathLength / 2; startOfRendering = absoluteStartOffset + (textAnchor == RNSVGTextAnchorMiddle ? -halfPathDistance : 0); - endOfRendering = startOfRendering + _pathLength; + endOfRendering = startOfRendering + measure.pathLength; } /* RNSVGTextPathSpacing spacing = textPath.getSpacing(); @@ -958,39 +956,10 @@ A negative value is an error (see Error processing). continue; } - // Investigation suggests binary search is faster at lineCount >= 16 - // https://gist.github.com/msand/4c7993319425f9d7933be58ad9ada1a4 - NSUInteger i = lineCount < 16 ? - [lengths - indexOfObjectPassingTest:^(NSNumber* length, NSUInteger index, BOOL * _Nonnull stop) { - BOOL contains = midPoint <= [length doubleValue]; - return contains; - }] - : - [lengths - indexOfObject:[NSNumber numberWithDouble:midPoint] - inSortedRange:NSMakeRange(0, lineCount) - options:NSBinarySearchingInsertionIndex - usingComparator:^(NSNumber* obj1, NSNumber* obj2) { - return [obj1 compare:obj2]; - }]; - - CGFloat totalLength = (CGFloat)[lengths[i] doubleValue]; - CGFloat prevLength = i == 0 ? 0 : (CGFloat)[lengths[i - 1] doubleValue]; - - CGFloat length = totalLength - prevLength; - CGFloat percent = (midPoint - prevLength) / length; - - NSArray * points = [lines objectAtIndex: i]; - CGPoint p1 = [[points objectAtIndex: 0] CGPointValue]; - CGPoint p2 = [[points objectAtIndex: 1] CGPointValue]; - - CGFloat ldx = p2.x - p1.x; - CGFloat ldy = p2.y - p1.y; - CGFloat angle = atan2(ldy, ldx); - - CGFloat px = p1.x + ldx * percent; - CGFloat py = p1.y + ldy * percent; + CGFloat angle; + CGFloat px; + CGFloat py; + [measure getPosAndTan:&angle midPoint:midPoint px:&px py:&py]; transform = CGAffineTransformConcat(CGAffineTransformMakeTranslation(px, py), transform); transform = CGAffineTransformConcat(CGAffineTransformMakeRotation(angle + r), transform); @@ -1061,24 +1030,18 @@ + (CGFloat)getTextAnchorOffset:(RNSVGTextAnchor)textAnchor width:(CGFloat) width - (void)setupTextPath:(CGContextRef)context { - lines = nil; - lengths = nil; textPath = nil; RNSVGText *parent = (RNSVGText*)[self superview]; - while (parent) { if ([parent class] == [RNSVGTextPath class]) { textPath = (RNSVGTextPath*) parent; - [textPath getPathLength:&_pathLength - lineCount:&lineCount - lengths:&lengths - lines:&lines - isClosed:&isClosed]; + RNSVGNode *template = [self.svgView getDefinedTemplate:textPath.href]; + CGPathRef path = [template getPath:nil]; + [measure extractPathData:path]; break; } else if (![parent isKindOfClass:[RNSVGText class]]) { break; } - parent = (RNSVGText*)[parent superview]; } } diff --git a/ios/Text/RNSVGTextPath.h b/ios/Text/RNSVGTextPath.h index 4ffd323eb..820f575e8 100644 --- a/ios/Text/RNSVGTextPath.h +++ b/ios/Text/RNSVGTextPath.h @@ -20,7 +20,4 @@ @property (nonatomic, strong) NSString *spacing; @property (nonatomic, strong) RNSVGLength *startOffset; -- (void)getPathLength:(CGFloat*)length lineCount:(NSUInteger*)lineCount lengths:(NSArray* __strong *)lengths lines:(NSArray* __strong *)lines isClosed:(BOOL*)isClosed; - - @end diff --git a/ios/Text/RNSVGTextPath.m b/ios/Text/RNSVGTextPath.m index 5005423ed..b9d1db477 100644 --- a/ios/Text/RNSVGTextPath.m +++ b/ios/Text/RNSVGTextPath.m @@ -8,109 +8,8 @@ #import "RNSVGTextPath.h" -#import "RNSVGBezierElement.h" - -/* Some 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 = (CGFloat).01; - -/** - * returns the distance between two points - */ -CGFloat RNSVGPerformanceBezier_distance(CGPoint p1, CGPoint p2) -{ - CGFloat dx = p2.x - p1.x; - CGFloat dy = p2.y - p1.y; - - return hypot(dx, dy); -} - -// 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 RNSVGPerformanceBezier_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 RNSVGPerformanceBezier_addLine(CGPoint *last, const CGPoint *next, NSMutableArray *lines, CGFloat *length, NSMutableArray *lengths) { - NSArray *line = @[[NSValue valueWithCGPoint:*last], [NSValue valueWithCGPoint:*next]]; - [lines addObject:line]; - *length += RNSVGPerformanceBezier_distance(*last, *next); - [lengths addObject:[NSNumber numberWithDouble:*length]]; - *last = *next; -} @implementation RNSVGTextPath -{ - CGPathRef _path; - NSMutableArray *lengths; - NSMutableArray *lines; - NSUInteger lineCount; - CGFloat length; - BOOL isClosed; -} - (void)setHref:(NSString *)href { @@ -166,102 +65,6 @@ - (void)setStartOffset:(RNSVGLength *)startOffset _startOffset = startOffset; } -- (void)getPathLength:(CGFloat*)lengthP lineCount:(NSUInteger*)lineCountP lengths:(NSArray* __strong *)lengthsP lines:(NSArray* __strong *)linesP isClosed:(BOOL*)isClosedP -{ - RNSVGNode *template = [self.svgView getDefinedTemplate:self.href]; - CGPathRef path = [template getPath:nil]; - - if (_path != path) { - _path = path; - 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 = [RNSVGBezierElement elementsFromCGPath:path]; - for (RNSVGBezierElement *element in elements) { - switch (element.elementType) - { - case kCGPathElementMoveToPoint: - origin = last = element.point; - break; - - case kCGPathElementAddLineToPoint: { - CGPoint next = element.point; - RNSVGPerformanceBezier_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 segment between the start and end points - CGPoint ctrl1 = bez[1]; - CGPoint ctrl2 = bez[2]; - CGPoint next = bez[3]; - CGFloat polyLen = - RNSVGPerformanceBezier_distance(last, ctrl1) + - RNSVGPerformanceBezier_distance(ctrl1, ctrl2) + - RNSVGPerformanceBezier_distance(ctrl2, next); - CGFloat chordLen = RNSVGPerformanceBezier_distance(last, next); - CGFloat error = polyLen - chordLen; - - // if the error is less than our accepted level of error - // then add a line, else, split the curve in half - if (error <= idealFlatness) { - RNSVGPerformanceBezier_addLine(&last, &next, lines, &length, lengths); - lineCount++; - } else { - CGPoint bez1[4], bez2[4]; - RNSVGPerformanceBezier_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; - } - } - break; - } - - case kCGPathElementCloseSubpath: { - CGPoint next = origin; - RNSVGPerformanceBezier_addLine(&last, &next, lines, &length, lengths); - lineCount++; - isClosed = YES; - break; - } - - default: - break; - } - } - } - - *lineCountP = lineCount; - *isClosedP = isClosed; - *lengthsP = lengths; - *lengthP = length; - *linesP = lines; -} - - (void)renderLayerTo:(CGContextRef)context rect:(CGRect)rect { [self renderGroupTo:context rect:rect]; diff --git a/ios/Utils/RNSVGPathMeasure.h b/ios/Utils/RNSVGPathMeasure.h new file mode 100644 index 000000000..ed0f9fe4a --- /dev/null +++ b/ios/Utils/RNSVGPathMeasure.h @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2015-present, react-native-community. + * 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. + */ + +#import + +@interface RNSVGPathMeasure : NSObject + +@property CGFloat pathLength; +@property CGPathRef textPath; +@property NSMutableArray *lengths; +@property NSMutableArray *lines; +@property NSUInteger lineCount; +@property BOOL isClosed; + +- (void)extractPathData:(CGPathRef)path; +- (void)getPosAndTan:(CGFloat *)angle midPoint:(CGFloat)midPoint px:(CGFloat *)px py:(CGFloat *)py; + +@end diff --git a/ios/Utils/RNSVGPathMeasure.m b/ios/Utils/RNSVGPathMeasure.m new file mode 100644 index 000000000..491182cad --- /dev/null +++ b/ios/Utils/RNSVGPathMeasure.m @@ -0,0 +1,214 @@ +#import "RNSVGPathMeasure.h" +#import "RNSVGBezierElement.h" + +/* Some 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 = (CGFloat).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 hypot(dx, dy); +} + +// 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; +} + +@implementation RNSVGPathMeasure + +- (void)addLine:(CGPoint *)last next:(const CGPoint *)next { + NSArray *line = @[[NSValue valueWithCGPoint:*last], [NSValue valueWithCGPoint:*next]]; + _pathLength += distance(*last, *next); + [_lengths addObject:[NSNumber numberWithDouble:_pathLength]]; + [_lines addObject:line]; + *last = *next; +} + +- (void)extractPathData:(CGPathRef)path { + CGPoint origin = CGPointMake (0.0, 0.0); + CGPoint last = CGPointMake (0.0, 0.0); + _lengths = [NSMutableArray array]; + _lines = [NSMutableArray array]; + _isClosed = NO; + _lineCount = 0; + _pathLength = 0; + NSArray *elements = [RNSVGBezierElement elementsFromCGPath:path]; + for (RNSVGBezierElement *element in elements) { + switch (element.elementType) + { + case kCGPathElementMoveToPoint: + origin = last = element.point; + break; + + case kCGPathElementAddLineToPoint: { + CGPoint next = element.point; + [self addLine:&last next:&next]; + _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 segment between the start and end points + CGPoint ctrl1 = bez[1]; + CGPoint ctrl2 = bez[2]; + CGPoint next = bez[3]; + CGFloat polyLen = + distance(last, ctrl1) + + distance(ctrl1, ctrl2) + + distance(ctrl2, next); + CGFloat chordLen = distance(last, next); + CGFloat error = polyLen - chordLen; + + // if the error is less than our accepted level of error + // then add a line, else, split the curve in half + if (error <= idealFlatness) { + [self addLine:&last next:&next]; + _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; + } + } + break; + } + + case kCGPathElementCloseSubpath: { + CGPoint next = origin; + [self addLine:&last next:&next]; + _lineCount++; + _isClosed = YES; + break; + } + + default: + break; + } + } +} + +- (void)getPosAndTan:(CGFloat *)angle midPoint:(CGFloat)midPoint px:(CGFloat *)px py:(CGFloat *)py { + // Investigation suggests binary search is faster at lineCount >= 16 + // https://gist.github.com/msand/4c7993319425f9d7933be58ad9ada1a4 + NSUInteger i = _lineCount < 16 ? + [_lengths + indexOfObjectPassingTest:^(NSNumber* length, NSUInteger index, BOOL * _Nonnull stop) { + BOOL contains = midPoint <= [length doubleValue]; + return contains; + }] + : + [_lengths + indexOfObject:[NSNumber numberWithDouble:midPoint] + inSortedRange:NSMakeRange(0, _lineCount) + options:NSBinarySearchingInsertionIndex + usingComparator:^(NSNumber* obj1, NSNumber* obj2) { + return [obj1 compare:obj2]; + }]; + + CGFloat totalLength = (CGFloat)[_lengths[i] doubleValue]; + CGFloat prevLength = i == 0 ? 0 : (CGFloat)[_lengths[i - 1] doubleValue]; + + CGFloat length = totalLength - prevLength; + CGFloat percent = (midPoint - prevLength) / length; + + NSArray * points = [_lines objectAtIndex: i]; + CGPoint p1 = [[points objectAtIndex: 0] CGPointValue]; + CGPoint p2 = [[points objectAtIndex: 1] CGPointValue]; + + CGFloat ldx = p2.x - p1.x; + CGFloat ldy = p2.y - p1.y; + *angle = atan2(ldy, ldx); + *px = p1.x + ldx * percent; + *py = p1.y + ldy * percent; +} + +@end diff --git a/ios/ViewManagers/RNSVGRenderableManager.m b/ios/ViewManagers/RNSVGRenderableManager.m index 221856174..538352dfe 100644 --- a/ios/ViewManagers/RNSVGRenderableManager.m +++ b/ios/ViewManagers/RNSVGRenderableManager.m @@ -10,6 +10,7 @@ #import #import #import "RNSVGRenderableManager.h" +#import "RNSVGPathMeasure.h" #import "RCTConvert+RNSVG.h" #import "RNSVGCGFCRule.h" @@ -37,29 +38,27 @@ - (RNSVGRenderable *)node RCT_EXPORT_VIEW_PROPERTY(vectorEffect, int) RCT_EXPORT_VIEW_PROPERTY(propList, NSArray) -- (void)isPointInFill:(nonnull NSNumber *)reactTag point:(CGPoint)point callback:(RCTResponseSenderBlock)callback attempt:(int)attempt { +typedef void (^RNSVGSuccessBlock)(RNSVGRenderable *view); +typedef void (^RNSVGFailBlock)(void); + +- (void)withTag:(nonnull NSNumber *)reactTag success:(RNSVGSuccessBlock)successBlock fail:(RNSVGFailBlock)failBlock attempt:(int)attempt { [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { __kindof UIView *view = viewRegistry[reactTag]; - UIView *target; if (!view) { if (attempt < 1) { void (^retryBlock)(void) = ^{ - [self isPointInFill:reactTag point:point callback:callback attempt:(attempt + 1)]; + [self withTag:reactTag success:successBlock fail:failBlock attempt:(attempt + 1)]; }; RCTExecuteOnUIManagerQueue(retryBlock); } else { - callback(@[[NSNumber numberWithBool:false]]); + failBlock(); } - return; - } - if ([view isKindOfClass:[RNSVGRenderable class]]) { + } else if ([view isKindOfClass:[RNSVGRenderable class]]) { RNSVGRenderable *svg = view; - target = [svg hitTest:point withEvent:nil]; - BOOL hit = target != nil; - callback(@[[NSNumber numberWithBool:hit]]); + successBlock(svg); } else { RCTLogError(@"Invalid svg returned from registry, expecting RNSVGRenderable, got: %@", view); - callback(@[[NSNumber numberWithBool:false]]); + failBlock(); } }]; } @@ -82,8 +81,57 @@ - (void)isPointInFill:(nonnull NSNumber *)reactTag point:(CGPoint)point callback CGFloat x = (CGFloat)[xo floatValue]; CGFloat y = (CGFloat)[yo floatValue]; CGPoint point = CGPointMake(x, y); - [self isPointInFill:reactTag point:point callback:callback attempt:0]; + [self + withTag:reactTag + success:^(RNSVGRenderable *svg){ + UIView *target = [svg hitTest:point withEvent:nil]; + BOOL hit = target != nil; + callback(@[[NSNumber numberWithBool:hit]]); + } + fail:^{ + callback(@[[NSNumber numberWithBool:false]]); + } + attempt:0]; +} + +RCT_EXPORT_METHOD(getTotalLength:(nonnull NSNumber *)reactTag callback:(RCTResponseSenderBlock)callback) +{ + [self + withTag:reactTag + success:^(RNSVGRenderable *svg){ + CGPathRef target = [svg getPath:nil]; + RNSVGPathMeasure *measure = [RNSVGPathMeasure init]; + [measure extractPathData:target]; + + CGFloat pathLegth = measure.pathLength; + callback(@[[NSNumber numberWithDouble:pathLegth]]); + } + fail:^{ + callback(@[[NSNumber numberWithBool:false]]); + } + attempt:0]; } +RCT_EXPORT_METHOD(getPointAtLength:(nonnull NSNumber *)reactTag options:(NSDictionary *)options callback:(RCTResponseSenderBlock)callback) +{ + id length = [options objectForKey:@"length"]; + CGFloat x = (CGFloat)[length floatValue]; + [self + withTag:reactTag + success:^(RNSVGRenderable *svg){ + CGPathRef target = [svg getPath:nil]; + RNSVGPathMeasure *measure = [[RNSVGPathMeasure alloc]init]; + [measure extractPathData:target]; + CGFloat angle; + CGFloat px; + CGFloat py; + [measure getPosAndTan:&angle midPoint:fmax(0, fmin(measure.pathLength, x)) px:&px py:&py]; + callback(@[[NSNumber numberWithDouble:px], [NSNumber numberWithDouble:py]]); + } + fail:^{ + callback(@[[NSNumber numberWithBool:false]]); + } + attempt:0]; +} @end