Skip to content

Commit

Permalink
feat: implement maskUnits (#2457)
Browse files Browse the repository at this point in the history
# Summary

Without the `maskUnits` attribute, masks may not render correctly, as
seen in issue #2449. This pull request adds support for `maskUnits` and
ensures proper cropping within the mask boundaries.

## Compatibility

| OS      | Implemented |
| ------- | :---------: |
| iOS     |    ✅      |
| MacOS   |    ✅      |
| Android |    ✅      |
  • Loading branch information
jakex7 authored Sep 23, 2024
1 parent 31ac520 commit 712201a
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 29 deletions.
4 changes: 4 additions & 0 deletions android/src/main/java/com/horcrux/svg/MaskView.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ public void setHeight(Dynamic height) {
invalidate();
}

public Brush.BrushUnits getMaskUnits() {
return mMaskUnits;
}

public void setMaskUnits(int maskUnits) {
switch (maskUnits) {
case 0:
Expand Down
27 changes: 21 additions & 6 deletions android/src/main/java/com/horcrux/svg/RenderableView.java
Original file line number Diff line number Diff line change
Expand Up @@ -411,12 +411,27 @@ void render(Canvas canvas, Paint paint, float opacity) {
}

// calculate mask bounds
float maskX = (float) relativeOnWidth(mask.mX);
float maskY = (float) relativeOnHeight(mask.mY);
float maskWidth = (float) relativeOnWidth(mask.mW);
float maskHeight = (float) relativeOnHeight(mask.mH);
RectF maskBounds;
if (mask.getMaskUnits() == Brush.BrushUnits.USER_SPACE_ON_USE) {
float maskX = (float) relativeOnWidth(mask.mX);
float maskY = (float) relativeOnHeight(mask.mY);
float maskWidth = (float) relativeOnWidth(mask.mW);
float maskHeight = (float) relativeOnHeight(mask.mH);
maskBounds = new RectF(maskX, maskY, maskX + maskWidth, maskY + maskHeight);
} else { // Brush.BrushUnits.OBJECT_BOUNDING_BOX
RectF clientRect = this.getClientRect();
if (this instanceof ImageView && clientRect == null) {
return;
}
mInvCTM.mapRect(clientRect);
float maskX = (float) relativeOnFraction(mask.mX, clientRect.width());
float maskY = (float) relativeOnFraction(mask.mY, clientRect.height());
float maskWidth = (float) relativeOnFraction(mask.mW, clientRect.width());
float maskHeight = (float) relativeOnFraction(mask.mH, clientRect.height());
maskBounds = new RectF(clientRect.left + maskX, clientRect.top + maskY, clientRect.left + maskX + maskWidth, clientRect.top + maskY + maskHeight);
}
// clip to mask bounds
canvas.clipRect(maskX, maskY, maskX + maskWidth, maskY + maskHeight);
canvas.clipRect(maskBounds);

mask.draw(canvas, paint, 1f);

Expand All @@ -426,7 +441,7 @@ void render(Canvas canvas, Paint paint, float opacity) {
// step 2 - alpha layer
canvas.saveLayer(null, dstInPaint);
// clip to mask bounds
canvas.clipRect(maskX, maskY, maskX + maskWidth, maskY + maskHeight);
canvas.clipRect(maskBounds);

mask.draw(canvas, paint, 1f);

Expand Down
6 changes: 3 additions & 3 deletions apple/Filters/RNSVGFeOffset.mm
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,12 @@ - (CIImage *)applyFilter:(NSMutableDictionary<NSString *, CIImage *> *)results
CGAffineTransform contextTransform = CGAffineTransformConcat(ctm, CGAffineTransformMakeTranslation(-ctm.tx, -ctm.ty));
#if !TARGET_OS_OSX // [macOS]
CGPoint translate = CGPointMake(dx, dy);
#else
CGPoint translate = CGPointMake(dx, -dy);
#else // [macOS
CGPoint translate = CGPointMake(dx, dy);
CGFloat scale = [RNSVGRenderUtils getScreenScale];
CGAffineTransform screenScaleCTM = CGAffineTransformMake(scale, 0, 0, scale, 0, 0);
translate = CGPointApplyAffineTransform(translate, screenScaleCTM);
#endif
#endif // macOS]
translate = CGPointApplyAffineTransform(translate, contextTransform);
CGAffineTransform transform = CGAffineTransformMakeTranslation(translate.x, translate.y);

Expand Down
39 changes: 21 additions & 18 deletions apple/RNSVGRenderable.mm
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,11 @@ - (void)renderTo:(CGContextRef)context rect:(CGRect)rect
CGAffineTransform screenScaleCTM = CGAffineTransformMake(scale, 0, 0, scale, 0, 0);
CGRect scaledRect = CGRectApplyAffineTransform(rect, screenScaleCTM);

#if TARGET_OS_OSX
CGImage *contentImage = [RNSVGRenderUtils renderToImage:self ctm:currentCTM rect:scaledRect clip:nil];
#else
CGImage *contentImage = [RNSVGRenderUtils renderToImage:self ctm:currentCTM rect:rect clip:nil];
#endif

if (filterNode) {
// https://www.w3.org/TR/SVG11/filters.html#FilterElement
Expand All @@ -292,12 +296,7 @@ - (void)renderTo:(CGContextRef)context rect:(CGRect)rect

if (!maskNode) {
CGContextConcatCTM(context, CGAffineTransformInvert(currentCTM));

// On macOS the currentCTM is inverted, so we need to transform it again
// https://stackoverflow.com/a/13358085
#if TARGET_OS_OSX
CGContextTranslateCTM(context, 0.0, rect.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CGContextDrawImage(context, rect, contentImage);
#else
CGContextDrawImage(context, scaledRect, contentImage);
Expand Down Expand Up @@ -328,18 +327,24 @@ - (void)renderTo:(CGContextRef)context rect:(CGRect)rect
#if TARGET_OS_OSX // [macOS]
// on macOS currentCTM is not scaled properly with screen scale so we need to scale it manually
CGContextConcatCTM(bcontext, screenScaleCTM);
CGContextTranslateCTM(bcontext, 0, rect.size.height);
CGContextScaleCTM(bcontext, 1, -1);

#endif // [macOS]
CGContextConcatCTM(bcontext, currentCTM);
// Clip to mask bounds and render the mask
CGSize currentBoundsSize = self.pathBounds.size;
CGFloat x = [self relativeOnFraction:[maskNode x] relative:currentBoundsSize.width];
CGFloat y = [self relativeOnFraction:[maskNode y] relative:currentBoundsSize.height];
CGFloat w = [self relativeOnFraction:[maskNode maskwidth] relative:currentBoundsSize.width];
CGFloat h = [self relativeOnFraction:[maskNode maskheight] relative:currentBoundsSize.height];
CGRect maskBounds = CGRectApplyAffineTransform(CGRectMake(x, y, w, h), screenScaleCTM);
CGRect maskBounds;
if ([maskNode maskUnits] == RNSVGUnits::kRNSVGUnitsUserSpaceOnUse) {
CGFloat x = [self relativeOn:[maskNode x] relative:width];
CGFloat y = [self relativeOn:[maskNode y] relative:height];
CGFloat w = [self relativeOn:[maskNode maskwidth] relative:width];
CGFloat h = [self relativeOn:[maskNode maskheight] relative:height];
maskBounds = CGRectMake(x, y, w, h);
} else {
CGSize currentBoundsSize = self.pathBounds.size;
CGFloat x = [self relativeOnFraction:[maskNode x] relative:currentBoundsSize.width];
CGFloat y = [self relativeOnFraction:[maskNode y] relative:currentBoundsSize.height];
CGFloat w = [self relativeOnFraction:[maskNode maskwidth] relative:currentBoundsSize.width];
CGFloat h = [self relativeOnFraction:[maskNode maskheight] relative:currentBoundsSize.height];
maskBounds = CGRectMake(self.pathBounds.origin.x + x, self.pathBounds.origin.y + y, w, h);
}
CGContextClipToRect(bcontext, maskBounds);
[maskNode renderLayerTo:bcontext rect:bounds];

Expand Down Expand Up @@ -394,12 +399,10 @@ - (void)renderTo:(CGContextRef)context rect:(CGRect)rect
UIGraphicsBeginImageContextWithOptions(rect.size, NO, scale);
CGContextRef newContext = UIGraphicsGetCurrentContext();

CGContextConcatCTM(newContext, CGAffineTransformInvert(CGContextGetCTM(newContext)));

CGContextSetBlendMode(newContext, kCGBlendModeCopy);
CGContextDrawImage(newContext, scaledRect, maskImage);
CGContextDrawImage(newContext, rect, maskImage);
CGContextSetBlendMode(newContext, kCGBlendModeSourceIn);
CGContextDrawImage(newContext, scaledRect, contentImage);
CGContextDrawImage(newContext, rect, contentImage);

CGImageRef blendedImage = CGBitmapContextCreateImage(newContext);
UIGraphicsEndImageContext();
Expand Down
9 changes: 7 additions & 2 deletions apple/Utils/RNSVGRenderUtils.mm
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,16 @@ + (CGImage *)renderToImage:(RNSVGRenderable *)renderable
clip:(CGRect *)clip
{
CGFloat scale = [self getScreenScale];
#if TARGET_OS_OSX // [macOS
UIGraphicsBeginImageContextWithOptions(rect.size, NO, 1.0);
#else // macOS]
UIGraphicsBeginImageContextWithOptions(rect.size, NO, scale);
#endif // [macOS]
CGContextRef cgContext = UIGraphicsGetCurrentContext();
#if !TARGET_OS_OSX
CGContextConcatCTM(cgContext, CGAffineTransformInvert(CGContextGetCTM(cgContext)));
#endif
#if TARGET_OS_OSX // [macOS
CGContextConcatCTM(cgContext, CGAffineTransformMakeScale(scale, scale));
#endif // macOS]
CGContextConcatCTM(cgContext, ctm);

if (clip) {
Expand Down

0 comments on commit 712201a

Please sign in to comment.