Skip to content

Commit

Permalink
feat: add FeMerge and FeMergeNode filters (#2369)
Browse files Browse the repository at this point in the history
# Summary

As mentioned in #2362
Introduce new filters:
* `FeMerge`
* `FeMergeNode`

## Example usage

```tsx
<Svg width="200" height="200">
  <Filter id="mergeWithOffset" width="180" height="180">
    <FeOffset dx="50" dy="50" result="test" />
    <FeOffset dx="100" dy="100" in="SourceGraphic" />
    <FeMerge>
      <FeMergeNode in="SourceGraphic" />
      <FeMergeNode in="test" />
      <FeMergeNode />
    </FeMerge>
  </Filter>
  <Rect
    x="0"
    y="0"
    width="100"
    height="100"
    stroke="black"
    fill="red"
    filter="url(#mergeWithOffset)"
  />
</Svg>
```

<img width="207" alt="image"
src="https://github.com/user-attachments/assets/9cb3ded6-f939-4b2b-8ece-df54e64fe898">

## Test Plan

`Example` app -> `Filters` -> `FeMerge`

## Compatibility

| OS      | Implemented |
| ------- | :---------: |
| iOS     |    ✅     |
| Android |    ✅     |

## Checklist

- [x] I have tested this on a device and a simulator
- [x] I added documentation in `README.md`
- [x] I updated the typed files (typescript)
  • Loading branch information
jakex7 authored Jul 31, 2024
1 parent a2e843b commit b8b022c
Show file tree
Hide file tree
Showing 25 changed files with 508 additions and 9 deletions.
1 change: 1 addition & 0 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1261,6 +1261,7 @@ Currently supported\* filters are:

- FeColorMatrix
- FeGaussianBlur
- FeMerge
- FeOffset

\*_More filters are coming soon_
Expand Down
43 changes: 43 additions & 0 deletions android/src/main/java/com/horcrux/svg/FeMergeView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.horcrux.svg;

import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableType;
import java.util.HashMap;

@SuppressLint("ViewConstructor")
class FeMergeView extends FilterPrimitiveView {
private ReadableArray mNodes;

public FeMergeView(ReactContext reactContext) {
super(reactContext);
}

public void setNodes(ReadableArray nodes) {
this.mNodes = nodes;
invalidate();
}

@Override
public Bitmap applyFilter(HashMap<String, Bitmap> resultsMap, Bitmap prevResult) {
Bitmap result =
Bitmap.createBitmap(prevResult.getWidth(), prevResult.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(result);
int nodesSize = this.mNodes.size();
for (int i = 0; i < nodesSize; i++) {
Bitmap sourceFromResults =
this.mNodes.getType(i) == ReadableType.String
? resultsMap.get(this.mNodes.getString(i))
: prevResult;
if (sourceFromResults != null) {
canvas.drawBitmap(sourceFromResults, 0, 0, new Paint());
}
}

return result;
}
}
20 changes: 20 additions & 0 deletions android/src/main/java/com/horcrux/svg/RenderableViewManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@
import com.facebook.react.viewmanagers.RNSVGFeColorMatrixManagerInterface;
import com.facebook.react.viewmanagers.RNSVGFeGaussianBlurManagerDelegate;
import com.facebook.react.viewmanagers.RNSVGFeGaussianBlurManagerInterface;
import com.facebook.react.viewmanagers.RNSVGFeMergeManagerDelegate;
import com.facebook.react.viewmanagers.RNSVGFeMergeManagerInterface;
import com.facebook.react.viewmanagers.RNSVGFeOffsetManagerDelegate;
import com.facebook.react.viewmanagers.RNSVGFeOffsetManagerInterface;
import com.facebook.react.viewmanagers.RNSVGFilterManagerDelegate;
Expand Down Expand Up @@ -589,6 +591,7 @@ protected enum SVGClass {
RNSVGFilter,
RNSVGFeColorMatrix,
RNSVGFeGaussianBlur,
RNSVGFeMerge,
RNSVGFeOffset,
RNSVGMarker,
RNSVGForeignObject,
Expand Down Expand Up @@ -640,6 +643,8 @@ protected VirtualView createViewInstance(@Nonnull ThemedReactContext reactContex
return new FeColorMatrixView(reactContext);
case RNSVGFeGaussianBlur:
return new FeGaussianBlurView(reactContext);
case RNSVGFeMerge:
return new FeMergeView(reactContext);
case RNSVGFeOffset:
return new FeOffsetView(reactContext);
case RNSVGMarker:
Expand Down Expand Up @@ -1630,6 +1635,21 @@ public void setEdgeMode(FeGaussianBlurView node, String edgeMode) {
}
}

static class FeMergeManager extends FilterPrimitiveManager<FeMergeView>
implements RNSVGFeMergeManagerInterface<FeMergeView> {
FeMergeManager() {
super(SVGClass.RNSVGFeMerge);
mDelegate = new RNSVGFeMergeManagerDelegate(this);
}

public static final String REACT_CLASS = "RNSVGFeMerge";

@ReactProp(name = "nodes")
public void setNodes(FeMergeView node, ReadableArray nodes) {
node.setNodes(nodes);
}
}

static class FeOffsetManager extends FilterPrimitiveManager<FeOffsetView>
implements RNSVGFeOffsetManagerInterface<FeOffsetView> {
FeOffsetManager() {
Expand Down
9 changes: 9 additions & 0 deletions android/src/main/java/com/horcrux/svg/SvgPackage.java
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,15 @@ public NativeModule get() {
return new FeGaussianBlurManager();
}
}));
specs.put(
FeMergeManager.REACT_CLASS,
ModuleSpec.viewManagerSpec(
new Provider<NativeModule>() {
@Override
public NativeModule get() {
return new FeMergeManager();
}
}));
specs.put(
FeOffsetManager.REACT_CLASS,
ModuleSpec.viewManagerSpec(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
*
* Do not edit this file as changes may cause incorrect behavior and will be lost
* once the code is regenerated.
*
* @generated by codegen project: GeneratePropsJavaDelegate.js
*/

package com.facebook.react.viewmanagers;

import android.view.View;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.DynamicFromObject;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.uimanager.BaseViewManagerDelegate;
import com.facebook.react.uimanager.BaseViewManagerInterface;

public class RNSVGFeMergeManagerDelegate<T extends View, U extends BaseViewManagerInterface<T> & RNSVGFeMergeManagerInterface<T>> extends BaseViewManagerDelegate<T, U> {
public RNSVGFeMergeManagerDelegate(U viewManager) {
super(viewManager);
}
@Override
public void setProperty(T view, String propName, @Nullable Object value) {
switch (propName) {
case "x":
mViewManager.setX(view, new DynamicFromObject(value));
break;
case "y":
mViewManager.setY(view, new DynamicFromObject(value));
break;
case "width":
mViewManager.setWidth(view, new DynamicFromObject(value));
break;
case "height":
mViewManager.setHeight(view, new DynamicFromObject(value));
break;
case "result":
mViewManager.setResult(view, value == null ? null : (String) value);
break;
case "nodes":
mViewManager.setNodes(view, (ReadableArray) value);
break;
default:
super.setProperty(view, propName, value);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
*
* Do not edit this file as changes may cause incorrect behavior and will be lost
* once the code is regenerated.
*
* @generated by codegen project: GeneratePropsJavaInterface.js
*/

package com.facebook.react.viewmanagers;

import android.view.View;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.ReadableArray;

public interface RNSVGFeMergeManagerInterface<T extends View> {
void setX(T view, Dynamic value);
void setY(T view, Dynamic value);
void setWidth(T view, Dynamic value);
void setHeight(T view, Dynamic value);
void setResult(T view, @Nullable String value);
void setNodes(T view, @Nullable ReadableArray value);
}
7 changes: 7 additions & 0 deletions apple/Filters/RNSVGFeMerge.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#import "RNSVGFilterPrimitive.h"

@interface RNSVGFeMerge : RNSVGFilterPrimitive

@property (nonatomic, copy) NSArray<NSString *> *nodes;

@end
105 changes: 105 additions & 0 deletions apple/Filters/RNSVGFeMerge.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#import "RNSVGFeMerge.h"

#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTConversions.h>
#import <React/RCTFabricComponentsPlugins.h>
#import <react/renderer/components/rnsvg/ComponentDescriptors.h>
#import <react/renderer/components/view/conversions.h>
#import "RNSVGFabricConversions.h"
#endif // RCT_NEW_ARCH_ENABLED

@implementation RNSVGFeMerge

#ifdef RCT_NEW_ARCH_ENABLED
using namespace facebook::react;

- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared<const RNSVGFeMergeProps>();
_props = defaultProps;
}
return self;
}

#pragma mark - RCTComponentViewProtocol

+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<RNSVGFeMergeComponentDescriptor>();
}

- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
{
const auto &newProps = static_cast<const RNSVGFeMergeProps &>(*props);

if (newProps.nodes.size() > 0) {
NSMutableArray *nodesArray = [NSMutableArray new];
for (auto node : newProps.nodes) {
id json = RNSVGConvertFollyDynamicToId(node);
if ([json isKindOfClass:[NSString class]]) {
[nodesArray addObject:[json stringValue]];
} else {
[nodesArray addObject:[NSNull null]];
}
}
self.nodes = nodesArray;
}

setCommonFilterProps(newProps, self);
_props = std::static_pointer_cast<RNSVGFeMergeProps const>(props);
}

- (void)prepareForRecycle
{
[super prepareForRecycle];
_nodes = nil;
}
#endif // RCT_NEW_ARCH_ENABLED

- (void)setNodes:(NSArray<NSString *> *)nodes
{
if (nodes == _nodes) {
return;
}

_nodes = nodes;
[self invalidate];
}

- (CIImage *)applyFilter:(NSMutableDictionary<NSString *, CIImage *> *)results
previousFilterResult:(CIImage *)previous
ctm:(CGAffineTransform)ctm
{
CIFilter *filter = [CIFilter filterWithName:@"CISourceOverCompositing"];
[filter setDefaults];

CIImage *result;

for (int i = 0; i < [self.nodes count]; i++) {
NSString *nodeKey = [self.nodes objectAtIndex:i];
CIImage *inputImage =
[nodeKey isEqual:[NSNull null]] ? previous : [results objectForKey:[self.nodes objectAtIndex:i]];
if (inputImage == nil) {
continue;
}
if (result == nil) {
result = inputImage;
continue;
}
[filter setValue:result forKey:@"inputBackgroundImage"];
[filter setValue:inputImage forKey:@"inputImage"];

result = [filter valueForKey:@"outputImage"];
}
return result;
}

#ifdef RCT_NEW_ARCH_ENABLED
Class<RCTComponentViewProtocol> RNSVGFeMergeCls(void)
{
return RNSVGFeMerge.class;
}
#endif // RCT_NEW_ARCH_ENABLED

@end
5 changes: 5 additions & 0 deletions apple/ViewManagers/RNSVGFeMergeManager.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#import "RNSVGFilterPrimitiveManager.h"

@interface RNSVGFeMergeManager : RNSVGFilterPrimitiveManager

@end
15 changes: 15 additions & 0 deletions apple/ViewManagers/RNSVGFeMergeManager.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#import "RNSVGFeMergeManager.h"
#import "RNSVGFeMerge.h"

@implementation RNSVGFeMergeManager

RCT_EXPORT_MODULE()

- (RNSVGFeMerge *)node
{
return [RNSVGFeMerge new];
}

RCT_EXPORT_VIEW_PROPERTY(nodes, NSArray<NSString *>)

@end
Loading

0 comments on commit b8b022c

Please sign in to comment.