2017-08-16 14:10:01 +08:00

412 lines
12 KiB
Objective-C

//
// LOTBezierPath.m
// Lottie
//
// Created by brandon_withrow on 7/20/17.
// Copyright © 2017 Airbnb. All rights reserved.
//
#import "LOTBezierPath.h"
#import "CGGeometry+LOTAdditions.h"
typedef struct LOT_Subpath LOT_Subpath;
struct LOT_Subpath {
CGPathElementType type;
CGFloat length;
CGPoint endPoint;
CGPoint controlPoint1;
CGPoint controlPoint2;
LOT_Subpath *nextSubpath;
};
@interface LOTBezierPath ()
@property (nonatomic, readonly) LOT_Subpath *headSubpath;
@end
@implementation LOTBezierPath {
LOT_Subpath *headSubpath_;
LOT_Subpath *tailSubpath_;
CGPoint subpathStartPoint_;
CGFloat *_lineDashPattern;
NSInteger _lineDashCount;
CGFloat _lineDashPhase;
CGMutablePathRef _path;
}
// MARK - Lifecycle
+ (instancetype)newPath {
return [[LOTBezierPath alloc] init];
}
- (instancetype)init
{
self = [super init];
if (self) {
_length = 0;
headSubpath_ = NULL;
tailSubpath_ = NULL;
_path = CGPathCreateMutable();
_lineWidth = 1;
_lineCapStyle = kCGLineCapButt;
_lineJoinStyle = kCGLineJoinMiter;
_miterLimit = 10;
_flatness = 0.6;
_usesEvenOddFillRule = NO;
_lineDashPattern = NULL;
_lineDashCount = 0;
_lineDashPhase = 0;
_cacheLengths = NO;
}
return self;
}
- (void)dealloc {
[self removeAllSubpaths];
if (_path) CGPathRelease(_path);
}
- (id)copyWithZone:(NSZone *)zone {
LOTBezierPath *copy = [[self class] new];
copy.cacheLengths = self.cacheLengths;
copy.lineWidth = self.lineWidth;
copy.lineCapStyle = self.lineCapStyle;
copy.lineJoinStyle = self.lineJoinStyle;
copy.miterLimit = self.miterLimit;
copy.flatness = self.flatness;
copy.usesEvenOddFillRule = self.usesEvenOddFillRule;
[copy LOT_appendPath:self];
return copy;
}
// MARK - Subpaths List
- (void)removeAllSubpaths {
LOT_Subpath *node = headSubpath_;
while (node) {
LOT_Subpath *nextNode = node->nextSubpath;
node->nextSubpath = NULL;
free(node);
node = nextNode;
}
headSubpath_ = NULL;
tailSubpath_ = NULL;
}
- (void)addSubpathWithType:(CGPathElementType)type
length:(CGFloat)length
endPoint:(CGPoint)endPoint
controlPoint1:(CGPoint)controlPoint1
controlPoint1:(CGPoint)controlPoint2 {
LOT_Subpath *subPath = (LOT_Subpath *)malloc(sizeof(LOT_Subpath));
subPath->type = type;
subPath->length = length;
subPath->endPoint = endPoint;
subPath->controlPoint1 = controlPoint1;
subPath->controlPoint2 = controlPoint2;
subPath->nextSubpath = NULL;
if (tailSubpath_ == NULL) {
headSubpath_ = subPath;
tailSubpath_ = subPath;
} else {
tailSubpath_->nextSubpath = subPath;
tailSubpath_ = subPath;
}
}
// MARK Getters Setters
- (CGPoint)currentPoint {
CGPoint previousPoint = tailSubpath_ ? tailSubpath_->endPoint : CGPointZero;
return previousPoint;
}
- (CGPathRef)CGPath {
return _path;
}
- (LOT_Subpath *)headSubpath {
return headSubpath_;
}
// MARK - External
- (void)LOT_moveToPoint:(CGPoint)point {
subpathStartPoint_ = point;
[self addSubpathWithType:kCGPathElementMoveToPoint length:0 endPoint:point controlPoint1:CGPointZero controlPoint1:CGPointZero];
CGPathMoveToPoint(_path, NULL, point.x, point.y);
}
- (void)LOT_addLineToPoint:(CGPoint)point {
CGFloat length = 0;
if (_cacheLengths) {
length = LOT_PointDistanceFromPoint(self.currentPoint, point);
_length = _length + length;
}
[self addSubpathWithType:kCGPathElementAddLineToPoint length:length endPoint:point controlPoint1:CGPointZero controlPoint1:CGPointZero];
CGPathAddLineToPoint(_path, NULL, point.x, point.y);
}
- (void)LOT_addCurveToPoint:(CGPoint)point
controlPoint1:(CGPoint)cp1
controlPoint2:(CGPoint)cp2 {
CGFloat length = 0;
if (_cacheLengths) {
length = LOT_CubicLengthWithPrecision(self.currentPoint, point, cp1, cp2, 5);
_length = _length + length;
}
[self addSubpathWithType:kCGPathElementAddCurveToPoint length:length endPoint:point controlPoint1:cp1 controlPoint1:cp2];
CGPathAddCurveToPoint(_path, NULL, cp1.x, cp1.y, cp2.x, cp2.y, point.x, point.y);
}
- (void)LOT_closePath {
CGFloat length = 0;
if (_cacheLengths) {
length = LOT_PointDistanceFromPoint(self.currentPoint, subpathStartPoint_);
_length = _length + length;
}
[self addSubpathWithType:kCGPathElementCloseSubpath length:length endPoint:subpathStartPoint_ controlPoint1:CGPointZero controlPoint1:CGPointZero];
CGPathCloseSubpath(_path);
}
- (void)_clearPathData {
_length = 0;
subpathStartPoint_ = CGPointZero;
CGPathRelease(_path);
_path = CGPathCreateMutable();
}
- (void)LOT_removeAllPoints {
[self removeAllSubpaths];
[self _clearPathData];
}
- (BOOL)containsPoint:(CGPoint)point {
return CGPathContainsPoint(_path, NULL, point, _usesEvenOddFillRule);
}
- (BOOL)isEmpty {
return CGPathIsEmpty(_path);
}
- (CGRect)bounds {
return CGPathGetBoundingBox(_path);
}
- (void)LOT_applyTransform:(CGAffineTransform)transform {
CGMutablePathRef mutablePath = CGPathCreateMutable();
CGPathAddPath(mutablePath, &transform, _path);
CGPathRelease(_path);
_path = mutablePath;
}
- (void)LOT_appendPath:(LOTBezierPath *)bezierPath {
CGPathAddPath(_path, NULL, bezierPath.CGPath);
LOT_Subpath *nextSubpath = bezierPath.headSubpath;
while (nextSubpath) {
CGFloat length = 0;
if (self.cacheLengths) {
if (bezierPath.cacheLengths) {
length = nextSubpath->length;
} else {
// No previous length data, measure.
if (nextSubpath->type == kCGPathElementAddLineToPoint) {
length = LOT_PointDistanceFromPoint(self.currentPoint, nextSubpath->endPoint);
} else if (nextSubpath->type == kCGPathElementAddCurveToPoint) {
length = LOT_CubicLengthWithPrecision(self.currentPoint, nextSubpath->endPoint, nextSubpath->controlPoint1, nextSubpath->controlPoint2, 5);
} else if (nextSubpath->type == kCGPathElementCloseSubpath) {
length = LOT_PointDistanceFromPoint(self.currentPoint, nextSubpath->endPoint);
}
}
}
_length = _length + length;
[self addSubpathWithType:nextSubpath->type
length:length
endPoint:nextSubpath->endPoint
controlPoint1:nextSubpath->controlPoint1
controlPoint1:nextSubpath->controlPoint2];
nextSubpath = nextSubpath->nextSubpath;
}
}
- (void)trimPathFromT:(CGFloat)fromT toT:(CGFloat)toT offset:(CGFloat)offset {
if (fromT > toT) {
CGFloat to = fromT;
fromT = toT;
toT = to;
}
offset = offset - floor(offset);
CGFloat fromLength = fromT + offset;
CGFloat toLength = toT + offset;
if (toT - fromT == 1) {
// Do Nothing, Full Path returned.
return;
}
if (fromLength == toLength) {
// Empty Path
[self LOT_removeAllPoints];
return;
}
if (fromLength >= 1) {
fromLength = fromLength - floor(fromLength);
}
if (toLength > 1) {
toLength = toLength - floor(toLength);
}
if (fromLength == 0 &&
toLength == 1) {
// Do Nothing. Full Path returned.
return;
}
if (fromLength == toLength) {
// Empty Path
[self LOT_removeAllPoints];
return;
}
CGFloat totalLength = _length;
[self _clearPathData];
LOT_Subpath *subpath = headSubpath_;
headSubpath_ = NULL;
tailSubpath_ = NULL;
fromLength = fromLength * totalLength;
toLength = toLength * totalLength;
CGFloat currentStartLength = fromLength < toLength ? fromLength : 0;
CGFloat currentEndLength = toLength;
CGFloat subpathBeginningLength = 0;
CGPoint currentPoint = CGPointZero;
while (subpath) {
CGFloat pathLength = subpath->length;
if (!_cacheLengths) {
if (subpath->type == kCGPathElementAddLineToPoint) {
pathLength = LOT_PointDistanceFromPoint(currentPoint, subpath->endPoint);
} else if (subpath->type == kCGPathElementAddCurveToPoint) {
pathLength = LOT_CubicLengthWithPrecision(currentPoint, subpath->endPoint, subpath->controlPoint1, subpath->controlPoint2, 5);
} else if (subpath->type == kCGPathElementCloseSubpath) {
pathLength = LOT_PointDistanceFromPoint(currentPoint, subpath->endPoint);
}
}
CGFloat subpathEndLength = subpathBeginningLength + pathLength;
if (subpath->type != kCGPathElementMoveToPoint &&
subpathEndLength > currentStartLength) {
// The end of this path overlaps the current drawing region
// x x x x
// ---------------ooooooooooooooooooooooooooooooooooooooooooooooooo-------------------
// Start |currentStartLength currentEndLength| End
CGFloat currentSpanStartT = LOT_RemapValue(currentStartLength, subpathBeginningLength, subpathEndLength, 0, 1);
CGFloat currentSpanEndT = LOT_RemapValue(currentEndLength, subpathBeginningLength, subpathEndLength, 0, 1);
// At this point currentSpan start and end T can be less than 0 or greater than 1
if (subpath->type == kCGPathElementAddLineToPoint) {
if (currentSpanStartT >= 0) {
// The current drawable span either starts with this subpath or along this subpath.
// If this is the middle of a segment then currentSpanStartT would be less than 0
if (currentSpanStartT > 0) {
currentPoint = LOT_PointInLine(currentPoint, subpath->endPoint, currentSpanStartT);
}
[self LOT_moveToPoint:currentPoint];
// Now we are ready to draw a line
}
CGPoint toPoint = subpath->endPoint;
if (currentSpanEndT < 1) {
// The end of the span is inside of the current subpath. Find it.
toPoint = LOT_PointInLine(currentPoint, subpath->endPoint, currentSpanEndT);
}
[self LOT_addLineToPoint:toPoint];
currentPoint = toPoint;
} else if (subpath->type == kCGPathElementAddCurveToPoint) {
CGPoint cp1, cp2, end;
cp1 = subpath->controlPoint1;
cp2 = subpath->controlPoint2;
end = subpath->endPoint;
if (currentSpanStartT >= 0) {
// The current drawable span either starts with this subpath or along this subpath.
// If this is the middle of a segment then currentSpanStartT would be less than 0
// Beginning of a segment Move start point and calculate cp1 and 2 is necessary
if (currentSpanStartT > 0) {
CGPoint A = LOT_PointInLine(currentPoint, cp1, currentSpanStartT);
CGPoint B = LOT_PointInLine(cp1, cp2, currentSpanStartT);
CGPoint C = LOT_PointInLine(cp2, end, currentSpanStartT);
CGPoint D = LOT_PointInLine(A, B, currentSpanStartT);
CGPoint E = LOT_PointInLine(B, C, currentSpanStartT);
CGPoint F = LOT_PointInLine(D, E, currentSpanStartT);
currentPoint = F;
cp1 = E;
cp2 = C;
currentSpanEndT = LOT_RemapValue(currentSpanEndT, currentSpanStartT, 1, 0, 1);
}
[self LOT_moveToPoint:currentPoint];
}
if (currentSpanEndT < 1) {
CGPoint A = LOT_PointInLine(currentPoint, cp1, currentSpanEndT);
CGPoint B = LOT_PointInLine(cp1, cp2, currentSpanEndT);
CGPoint C = LOT_PointInLine(cp2, end, currentSpanEndT);
CGPoint D = LOT_PointInLine(A, B, currentSpanEndT);
CGPoint E = LOT_PointInLine(B, C, currentSpanEndT);
CGPoint F = LOT_PointInLine(D, E, currentSpanEndT);
cp1 = A;
cp2 = D;
end = F;
}
[self LOT_addCurveToPoint:end controlPoint1:cp1 controlPoint2:cp2];
}
if (currentSpanEndT <= 1) {
// We have possibly reached the end.
// Current From and To will possibly need to be reset.
if (fromLength < toLength) {
while (subpath) {
LOT_Subpath *nextNode = subpath->nextSubpath;
subpath->nextSubpath = NULL;
free(subpath);
subpath = nextNode;
}
break;
} else {
currentStartLength = fromLength;
currentEndLength = totalLength;
}
}
}
currentPoint = subpath->endPoint;
subpathBeginningLength = subpathEndLength;
LOT_Subpath *nextNode = subpath->nextSubpath;
subpath->nextSubpath = NULL;
free(subpath);
subpath = nextNode;
}
}
@end