From b15d0791e0fbd65bed932e674fb5dfe22e30e461 Mon Sep 17 00:00:00 2001 From: Horcrux Date: Tue, 10 Jan 2017 12:30:48 +0800 Subject: [PATCH] Add startOffset support --- elements/TextPath.js | 11 ++- ios/Text/RNSVGBezierPath.h | 5 +- ios/Text/RNSVGBezierPath.m | 125 ++++++++++++++---------- ios/Text/RNSVGTSpan.h | 2 + ios/Text/RNSVGTSpan.m | 7 +- ios/Text/RNSVGText.h | 1 - ios/Text/RNSVGTextPath.h | 1 + ios/Text/RNSVGTextPath.m | 4 +- ios/ViewManagers/RNSVGTextPathManager.m | 1 + lib/attributes.js | 3 +- lib/extract/extractText.js | 4 +- 11 files changed, 99 insertions(+), 65 deletions(-) diff --git a/elements/TextPath.js b/elements/TextPath.js index 5b1232e0..3074cd47 100644 --- a/elements/TextPath.js +++ b/elements/TextPath.js @@ -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)} />; } } diff --git a/ios/Text/RNSVGBezierPath.h b/ios/Text/RNSVGBezierPath.h index bd34196a..7ab1b609 100644 --- a/ios/Text/RNSVGBezierPath.h +++ b/ios/Text/RNSVGBezierPath.h @@ -11,7 +11,10 @@ @interface RNSVGBezierPath : NSObject -- (instancetype)initWithBezierCurves:(NSArray *)bezierCurves; ++ (BOOL) hasReachedEnd:(CGAffineTransform)transform; ++ (BOOL) hasReachedStart:(CGAffineTransform) transform; + +- (instancetype)initWithBezierCurvesAndStartOffset:(NSArray *)bezierCurves startOffset:(CGFloat)startOffset; - (CGAffineTransform)transformAtDistance:(CGFloat)distance; @end diff --git a/ios/Text/RNSVGBezierPath.m b/ios/Text/RNSVGBezierPath.m index 0359a5ea..181fc437 100644 --- a/ios/Text/RNSVGBezierPath.m +++ b/ios/Text/RNSVGBezierPath.m @@ -17,54 +17,70 @@ @implementation RNSVGBezierPath { 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 *)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 diff --git a/ios/Text/RNSVGTSpan.h b/ios/Text/RNSVGTSpan.h index 3e04fc0b..062c231e 100644 --- a/ios/Text/RNSVGTSpan.h +++ b/ios/Text/RNSVGTSpan.h @@ -14,4 +14,6 @@ @property (nonatomic, strong) NSString *content; +- (CGAffineTransform)getTextPathTransform:(CGFloat)distance; + @end diff --git a/ios/Text/RNSVGTSpan.m b/ios/Text/RNSVGTSpan.m index 216160dd..24cd4956 100644 --- a/ios/Text/RNSVGTSpan.m +++ b/ios/Text/RNSVGTSpan.m @@ -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; diff --git a/ios/Text/RNSVGText.h b/ios/Text/RNSVGText.h index 6610f899..4f2c2c4e 100644 --- a/ios/Text/RNSVGText.h +++ b/ios/Text/RNSVGText.h @@ -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; diff --git a/ios/Text/RNSVGTextPath.h b/ios/Text/RNSVGTextPath.h index 7082fcc5..db65a725 100644 --- a/ios/Text/RNSVGTextPath.h +++ b/ios/Text/RNSVGTextPath.h @@ -14,6 +14,7 @@ @interface RNSVGTextPath : RNSVGText @property (nonatomic, strong) NSString *href; +@property (nonatomic, strong) NSString *startOffset; - (RNSVGBezierPath *)getBezierPath; diff --git a/ios/Text/RNSVGTextPath.m b/ios/Text/RNSVGTextPath.m index 1b6acd71..ce59e622 100644 --- a/ios/Text/RNSVGTextPath.m +++ b/ios/Text/RNSVGTextPath.m @@ -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 diff --git a/ios/ViewManagers/RNSVGTextPathManager.m b/ios/ViewManagers/RNSVGTextPathManager.m index f0c58739..3ff778cd 100644 --- a/ios/ViewManagers/RNSVGTextPathManager.m +++ b/ios/ViewManagers/RNSVGTextPathManager.m @@ -20,5 +20,6 @@ RCT_EXPORT_MODULE() } RCT_EXPORT_VIEW_PROPERTY(href, NSString) +RCT_EXPORT_VIEW_PROPERTY(startOffset, NSString) @end diff --git a/lib/attributes.js b/lib/attributes.js index 18a777cd..08c8318c 100644 --- a/lib/attributes.js +++ b/lib/attributes.js @@ -99,7 +99,8 @@ const TextAttributes = merge({ }, RenderableAttributes); const TextPathAttributes = merge({ - href: true + href: true, + startOffset: true }, RenderableAttributes); const TSpanAttibutes = merge({ diff --git a/lib/extract/extractText.js b/lib/extract/extractText.js index 31d79558..1afd8f7d 100644 --- a/lib/extract/extractText.js +++ b/lib/extract/extractText.js @@ -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 }