diff --git a/android/src/main/java/com/horcrux/svg/RNSVGMarkerPosition.java b/android/src/main/java/com/horcrux/svg/RNSVGMarkerPosition.java new file mode 100644 index 000000000..66cf12761 --- /dev/null +++ b/android/src/main/java/com/horcrux/svg/RNSVGMarkerPosition.java @@ -0,0 +1,180 @@ +package com.horcrux.svg; + +import java.util.ArrayList; + +enum RNSVGMarkerType { + kStartMarker, + kMidMarker, + kEndMarker +} + +enum ElementType { + kCGPathElementAddCurveToPoint, + kCGPathElementAddQuadCurveToPoint, + kCGPathElementMoveToPoint, + kCGPathElementAddLineToPoint, + kCGPathElementCloseSubpath +} + +class Point { + double x; + double y; + Point(double x, double y){ + this.x = x; + this.y = y; + } +} + +class SegmentData { + Point start_tangent; // Tangent in the start point of the segment. + Point end_tangent; // Tangent in the end point of the segment. + Point position; // The end point of the segment. +} + +class RNSVGMarkerPosition { + + static private ArrayList positions_; + static private int element_index_; + static private Point origin_; + static private Point subpath_start_; + static private Point in_slope_; + static private Point out_slope_; + @SuppressWarnings("unused") + static private boolean auto_start_reverse_; // TODO + + RNSVGMarkerType type; + Point origin; + double angle; + + private RNSVGMarkerPosition(RNSVGMarkerType type, Point origin, double angle) { + this.type = type; + this.origin = origin; + this.angle = angle; + } + + static ArrayList fromPath(ArrayList elements) { + positions_ = new ArrayList<>(); + element_index_ = 0; + origin_ = new Point(0, 0); + subpath_start_ = new Point(0, 0); + for (PathElement e : elements) { + UpdateFromPathElement(e); + } + PathIsDone(); + return positions_; + } + + private static void PathIsDone() { + double angle = CurrentAngle(RNSVGMarkerType.kEndMarker); + positions_.add(new RNSVGMarkerPosition(RNSVGMarkerType.kEndMarker, origin_, angle)); + } + + private static double BisectingAngle(double in_angle, double out_angle) { + // WK193015: Prevent bugs due to angles being non-continuous. + if (Math.abs(in_angle - out_angle) > 180) + in_angle += 360; + return (in_angle + out_angle) / 2; + } + + private static double rad2deg(double rad) { + double RNSVG_radToDeg = 180 / Math.PI; + return rad * RNSVG_radToDeg; + } + + private static double SlopeAngleRadians(Point p) { + return Math.atan2(p.y, p.x); + } + + private static 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; + } + + private static Point subtract(Point p1, Point p2) { + return new Point(p2.x - p1.x, p2.y - p1.y); + } + + private static boolean isZero(Point p) { + return p.x == 0 && p.y == 0; + } + + private static void ComputeQuadTangents(SegmentData data, + Point start, + Point control, + Point end) { + data.start_tangent = subtract(control, start); + data.end_tangent = subtract(end, control); + if (isZero(data.start_tangent)) + data.start_tangent = data.end_tangent; + else if (isZero(data.end_tangent)) + data.end_tangent = data.start_tangent; + } + + private static SegmentData ExtractPathElementFeatures(PathElement element) { + SegmentData data = new SegmentData(); + Point[] 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 (isZero(data.start_tangent)) + ComputeQuadTangents(data, points[0], points[1], points[2]); + else if (isZero(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; + } + + private static void UpdateFromPathElement(PathElement 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 ? RNSVGMarkerType.kStartMarker : RNSVGMarkerType.kMidMarker; + double angle = CurrentAngle(marker_type); + positions_.add(new RNSVGMarkerPosition(marker_type, origin_, 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 == ElementType.kCGPathElementMoveToPoint) + subpath_start_ = element.points[0]; + else if (element.type == ElementType.kCGPathElementCloseSubpath) + subpath_start_ = new Point(0, 0); + ++element_index_; + } +} diff --git a/ios/Utils/RNSVGMarkerPosition.m b/ios/Utils/RNSVGMarkerPosition.m index 03ee267a7..b8e1aebef 100644 --- a/ios/Utils/RNSVGMarkerPosition.m +++ b/ios/Utils/RNSVGMarkerPosition.m @@ -24,6 +24,9 @@ + (instancetype) markerPosition:(RNSVGMarkerType)type origin:(CGPoint)origin ang + (NSArray*) fromCGPath:(CGPathRef)path { positions_ = [[NSMutableArray alloc] init]; + element_index_ = 0; + origin_ = RNSVGNULLPOINT; + subpath_start_ = RNSVGNULLPOINT; CGPathApply(path, (__bridge void *)positions_, UpdateFromPathElement); PathIsDone(); return positions_;