You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
227 lines
7.6 KiB
227 lines
7.6 KiB
#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
|
|
*/
|
|
static 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.
|
|
*
|
|
*/
|
|
static 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)reset
|
|
{
|
|
_lengths = nil;
|
|
_lines = nil;
|
|
_isClosed = NO;
|
|
_lineCount = 0;
|
|
_pathLength = 0;
|
|
_path = nil;
|
|
}
|
|
|
|
- (void)extractPathData:(CGPathRef)path
|
|
{
|
|
if (path == _path) {
|
|
return;
|
|
}
|
|
_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;
|
|
_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 x:(CGFloat *)x y:(CGFloat *)y
|
|
{
|
|
// 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);
|
|
*x = p1.x + ldx * percent;
|
|
*y = p1.y + ldy * percent;
|
|
}
|
|
|
|
@end
|