mirror of
https://github.com/zoriya/react-native-svg.git
synced 2025-12-06 07:06:11 +00:00
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:
@@ -58,6 +58,10 @@ class MaskView extends GroupView {
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public Brush.BrushUnits getMaskUnits() {
|
||||
return mMaskUnits;
|
||||
}
|
||||
|
||||
public void setMaskUnits(int maskUnits) {
|
||||
switch (maskUnits) {
|
||||
case 0:
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user