Skip to content

Commit

Permalink
feat: FeBlend (#2489)
Browse files Browse the repository at this point in the history
# Summary

Continuation of #2362 implementing `FeBlend` filter
https://www.w3.org/TR/SVG11/filters.html#feBlendElement

## Test Plan

Example app → Filters → `FeBlend`

## Compatibility

| OS      | Implemented |
| ------- | :---------: |
| iOS     |    ✅      |
| macOS   |    ✅ _*_      |
| Android |    ✅      |
| Web     |    ✅      |
  • Loading branch information
jakex7 authored Oct 16, 2024
1 parent cb30bd6 commit 096fdc2
Show file tree
Hide file tree
Showing 24 changed files with 736 additions and 4 deletions.
2 changes: 1 addition & 1 deletion USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1300,6 +1300,7 @@ Filter effects are a way of processing an element’s rendering before it is dis
The following filters have been implemented:

- FeBlend
- FeColorMatrix
- FeFlood
- FeGaussianBlur
Expand All @@ -1308,7 +1309,6 @@ The following filters have been implemented:

Not supported yet:

- FeBlend
- FeComponentTransfer
- FeComposite
- FeConvolveMatrix
Expand Down
61 changes: 61 additions & 0 deletions android/src/main/java/com/horcrux/svg/CustomFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.horcrux.svg;

import android.graphics.Bitmap;

interface CustomFilterFunction {
float[] execute(float[] src, float[] dst);
}

public class CustomFilter {
public static Bitmap apply(Bitmap srcBmp, Bitmap dstBmp, CustomFilterFunction func) {
int width = srcBmp.getWidth();
int height = srcBmp.getHeight();
int[] srcPixels = new int[width * height];
int[] dstPixels = new int[width * height];
int[] resPixels = new int[width * height];
int srcArgb = 0;
float[] src = new float[] {0, 0, 0, 0};
int dstArgb = 0;
float[] dst = new float[] {0, 0, 0, 0};

try {
srcBmp.getPixels(srcPixels, 0, width, 0, 0, width, height);
dstBmp.getPixels(dstPixels, 0, width, 0, 0, width, height);
} catch (IllegalArgumentException | ArrayIndexOutOfBoundsException ignored) {
}

for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
srcArgb = srcPixels[y * width + x];
src[0] = ((srcArgb >> 24) & 0xff) / 255f;
src[1] = ((srcArgb >> 16) & 0xff) / 255f;
src[2] = ((srcArgb >> 8) & 0xff) / 255f;
src[3] = (srcArgb & 0xff) / 255f;

dstArgb = dstPixels[y * width + x];
dst[0] = ((dstArgb >> 24) & 0xff) / 255f;
dst[1] = ((dstArgb >> 16) & 0xff) / 255f;
dst[2] = ((dstArgb >> 8) & 0xff) / 255f;
dst[3] = (dstArgb & 0xff) / 255f;

resPixels[y * width + x] = normalizeFromFloats(func.execute(src, dst));
}
}

return Bitmap.createBitmap(resPixels, width, height, Bitmap.Config.ARGB_8888);
}

public static int normalizeFromFloat(float c) {
return Math.min(255, Math.max(0, Math.round(c * 255)));
}

public static int normalizeFromFloats(float[] res) {
if (res.length < 4 || normalizeFromFloat(res[0]) <= 0) {
return 0;
}
return (normalizeFromFloat(res[0]) << 24)
| (normalizeFromFloat(res[1] / res[0]) << 16)
| (normalizeFromFloat(res[2] / res[0]) << 8)
| normalizeFromFloat(res[3] / res[0]);
}
}
92 changes: 92 additions & 0 deletions android/src/main/java/com/horcrux/svg/FeBlendView.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.horcrux.svg;

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

