Skip to content

Commit

Permalink
feat: implement mask-type property (#2152)
Browse files Browse the repository at this point in the history
# Summary

"mask-type: alpha" is not supported. 
resolve issue: #1790  

## Explanation

svg example:
```
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100" fill="none">
<g clip-path="url(#clip0_8_3)">
<rect width="100" height="100" fill="white"/>
<mask id="mask0_8_3" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
<circle cx="50" cy="50" r="50" fill="#000000"/>
</mask>
<g mask="url(#mask0_8_3)">
<rect x="-26" y="-78" width="209" height="263" fill="#252E74"/>
</g>
</g>
<defs>
<clipPath id="clip0_8_3">
<rect width="100" height="100" fill="white"/>
</clipPath>
</defs>
</svg>
```

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

<!-- Check completed item, when applicable, via: [X] -->

- [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 <[email protected]>
Co-authored-by: Jakub Grzywacz <[email protected]>
  • Loading branch information
3 people authored Jul 5, 2024
1 parent 7c1602e commit 832522d
Show file tree
Hide file tree
Showing 19 changed files with 166 additions and 40 deletions.
23 changes: 23 additions & 0 deletions android/src/main/java/com/horcrux/svg/MaskView.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -75,6 +82,22 @@ public void setMaskContentUnits(int maskContentUnits) {
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) {
Expand Down
26 changes: 15 additions & 11 deletions android/src/main/java/com/horcrux/svg/RenderableView.java
Original file line number Diff line number Diff line change
Expand Up @@ -350,17 +350,21 @@ void render(Canvas canvas, Paint paint, float opacity) {
// 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1275,6 +1275,11 @@ public void setMaskUnits(MaskView node, int maskUnits) {
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<ForeignObjectView>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ public void setProperty(T view, String propName, @Nullable Object value) {
case "maskContentUnits":
mViewManager.setMaskContentUnits(view, value == null ? 0 : ((Double) value).intValue());
break;
case "maskType":
mViewManager.setMaskType(view, value == null ? 0 : ((Double) value).intValue());
break;
default:
super.setProperty(view, propName, value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,5 @@ public interface RNSVGMaskManagerInterface<T extends View> {
void setWidth(T view, Dynamic value);
void setMaskUnits(T view, int value);
void setMaskContentUnits(T view, int value);
void setMaskType(T view, int value);
}
5 changes: 3 additions & 2 deletions apple/Brushes/RNSVGPainter.mm
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,11 @@ - (void)paintRadialGradient:(CGContextRef)context bounds:(CGRect)bounds
rx = width;
ry = height;
CGGradientRelease(gradient);
NSArray<NSNumber *> *gradientArray = @[_colors.firstObject, _colors.lastObject, _colors[_colors.count-2], _colors.lastObject];
NSArray<NSNumber *> *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;
Expand Down
31 changes: 17 additions & 14 deletions apple/Elements/RNSVGImage.mm
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,13 @@ - (void)didReceiveImage:(UIImage *)image metadata:(id)metadata fromObserver:(voi
auto imageSource = _state->getData().getImageSource();
imageSource.size = {image.size.width, image.size.height};
if (_eventEmitter != nullptr) {
static_cast<const RNSVGImageEventEmitter &>(*_eventEmitter).onLoad({.source = {
.width = imageSource.size.width * imageSource.scale,
.height = imageSource.size.height * imageSource.scale,
.uri = imageSource.uri, }});
static_cast<const RNSVGImageEventEmitter &>(*_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);
Expand Down Expand Up @@ -210,20 +213,20 @@ - (void)setSrc:(RCTImageSource *)src
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];
});
}];
Expand Down
1 change: 1 addition & 0 deletions apple/Elements/RNSVGMask.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 11 additions & 0 deletions apple/Elements/RNSVGMask.mm
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &

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<RNSVGMaskProps const>(props);
Expand All @@ -76,6 +77,7 @@ - (void)prepareForRecycle
_maskwidth = nil;
_maskUnits = kRNSVGUnitsObjectBoundingBox;
_maskContentUnits = kRNSVGUnitsObjectBoundingBox;
_maskType = kRNSVGMaskTypeLuminance;
}
#endif // RCT_NEW_ARCH_ENABLED

Expand Down Expand Up @@ -150,6 +152,15 @@ - (void)setMaskContentUnits:(RNSVGUnits)maskContentUnits
[self invalidate];
}

- (void)setMaskType:(RNSVGMaskType)maskType
{
if (maskType == _maskType) {
return;
}
_maskType = maskType;
[self invalidate];
}

@end

#ifdef RCT_NEW_ARCH_ENABLED
Expand Down
22 changes: 12 additions & 10 deletions apple/RNSVGRenderable.mm
Original file line number Diff line number Diff line change
Expand Up @@ -294,16 +294,18 @@ - (void)renderTo:(CGContextRef)context rect:(CGRect)rect
// 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
Expand Down Expand Up @@ -361,11 +363,11 @@ - (void)renderTo:(CGContextRef)context rect:(CGRect)rect
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);
Expand Down
2 changes: 2 additions & 0 deletions apple/Utils/RCTConvert+RNSVG.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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;
Expand Down
9 changes: 9 additions & 0 deletions apple/Utils/RCTConvert+RNSVG.mm
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ @implementation RCTConvert (RNSVG)
kRNSVGUnitsObjectBoundingBox,
intValue)

