mirror of
https://github.com/zoriya/react-native-svg.git
synced 2025-12-20 14:05:09 +00:00
Add startOffset support
This commit is contained in:
@@ -3,7 +3,7 @@ import createReactNativeComponentClass from 'react-native/Libraries/Renderer/src
|
||||
import {TextPathAttributes} from '../lib/attributes';
|
||||
import extractText from '../lib/extract/extractText';
|
||||
import Shape from './Shape';
|
||||
import {pathProps, fontProps} from '../lib/props';
|
||||
import {pathProps, fontProps, numberProp} from '../lib/props';
|
||||
import TSpan from './TSpan';
|
||||
|
||||
const idExpReg = /^#(.+)$/;
|
||||
@@ -15,11 +15,11 @@ class TextPath extends Shape {
|
||||
...pathProps,
|
||||
...fontProps,
|
||||
href: PropTypes.string.isRequired,
|
||||
textAnchor: PropTypes.oneOf(['start', 'middle', 'end'])
|
||||
startOffset: numberProp
|
||||
};
|
||||
|
||||
render() {
|
||||
let {children, href, ...props} = this.props;
|
||||
let {children, href, startOffset, ...props} = this.props;
|
||||
if (href) {
|
||||
let matched = href.match(idExpReg);
|
||||
|
||||
@@ -33,7 +33,10 @@ class TextPath extends Shape {
|
||||
x: null,
|
||||
y: null
|
||||
})}
|
||||
{...extractText({children}, true)}
|
||||
{...extractText({
|
||||
children,
|
||||
startOffset
|
||||
}, true)}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,10 @@
|
||||
|
||||
@interface RNSVGBezierPath : NSObject
|
||||
|
||||
- (instancetype)initWithBezierCurves:(NSArray *)bezierCurves;
|
||||
+ (BOOL) hasReachedEnd:(CGAffineTransform)transform;
|
||||
+ (BOOL) hasReachedStart:(CGAffineTransform) transform;
|
||||
|
||||
- (instancetype)initWithBezierCurvesAndStartOffset:(NSArray<NSArray *> *)bezierCurves startOffset:(CGFloat)startOffset;
|
||||
- (CGAffineTransform)transformAtDistance:(CGFloat)distance;
|
||||
|
||||
@end
|
||||
|
||||
@@ -17,54 +17,70 @@
|
||||
@implementation RNSVGBezierPath
|
||||
{
|
||||
NSArray<NSArray *> *_bezierCurves;
|
||||
NSUInteger _bezierIndex;
|
||||
CGFloat _offset;
|
||||
CGFloat _lastX;
|
||||
int _currentBezierIndex;
|
||||
CGFloat _startOffset;
|
||||
CGFloat _lastOffset;
|
||||
CGFloat _lastRecord;
|
||||
CGFloat _lastDistance;
|
||||
CGPoint _lastPoint;
|
||||
CGPoint _P0;
|
||||
CGPoint _P1;
|
||||
CGPoint _P2;
|
||||
CGPoint _P3;
|
||||
BOOL _reachedEnd;
|
||||
}
|
||||
|
||||
- (instancetype)initWithBezierCurves:(NSArray *)bezierCurves
|
||||
- (instancetype)initWithBezierCurvesAndStartOffset:(NSArray<NSArray *> *)bezierCurves startOffset:(CGFloat)startOffset
|
||||
{
|
||||
|
||||
if (self = [super init]) {
|
||||
_bezierCurves = bezierCurves;
|
||||
_bezierIndex = 0;
|
||||
_offset = 0;
|
||||
_lastX = 0;
|
||||
_lastDistance = 0;
|
||||
_currentBezierIndex = _lastOffset = _lastRecord = _lastDistance = 0;
|
||||
_startOffset = startOffset;
|
||||
[self setControlPoints];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
static CGFloat Bezier(CGFloat t, CGFloat P0, CGFloat P1, CGFloat P2, CGFloat P3) {
|
||||
static CGAffineTransform getReachedEndTansform() {
|
||||
return CGAffineTransformMakeScale(0, 1);
|
||||
}
|
||||
|
||||
static CGAffineTransform getUnreadedTransform() {
|
||||
return CGAffineTransformMakeScale(1, 0);
|
||||
}
|
||||
|
||||
+ (BOOL) hasReachedEnd:(CGAffineTransform)transform
|
||||
{
|
||||
return transform.a == 0;
|
||||
}
|
||||
|
||||
+ (BOOL) hasReachedStart:(CGAffineTransform) transform
|
||||
{
|
||||
return transform.d == 0;
|
||||
}
|
||||
|
||||
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)pointForOffset:(CGFloat)t {
|
||||
CGFloat x = Bezier(t, _P0.x, _P1.x, _P2.x, _P3.x);
|
||||
CGFloat y = Bezier(t, _P0.y, _P1.y, _P2.y, _P3.y);
|
||||
- (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 BezierPrime(CGFloat t, CGFloat P0, CGFloat P1, CGFloat P2, CGFloat P3) {
|
||||
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)angleForOffset:(CGFloat)t {
|
||||
CGFloat dx = BezierPrime(t, _P0.x, _P1.x, _P2.x, _P3.x);
|
||||
CGFloat dy = BezierPrime(t, _P0.y, _P1.y, _P2.y, _P3.y);
|
||||
- (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 Distance(CGPoint a, CGPoint b) {
|
||||
CGFloat dx = a.x - b.x;
|
||||
CGFloat dy = a.y - b.y;
|
||||
return hypot(dx, dy);
|
||||
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
|
||||
@@ -76,65 +92,66 @@ static CGFloat Distance(CGPoint a, CGPoint b) {
|
||||
// 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.0005; // 0.0001 - 0.001 work well
|
||||
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 = Distance(point, [self pointForOffset:newOffset]);
|
||||
newDistance = calculateDistance(point, [self pointAtOffset:newOffset]);
|
||||
}
|
||||
|
||||
|
||||
_lastDistance = newDistance;
|
||||
return newOffset;
|
||||
}
|
||||
|
||||
- (void)setControlPoints
|
||||
{
|
||||
NSArray *bezier = [_bezierCurves objectAtIndex:_bezierIndex];
|
||||
_bezierIndex++;
|
||||
NSArray *bezier = [_bezierCurves objectAtIndex:_currentBezierIndex++];
|
||||
|
||||
// set start point
|
||||
if (bezier.count == 1) {
|
||||
_lastPoint = _P0 = [(NSValue *)[bezier objectAtIndex:0] CGPointValue];
|
||||
_lastPoint = _P0 = [[bezier objectAtIndex:0] CGPointValue];
|
||||
[self setControlPoints];
|
||||
} else if (bezier.count == 3) {
|
||||
_P1 = [(NSValue *)[bezier objectAtIndex:0] CGPointValue];
|
||||
_P2 = [(NSValue *)[bezier objectAtIndex:1] CGPointValue];
|
||||
_P3 = [(NSValue *)[bezier objectAtIndex:2] CGPointValue];
|
||||
_P1 = [[bezier objectAtIndex:0] CGPointValue];
|
||||
_P2 = [[bezier objectAtIndex:1] CGPointValue];
|
||||
_P3 = [[bezier objectAtIndex:2] CGPointValue];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (CGAffineTransform)transformAtDistance:(CGFloat)distance
|
||||
{
|
||||
if (_offset == 0) {
|
||||
if (_bezierCurves.count == _bezierIndex) {
|
||||
return CGAffineTransformMakeScale(0, 0);
|
||||
} else {
|
||||
[self setControlPoints];
|
||||
}
|
||||
distance += _startOffset;
|
||||
if (_reachedEnd) {
|
||||
return getReachedEndTansform();
|
||||
} else if (distance < 0) {
|
||||
return getUnreadedTransform();
|
||||
}
|
||||
|
||||
CGFloat offset = [self offsetAtDistance:distance - _lastX
|
||||
|
||||
CGFloat offset = [self offsetAtDistance:distance - _lastRecord
|
||||
fromPoint:_lastPoint
|
||||
offset:_offset];
|
||||
CGPoint glyphPoint = [self pointForOffset:offset];
|
||||
CGFloat angle = [self angleForOffset:offset];
|
||||
|
||||
offset:_lastOffset];
|
||||
|
||||
if (offset < 1) {
|
||||
_offset = offset;
|
||||
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 getReachedEndTansform();
|
||||
} else {
|
||||
_offset = 0;
|
||||
_lastOffset = 0;
|
||||
_lastPoint = _P0 = _P3;
|
||||
_lastX += _lastDistance;
|
||||
return [self transformAtDistance:distance];
|
||||
_lastRecord += _lastDistance;
|
||||
[self setControlPoints];
|
||||
return [self transformAtDistance:distance - _startOffset];
|
||||
}
|
||||
|
||||
_lastX = distance;
|
||||
CGAffineTransform transform = CGAffineTransformMakeTranslation(glyphPoint.x, glyphPoint.y);
|
||||
transform = CGAffineTransformRotate(transform, angle);
|
||||
|
||||
return transform;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -14,4 +14,6 @@
|
||||
|
||||
@property (nonatomic, strong) NSString *content;
|
||||
|
||||
- (CGAffineTransform)getTextPathTransform:(CGFloat)distance;
|
||||
|
||||
@end
|
||||
|
||||
@@ -32,8 +32,9 @@
|
||||
if (!self.content) {
|
||||
return [self getGroupPath:context];
|
||||
}
|
||||
[self initialTextPath];
|
||||
[self setContextBoundingBox:CGContextGetClipBoundingBox(context)];
|
||||
|
||||
[self initialTextPath];
|
||||
CGMutablePathRef path = CGPathCreateMutable();
|
||||
|
||||
if ([self.content isEqualToString:@""]) {
|
||||
@@ -98,8 +99,10 @@
|
||||
|
||||
CGAffineTransform textPathTransform = [self getTextPathTransform:computedPoint.x];
|
||||
|
||||
if (!textPathTransform.a || !textPathTransform.d) {
|
||||
if ([RNSVGBezierPath hasReachedEnd:textPathTransform]) {
|
||||
break;
|
||||
} else if ([RNSVGBezierPath hasReachedStart:textPathTransform]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
CGAffineTransform transform;
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
- (CTFontRef)getComputedFont;
|
||||
- (RNSVGGlyphPoint)getComputedGlyphPoint:(NSUInteger *)index glyphOffset:(CGPoint)glyphOffset;
|
||||
- (RNSVGText *)getTextRoot;
|
||||
- (CGAffineTransform)getTextPathTransform:(CGFloat)distance;
|
||||
- (CGPathRef)getGroupPath:(CGContextRef)context;
|
||||
- (void)resetTextPathAttributes;
|
||||
- (void)traverseTextSuperviews:(BOOL (^)(__kindof RNSVGText *node))block;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
@interface RNSVGTextPath : RNSVGText
|
||||
|
||||
@property (nonatomic, strong) NSString *href;
|
||||
@property (nonatomic, strong) NSString *startOffset;
|
||||
|
||||
- (RNSVGBezierPath *)getBezierPath;
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
- (void)renderLayerTo:(CGContextRef)context
|
||||
{
|
||||
[self setContextBoundingBox:CGContextGetClipBoundingBox(context)];
|
||||
[self renderGroupTo:context];
|
||||
}
|
||||
|
||||
@@ -33,7 +34,8 @@
|
||||
}
|
||||
|
||||
RNSVGPath *path = template;
|
||||
return [[RNSVGBezierPath alloc] initWithBezierCurves:[path getBezierCurves]];
|
||||
CGFloat startOffset = [self getWidthRelatedValue:self.startOffset];
|
||||
return [[RNSVGBezierPath alloc] initWithBezierCurvesAndStartOffset:[path getBezierCurves] startOffset:startOffset];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -20,5 +20,6 @@ RCT_EXPORT_MODULE()
|
||||
}
|
||||
|
||||
RCT_EXPORT_VIEW_PROPERTY(href, NSString)
|
||||
RCT_EXPORT_VIEW_PROPERTY(startOffset, NSString)
|
||||
|
||||
@end
|
||||
|
||||
@@ -99,7 +99,8 @@ const TextAttributes = merge({
|
||||
}, RenderableAttributes);
|
||||
|
||||
const TextPathAttributes = merge({
|
||||
href: true
|
||||
href: true,
|
||||
startOffset: true
|
||||
}, RenderableAttributes);
|
||||
|
||||
const TSpanAttibutes = merge({
|
||||
|
||||
@@ -88,7 +88,8 @@ export default function(props, container) {
|
||||
y,
|
||||
dx,
|
||||
dy,
|
||||
textAnchor
|
||||
textAnchor,
|
||||
startOffset
|
||||
} = props;
|
||||
|
||||
|
||||
@@ -121,6 +122,7 @@ export default function(props, container) {
|
||||
content,
|
||||
deltaX,
|
||||
deltaY,
|
||||
startOffset: (startOffset || 0).toString(),
|
||||
positionX: _.isNil(x) ? null : x,
|
||||
positionY: _.isNil(y) ? null : y
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user