@SuppressLint("ViewConstructor")
class FeBlendView extends FilterPrimitiveView {
String mIn1;
String mIn2;
FilterProperties.FeBlendMode mMode;

public FeBlendView(ReactContext reactContext) {
super(reactContext);
super.mFilterSubregion.mX = new SVGLength(0);
super.mFilterSubregion.mY = new SVGLength(0);
super.mFilterSubregion.mW = new SVGLength("100%");
super.mFilterSubregion.mH = new SVGLength("100%");
}

public void setIn1(String in1) {
this.mIn1 = in1;
invalidate();
}

public void setIn2(String in2) {
this.mIn2 = in2;
invalidate();
}

public void setMode(String mode) {
this.mMode = FilterProperties.FeBlendMode.getEnum(mode);
invalidate();
}

@Override
public Bitmap applyFilter(HashMap<String, Bitmap> resultsMap, Bitmap prevResult) {
Bitmap in1 = getSource(resultsMap, prevResult, this.mIn1);
Bitmap in2 = getSource(resultsMap, prevResult, this.mIn2);

if (this.mMode == FilterProperties.FeBlendMode.MULTIPLY) {
CustomFilterFunction multiply =
(src, dst) -> {
float[] res = new float[4];
res[0] = 1f - (1f - src[0]) * (1f - dst[0]);
res[1] =
src[1] * src[0] * (1f - dst[0])
+ dst[1] * dst[0] * (1f - src[0])
+ src[1] * src[0] * dst[1] * dst[0];
res[2] =
src[2] * src[0] * (1f - dst[0])
+ dst[2] * dst[0] * (1f - src[0])
+ src[2] * src[0] * dst[2] * dst[0];
res[3] =
src[3] * src[0] * (1f - dst[0])
+ dst[3] * dst[0] * (1f - src[0])
+ src[3] * src[0] * dst[3] * dst[0];
return res;
};
return CustomFilter.apply(in1, in2, multiply);
}

Bitmap result = Bitmap.createBitmap(in1.getWidth(), in1.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(result);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
canvas.drawBitmap(in1, 0, 0, paint);

switch (this.mMode) {
case NORMAL -> {
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OVER));
}
case SCREEN -> {
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SCREEN));
}
case LIGHTEN -> {
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.LIGHTEN));
}
case DARKEN -> {
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DARKEN));
}
case MULTIPLY -> {}
}
canvas.drawBitmap(in2, 0, 0, paint);

return result;
}
}
37 changes: 37 additions & 0 deletions android/src/main/java/com/horcrux/svg/FilterProperties.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,43 @@ public String toString() {
}
}

enum FeBlendMode {
UNKNOWN("unknown"),
NORMAL("normal"),
MULTIPLY("multiply"),
SCREEN("screen"),
DARKEN("darken"),
LIGHTEN("lighten"),
;

private final String mode;

FeBlendMode(String mode) {
this.mode = mode;
}

static FeBlendMode getEnum(String strVal) {
if (!typeToEnum.containsKey(strVal)) {
throw new IllegalArgumentException("Unknown String Value: " + strVal);
}
return typeToEnum.get(strVal);
}

private static final Map<String, FeBlendMode> typeToEnum = new HashMap<>();

static {
for (final FeBlendMode en : FeBlendMode.values()) {
typeToEnum.put(en.mode, en);
}
}

@Nonnull
@Override
public String toString() {
return mode;
}
}

