148 lines
4.5 KiB
Mathematica
148 lines
4.5 KiB
Mathematica
|
/**
|
||
|
* Copyright (c) 2015-present, Horcrux.
|
||
|
* 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.
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* based on CurvyText by iosptl: https://github.com/iosptl/ios7ptl/blob/master/ch21-Text/CurvyText/CurvyText/CurvyTextView.m
|
||
|
*/
|
||
|
|
||
|
#import "RNSVGBezierTransformer.h"
|
||
|
|
||
|
@implementation RNSVGBezierTransformer
|
||
|
{
|
||
|
NSArray<NSArray *> *_bezierCurves;
|
||
|
int _currentBezierIndex;
|
||
|
CGFloat _startOffset;
|
||
|
CGFloat _lastOffset;
|
||
|
CGFloat _lastRecord;
|
||
|
CGFloat _lastDistance;
|
||
|
CGPoint _lastPoint;
|
||
|
CGPoint _P0;
|
||
|
CGPoint _P1;
|
||
|
CGPoint _P2;
|
||
|
CGPoint _P3;
|
||
|
BOOL _reachedEnd;
|
||
|
BOOL _reachedStart;
|
||
|
}
|
||
|
|
||
|
- (instancetype)initWithBezierCurvesAndStartOffset:(NSArray<NSArray *> *)bezierCurves startOffset:(CGFloat)startOffset
|
||
|
{
|
||
|
if (self = [super init]) {
|
||
|
_bezierCurves = bezierCurves;
|
||
|
_currentBezierIndex = _lastOffset = _lastRecord = _lastDistance = 0;
|
||
|
_startOffset = startOffset;
|
||
|
[self setControlPoints];
|
||
|
}
|
||
|
return self;
|
||
|
}
|
||
|
|
||
|
static CGFloat calculateBezier(CGFloat t, CGFloat P0, CGFloat P1, CGFloat P2, CGFloat P3) {
|
||
|
return (1-t)*(1-t)*(1-t)*P0+3*(1-t)*(1-t)*t*P1+3*(1-t)*t*t*P2+t*t*t*P3;
|
||
|
}
|
||
|
|
||
|
- (CGPoint)pointAtOffset:(CGFloat)t {
|
||
|
CGFloat x = calculateBezier(t, _P0.x, _P1.x, _P2.x, _P3.x);
|
||
|
CGFloat y = calculateBezier(t, _P0.y, _P1.y, _P2.y, _P3.y);
|
||
|
return CGPointMake(x, y);
|
||
|
}
|
||
|
|
||
|
static CGFloat calculateBezierPrime(CGFloat t, CGFloat P0, CGFloat P1, CGFloat P2, CGFloat P3) {
|
||
|
return -3*(1-t)*(1-t)*P0+(3*(1-t)*(1-t)*P1)-(6*t*(1-t)*P1)-(3*t*t*P2)+(6*t*(1-t)*P2)+3*t*t*P3;
|
||
|
}
|
||
|
|
||
|
- (CGFloat)angleAtOffset:(CGFloat)t {
|
||
|
CGFloat dx = calculateBezierPrime(t, _P0.x, _P1.x, _P2.x, _P3.x);
|
||
|
CGFloat dy = calculateBezierPrime(t, _P0.y, _P1.y, _P2.y, _P3.y);
|
||
|
return atan2(dy, dx);
|
||
|
}
|
||
|
|
||
|
static CGFloat calculateDistance(CGPoint a, CGPoint b) {
|
||
|
return hypot(a.x - b.x, a.y - b.y);
|
||
|
}
|
||
|
|
||
|
// Simplistic routine to find the offset along Bezier that is
|
||
|
// `distance` away from `point`. `offset` is the offset used to
|
||
|
// generate `point`, and saves us the trouble of recalculating it
|
||
|
// This routine just walks forward until it finds a point at least
|
||
|
// `distance` away. Good optimizations here would reduce the number
|
||
|
// of guesses, but this is tricky since if we go too far out, the
|
||
|
// curve might loop back on leading to incorrect results. Tuning
|
||
|
// kStep is good start.
|
||
|
- (CGFloat)offsetAtDistance:(CGFloat)distance
|
||
|
fromPoint:(CGPoint)point
|
||
|
offset:(CGFloat)offset {
|
||
|
|
||
|
const CGFloat kStep = 0.001; // 0.0001 - 0.001 work well
|
||
|
CGFloat newDistance = 0;
|
||
|
CGFloat newOffset = offset + kStep;
|
||
|
while (newDistance <= distance && newOffset < 1.0) {
|
||
|
newOffset += kStep;
|
||
|
newDistance = calculateDistance(point, [self pointAtOffset:newOffset]);
|
||
|
}
|
||
|
|
||
|
_lastDistance = newDistance;
|
||
|
return newOffset;
|
||
|
}
|
||
|
|
||
|
- (void)setControlPoints
|
||
|
{
|
||
|
NSArray *bezier = [_bezierCurves objectAtIndex:_currentBezierIndex++];
|
||
|
|
||
|
// set start point
|
||
|
if (bezier.count == 1) {
|
||
|
_lastPoint = _P0 = [[bezier objectAtIndex:0] CGPointValue];
|
||
|
[self setControlPoints];
|
||
|
} else if (bezier.count == 3) {
|
||
|
_P1 = [[bezier objectAtIndex:0] CGPointValue];
|
||
|
_P2 = [[bezier objectAtIndex:1] CGPointValue];
|
||
|
_P3 = [[bezier objectAtIndex:2] CGPointValue];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
- (CGAffineTransform)getTransformAtDistance:(CGFloat)distance
|
||
|
{
|
||
|
distance += _startOffset;
|
||
|
_reachedStart = distance >= 0;
|
||
|
if (_reachedEnd || !_reachedStart) {
|
||
|
return CGAffineTransformIdentity;
|
||
|
}
|
||
|
|
||
|
CGFloat offset = [self offsetAtDistance:distance - _lastRecord
|
||
|
fromPoint:_lastPoint
|
||
|
offset:_lastOffset];
|
||
|
|
||
|
if (offset < 1) {
|
||
|
CGPoint glyphPoint = [self pointAtOffset:offset];
|
||
|
_lastOffset = offset;
|
||
|
_lastPoint = glyphPoint;
|
||
|
_lastRecord = distance;
|
||
|
return CGAffineTransformRotate(CGAffineTransformMakeTranslation(glyphPoint.x, glyphPoint.y), [self angleAtOffset:offset]);
|
||
|
} else if (_bezierCurves.count == _currentBezierIndex) {
|
||
|
_reachedEnd = YES;
|
||
|
return CGAffineTransformIdentity;
|
||
|
} else {
|
||
|
_lastOffset = 0;
|
||
|
_lastPoint = _P0 = _P3;
|
||
|
_lastRecord += _lastDistance;
|
||
|
[self setControlPoints];
|
||
|
return [self getTransformAtDistance:distance - _startOffset];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
- (BOOL)hasReachedEnd
|
||
|
{
|
||
|
return _reachedEnd;
|
||
|
}
|
||
|
|
||
|
- (BOOL)hasReachedStart
|
||
|
{
|
||
|
return _reachedStart;
|
||
|
}
|
||
|
|
||
|
@end
|