Skip to content

Commit

Permalink
Fix spec conformance of clipping path with multiple child elements.
Browse files Browse the repository at this point in the history
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 #752
Fix #280
Fix #517

[android] Fix #766
`Region.Op.REPLACE` is deprecated in API level 28
Replace with clipPath (Path path) to Intersect instead.
  • Loading branch information
msand committed Aug 31, 2018
1 parent f1f0e2f commit a1097b8
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 30 deletions.
33 changes: 32 additions & 1 deletion android/src/main/java/com/horcrux/svg/GroupShadowNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -119,7 +121,36 @@ protected Path getPath(final Canvas canvas, final Paint paint) {
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);
}
}
}
});
Expand Down
5 changes: 5 additions & 0 deletions android/src/main/java/com/horcrux/svg/TextShadowNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ protected Path getPath(Canvas canvas, Paint paint) {
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();
Expand Down
15 changes: 11 additions & 4 deletions android/src/main/java/com/horcrux/svg/VirtualNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
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;
Expand Down Expand Up @@ -239,10 +241,15 @@ public void setResponsible(boolean responsible) {

@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);
Expand All @@ -265,7 +272,7 @@ void clip(Canvas canvas, Paint paint) {
Path clip = getClipPath(canvas, paint);

if (clip != null) {
canvas.clipPath(clip, Region.Op.REPLACE);
canvas.clipPath(clip);
}
}

Expand Down
2 changes: 2 additions & 0 deletions ios/Elements/RNSVGClipPath.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@

@interface RNSVGClipPath : RNSVGGroup

- (BOOL)isSimpleClipPath;

@end
17 changes: 12 additions & 5 deletions ios/Elements/RNSVGClipPath.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<UIView*> *children = self.subviews;
if (children.count == 1) {
UIView* child = children[0];
if ([child class] != [RNSVGGroup class]) {
return true;
}
}
return false;
}

@end
29 changes: 20 additions & 9 deletions ios/Elements/RNSVGGroup.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

#import "RNSVGGroup.h"
#import "RNSVGClipPath.h"

@implementation RNSVGGroup
{
Expand All @@ -33,7 +34,7 @@ - (void)renderLayerTo:(CGContextRef)context rect:(CGRect)rect
- (void)renderGroupTo:(CGContextRef)context rect:(CGRect)rect
{
[self pushGlyphContext];

__block CGRect groupRect = CGRectNull;

[self traverseSubviews:^(UIView *node) {
Expand All @@ -48,7 +49,7 @@ - (void)renderGroupTo:(CGContextRef)context rect:(CGRect)rect
}

[svgNode renderTo:context rect:rect];

CGRect nodeRect = svgNode.clientRect;
if (!CGRectIsEmpty(nodeRect)) {
groupRect = CGRectUnion(groupRect, nodeRect);
Expand Down Expand Up @@ -124,12 +125,22 @@ - (CGPathRef)getPath:(CGContextRef)context
- (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];
Expand All @@ -156,12 +167,12 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
return (node.responsible || (node != hitChild)) ? hitChild : self;
}
}

UIView *hitSelf = [super hitTest:transformed withEvent:event];
if (hitSelf) {
return hitSelf;
}

return nil;
}

Expand Down
39 changes: 34 additions & 5 deletions ios/RNSVGNode.m
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ @implementation RNSVGNode
RNSVGGlyphContext *glyphContext;
BOOL _transparent;
CGPathRef _cachedClipPath;
CGImageRef _clipMask;
}

CGFloat const RNSVG_M_SQRT1_2l = 0.707106781186547524400844362104849039;
Expand Down Expand Up @@ -169,8 +170,10 @@ - (void)setClipPath:(NSString *)clipPath
return;
}
CGPathRelease(_cachedClipPath);
CGImageRelease(_clipMask);
_cachedClipPath = nil;
_clipPath = clipPath;
_clipMask = nil;
[self invalidate];
}

Expand Down Expand Up @@ -201,7 +204,26 @@ - (CGPathRef)getClipPath
- (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];
Expand All @@ -212,11 +234,16 @@ - (void)clip:(CGContextRef)context
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);
}
}
}
Expand Down Expand Up @@ -343,6 +370,8 @@ - (void)traverseSubviews:(BOOL (^)(__kindof UIView *node))block
- (void)dealloc
{
CGPathRelease(_cachedClipPath);
CGImageRelease(_clipMask);
_clipMask = nil;
}

@end
23 changes: 17 additions & 6 deletions ios/RNSVGRenderable.m
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

#import "RNSVGRenderable.h"
#import "RNSVGClipPath.h"

@implementation RNSVGRenderable
{
Expand Down Expand Up @@ -187,7 +188,7 @@ - (void)renderLayerTo:(CGContextRef)context rect:(CGRect)rect
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);
Expand Down Expand Up @@ -270,15 +271,15 @@ - (void)setHitArea:(CGPathRef)path
_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];
CGPathRef strokePath = CGPathCreateCopyByStrokingPath(hitArea, nil, width, self.strokeLinecap, self.strokeLinejoin, self.strokeMiterlimit);
CGPathAddPath(hitArea, nil, strokePath);
CGPathRelease(strokePath);
}

_hitArea = CGPathRetain(CFAutorelease(CGPathCreateCopy(hitArea)));
CGPathRelease(hitArea);

Expand All @@ -304,9 +305,19 @@ - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
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;
Expand Down

0 comments on commit a1097b8

Please sign in to comment.