feat: implement maskUnits (#2457)

# 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 |          |
This commit is contained in:
Jakub Grzywacz
2024-09-23 14:03:15 +02:00
committed by GitHub
parent 31ac520f63
commit 712201a19e
5 changed files with 56 additions and 29 deletions

View File

@@ -58,6 +58,10 @@ class MaskView extends GroupView {
invalidate();
}
public Brush.BrushUnits getMaskUnits() {
return mMaskUnits;
}
public void setMaskUnits(int maskUnits) {
switch (maskUnits) {
case 0:

View File

@@ -411,12 +411,27 @@ public abstract class RenderableView extends VirtualView implements ReactHitSlop
}
// calculate mask bounds
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);
@@ -426,7 +441,7 @@ public abstract class RenderableView extends VirtualView implements ReactHitSlop
// 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);

View File

@@ -117,12 +117,12 @@ using namespace facebook::react;
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);

View File

@@ -270,7 +270,11 @@ UInt32 saturate(CGFloat value)
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
@@ -292,12 +296,7 @@ UInt32 saturate(CGFloat value)
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);
@@ -328,18 +327,24 @@ UInt32 saturate(CGFloat value)
#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
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];
CGRect maskBounds = CGRectApplyAffineTransform(CGRectMake(x, y, w, h), screenScaleCTM);
maskBounds = CGRectMake(self.pathBounds.origin.x + x, self.pathBounds.origin.y + y, w, h);
}
CGContextClipToRect(bcontext, maskBounds);
[maskNode renderLayerTo:bcontext rect:bounds];
@@ -394,12 +399,10 @@ UInt32 saturate(CGFloat value)
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();

View File

@@ -36,11 +36,16 @@
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) {