diff --git a/Example/examples/Clipping.js b/Example/examples/Clipping.js index 123ce584..1b108877 100644 --- a/Example/examples/Clipping.js +++ b/Example/examples/Clipping.js @@ -124,6 +124,13 @@ class ClipPathElement extends Component{ + Q - +#import "UIBezierPath-Points.h" #import "RNSVGPath.h" #import "RNSVGTextFrame.h" #import "RNSVGGlyphCache.h" @@ -16,5 +16,6 @@ @property (nonatomic, assign) CTTextAlignment alignment; @property (nonatomic, assign) RNSVGTextFrame textFrame; +@property (nonatomic, assign) CGPathRef path; @end diff --git a/ios/RNSVGText.m b/ios/RNSVGText.m index c94be29c..8ea742d0 100644 --- a/ios/RNSVGText.m +++ b/ios/RNSVGText.m @@ -39,15 +39,25 @@ static void RNSVGFreeTextFrame(RNSVGTextFrame frame) _textFrame = frame; } +- (void)setPath:(CGPathRef)path +{ + if (path == _path) { + return; + } + [self invalidate]; + CGPathRelease(_path); + _path = CGPathRetain(path); +} + - (void)dealloc { + CGPathRelease(_path); RNSVGFreeTextFrame(_textFrame); } - (void)renderLayerTo:(CGContextRef)context { self.d = [self getPath: context]; - [super renderLayerTo:context]; } @@ -55,15 +65,15 @@ static void RNSVGFreeTextFrame(RNSVGTextFrame frame) { CGMutablePathRef path = CGPathCreateMutable(); RNSVGTextFrame frame = self.textFrame; - for (int i = 0; i < frame.count; i++) { CGFloat shift; + CGFloat width = frame.widths[i]; switch (self.alignment) { case kCTTextAlignmentRight: - shift = frame.widths[i]; + shift = width; break; case kCTTextAlignmentCenter: - shift = (frame.widths[i] / 2); + shift = width / 2; break; default: shift = 0; @@ -71,7 +81,7 @@ static void RNSVGFreeTextFrame(RNSVGTextFrame frame) } // We should consider snapping this shift to device pixels to improve rendering quality // when a line has subpixel width. - CGAffineTransform offset = CGAffineTransformMakeTranslation(-shift, frame.baseLine + frame.lineHeight * i); + CGAffineTransform offset = CGAffineTransformMakeTranslation(-shift, frame.baseLine + frame.lineHeight * i + (self.path == NULL ? 0 : -frame.lineHeight)); CGPathAddPath(path, &offset, [self setLinePath:frame.lines[i]]); } @@ -80,6 +90,7 @@ static void RNSVGFreeTextFrame(RNSVGTextFrame frame) - (CGPathRef)setLinePath:(CTLineRef)line { + CGAffineTransform upsideDown = CGAffineTransformMakeScale(1.0, -1.0); CGMutablePathRef path = CGPathCreateMutable(); RNSVGGlyphCache *cache = [[RNSVGGlyphCache alloc] init]; @@ -101,12 +112,31 @@ static void RNSVGFreeTextFrame(RNSVGTextFrame frame) CTRunGetGlyphs(run, CFRangeMake(0, 0), glyphs); CFDictionaryRef attributes = CTRunGetAttributes(run); CTFontRef runFont = CFDictionaryGetValue(attributes, kCTFontAttributeName); - for(CFIndex j = 0; j < runGlyphCount; ++j, ++glyphIndex) { CGPathRef letter = [cache pathForGlyph:glyphs[j] fromFont:runFont]; CGPoint point = positions[j]; if (letter != NULL) { - CGAffineTransform transform = CGAffineTransformTranslate(upsideDown, point.x, point.y); + CGAffineTransform transform; + + // draw glyphs along path + if (self.path != NULL) { + CGPoint slope; + CGRect bounding = CGPathGetBoundingBox(letter); + UIBezierPath* path = [UIBezierPath bezierPathWithCGPath:self.path]; + CGFloat percentConsumed = (point.x + bounding.size.width) / path.length; + if (percentConsumed >= 1.0f) { + continue; + } + + CGPoint targetPoint = [path pointAtPercent:percentConsumed withSlope: &slope]; + float angle = atan(slope.y / slope.x); // + M_PI; + if (slope.x < 0) angle += M_PI; // going left, update the angle + transform = CGAffineTransformMakeTranslation(targetPoint.x - bounding.size.width, targetPoint.y); + transform = CGAffineTransformRotate(transform, angle); + transform = CGAffineTransformScale(transform, 1.0, -1.0); + } else { + transform = CGAffineTransformTranslate(upsideDown, point.x, point.y); + } CGPathAddPath(path, &transform, letter); } } diff --git a/ios/UIBezierPath-Points.h b/ios/UIBezierPath-Points.h new file mode 100644 index 00000000..7f77604c --- /dev/null +++ b/ios/UIBezierPath-Points.h @@ -0,0 +1,27 @@ +/** + * 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. + */ + +/* + Erica Sadun, http://ericasadun.com + iPhone Developer's Cookbook, 6.x Edition + BSD License, Use at your own risk + */ + +#import +#import + +@interface UIBezierPath (Points) +@property (nonatomic, readonly) NSArray *points; +@property (nonatomic, readonly) NSArray *bezierElements; +@property (nonatomic, readonly) CGFloat length; + +- (NSArray *) pointPercentArray; +- (CGPoint) pointAtPercent: (CGFloat) percent withSlope: (CGPoint *) slope; ++ (UIBezierPath *) pathWithPoints: (NSArray *) points; ++ (UIBezierPath *) pathWithElements: (NSArray *) elements; +@end \ No newline at end of file diff --git a/ios/UIBezierPath-Points.m b/ios/UIBezierPath-Points.m new file mode 100644 index 00000000..e29df010 --- /dev/null +++ b/ios/UIBezierPath-Points.m @@ -0,0 +1,219 @@ +/** + * 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. + */ + +/* + Erica Sadun, http://ericasadun.com + iPhone Developer's Cookbook, 6.x Edition + BSD License, Use at your own risk + */ + +#import "UIBezierPath-Points.h" + +#define POINTSTRING(_CGPOINT_) (NSStringFromCGPoint(_CGPOINT_)) +#define VALUE(_INDEX_) [NSValue valueWithCGPoint:points[_INDEX_]] +#define POINT(_INDEX_) [(NSValue *)[points objectAtIndex:_INDEX_] CGPointValue] + +// Return distance between two points +static float distance (CGPoint p1, CGPoint p2) +{ + float dx = p2.x - p1.x; + float dy = p2.y - p1.y; + + return sqrt(dx*dx + dy*dy); +} + +@implementation UIBezierPath (Points) +void getPointsFromBezier(void *info, const CGPathElement *element) +{ + NSMutableArray *bezierPoints = (__bridge NSMutableArray *)info; + CGPathElementType type = element->type; + CGPoint *points = element->points; + if (type != kCGPathElementCloseSubpath) + { + if ((type == kCGPathElementAddLineToPoint) || + (type == kCGPathElementMoveToPoint)) + [bezierPoints addObject:VALUE(0)]; + else if (type == kCGPathElementAddQuadCurveToPoint) + [bezierPoints addObject:VALUE(1)]; + else if (type == kCGPathElementAddCurveToPoint) + [bezierPoints addObject:VALUE(2)]; + } +} + +- (NSArray *)points +{ + NSMutableArray *points = [NSMutableArray array]; + CGPathApply(self.CGPath, (__bridge void *)points, getPointsFromBezier); + return points; +} + +// Return a Bezier path buit with the supplied points ++ (UIBezierPath *) pathWithPoints: (NSArray *) points +{ + UIBezierPath *path = [UIBezierPath bezierPath]; + if (points.count == 0) return path; + [path moveToPoint:POINT(0)]; + for (int i = 1; i < points.count; i++) + [path addLineToPoint:POINT(i)]; + return path; +} + +- (CGFloat) length +{ + NSArray *points = self.points; + float totalPointLength = 0.0f; + for (int i = 1; i < points.count; i++) + totalPointLength += distance(POINT(i), POINT(i-1)); + return totalPointLength; +} + +- (NSArray *) pointPercentArray +{ + // Use total length to calculate the percent of path consumed at each control point + NSArray *points = self.points; + int pointCount = points.count; + + float totalPointLength = self.length; + float distanceTravelled = 0.0f; + + NSMutableArray *pointPercentArray = [NSMutableArray array]; + [pointPercentArray addObject:@(0.0)]; + + for (int i = 1; i < pointCount; i++) + { + distanceTravelled += distance(POINT(i), POINT(i-1)); + [pointPercentArray addObject:@(distanceTravelled / totalPointLength)]; + } + + // Add a final item just to stop with. Probably not needed. + [pointPercentArray addObject:[NSNumber numberWithFloat:1.1f]]; // 110% + + return pointPercentArray; +} + +- (CGPoint) pointAtPercent: (CGFloat) percent withSlope: (CGPoint *) slope +{ + NSArray *points = self.points; + NSArray *percentArray = self.pointPercentArray; + CFIndex lastPointIndex = points.count - 1; + + if (!points.count) { + return CGPointZero; + } + + // Check for 0% and 100% + if (percent <= 0.0f) { + return POINT(0); + } + if (percent >= 1.0f) { + return POINT(lastPointIndex); + } + + // Find a corresponding pair of points in the path + CFIndex index = 1; + while ((index < percentArray.count) && + (percent > ((NSNumber *)percentArray[index]).floatValue)) { + index++; + } + + // This should not happen. + if (index > lastPointIndex) { + return POINT(lastPointIndex); + } + + // Calculate the intermediate distance between the two points + CGPoint point1 = POINT(index -1); + CGPoint point2 = POINT(index); + + float percent1 = [[percentArray objectAtIndex:index - 1] floatValue]; + float percent2 = [[percentArray objectAtIndex:index] floatValue]; + float percentOffset = (percent - percent1) / (percent2 - percent1); + + float dx = point2.x - point1.x; + float dy = point2.y - point1.y; + + // Store dy, dx for retrieving arctan + if (slope) { + *slope = CGPointMake(dx, dy); + } + + // Calculate new point + CGFloat newX = point1.x + (percentOffset * dx); + CGFloat newY = point1.y + (percentOffset * dy); + CGPoint targetPoint = CGPointMake(newX, newY); + + return targetPoint; +} + +void getBezierElements(void *info, const CGPathElement *element) +{ + NSMutableArray *bezierElements = (__bridge NSMutableArray *)info; + CGPathElementType type = element->type; + CGPoint *points = element->points; + + switch (type) + { + case kCGPathElementCloseSubpath: + [bezierElements addObject:@[@(type)]]; + break; + case kCGPathElementMoveToPoint: + case kCGPathElementAddLineToPoint: + [bezierElements addObject:@[@(type), VALUE(0)]]; + break; + case kCGPathElementAddQuadCurveToPoint: + [bezierElements addObject:@[@(type), VALUE(0), VALUE(1)]]; + break; + case kCGPathElementAddCurveToPoint: + [bezierElements addObject:@[@(type), VALUE(0), VALUE(1), VALUE(2)]]; + break; + } +} + +- (NSArray *) bezierElements +{ + NSMutableArray *elements = [NSMutableArray array]; + CGPathApply(self.CGPath, (__bridge void *)elements, getBezierElements); + return elements; +} + ++ (UIBezierPath *) pathWithElements: (NSArray *) elements +{ + UIBezierPath *path = [UIBezierPath bezierPath]; + if (elements.count == 0) return path; + + for (NSArray *points in elements) + { + if (!points.count) continue; + CGPathElementType elementType = [points[0] integerValue]; + switch (elementType) + { + case kCGPathElementCloseSubpath: + [path closePath]; + break; + case kCGPathElementMoveToPoint: + if (points.count == 2) + [path moveToPoint:POINT(1)]; + break; + case kCGPathElementAddLineToPoint: + if (points.count == 2) + [path addLineToPoint:POINT(1)]; + break; + case kCGPathElementAddQuadCurveToPoint: + if (points.count == 3) + [path addQuadCurveToPoint:POINT(2) controlPoint:POINT(1)]; + break; + case kCGPathElementAddCurveToPoint: + if (points.count == 4) + [path addCurveToPoint:POINT(3) controlPoint1:POINT(1) controlPoint2:POINT(2)]; + break; + } + } + + return path; +} +@end diff --git a/ios/ViewManagers/RNSVGTextManager.m b/ios/ViewManagers/RNSVGTextManager.m index 04500654..76280e35 100644 --- a/ios/ViewManagers/RNSVGTextManager.m +++ b/ios/ViewManagers/RNSVGTextManager.m @@ -22,5 +22,6 @@ RCT_EXPORT_MODULE() RCT_EXPORT_VIEW_PROPERTY(alignment, CTTextAlignment) RCT_REMAP_VIEW_PROPERTY(frame, textFrame, RNSVGTextFrame) +RCT_EXPORT_VIEW_PROPERTY(path, CGPath) @end