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