RCT_ENUM_CONVERTER(
RNSVGMaskType,
(@{
@"luminance" : @(kRNSVGMaskTypeLuminance),
@"alpha" : @(kRNSVGMaskTypeAlpha),
}),
kRNSVGMaskTypeLuminance,
intValue)

+ (RNSVGBrush *)RNSVGBrush:(id)json
{
if ([json isKindOfClass:[NSNumber class]]) {
Expand Down
4 changes: 4 additions & 0 deletions apple/Utils/RNSVGMaskType.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
typedef CF_ENUM(int32_t, RNSVGMaskType) {
kRNSVGMaskTypeLuminance,
kRNSVGMaskTypeAlpha
};
1 change: 1 addition & 0 deletions apple/ViewManagers/RNSVGMaskManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@ - (RNSVGMask *)node
}
RCT_EXPORT_VIEW_PROPERTY(maskUnits, RNSVGUnits)
RCT_EXPORT_VIEW_PROPERTY(maskContentUnits, RNSVGUnits)
RCT_EXPORT_VIEW_PROPERTY(maskType, RNSVGMaskType)

@end
3 changes: 2 additions & 1 deletion apps/test-examples/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -23,5 +24,5 @@ import Test2276 from './src/Test2276';
import Test2327 from './src/Test2327';

export default function App() {
return <ColorTest />;
return <Test1790 />;
}
35 changes: 35 additions & 0 deletions apps/test-examples/src/Test1790.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import {View} from 'react-native';
import {SvgXml} from 'react-native-svg';

const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100" fill="none">
<g clip-path="url(#clip0_8_3)">
<rect width="100" height="100" fill="white"/>
<mask id="mask0_8_3" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
<circle cx="50" cy="50" r="50" fill="#7e7e7e"/>
</mask>
<g mask="url(#mask0_8_3)">
<rect x="-26" y="-78" width="209" height="263" fill="#252E74"/>
</g>
</g>
<defs>
<clipPath id="clip0_8_3">
<rect width="100" height="100" fill="white"/>
</clipPath>
</defs>
</svg>
`;

export default function Test1790() {
return (
<View
style={{
flex: 1,
backgroundColor: 'red',
alignItems: 'center',
justifyContent: 'center',
}}>
<SvgXml xml={svg} />
</View>
);
}
19 changes: 17 additions & 2 deletions src/elements/Mask.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<MaskProps> {
Expand All @@ -32,8 +38,16 @@ export default class Mask extends Shape<MaskProps> {

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,
Expand All @@ -42,6 +56,7 @@ export default class Mask extends Shape<MaskProps> {
maskUnits: maskUnits !== undefined ? units[maskUnits] : 0,
maskContentUnits:
maskContentUnits !== undefined ? units[maskContentUnits] : 1,
maskType: maskType[props?.maskType || style?.maskType || 'luminance'],
};
return (
<RNSVGMask
Expand Down
1 change: 1 addition & 0 deletions src/fabric/MaskNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ interface NativeProps
width?: UnsafeMixed<NumberProp>;
maskUnits?: Int32;
maskContentUnits?: Int32;
maskType?: Int32;
}

export default codegenNativeComponent<NativeProps>('RNSVGMask');
4 changes: 4 additions & 0 deletions src/lib/maskType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const maskType = {
luminance: 0,
alpha: 1,
} as const;

0 comments on commit 832522d

Please sign in to comment.