enum FeColorMatrixType {
MATRIX("matrix"),
SATURATE("saturate"),
Expand Down
30 changes: 30 additions & 0 deletions android/src/main/java/com/horcrux/svg/RenderableViewManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@
import com.facebook.react.viewmanagers.RNSVGDefsManagerInterface;
import com.facebook.react.viewmanagers.RNSVGEllipseManagerDelegate;
import com.facebook.react.viewmanagers.RNSVGEllipseManagerInterface;
import com.facebook.react.viewmanagers.RNSVGFeBlendManagerDelegate;
import com.facebook.react.viewmanagers.RNSVGFeBlendManagerInterface;
import com.facebook.react.viewmanagers.RNSVGFeColorMatrixManagerDelegate;
import com.facebook.react.viewmanagers.RNSVGFeColorMatrixManagerInterface;
import com.facebook.react.viewmanagers.RNSVGFeFloodManagerDelegate;
Expand Down Expand Up @@ -591,6 +593,7 @@ protected enum SVGClass {
RNSVGPattern,
RNSVGMask,
RNSVGFilter,
RNSVGFeBlend,
RNSVGFeColorMatrix,
RNSVGFeFlood,
RNSVGFeGaussianBlur,
Expand Down Expand Up @@ -642,6 +645,8 @@ protected VirtualView createViewInstance(@Nonnull ThemedReactContext reactContex
return new MaskView(reactContext);
case RNSVGFilter:
return new FilterView(reactContext);
case RNSVGFeBlend:
return new FeBlendView(reactContext);
case RNSVGFeColorMatrix:
return new FeColorMatrixView(reactContext);
case RNSVGFeFlood:
Expand Down Expand Up @@ -1585,6 +1590,31 @@ public void setResult(T node, String result) {
}
}

static class FeBlendManager extends FilterPrimitiveManager<FeBlendView>
implements RNSVGFeBlendManagerInterface<FeBlendView> {
FeBlendManager() {
super(SVGClass.RNSVGFeBlend);
mDelegate = new RNSVGFeBlendManagerDelegate(this);
}

public static final String REACT_CLASS = "RNSVGFeBlend";

@ReactProp(name = "in1")
public void setIn1(FeBlendView node, String in1) {
node.setIn1(in1);
}

@ReactProp(name = "in2")
public void setIn2(FeBlendView node, String in2) {
node.setIn2(in2);
}

@ReactProp(name = "mode")
public void setMode(FeBlendView node, String mode) {
node.setMode(mode);
}
}

static class FeColorMatrixManager extends FilterPrimitiveManager<FeColorMatrixView>
implements RNSVGFeColorMatrixManagerInterface<FeColorMatrixView> {
FeColorMatrixManager() {
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 @@ -214,6 +214,15 @@ public NativeModule get() {
return new FilterManager();
}
}));
specs.put(
FeBlendManager.REACT_CLASS,
ModuleSpec.viewManagerSpec(
new Provider<NativeModule>() {
@Override
public NativeModule get() {
return new FeBlendManager();
}
}));
specs.put(
FeColorMatrixManager.REACT_CLASS,
ModuleSpec.viewManagerSpec(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* 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.uimanager.BaseViewManagerDelegate;
import com.facebook.react.uimanager.BaseViewManagerInterface;

public class RNSVGFeBlendManagerDelegate<T extends View, U extends BaseViewManagerInterface<T> & RNSVGFeBlendManagerInterface<T>> extends BaseViewManagerDelegate<T, U> {
public RNSVGFeBlendManagerDelegate(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 "in1":
mViewManager.setIn1(view, value == null ? null : (String) value);
break;
case "in2":
mViewManager.setIn2(view, value == null ? null : (String) value);
break;
case "mode":
mViewManager.setMode(view, (String) value);
break;
default:
super.setProperty(view, propName, value);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* 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;

public interface RNSVGFeBlendManagerInterface<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 setIn1(T view, @Nullable String value);
void setIn2(T view, @Nullable String value);
void setMode(T view, @Nullable String value);
}
8 changes: 8 additions & 0 deletions apple/Filters/RNSVGBlendMode.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
typedef CF_ENUM(int32_t, RNSVGBlendMode) {
SVG_FEBLEND_MODE_UNKNOWN,
SVG_FEBLEND_MODE_NORMAL,
SVG_FEBLEND_MODE_MULTIPLY,
SVG_FEBLEND_MODE_SCREEN,
SVG_FEBLEND_MODE_DARKEN,
SVG_FEBLEND_MODE_LIGHTEN,
};
10 changes: 10 additions & 0 deletions apple/Filters/RNSVGFeBlend.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#import "RNSVGBlendMode.h"
#import "RNSVGFilterPrimitive.h"

@interface RNSVGFeBlend : RNSVGFilterPrimitive

@property (nonatomic, strong) NSString *in1;
@property (nonatomic, strong) NSString *in2;
@property (nonatomic, assign) RNSVGBlendMode mode;

@end
Loading

0 comments on commit 096fdc2

Please sign in to comment.