diff --git a/.eslintrc.js b/.eslintrc.js index c7c32195..a36c9c57 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -7,7 +7,13 @@ module.exports = { 'prettier', 'plugin:import/typescript', ], - plugins: ['react', 'react-native', 'import', '@typescript-eslint', 'react-hooks'], + plugins: [ + 'react', + 'react-native', + 'import', + '@typescript-eslint', + 'react-hooks', + ], env: { 'react-native/react-native': true, }, @@ -15,6 +21,7 @@ module.exports = { 'import/core-modules': [ 'react-native-svg', 'react-native-svg/css', + 'react-native-svg/filter-image', ], 'import/resolver': { 'babel-module': { diff --git a/README.md b/README.md index 6887cfc9..ee0207b0 100644 --- a/README.md +++ b/README.md @@ -126,10 +126,6 @@ If you suspect that you've found a spec conformance bug, then you can test using To check how to use the library, see [USAGE.md](https://github.com/react-native-svg/react-native-svg/blob/main/USAGE.md) -## TODO: - -1. Filters ([connected PR](https://github.com/react-native-svg/react-native-svg/pull/896)) - ## Known issues: 1. Unable to apply focus point of RadialGradient on Android. diff --git a/USAGE.md b/USAGE.md index fe244a27..4735fb8c 100644 --- a/USAGE.md +++ b/USAGE.md @@ -1252,3 +1252,84 @@ const styles = StyleSheet.create({ }, }); ``` + +## Filters + +Filter effects are a way of processing an element’s rendering before it is displayed in the document. Typically, rendering an element via CSS or SVG can conceptually be described as if the element, including its children, are drawn into a buffer (such as a raster image) and then that buffer is composited into the elements parent. Filters apply an effect before the compositing stage. Examples of such effects are blurring, changing color intensity and warping the image. + +Currently supported\* filters are: + +- FeColorMatrix + +\*_More filters are coming soon_ + +Exmaple use of filters: + +```jsx +import React from 'react'; +import { FeColorMatrix, Filter, Rect, Svg } from 'react-native-svg'; + +export default () => { + return ( + + + + + + + ); +}; +``` + +![FeColorMatrix](./screenshots/feColorMatrix.png) + +More info: + +## FilterImage + +`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 ( + + ); +}; +const styles = StyleSheet.create({ + image: { + width: 200, + height: 200, + }, +}); +``` + +![FilterImage](./screenshots/filterImage.png) diff --git a/android/src/main/java/com/horcrux/svg/FeColorMatrixView.java b/android/src/main/java/com/horcrux/svg/FeColorMatrixView.java new file mode 100644 index 00000000..532011a9 --- /dev/null +++ b/android/src/main/java/com/horcrux/svg/FeColorMatrixView.java @@ -0,0 +1,99 @@ +package com.horcrux.svg; + +import android.annotation.SuppressLint; +import android.graphics.Bitmap; +import android.graphics.ColorMatrix; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableArray; +import java.util.HashMap; + +@SuppressLint("ViewConstructor") +class FeColorMatrixView extends FilterPrimitiveView { + String mIn1; + FilterProperties.FeColorMatrixType mType; + ReadableArray mValues; + + public FeColorMatrixView(ReactContext reactContext) { + super(reactContext); + } + + public void setIn1(String in1) { + this.mIn1 = in1; + invalidate(); + } + + public void setType(String type) { + this.mType = FilterProperties.FeColorMatrixType.getEnum(type); + invalidate(); + } + + public void setValues(ReadableArray values) { + this.mValues = values; + invalidate(); + } + + @Override + public Bitmap applyFilter(HashMap resultsMap, Bitmap prevResult) { + Bitmap source = getSource(resultsMap, prevResult, this.mIn1); + + ColorMatrix colorMatrix = new ColorMatrix(); + switch (this.mType) { + case MATRIX: + if (this.mValues.size() < 20) return source; + + float[] rawMatrix = new float[mValues.size()]; + + for (int i = 0; i < this.mValues.size(); i++) { + rawMatrix[i] = (float) this.mValues.getDouble(i); + } + + colorMatrix.set(rawMatrix); + break; + case SATURATE: + if (this.mValues.size() != 1) return source; + + colorMatrix.setSaturation((float) this.mValues.getDouble(0)); + break; + case HUE_ROTATE: + if (this.mValues.size() != 1) return source; + + float hue = (float) this.mValues.getDouble(0); + float cosHue = (float) Math.cos(hue * Math.PI / 180); + float sinHue = (float) Math.sin(hue * Math.PI / 180); + + colorMatrix.set( + new float[] { + 0.213f + cosHue * 0.787f - sinHue * 0.213f, // 0 + 0.715f - cosHue * 0.715f - sinHue * 0.715f, // 1 + 0.072f - cosHue * 0.072f + sinHue * 0.928f, // 2 + 0, // 3 + 0, // 4 + 0.213f - cosHue * 0.213f + sinHue * 0.143f, // 5 + 0.715f + cosHue * 0.285f + sinHue * 0.140f, // 6 + 0.072f - cosHue * 0.072f - sinHue * 0.283f, // 7 + 0, // 8 + 0, // 9 + 0.213f - cosHue * 0.213f - sinHue * 0.787f, // 10 + 0.715f - cosHue * 0.715f + sinHue * 0.715f, // 11 + 0.072f + cosHue * 0.928f + sinHue * 0.072f, // 12 + 0, // 13 + 0, // 14 + 0, // 15 + 0, // 16 + 0, // 17 + 1, // 18 + 0, // 19 + }); + break; + case LUMINANCE_TO_ALPHA: + colorMatrix.set( + 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, 0, 0, 0, + 0, 1 + }); + break; + } + + return FilterUtils.getBitmapWithColorMatrix(colorMatrix, source); + } +} diff --git a/android/src/main/java/com/horcrux/svg/FilterPrimitiveView.java b/android/src/main/java/com/horcrux/svg/FilterPrimitiveView.java new file mode 100644 index 00000000..42000fa8 --- /dev/null +++ b/android/src/main/java/com/horcrux/svg/FilterPrimitiveView.java @@ -0,0 +1,62 @@ +package com.horcrux.svg; + +import android.annotation.SuppressLint; +import android.graphics.Bitmap; +import com.facebook.react.bridge.Dynamic; +import com.facebook.react.bridge.ReactContext; +import java.util.HashMap; + +@SuppressLint("ViewConstructor") +class FilterPrimitiveView extends DefinitionView { + SVGLength mX; + SVGLength mY; + SVGLength mW; + SVGLength mH; + private String mResult; + + public FilterPrimitiveView(ReactContext reactContext) { + super(reactContext); + } + + public void setX(Dynamic x) { + mX = SVGLength.from(x); + invalidate(); + } + + public void setY(Dynamic y) { + mY = SVGLength.from(y); + invalidate(); + } + + public void setWidth(Dynamic width) { + mW = SVGLength.from(width); + invalidate(); + } + + public void setHeight(Dynamic height) { + mH = SVGLength.from(height); + invalidate(); + } + + public void setResult(String result) { + mResult = result; + invalidate(); + } + + public String getResult() { + return mResult; + } + + protected static Bitmap getSource( + HashMap resultsMap, Bitmap prevResult, String in1) { + Bitmap sourceFromResults = in1 != null ? resultsMap.get(in1) : null; + return sourceFromResults != null ? sourceFromResults : prevResult; + } + + public Bitmap applyFilter(HashMap resultsMap, Bitmap prevResult) { + return null; + } + + @Override + void saveDefinition() {} +} diff --git a/android/src/main/java/com/horcrux/svg/FilterProperties.java b/android/src/main/java/com/horcrux/svg/FilterProperties.java new file mode 100644 index 00000000..73d3b647 --- /dev/null +++ b/android/src/main/java/com/horcrux/svg/FilterProperties.java @@ -0,0 +1,110 @@ +package com.horcrux.svg; + +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nonnull; + +class FilterProperties { + enum Units { + OBJECT_BOUNDING_BOX("objectBoundingBox"), + USER_SPACE_ON_USE("userSpaceOnUse"), + ; + + private final String units; + + Units(String units) { + this.units = units; + } + + static Units getEnum(String strVal) { + if (!unitsToEnum.containsKey(strVal)) { + throw new IllegalArgumentException("Unknown 'Unit' Value: " + strVal); + } + return unitsToEnum.get(strVal); + } + + private static final Map unitsToEnum = new HashMap<>(); + + static { + for (final Units en : Units.values()) { + unitsToEnum.put(en.units, en); + } + } + + @Nonnull + @Override + public String toString() { + return units; + } + } + + enum EdgeMode { + UNKNOWN("unknown"), + DUPLICATE("duplicate"), + WRAP("wrap"), + NONE("none"), + ; + + private final String edgeMode; + + EdgeMode(String edgeMode) { + this.edgeMode = edgeMode; + } + + static EdgeMode getEnum(String strVal) { + if (!edgeModeToEnum.containsKey(strVal)) { + throw new IllegalArgumentException("Unknown 'edgeMode' Value: " + strVal); + } + return edgeModeToEnum.get(strVal); + } + + private static final Map edgeModeToEnum = new HashMap<>(); + + static { + for (final EdgeMode en : EdgeMode.values()) { + edgeModeToEnum.put(en.edgeMode, en); + } + } + + @Nonnull + @Override + public String toString() { + return edgeMode; + } + } + + enum FeColorMatrixType { + MATRIX("matrix"), + SATURATE("saturate"), + HUE_ROTATE("hueRotate"), + LUMINANCE_TO_ALPHA("luminanceToAlpha"), + ; + + private final String type; + + FeColorMatrixType(String type) { + this.type = type; + } + + static FeColorMatrixType getEnum(String strVal) { + if (!typeToEnum.containsKey(strVal)) { + throw new IllegalArgumentException("Unknown String Value: " + strVal); + } + return typeToEnum.get(strVal); + } + + private static final Map typeToEnum = new HashMap<>(); + + static { + for (final FeColorMatrixType en : FeColorMatrixType.values()) { + typeToEnum.put(en.type, en); + } + } + + @Nonnull + @Override + public String toString() { + return type; + } + } +} diff --git a/android/src/main/java/com/horcrux/svg/FilterUtils.java b/android/src/main/java/com/horcrux/svg/FilterUtils.java new file mode 100644 index 00000000..ab9634dd --- /dev/null +++ b/android/src/main/java/com/horcrux/svg/FilterUtils.java @@ -0,0 +1,37 @@ +package com.horcrux.svg; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.ColorMatrix; +import android.graphics.ColorMatrixColorFilter; +import android.graphics.Paint; + +public class FilterUtils { + + public static Bitmap getBitmapWithColorMatrix(ColorMatrix colorMatrix, Bitmap sourceBitmap) { + Bitmap results = + Bitmap.createBitmap( + sourceBitmap.getWidth(), sourceBitmap.getHeight(), sourceBitmap.getConfig()); + + Canvas canvas = new Canvas(results); + + Paint paint = new Paint(); + paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix)); + canvas.drawBitmap(sourceBitmap, 0, 0, paint); + + return results; + } + + public static Bitmap applySourceAlphaFilter(Bitmap source) { + ColorMatrix colorMatrix = new ColorMatrix(); + colorMatrix.set( + new float[] { + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, + 0, 0, 0, 0, 1 + }); + return getBitmapWithColorMatrix(colorMatrix, source); + } +} diff --git a/android/src/main/java/com/horcrux/svg/FilterView.java b/android/src/main/java/com/horcrux/svg/FilterView.java new file mode 100644 index 00000000..a3d90293 --- /dev/null +++ b/android/src/main/java/com/horcrux/svg/FilterView.java @@ -0,0 +1,113 @@ +package com.horcrux.svg; + +import android.annotation.SuppressLint; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.util.Log; +import android.view.View; +import com.facebook.react.bridge.Dynamic; +import com.facebook.react.bridge.ReactContext; +import java.util.HashMap; + +@SuppressLint("ViewConstructor") +class FilterView extends DefinitionView { + private final HashMap mResultsMap = new HashMap<>(); + + SVGLength mX; + SVGLength mY; + SVGLength mW; + SVGLength mH; + + private FilterProperties.Units mFilterUnits; + + @SuppressWarnings({"FieldCanBeLocal", "unused"}) + private FilterProperties.Units mPrimitiveUnits; + + public FilterView(ReactContext reactContext) { + super(reactContext); + } + + public void setX(Dynamic x) { + mX = SVGLength.from(x); + invalidate(); + } + + public void setY(Dynamic y) { + mY = SVGLength.from(y); + invalidate(); + } + + public void setWidth(Dynamic width) { + mW = SVGLength.from(width); + invalidate(); + } + + public void setHeight(Dynamic height) { + mH = SVGLength.from(height); + invalidate(); + } + + public void setFilterUnits(String filterUnits) { + mFilterUnits = FilterProperties.Units.getEnum(filterUnits); + invalidate(); + } + + public void setPrimitiveUnits(String primitiveUnits) { + mPrimitiveUnits = FilterProperties.Units.getEnum(primitiveUnits); + invalidate(); + } + + @Override + void saveDefinition() { + if (mName != null) { + SvgView svg = getSvgView(); + if (svg != null) { + svg.defineFilter(this, mName); + } + } + } + + public Bitmap applyFilter( + Bitmap source, Bitmap background, Rect renderableBounds, Rect canvasBounds) { + mResultsMap.clear(); + mResultsMap.put("SourceGraphic", source); + mResultsMap.put("SourceAlpha", FilterUtils.applySourceAlphaFilter(source)); + mResultsMap.put("BackgroundImage", background); + mResultsMap.put("BackgroundAlpha", FilterUtils.applySourceAlphaFilter(background)); + + Bitmap res = source; + + for (int i = 0; i < getChildCount(); i++) { + View node = getChildAt(i); + if (node instanceof FilterPrimitiveView currentFilter) { + res = currentFilter.applyFilter(mResultsMap, res); + String resultName = currentFilter.getResult(); + if (resultName != null) { + mResultsMap.put(resultName, res); + } + } else { + Log.e("RNSVG", "Invalid `Filter` child: Filter children can only be `Fe...` components"); + } + } + + // crop Bitmap to filter coordinates + int x, y, width, height; + if (this.mFilterUnits == FilterProperties.Units.USER_SPACE_ON_USE) { + x = (int) this.relativeOn(this.mX, canvasBounds.width()); + y = (int) this.relativeOn(this.mY, canvasBounds.height()); + width = (int) this.relativeOn(this.mW, canvasBounds.width()); + height = (int) this.relativeOn(this.mH, canvasBounds.height()); + } else { // FilterProperties.Units.OBJECT_BOUNDING_BOX + x = (int) this.relativeOnFraction(this.mX, renderableBounds.width()); + y = (int) this.relativeOnFraction(this.mY, renderableBounds.height()); + width = (int) this.relativeOnFraction(this.mW, renderableBounds.width()); + height = (int) this.relativeOnFraction(this.mH, renderableBounds.height()); + } + Rect cropRect = new Rect(x, y, x + width, y + height); + Bitmap resultBitmap = Bitmap.createBitmap(res.getWidth(), res.getHeight(), res.getConfig()); + Canvas canvas = new Canvas(resultBitmap); + canvas.drawBitmap(res, cropRect, cropRect, null); + return resultBitmap; + } +} diff --git a/android/src/main/java/com/horcrux/svg/PathParser.java b/android/src/main/java/com/horcrux/svg/PathParser.java index 18d3d28a..3f3c0ba1 100644 --- a/android/src/main/java/com/horcrux/svg/PathParser.java +++ b/android/src/main/java/com/horcrux/svg/PathParser.java @@ -62,7 +62,8 @@ class PathParser { if (!has_prev_cmd && first_char != 'M' && first_char != 'm') { // The first segment must be a MoveTo. - throw new IllegalArgumentException(String.format("Unexpected character '%c' (i=%d, s=%s)", first_char, i, s)); + throw new IllegalArgumentException( + String.format("Unexpected character '%c' (i=%d, s=%s)", first_char, i, s)); } // TODO: simplify @@ -75,7 +76,8 @@ class PathParser { } else if (is_number_start(first_char) && has_prev_cmd) { if (prev_cmd == 'Z' || prev_cmd == 'z') { // ClosePath cannot be followed by a number. - throw new IllegalArgumentException(String.format("Unexpected number after 'z' (s=%s)", s)); + throw new IllegalArgumentException( + String.format("Unexpected number after 'z' (s=%s)", s)); } if (prev_cmd == 'M' || prev_cmd == 'm') { @@ -93,7 +95,8 @@ class PathParser { cmd = prev_cmd; } } else { - throw new IllegalArgumentException(String.format("Unexpected character '%c' (i=%d, s=%s)", first_char, i, s)); + throw new IllegalArgumentException( + String.format("Unexpected character '%c' (i=%d, s=%s)", first_char, i, s)); } boolean absolute = is_absolute(cmd); @@ -226,8 +229,8 @@ class PathParser { } default: { - throw new IllegalArgumentException(String.format("Unexpected comand '%c' (s=%s)", cmd, s)); - + throw new IllegalArgumentException( + String.format("Unexpected comand '%c' (s=%s)", cmd, s)); } } @@ -624,7 +627,8 @@ class PathParser { c = s.charAt(i); } } else if (c != '.') { - throw new IllegalArgumentException(String.format("Invalid number formating character '%c' (i=%d, s=%s)", c, i, s)); + throw new IllegalArgumentException( + String.format("Invalid number formating character '%c' (i=%d, s=%s)", c, i, s)); } // Consume fraction. @@ -649,7 +653,8 @@ class PathParser { } else if (c >= '0' && c <= '9') { skip_digits(); } else { - throw new IllegalArgumentException(String.format("Invalid number formating character '%c' (i=%d, s=%s)", c, i, s)); + throw new IllegalArgumentException( + String.format("Invalid number formating character '%c' (i=%d, s=%s)", c, i, s)); } } } @@ -659,7 +664,8 @@ class PathParser { // inf, nan, etc. are an error. if (Float.isInfinite(n) || Float.isNaN(n)) { - throw new IllegalArgumentException(String.format("Invalid number '%s' (start=%d, i=%d, s=%s)", num, start, i, s)); + throw new IllegalArgumentException( + String.format("Invalid number '%s' (start=%d, i=%d, s=%s)", num, start, i, s)); } return n; diff --git a/android/src/main/java/com/horcrux/svg/RenderableView.java b/android/src/main/java/com/horcrux/svg/RenderableView.java index e8143bf0..ff842acc 100644 --- a/android/src/main/java/com/horcrux/svg/RenderableView.java +++ b/android/src/main/java/com/horcrux/svg/RenderableView.java @@ -8,6 +8,7 @@ package com.horcrux.svg; +import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; @@ -94,6 +95,8 @@ public abstract class RenderableView extends VirtualView implements ReactHitSlop private @Nullable ArrayList mPropList; private @Nullable ArrayList mAttributeList; + @Nullable String mFilter; + private static final Pattern regex = Pattern.compile("[0-9.-]+"); @Nullable @@ -326,29 +329,64 @@ public abstract class RenderableView extends VirtualView implements ReactHitSlop invalidate(); } + public void setFilter(String filter) { + mFilter = filter; + invalidate(); + } + void render(Canvas canvas, Paint paint, float opacity) { MaskView mask = null; + FilterView filter = null; if (mMask != null) { SvgView root = getSvgView(); mask = (MaskView) root.getDefinedMask(mMask); } + if (mFilter != null) { + SvgView root = getSvgView(); + filter = (FilterView) root.getDefinedFilter(mFilter); + } - if (mask != null) { - // https://www.w3.org/TR/SVG11/masking.html - // Adding a mask involves several steps - // 1. applying luminanceToAlpha to the mask element - // 2. merging the alpha channel of the element with the alpha channel from the previous step - // 3. applying the result from step 2 to the target element + if (mask != null || filter != null) { + if (filter != null) { + Paint bitmapPaint = new Paint(Paint.FILTER_BITMAP_FLAG); + canvas.saveLayer(null, bitmapPaint); - canvas.saveLayer(null, paint); - draw(canvas, paint, opacity); + Rect canvasBounds = this.getSvgView().getCanvasBounds(); - Paint dstInPaint = new Paint(); - dstInPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); + // draw element to self bitmap + Bitmap elementBitmap = + Bitmap.createBitmap( + canvasBounds.width(), canvasBounds.height(), Bitmap.Config.ARGB_8888); + Canvas elementCanvas = new Canvas(elementBitmap); - // prepare step 3 - combined layer - canvas.saveLayer(null, dstInPaint); + draw(elementCanvas, paint, opacity); + + // apply filters + Bitmap backgroundBitmap = this.getSvgView().getCurrentBitmap(); + elementBitmap = + filter.applyFilter( + elementBitmap, backgroundBitmap, elementCanvas.getClipBounds(), canvasBounds); + + // draw bitmap to canvas + canvas.drawBitmap(elementBitmap, 0, 0, bitmapPaint); + } else { + canvas.saveLayer(null, paint); + draw(canvas, paint, opacity); + } + + if (mask != null) { + // https://www.w3.org/TR/SVG11/masking.html + // Adding a mask involves several steps + // 1. applying luminanceToAlpha to the mask element + // 2. merging the alpha channel of the element with the alpha channel from the previous step + // 3. applying the result from step 2 to the target element + + Paint dstInPaint = new Paint(); + dstInPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); + + // prepare step 3 - combined layer + canvas.saveLayer(null, dstInPaint); if (mask.getMaskType() == MaskView.MaskType.LUMINANCE) { // step 1 - luminance layer @@ -366,32 +404,32 @@ public abstract class RenderableView extends VirtualView implements ReactHitSlop canvas.saveLayer(null, paint); } - // calculate mask bounds - float maskX = (float) relativeOnWidth(mask.mX); - float maskY = (float) relativeOnHeight(mask.mY); - float maskWidth = (float) relativeOnWidth(mask.mW); - float maskHeight = (float) relativeOnHeight(mask.mH); - // clip to mask bounds - canvas.clipRect(maskX, maskY, maskX + maskWidth, maskY + maskHeight); + // calculate mask bounds + float maskX = (float) relativeOnWidth(mask.mX); + float maskY = (float) relativeOnHeight(mask.mY); + float maskWidth = (float) relativeOnWidth(mask.mW); + float maskHeight = (float) relativeOnHeight(mask.mH); + // clip to mask bounds + canvas.clipRect(maskX, maskY, maskX + maskWidth, maskY + maskHeight); - mask.draw(canvas, paint, 1f); + mask.draw(canvas, paint, 1f); - // close luminance layer - canvas.restore(); + // close luminance layer + canvas.restore(); - // step 2 - alpha layer - canvas.saveLayer(null, dstInPaint); - // clip to mask bounds - canvas.clipRect(maskX, maskY, maskX + maskWidth, maskY + maskHeight); + // step 2 - alpha layer + canvas.saveLayer(null, dstInPaint); + // clip to mask bounds + canvas.clipRect(maskX, maskY, maskX + maskWidth, maskY + maskHeight); - mask.draw(canvas, paint, 1f); + mask.draw(canvas, paint, 1f); - // close alpha layer - canvas.restore(); - - // close combined layer - canvas.restore(); + // close alpha layer + canvas.restore(); + // close combined layer + canvas.restore(); + } // close element layer canvas.restore(); } else { diff --git a/android/src/main/java/com/horcrux/svg/RenderableViewManager.java b/android/src/main/java/com/horcrux/svg/RenderableViewManager.java index eb9ec480..bf9de5d3 100644 --- a/android/src/main/java/com/horcrux/svg/RenderableViewManager.java +++ b/android/src/main/java/com/horcrux/svg/RenderableViewManager.java @@ -103,6 +103,10 @@ import com.facebook.react.viewmanagers.RNSVGDefsManagerDelegate; import com.facebook.react.viewmanagers.RNSVGDefsManagerInterface; import com.facebook.react.viewmanagers.RNSVGEllipseManagerDelegate; import com.facebook.react.viewmanagers.RNSVGEllipseManagerInterface; +import com.facebook.react.viewmanagers.RNSVGFeColorMatrixManagerDelegate; +import com.facebook.react.viewmanagers.RNSVGFeColorMatrixManagerInterface; +import com.facebook.react.viewmanagers.RNSVGFilterManagerDelegate; +import com.facebook.react.viewmanagers.RNSVGFilterManagerInterface; import com.facebook.react.viewmanagers.RNSVGForeignObjectManagerDelegate; import com.facebook.react.viewmanagers.RNSVGForeignObjectManagerInterface; import com.facebook.react.viewmanagers.RNSVGGroupManagerDelegate; @@ -578,6 +582,8 @@ class VirtualViewManager extends ViewGroupManager extends ViewGroupManager extends VirtualViewManager super(svgclass); } + @ReactProp(name = "filter") + public void setFilter(T node, String filter) { + node.setFilter(filter); + } + static class GroupViewManagerAbstract extends RenderableViewManager { GroupViewManagerAbstract(SVGClass svgClass) { super(svgClass); @@ -1282,6 +1297,96 @@ class RenderableViewManager extends VirtualViewManager } } + static class FilterManager extends VirtualViewManager + implements RNSVGFilterManagerInterface { + FilterManager() { + super(SVGClass.RNSVGFilter); + mDelegate = new RNSVGFilterManagerDelegate(this); + } + + public static final String REACT_CLASS = "RNSVGFilter"; + + @ReactProp(name = "x") + public void setX(FilterView node, Dynamic x) { + node.setX(x); + } + + @ReactProp(name = "y") + public void setY(FilterView node, Dynamic y) { + node.setY(y); + } + + @ReactProp(name = "width") + public void setWidth(FilterView node, Dynamic width) { + node.setWidth(width); + } + + @ReactProp(name = "height") + public void setHeight(FilterView node, Dynamic height) { + node.setHeight(height); + } + + @ReactProp(name = "filterUnits") + public void setFilterUnits(FilterView node, String filterUnits) { + node.setFilterUnits(filterUnits); + } + + @ReactProp(name = "primitiveUnits") + public void setPrimitiveUnits(FilterView node, String primitiveUnits) { + node.setPrimitiveUnits(primitiveUnits); + } + } + + static class FeColorMatrixManager extends VirtualViewManager + implements RNSVGFeColorMatrixManagerInterface { + FeColorMatrixManager() { + super(SVGClass.RNSVGFeColorMatrix); + mDelegate = new RNSVGFeColorMatrixManagerDelegate(this); + } + + public static final String REACT_CLASS = "RNSVGFeColorMatrix"; + + @ReactProp(name = "x") + public void setX(FeColorMatrixView node, Dynamic x) { + node.setX(x); + } + + @ReactProp(name = "y") + public void setY(FeColorMatrixView node, Dynamic y) { + node.setY(y); + } + + @ReactProp(name = "width") + public void setWidth(FeColorMatrixView node, Dynamic width) { + node.setWidth(width); + } + + @ReactProp(name = "height") + public void setHeight(FeColorMatrixView node, Dynamic height) { + node.setHeight(height); + } + + @ReactProp(name = "result") + public void setResult(FeColorMatrixView node, String result) { + node.setResult(result); + } + + @ReactProp(name = "in1") + public void setIn1(FeColorMatrixView node, String in1) { + node.setIn1(in1); + } + + @ReactProp(name = "type") + public void setType(FeColorMatrixView node, String type) { + node.setType(type); + } + + @ReactProp(name = "values") + public void setValues(FeColorMatrixView node, @Nullable ReadableArray values) { + node.setValues(values); + } + } + static class ForeignObjectManager extends GroupViewManagerAbstract implements RNSVGForeignObjectManagerInterface { ForeignObjectManager() { diff --git a/android/src/main/java/com/horcrux/svg/SvgPackage.java b/android/src/main/java/com/horcrux/svg/SvgPackage.java index f75cab55..bab49548 100644 --- a/android/src/main/java/com/horcrux/svg/SvgPackage.java +++ b/android/src/main/java/com/horcrux/svg/SvgPackage.java @@ -205,6 +205,24 @@ public class SvgPackage extends TurboReactPackage implements ViewManagerOnDemand return new MaskManager(); } })); + specs.put( + FilterManager.REACT_CLASS, + ModuleSpec.viewManagerSpec( + new Provider() { + @Override + public NativeModule get() { + return new FilterManager(); + } + })); + specs.put( + FeColorMatrixManager.REACT_CLASS, + ModuleSpec.viewManagerSpec( + new Provider() { + @Override + public NativeModule get() { + return new FeColorMatrixManager(); + } + })); specs.put( ForeignObjectManager.REACT_CLASS, ModuleSpec.viewManagerSpec( diff --git a/android/src/main/java/com/horcrux/svg/SvgView.java b/android/src/main/java/com/horcrux/svg/SvgView.java index dd50add2..81fbdf1b 100644 --- a/android/src/main/java/com/horcrux/svg/SvgView.java +++ b/android/src/main/java/com/horcrux/svg/SvgView.java @@ -58,6 +58,7 @@ public class SvgView extends ReactViewGroup implements ReactCompoundView, ReactC } private @Nullable Bitmap mBitmap; + private @Nullable Bitmap mCurrentBitmap; private boolean mRemovalTransitionStarted; public SvgView(ReactContext reactContext) { @@ -161,6 +162,7 @@ public class SvgView extends ReactViewGroup implements ReactCompoundView, ReactC private final Map mDefinedTemplates = new HashMap<>(); private final Map mDefinedMarkers = new HashMap<>(); private final Map mDefinedMasks = new HashMap<>(); + private final Map mDefinedFilters = new HashMap<>(); private final Map mDefinedBrushes = new HashMap<>(); private Canvas mCanvas; private final float mScale; @@ -264,7 +266,7 @@ public class SvgView extends ReactViewGroup implements ReactCompoundView, ReactC return null; } Bitmap bitmap = Bitmap.createBitmap((int) width, (int) height, Bitmap.Config.ARGB_8888); - + mCurrentBitmap = bitmap; drawChildren(new Canvas(bitmap)); return bitmap; } @@ -423,6 +425,14 @@ public class SvgView extends ReactViewGroup implements ReactCompoundView, ReactC return mDefinedMasks.get(maskRef); } + void defineFilter(VirtualView filter, String filterRef) { + mDefinedFilters.put(filterRef, filter); + } + + VirtualView getDefinedFilter(String filterRef) { + return mDefinedFilters.get(filterRef); + } + void defineMarker(VirtualView marker, String markerRef) { mDefinedMarkers.put(markerRef, marker); } @@ -430,4 +440,8 @@ public class SvgView extends ReactViewGroup implements ReactCompoundView, ReactC VirtualView getDefinedMarker(String markerRef) { return mDefinedMarkers.get(markerRef); } + + public Bitmap getCurrentBitmap() { + return mCurrentBitmap; + } } diff --git a/android/src/main/java/com/horcrux/svg/TextView.java b/android/src/main/java/com/horcrux/svg/TextView.java index 280bda1d..fdf5cb2a 100644 --- a/android/src/main/java/com/horcrux/svg/TextView.java +++ b/android/src/main/java/com/horcrux/svg/TextView.java @@ -20,7 +20,6 @@ import android.view.View; import android.view.ViewParent; import com.facebook.react.bridge.Dynamic; import com.facebook.react.bridge.ReactContext; -import com.facebook.react.bridge.ReadableArray; import java.util.ArrayList; import javax.annotation.Nullable; diff --git a/android/src/main/java/com/horcrux/svg/VirtualView.java b/android/src/main/java/com/horcrux/svg/VirtualView.java index c414af23..ad2a1aef 100644 --- a/android/src/main/java/com/horcrux/svg/VirtualView.java +++ b/android/src/main/java/com/horcrux/svg/VirtualView.java @@ -418,34 +418,36 @@ public abstract class VirtualView extends ReactViewGroup { return svgView; } - double relativeOnWidth(SVGLength length) { + double relativeOnFraction(SVGLength length, float relative) { + SVGLength.UnitType unit = length.unit; + if (unit == SVGLength.UnitType.NUMBER) { + return length.value * relative; + } else if (unit == SVGLength.UnitType.PERCENTAGE) { + return length.value / 100 * relative; + } + return fromRelativeFast(length); + } + + double relativeOn(SVGLength length, float relative) { SVGLength.UnitType unit = length.unit; if (unit == SVGLength.UnitType.NUMBER) { return length.value * mScale; } else if (unit == SVGLength.UnitType.PERCENTAGE) { - return length.value / 100 * getCanvasWidth(); + return length.value / 100 * relative; } return fromRelativeFast(length); } + double relativeOnWidth(SVGLength length) { + return relativeOn(length, getCanvasWidth()); + } + double relativeOnHeight(SVGLength length) { - SVGLength.UnitType unit = length.unit; - if (unit == SVGLength.UnitType.NUMBER) { - return length.value * mScale; - } else if (unit == SVGLength.UnitType.PERCENTAGE) { - return length.value / 100 * getCanvasHeight(); - } - return fromRelativeFast(length); + return relativeOn(length, getCanvasHeight()); } double relativeOnOther(SVGLength length) { - SVGLength.UnitType unit = length.unit; - if (unit == SVGLength.UnitType.NUMBER) { - return length.value * mScale; - } else if (unit == SVGLength.UnitType.PERCENTAGE) { - return length.value / 100 * getCanvasDiagonal(); - } - return fromRelativeFast(length); + return relativeOn(length, (float) getCanvasDiagonal()); } /** diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGCircleManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGCircleManagerDelegate.java index dc1b2207..da553fe5 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGCircleManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGCircleManagerDelegate.java @@ -99,6 +99,9 @@ public class RNSVGCircleManagerDelegate { void setStrokeMiterlimit(T view, float value); void setVectorEffect(T view, int value); void setPropList(T view, @Nullable ReadableArray value); + void setFilter(T view, @Nullable String value); void setCx(T view, Dynamic value); void setCy(T view, Dynamic value); void setR(T view, Dynamic value); diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGClipPathManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGClipPathManagerDelegate.java index f1006e5d..4fa1fffd 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGClipPathManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGClipPathManagerDelegate.java @@ -99,6 +99,9 @@ public class RNSVGClipPathManagerDelegate { void setStrokeMiterlimit(T view, float value); void setVectorEffect(T view, int value); void setPropList(T view, @Nullable ReadableArray value); + void setFilter(T view, @Nullable String value); void setFontSize(T view, Dynamic value); void setFontWeight(T view, Dynamic value); void setFont(T view, Dynamic value); diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGEllipseManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGEllipseManagerDelegate.java index 38523cce..dce4c824 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGEllipseManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGEllipseManagerDelegate.java @@ -99,6 +99,9 @@ public class RNSVGEllipseManagerDelegate { void setStrokeMiterlimit(T view, float value); void setVectorEffect(T view, int value); void setPropList(T view, @Nullable ReadableArray value); + void setFilter(T view, @Nullable String value); void setCx(T view, Dynamic value); void setCy(T view, Dynamic value); void setRx(T view, Dynamic value); diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGFeColorMatrixManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGFeColorMatrixManagerDelegate.java new file mode 100644 index 00000000..4260b9e2 --- /dev/null +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGFeColorMatrixManagerDelegate.java @@ -0,0 +1,54 @@ +/** +* 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 RNSVGFeColorMatrixManagerDelegate & RNSVGFeColorMatrixManagerInterface> extends BaseViewManagerDelegate { + public RNSVGFeColorMatrixManagerDelegate(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 "type": + mViewManager.setType(view, (String) value); + break; + case "values": + mViewManager.setValues(view, (ReadableArray) value); + break; + default: + super.setProperty(view, propName, value); + } + } +} diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGFeColorMatrixManagerInterface.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGFeColorMatrixManagerInterface.java new file mode 100644 index 00000000..fef1a08c --- /dev/null +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGFeColorMatrixManagerInterface.java @@ -0,0 +1,26 @@ +/** +* 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 RNSVGFeColorMatrixManagerInterface { + 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 setType(T view, @Nullable String value); + void setValues(T view, @Nullable ReadableArray value); +} diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGFilterManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGFilterManagerDelegate.java new file mode 100644 index 00000000..86d3dd7e --- /dev/null +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGFilterManagerDelegate.java @@ -0,0 +1,50 @@ +/** +* 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 RNSVGFilterManagerDelegate & RNSVGFilterManagerInterface> extends BaseViewManagerDelegate { + public RNSVGFilterManagerDelegate(U viewManager) { + super(viewManager); + } + @Override + public void setProperty(T view, String propName, @Nullable Object value) { + switch (propName) { + case "name": + mViewManager.setName(view, value == null ? null : (String) value); + break; + case "x": + mViewManager.setX(view, new DynamicFromObject(value)); + break; + case "y": + mViewManager.setY(view, new DynamicFromObject(value)); + break; + case "height": + mViewManager.setHeight(view, new DynamicFromObject(value)); + break; + case "width": + mViewManager.setWidth(view, new DynamicFromObject(value)); + break; + case "filterUnits": + mViewManager.setFilterUnits(view, (String) value); + break; + case "primitiveUnits": + mViewManager.setPrimitiveUnits(view, (String) value); + break; + default: + super.setProperty(view, propName, value); + } + } +} diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGFilterManagerInterface.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGFilterManagerInterface.java new file mode 100644 index 00000000..80417faa --- /dev/null +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGFilterManagerInterface.java @@ -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; + +public interface RNSVGFilterManagerInterface { + void setName(T view, @Nullable String value); + void setX(T view, Dynamic value); + void setY(T view, Dynamic value); + void setHeight(T view, Dynamic value); + void setWidth(T view, Dynamic value); + void setFilterUnits(T view, @Nullable String value); + void setPrimitiveUnits(T view, @Nullable String value); +} diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGForeignObjectManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGForeignObjectManagerDelegate.java index ffc9eee7..14dda573 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGForeignObjectManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGForeignObjectManagerDelegate.java @@ -99,6 +99,9 @@ public class RNSVGForeignObjectManagerDelegate { void setStrokeMiterlimit(T view, float value); void setVectorEffect(T view, int value); void setPropList(T view, @Nullable ReadableArray value); + void setFilter(T view, @Nullable String value); void setFontSize(T view, Dynamic value); void setFontWeight(T view, Dynamic value); void setFont(T view, Dynamic value); diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGGroupManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGGroupManagerDelegate.java index 5919fbf1..95516fa9 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGGroupManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGGroupManagerDelegate.java @@ -99,6 +99,9 @@ public class RNSVGGroupManagerDelegate { void setStrokeMiterlimit(T view, float value); void setVectorEffect(T view, int value); void setPropList(T view, @Nullable ReadableArray value); + void setFilter(T view, @Nullable String value); void setFontSize(T view, Dynamic value); void setFontWeight(T view, Dynamic value); void setFont(T view, Dynamic value); diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGImageManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGImageManagerDelegate.java index c0d90d2d..27f537c3 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGImageManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGImageManagerDelegate.java @@ -99,6 +99,9 @@ public class RNSVGImageManagerDelegate { void setStrokeMiterlimit(T view, float value); void setVectorEffect(T view, int value); void setPropList(T view, @Nullable ReadableArray value); + void setFilter(T view, @Nullable String value); void setX(T view, Dynamic value); void setY(T view, Dynamic value); void setWidth(T view, Dynamic value); diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGLineManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGLineManagerDelegate.java index b75db014..4455723d 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGLineManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGLineManagerDelegate.java @@ -99,6 +99,9 @@ public class RNSVGLineManagerDelegate { void setStrokeMiterlimit(T view, float value); void setVectorEffect(T view, int value); void setPropList(T view, @Nullable ReadableArray value); + void setFilter(T view, @Nullable String value); void setX1(T view, Dynamic value); void setY1(T view, Dynamic value); void setX2(T view, Dynamic value); diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGMarkerManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGMarkerManagerDelegate.java index 4fe754aa..4f8bb575 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGMarkerManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGMarkerManagerDelegate.java @@ -99,6 +99,9 @@ public class RNSVGMarkerManagerDelegate { void setStrokeMiterlimit(T view, float value); void setVectorEffect(T view, int value); void setPropList(T view, @Nullable ReadableArray value); + void setFilter(T view, @Nullable String value); void setFontSize(T view, Dynamic value); void setFontWeight(T view, Dynamic value); void setFont(T view, Dynamic value); 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 8d7cd927..42512da2 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGMaskManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGMaskManagerDelegate.java @@ -99,6 +99,9 @@ public class RNSVGMaskManagerDelegate { void setStrokeMiterlimit(T view, float value); void setVectorEffect(T view, int value); void setPropList(T view, @Nullable ReadableArray value); + void setFilter(T view, @Nullable String value); void setFontSize(T view, Dynamic value); void setFontWeight(T view, Dynamic value); void setFont(T view, Dynamic value); diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGPathManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGPathManagerDelegate.java index 01f99ac7..b363e948 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGPathManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGPathManagerDelegate.java @@ -99,6 +99,9 @@ public class RNSVGPathManagerDelegate { void setStrokeMiterlimit(T view, float value); void setVectorEffect(T view, int value); void setPropList(T view, @Nullable ReadableArray value); + void setFilter(T view, @Nullable String value); void setD(T view, @Nullable String value); } diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGPatternManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGPatternManagerDelegate.java index 1d9eff8b..64a10d7b 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGPatternManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGPatternManagerDelegate.java @@ -99,6 +99,9 @@ public class RNSVGPatternManagerDelegate { void setStrokeMiterlimit(T view, float value); void setVectorEffect(T view, int value); void setPropList(T view, @Nullable ReadableArray value); + void setFilter(T view, @Nullable String value); void setFontSize(T view, Dynamic value); void setFontWeight(T view, Dynamic value); void setFont(T view, Dynamic value); diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGRectManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGRectManagerDelegate.java index 3f4ca43c..95e57def 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGRectManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGRectManagerDelegate.java @@ -99,6 +99,9 @@ public class RNSVGRectManagerDelegate { void setStrokeMiterlimit(T view, float value); void setVectorEffect(T view, int value); void setPropList(T view, @Nullable ReadableArray value); + void setFilter(T view, @Nullable String value); void setX(T view, Dynamic value); void setY(T view, Dynamic value); void setHeight(T view, Dynamic value); diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGSymbolManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGSymbolManagerDelegate.java index 7d916b85..065f1ee3 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGSymbolManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGSymbolManagerDelegate.java @@ -99,6 +99,9 @@ public class RNSVGSymbolManagerDelegate { void setStrokeMiterlimit(T view, float value); void setVectorEffect(T view, int value); void setPropList(T view, @Nullable ReadableArray value); + void setFilter(T view, @Nullable String value); void setFontSize(T view, Dynamic value); void setFontWeight(T view, Dynamic value); void setFont(T view, Dynamic value); diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGTSpanManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGTSpanManagerDelegate.java index f19635a2..d5b854e4 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGTSpanManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGTSpanManagerDelegate.java @@ -99,6 +99,9 @@ public class RNSVGTSpanManagerDelegate { void setStrokeMiterlimit(T view, float value); void setVectorEffect(T view, int value); void setPropList(T view, @Nullable ReadableArray value); + void setFilter(T view, @Nullable String value); void setFontSize(T view, Dynamic value); void setFontWeight(T view, Dynamic value); void setFont(T view, Dynamic value); diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGTextManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGTextManagerDelegate.java index 3baa804b..b57cf8ff 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGTextManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGTextManagerDelegate.java @@ -99,6 +99,9 @@ public class RNSVGTextManagerDelegate { void setStrokeMiterlimit(T view, float value); void setVectorEffect(T view, int value); void setPropList(T view, @Nullable ReadableArray value); + void setFilter(T view, @Nullable String value); void setFontSize(T view, Dynamic value); void setFontWeight(T view, Dynamic value); void setFont(T view, Dynamic value); diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGTextPathManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGTextPathManagerDelegate.java index 4a90b5c6..eec9f0ac 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGTextPathManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGTextPathManagerDelegate.java @@ -99,6 +99,9 @@ public class RNSVGTextPathManagerDelegate { void setStrokeMiterlimit(T view, float value); void setVectorEffect(T view, int value); void setPropList(T view, @Nullable ReadableArray value); + void setFilter(T view, @Nullable String value); void setFontSize(T view, Dynamic value); void setFontWeight(T view, Dynamic value); void setFont(T view, Dynamic value); diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGUseManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGUseManagerDelegate.java index c6eab3ae..0f93e593 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGUseManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGUseManagerDelegate.java @@ -99,6 +99,9 @@ public class RNSVGUseManagerDelegate { void setStrokeMiterlimit(T view, float value); void setVectorEffect(T view, int value); void setPropList(T view, @Nullable ReadableArray value); + void setFilter(T view, @Nullable String value); void setHref(T view, @Nullable String value); void setX(T view, Dynamic value); void setY(T view, Dynamic value); diff --git a/android/src/paper/java/com/horcrux/svg/NativeSvgViewModuleSpec.java b/android/src/paper/java/com/horcrux/svg/NativeSvgViewModuleSpec.java index 4ad70b88..e20a6db2 100644 --- a/android/src/paper/java/com/horcrux/svg/NativeSvgViewModuleSpec.java +++ b/android/src/paper/java/com/horcrux/svg/NativeSvgViewModuleSpec.java @@ -1,15 +1,13 @@ - /** - * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * 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. + *

Do not edit this file as changes may cause incorrect behavior and will be lost once the code + * is regenerated. * * @generated by codegen project: GenerateModuleJavaSpec.js - * * @nolint */ - package com.horcrux.svg; import com.facebook.proguard.annotations.DoNotStrip; @@ -22,7 +20,8 @@ import com.facebook.react.turbomodule.core.interfaces.TurboModule; import javax.annotation.Nonnull; import javax.annotation.Nullable; -public abstract class NativeSvgViewModuleSpec extends ReactContextBaseJavaModule implements TurboModule { +public abstract class NativeSvgViewModuleSpec extends ReactContextBaseJavaModule + implements TurboModule { public static final String NAME = "RNSVGSvgViewModule"; public NativeSvgViewModuleSpec(ReactApplicationContext reactContext) { @@ -36,5 +35,6 @@ public abstract class NativeSvgViewModuleSpec extends ReactContextBaseJavaModule @ReactMethod @DoNotStrip - public abstract void toDataURL(@Nullable Double tag, @Nullable ReadableMap options, @Nullable Callback callback); + public abstract void toDataURL( + @Nullable Double tag, @Nullable ReadableMap options, @Nullable Callback callback); } diff --git a/apple/Elements/RNSVGSvgView.h b/apple/Elements/RNSVGSvgView.h index e589dad3..fa900d7b 100644 --- a/apple/Elements/RNSVGSvgView.h +++ b/apple/Elements/RNSVGSvgView.h @@ -63,6 +63,10 @@ - (RNSVGNode *)getDefinedMask:(NSString *)maskName; +- (void)defineFilter:(RNSVGNode *)filter filterName:(NSString *)filterName; + +- (RNSVGNode *)getDefinedFilter:(NSString *)filterName; + - (NSString *)getDataURLWithBounds:(CGRect)bounds; - (CGRect)getContextBounds; diff --git a/apple/Elements/RNSVGSvgView.mm b/apple/Elements/RNSVGSvgView.mm index eca7cd34..61b188ee 100644 --- a/apple/Elements/RNSVGSvgView.mm +++ b/apple/Elements/RNSVGSvgView.mm @@ -25,6 +25,7 @@ NSMutableDictionary *_painters; NSMutableDictionary *_markers; NSMutableDictionary *_masks; + NSMutableDictionary *_filters; CGAffineTransform _invviewBoxTransform; bool rendered; } @@ -113,6 +114,7 @@ using namespace facebook::react; _painters = nil; _markers = nil; _masks = nil; + _filters = nil; _invviewBoxTransform = CGAffineTransformIdentity; rendered = NO; } @@ -436,6 +438,19 @@ using namespace facebook::react; return _masks ? [_masks objectForKey:maskName] : nil; } +- (void)defineFilter:(RNSVGNode *)filter filterName:(NSString *)filterName +{ + if (!_filters) { + _filters = [[NSMutableDictionary alloc] init]; + } + [_filters setObject:filter forKey:filterName]; +} + +- (RNSVGNode *)getDefinedFilter:(NSString *)filterName +{ + return _filters ? [_filters objectForKey:filterName] : nil; +} + - (CGRect)getContextBounds { return CGContextGetClipBoundingBox(UIGraphicsGetCurrentContext()); diff --git a/apple/Filters/RNSVGColorMatrixType.h b/apple/Filters/RNSVGColorMatrixType.h new file mode 100644 index 00000000..adeee7c7 --- /dev/null +++ b/apple/Filters/RNSVGColorMatrixType.h @@ -0,0 +1,7 @@ +typedef CF_ENUM(int32_t, RNSVGColorMatrixType) { + SVG_FECOLORMATRIX_TYPE_UNKNOWN, + SVG_FECOLORMATRIX_TYPE_MATRIX, + SVG_FECOLORMATRIX_TYPE_SATURATE, + SVG_FECOLORMATRIX_TYPE_HUEROTATE, + SVG_FECOLORMATRIX_TYPE_LUMINANCETOALPHA +}; diff --git a/apple/Filters/RNSVGEdgeModeTypes.h b/apple/Filters/RNSVGEdgeModeTypes.h new file mode 100644 index 00000000..8f6ae4df --- /dev/null +++ b/apple/Filters/RNSVGEdgeModeTypes.h @@ -0,0 +1,6 @@ +typedef CF_ENUM(int32_t, RNSVGEdgeModeTypes) { + SVG_EDGEMODE_UNKNOWN, + SVG_EDGEMODE_DUPLICATE, + SVG_EDGEMODE_WRAP, + SVG_EDGEMODE_NONE +}; diff --git a/apple/Filters/RNSVGFeColorMatrix.h b/apple/Filters/RNSVGFeColorMatrix.h new file mode 100644 index 00000000..abe0bfa5 --- /dev/null +++ b/apple/Filters/RNSVGFeColorMatrix.h @@ -0,0 +1,10 @@ +#import "RNSVGColorMatrixType.h" +#import "RNSVGFilterPrimitive.h" + +@interface RNSVGFeColorMatrix : RNSVGFilterPrimitive + +@property (nonatomic, strong) NSString *in1; +@property (nonatomic, assign) RNSVGColorMatrixType type; +@property (nonatomic, strong) NSArray *values; + +@end diff --git a/apple/Filters/RNSVGFeColorMatrix.mm b/apple/Filters/RNSVGFeColorMatrix.mm new file mode 100644 index 00000000..70165778 --- /dev/null +++ b/apple/Filters/RNSVGFeColorMatrix.mm @@ -0,0 +1,174 @@ +#import "RNSVGFeColorMatrix.h" + +#ifdef RCT_NEW_ARCH_ENABLED +#import +#import +#import +#import +#import "RNSVGConvert.h" +#import "RNSVGFabricConversions.h" +#endif // RCT_NEW_ARCH_ENABLED + +@implementation RNSVGFeColorMatrix + +#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(); + _props = defaultProps; + } + return self; +} + +#pragma mark - RCTComponentViewProtocol + ++ (ComponentDescriptorProvider)componentDescriptorProvider +{ + return concreteComponentDescriptorProvider(); +} + +- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps +{ + const auto &newProps = static_cast(*props); + + self.in1 = RCTNSStringFromStringNilIfEmpty(newProps.in1); + if (newProps.values.size() > 0) { + NSMutableArray *valuesArray = [NSMutableArray new]; + for (auto number : newProps.values) { + [valuesArray addObject:[NSNumber numberWithFloat:number]]; + } + self.values = valuesArray; + } + self.type = [RNSVGConvert RNSVGColorMatrixTypeFromCppEquivalent:newProps.type]; + + setCommonFilterProps(newProps, self); + _props = std::static_pointer_cast(props); +} + +- (void)prepareForRecycle +{ + [super prepareForRecycle]; + _in1 = nil; + _values = nil; + _type = RNSVGColorMatrixType::SVG_FECOLORMATRIX_TYPE_MATRIX; +} +#endif // RCT_NEW_ARCH_ENABLED + +- (void)setIn1:(NSString *)in1 +{ + if ([in1 isEqualToString:_in1]) { + return; + } + + _in1 = in1; + [self invalidate]; +} + +- (void)setValues:(NSArray *)values +{ + if (values == _values) { + return; + } + + _values = values; + [self invalidate]; +} + +- (void)setType:(RNSVGColorMatrixType)type +{ + if (type == _type) { + return; + } + _type = type; + [self invalidate]; +} + +#define deg2rad(degrees) ((M_PI * degrees) / 180) + +- (CIImage *)applyFilter:(NSMutableDictionary *)results previousFilterResult:(CIImage *)previous +{ + CIImage *inResults = self.in1 ? [results objectForKey:self.in1] : nil; + CIImage *inputImage = inResults ? inResults : previous; + + CIFilter *filter = nil; + NSArray *array = self.values; + NSUInteger count = [array count]; + + switch (self.type) { + case SVG_FECOLORMATRIX_TYPE_UNKNOWN: + return nil; + case SVG_FECOLORMATRIX_TYPE_MATRIX: { + if (count != 20) { + return nil; + } + CGFloat v[20] = {0}; + for (NSUInteger i = 0; i < count; i++) { + v[i] = (CGFloat)[array[i] doubleValue]; + } + filter = [CIFilter filterWithName:@"CIColorMatrix"]; + [filter setDefaults]; + [filter setValue:[CIVector vectorWithX:v[0] Y:v[1] Z:v[2] W:v[3]] forKey:@"inputRVector"]; + [filter setValue:[CIVector vectorWithX:v[5] Y:v[6] Z:v[7] W:v[8]] forKey:@"inputGVector"]; + [filter setValue:[CIVector vectorWithX:v[10] Y:v[11] Z:v[12] W:v[13]] forKey:@"inputBVector"]; + [filter setValue:[CIVector vectorWithX:v[15] Y:v[16] Z:v[17] W:v[18]] forKey:@"inputAVector"]; + [filter setValue:[CIVector vectorWithX:v[4] Y:v[9] Z:v[14] W:v[19]] forKey:@"inputBiasVector"]; + break; + } + case SVG_FECOLORMATRIX_TYPE_SATURATE: { + if (count != 1) { + return nil; + } + float saturation = [array[0] floatValue]; + filter = [CIFilter filterWithName:@"CIColorControls"]; + [filter setDefaults]; + [filter setValue:[NSNumber numberWithFloat:saturation] forKey:@"inputSaturation"]; + break; + } + case SVG_FECOLORMATRIX_TYPE_HUEROTATE: { + if (count != 1) { + return nil; + } + double deg = [array[0] doubleValue]; + filter = [CIFilter filterWithName:@"CIHueAdjust"]; + [filter setDefaults]; + float radians = (float)deg2rad(deg); + [filter setValue:[NSNumber numberWithFloat:radians] forKey:@"inputAngle"]; + break; + } + case SVG_FECOLORMATRIX_TYPE_LUMINANCETOALPHA: { + if (count != 0) { + return nil; + } + filter = [CIFilter filterWithName:@"CIColorMatrix"]; + [filter setDefaults]; + CGFloat zero[4] = {0, 0, 0, 0}; + CGFloat alpha[4] = {0.2125, 0.7154, 0.0721, 0}; + [filter setValue:[CIVector vectorWithValues:zero count:4] forKey:@"inputRVector"]; + [filter setValue:[CIVector vectorWithValues:zero count:4] forKey:@"inputGVector"]; + [filter setValue:[CIVector vectorWithValues:zero count:4] forKey:@"inputBVector"]; + [filter setValue:[CIVector vectorWithValues:alpha count:4] forKey:@"inputAVector"]; + [filter setValue:[CIVector vectorWithValues:zero count:4] forKey:@"inputBiasVector"]; + break; + } + default: + return nil; + } + + [filter setValue:inputImage forKey:@"inputImage"]; + + return [filter valueForKey:@"outputImage"]; + + return nil; +} + +#ifdef RCT_NEW_ARCH_ENABLED +Class RNSVGFeColorMatrixCls(void) +{ + return RNSVGFeColorMatrix.class; +} +#endif // RCT_NEW_ARCH_ENABLED + +@end diff --git a/apple/Filters/RNSVGFilter.h b/apple/Filters/RNSVGFilter.h new file mode 100644 index 00000000..51a74ad6 --- /dev/null +++ b/apple/Filters/RNSVGFilter.h @@ -0,0 +1,18 @@ +#import "RNSVGNode.h" + +@interface RNSVGFilter : RNSVGNode + +@property (nonatomic, strong) RNSVGLength *x; +@property (nonatomic, strong) RNSVGLength *y; +@property (nonatomic, strong) RNSVGLength *width; +@property (nonatomic, strong) RNSVGLength *height; +@property (nonatomic, assign) RNSVGUnits filterUnits; +@property (nonatomic, assign) RNSVGUnits primitiveUnits; + +- (CIImage *)applyFilter:(CIImage *)img + backgroundImg:(CIImage *)backgroundImg + renderableBounds:(CGRect)renderableBounds + canvasBounds:(CGRect)canvasBounds + ctm:(CGAffineTransform)ctm; + +@end diff --git a/apple/Filters/RNSVGFilter.mm b/apple/Filters/RNSVGFilter.mm new file mode 100644 index 00000000..9424c9ce --- /dev/null +++ b/apple/Filters/RNSVGFilter.mm @@ -0,0 +1,232 @@ +#import "RNSVGFilter.h" +#import "RNSVGFilterPrimitive.h" + +#ifdef RCT_NEW_ARCH_ENABLED +#import +#import +#import +#import +#import "RNSVGConvert.h" +#import "RNSVGFabricConversions.h" +#endif // RCT_NEW_ARCH_ENABLED + +@implementation RNSVGFilter { + NSMutableDictionary *resultsMap; +} + +#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(); + _props = defaultProps; + } + return self; +} + +#pragma mark - RCTComponentViewProtocol + ++ (ComponentDescriptorProvider)componentDescriptorProvider +{ + return concreteComponentDescriptorProvider(); +} + +- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps +{ + const auto &newProps = static_cast(*props); + + self.name = RCTNSStringFromStringNilIfEmpty(newProps.name); + id x = RNSVGConvertFollyDynamicToId(newProps.x); + if (x != nil) { + self.x = [RCTConvert RNSVGLength:x]; + } + id y = RNSVGConvertFollyDynamicToId(newProps.y); + if (y != nil) { + self.y = [RCTConvert RNSVGLength:y]; + } + id height = RNSVGConvertFollyDynamicToId(newProps.height); + if (height != nil) { + self.height = [RCTConvert RNSVGLength:height]; + } + id width = RNSVGConvertFollyDynamicToId(newProps.width); + if (width != nil) { + self.width = [RCTConvert RNSVGLength:width]; + } + self.filterUnits = [RNSVGConvert RNSVGUnitsFromFilterUnitsCppEquivalent:newProps.filterUnits]; + self.primitiveUnits = [RNSVGConvert RNSVGUnitsFromPrimitiveUnitsCppEquivalent:newProps.primitiveUnits]; + + _props = std::static_pointer_cast(props); +} + +- (void)prepareForRecycle +{ + [super prepareForRecycle]; + _x = nil; + _y = nil; + _height = nil; + _width = nil; + _filterUnits = kRNSVGUnitsObjectBoundingBox; + _primitiveUnits = kRNSVGUnitsUserSpaceOnUse; +} +#endif // RCT_NEW_ARCH_ENABLED + +- (id)init +{ + if (self = [super init]) { + resultsMap = [NSMutableDictionary dictionary]; + } + return self; +} + +- (CIImage *)applyFilter:(CIImage *)img + backgroundImg:(CIImage *)backgroundImg + renderableBounds:(CGRect)renderableBounds + canvasBounds:(CGRect)canvasBounds + ctm:(CGAffineTransform)ctm +{ + [resultsMap removeAllObjects]; + [resultsMap setObject:img forKey:@"SourceGraphic"]; + [resultsMap setObject:applySourceAlphaFilter(img) forKey:@"SourceAlpha"]; + [resultsMap setObject:backgroundImg forKey:@"BackgroundImage"]; + [resultsMap setObject:applySourceAlphaFilter(backgroundImg) forKey:@"BackgroundAlpha"]; + + CIImage *result = img; + RNSVGFilterPrimitive *currentFilter; + for (RNSVGNode *node in self.subviews) { + if ([node isKindOfClass:[RNSVGFilterPrimitive class]]) { + currentFilter = (RNSVGFilterPrimitive *)node; + result = [currentFilter applyFilter:resultsMap previousFilterResult:result]; + if (currentFilter.result) { + [resultsMap setObject:result forKey:currentFilter.result]; + } + } else { + RCTLogError(@"Invalid `Filter` subview: Filter children can only be `Fe...` components"); + } + } + + // Crop results to filter bounds + CIFilter *crop = [CIFilter filterWithName:@"CICrop"]; + [crop setDefaults]; + [crop setValue:result forKey:@"inputImage"]; + + CGFloat scaleX = ctm.a, scaleY = fabs(ctm.d); + CGFloat x, y, width, height; + if (self.filterUnits == kRNSVGUnitsUserSpaceOnUse) { + x = [self relativeOn:self.x relative:canvasBounds.size.width / scaleX]; + y = [self relativeOn:self.y relative:canvasBounds.size.height / scaleY]; + width = [self relativeOn:self.width relative:canvasBounds.size.width / scaleX]; + height = [self relativeOn:self.height relative:canvasBounds.size.height / scaleY]; + } else { // kRNSVGUnitsObjectBoundingBox + x = renderableBounds.origin.x + [self relativeOnFraction:self.x relative:renderableBounds.size.width]; + y = renderableBounds.origin.y + [self relativeOnFraction:self.y relative:renderableBounds.size.height]; + width = [self relativeOnFraction:self.width relative:renderableBounds.size.width]; + height = [self relativeOnFraction:self.height relative:renderableBounds.size.height]; + } + CGRect cropCGRect = CGRectMake(x, y, width, height); + cropCGRect = CGRectApplyAffineTransform(cropCGRect, ctm); + CIVector *cropRect = [CIVector vectorWithCGRect:cropCGRect]; + [crop setValue:cropRect forKey:@"inputRectangle"]; + + return [crop valueForKey:@"outputImage"]; +} + +static CIFilter *sourceAlphaFilter() +{ + CIFilter *sourceAlpha = [CIFilter filterWithName:@"CIColorMatrix"]; + CGFloat zero[4] = {0, 0, 0, 0}; + [sourceAlpha setDefaults]; + [sourceAlpha setValue:[CIVector vectorWithValues:zero count:4] forKey:@"inputRVector"]; + [sourceAlpha setValue:[CIVector vectorWithValues:zero count:4] forKey:@"inputGVector"]; + [sourceAlpha setValue:[CIVector vectorWithValues:zero count:4] forKey:@"inputBVector"]; + [sourceAlpha setValue:[CIVector vectorWithX:0.0 Y:0.0 Z:0.0 W:1.0] forKey:@"inputAVector"]; + [sourceAlpha setValue:[CIVector vectorWithValues:zero count:4] forKey:@"inputBiasVector"]; + return sourceAlpha; +} + +static CIImage *applySourceAlphaFilter(CIImage *inputImage) +{ + CIFilter *sourceAlpha = sourceAlphaFilter(); + [sourceAlpha setValue:inputImage forKey:@"inputImage"]; + return [sourceAlpha valueForKey:@"outputImage"]; +} + +- (RNSVGPlatformView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + return nil; +} + +- (void)parseReference +{ + [self.svgView defineFilter:self filterName:self.name]; +} + +- (void)setX:(RNSVGLength *)x +{ + if ([x isEqualTo:_x]) { + return; + } + + _x = x; + [self invalidate]; +} + +- (void)setY:(RNSVGLength *)y +{ + if ([y isEqualTo:_y]) { + return; + } + + _y = y; + [self invalidate]; +} + +- (void)setWidth:(RNSVGLength *)width +{ + if ([width isEqualTo:_width]) { + return; + } + + _width = width; + [self invalidate]; +} + +- (void)setHeight:(RNSVGLength *)height +{ + if ([height isEqualTo:_height]) { + return; + } + + _height = height; + [self invalidate]; +} + +- (void)setFilterUnits:(RNSVGUnits)filterUnits +{ + if (filterUnits == _filterUnits) { + return; + } + + _filterUnits = filterUnits; + [self invalidate]; +} + +- (void)setPrimitiveUnits:(RNSVGUnits)primitiveUnits +{ + if (primitiveUnits == _primitiveUnits) { + return; + } + + _primitiveUnits = primitiveUnits; + [self invalidate]; +} + +@end + +#ifdef RCT_NEW_ARCH_ENABLED +Class RNSVGFilterCls(void) +{ + return RNSVGFilter.class; +} +#endif // RCT_NEW_ARCH_ENABLED diff --git a/apple/Filters/RNSVGFilterPrimitive.h b/apple/Filters/RNSVGFilterPrimitive.h new file mode 100644 index 00000000..7d45e2cf --- /dev/null +++ b/apple/Filters/RNSVGFilterPrimitive.h @@ -0,0 +1,14 @@ +#import "RNSVGNode.h" + +@interface RNSVGFilterPrimitive : RNSVGNode + +@property (nonatomic, strong) RNSVGLength *x; +@property (nonatomic, strong) RNSVGLength *y; +@property (nonatomic, strong) RNSVGLength *width; +@property (nonatomic, strong) RNSVGLength *height; +@property (nonatomic, strong) NSString *result; + +- (CIImage *)applyFilter:(NSMutableDictionary *)results previousFilterResult:(CIImage *)previous; +- (CIImage *)cropResult:(CIImage *)result; + +@end diff --git a/apple/Filters/RNSVGFilterPrimitive.mm b/apple/Filters/RNSVGFilterPrimitive.mm new file mode 100644 index 00000000..3dcc4f83 --- /dev/null +++ b/apple/Filters/RNSVGFilterPrimitive.mm @@ -0,0 +1,110 @@ +#import +#import + +#ifdef RCT_NEW_ARCH_ENABLED +#import +#import +#import +#import +#import "RNSVGFabricConversions.h" +#endif // RCT_NEW_ARCH_ENABLED + +@implementation RNSVGFilterPrimitive + +#ifdef RCT_NEW_ARCH_ENABLED +- (void)prepareForRecycle +{ + [super prepareForRecycle]; + _x = nil; + _y = nil; + _height = nil; + _width = nil; + _result = nil; +} +#endif // RCT_NEW_ARCH_ENABLED + +- (RNSVGPlatformView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + return nil; +} + +- (void)parseReference +{ +} + +- (void)setX:(RNSVGLength *)x +{ + if ([x isEqualTo:_x]) { + return; + } + + _x = x; + [self invalidate]; +} + +- (void)invalidate +{ + self.dirty = false; + [super invalidate]; +} + +- (void)setY:(RNSVGLength *)y +{ + if ([y isEqualTo:_y]) { + return; + } + + _y = y; + [self invalidate]; +} + +- (void)setWidth:(RNSVGLength *)width +{ + if ([width isEqualTo:_width]) { + return; + } + + _width = width; + [self invalidate]; +} + +- (void)setHeight:(RNSVGLength *)height +{ + if ([height isEqualTo:_height]) { + return; + } + + _height = height; + [self invalidate]; +} + +- (void)setResult:(NSString *)result +{ + if ([result isEqualToString:_result]) { + return; + } + + _result = result; + [self invalidate]; +} + +- (CIImage *)applyFilter:(NSMutableDictionary *)results previousFilterResult:(CIImage *)previous +{ + return previous; +} + +- (CIImage *)cropResult:(CIImage *)result +{ + CIFilter *filter = [CIFilter filterWithName:@"CICrop"]; + [filter setDefaults]; + [filter setValue:result forKey:@"inputImage"]; + CGFloat x = [self relativeOnWidth:self.x]; + CGFloat y = [self relativeOnHeight:self.y]; + CGFloat width = [self relativeOnWidth:self.width]; + CGFloat height = [self relativeOnHeight:self.height]; + + [filter setValue:[CIVector vectorWithX:x Y:y Z:width W:height] forKey:@"inputRectangle"]; + return [filter valueForKey:@"outputImage"]; +} + +@end diff --git a/apple/RNSVGNode.h b/apple/RNSVGNode.h index 3c65e102..a7abe078 100644 --- a/apple/RNSVGNode.h +++ b/apple/RNSVGNode.h @@ -121,6 +121,8 @@ extern CGFloat const RNSVG_DEFAULT_FONT_SIZE; - (CGFloat)relativeOn:(RNSVGLength *)length relative:(CGFloat)relative; +- (CGFloat)relativeOnFraction:(RNSVGLength *)length relative:(CGFloat)relative; + - (CGFloat)relativeOnWidth:(RNSVGLength *)length; - (CGFloat)relativeOnHeight:(RNSVGLength *)length; diff --git a/apple/RNSVGNode.mm b/apple/RNSVGNode.mm index 6987cf1e..390979af 100644 --- a/apple/RNSVGNode.mm +++ b/apple/RNSVGNode.mm @@ -446,6 +446,17 @@ CGFloat const RNSVG_DEFAULT_FONT_SIZE = 12; fontSize:[self getFontSizeFromContext]]; } +- (CGFloat)relativeOnFraction:(RNSVGLength *)length relative:(CGFloat)relative +{ + RNSVGLengthUnitType unit = length.unit; + if (unit == SVG_LENGTHTYPE_NUMBER) { + return relative * length.value; + } else if (unit == SVG_LENGTHTYPE_PERCENTAGE) { + return length.value / 100 * relative; + } + return [self fromRelative:length]; +} + - (CGFloat)relativeOn:(RNSVGLength *)length relative:(CGFloat)relative { RNSVGLengthUnitType unit = length.unit; @@ -459,35 +470,17 @@ CGFloat const RNSVG_DEFAULT_FONT_SIZE = 12; - (CGFloat)relativeOnWidth:(RNSVGLength *)length { - RNSVGLengthUnitType unit = length.unit; - if (unit == SVG_LENGTHTYPE_NUMBER) { - return length.value; - } else if (unit == SVG_LENGTHTYPE_PERCENTAGE) { - return length.value / 100 * [self getCanvasWidth]; - } - return [self fromRelative:length]; + return [self relativeOn:length relative:[self getCanvasWidth]]; } - (CGFloat)relativeOnHeight:(RNSVGLength *)length { - RNSVGLengthUnitType unit = length.unit; - if (unit == SVG_LENGTHTYPE_NUMBER) { - return length.value; - } else if (unit == SVG_LENGTHTYPE_PERCENTAGE) { - return length.value / 100 * [self getCanvasHeight]; - } - return [self fromRelative:length]; + return [self relativeOn:length relative:[self getCanvasHeight]]; } - (CGFloat)relativeOnOther:(RNSVGLength *)length { - RNSVGLengthUnitType unit = length.unit; - if (unit == SVG_LENGTHTYPE_NUMBER) { - return length.value; - } else if (unit == SVG_LENGTHTYPE_PERCENTAGE) { - return length.value / 100 * [self getCanvasDiagonal]; - } - return [self fromRelative:length]; + return [self relativeOn:length relative:[self getCanvasDiagonal]]; } - (CGFloat)fromRelative:(RNSVGLength *)length diff --git a/apple/RNSVGRenderable.h b/apple/RNSVGRenderable.h index a4f02baa..0515b4c9 100644 --- a/apple/RNSVGRenderable.h +++ b/apple/RNSVGRenderable.h @@ -33,6 +33,7 @@ @property (nonatomic, assign) RNSVGVectorEffect vectorEffect; @property (nonatomic, copy) NSArray *propList; @property (nonatomic, assign) CGPathRef hitArea; +@property (nonatomic, strong) NSString *filter; - (void)setHitArea:(CGPathRef)path; diff --git a/apple/RNSVGRenderable.mm b/apple/RNSVGRenderable.mm index 02b45c04..e1b63bb9 100644 --- a/apple/RNSVGRenderable.mm +++ b/apple/RNSVGRenderable.mm @@ -10,9 +10,11 @@ #import #import "RNSVGBezierElement.h" #import "RNSVGClipPath.h" +#import "RNSVGFilter.h" #import "RNSVGMarker.h" #import "RNSVGMarkerPosition.h" #import "RNSVGMask.h" +#import "RNSVGRenderUtils.h" #import "RNSVGVectorEffect.h" #import "RNSVGViewBox.h" @@ -175,6 +177,15 @@ static RNSVGRenderable *_contextElement; [self invalidate]; } +- (void)setFilter:(NSString *)filter +{ + if ([_filter isEqualToString:filter]) { + return; + } + _filter = filter; + [self invalidate]; +} + - (void)dealloc { CGPathRelease(_hitArea); @@ -219,6 +230,7 @@ static RNSVGRenderable *_contextElement; _strokeDashoffset = 0; _vectorEffect = kRNSVGVectorEffectDefault; _propList = nil; + _filter = nil; } #endif // RCT_NEW_ARCH_ENABLED @@ -238,9 +250,7 @@ UInt32 saturate(CGFloat value) [self beginTransparencyLayer:context]; - if (self.mask) { - // https://www.w3.org/TR/SVG11/masking.html#MaskElement - RNSVGMask *_maskNode = (RNSVGMask *)[self.svgView getDefinedMask:self.mask]; + if (self.mask || self.filter) { CGFloat height = rect.size.height; CGFloat width = rect.size.width; CGFloat scale = 0.0; @@ -266,123 +276,151 @@ UInt32 saturate(CGFloat value) // Get current context transformations for offscreenContext CGAffineTransform currentCTM = CGContextGetCTM(context); - // Allocate pixel buffer and bitmap context for mask - NSUInteger bytesPerPixel = 4; - NSUInteger bitsPerComponent = 8; - NSUInteger bytesPerRow = bytesPerPixel * scaledWidth; - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - UInt32 *pixels = (UInt32 *)calloc(npixels, sizeof(UInt32)); - CGContextRef bcontext = CGBitmapContextCreate( - pixels, - scaledWidth, - scaledHeight, - bitsPerComponent, - bytesPerRow, - colorSpace, - kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); -#if TARGET_OS_OSX // [macOS] - // on macOS currentCTM is not scaled properly with screen scale so we need to scale it manually - CGContextConcatCTM(bcontext, screenScaleCTM); -#endif // [macOS] - CGContextConcatCTM(bcontext, currentCTM); + CGImage *contentImage = [RNSVGRenderUtils renderToImage:self ctm:currentCTM rect:scaledRect clip:nil]; - // Clip to mask bounds and render the mask - CGFloat x = [self relativeOn:[_maskNode x] relative:width]; - CGFloat y = [self relativeOn:[_maskNode y] relative:height]; - CGFloat w = [self relativeOn:[_maskNode maskwidth] relative:width]; - CGFloat h = [self relativeOn:[_maskNode maskheight] relative:height]; - CGRect maskBounds = CGRectApplyAffineTransform(CGRectMake(x, y, w, h), screenScaleCTM); - CGContextClipToRect(bcontext, maskBounds); - [_maskNode renderLayerTo:bcontext rect:scaledRect]; + if (self.filter) { + // https://www.w3.org/TR/SVG11/filters.html#FilterElement + RNSVGFilter *filterNode = (RNSVGFilter *)[self.svgView getDefinedFilter:self.filter]; - // Apply luminanceToAlpha filter primitive - // https://www.w3.org/TR/SVG11/filters.html#feColorMatrixElement - UInt32 *currentPixel = pixels; - if (_maskNode.maskType == kRNSVGMaskTypeLuminance) { - for (NSUInteger i = 0; i < npixels; i++) { - UInt32 color = *currentPixel; + CIImage *content = [CIImage imageWithCGImage:contentImage]; - UInt32 r = color & 0xFF; - UInt32 g = (color >> 8) & 0xFF; - UInt32 b = (color >> 16) & 0xFF; + CGImage *backgroundImage = CGBitmapContextCreateImage(context); + CIImage *background = + (backgroundImage != nil) ? [CIImage imageWithCGImage:backgroundImage] : [CIImage emptyImage]; - CGFloat luma = (CGFloat)(0.299 * r + 0.587 * g + 0.144 * b); - *currentPixel = saturate(luma) << 24; - currentPixel++; + content = [filterNode applyFilter:content + backgroundImg:background + renderableBounds:self.pathBounds + canvasBounds:scaledRect + ctm:currentCTM]; + + CGImageRelease(contentImage); + contentImage = [[RNSVGRenderUtils sharedCIContext] createCGImage:content fromRect:scaledRect]; + + if (!self.mask) { + CGContextConcatCTM(context, CGAffineTransformInvert(currentCTM)); + CGContextDrawImage(context, scaledRect, contentImage); + CGContextConcatCTM(context, currentCTM); } - } - // Create mask image and release memory - CGImageRef maskImage = CGBitmapContextCreateImage(bcontext); - CGColorSpaceRelease(colorSpace); - CGContextRelease(bcontext); - free(pixels); + CGImageRelease(backgroundImage); + } + if (self.mask) { + // https://www.w3.org/TR/SVG11/masking.html#MaskElement + RNSVGMask *_maskNode = (RNSVGMask *)[self.svgView getDefinedMask:self.mask]; + + // Allocate pixel buffer and bitmap context for mask + NSUInteger bytesPerPixel = 4; + NSUInteger bitsPerComponent = 8; + NSUInteger bytesPerRow = bytesPerPixel * scaledWidth; + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + UInt32 *pixels = (UInt32 *)calloc(npixels, sizeof(UInt32)); + CGContextRef bcontext = CGBitmapContextCreate( + pixels, + scaledWidth, + scaledHeight, + bitsPerComponent, + bytesPerRow, + colorSpace, + kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); +#if TARGET_OS_OSX // [macOS] + // on macOS currentCTM is not scaled properly with screen scale so we need to scale it manually + CGContextConcatCTM(bcontext, screenScaleCTM); +#endif // [macOS] + CGContextConcatCTM(bcontext, currentCTM); + + // Clip to mask bounds and render the mask + CGFloat x = [self relativeOn:[_maskNode x] relative:width]; + CGFloat y = [self relativeOn:[_maskNode y] relative:height]; + CGFloat w = [self relativeOn:[_maskNode maskwidth] relative:width]; + CGFloat h = [self relativeOn:[_maskNode maskheight] relative:height]; + CGRect maskBounds = CGRectApplyAffineTransform(CGRectMake(x, y, w, h), screenScaleCTM); + CGContextClipToRect(bcontext, maskBounds); + [_maskNode renderLayerTo:bcontext rect:scaledRect]; + + // Apply luminanceToAlpha filter primitive + // https://www.w3.org/TR/SVG11/filters.html#feColorMatrixElement + UInt32 *currentPixel = pixels; + 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; + + CGFloat luma = (CGFloat)(0.299 * r + 0.587 * g + 0.144 * b); + *currentPixel = saturate(luma) << 24; + currentPixel++; + } + } + + // Create mask image and release memory + CGImageRef maskImage = CGBitmapContextCreateImage(bcontext); + CGColorSpaceRelease(colorSpace); + CGContextRelease(bcontext); + free(pixels); #if !TARGET_OS_OSX // [macOS] - UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat defaultFormat]; - UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:rect.size format:format]; + UIGraphicsImageRendererFormat *format = [UIGraphicsImageRendererFormat defaultFormat]; + UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:rect.size format:format]; - // Get the content image - UIImage *contentImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull rendererContext) { - CGContextConcatCTM( - rendererContext.CGContext, CGAffineTransformInvert(CGContextGetCTM(rendererContext.CGContext))); - CGContextConcatCTM(rendererContext.CGContext, currentCTM); - [self renderLayerTo:rendererContext.CGContext rect:scaledRect]; - }]; + // Blend current element and mask + UIImage *blendedImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull rendererContext) { + CGContextConcatCTM( + rendererContext.CGContext, CGAffineTransformInvert(CGContextGetCTM(rendererContext.CGContext))); + CGContextTranslateCTM(rendererContext.CGContext, 0.0, scaledHeight); + CGContextScaleCTM(rendererContext.CGContext, 1.0, -1.0); - // Blend current element and mask - UIImage *blendedImage = [renderer imageWithActions:^(UIGraphicsImageRendererContext *_Nonnull rendererContext) { - CGContextConcatCTM( - rendererContext.CGContext, CGAffineTransformInvert(CGContextGetCTM(rendererContext.CGContext))); - CGContextTranslateCTM(rendererContext.CGContext, 0.0, scaledHeight); - CGContextScaleCTM(rendererContext.CGContext, 1.0, -1.0); + CGContextSetBlendMode(rendererContext.CGContext, kCGBlendModeCopy); + CGContextDrawImage(rendererContext.CGContext, scaledRect, maskImage); + CGContextSetBlendMode(rendererContext.CGContext, kCGBlendModeSourceIn); + CGContextDrawImage(rendererContext.CGContext, scaledRect, contentImage); + }]; - CGContextSetBlendMode(rendererContext.CGContext, kCGBlendModeCopy); - CGContextDrawImage(rendererContext.CGContext, scaledRect, maskImage); - CGContextSetBlendMode(rendererContext.CGContext, kCGBlendModeSourceIn); - CGContextDrawImage(rendererContext.CGContext, scaledRect, contentImage.CGImage); - }]; + // Render blended result into current render context + CGContextConcatCTM(context, CGAffineTransformInvert(currentCTM)); + [blendedImage drawInRect:scaledRect]; + CGContextConcatCTM(context, currentCTM); - // Render blended result into current render context - CGContextConcatCTM(context, CGAffineTransformInvert(currentCTM)); - [blendedImage drawInRect:scaledRect]; - CGContextConcatCTM(context, currentCTM); - - // Render blended result into current render context - CGImageRelease(maskImage); + // Render blended result into current render context + CGImageRelease(maskImage); #else // [macOS // Render content of current SVG Renderable to image - UIGraphicsBeginImageContextWithOptions(scaledRect.size, NO, 1.0); - CGContextRef newContext = UIGraphicsGetCurrentContext(); - CGContextConcatCTM(newContext, CGAffineTransformInvert(CGContextGetCTM(newContext))); - CGContextConcatCTM(newContext, screenScaleCTM); - CGContextConcatCTM(newContext, currentCTM); - [self renderLayerTo:newContext rect:scaledRect]; - CGImageRef contentImage = CGBitmapContextCreateImage(newContext); - UIGraphicsEndImageContext(); + UIGraphicsBeginImageContextWithOptions(scaledRect.size, NO, 1.0); + CGContextRef newContext = UIGraphicsGetCurrentContext(); + CGContextConcatCTM(newContext, CGAffineTransformInvert(CGContextGetCTM(newContext))); + CGContextConcatCTM(newContext, screenScaleCTM); + CGContextConcatCTM(newContext, currentCTM); + [self renderLayerTo:newContext rect:scaledRect]; + CGImageRef contentImage = CGBitmapContextCreateImage(newContext); + UIGraphicsEndImageContext(); - // Blend current element and mask - UIGraphicsBeginImageContextWithOptions(scaledRect.size, NO, 1.0); - newContext = UIGraphicsGetCurrentContext(); - CGContextConcatCTM(newContext, CGAffineTransformInvert(CGContextGetCTM(newContext))); + // Blend current element and mask + UIGraphicsBeginImageContextWithOptions(scaledRect.size, NO, 0.0); + newContext = UIGraphicsGetCurrentContext(); + CGContextTranslateCTM(newContext, 0.0, height); + CGContextScaleCTM(newContext, 1.0, -1.0); - CGContextSetBlendMode(newContext, kCGBlendModeCopy); - CGContextDrawImage(newContext, scaledRect, maskImage); - CGImageRelease(maskImage); + CGContextSetBlendMode(newContext, kCGBlendModeCopy); + CGContextDrawImage(newContext, scaledRect, maskImage); + CGImageRelease(maskImage); - CGContextSetBlendMode(newContext, kCGBlendModeSourceIn); - CGContextDrawImage(newContext, scaledRect, contentImage); - CGImageRelease(contentImage); + CGContextSetBlendMode(newContext, kCGBlendModeSourceIn); + CGContextDrawImage(newContext, scaledRect, contentImage); + CGImageRelease(contentImage); - CGImageRef blendedImage = CGBitmapContextCreateImage(newContext); - UIGraphicsEndImageContext(); + CGImageRef blendedImage = CGBitmapContextCreateImage(newContext); + UIGraphicsEndImageContext(); - // Render blended result into current render context - CGContextConcatCTM(context, CGAffineTransformInvert(currentCTM)); - CGContextDrawImage(context, rect, blendedImage); - CGContextConcatCTM(context, currentCTM); - CGImageRelease(blendedImage); + // Render blended result into current render context + CGContextConcatCTM(context, CGAffineTransformInvert(currentCTM)); + CGContextDrawImage(context, rect, blendedImage); + CGContextConcatCTM(context, currentCTM); + CGImageRelease(blendedImage); #endif // macOS] + } + CGImageRelease(contentImage); } else { [self renderLayerTo:context rect:rect]; } diff --git a/apple/Utils/RCTConvert+RNSVG.h b/apple/Utils/RCTConvert+RNSVG.h index 05d700e4..ef8acda6 100644 --- a/apple/Utils/RCTConvert+RNSVG.h +++ b/apple/Utils/RCTConvert+RNSVG.h @@ -11,6 +11,8 @@ #import #import "RCTConvert+RNSVG.h" #import "RNSVGCGFCRule.h" +#import "RNSVGColorMatrixType.h" +#import "RNSVGEdgeModeTypes.h" #import "RNSVGLength.h" #import "RNSVGMaskType.h" #import "RNSVGPathParser.h" diff --git a/apple/Utils/RCTConvert+RNSVG.mm b/apple/Utils/RCTConvert+RNSVG.mm index 34606f83..5fa3ab61 100644 --- a/apple/Utils/RCTConvert+RNSVG.mm +++ b/apple/Utils/RCTConvert+RNSVG.mm @@ -51,6 +51,27 @@ RCT_ENUM_CONVERTER( kRNSVGMaskTypeLuminance, intValue) +RCT_ENUM_CONVERTER( + RNSVGEdgeModeTypes, + (@{ + @"duplicate" : @(SVG_EDGEMODE_DUPLICATE), + @"wrap" : @(SVG_EDGEMODE_WRAP), + @"none" : @(SVG_EDGEMODE_NONE), + }), + SVG_FECOLORMATRIX_TYPE_UNKNOWN, + intValue) + +RCT_ENUM_CONVERTER( + RNSVGColorMatrixType, + (@{ + @"matrix" : @(SVG_FECOLORMATRIX_TYPE_MATRIX), + @"saturate" : @(SVG_FECOLORMATRIX_TYPE_SATURATE), + @"hueRotate" : @(SVG_FECOLORMATRIX_TYPE_HUEROTATE), + @"luminanceToAlpha" : @(SVG_FECOLORMATRIX_TYPE_LUMINANCETOALPHA), + }), + SVG_FECOLORMATRIX_TYPE_UNKNOWN, + intValue) + + (RNSVGBrush *)RNSVGBrush:(id)json { if ([json isKindOfClass:[NSNumber class]]) { diff --git a/apple/Utils/RNSVGConvert.h b/apple/Utils/RNSVGConvert.h new file mode 100644 index 00000000..c4040356 --- /dev/null +++ b/apple/Utils/RNSVGConvert.h @@ -0,0 +1,17 @@ +#ifdef RCT_NEW_ARCH_ENABLED +#import +#import "RNSVGColorMatrixType.h" +#import "RNSVGEdgeModeTypes.h" +#import "RNSVGUnits.h" + +namespace react = facebook::react; + +@interface RNSVGConvert : NSObject + ++ (RNSVGUnits)RNSVGUnitsFromFilterUnitsCppEquivalent:(react::RNSVGFilterFilterUnits)svgUnits; ++ (RNSVGUnits)RNSVGUnitsFromPrimitiveUnitsCppEquivalent:(react::RNSVGFilterPrimitiveUnits)svgUnits; ++ (RNSVGColorMatrixType)RNSVGColorMatrixTypeFromCppEquivalent:(react::RNSVGFeColorMatrixType)type; + +@end + +#endif // RCT_NEW_ARCH_ENABLED diff --git a/apple/Utils/RNSVGConvert.mm b/apple/Utils/RNSVGConvert.mm new file mode 100644 index 00000000..80548b8e --- /dev/null +++ b/apple/Utils/RNSVGConvert.mm @@ -0,0 +1,42 @@ +#import "RNSVGConvert.h" + +#ifdef RCT_NEW_ARCH_ENABLED +@implementation RNSVGConvert + ++ (RNSVGUnits)RNSVGUnitsFromFilterUnitsCppEquivalent:(react::RNSVGFilterFilterUnits)svgUnits +{ + switch (svgUnits) { + case react::RNSVGFilterFilterUnits::UserSpaceOnUse: + return kRNSVGUnitsUserSpaceOnUse; + case react::RNSVGFilterFilterUnits::ObjectBoundingBox: + return kRNSVGUnitsObjectBoundingBox; + } +} + ++ (RNSVGUnits)RNSVGUnitsFromPrimitiveUnitsCppEquivalent:(react::RNSVGFilterPrimitiveUnits)svgUnits +{ + switch (svgUnits) { + case react::RNSVGFilterPrimitiveUnits::UserSpaceOnUse: + return kRNSVGUnitsUserSpaceOnUse; + case react::RNSVGFilterPrimitiveUnits::ObjectBoundingBox: + return kRNSVGUnitsObjectBoundingBox; + } +} + ++ (RNSVGColorMatrixType)RNSVGColorMatrixTypeFromCppEquivalent:(react::RNSVGFeColorMatrixType)type; +{ + switch (type) { + case react::RNSVGFeColorMatrixType::Matrix: + return SVG_FECOLORMATRIX_TYPE_MATRIX; + case react::RNSVGFeColorMatrixType::Saturate: + return SVG_FECOLORMATRIX_TYPE_SATURATE; + case react::RNSVGFeColorMatrixType::HueRotate: + return SVG_FECOLORMATRIX_TYPE_HUEROTATE; + case react::RNSVGFeColorMatrixType::LuminanceToAlpha: + return SVG_FECOLORMATRIX_TYPE_LUMINANCETOALPHA; + } +} + +@end + +#endif // RCT_NEW_ARCH_ENABLED diff --git a/apple/Utils/RNSVGFabricConversions.h b/apple/Utils/RNSVGFabricConversions.h index 0e0f521a..abf95512 100644 --- a/apple/Utils/RNSVGFabricConversions.h +++ b/apple/Utils/RNSVGFabricConversions.h @@ -1,4 +1,5 @@ #import "RNSVGContextBrush.h" +#import "RNSVGFilterPrimitive.h" #import "RNSVGGroup.h" #import "RNSVGLength.h" #import "RNSVGPainterBrush.h" @@ -167,6 +168,7 @@ void setCommonRenderableProps(const T &renderableProps, RNSVGRenderable *rendera } renderableNode.propList = propArray; } + renderableNode.filter = RCTNSStringFromStringNilIfEmpty(renderableProps.filter); } template @@ -193,6 +195,28 @@ void setCommonGroupProps(const T &groupProps, RNSVGGroup *groupNode) } } +template +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 void setCommonTextProps(const T &textProps, RNSVGText *textNode) { diff --git a/apple/Utils/RNSVGRenderUtils.h b/apple/Utils/RNSVGRenderUtils.h new file mode 100644 index 00000000..50a54df9 --- /dev/null +++ b/apple/Utils/RNSVGRenderUtils.h @@ -0,0 +1,11 @@ +#import "RNSVGRenderable.h" + +@interface RNSVGRenderUtils : NSObject + ++ (CIContext *)sharedCIContext; ++ (CGImage *)renderToImage:(RNSVGRenderable *)renderable + ctm:(CGAffineTransform)ctm + rect:(CGRect)rect + clip:(CGRect *)clip; + +@end diff --git a/apple/Utils/RNSVGRenderUtils.mm b/apple/Utils/RNSVGRenderUtils.mm new file mode 100644 index 00000000..b769c5a8 --- /dev/null +++ b/apple/Utils/RNSVGRenderUtils.mm @@ -0,0 +1,35 @@ +#import "RNSVGRenderUtils.h" + +@implementation RNSVGRenderUtils + ++ (CIContext *)sharedCIContext +{ + static CIContext *sharedCIContext = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedCIContext = [[CIContext alloc] init]; + }); + + return sharedCIContext; +} + ++ (CGImage *)renderToImage:(RNSVGRenderable *)renderable + ctm:(CGAffineTransform)ctm + rect:(CGRect)rect + clip:(CGRect *)clip +{ + UIGraphicsBeginImageContextWithOptions(rect.size, NO, 1.0); + CGContextRef cgContext = UIGraphicsGetCurrentContext(); + CGContextConcatCTM(cgContext, CGAffineTransformInvert(CGContextGetCTM(cgContext))); + CGContextConcatCTM(cgContext, ctm); + + if (clip) { + CGContextClipToRect(cgContext, *clip); + } + [renderable renderLayerTo:cgContext rect:rect]; + CGImageRef contentImage = CGBitmapContextCreateImage(cgContext); + UIGraphicsEndImageContext(); + return contentImage; +} + +@end diff --git a/apple/ViewManagers/RNSVGFeColorMatrixManager.h b/apple/ViewManagers/RNSVGFeColorMatrixManager.h new file mode 100644 index 00000000..c04c126f --- /dev/null +++ b/apple/ViewManagers/RNSVGFeColorMatrixManager.h @@ -0,0 +1,5 @@ +#import "RNSVGFilterPrimitiveManager.h" + +@interface RNSVGFeColorMatrixManager : RNSVGFilterPrimitiveManager + +@end diff --git a/apple/ViewManagers/RNSVGFeColorMatrixManager.mm b/apple/ViewManagers/RNSVGFeColorMatrixManager.mm new file mode 100644 index 00000000..1ed206e0 --- /dev/null +++ b/apple/ViewManagers/RNSVGFeColorMatrixManager.mm @@ -0,0 +1,18 @@ +#import "RNSVGFeColorMatrixManager.h" +#import "RNSVGColorMatrixType.h" +#import "RNSVGFeColorMatrix.h" + +@implementation RNSVGFeColorMatrixManager + +RCT_EXPORT_MODULE() + +- (RNSVGFeColorMatrix *)node +{ + return [RNSVGFeColorMatrix new]; +} + +RCT_EXPORT_VIEW_PROPERTY(in1, NSString) +RCT_EXPORT_VIEW_PROPERTY(type, RNSVGColorMatrixType) +RCT_EXPORT_VIEW_PROPERTY(values, NSArray) + +@end diff --git a/apple/ViewManagers/RNSVGFilterManager.h b/apple/ViewManagers/RNSVGFilterManager.h new file mode 100644 index 00000000..a318cab2 --- /dev/null +++ b/apple/ViewManagers/RNSVGFilterManager.h @@ -0,0 +1,5 @@ +#import "RNSVGNodeManager.h" + +@interface RNSVGFilterManager : RNSVGNodeManager + +@end diff --git a/apple/ViewManagers/RNSVGFilterManager.mm b/apple/ViewManagers/RNSVGFilterManager.mm new file mode 100644 index 00000000..4fd6234a --- /dev/null +++ b/apple/ViewManagers/RNSVGFilterManager.mm @@ -0,0 +1,26 @@ +#import "RNSVGFilterManager.h" +#import "RNSVGFilter.h" + +@implementation RNSVGFilterManager + +RCT_EXPORT_MODULE() + +- (RNSVGFilter *)node +{ + return [RNSVGFilter new]; +} + +RCT_EXPORT_VIEW_PROPERTY(x, RNSVGLength *) +RCT_EXPORT_VIEW_PROPERTY(y, RNSVGLength *) +RCT_CUSTOM_VIEW_PROPERTY(width, id, RNSVGFilter) +{ + view.width = [RCTConvert RNSVGLength:json]; +} +RCT_CUSTOM_VIEW_PROPERTY(height, id, RNSVGFilter) +{ + view.height = [RCTConvert RNSVGLength:json]; +} +RCT_EXPORT_VIEW_PROPERTY(filterUnits, RNSVGUnits) +RCT_EXPORT_VIEW_PROPERTY(primitiveUnits, RNSVGUnits) + +@end diff --git a/apple/ViewManagers/RNSVGFilterPrimitiveManager.h b/apple/ViewManagers/RNSVGFilterPrimitiveManager.h new file mode 100644 index 00000000..3d1e7fa1 --- /dev/null +++ b/apple/ViewManagers/RNSVGFilterPrimitiveManager.h @@ -0,0 +1,5 @@ +#import "RNSVGNodeManager.h" + +@interface RNSVGFilterPrimitiveManager : RNSVGNodeManager + +@end diff --git a/apple/ViewManagers/RNSVGFilterPrimitiveManager.mm b/apple/ViewManagers/RNSVGFilterPrimitiveManager.mm new file mode 100644 index 00000000..780cf983 --- /dev/null +++ b/apple/ViewManagers/RNSVGFilterPrimitiveManager.mm @@ -0,0 +1,25 @@ +#import "RNSVGFilterPrimitiveManager.h" +#import "RNSVGFilterPrimitive.h" + +@implementation RNSVGFilterPrimitiveManager + +RCT_EXPORT_MODULE() + +- (RNSVGFilterPrimitive *)node +{ + return [RNSVGFilterPrimitive new]; +} + +RCT_EXPORT_VIEW_PROPERTY(x, RNSVGLength *) +RCT_EXPORT_VIEW_PROPERTY(y, RNSVGLength *) +RCT_CUSTOM_VIEW_PROPERTY(width, id, RNSVGFilterPrimitive) +{ + view.width = [RCTConvert RNSVGLength:json]; +} +RCT_CUSTOM_VIEW_PROPERTY(height, id, RNSVGFilterPrimitive) +{ + view.height = [RCTConvert RNSVGLength:json]; +} +RCT_EXPORT_VIEW_PROPERTY(result, NSString) + +@end diff --git a/apple/ViewManagers/RNSVGRenderableManager.mm b/apple/ViewManagers/RNSVGRenderableManager.mm index 3bbf5913..8d9253bc 100644 --- a/apple/ViewManagers/RNSVGRenderableManager.mm +++ b/apple/ViewManagers/RNSVGRenderableManager.mm @@ -37,5 +37,6 @@ RCT_EXPORT_VIEW_PROPERTY(strokeDashoffset, CGFloat) RCT_EXPORT_VIEW_PROPERTY(strokeMiterlimit, CGFloat) RCT_EXPORT_VIEW_PROPERTY(vectorEffect, int) RCT_EXPORT_VIEW_PROPERTY(propList, NSArray) +RCT_EXPORT_VIEW_PROPERTY(filter, NSString) @end diff --git a/apps/examples/App.tsx b/apps/examples/App.tsx index 9a9d1ad3..086a108f 100644 --- a/apps/examples/App.tsx +++ b/apps/examples/App.tsx @@ -13,86 +13,13 @@ import { ScrollView, TouchableHighlight, TouchableOpacity, + SafeAreaView, } from 'react-native'; import {Modal, Platform} from 'react-native'; import {Svg, Circle, Line} from 'react-native-svg'; import * as examples from './src/examples'; - -const hairline = StyleSheet.hairlineWidth; - -const styles = StyleSheet.create({ - container: { - flex: 1, - paddingTop: 20, - alignItems: 'center', - overflow: 'hidden', - }, - contentContainer: { - alignSelf: 'stretch', - borderTopWidth: hairline, - borderTopColor: '#ccc', - borderBottomWidth: hairline, - borderBottomColor: '#ccc', - flexWrap: 'wrap', - flexDirection: 'row', - marginHorizontal: 10, - }, - welcome: { - padding: 10, - color: '#f60', - fontSize: 18, - fontWeight: 'bold', - }, - link: { - height: 40, - alignSelf: 'stretch', - width: Dimensions.get('window').width / 2 - 10, - }, - title: { - marginLeft: 10, - }, - cell: { - height: 40, - paddingHorizontal: 10, - alignSelf: 'stretch', - alignItems: 'center', - flexDirection: 'row', - borderTopWidth: hairline, - borderTopColor: '#ccc', - marginTop: -hairline, - backgroundColor: 'transparent', - }, - close: { - position: 'absolute', - right: 20, - top: 40, - }, - scroll: { - position: 'absolute', - top: 30, - right: 10, - bottom: 20, - left: 10, - backgroundColor: '#fff', - }, - scrollContent: { - borderTopWidth: hairline, - borderTopColor: '#ccc', - }, - example: { - paddingVertical: 25, - alignSelf: 'stretch', - alignItems: 'center', - borderBottomWidth: hairline, - borderBottomColor: '#ccc', - }, - sampleTitle: { - marginHorizontal: 15, - fontSize: 16, - color: '#666', - }, -}); +import {commonStyles} from './src/commonStyles'; const names: (keyof typeof examples)[] = [ 'Svg', @@ -116,6 +43,8 @@ const names: (keyof typeof examples)[] = [ 'Transforms', 'Markers', 'Mask', + 'Filters', + 'FilterImage', ]; const initialState = { @@ -142,8 +71,8 @@ export default class SvgExample extends Component { content: ( {samples.map((Sample, i) => ( - - {Sample.title} + + {Sample.title} ))} @@ -171,9 +100,9 @@ export default class SvgExample extends Component { underlayColor="#ccc" key={`example-${name}`} onPress={() => this.show(name)}> - + {icon} - {name} + {name} ); @@ -182,13 +111,15 @@ export default class SvgExample extends Component { modalContent = () => ( <> - - {this.state.content} - - + + + {this.state.content} + + + @@ -196,14 +127,14 @@ export default class SvgExample extends Component { - + ); render() { return ( - - SVG library for React Apps + + SVG library for React Apps {this.getExamples()} {(Platform.OS === 'windows' || Platform.OS === 'macos') && this.state.modal ? ( @@ -217,7 +148,50 @@ export default class SvgExample extends Component { {this.modalContent()} )} - + ); } } + +const hairline = StyleSheet.hairlineWidth; + +const styles = StyleSheet.create({ + container: { + flex: 1, + paddingTop: 20, + alignItems: 'center', + overflow: 'hidden', + }, + contentContainer: { + alignSelf: 'stretch', + borderTopWidth: hairline, + borderTopColor: '#ccc', + borderBottomWidth: hairline, + borderBottomColor: '#ccc', + flexWrap: 'wrap', + flexDirection: 'row', + marginHorizontal: 10, + }, + link: { + height: 40, + alignSelf: 'stretch', + width: Dimensions.get('window').width / 2 - 10, + }, + close: { + position: 'absolute', + right: 20, + top: 20, + }, + scroll: { + position: 'absolute', + top: 30, + right: 10, + bottom: 20, + left: 10, + backgroundColor: '#fff', + }, + scrollContent: { + borderTopWidth: hairline, + borderTopColor: '#ccc', + }, +}); diff --git a/apps/examples/src/assets/office.jpg b/apps/examples/src/assets/office.jpg new file mode 100644 index 00000000..fe64aacb Binary files /dev/null and b/apps/examples/src/assets/office.jpg differ diff --git a/apps/examples/src/commonStyles.ts b/apps/examples/src/commonStyles.ts new file mode 100644 index 00000000..96a756c3 --- /dev/null +++ b/apps/examples/src/commonStyles.ts @@ -0,0 +1,41 @@ +import {StyleSheet} from 'react-native'; + +const hairline = StyleSheet.hairlineWidth; + +export const commonStyles = StyleSheet.create({ + welcome: { + padding: 10, + color: '#f60', + fontSize: 18, + fontWeight: 'bold', + textAlign: 'center', + }, + link: { + height: 40, + }, + title: { + marginLeft: 10, + }, + cell: { + height: 40, + paddingHorizontal: 10, + alignSelf: 'stretch', + alignItems: 'center', + flexDirection: 'row', + borderTopWidth: hairline, + borderTopColor: '#ccc', + marginTop: -hairline, + }, + example: { + paddingVertical: 25, + alignSelf: 'stretch', + alignItems: 'center', + borderBottomWidth: hairline, + borderBottomColor: '#ccc', + }, + sampleTitle: { + marginHorizontal: 15, + fontSize: 16, + color: '#666', + }, +}); diff --git a/apps/examples/src/examples.tsx b/apps/examples/src/examples.tsx index 22b7d34c..b5567a9d 100644 --- a/apps/examples/src/examples.tsx +++ b/apps/examples/src/examples.tsx @@ -19,6 +19,8 @@ import * as Reanimated from './examples/Reanimated'; import * as Transforms from './examples/Transforms'; import * as Markers from './examples/Markers'; import * as Mask from './examples/Mask'; +import * as Filters from './examples/Filters'; +import * as FilterImage from './examples/FilterImage'; export { Svg, @@ -42,4 +44,6 @@ export { Transforms, Markers, Mask, + Filters, + FilterImage, }; diff --git a/apps/examples/src/examples/FilterImage/FilterPicker.tsx b/apps/examples/src/examples/FilterImage/FilterPicker.tsx new file mode 100644 index 00000000..76edce06 --- /dev/null +++ b/apps/examples/src/examples/FilterImage/FilterPicker.tsx @@ -0,0 +1,142 @@ +import React, {useState} from 'react'; +import { + Dimensions, + FlatList, + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native'; +import {FilterImage, Filters} from 'react-native-svg/filter-image'; + +const img = require('../../assets/office.jpg'); + +const normal: Filters = []; +const losAngeles: Filters = [ + { + name: 'colorMatrix', + type: 'matrix', + values: [1.8, 0, 0, 0, 0, 0, 1.3, 0, 0, 0, 0, 0, 1.2, 0, 0, 0, 0, 0, 1, 0], + }, +]; +const lagos: Filters = [ + { + name: 'colorMatrix', + type: 'matrix', + values: [ + 1.4, 0, 0, 0, 0, 0, 1.2, 0, 0, 0, 0, 0, 1.5, 0, 0, 0, 0, 0, 0.9, 0, + ], + }, +]; +const tokyo: Filters = [ + {name: 'colorMatrix', type: 'saturate', values: [1.5]}, + { + 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 saturated: Filters = [ + {name: 'colorMatrix', type: 'saturate', values: [1.5]}, +]; +const boring: Filters = [ + { + name: 'colorMatrix', + type: 'matrix', + values: [ + 0.6965, 0.3845, 0.0945, 0, 0, 0.1745, 0.8430000000000001, 0.084, 0, 0, + 0.136, 0.267, 0.5655, 0, 0, 0, 0, 0, 1, 0, + ], + }, +]; +const filters = { + normal, + losAngeles, + lagos, + tokyo, + saturated, + boring, +} as const; + +type FilterKeys = + | 'normal' + | 'losAngeles' + | 'lagos' + | 'tokyo' + | 'saturated' + | 'boring'; +const filterKeys = Object.keys(filters) as FilterKeys[]; +const FilterImagePickerExample = () => { + const [currentFilter, setCurrentFilter] = useState('normal'); + + return ( + + + + + { + return ( + setCurrentFilter(item)}> + + + {item} + + + ); + }} + /> + + + ); +}; +FilterImagePickerExample.title = 'Filter picker'; + +const styles = StyleSheet.create({ + container: { + flex: 1, + height: Dimensions.get('window').height - 150, + width: '100%', + }, + image: {flex: 1, width: '100%', height: '100%'}, + list: { + marginTop: 8, + marginHorizontal: 8, + }, + listElement: {gap: 8}, + listElementImage: {width: 70, height: 70}, + listElementTitle: { + width: 70, + textAlign: 'center', + marginTop: 2, + marginBottom: 8, + }, +}); + +const icon = ( + +); + +const samples = [FilterImagePickerExample]; +export {icon, samples}; diff --git a/apps/examples/src/examples/FilterImage/LocalImage.tsx b/apps/examples/src/examples/FilterImage/LocalImage.tsx new file mode 100644 index 00000000..21bd2965 --- /dev/null +++ b/apps/examples/src/examples/FilterImage/LocalImage.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import {View} from 'react-native'; +import {FilterImage} from 'react-native-svg/filter-image'; + +const testImage = require('../../assets/image.jpg'); + +const FilterImageLocalExample = () => { + return ( + + + + ); +}; +FilterImageLocalExample.title = 'Local image with filter'; + +const icon = ( + +); + +const samples = [FilterImageLocalExample]; +export {icon, samples}; diff --git a/apps/examples/src/examples/FilterImage/RemoteImage.tsx b/apps/examples/src/examples/FilterImage/RemoteImage.tsx new file mode 100644 index 00000000..a4f20d92 --- /dev/null +++ b/apps/examples/src/examples/FilterImage/RemoteImage.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import {View} from 'react-native'; +import {FilterImage} from 'react-native-svg/filter-image'; + +const testSource = { + uri: 'https://cdn.pixabay.com/photo/2023/03/17/11/39/mountain-7858482_1280.jpg', +}; + +const FilterImageRemoteExample = () => { + return ( + + + + ); +}; +FilterImageRemoteExample.title = 'Remote image with filter'; + +const FilterImageFewFiltersExample = () => { + return ( + + + + ); +}; +FilterImageFewFiltersExample.title = 'Remote image with filters'; + +const icon = ( + +); + +const samples = [FilterImageRemoteExample, FilterImageFewFiltersExample]; +export {icon, samples}; diff --git a/apps/examples/src/examples/FilterImage/examples.tsx b/apps/examples/src/examples/FilterImage/examples.tsx new file mode 100644 index 00000000..7aaf1c8a --- /dev/null +++ b/apps/examples/src/examples/FilterImage/examples.tsx @@ -0,0 +1,4 @@ +import * as LocalImage from './LocalImage'; +import * as RemoteImage from './RemoteImage'; +import * as FilterPicker from './FilterPicker'; +export {LocalImage, RemoteImage, FilterPicker}; diff --git a/apps/examples/src/examples/FilterImage/index.tsx b/apps/examples/src/examples/FilterImage/index.tsx new file mode 100644 index 00000000..042aaf14 --- /dev/null +++ b/apps/examples/src/examples/FilterImage/index.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import {StyleSheet, Text, TouchableHighlight, View} from 'react-native'; + +import {FilterImage} from 'react-native-svg/filter-image'; +import {commonStyles} from '../../commonStyles'; +import * as examples from './examples'; + +const FilterImageList = () => { + const [example, setExample] = React.useState( + null, + ); + + if (example) { + return ( + <> + {examples[example].samples.map((Sample, i) => ( + + {Sample.title} + + + ))} + + ); + } + + return ( + + Filter Image + {Object.keys(examples).map((element, i) => { + const name = element as keyof typeof examples; + return ( + setExample(name)}> + + {examples[name].icon} + {name} + + + ); + })} + + ); +}; +FilterImageList.title = ''; + +const styles = StyleSheet.create({ + container: {width: '100%'}, + link: {height: 40}, +}); + +const icon = ( + +); + +const samples = [FilterImageList]; + +export {icon, samples}; diff --git a/apps/examples/src/examples/Filters/FeColorMatrix.tsx b/apps/examples/src/examples/Filters/FeColorMatrix.tsx new file mode 100644 index 00000000..40319409 --- /dev/null +++ b/apps/examples/src/examples/Filters/FeColorMatrix.tsx @@ -0,0 +1,130 @@ +import React, {Component} from 'react'; +import {Svg, Circle, FeColorMatrix, Filter, G} from 'react-native-svg'; + +class ReferenceExample extends Component { + static title = 'Reference'; + render() { + return ( + + + + + + ); + } +} +class IdentityExample extends Component { + static title = 'Identity matrix'; + render() { + return ( + + + + + + + + + + + ); + } +} +class RgbToGreenExample extends Component { + static title = 'RGB to Green'; + render() { + return ( + + + + + + + + + + + ); + } +} +class SaturateExample extends Component { + static title = 'Saturate'; + render() { + return ( + + + + + + + + + + + ); + } +} + +class HueRotateExample extends Component { + static title = 'Hue Rotate'; + render() { + return ( + + + + + + + + + + + ); + } +} + +class LuminanceToAlphaExample extends Component { + static title = 'Luminance to alpha'; + render() { + return ( + + + + + + + + + + + ); + } +} + +const icon = ( + + + + + + + + + + +); + +const samples = [ + ReferenceExample, + IdentityExample, + RgbToGreenExample, + SaturateExample, + HueRotateExample, + LuminanceToAlphaExample, +]; +export {icon, samples}; diff --git a/apps/examples/src/examples/Filters/ReanimatedFeColorMatrix.tsx b/apps/examples/src/examples/Filters/ReanimatedFeColorMatrix.tsx new file mode 100644 index 00000000..09cd0ab2 --- /dev/null +++ b/apps/examples/src/examples/Filters/ReanimatedFeColorMatrix.tsx @@ -0,0 +1,56 @@ +import React, {useEffect} from 'react'; +import Animated, { + AnimatedProps, + useAnimatedProps, + useSharedValue, + withRepeat, + withTiming, +} from 'react-native-reanimated'; +import { + Circle, + FeColorMatrix, + FeColorMatrixProps, + Filter, + Image, + Svg, +} from 'react-native-svg'; + +const AnimatedFeColorMatrix = Animated.createAnimatedComponent( + FeColorMatrix as any, +) as React.FunctionComponent>; +const ReanimatedHueRotateExample = () => { + const hue = useSharedValue(0); + + useEffect(() => { + hue.value = withRepeat(withTiming(360, {duration: 2000}), -1, true); + }, []); + const animatedProps = useAnimatedProps(() => { + return {values: [hue.value]}; + }); + + return ( + + + + + + + ); +}; +ReanimatedHueRotateExample.title = 'Reanimated Hue Rotate'; + +const icon = ( + + + + + +); + +const samples = [ReanimatedHueRotateExample]; +export {icon, samples}; diff --git a/apps/examples/src/examples/Filters/examples.tsx b/apps/examples/src/examples/Filters/examples.tsx new file mode 100644 index 00000000..4d239a51 --- /dev/null +++ b/apps/examples/src/examples/Filters/examples.tsx @@ -0,0 +1,3 @@ +import * as FeColorMatrix from './FeColorMatrix'; +import * as ReanimatedFeColorMatrix from './ReanimatedFeColorMatrix'; +export {FeColorMatrix, ReanimatedFeColorMatrix}; diff --git a/apps/examples/src/examples/Filters/index.tsx b/apps/examples/src/examples/Filters/index.tsx new file mode 100644 index 00000000..67526a5d --- /dev/null +++ b/apps/examples/src/examples/Filters/index.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import {StyleSheet, Text, TouchableHighlight, View} from 'react-native'; +import {Circle, Svg} from 'react-native-svg'; + +import * as examples from './examples'; +import {commonStyles} from '../../commonStyles'; + +const FiltersList = () => { + const [example, setExample] = React.useState( + null, + ); + + if (example) { + return ( + <> + {examples[example].samples.map((Sample, i) => ( + + {Sample.title} + + + ))} + + ); + } + + return ( + + SVG Filters + {Object.keys(examples).map((element, i) => { + const name = element as keyof typeof examples; + return ( + setExample(name)}> + + {examples[name].icon} + {name} + + + ); + })} + + ); +}; +FiltersList.title = ''; + +const styles = StyleSheet.create({ + container: {width: '100%'}, + link: {height: 40}, +}); + +const icon = ( + + + + + +); + +const samples = [FiltersList]; + +export {icon, samples}; diff --git a/filter-image/package.json b/filter-image/package.json new file mode 100644 index 00000000..4ffb83ed --- /dev/null +++ b/filter-image/package.json @@ -0,0 +1,6 @@ +{ + "main": "../lib/commonjs/filter-image/index", + "module": "../lib/module/filter-image/index", + "react-native": "../src/filter-image/index", + "types": "../lib/typescript/filter-image/index" +} diff --git a/package.json b/package.json index 0cf88ee1..9346bacc 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "lib", "src", "css", + "filter-image", "RNSVG.podspec", "!android/build", "windows", @@ -64,7 +65,8 @@ }, "dependencies": { "css-select": "^5.1.0", - "css-tree": "^1.1.3" + "css-tree": "^1.1.3", + "warn-once": "0.1.1" }, "devDependencies": { "@react-native-community/eslint-config": "^3.0.2", diff --git a/react-native.config.js b/react-native.config.js index f1254afa..3d2f58f8 100644 --- a/react-native.config.js +++ b/react-native.config.js @@ -9,34 +9,38 @@ try { } module.exports = { - dependency: { - platforms: { - android: supportsCodegenConfig ? { - componentDescriptors: [ - "RNSVGCircleComponentDescriptor", - "RNSVGClipPathComponentDescriptor", - "RNSVGDefsComponentDescriptor", - "RNSVGEllipseComponentDescriptor", - "RNSVGForeignObjectComponentDescriptor", - "RNSVGGroupComponentDescriptor", - "RNSVGImageComponentDescriptor", - "RNSVGLinearGradientComponentDescriptor", - "RNSVGLineComponentDescriptor", - "RNSVGMarkerComponentDescriptor", - "RNSVGMaskComponentDescriptor", - "RNSVGPathComponentDescriptor", - "RNSVGPatternComponentDescriptor", - "RNSVGRadialGradientComponentDescriptor", - "RNSVGRectComponentDescriptor", - "RNSVGSvgViewAndroidComponentDescriptor", - "RNSVGSymbolComponentDescriptor", - "RNSVGTextComponentDescriptor", - "RNSVGTextPathComponentDescriptor", - "RNSVGTSpanComponentDescriptor", - "RNSVGUseComponentDescriptor" - ], - cmakeListsPath: "../android/src/main/jni/CMakeLists.txt" - } : {}, - }, + dependency: { + platforms: { + android: supportsCodegenConfig + ? { + componentDescriptors: [ + 'RNSVGCircleComponentDescriptor', + 'RNSVGClipPathComponentDescriptor', + 'RNSVGDefsComponentDescriptor', + 'RNSVGFeColorMatrixComponentDescriptor', + 'RNSVGFilterComponentDescriptor', + 'RNSVGEllipseComponentDescriptor', + 'RNSVGForeignObjectComponentDescriptor', + 'RNSVGGroupComponentDescriptor', + 'RNSVGImageComponentDescriptor', + 'RNSVGLinearGradientComponentDescriptor', + 'RNSVGLineComponentDescriptor', + 'RNSVGMarkerComponentDescriptor', + 'RNSVGMaskComponentDescriptor', + 'RNSVGPathComponentDescriptor', + 'RNSVGPatternComponentDescriptor', + 'RNSVGRadialGradientComponentDescriptor', + 'RNSVGRectComponentDescriptor', + 'RNSVGSvgViewAndroidComponentDescriptor', + 'RNSVGSymbolComponentDescriptor', + 'RNSVGTextComponentDescriptor', + 'RNSVGTextPathComponentDescriptor', + 'RNSVGTSpanComponentDescriptor', + 'RNSVGUseComponentDescriptor', + ], + cmakeListsPath: '../android/src/main/jni/CMakeLists.txt', + } + : {}, }, - } + }, +}; diff --git a/screenshots/feColorMatrix.png b/screenshots/feColorMatrix.png new file mode 100644 index 00000000..764e6e1b Binary files /dev/null and b/screenshots/feColorMatrix.png differ diff --git a/screenshots/filterImage.png b/screenshots/filterImage.png new file mode 100644 index 00000000..3a6654e8 Binary files /dev/null and b/screenshots/filterImage.png differ diff --git a/src/ReactNativeSVG.ts b/src/ReactNativeSVG.ts index 59882e64..e7dc7d1e 100644 --- a/src/ReactNativeSVG.ts +++ b/src/ReactNativeSVG.ts @@ -23,6 +23,8 @@ import Pattern from './elements/Pattern'; import Mask from './elements/Mask'; import Marker from './elements/Marker'; import ForeignObject from './elements/ForeignObject'; +import Filter from './elements/filters/Filter'; +import FeColorMatrix from './elements/filters/FeColorMatrix'; import { parse, @@ -67,6 +69,8 @@ import { RNSVGTextPath, RNSVGTSpan, RNSVGUse, + RNSVGFilter, + RNSVGFeColorMatrix, } from './fabric'; export { @@ -103,6 +107,9 @@ export type { PatternProps } from './elements/Pattern'; export type { MaskProps } from './elements/Mask'; export type { MarkerProps } from './elements/Marker'; export type { ForeignObjectProps } from './elements/ForeignObject'; +export type { FilterProps } from './elements/filters/Filter'; +export type { FeColorMatrixProps } from './elements/filters/FeColorMatrix'; +export type { FilterPrimitiveCommonProps } from './elements/filters/FilterPrimitive'; export * from './lib/extract/types'; @@ -140,6 +147,8 @@ export { camelCase, fetchText, Shape, + Filter, + FeColorMatrix, RNSVGMarker, RNSVGMask, RNSVGPattern, @@ -162,6 +171,8 @@ export { RNSVGSvgAndroid, RNSVGSvgIOS, RNSVGForeignObject, + RNSVGFilter, + RNSVGFeColorMatrix, }; export type { diff --git a/src/elements/Mask.tsx b/src/elements/Mask.tsx index 33301fb7..fe0eeb79 100644 --- a/src/elements/Mask.tsx +++ b/src/elements/Mask.tsx @@ -1,16 +1,18 @@ import type { ReactNode } from 'react'; import * as React from 'react'; import { withoutXY } from '../lib/extract/extractProps'; -import type { CommonPathProps, NumberProp } from '../lib/extract/types'; +import type { + CommonPathProps, + MaskType, + NumberProp, + Units, +} from '../lib/extract/types'; 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; id?: string; @@ -18,11 +20,11 @@ export interface MaskProps extends CommonPathProps { y?: NumberProp; width?: NumberProp; height?: NumberProp; - maskUnits?: TMaskUnits; - maskContentUnits?: TMaskUnits; - maskType?: TMaskType; + maskUnits?: Units; + maskContentUnits?: Units; + maskType?: MaskType; style?: { - maskType: TMaskType; + maskType: MaskType; }; } diff --git a/src/elements/filters/FeColorMatrix.tsx b/src/elements/filters/FeColorMatrix.tsx new file mode 100644 index 00000000..caa06199 --- /dev/null +++ b/src/elements/filters/FeColorMatrix.tsx @@ -0,0 +1,37 @@ +import * as React from 'react'; +import { NativeMethods } from 'react-native'; +import RNSVGFeColorMatrix from '../../fabric/FeColorMatrixNativeComponent'; +import { + extractFeColorMatrix, + extractFilter, +} from '../../lib/extract/extractFilter'; +import { FilterColorMatrixType } from '../../lib/extract/types'; +import FilterPrimitive from './FilterPrimitive'; + +export type FeColorMatrixProps = { + in?: string; + type?: FilterColorMatrixType; + values?: number | Array | string; +}; + +export default class FeColorMatrix extends FilterPrimitive { + static displayName = 'FeColorMatrix'; + + static defaultProps = { + ...this.defaultPrimitiveProps, + type: 'matrix', + values: '', + }; + + render() { + return ( + + this.refMethod(ref as (FeColorMatrix & NativeMethods) | null) + } + {...extractFilter(this.props)} + {...extractFeColorMatrix(this.props)} + /> + ); + } +} diff --git a/src/elements/filters/Filter.tsx b/src/elements/filters/Filter.tsx new file mode 100644 index 00000000..a0d4dbf6 --- /dev/null +++ b/src/elements/filters/Filter.tsx @@ -0,0 +1,54 @@ +import * as React from 'react'; +import { NativeMethods } from 'react-native'; +import RNSVGFilter from '../../fabric/FilterNativeComponent'; +import { NumberProp, Units } from '../../lib/extract/types'; +import Shape from '../Shape'; +import warnOnce from 'warn-once'; + +export interface FilterProps { + children?: React.ReactNode; + id?: string; + x?: NumberProp; + y?: NumberProp; + width?: NumberProp; + height?: NumberProp; + filterUnits?: Units; + // TODO: Implement + primitiveUnits?: Units; +} + +export default class Filter extends Shape { + static displayName = 'Filter'; + + static defaultProps = { + x: '-10%', + y: '-10%', + width: '120%', + height: '120%', + filterUnits: 'objectBoundingBox', + // primitiveUnits: 'userSpaceOnUse', + }; + + render() { + const { id, x, y, width, height, filterUnits, primitiveUnits } = this.props; + warnOnce( + !!primitiveUnits, + "WARNING: Filter's `primitiveUnits` prop is not supported yet" + ); + const filterProps = { + name: id, + x, + y, + width, + height, + filterUnits: filterUnits || 'objectBoundingBox', + }; + return ( + this.refMethod(ref as (Filter & NativeMethods) | null)} + {...filterProps}> + {this.props.children} + + ); + } +} diff --git a/src/elements/filters/FilterPrimitive.tsx b/src/elements/filters/FilterPrimitive.tsx new file mode 100644 index 00000000..567666d6 --- /dev/null +++ b/src/elements/filters/FilterPrimitive.tsx @@ -0,0 +1,35 @@ +import { Component } from 'react'; +import { NativeMethods } from 'react-native'; +import { NumberProp } from '../../lib/extract/types'; + +export interface FilterPrimitiveCommonProps { + x?: NumberProp; + y?: NumberProp; + width?: NumberProp; + height?: NumberProp; + result?: string; +} + +export default class FilterPrimitive

extends Component< + P & FilterPrimitiveCommonProps +> { + [x: string]: unknown; + root: (FilterPrimitive

& NativeMethods) | null = null; + + static defaultPrimitiveProps = { + x: '0%', + y: '0%', + width: '100%', + height: '100%', + }; + + refMethod: (instance: (FilterPrimitive

& NativeMethods) | null) => void = ( + instance: (FilterPrimitive

& NativeMethods) | null + ) => { + this.root = instance; + }; + + setNativeProps = (props: P) => { + this.root?.setNativeProps(props); + }; +} diff --git a/src/fabric/CircleNativeComponent.ts b/src/fabric/CircleNativeComponent.ts index 40f2b945..260ea3aa 100644 --- a/src/fabric/CircleNativeComponent.ts +++ b/src/fabric/CircleNativeComponent.ts @@ -45,6 +45,7 @@ interface SvgRenderableCommonProps { strokeMiterlimit?: Float; vectorEffect?: WithDefault; propList?: ReadonlyArray; + filter?: string; } interface NativeProps diff --git a/src/fabric/ClipPathNativeComponent.ts b/src/fabric/ClipPathNativeComponent.ts index c009f91f..8f2cd675 100644 --- a/src/fabric/ClipPathNativeComponent.ts +++ b/src/fabric/ClipPathNativeComponent.ts @@ -45,6 +45,7 @@ interface SvgRenderableCommonProps { strokeMiterlimit?: Float; vectorEffect?: WithDefault; propList?: ReadonlyArray; + filter?: string; } interface SvgGroupCommonProps { diff --git a/src/fabric/EllipseNativeComponent.ts b/src/fabric/EllipseNativeComponent.ts index 6233360e..1a95c1c7 100644 --- a/src/fabric/EllipseNativeComponent.ts +++ b/src/fabric/EllipseNativeComponent.ts @@ -45,6 +45,7 @@ interface SvgRenderableCommonProps { strokeMiterlimit?: Float; vectorEffect?: WithDefault; propList?: ReadonlyArray; + filter?: string; } interface NativeProps diff --git a/src/fabric/FeColorMatrixNativeComponent.ts b/src/fabric/FeColorMatrixNativeComponent.ts new file mode 100644 index 00000000..62f167d2 --- /dev/null +++ b/src/fabric/FeColorMatrixNativeComponent.ts @@ -0,0 +1,24 @@ +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; +import type { ViewProps } from './utils'; + +import { NumberProp } from '../lib/extract/types'; +import type { UnsafeMixed } from './codegenUtils'; +import { Float, WithDefault } from 'react-native/Libraries/Types/CodegenTypes'; + +type ColorMatrixType = 'matrix' | 'saturate' | 'hueRotate' | 'luminanceToAlpha'; + +interface FilterPrimitiveCommonProps { + x?: UnsafeMixed; + y?: UnsafeMixed; + width?: UnsafeMixed; + height?: UnsafeMixed; + result?: string; +} + +export interface NativeProps extends ViewProps, FilterPrimitiveCommonProps { + in1?: string; + type?: WithDefault; + values?: ReadonlyArray; +} + +export default codegenNativeComponent('RNSVGFeColorMatrix'); diff --git a/src/fabric/FilterNativeComponent.ts b/src/fabric/FilterNativeComponent.ts new file mode 100644 index 00000000..11066d07 --- /dev/null +++ b/src/fabric/FilterNativeComponent.ts @@ -0,0 +1,19 @@ +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; +import { NumberProp } from '../lib/extract/types'; +import type { UnsafeMixed } from './codegenUtils'; +import type { ViewProps } from './utils'; +import { WithDefault } from 'react-native/Libraries/Types/CodegenTypes'; + +type Units = 'userSpaceOnUse' | 'objectBoundingBox'; + +interface NativeProps extends ViewProps { + name?: string; + x?: UnsafeMixed; + y?: UnsafeMixed; + height?: UnsafeMixed; + width?: UnsafeMixed; + filterUnits?: WithDefault; + primitiveUnits?: WithDefault; +} + +export default codegenNativeComponent('RNSVGFilter'); diff --git a/src/fabric/ForeignObjectNativeComponent.ts b/src/fabric/ForeignObjectNativeComponent.ts index fe5192bd..b2b1622d 100644 --- a/src/fabric/ForeignObjectNativeComponent.ts +++ b/src/fabric/ForeignObjectNativeComponent.ts @@ -45,6 +45,7 @@ interface SvgRenderableCommonProps { strokeMiterlimit?: Float; vectorEffect?: WithDefault; propList?: ReadonlyArray; + filter?: string; } interface SvgGroupCommonProps { diff --git a/src/fabric/GroupNativeComponent.ts b/src/fabric/GroupNativeComponent.ts index 0c0f6c69..90798454 100644 --- a/src/fabric/GroupNativeComponent.ts +++ b/src/fabric/GroupNativeComponent.ts @@ -45,6 +45,7 @@ interface SvgRenderableCommonProps { strokeMiterlimit?: Float; vectorEffect?: WithDefault; propList?: ReadonlyArray; + filter?: string; } interface SvgGroupCommonProps { diff --git a/src/fabric/ImageNativeComponent.ts b/src/fabric/ImageNativeComponent.ts index e9270399..027af78e 100644 --- a/src/fabric/ImageNativeComponent.ts +++ b/src/fabric/ImageNativeComponent.ts @@ -58,6 +58,7 @@ interface SvgRenderableCommonProps { strokeMiterlimit?: Float; vectorEffect?: WithDefault; propList?: ReadonlyArray; + filter?: string; } interface NativeProps diff --git a/src/fabric/LineNativeComponent.ts b/src/fabric/LineNativeComponent.ts index e873047a..3e02a81d 100644 --- a/src/fabric/LineNativeComponent.ts +++ b/src/fabric/LineNativeComponent.ts @@ -45,6 +45,7 @@ interface SvgRenderableCommonProps { strokeMiterlimit?: Float; vectorEffect?: WithDefault; propList?: ReadonlyArray; + filter?: string; } interface NativeProps diff --git a/src/fabric/MarkerNativeComponent.ts b/src/fabric/MarkerNativeComponent.ts index 416817e7..c3441601 100644 --- a/src/fabric/MarkerNativeComponent.ts +++ b/src/fabric/MarkerNativeComponent.ts @@ -45,6 +45,7 @@ interface SvgRenderableCommonProps { strokeMiterlimit?: Float; vectorEffect?: WithDefault; propList?: ReadonlyArray; + filter?: string; } interface SvgGroupCommonProps { diff --git a/src/fabric/MaskNativeComponent.ts b/src/fabric/MaskNativeComponent.ts index a273396f..65b547e2 100644 --- a/src/fabric/MaskNativeComponent.ts +++ b/src/fabric/MaskNativeComponent.ts @@ -45,6 +45,7 @@ interface SvgRenderableCommonProps { strokeMiterlimit?: Float; vectorEffect?: WithDefault; propList?: ReadonlyArray; + filter?: string; } interface SvgGroupCommonProps { diff --git a/src/fabric/PathNativeComponent.ts b/src/fabric/PathNativeComponent.ts index be5dae35..866fd562 100644 --- a/src/fabric/PathNativeComponent.ts +++ b/src/fabric/PathNativeComponent.ts @@ -45,6 +45,7 @@ interface SvgRenderableCommonProps { strokeMiterlimit?: Float; vectorEffect?: WithDefault; propList?: ReadonlyArray; + filter?: string; } interface NativeProps diff --git a/src/fabric/PatternNativeComponent.ts b/src/fabric/PatternNativeComponent.ts index 05e019fd..4bd2afe0 100644 --- a/src/fabric/PatternNativeComponent.ts +++ b/src/fabric/PatternNativeComponent.ts @@ -45,6 +45,7 @@ interface SvgRenderableCommonProps { strokeMiterlimit?: Float; vectorEffect?: WithDefault; propList?: ReadonlyArray; + filter?: string; } interface SvgGroupCommonProps { diff --git a/src/fabric/RectNativeComponent.ts b/src/fabric/RectNativeComponent.ts index 6db2e304..80e115b1 100644 --- a/src/fabric/RectNativeComponent.ts +++ b/src/fabric/RectNativeComponent.ts @@ -45,6 +45,7 @@ interface SvgRenderableCommonProps { strokeMiterlimit?: Float; vectorEffect?: WithDefault; propList?: ReadonlyArray; + filter?: string; } interface NativeProps diff --git a/src/fabric/SymbolNativeComponent.ts b/src/fabric/SymbolNativeComponent.ts index 52abe543..2d537105 100644 --- a/src/fabric/SymbolNativeComponent.ts +++ b/src/fabric/SymbolNativeComponent.ts @@ -45,6 +45,7 @@ interface SvgRenderableCommonProps { strokeMiterlimit?: Float; vectorEffect?: WithDefault; propList?: ReadonlyArray; + filter?: string; } interface SvgGroupCommonProps { diff --git a/src/fabric/TSpanNativeComponent.ts b/src/fabric/TSpanNativeComponent.ts index 80246c78..60e67a56 100644 --- a/src/fabric/TSpanNativeComponent.ts +++ b/src/fabric/TSpanNativeComponent.ts @@ -45,6 +45,7 @@ interface SvgRenderableCommonProps { strokeMiterlimit?: Float; vectorEffect?: WithDefault; propList?: ReadonlyArray; + filter?: string; } interface SvgGroupCommonProps { diff --git a/src/fabric/TextNativeComponent.ts b/src/fabric/TextNativeComponent.ts index bbeba338..de3d109a 100644 --- a/src/fabric/TextNativeComponent.ts +++ b/src/fabric/TextNativeComponent.ts @@ -45,6 +45,7 @@ interface SvgRenderableCommonProps { strokeMiterlimit?: Float; vectorEffect?: WithDefault; propList?: ReadonlyArray; + filter?: string; } interface SvgGroupCommonProps { diff --git a/src/fabric/TextPathNativeComponent.ts b/src/fabric/TextPathNativeComponent.ts index 2e509b2c..73c292e4 100644 --- a/src/fabric/TextPathNativeComponent.ts +++ b/src/fabric/TextPathNativeComponent.ts @@ -45,6 +45,7 @@ interface SvgRenderableCommonProps { strokeMiterlimit?: Float; vectorEffect?: WithDefault; propList?: ReadonlyArray; + filter?: string; } interface SvgGroupCommonProps { diff --git a/src/fabric/UseNativeComponent.ts b/src/fabric/UseNativeComponent.ts index 0a88f9a2..4fd51ab3 100644 --- a/src/fabric/UseNativeComponent.ts +++ b/src/fabric/UseNativeComponent.ts @@ -45,6 +45,7 @@ interface SvgRenderableCommonProps { strokeMiterlimit?: Float; vectorEffect?: WithDefault; propList?: ReadonlyArray; + filter?: string; } interface NativeProps diff --git a/src/fabric/index.ts b/src/fabric/index.ts index 8e23dbb3..e7f39504 100644 --- a/src/fabric/index.ts +++ b/src/fabric/index.ts @@ -20,6 +20,8 @@ import RNSVGText from './TextNativeComponent'; import RNSVGTextPath from './TextPathNativeComponent'; import RNSVGTSpan from './TSpanNativeComponent'; import RNSVGUse from './UseNativeComponent'; +import RNSVGFilter from './FilterNativeComponent'; +import RNSVGFeColorMatrix from './FeColorMatrixNativeComponent'; export { RNSVGCircle, @@ -44,4 +46,6 @@ export { RNSVGTextPath, RNSVGTSpan, RNSVGUse, + RNSVGFilter, + RNSVGFeColorMatrix, }; diff --git a/src/filter-image/FilterImage.tsx b/src/filter-image/FilterImage.tsx new file mode 100644 index 00000000..beee1241 --- /dev/null +++ b/src/filter-image/FilterImage.tsx @@ -0,0 +1,54 @@ +import { ImageProps, Image as RNImage, StyleSheet, View } from 'react-native'; +import { FeColorMatrix, Filter, Image, Svg } from '../index'; +import { Filters } from './types'; +import { extractResizeMode } from './extractImage'; + +export interface FilterImageProps extends ImageProps { + filters: Filters; +} + +const getFilters = (filters: FilterImageProps['filters']) => { + return filters?.map((filter, index) => { + const { name, ...filterProps } = filter; + switch (name) { + case 'colorMatrix': + return ; + default: + return null; + } + }); +}; + +export const FilterImage = (props: FilterImageProps) => { + const { source, style, ...imageProps } = props; + + const src = RNImage.resolveAssetSource(source); + const styles = StyleSheet.flatten(style); + const width = props.width || styles?.width || src.width; + const height = props.height || styles?.height || src.height; + const preserveAspectRatio = extractResizeMode(props.resizeMode); + + return ( + + + {getFilters(props.filters)} + + + + ); +}; diff --git a/src/filter-image/extractImage.ts b/src/filter-image/extractImage.ts new file mode 100644 index 00000000..7cfdbea7 --- /dev/null +++ b/src/filter-image/extractImage.ts @@ -0,0 +1,12 @@ +export const extractResizeMode = (resizeMode?: string) => { + switch (resizeMode) { + case 'contain': + return 'xMidYMid meet'; + case 'stretch': + return 'none'; + case 'center': + return 'xMidYMid meet'; + default: + return 'xMidYMid slice'; + } +}; diff --git a/src/filter-image/index.tsx b/src/filter-image/index.tsx new file mode 100644 index 00000000..d9d0ec71 --- /dev/null +++ b/src/filter-image/index.tsx @@ -0,0 +1,6 @@ +import { FilterImage, FilterImageProps } from './FilterImage'; +import { Filters } from './types'; + +export { FilterImage }; + +export type { FilterImageProps, Filters }; diff --git a/src/filter-image/types.ts b/src/filter-image/types.ts new file mode 100644 index 00000000..1dc7803d --- /dev/null +++ b/src/filter-image/types.ts @@ -0,0 +1,7 @@ +import { FilterPrimitiveCommonProps } from '../elements/filters/FilterPrimitive'; +import { FeColorMatrixProps } from '../index'; + +export type FilterElement = ({ name: 'colorMatrix' } & FeColorMatrixProps) & + FilterPrimitiveCommonProps; + +export type Filters = Array; diff --git a/src/lib/extract/extractFilter.ts b/src/lib/extract/extractFilter.ts new file mode 100644 index 00000000..63a8e4bc --- /dev/null +++ b/src/lib/extract/extractFilter.ts @@ -0,0 +1,55 @@ +import { FeColorMatrixProps as FeColorMatrixComponentProps } from '../../elements/filters/FeColorMatrix'; +import { NativeProps as FeColorMatrixNativeProps } from '../../fabric/FeColorMatrixNativeComponent'; +import { NumberProp } from './types'; + +const spaceReg = /\s+/; + +interface FilterPrimitiveCommonProps { + x?: NumberProp; + y?: NumberProp; + width?: NumberProp; + height?: NumberProp; + result?: string; +} + +export const extractFilter = ( + props: FilterPrimitiveCommonProps +): FilterPrimitiveCommonProps => { + const { x, y, width, height, result } = props; + const extracted: FilterPrimitiveCommonProps = { + x, + y, + width, + height, + result, + }; + + return extracted; +}; + +export const extractFeColorMatrix = ( + props: FeColorMatrixComponentProps +): FeColorMatrixNativeProps => { + const extracted: FeColorMatrixNativeProps = {}; + + if (props.in) { + extracted.in1 = props.in; + } + if (props.values !== undefined) { + if (Array.isArray(props.values)) { + extracted.values = props.values; + } else if (typeof props.values === 'number') { + extracted.values = [props.values]; + } else if (typeof props.values === 'string') { + extracted.values = props.values + .split(spaceReg) + .map(parseFloat) + .filter((el: number) => !isNaN(el)); + } else { + console.warn('Invalid value for FeColorMatrix `values` prop'); + } + } + if (props.type) extracted.type = props.type; + + return extracted; +}; diff --git a/src/lib/extract/extractProps.ts b/src/lib/extract/extractProps.ts index 90a89f36..090041e4 100644 --- a/src/lib/extract/extractProps.ts +++ b/src/lib/extract/extractProps.ts @@ -41,6 +41,7 @@ export default function extractProps( props: { id?: string; mask?: string; + filter?: string; marker?: string; markerStart?: string; markerMid?: string; @@ -67,6 +68,7 @@ export default function extractProps( clipRule, display, mask, + filter, marker, markerStart = marker, markerMid = marker, @@ -159,6 +161,20 @@ export default function extractProps( } } + if (filter) { + const matched = filter.match(idPattern); + + if (matched) { + extracted.filter = matched[1]; + } else { + console.warn( + 'Invalid `filter` prop, expected a filter like "#id", but got: "' + + filter + + '"' + ); + } + } + return extracted; } diff --git a/src/lib/extract/types.ts b/src/lib/extract/types.ts index 821f7941..7f308d99 100644 --- a/src/lib/extract/types.ts +++ b/src/lib/extract/types.ts @@ -82,6 +82,13 @@ export type TextPathMidLine = 'sharp' | 'smooth'; export type Linecap = 'butt' | 'square' | 'round'; export type Linejoin = 'miter' | 'bevel' | 'round'; +export type FilterEdgeMode = 'duplicate' | 'wrap' | 'none'; +export type FilterColorMatrixType = + | 'matrix' + | 'saturate' + | 'hueRotate' + | 'luminanceToAlpha'; + export interface TouchableProps { disabled?: boolean; onPress?: (event: GestureResponderEvent) => void; @@ -221,10 +228,16 @@ export interface TransformedProps { y: number; } +export type MaskType = 'alpha' | 'luminance'; + export interface CommonMaskProps { mask?: string; } +export interface CommonFilterProps { + filter?: string; +} + export interface CommonMarkerProps { marker?: string; markerStart?: string; @@ -242,6 +255,7 @@ export interface AccessibilityProps { testID?: string; } +// FIXME: This interface should probably be named CommonRenderableProps export interface CommonPathProps extends FillProps, StrokeProps, @@ -253,6 +267,7 @@ export interface CommonPathProps DefinitionProps, CommonMarkerProps, CommonMaskProps, + CommonFilterProps, NativeProps, AccessibilityProps {} diff --git a/src/xml.tsx b/src/xml.tsx index 61ff3370..77e49c9a 100644 --- a/src/xml.tsx +++ b/src/xml.tsx @@ -25,6 +25,8 @@ import ClipPath from './elements/ClipPath'; import Pattern from './elements/Pattern'; import Mask from './elements/Mask'; import Marker from './elements/Marker'; +import Filter from './elements/filters/Filter'; +import FeColorMatrix from './elements/filters/FeColorMatrix'; export const tags: { [tag: string]: ComponentType } = { svg: Svg, @@ -50,6 +52,8 @@ export const tags: { [tag: string]: ComponentType } = { pattern: Pattern, mask: Mask, marker: Marker, + filter: Filter, + feColorMatrix: FeColorMatrix, }; function missingTag() { diff --git a/tsconfig.json b/tsconfig.json index 435a148a..e47cd830 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,8 @@ "declaration": true, "paths": { "react-native-svg": ["./src"], - "react-native-svg/css": ["./src/css/index.tsx"] + "react-native-svg/css": ["./src/css/index.tsx"], + "react-native-svg/filter-image": ["./src/filter-image/index.tsx"] }, "preserveSymlinks": true, "target": "es6", diff --git a/yarn.lock b/yarn.lock index 805243b6..e5d7aecb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9825,6 +9825,11 @@ walker@^1.0.7, walker@^1.0.8: dependencies: makeerror "1.0.12" +warn-once@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/warn-once/-/warn-once-0.1.1.tgz#952088f4fb56896e73fd4e6a3767272a3fccce43" + integrity sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q== + wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"