mirror of
https://github.com/zoriya/react-native-svg.git
synced 2025-12-05 22:56:11 +00:00
# Summary Introducing the long-awaited **Filters** in `react-native-svg` 🎉 ### Motivation This PR is the beginning of bringing support of SVG Filters into `react-native-svg`. * **related issues**: This PR series will address the following issues: #150, #176, #635, #883, #994, #996, #1216 * **feature overview**: This PR is a boilerplate for Filters * introducing `Filter` component and `FeColorMatrix` as a start. * It also introduces a new subdirectory called `react-native-svg/filter-image` with a `FilterImage` component. # Usage ## Filter and Fe... Filters are compatible with the web familiar standard, so most things should be compatible out-of-the-box and changes will be limited to using a capital letter as it's component. ### Example ```tsx import React from 'react'; import { FeColorMatrix, Filter, Rect, Svg } from 'react-native-svg'; export default () => { return ( <Svg height="300" width="300"> <Filter id="myFilter"> <FeColorMatrix type="saturate" values="0.2" /> </Filter> <Rect x="0" y="0" width="300" height="300" fill="red" filter="url(#myFilter)" /> </Svg> ); }; ```  ## Filter Image `FilterImage` is a new component that is not strictly related to SVG. Its behavior should be the same as a regular `Image` component from React Native with one exception - the additional prop `filters`, which accepts an array of filters to apply to the image. ### Example ```tsx import React from 'react'; import { StyleSheet } from 'react-native'; import { FilterImage } from 'react-native-svg/filter-image'; const myImage = require('./myImage.jpg'); export default () => { return ( <FilterImage style={styles.image} source={myImage} filters={[ { name: 'colorMatrix', type: 'saturate', values: 0.2 }, { name: 'colorMatrix', type: 'matrix', values: [ 0.2, 0.2, 0.2, 0, 0, 0.2, 0.2, 0.2, 0, 0, 0.2, 0.2, 0.2, 0, 0, 0, 0, 0, 1, 0, ], }, ]} /> ); }; const styles = StyleSheet.create({ image: { width: 200, height: 200, }, }); ```  ## Test Plan **Example App**: Updated the example app with various filter effects, showcasing real-world usage. ## Compatibility | OS | Implemented | | ------- | :---------: | | iOS | ✅ | | Android | ✅ | ## Checklist - [x] I have tested this on a device and a simulator - [x] I added documentation in `README.md` and `USAGE.md` - [x] I updated the typed files (typescript)
278 lines
11 KiB
Objective-C
278 lines
11 KiB
Objective-C
#import "RNSVGContextBrush.h"
|
|
#import "RNSVGFilterPrimitive.h"
|
|
#import "RNSVGGroup.h"
|
|
#import "RNSVGLength.h"
|
|
#import "RNSVGPainterBrush.h"
|
|
#import "RNSVGRenderable.h"
|
|
#import "RNSVGSolidColorBrush.h"
|
|
#import "RNSVGText.h"
|
|
#import "RNSVGVBMOS.h"
|
|
|
|
#import <React/RCTConversions.h>
|
|
#import <React/RCTFabricComponentsPlugins.h>
|
|
|
|
#import <folly/dynamic.h>
|
|
|
|
// copied from RCTFollyConvert
|
|
static id RNSVGConvertFollyDynamicToId(const folly::dynamic &dyn)
|
|
{
|
|
// I could imagine an implementation which avoids copies by wrapping the
|
|
// dynamic in a derived class of NSDictionary. We can do that if profiling
|
|
// implies it will help.
|
|
|
|
switch (dyn.type()) {
|
|
case folly::dynamic::NULLT:
|
|
return nil;
|
|
case folly::dynamic::BOOL:
|
|
return dyn.getBool() ? @YES : @NO;
|
|
case folly::dynamic::INT64:
|
|
return @(dyn.getInt());
|
|
case folly::dynamic::DOUBLE:
|
|
return @(dyn.getDouble());
|
|
case folly::dynamic::STRING:
|
|
return [[NSString alloc] initWithBytes:dyn.c_str() length:dyn.size() encoding:NSUTF8StringEncoding];
|
|
case folly::dynamic::ARRAY: {
|
|
NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:dyn.size()];
|
|
for (const auto &elem : dyn) {
|
|
id value = RNSVGConvertFollyDynamicToId(elem);
|
|
if (value) {
|
|
[array addObject:value];
|
|
}
|
|
}
|
|
return array;
|
|
}
|
|
case folly::dynamic::OBJECT: {
|
|
NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithCapacity:dyn.size()];
|
|
for (const auto &elem : dyn.items()) {
|
|
id key = RNSVGConvertFollyDynamicToId(elem.first);
|
|
id value = RNSVGConvertFollyDynamicToId(elem.second);
|
|
if (key && value) {
|
|
dict[key] = value;
|
|
}
|
|
}
|
|
return dict;
|
|
}
|
|
}
|
|
}
|
|
|
|
template <typename T>
|
|
RNSVGBrush *brushFromColorStruct(const T &fillObject)
|
|
{
|
|
int type = fillObject.type;
|
|
|
|
switch (type) {
|
|
case -1: // empty struct
|
|
return nil;
|
|
case 0: // solid color
|
|
{
|
|
// These are probably expensive allocations since it's often the same value.
|
|
// We should memoize colors but look ups may be just as expensive.
|
|
RNSVGColor *color = RCTUIColorFromSharedColor(fillObject.payload) ?: [RNSVGColor clearColor];
|
|
return [[RNSVGSolidColorBrush alloc] initWithColor:color];
|
|
}
|
|
case 1: // brush
|
|
{
|
|
NSArray *arr = @[ @(type), RCTNSStringFromString(fillObject.brushRef) ];
|
|
return [[RNSVGPainterBrush alloc] initWithArray:arr];
|
|
}
|
|
case 2: // currentColor
|
|
return [[RNSVGBrush alloc] initWithArray:nil];
|
|
case 3: // context-fill
|
|
return [[RNSVGContextBrush alloc] initFill];
|
|
case 4: // context-stroke
|
|
return [[RNSVGContextBrush alloc] initStroke];
|
|
default:
|
|
RCTLogError(@"Unknown brush type: %d", type);
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
template <typename T>
|
|
void setCommonNodeProps(const T &nodeProps, RNSVGNode *node)
|
|
{
|
|
node.name = RCTNSStringFromStringNilIfEmpty(nodeProps.name);
|
|
node.opacity = nodeProps.opacity;
|
|
if (nodeProps.matrix.size() == 6) {
|
|
node.matrix = CGAffineTransformMake(
|
|
nodeProps.matrix.at(0),
|
|
nodeProps.matrix.at(1),
|
|
nodeProps.matrix.at(2),
|
|
nodeProps.matrix.at(3),
|
|
nodeProps.matrix.at(4),
|
|
nodeProps.matrix.at(5));
|
|
}
|
|
CATransform3D transform3d = RCTCATransform3DFromTransformMatrix(nodeProps.transform);
|
|
CGAffineTransform transform = CATransform3DGetAffineTransform(transform3d);
|
|
node.invTransform = CGAffineTransformInvert(transform);
|
|
node.transforms = transform;
|
|
node.mask = RCTNSStringFromStringNilIfEmpty(nodeProps.mask);
|
|
node.markerStart = RCTNSStringFromStringNilIfEmpty(nodeProps.markerStart);
|
|
node.markerMid = RCTNSStringFromStringNilIfEmpty(nodeProps.markerMid);
|
|
node.markerEnd = RCTNSStringFromStringNilIfEmpty(nodeProps.markerEnd);
|
|
node.clipPath = RCTNSStringFromStringNilIfEmpty(nodeProps.clipPath);
|
|
node.clipRule = nodeProps.clipRule == 0 ? kRNSVGCGFCRuleEvenodd : kRNSVGCGFCRuleNonzero;
|
|
node.responsible = nodeProps.responsible;
|
|
// onLayout
|
|
node.display = RCTNSStringFromStringNilIfEmpty(nodeProps.display);
|
|
std::string pointerEvents = nodeProps.pointerEvents;
|
|
NSString *pointerEventsString = RCTNSStringFromStringNilIfEmpty(pointerEvents);
|
|
if ([pointerEventsString isEqualToString:@"auto"]) {
|
|
node.pointerEvents = RCTPointerEventsUnspecified;
|
|
} else if ([pointerEventsString isEqualToString:@"none"]) {
|
|
node.pointerEvents = RCTPointerEventsNone;
|
|
} else if ([pointerEventsString isEqualToString:@"box-none"]) {
|
|
node.pointerEvents = RCTPointerEventsNone;
|
|
} else if ([pointerEventsString isEqualToString:@"box-only"]) {
|
|
node.pointerEvents = RCTPointerEventsNone;
|
|
} else {
|
|
node.pointerEvents = RCTPointerEventsUnspecified;
|
|
}
|
|
node.accessibilityIdentifier = RCTNSStringFromStringNilIfEmpty(nodeProps.testId);
|
|
node.isAccessibilityElement = nodeProps.accessible;
|
|
node.accessibilityLabel = RCTNSStringFromStringNilIfEmpty(nodeProps.accessibilityLabel);
|
|
}
|
|
|
|
template <typename T>
|
|
void setCommonRenderableProps(const T &renderableProps, RNSVGRenderable *renderableNode)
|
|
{
|
|
setCommonNodeProps(renderableProps, renderableNode);
|
|
renderableNode.fill = brushFromColorStruct(renderableProps.fill);
|
|
renderableNode.fillOpacity = renderableProps.fillOpacity;
|
|
renderableNode.fillRule = renderableProps.fillRule == 0 ? kRNSVGCGFCRuleEvenodd : kRNSVGCGFCRuleNonzero;
|
|
renderableNode.stroke = brushFromColorStruct(renderableProps.stroke);
|
|
renderableNode.strokeOpacity = renderableProps.strokeOpacity;
|
|
id strokeWidth = RNSVGConvertFollyDynamicToId(renderableProps.strokeWidth);
|
|
if (strokeWidth != nil) {
|
|
renderableNode.strokeWidth = [RCTConvert RNSVGLength:strokeWidth];
|
|
}
|
|
renderableNode.strokeLinecap = renderableProps.strokeLinecap == 0 ? kCGLineCapButt
|
|
: renderableProps.strokeLinecap == 1 ? kCGLineCapRound
|
|
: kCGLineCapSquare;
|
|
renderableNode.strokeLinejoin = renderableProps.strokeLinejoin == 0 ? kCGLineJoinMiter
|
|
: renderableProps.strokeLinejoin == 1 ? kCGLineJoinRound
|
|
: kCGLineJoinBevel;
|
|
id strokeDasharray = RNSVGConvertFollyDynamicToId(renderableProps.strokeDasharray);
|
|
if (strokeDasharray != nil) {
|
|
renderableNode.strokeDasharray = [RCTConvert RNSVGLengthArray:strokeDasharray];
|
|
}
|
|
renderableNode.strokeDashoffset = renderableProps.strokeDashoffset;
|
|
renderableNode.strokeMiterlimit = renderableProps.strokeMiterlimit;
|
|
renderableNode.vectorEffect = renderableProps.vectorEffect == 0 ? kRNSVGVectorEffectDefault
|
|
: renderableProps.vectorEffect == 1 ? kRNSVGVectorEffectNonScalingStroke
|
|
: renderableProps.vectorEffect == 2 ? kRNSVGVectorEffectInherit
|
|
: kRNSVGVectorEffectUri;
|
|
if (renderableProps.propList.size() > 0) {
|
|
NSMutableArray<NSString *> *propArray = [NSMutableArray new];
|
|
for (auto str : renderableProps.propList) {
|
|
[propArray addObject:RCTNSStringFromString(str)];
|
|
}
|
|
renderableNode.propList = propArray;
|
|
}
|
|
renderableNode.filter = RCTNSStringFromStringNilIfEmpty(renderableProps.filter);
|
|
}
|
|
|
|
template <typename T>
|
|
void setCommonGroupProps(const T &groupProps, RNSVGGroup *groupNode)
|
|
{
|
|
setCommonRenderableProps(groupProps, groupNode);
|
|
|
|
id fontSize = RNSVGConvertFollyDynamicToId(groupProps.fontSize);
|
|
if (fontSize != nil) {
|
|
groupNode.font = @{@"fontSize" : fontSize};
|
|
}
|
|
id fontWeight = RNSVGConvertFollyDynamicToId(groupProps.fontWeight);
|
|
if (fontWeight != nil) {
|
|
groupNode.font = @{@"fontWeight" : fontWeight};
|
|
}
|
|
id font = RNSVGConvertFollyDynamicToId(groupProps.font);
|
|
if (font != nil) {
|
|
NSDictionary *fontDict = (NSDictionary *)font;
|
|
if (groupNode.font == nil || fontDict.count > 0) {
|
|
// some of text's rendering logic requires that `font` is not nil so we always set it
|
|
// even if to an empty dict
|
|
groupNode.font = fontDict;
|
|
}
|
|
}
|
|
}
|
|
|
|
template <typename T>
|
|
void setCommonFilterProps(const T &filterProps, RNSVGFilterPrimitive *filterPrimitiveNode)
|
|
{
|
|
id x = RNSVGConvertFollyDynamicToId(filterProps.x);
|
|
if (x != nil) {
|
|
filterPrimitiveNode.x = [RCTConvert RNSVGLength:x];
|
|
}
|
|
id y = RNSVGConvertFollyDynamicToId(filterProps.y);
|
|
if (y != nil) {
|
|
filterPrimitiveNode.y = [RCTConvert RNSVGLength:y];
|
|
}
|
|
id height = RNSVGConvertFollyDynamicToId(filterProps.height);
|
|
if (height != nil) {
|
|
filterPrimitiveNode.height = [RCTConvert RNSVGLength:height];
|
|
}
|
|
id width = RNSVGConvertFollyDynamicToId(filterProps.width);
|
|
if (width != nil) {
|
|
filterPrimitiveNode.width = [RCTConvert RNSVGLength:width];
|
|
}
|
|
filterPrimitiveNode.result = RCTNSStringFromStringNilIfEmpty(filterProps.result);
|
|
}
|
|
|
|
template <typename T>
|
|
void setCommonTextProps(const T &textProps, RNSVGText *textNode)
|
|
{
|
|
setCommonGroupProps(textProps, textNode);
|
|
id deltaX = RNSVGConvertFollyDynamicToId(textProps.dx);
|
|
if (deltaX != nil) {
|
|
textNode.deltaX = [RCTConvert RNSVGLengthArray:deltaX];
|
|
}
|
|
id deltaY = RNSVGConvertFollyDynamicToId(textProps.dy);
|
|
if (deltaY != nil) {
|
|
textNode.deltaY = [RCTConvert RNSVGLengthArray:deltaY];
|
|
}
|
|
id positionX = RNSVGConvertFollyDynamicToId(textProps.x);
|
|
if (positionX != nil) {
|
|
textNode.positionX = [RCTConvert RNSVGLengthArray:positionX];
|
|
}
|
|
id positionY = RNSVGConvertFollyDynamicToId(textProps.y);
|
|
if (positionY != nil) {
|
|
textNode.positionY = [RCTConvert RNSVGLengthArray:positionY];
|
|
}
|
|
id rotate = RNSVGConvertFollyDynamicToId(textProps.rotate);
|
|
if (rotate != nil) {
|
|
textNode.rotate = [RCTConvert RNSVGLengthArray:rotate];
|
|
}
|
|
id textLength = RNSVGConvertFollyDynamicToId(textProps.textLength);
|
|
if (textLength != nil) {
|
|
textNode.textLength = [RCTConvert RNSVGLength:textLength];
|
|
}
|
|
id inlineSize = RNSVGConvertFollyDynamicToId(textProps.inlineSize);
|
|
if (inlineSize != nil) {
|
|
textNode.inlineSize = [RCTConvert RNSVGLength:inlineSize];
|
|
}
|
|
id baselineShift = RNSVGConvertFollyDynamicToId(textProps.baselineShift);
|
|
if (baselineShift != nil) {
|
|
if ([baselineShift isKindOfClass:[NSString class]]) {
|
|
NSString *stringValue = (NSString *)baselineShift;
|
|
textNode.baselineShift = stringValue;
|
|
} else {
|
|
textNode.baselineShift = [NSString stringWithFormat:@"%f", [baselineShift doubleValue]];
|
|
}
|
|
}
|
|
textNode.lengthAdjust = RCTNSStringFromStringNilIfEmpty(textProps.lengthAdjust);
|
|
textNode.alignmentBaseline = RCTNSStringFromStringNilIfEmpty(textProps.alignmentBaseline);
|
|
}
|
|
|
|
static RNSVGVBMOS intToRNSVGVBMOS(int value)
|
|
{
|
|
switch (value) {
|
|
case 0:
|
|
return kRNSVGVBMOSMeet;
|
|
case 1:
|
|
return kRNSVGVBMOSSlice;
|
|
case 2:
|
|
return kRNSVGVBMOSNone;
|
|
default:
|
|
return kRNSVGVBMOSMeet;
|
|
}
|
|
}
|