mirror of
https://github.com/zoriya/react-native-svg.git
synced 2025-12-06 07:06:11 +00:00
feat(ios): implement getTotalLength and getPointAtLength
This commit is contained in:
@@ -52,7 +52,6 @@ class RNSVGRenderableManager extends ReactContextBaseJavaModule {
|
||||
} else {
|
||||
successCallback.invoke(false);
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
float scale = svg.mScale;
|
||||
src[0] *= scale;
|
||||
|
||||
@@ -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 = "<group>"; };
|
||||
9482DEF823460EC700FC486E /* RNSVGContextBrush.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNSVGContextBrush.m; sourceTree = "<group>"; };
|
||||
9482DEF923460EC800FC486E /* RNSVGContextBrush.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNSVGContextBrush.h; sourceTree = "<group>"; };
|
||||
9482DF00234680A200FC486E /* RNSVGPathMeasure.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RNSVGPathMeasure.m; path = Utils/RNSVGPathMeasure.m; sourceTree = "<group>"; };
|
||||
9482DF01234680A200FC486E /* RNSVGPathMeasure.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSVGPathMeasure.h; path = Utils/RNSVGPathMeasure.h; sourceTree = "<group>"; };
|
||||
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 */,
|
||||
|
||||
@@ -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 @@ static CGFloat RNSVGTSpan_radToDeg = 180 / (CGFloat)M_PI;
|
||||
@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 @@ static CGFloat RNSVGTSpan_radToDeg = 180 / (CGFloat)M_PI;
|
||||
|
||||
emoji = [NSMutableArray arrayWithCapacity:0];
|
||||
emojiTransform = [NSMutableArray arrayWithCapacity:0];
|
||||
measure = [[RNSVGPathMeasure alloc]init];
|
||||
|
||||
return self;
|
||||
}
|
||||
@@ -476,7 +474,7 @@ TopAlignedLabel *label;
|
||||
|
||||
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 @@ TopAlignedLabel *label;
|
||||
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 @@ TopAlignedLabel *label;
|
||||
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 @@ TopAlignedLabel *label;
|
||||
|
||||
- (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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,109 +8,8 @@
|
||||
|
||||
|
||||
#import "RNSVGTextPath.h"
|
||||
#import "RNSVGBezierElement.h"
|
||||
|
||||
/* Some Bezier logic from PerformanceBezier */
|
||||
/*
|
||||
|
||||
## License
|
||||
|
||||
<a rel="license" href="http://creativecommons.org/licenses/by/3.0/us/"><img alt="Creative Commons License" style="border-width:0" src="https://i.creativecommons.org/l/by/3.0/us/88x31.png" /></a><br />This work is licensed under a <a rel="license" href="http://creativecommons.org/licenses/by/3.0/us/">Creative Commons Attribution 3.0 United States License</a>.
|
||||
|
||||
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 RNSVGPerformanceBezier_addLine(CGPoint *last, const CGPoint *next, NSMutabl
|
||||
_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];
|
||||
|
||||
23
ios/Utils/RNSVGPathMeasure.h
Normal file
23
ios/Utils/RNSVGPathMeasure.h
Normal file
@@ -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 <UIKit/UIKit.h>
|
||||
|
||||
@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
|
||||
214
ios/Utils/RNSVGPathMeasure.m
Normal file
214
ios/Utils/RNSVGPathMeasure.m
Normal file
@@ -0,0 +1,214 @@
|
||||
#import "RNSVGPathMeasure.h"
|
||||
#import "RNSVGBezierElement.h"
|
||||
|
||||
/* Some Bezier logic from PerformanceBezier */
|
||||
/*
|
||||
|
||||
## License
|
||||
|
||||
<a rel="license" href="http://creativecommons.org/licenses/by/3.0/us/"><img alt="Creative Commons License" style="border-width:0" src="https://i.creativecommons.org/l/by/3.0/us/88x31.png" /></a><br />This work is licensed under a <a rel="license" href="http://creativecommons.org/licenses/by/3.0/us/">Creative Commons Attribution 3.0 United States License</a>.
|
||||
|
||||
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
|
||||
@@ -10,6 +10,7 @@
|
||||
#import <React/RCTUIManager.h>
|
||||
#import <React/RCTUIManagerUtils.h>
|
||||
#import "RNSVGRenderableManager.h"
|
||||
#import "RNSVGPathMeasure.h"
|
||||
|
||||
#import "RCTConvert+RNSVG.h"
|
||||
#import "RNSVGCGFCRule.h"
|
||||
@@ -37,29 +38,27 @@ RCT_EXPORT_VIEW_PROPERTY(strokeMiterlimit, CGFloat)
|
||||
RCT_EXPORT_VIEW_PROPERTY(vectorEffect, int)
|
||||
RCT_EXPORT_VIEW_PROPERTY(propList, NSArray<NSString *>)
|
||||
|
||||
- (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<NSNumber *,UIView *> *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 @@ RCT_EXPORT_METHOD(isPointInFill:(nonnull NSNumber *)reactTag options:(NSDictiona
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user