From a1097b85942c559ebcef6b93fa2ce601434d9c50 Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Sat, 1 Sep 2018 02:55:36 +0300 Subject: [PATCH] Fix spec conformance of clipping path with multiple child elements. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://www.w3.org/TR/SVG11/masking.html#EstablishingANewClippingPath The raw geometry of each child element exclusive of rendering properties such as ‘fill’, ‘stroke’, ‘stroke-width’ within a ‘clipPath’ conceptually defines a 1-bit mask (with the possible exception of anti-aliasing along the edge of the geometry) which represents the silhouette of the graphics associated with that element. Anything outside the outline of the object is masked out. If a child element is made invisible by ‘display’ or ‘visibility’ it does not contribute to the clipping path. When the ‘clipPath’ element contains multiple child elements, the silhouettes of the child elements are logically OR'd together to create a single silhouette which is then used to restrict the region onto which paint can be applied. Thus, a point is inside the clipping path if it is inside any of the children of the ‘clipPath’. For a given graphics element, the actual clipping path used will be the intersection of the clipping path specified by its ‘clip-path’ property (if any) with any clipping paths on its ancestors, as specified by the ‘clip-path’ property on the ancestor elements, or by the ‘overflow’ property on ancestor elements which establish a new viewport. Also, see the discussion of the initial clipping path.) Fixes issues highlighted by https://github.com/react-native-community/react-native-svg/issues/752 Fix https://github.com/react-native-community/react-native-svg/issues/280 Fix https://github.com/react-native-community/react-native-svg/issues/517 [android] Fix https://github.com/react-native-community/react-native-svg/issues/766 `Region.Op.REPLACE` is deprecated in API level 28 Replace with clipPath (Path path) to Intersect instead. --- .../java/com/horcrux/svg/GroupShadowNode.java | 33 +++++++++++++++- .../java/com/horcrux/svg/TextShadowNode.java | 5 +++ .../java/com/horcrux/svg/VirtualNode.java | 15 +++++-- ios/Elements/RNSVGClipPath.h | 2 + ios/Elements/RNSVGClipPath.m | 17 +++++--- ios/Elements/RNSVGGroup.m | 29 +++++++++----- ios/RNSVGNode.m | 39 ++++++++++++++++--- ios/RNSVGRenderable.m | 23 ++++++++--- 8 files changed, 133 insertions(+), 30 deletions(-) diff --git a/android/src/main/java/com/horcrux/svg/GroupShadowNode.java b/android/src/main/java/com/horcrux/svg/GroupShadowNode.java index 351c1eb6..f62f6071 100644 --- a/android/src/main/java/com/horcrux/svg/GroupShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/GroupShadowNode.java @@ -10,9 +10,11 @@ package com.horcrux.svg; import android.graphics.Canvas; +import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; +import android.os.Build; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.uimanager.ReactShadowNode; @@ -119,7 +121,36 @@ class GroupShadowNode extends RenderableShadowNode { traverseChildren(new NodeRunnable() { public void run(ReactShadowNode node) { if (node instanceof VirtualNode) { - path.addPath(((VirtualNode)node).getPath(canvas, paint)); + VirtualNode n = (VirtualNode)node; + Matrix transform = n.mMatrix; + path.addPath(n.getPath(canvas, paint), transform); + } + } + }); + + return path; + } + + protected Path getPath(final Canvas canvas, final Paint paint, final Path.Op op) { + final Path path = new Path(); + + traverseChildren(new NodeRunnable() { + public void run(ReactShadowNode node) { + if (node instanceof VirtualNode) { + VirtualNode n = (VirtualNode)node; + Matrix transform = n.mMatrix; + Path p2; + if (n instanceof GroupShadowNode) { + p2 = ((GroupShadowNode)n).getPath(canvas, paint, op); + } else { + p2 = n.getPath(canvas, paint); + } + p2.transform(transform); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + path.op(p2, op); + } else { + path.addPath(p2); + } } } }); diff --git a/android/src/main/java/com/horcrux/svg/TextShadowNode.java b/android/src/main/java/com/horcrux/svg/TextShadowNode.java index d41ded01..400395df 100644 --- a/android/src/main/java/com/horcrux/svg/TextShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/TextShadowNode.java @@ -140,6 +140,11 @@ class TextShadowNode extends GroupShadowNode { return groupPath; } + @Override + protected Path getPath(Canvas canvas, Paint paint, Path.Op op) { + return getPath(canvas, paint); + } + AlignmentBaseline getAlignmentBaseline() { if (mAlignmentBaseline == null) { ReactShadowNode parent = this.getParent(); diff --git a/android/src/main/java/com/horcrux/svg/VirtualNode.java b/android/src/main/java/com/horcrux/svg/VirtualNode.java index 761126f5..e1dadc6c 100644 --- a/android/src/main/java/com/horcrux/svg/VirtualNode.java +++ b/android/src/main/java/com/horcrux/svg/VirtualNode.java @@ -27,6 +27,8 @@ import com.facebook.react.uimanager.UIManagerModule; import com.facebook.react.uimanager.annotations.ReactProp; import com.facebook.react.uimanager.events.EventDispatcher; +import java.util.List; + import javax.annotation.Nullable; import static com.horcrux.svg.FontData.DEFAULT_FONT_SIZE; @@ -239,10 +241,15 @@ abstract class VirtualNode extends LayoutShadowNode { @Nullable Path getClipPath(Canvas canvas, Paint paint) { if (mClipPath != null) { - VirtualNode node = getSvgShadowNode().getDefinedClipPath(mClipPath); + ClipPathShadowNode mClipNode = (ClipPathShadowNode) getSvgShadowNode().getDefinedClipPath(mClipPath); - if (node != null) { - Path clipPath = node.getPath(canvas, paint); + if (mClipNode != null) { + Path clipPath; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { + clipPath = mClipNode.getPath(canvas, paint, Path.Op.UNION); + } else { + clipPath = mClipNode.getPath(canvas, paint); + } switch (mClipRule) { case CLIP_RULE_EVENODD: clipPath.setFillType(Path.FillType.EVEN_ODD); @@ -265,7 +272,7 @@ abstract class VirtualNode extends LayoutShadowNode { Path clip = getClipPath(canvas, paint); if (clip != null) { - canvas.clipPath(clip, Region.Op.REPLACE); + canvas.clipPath(clip); } } diff --git a/ios/Elements/RNSVGClipPath.h b/ios/Elements/RNSVGClipPath.h index bed8b4a2..27697e2a 100644 --- a/ios/Elements/RNSVGClipPath.h +++ b/ios/Elements/RNSVGClipPath.h @@ -14,4 +14,6 @@ @interface RNSVGClipPath : RNSVGGroup +- (BOOL)isSimpleClipPath; + @end diff --git a/ios/Elements/RNSVGClipPath.m b/ios/Elements/RNSVGClipPath.m index 3ea72d6f..3228920c 100644 --- a/ios/Elements/RNSVGClipPath.m +++ b/ios/Elements/RNSVGClipPath.m @@ -10,15 +10,22 @@ @implementation RNSVGClipPath -- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event -{ - return nil; -} - - (void)parseReference { [self.svgView defineClipPath:self clipPathName:self.name]; } +- (BOOL)isSimpleClipPath +{ + NSArray *children = self.subviews; + if (children.count == 1) { + UIView* child = children[0]; + if ([child class] != [RNSVGGroup class]) { + return true; + } + } + return false; +} + @end diff --git a/ios/Elements/RNSVGGroup.m b/ios/Elements/RNSVGGroup.m index 2366efc3..1871994c 100644 --- a/ios/Elements/RNSVGGroup.m +++ b/ios/Elements/RNSVGGroup.m @@ -7,6 +7,7 @@ */ #import "RNSVGGroup.h" +#import "RNSVGClipPath.h" @implementation RNSVGGroup { @@ -33,7 +34,7 @@ - (void)renderGroupTo:(CGContextRef)context rect:(CGRect)rect { [self pushGlyphContext]; - + __block CGRect groupRect = CGRectNull; [self traverseSubviews:^(UIView *node) { @@ -48,7 +49,7 @@ } [svgNode renderTo:context rect:rect]; - + CGRect nodeRect = svgNode.clientRect; if (!CGRectIsEmpty(nodeRect)) { groupRect = CGRectUnion(groupRect, nodeRect); @@ -124,12 +125,22 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { CGPoint transformed = CGPointApplyAffineTransform(point, self.invmatrix); - - CGPathRef clip = [self getClipPath]; - if (clip && !CGPathContainsPoint(clip, nil, transformed, self.clipRule == kRNSVGCGFCRuleEvenodd)) { - return nil; + + if (self.clipPath) { + RNSVGClipPath *clipNode = (RNSVGClipPath*)[self.svgView getDefinedClipPath:self.clipPath]; + if ([clipNode isSimpleClipPath]) { + CGPathRef clipPath = [self getClipPath]; + if (clipPath && !CGPathContainsPoint(clipPath, nil, transformed, self.clipRule == kRNSVGCGFCRuleEvenodd)) { + return nil; + } + } else { + RNSVGRenderable *clipGroup = (RNSVGRenderable*)clipNode; + if (![clipGroup hitTest:transformed withEvent:event]) { + return nil; + } + } } - + if (!event) { NSPredicate *const anyActive = [NSPredicate predicateWithFormat:@"active == TRUE"]; NSArray *const filtered = [self.subviews filteredArrayUsingPredicate:anyActive]; @@ -156,12 +167,12 @@ return (node.responsible || (node != hitChild)) ? hitChild : self; } } - + UIView *hitSelf = [super hitTest:transformed withEvent:event]; if (hitSelf) { return hitSelf; } - + return nil; } diff --git a/ios/RNSVGNode.m b/ios/RNSVGNode.m index adccc252..f157a399 100644 --- a/ios/RNSVGNode.m +++ b/ios/RNSVGNode.m @@ -22,6 +22,7 @@ RNSVGGlyphContext *glyphContext; BOOL _transparent; CGPathRef _cachedClipPath; + CGImageRef _clipMask; } CGFloat const RNSVG_M_SQRT1_2l = 0.707106781186547524400844362104849039; @@ -169,8 +170,10 @@ CGFloat const RNSVG_DEFAULT_FONT_SIZE = 12; return; } CGPathRelease(_cachedClipPath); + CGImageRelease(_clipMask); _cachedClipPath = nil; _clipPath = clipPath; + _clipMask = nil; [self invalidate]; } @@ -201,7 +204,26 @@ CGFloat const RNSVG_DEFAULT_FONT_SIZE = 12; - (CGPathRef)getClipPath:(CGContextRef)context { if (self.clipPath) { - _cachedClipPath = CGPathRetain([[self.svgView getDefinedClipPath:self.clipPath] getPath:context]); + RNSVGClipPath *_clipNode = (RNSVGClipPath*)[self.svgView getDefinedClipPath:self.clipPath]; + _cachedClipPath = CGPathRetain([_clipNode getPath:context]); + if (_clipMask) { + CGImageRelease(_clipMask); + } + if ([_clipNode isSimpleClipPath]) { + _clipMask = nil; + } else { + CGRect bounds = CGContextGetClipBoundingBox(context); + CGSize size = bounds.size; + + UIGraphicsBeginImageContextWithOptions(size, NO, 0.0); + CGContextRef newContext = UIGraphicsGetCurrentContext(); + CGContextTranslateCTM(newContext, 0.0, size.height); + CGContextScaleCTM(newContext, 1.0, -1.0); + + [_clipNode renderLayerTo:newContext rect:bounds]; + _clipMask = CGBitmapContextCreateImage(newContext); + UIGraphicsEndImageContext(); + } } return [self getClipPath]; @@ -212,11 +234,16 @@ CGFloat const RNSVG_DEFAULT_FONT_SIZE = 12; CGPathRef clipPath = [self getClipPath:context]; if (clipPath) { - CGContextAddPath(context, clipPath); - if (self.clipRule == kRNSVGCGFCRuleEvenodd) { - CGContextEOClip(context); + if (!_clipMask) { + CGContextAddPath(context, clipPath); + if (self.clipRule == kRNSVGCGFCRuleEvenodd) { + CGContextEOClip(context); + } else { + CGContextClip(context); + } } else { - CGContextClip(context); + CGRect bounds = CGContextGetClipBoundingBox(context); + CGContextClipToMask(context, bounds, _clipMask); } } } @@ -343,6 +370,8 @@ CGFloat const RNSVG_DEFAULT_FONT_SIZE = 12; - (void)dealloc { CGPathRelease(_cachedClipPath); + CGImageRelease(_clipMask); + _clipMask = nil; } @end diff --git a/ios/RNSVGRenderable.m b/ios/RNSVGRenderable.m index 98ee006e..c584ae1a 100644 --- a/ios/RNSVGRenderable.m +++ b/ios/RNSVGRenderable.m @@ -7,6 +7,7 @@ */ #import "RNSVGRenderable.h" +#import "RNSVGClipPath.h" @implementation RNSVGRenderable { @@ -187,7 +188,7 @@ self.path = CGPathRetain(CFAutorelease(CGPathCreateCopy([self getPath:context]))); [self setHitArea:self.path]; } - + const CGRect pathBounding = CGPathGetBoundingBox(self.path); const CGAffineTransform svgToClientTransform = CGAffineTransformConcat(CGContextGetCTM(context), self.svgView.invInitialCTM); self.clientRect = CGRectApplyAffineTransform(pathBounding, svgToClientTransform); @@ -270,7 +271,7 @@ _hitArea = nil; // Add path to hitArea CGMutablePathRef hitArea = CGPathCreateMutableCopy(path); - + if (self.stroke && self.strokeWidth) { // Add stroke to hitArea CGFloat width = [self relativeOnOther:self.strokeWidth]; @@ -278,7 +279,7 @@ CGPathAddPath(hitArea, nil, strokePath); CGPathRelease(strokePath); } - + _hitArea = CGPathRetain(CFAutorelease(CGPathCreateCopy(hitArea))); CGPathRelease(hitArea); @@ -304,9 +305,19 @@ return nil; } - CGPathRef clipPath = [self getClipPath]; - if (clipPath && !CGPathContainsPoint(clipPath, nil, transformed, self.clipRule == kRNSVGCGFCRuleEvenodd)) { - return nil; + if (self.clipPath) { + RNSVGClipPath *clipNode = (RNSVGClipPath*)[self.svgView getDefinedClipPath:self.clipPath]; + if ([clipNode isSimpleClipPath]) { + CGPathRef clipPath = [self getClipPath]; + if (clipPath && !CGPathContainsPoint(clipPath, nil, transformed, self.clipRule == kRNSVGCGFCRuleEvenodd)) { + return nil; + } + } else { + RNSVGRenderable *clipGroup = (RNSVGRenderable*)clipNode; + if (![clipGroup hitTest:transformed withEvent:event]) { + return nil; + } + } } return self;