From 832522d1c19dbd07df669eb65eb20fe928b6826b Mon Sep 17 00:00:00 2001 From: SergeyYurkevich Date: Fri, 5 Jul 2024 16:11:48 +0300 Subject: [PATCH] feat: implement mask-type property (#2152) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Summary "mask-type: alpha" is not supported. resolve issue: #1790 ## Explanation svg example: ``` ``` Current behavior: ![image](https://github.com/software-mansion/react-native-svg/assets/17138397/2dca6f46-fe8f-48f3-80f9-799563911e8b) Expected behavior: ![image](https://github.com/software-mansion/react-native-svg/assets/17138397/fb49cf0b-d677-491f-8215-9c9b1d69080f) ## Compatibility | OS | Implemented | | ------- | :---------: | | iOS | ✅ | | Android | ✅ | ## Checklist - [x] I have tested this on a device and a simulator - [ ] I added documentation in `README.md` - [x] I updated the typed files (typescript) - [ ] I added a test for the API in the `__tests__` folder --------- Co-authored-by: Sergey Co-authored-by: Jakub Grzywacz --- .../main/java/com/horcrux/svg/MaskView.java | 23 ++++++++++++ .../java/com/horcrux/svg/RenderableView.java | 26 ++++++++------ .../horcrux/svg/RenderableViewManager.java | 5 +++ .../RNSVGMaskManagerDelegate.java | 3 ++ .../RNSVGMaskManagerInterface.java | 1 + apple/Brushes/RNSVGPainter.mm | 5 +-- apple/Elements/RNSVGImage.mm | 31 ++++++++-------- apple/Elements/RNSVGMask.h | 1 + apple/Elements/RNSVGMask.mm | 11 ++++++ apple/RNSVGRenderable.mm | 22 ++++++------ apple/Utils/RCTConvert+RNSVG.h | 2 ++ apple/Utils/RCTConvert+RNSVG.mm | 9 +++++ apple/Utils/RNSVGMaskType.h | 4 +++ apple/ViewManagers/RNSVGMaskManager.mm | 1 + apps/test-examples/App.js | 3 +- apps/test-examples/src/Test1790.tsx | 35 +++++++++++++++++++ src/elements/Mask.tsx | 19 ++++++++-- src/fabric/MaskNativeComponent.ts | 1 + src/lib/maskType.ts | 4 +++ 19 files changed, 166 insertions(+), 40 deletions(-) create mode 100644 apple/Utils/RNSVGMaskType.h create mode 100644 apps/test-examples/src/Test1790.tsx create mode 100644 src/lib/maskType.ts diff --git a/android/src/main/java/com/horcrux/svg/MaskView.java b/android/src/main/java/com/horcrux/svg/MaskView.java index 91369673..6dc4fc02 100644 --- a/android/src/main/java/com/horcrux/svg/MaskView.java +++ b/android/src/main/java/com/horcrux/svg/MaskView.java @@ -27,6 +27,13 @@ class MaskView extends GroupView { @SuppressWarnings({"FieldCanBeLocal", "unused"}) private Brush.BrushUnits mMaskContentUnits; + MaskType mMaskType; + + enum MaskType { + LUMINANCE, + ALPHA + } + public MaskView(ReactContext reactContext) { super(reactContext); } @@ -75,6 +82,22 @@ class MaskView extends GroupView { invalidate(); } + public MaskType getMaskType() { + return mMaskType; + } + + public void setMaskType(int maskType) { + switch (maskType) { + case 0: + mMaskType = MaskType.LUMINANCE; + break; + case 1: + mMaskType = MaskType.ALPHA; + break; + } + invalidate(); + } + @Override void saveDefinition() { if (mName != null) { diff --git a/android/src/main/java/com/horcrux/svg/RenderableView.java b/android/src/main/java/com/horcrux/svg/RenderableView.java index f1783221..e8143bf0 100644 --- a/android/src/main/java/com/horcrux/svg/RenderableView.java +++ b/android/src/main/java/com/horcrux/svg/RenderableView.java @@ -350,17 +350,21 @@ public abstract class RenderableView extends VirtualView implements ReactHitSlop // prepare step 3 - combined layer canvas.saveLayer(null, dstInPaint); - // step 1 - luminance layer - // prepare maskPaint with luminanceToAlpha - // https://www.w3.org/TR/SVG11/filters.html#InterfaceSVGFEMergeElement:~:text=not%20applicable.%20A-,luminanceToAlpha,-operation%20is%20equivalent - Paint luminancePaint = new Paint(); - ColorMatrix luminanceToAlpha = - new ColorMatrix( - new float[] { - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.2125f, 0.7154f, 0.0721f, 0, 0 - }); - luminancePaint.setColorFilter(new ColorMatrixColorFilter(luminanceToAlpha)); - canvas.saveLayer(null, luminancePaint); + if (mask.getMaskType() == MaskView.MaskType.LUMINANCE) { + // step 1 - luminance layer + // prepare maskPaint with luminanceToAlpha + // https://www.w3.org/TR/SVG11/filters.html#InterfaceSVGFEMergeElement:~:text=not%20applicable.%20A-,luminanceToAlpha,-operation%20is%20equivalent + Paint luminancePaint = new Paint(); + ColorMatrix luminanceToAlpha = + new ColorMatrix( + new float[]{ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.2125f, 0.7154f, 0.0721f, 0, 0 + }); + luminancePaint.setColorFilter(new ColorMatrixColorFilter(luminanceToAlpha)); + canvas.saveLayer(null, luminancePaint); + } else { + canvas.saveLayer(null, paint); + } // calculate mask bounds float maskX = (float) relativeOnWidth(mask.mX); diff --git a/android/src/main/java/com/horcrux/svg/RenderableViewManager.java b/android/src/main/java/com/horcrux/svg/RenderableViewManager.java index 1d4368bf..eb9ec480 100644 --- a/android/src/main/java/com/horcrux/svg/RenderableViewManager.java +++ b/android/src/main/java/com/horcrux/svg/RenderableViewManager.java @@ -1275,6 +1275,11 @@ class RenderableViewManager extends VirtualViewManager public void setMaskContentUnits(MaskView node, int maskContentUnits) { node.setMaskContentUnits(maskContentUnits); } + + @ReactProp(name = "maskType") + public void setMaskType(MaskView node, int maskType) { + node.setMaskType(maskType); + } } static class ForeignObjectManager extends GroupViewManagerAbstract diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGMaskManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGMaskManagerDelegate.java index bc2a92b2..8d7cd927 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGMaskManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGMaskManagerDelegate.java @@ -126,6 +126,9 @@ public class RNSVGMaskManagerDelegate { void setWidth(T view, Dynamic value); void setMaskUnits(T view, int value); void setMaskContentUnits(T view, int value); + void setMaskType(T view, int value); } diff --git a/apple/Brushes/RNSVGPainter.mm b/apple/Brushes/RNSVGPainter.mm index 6a30528d..9a640af4 100644 --- a/apple/Brushes/RNSVGPainter.mm +++ b/apple/Brushes/RNSVGPainter.mm @@ -237,10 +237,11 @@ void PatternFunction(void *info, CGContextRef context) rx = width; ry = height; CGGradientRelease(gradient); - NSArray *gradientArray = @[_colors.firstObject, _colors.lastObject, _colors[_colors.count-2], _colors.lastObject]; + NSArray *gradientArray = + @[ _colors.firstObject, _colors.lastObject, _colors[_colors.count - 2], _colors.lastObject ]; gradient = CGGradientRetain([RCTConvert RNSVGCGGradient:gradientArray]); } - + double ratio = ry / rx; CGFloat fx = [self getVal:[_points objectAtIndex:0] relative:width] + offsetX; diff --git a/apple/Elements/RNSVGImage.mm b/apple/Elements/RNSVGImage.mm index 1242c93c..62b961fc 100644 --- a/apple/Elements/RNSVGImage.mm +++ b/apple/Elements/RNSVGImage.mm @@ -136,10 +136,13 @@ using namespace facebook::react; auto imageSource = _state->getData().getImageSource(); imageSource.size = {image.size.width, image.size.height}; if (_eventEmitter != nullptr) { - static_cast(*_eventEmitter).onLoad({.source = { - .width = imageSource.size.width * imageSource.scale, - .height = imageSource.size.height * imageSource.scale, - .uri = imageSource.uri, }}); + static_cast(*_eventEmitter) + .onLoad( + {.source = { + .width = imageSource.size.width * imageSource.scale, + .height = imageSource.size.height * imageSource.scale, + .uri = imageSource.uri, + }}); } dispatch_async(dispatch_get_main_queue(), ^{ self->_image = CGImageRetain(image.CGImage); @@ -210,20 +213,20 @@ using namespace facebook::react; dispatch_async(dispatch_get_main_queue(), ^{ self->_image = CGImageRetain(image.CGImage); self->_imageSize = CGSizeMake(CGImageGetWidth(self->_image), CGImageGetHeight(self->_image)); - if (self->_onLoad) { - RCTImageSource *sourceLoaded; + if (self->_onLoad) { + RCTImageSource *sourceLoaded; #if TARGET_OS_OSX // [macOS] - sourceLoaded = [src imageSourceWithSize:image.size scale:1]; + sourceLoaded = [src imageSourceWithSize:image.size scale:1]; #else sourceLoaded = [src imageSourceWithSize:image.size scale:image.scale]; #endif - NSDictionary *dict = @{ - @"uri" : sourceLoaded.request.URL.absoluteString, - @"width" : @(sourceLoaded.size.width), - @"height" : @(sourceLoaded.size.height), - }; - self->_onLoad(@{@"source" : dict}); - } + NSDictionary *dict = @{ + @"uri" : sourceLoaded.request.URL.absoluteString, + @"width" : @(sourceLoaded.size.width), + @"height" : @(sourceLoaded.size.height), + }; + self->_onLoad(@{@"source" : dict}); + } [self invalidate]; }); }]; diff --git a/apple/Elements/RNSVGMask.h b/apple/Elements/RNSVGMask.h index bea4e4d7..04d71db3 100644 --- a/apple/Elements/RNSVGMask.h +++ b/apple/Elements/RNSVGMask.h @@ -10,5 +10,6 @@ @property (nonatomic, strong) RNSVGLength *maskheight; @property (nonatomic, assign) RNSVGUnits maskUnits; @property (nonatomic, assign) RNSVGUnits maskContentUnits; +@property (nonatomic, assign) RNSVGMaskType maskType; @end diff --git a/apple/Elements/RNSVGMask.mm b/apple/Elements/RNSVGMask.mm index 4113bf2e..bc1cee25 100644 --- a/apple/Elements/RNSVGMask.mm +++ b/apple/Elements/RNSVGMask.mm @@ -62,6 +62,7 @@ using namespace facebook::react; self.maskUnits = newProps.maskUnits == 0 ? kRNSVGUnitsObjectBoundingBox : kRNSVGUnitsUserSpaceOnUse; self.maskContentUnits = newProps.maskUnits == 0 ? kRNSVGUnitsObjectBoundingBox : kRNSVGUnitsUserSpaceOnUse; + self.maskType = newProps.maskType == 0 ? kRNSVGMaskTypeLuminance : kRNSVGMaskTypeAlpha; setCommonGroupProps(newProps, self); _props = std::static_pointer_cast(props); @@ -76,6 +77,7 @@ using namespace facebook::react; _maskwidth = nil; _maskUnits = kRNSVGUnitsObjectBoundingBox; _maskContentUnits = kRNSVGUnitsObjectBoundingBox; + _maskType = kRNSVGMaskTypeLuminance; } #endif // RCT_NEW_ARCH_ENABLED @@ -150,6 +152,15 @@ using namespace facebook::react; [self invalidate]; } +- (void)setMaskType:(RNSVGMaskType)maskType +{ + if (maskType == _maskType) { + return; + } + _maskType = maskType; + [self invalidate]; +} + @end #ifdef RCT_NEW_ARCH_ENABLED diff --git a/apple/RNSVGRenderable.mm b/apple/RNSVGRenderable.mm index afaa6887..02e01d08 100644 --- a/apple/RNSVGRenderable.mm +++ b/apple/RNSVGRenderable.mm @@ -294,16 +294,18 @@ UInt32 saturate(CGFloat value) // Apply luminanceToAlpha filter primitive // https://www.w3.org/TR/SVG11/filters.html#feColorMatrixElement UInt32 *currentPixel = pixels; - for (NSUInteger i = 0; i < npixels; i++) { - UInt32 color = *currentPixel; + if (_maskNode.maskType == kRNSVGMaskTypeLuminance) { + for (NSUInteger i = 0; i < npixels; i++) { + UInt32 color = *currentPixel; - UInt32 r = color & 0xFF; - UInt32 g = (color >> 8) & 0xFF; - UInt32 b = (color >> 16) & 0xFF; + UInt32 r = color & 0xFF; + UInt32 g = (color >> 8) & 0xFF; + UInt32 b = (color >> 16) & 0xFF; - CGFloat luma = (CGFloat)(0.299 * r + 0.587 * g + 0.144 * b); - *currentPixel = saturate(luma) << 24; - currentPixel++; + CGFloat luma = (CGFloat)(0.299 * r + 0.587 * g + 0.144 * b); + *currentPixel = saturate(luma) << 24; + currentPixel++; + } } // Create mask image and release memory @@ -361,11 +363,11 @@ UInt32 saturate(CGFloat value) CGContextScaleCTM(newContext, 1.0, -1.0); CGContextSetBlendMode(newContext, kCGBlendModeCopy); - CGContextDrawImage(newContext, maskBounds, maskImage); + CGContextDrawImage(newContext, maskBounds, maskImage); CGImageRelease(maskImage); CGContextSetBlendMode(newContext, kCGBlendModeSourceIn); - CGContextDrawImage(newContext, maskBounds, contentImage); + CGContextDrawImage(newContext, maskBounds, contentImage); CGImageRelease(contentImage); CGImageRef blendedImage = CGBitmapContextCreateImage(newContext); diff --git a/apple/Utils/RCTConvert+RNSVG.h b/apple/Utils/RCTConvert+RNSVG.h index cc0f3711..05d700e4 100644 --- a/apple/Utils/RCTConvert+RNSVG.h +++ b/apple/Utils/RCTConvert+RNSVG.h @@ -12,6 +12,7 @@ #import "RCTConvert+RNSVG.h" #import "RNSVGCGFCRule.h" #import "RNSVGLength.h" +#import "RNSVGMaskType.h" #import "RNSVGPathParser.h" #import "RNSVGUnits.h" #import "RNSVGVBMOS.h" @@ -25,6 +26,7 @@ + (RNSVGCGFCRule)RNSVGCGFCRule:(id)json; + (RNSVGVBMOS)RNSVGVBMOS:(id)json; + (RNSVGUnits)RNSVGUnits:(id)json; ++ (RNSVGMaskType)RNSVGMaskType:(id)json; + (RNSVGBrush *)RNSVGBrush:(id)json; + (RNSVGPathParser *)RNSVGCGPath:(NSString *)d; + (CGRect)RNSVGCGRect:(id)json offset:(NSUInteger)offset; diff --git a/apple/Utils/RCTConvert+RNSVG.mm b/apple/Utils/RCTConvert+RNSVG.mm index 91abfb9f..34606f83 100644 --- a/apple/Utils/RCTConvert+RNSVG.mm +++ b/apple/Utils/RCTConvert+RNSVG.mm @@ -42,6 +42,15 @@ RCT_ENUM_CONVERTER( kRNSVGUnitsObjectBoundingBox, intValue) +RCT_ENUM_CONVERTER( + RNSVGMaskType, + (@{ + @"luminance" : @(kRNSVGMaskTypeLuminance), + @"alpha" : @(kRNSVGMaskTypeAlpha), + }), + kRNSVGMaskTypeLuminance, + intValue) + + (RNSVGBrush *)RNSVGBrush:(id)json { if ([json isKindOfClass:[NSNumber class]]) { diff --git a/apple/Utils/RNSVGMaskType.h b/apple/Utils/RNSVGMaskType.h new file mode 100644 index 00000000..c4bdac7e --- /dev/null +++ b/apple/Utils/RNSVGMaskType.h @@ -0,0 +1,4 @@ +typedef CF_ENUM(int32_t, RNSVGMaskType) { + kRNSVGMaskTypeLuminance, + kRNSVGMaskTypeAlpha +}; diff --git a/apple/ViewManagers/RNSVGMaskManager.mm b/apple/ViewManagers/RNSVGMaskManager.mm index 8dbe8e7c..268c11a7 100644 --- a/apple/ViewManagers/RNSVGMaskManager.mm +++ b/apple/ViewManagers/RNSVGMaskManager.mm @@ -30,5 +30,6 @@ RCT_CUSTOM_VIEW_PROPERTY(width, id, RNSVGMask) } RCT_EXPORT_VIEW_PROPERTY(maskUnits, RNSVGUnits) RCT_EXPORT_VIEW_PROPERTY(maskContentUnits, RNSVGUnits) +RCT_EXPORT_VIEW_PROPERTY(maskType, RNSVGMaskType) @end diff --git a/apps/test-examples/App.js b/apps/test-examples/App.js index 7ed18944..7548dff6 100644 --- a/apps/test-examples/App.js +++ b/apps/test-examples/App.js @@ -8,6 +8,7 @@ import Test1374 from './src/Test1374'; import Test1442 from './src/Test1442'; import Test1451 from './src/Test1451'; import Test1718 from './src/Test1718'; +import Test1790 from './src/Test1790'; import Test1813 from './src/Test1813'; import Test1845 from './src/Test1845'; import Test1986 from './src/Test1986'; @@ -23,5 +24,5 @@ import Test2276 from './src/Test2276'; import Test2327 from './src/Test2327'; export default function App() { - return ; + return ; } diff --git a/apps/test-examples/src/Test1790.tsx b/apps/test-examples/src/Test1790.tsx new file mode 100644 index 00000000..28319e49 --- /dev/null +++ b/apps/test-examples/src/Test1790.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import {View} from 'react-native'; +import {SvgXml} from 'react-native-svg'; + +const svg = ` + + + + + + + + + + + + + + + +`; + +export default function Test1790() { + return ( + + + + ); +} diff --git a/src/elements/Mask.tsx b/src/elements/Mask.tsx index 26d7ff0d..33301fb7 100644 --- a/src/elements/Mask.tsx +++ b/src/elements/Mask.tsx @@ -6,8 +6,10 @@ import units from '../lib/units'; import Shape from './Shape'; import RNSVGMask from '../fabric/MaskNativeComponent'; import type { NativeMethods } from 'react-native'; +import { maskType } from '../lib/maskType'; export type TMaskUnits = 'userSpaceOnUse' | 'objectBoundingBox'; +export type TMaskType = 'alpha' | 'luminance'; export interface MaskProps extends CommonPathProps { children?: ReactNode; @@ -18,6 +20,10 @@ export interface MaskProps extends CommonPathProps { height?: NumberProp; maskUnits?: TMaskUnits; maskContentUnits?: TMaskUnits; + maskType?: TMaskType; + style?: { + maskType: TMaskType; + }; } export default class Mask extends Shape { @@ -32,8 +38,16 @@ export default class Mask extends Shape { render() { const { props } = this; - const { x, y, width, height, maskUnits, maskContentUnits, children } = - props; + const { + x, + y, + width, + height, + maskUnits, + maskContentUnits, + children, + style, + } = props; const maskProps = { x, y, @@ -42,6 +56,7 @@ export default class Mask extends Shape { maskUnits: maskUnits !== undefined ? units[maskUnits] : 0, maskContentUnits: maskContentUnits !== undefined ? units[maskContentUnits] : 1, + maskType: maskType[props?.maskType || style?.maskType || 'luminance'], }; return ( ; maskUnits?: Int32; maskContentUnits?: Int32; + maskType?: Int32; } export default codegenNativeComponent('RNSVGMask'); diff --git a/src/lib/maskType.ts b/src/lib/maskType.ts new file mode 100644 index 00000000..9fbd56f0 --- /dev/null +++ b/src/lib/maskType.ts @@ -0,0 +1,4 @@ +export const maskType = { + luminance: 0, + alpha: 1, +} as const;