From 589363d4e683d22385730ce981d73e19cd150da3 Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Sun, 29 Sep 2019 03:58:13 +0300 Subject: [PATCH] feat(ios): implement marker rendering --- ios/Elements/RNSVGMarker.h | 7 +- ios/Elements/RNSVGMarker.m | 70 +++++++++++--- ios/RNSVGNode.m | 1 + ios/RNSVGRenderable.m | 34 +++++++ ios/Utils/RNSVGMarkerPosition.h | 25 +++++ ios/Utils/RNSVGMarkerPosition.m | 158 ++++++++++++++++++++++++++++++++ src/lib/extract/extractProps.ts | 14 ++- 7 files changed, 291 insertions(+), 18 deletions(-) create mode 100644 ios/Utils/RNSVGMarkerPosition.h create mode 100644 ios/Utils/RNSVGMarkerPosition.m diff --git a/ios/Elements/RNSVGMarker.h b/ios/Elements/RNSVGMarker.h index 4af82eb78..a0744b9b9 100644 --- a/ios/Elements/RNSVGMarker.h +++ b/ios/Elements/RNSVGMarker.h @@ -1,6 +1,7 @@ #import "RNSVGGroup.h" #import "RNSVGLength.h" +#import "RNSVGMarkerPosition.h" @interface RNSVGMarker : RNSVGGroup @@ -8,8 +9,8 @@ @property (nonatomic, strong) RNSVGLength *refY; @property (nonatomic, strong) RNSVGLength *markerWidth; @property (nonatomic, strong) RNSVGLength *markerHeight; -@property (nonatomic, assign) NSString *markerUnits; -@property (nonatomic, assign) NSString *orient; +@property (nonatomic, strong) NSString *markerUnits; +@property (nonatomic, strong) NSString *orient; @property (nonatomic, assign) CGFloat minX; @property (nonatomic, assign) CGFloat minY; @@ -18,4 +19,6 @@ @property (nonatomic, strong) NSString *align; @property (nonatomic, assign) RNSVGVBMOS meetOrSlice; +- (void)renderMarker:(CGContextRef)context rect:(CGRect)rect position:(RNSVGMarkerPosition*)position strokeWidth:(CGFloat)strokeWidth; + @end diff --git a/ios/Elements/RNSVGMarker.m b/ios/Elements/RNSVGMarker.m index 7e6f9bc6f..57192e1f1 100644 --- a/ios/Elements/RNSVGMarker.m +++ b/ios/Elements/RNSVGMarker.m @@ -9,6 +9,7 @@ #import "RNSVGPainter.h" #import "RNSVGBrushType.h" #import "RNSVGNode.h" +#import "RNSVGViewBox.h" @implementation RNSVGMarker @@ -21,6 +22,12 @@ - (void)parseReference { self.dirty = false; [self.svgView defineMarker:self markerName:self.name]; + [self traverseSubviews:^(RNSVGNode *node) { + if ([node isKindOfClass:[RNSVGNode class]]) { + [node parseReference]; + } + return YES; + }]; } - (void)setX:(RNSVGLength *)refX @@ -28,7 +35,7 @@ - (void)setX:(RNSVGLength *)refX if ([refX isEqualTo:_refX]) { return; } - + _refX = refX; [self invalidate]; } @@ -38,7 +45,7 @@ - (void)setY:(RNSVGLength *)refY if ([refY isEqualTo:_refY]) { return; } - + _refY = refY; [self invalidate]; } @@ -48,7 +55,7 @@ - (void)setMarkerWidth:(RNSVGLength *)markerWidth if ([markerWidth isEqualTo:_markerWidth]) { return; } - + _markerWidth = markerWidth; [self invalidate]; } @@ -58,17 +65,17 @@ - (void)setMarkerHeight:(RNSVGLength *)markerHeight if ([markerHeight isEqualTo:_markerHeight]) { return; } - + _markerHeight = markerHeight; [self invalidate]; } - (void)setMarkerUnits:(NSString *)markerUnits { - if (markerUnits == _markerUnits) { + if ([_markerUnits isEqualToString:markerUnits]) { return; } - + _markerUnits = markerUnits; [self invalidate]; } @@ -78,7 +85,7 @@ - (void)setOrient:(NSString *)orient if ([orient isEqualToString:_orient]) { return; } - + [self invalidate]; _orient = orient; } @@ -88,7 +95,7 @@ - (void)setMinX:(CGFloat)minX if (minX == _minX) { return; } - + [self invalidate]; _minX = minX; } @@ -98,7 +105,7 @@ - (void)setMinY:(CGFloat)minY if (minY == _minY) { return; } - + [self invalidate]; _minY = minY; } @@ -108,7 +115,7 @@ - (void)setVbWidth:(CGFloat)vbWidth if (vbWidth == _vbWidth) { return; } - + [self invalidate]; _vbWidth = vbWidth; } @@ -118,7 +125,7 @@ - (void)setVbHeight:(CGFloat)vbHeight if (_vbHeight == vbHeight) { return; } - + [self invalidate]; _vbHeight = vbHeight; } @@ -128,7 +135,7 @@ - (void)setAlign:(NSString *)align if ([align isEqualToString:_align]) { return; } - + [self invalidate]; _align = align; } @@ -138,10 +145,47 @@ - (void)setMeetOrSlice:(RNSVGVBMOS)meetOrSlice if (meetOrSlice == _meetOrSlice) { return; } - + [self invalidate]; _meetOrSlice = meetOrSlice; } +- (void)renderMarker:(CGContextRef)context rect:(CGRect)rect position:(RNSVGMarkerPosition*)position strokeWidth:(CGFloat)strokeWidth +{ + CGContextSaveGState(context); + + CGPoint origin = [position origin]; + CGAffineTransform transform = CGAffineTransformMakeTranslation(origin.x, origin.y); + + float markerAngle = [@"auto" isEqualToString:_orient] ? -1 : [_orient doubleValue]; + transform = CGAffineTransformRotate(transform, markerAngle == -1 ? [position angle] : markerAngle); + + bool useStrokeWidth = [@"strokeWidth" isEqualToString:_markerUnits]; + if (useStrokeWidth) { + transform = CGAffineTransformScale(transform, strokeWidth, strokeWidth); + } + + CGContextConcatCTM(context, transform); + + CGFloat width = [self relativeOnWidth:self.markerWidth]; + CGFloat height = [self relativeOnHeight:self.markerHeight]; + CGRect eRect = CGRectMake(0, 0, width, height); + if (self.align) { + CGAffineTransform viewBoxTransform = [RNSVGViewBox getTransform:CGRectMake(self.minX, self.minY, self.vbWidth, self.vbHeight) + eRect:eRect + align:self.align + meetOrSlice:self.meetOrSlice]; + CGContextConcatCTM(context, viewBoxTransform); + } + + CGFloat x = [self relativeOnWidth:self.refX]; + CGFloat y = [self relativeOnHeight:self.refY]; + CGContextTranslateCTM(context, -x, -y); + + [self renderGroupTo:context rect:eRect]; + + CGContextRestoreGState(context); +} + @end diff --git a/ios/RNSVGNode.m b/ios/RNSVGNode.m index 0c634e495..c46d1e665 100644 --- a/ios/RNSVGNode.m +++ b/ios/RNSVGNode.m @@ -36,6 +36,7 @@ - (instancetype)init { if (self = [super init]) { self.opacity = 1; + self.matrix = CGAffineTransformIdentity; self.transforms = CGAffineTransformIdentity; self.invTransform = CGAffineTransformIdentity; _merging = false; diff --git a/ios/RNSVGRenderable.m b/ios/RNSVGRenderable.m index bc1607680..5d5b85a05 100644 --- a/ios/RNSVGRenderable.m +++ b/ios/RNSVGRenderable.m @@ -11,6 +11,9 @@ #import "RNSVGMask.h" #import "RNSVGViewBox.h" #import "RNSVGVectorEffect.h" +#import "RNSVGBezierElement.h" +#import "RNSVGMarker.h" +#import "RNSVGMarkerPosition.h" @implementation RNSVGRenderable { @@ -277,6 +280,8 @@ - (void)renderTo:(CGContextRef)context rect:(CGRect)rect [self endTransparencyLayer:context]; CGContextRestoreGState(context); + + [self renderMarkers:context path:self.path rect:&rect]; } - (void)prepareStrokeDash:(NSUInteger)count strokeDasharray:(NSArray *)strokeDasharray { @@ -294,6 +299,35 @@ - (void)prepareStrokeDash:(NSUInteger)count strokeDasharray:(NSArray* positions = [RNSVGMarkerPosition fromCGPath:path]; + CGFloat width = self.strokeWidth ? [self relativeOnOther:self.strokeWidth] : 1; + for (RNSVGMarkerPosition* position in positions) { + RNSVGMarkerType type = [position type]; + switch (type) { + case kStartMarker: + [markerStart renderMarker:context rect:*rect position:position strokeWidth:width]; + break; + + case kMidMarker: + [markerMid renderMarker:context rect:*rect position:position strokeWidth:width]; + break; + + case kEndMarker: + [markerEnd renderMarker:context rect:*rect position:position strokeWidth:width]; + break; + + default: + break; + } + } + } +} + - (void)renderLayerTo:(CGContextRef)context rect:(CGRect)rect { CGPathRef path = self.path; diff --git a/ios/Utils/RNSVGMarkerPosition.h b/ios/Utils/RNSVGMarkerPosition.h new file mode 100644 index 000000000..987870069 --- /dev/null +++ b/ios/Utils/RNSVGMarkerPosition.h @@ -0,0 +1,25 @@ + +#import +#import + +typedef enum RNSVGMarkerType { + kStartMarker, + kMidMarker, + kEndMarker +} RNSVGMarkerType; + +#define RNSVGNULLPOINT CGRectNull.origin + +@interface RNSVGMarkerPosition : NSObject + +// Element storage +@property (nonatomic, assign) RNSVGMarkerType type; +@property (nonatomic, assign) CGPoint origin; +@property (nonatomic, assign) float angle; + +// Instance creation ++ (instancetype) markerPosition:(RNSVGMarkerType)type origin:(CGPoint)origin angle:(float)angle; + ++ (NSArray*) fromCGPath:(CGPathRef)path; + +@end diff --git a/ios/Utils/RNSVGMarkerPosition.m b/ios/Utils/RNSVGMarkerPosition.m new file mode 100644 index 000000000..03ee267a7 --- /dev/null +++ b/ios/Utils/RNSVGMarkerPosition.m @@ -0,0 +1,158 @@ + +#import "RNSVGMarkerPosition.h" + +@implementation RNSVGMarkerPosition +- (instancetype) init +{ + self = [super init]; + if (self) + { + _type = kStartMarker; + _origin = RNSVGNULLPOINT; + _angle = 0; + } + return self; +} + ++ (instancetype) markerPosition:(RNSVGMarkerType)type origin:(CGPoint)origin angle:(float)angle { + RNSVGMarkerPosition *newElement = [[self alloc] init]; + newElement.type = type; + newElement.origin = origin; + newElement.angle = angle; + return newElement; +} + ++ (NSArray*) fromCGPath:(CGPathRef)path { + positions_ = [[NSMutableArray alloc] init]; + CGPathApply(path, (__bridge void *)positions_, UpdateFromPathElement); + PathIsDone(); + return positions_; +} + +void PathIsDone() { + float angle = CurrentAngle(kEndMarker); + [positions_ addObject:[RNSVGMarkerPosition markerPosition:kEndMarker origin:origin_ angle:angle]]; +} + +static double BisectingAngle(double in_angle, double out_angle) { + // WK193015: Prevent bugs due to angles being non-continuous. + if (fabs(in_angle - out_angle) > 180) + in_angle += 360; + return (in_angle + out_angle) / 2; +} + +static CGFloat RNSVG_radToDeg = 180 / (CGFloat)M_PI; + +double rad2deg(CGFloat rad) { + return rad * RNSVG_radToDeg; +} + +CGFloat SlopeAngleRadians(CGSize p) { + CGFloat angle = atan2(p.height, p.width); + return angle; +} + +double CurrentAngle(RNSVGMarkerType type) { + // For details of this calculation, see: + // http://www.w3.org/TR/SVG/single-page.html#painting-MarkerElement + double in_angle = rad2deg(SlopeAngleRadians(in_slope_)); + double out_angle = rad2deg(SlopeAngleRadians(out_slope_)); + switch (type) { + case kStartMarker: + if (auto_start_reverse_) + out_angle += 180; + return out_angle; + case kMidMarker: + return BisectingAngle(in_angle, out_angle); + case kEndMarker: + return in_angle; + } + return 0; +} + +typedef struct SegmentData { + CGSize start_tangent; // Tangent in the start point of the segment. + CGSize end_tangent; // Tangent in the end point of the segment. + CGPoint position; // The end point of the segment. +} SegmentData; + +CGSize subtract(CGPoint* p1, CGPoint* p2) { + return CGSizeMake(p2->x - p1->x, p2->y - p1->y); +} + +static void ComputeQuadTangents(SegmentData* data, + CGPoint* start, + CGPoint* control, + CGPoint* end) { + data->start_tangent = subtract(control, start); + data->end_tangent = subtract(end, control); + if (CGSizeEqualToSize(CGSizeZero, data->start_tangent)) + data->start_tangent = data->end_tangent; + else if (CGSizeEqualToSize(CGSizeZero, data->end_tangent)) + data->end_tangent = data->start_tangent; +} + +SegmentData ExtractPathElementFeatures(const CGPathElement* element) { + SegmentData data; + CGPoint* points = element->points; + switch (element->type) { + case kCGPathElementAddCurveToPoint: + data.position = points[2]; + data.start_tangent = subtract(&points[0], &origin_); + data.end_tangent = subtract(&points[2], &points[1]); + if (CGSizeEqualToSize(CGSizeZero, data.start_tangent)) + ComputeQuadTangents(&data, &points[0], &points[1], &points[2]); + else if (CGSizeEqualToSize(CGSizeZero, data.end_tangent)) + ComputeQuadTangents(&data, &origin_, &points[0], &points[1]); + break; + case kCGPathElementAddQuadCurveToPoint: + data.position = points[1]; + ComputeQuadTangents(&data, &origin_, &points[0], &points[1]); + break; + case kCGPathElementMoveToPoint: + case kCGPathElementAddLineToPoint: + data.position = points[0]; + data.start_tangent = subtract(&data.position, &origin_); + data.end_tangent = subtract(&data.position, &origin_); + break; + case kCGPathElementCloseSubpath: + data.position = subpath_start_; + data.start_tangent = subtract(&data.position, &origin_); + data.end_tangent = subtract(&data.position, &origin_); + break; + } + return data; +} + +void UpdateFromPathElement(void *info, const CGPathElement *element) { + SegmentData segment_data = ExtractPathElementFeatures(element); + // First update the outgoing slope for the previous element. + out_slope_ = segment_data.start_tangent; + // Record the marker for the previous element. + if (element_index_ > 0) { + RNSVGMarkerType marker_type = + element_index_ == 1 ? kStartMarker : kMidMarker; + float angle = CurrentAngle(marker_type); + [positions_ addObject:[RNSVGMarkerPosition markerPosition:marker_type origin:origin_ angle:angle]]; + } + // Update the incoming slope for this marker position. + in_slope_ = segment_data.end_tangent; + // Update marker position. + origin_ = segment_data.position; + // If this is a 'move to' segment, save the point for use with 'close'. + if (element->type == kCGPathElementMoveToPoint) + subpath_start_ = element->points[0]; + else if (element->type == kCGPathElementCloseSubpath) + subpath_start_ = CGPointZero; + ++element_index_; +} + +NSMutableArray* positions_; +unsigned element_index_; +CGPoint origin_; +CGPoint subpath_start_; +CGSize in_slope_; +CGSize out_slope_; +bool auto_start_reverse_; + +@end diff --git a/src/lib/extract/extractProps.ts b/src/lib/extract/extractProps.ts index 6bada4946..09ac5610c 100644 --- a/src/lib/extract/extractProps.ts +++ b/src/lib/extract/extractProps.ts @@ -23,6 +23,14 @@ export function propsAndStyles(props: Object & { style?: [] | {} }) { }; } +function getMarker(marker?: string) { + if (!marker) { + return undefined; + } + const matched = marker.match(idPattern); + return matched ? matched[1] : undefined; +} + export default function extractProps( props: { id?: string; @@ -70,9 +78,9 @@ export default function extractProps( markerEnd?: string; } = { matrix, - markerStart, - markerMid, - markerEnd, + markerStart: getMarker(markerStart), + markerMid: getMarker(markerMid), + markerEnd: getMarker(markerEnd), onLayout, ...transformProps, propList: styleProperties,