mirror of
https://github.com/zoriya/react-native-svg.git
synced 2025-12-06 07:06:11 +00:00
feat: implement filter region (#2441)
# Summary Implement proper handling for filter region according to the specs: * [FilterEffectsRegion](https://www.w3.org/TR/SVG11/filters.html#FilterEffectsRegion) * [FilterPrimitiveSubRegion](https://www.w3.org/TR/SVG11/filters.html#FilterPrimitiveSubRegion) enabling user to specify * `filterUnits` * `primitiveUnits` * `x` * `y` * `width` * `height` on `Filter` element and the last four on filter primitives. ## Compatibility | OS | Implemented | | ------- | :---------: | | iOS | ✅ | | MacOS | ✅ | | Android | ✅ | | Web | ✅ |
This commit is contained in:
@@ -3,7 +3,7 @@ package com.horcrux.svg;
|
|||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Paint;
|
import android.graphics.RectF;
|
||||||
import com.facebook.react.bridge.Dynamic;
|
import com.facebook.react.bridge.Dynamic;
|
||||||
import com.facebook.react.bridge.ReactContext;
|
import com.facebook.react.bridge.ReactContext;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -42,8 +42,10 @@ class FeOffsetView extends FilterPrimitiveView {
|
|||||||
|
|
||||||
float dx = this.mDx != null ? (float) this.relativeOnWidth(this.mDx) : 0;
|
float dx = this.mDx != null ? (float) this.relativeOnWidth(this.mDx) : 0;
|
||||||
float dy = this.mDy != null ? (float) this.relativeOnHeight(this.mDy) : 0;
|
float dy = this.mDy != null ? (float) this.relativeOnHeight(this.mDy) : 0;
|
||||||
|
RectF frame = new RectF(0, 0, dx, dy);
|
||||||
|
this.getSvgView().getCtm().mapRect(frame);
|
||||||
|
|
||||||
canvas.drawBitmap(source, dx, dy, new Paint());
|
canvas.drawBitmap(source, frame.width(), frame.height(), null);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,33 +8,31 @@ import java.util.HashMap;
|
|||||||
|
|
||||||
@SuppressLint("ViewConstructor")
|
@SuppressLint("ViewConstructor")
|
||||||
class FilterPrimitiveView extends DefinitionView {
|
class FilterPrimitiveView extends DefinitionView {
|
||||||
SVGLength mX;
|
|
||||||
SVGLength mY;
|
|
||||||
SVGLength mW;
|
|
||||||
SVGLength mH;
|
|
||||||
private String mResult;
|
private String mResult;
|
||||||
|
public final FilterRegion mFilterRegion;
|
||||||
|
|
||||||
public FilterPrimitiveView(ReactContext reactContext) {
|
public FilterPrimitiveView(ReactContext reactContext) {
|
||||||
super(reactContext);
|
super(reactContext);
|
||||||
|
mFilterRegion = new FilterRegion();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setX(Dynamic x) {
|
public void setX(Dynamic x) {
|
||||||
mX = SVGLength.from(x);
|
mFilterRegion.setX(x);
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setY(Dynamic y) {
|
public void setY(Dynamic y) {
|
||||||
mY = SVGLength.from(y);
|
mFilterRegion.setY(y);
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setWidth(Dynamic width) {
|
public void setWidth(Dynamic width) {
|
||||||
mW = SVGLength.from(width);
|
mFilterRegion.setWidth(width);
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setHeight(Dynamic height) {
|
public void setHeight(Dynamic height) {
|
||||||
mH = SVGLength.from(height);
|
mFilterRegion.setHeight(height);
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
49
android/src/main/java/com/horcrux/svg/FilterRegion.java
Normal file
49
android/src/main/java/com/horcrux/svg/FilterRegion.java
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package com.horcrux.svg;
|
||||||
|
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import com.facebook.react.bridge.Dynamic;
|
||||||
|
|
||||||
|
public class FilterRegion {
|
||||||
|
SVGLength mX;
|
||||||
|
SVGLength mY;
|
||||||
|
SVGLength mW;
|
||||||
|
SVGLength mH;
|
||||||
|
|
||||||
|
public void setX(Dynamic x) {
|
||||||
|
mX = SVGLength.from(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setY(Dynamic y) {
|
||||||
|
mY = SVGLength.from(y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWidth(Dynamic width) {
|
||||||
|
mW = SVGLength.from(width);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHeight(Dynamic height) {
|
||||||
|
mH = SVGLength.from(height);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Rect getCropRect(VirtualView view, FilterProperties.Units units, RectF renderableBounds) {
|
||||||
|
double x, y, width, height;
|
||||||
|
if (units == FilterProperties.Units.USER_SPACE_ON_USE) {
|
||||||
|
x = view.relativeOn(this.mX, view.getSvgView().getCanvasWidth());
|
||||||
|
y = view.relativeOn(this.mY, view.getSvgView().getCanvasHeight());
|
||||||
|
width = view.relativeOn(this.mW, view.getSvgView().getCanvasWidth());
|
||||||
|
height = view.relativeOn(this.mH, view.getSvgView().getCanvasHeight());
|
||||||
|
return new Rect((int) x, (int) y, (int) (x + width), (int) (y + height));
|
||||||
|
} else { // FilterProperties.Units.OBJECT_BOUNDING_BOX
|
||||||
|
x = view.relativeOnFraction(this.mX, renderableBounds.width());
|
||||||
|
y = view.relativeOnFraction(this.mY, renderableBounds.height());
|
||||||
|
width = view.relativeOnFraction(this.mW, renderableBounds.width());
|
||||||
|
height = view.relativeOnFraction(this.mH, renderableBounds.height());
|
||||||
|
return new Rect(
|
||||||
|
(int) (renderableBounds.left + x),
|
||||||
|
(int) (renderableBounds.top + y),
|
||||||
|
(int) (renderableBounds.left + x + width),
|
||||||
|
(int) (renderableBounds.top + y + height));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,9 @@ package com.horcrux.svg;
|
|||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.RectF;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import com.facebook.react.bridge.Dynamic;
|
import com.facebook.react.bridge.Dynamic;
|
||||||
@@ -14,37 +16,32 @@ import java.util.HashMap;
|
|||||||
class FilterView extends DefinitionView {
|
class FilterView extends DefinitionView {
|
||||||
private final HashMap<String, Bitmap> mResultsMap = new HashMap<>();
|
private final HashMap<String, Bitmap> mResultsMap = new HashMap<>();
|
||||||
|
|
||||||
SVGLength mX;
|
|
||||||
SVGLength mY;
|
|
||||||
SVGLength mW;
|
|
||||||
SVGLength mH;
|
|
||||||
|
|
||||||
private FilterProperties.Units mFilterUnits;
|
private FilterProperties.Units mFilterUnits;
|
||||||
|
|
||||||
@SuppressWarnings({"FieldCanBeLocal", "unused"})
|
|
||||||
private FilterProperties.Units mPrimitiveUnits;
|
private FilterProperties.Units mPrimitiveUnits;
|
||||||
|
private final FilterRegion mFilterRegion;
|
||||||
|
|
||||||
public FilterView(ReactContext reactContext) {
|
public FilterView(ReactContext reactContext) {
|
||||||
super(reactContext);
|
super(reactContext);
|
||||||
|
mFilterRegion = new FilterRegion();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setX(Dynamic x) {
|
public void setX(Dynamic x) {
|
||||||
mX = SVGLength.from(x);
|
mFilterRegion.setX(x);
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setY(Dynamic y) {
|
public void setY(Dynamic y) {
|
||||||
mY = SVGLength.from(y);
|
mFilterRegion.setY(y);
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setWidth(Dynamic width) {
|
public void setWidth(Dynamic width) {
|
||||||
mW = SVGLength.from(width);
|
mFilterRegion.setWidth(width);
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setHeight(Dynamic height) {
|
public void setHeight(Dynamic height) {
|
||||||
mH = SVGLength.from(height);
|
mFilterRegion.setHeight(height);
|
||||||
invalidate();
|
invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,8 +65,7 @@ class FilterView extends DefinitionView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Bitmap applyFilter(
|
public Bitmap applyFilter(Bitmap source, Bitmap background, RectF renderableBounds) {
|
||||||
Bitmap source, Bitmap background, Rect renderableBounds, Rect canvasBounds) {
|
|
||||||
mResultsMap.clear();
|
mResultsMap.clear();
|
||||||
mResultsMap.put("SourceGraphic", source);
|
mResultsMap.put("SourceGraphic", source);
|
||||||
mResultsMap.put("SourceAlpha", FilterUtils.applySourceAlphaFilter(source));
|
mResultsMap.put("SourceAlpha", FilterUtils.applySourceAlphaFilter(source));
|
||||||
@@ -77,12 +73,20 @@ class FilterView extends DefinitionView {
|
|||||||
mResultsMap.put("BackgroundAlpha", FilterUtils.applySourceAlphaFilter(background));
|
mResultsMap.put("BackgroundAlpha", FilterUtils.applySourceAlphaFilter(background));
|
||||||
|
|
||||||
Bitmap res = source;
|
Bitmap res = source;
|
||||||
|
Bitmap resultBitmap = Bitmap.createBitmap(res.getWidth(), res.getHeight(), res.getConfig());
|
||||||
|
Canvas canvas = new Canvas(resultBitmap);
|
||||||
|
Rect cropRect;
|
||||||
|
|
||||||
for (int i = 0; i < getChildCount(); i++) {
|
for (int i = 0; i < getChildCount(); i++) {
|
||||||
View node = getChildAt(i);
|
View node = getChildAt(i);
|
||||||
if (node instanceof FilterPrimitiveView) {
|
if (node instanceof FilterPrimitiveView) {
|
||||||
FilterPrimitiveView currentFilter = (FilterPrimitiveView) node;
|
FilterPrimitiveView currentFilter = (FilterPrimitiveView) node;
|
||||||
res = currentFilter.applyFilter(mResultsMap, res);
|
resultBitmap.eraseColor(Color.TRANSPARENT);
|
||||||
|
cropRect =
|
||||||
|
currentFilter.mFilterRegion.getCropRect(
|
||||||
|
currentFilter, this.mPrimitiveUnits, renderableBounds);
|
||||||
|
canvas.drawBitmap(currentFilter.applyFilter(mResultsMap, res), cropRect, cropRect, null);
|
||||||
|
res = resultBitmap.copy(Bitmap.Config.ARGB_8888, true);
|
||||||
String resultName = currentFilter.getResult();
|
String resultName = currentFilter.getResult();
|
||||||
if (resultName != null) {
|
if (resultName != null) {
|
||||||
mResultsMap.put(resultName, res);
|
mResultsMap.put(resultName, res);
|
||||||
@@ -93,21 +97,8 @@ class FilterView extends DefinitionView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// crop Bitmap to filter coordinates
|
// crop Bitmap to filter coordinates
|
||||||
int x, y, width, height;
|
resultBitmap.eraseColor(Color.TRANSPARENT);
|
||||||
if (this.mFilterUnits == FilterProperties.Units.USER_SPACE_ON_USE) {
|
cropRect = this.mFilterRegion.getCropRect(this, this.mFilterUnits, renderableBounds);
|
||||||
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);
|
canvas.drawBitmap(res, cropRect, cropRect, null);
|
||||||
return resultBitmap;
|
return resultBitmap;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -352,24 +352,30 @@ public abstract class RenderableView extends VirtualView implements ReactHitSlop
|
|||||||
Paint bitmapPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
|
Paint bitmapPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
|
||||||
canvas.saveLayer(null, bitmapPaint);
|
canvas.saveLayer(null, bitmapPaint);
|
||||||
|
|
||||||
Rect canvasBounds = this.getSvgView().getCanvasBounds();
|
Bitmap backgroundBitmap = this.getSvgView().getCurrentBitmap();
|
||||||
|
|
||||||
// draw element to self bitmap
|
// draw element to self bitmap
|
||||||
Bitmap elementBitmap =
|
Bitmap elementBitmap =
|
||||||
Bitmap.createBitmap(
|
Bitmap.createBitmap(canvas.getWidth(), canvas.getHeight(), Bitmap.Config.ARGB_8888);
|
||||||
canvasBounds.width(), canvasBounds.height(), Bitmap.Config.ARGB_8888);
|
|
||||||
Canvas elementCanvas = new Canvas(elementBitmap);
|
Canvas elementCanvas = new Canvas(elementBitmap);
|
||||||
|
elementCanvas.setMatrix(canvas.getMatrix());
|
||||||
|
|
||||||
draw(elementCanvas, paint, opacity);
|
draw(elementCanvas, paint, opacity);
|
||||||
|
|
||||||
|
// get renderableBounds
|
||||||
|
this.initBounds();
|
||||||
|
RectF clientRect = this.getClientRect();
|
||||||
|
if (this instanceof ImageView && clientRect == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// apply filters
|
// apply filters
|
||||||
Bitmap backgroundBitmap = this.getSvgView().getCurrentBitmap();
|
elementBitmap = filter.applyFilter(elementBitmap, backgroundBitmap, clientRect);
|
||||||
elementBitmap =
|
|
||||||
filter.applyFilter(
|
|
||||||
elementBitmap, backgroundBitmap, elementCanvas.getClipBounds(), canvasBounds);
|
|
||||||
|
|
||||||
// draw bitmap to canvas
|
// draw bitmap 1:1 to canvas
|
||||||
|
int saveCount = canvas.save();
|
||||||
|
canvas.setMatrix(null);
|
||||||
canvas.drawBitmap(elementBitmap, 0, 0, bitmapPaint);
|
canvas.drawBitmap(elementBitmap, 0, 0, bitmapPaint);
|
||||||
|
canvas.restoreToCount(saveCount);
|
||||||
} else {
|
} else {
|
||||||
canvas.saveLayer(null, paint);
|
canvas.saveLayer(null, paint);
|
||||||
draw(canvas, paint, opacity);
|
draw(canvas, paint, opacity);
|
||||||
|
|||||||
@@ -296,6 +296,18 @@ public class SvgView extends ReactViewGroup implements ReactCompoundView, ReactC
|
|||||||
return mCanvas.getClipBounds();
|
return mCanvas.getClipBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float getCanvasWidth() {
|
||||||
|
return mCanvas.getWidth();
|
||||||
|
}
|
||||||
|
|
||||||
|
float getCanvasHeight() {
|
||||||
|
return mCanvas.getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
Matrix getCtm() {
|
||||||
|
return mCanvas.getMatrix();
|
||||||
|
}
|
||||||
|
|
||||||
synchronized void drawChildren(final Canvas canvas) {
|
synchronized void drawChildren(final Canvas canvas) {
|
||||||
mRendered = true;
|
mRendered = true;
|
||||||
mCanvas = canvas;
|
mCanvas = canvas;
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ using namespace facebook::react;
|
|||||||
}
|
}
|
||||||
CGRect bounds = definedTemplate.clientRect;
|
CGRect bounds = definedTemplate.clientRect;
|
||||||
self.clientRect = bounds;
|
self.clientRect = bounds;
|
||||||
|
self.pathBounds = definedTemplate.pathBounds;
|
||||||
|
|
||||||
CGAffineTransform current = CGContextGetCTM(context);
|
CGAffineTransform current = CGContextGetCTM(context);
|
||||||
CGAffineTransform svgToClientTransform = CGAffineTransformConcat(current, self.svgView.invInitialCTM);
|
CGAffineTransform svgToClientTransform = CGAffineTransformConcat(current, self.svgView.invInitialCTM);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#import "RNSVGFilterRegion.h"
|
||||||
#import "RNSVGNode.h"
|
#import "RNSVGNode.h"
|
||||||
|
|
||||||
@interface RNSVGFilter : RNSVGNode
|
@interface RNSVGFilter : RNSVGNode
|
||||||
@@ -14,5 +15,8 @@
|
|||||||
renderableBounds:(CGRect)renderableBounds
|
renderableBounds:(CGRect)renderableBounds
|
||||||
canvasBounds:(CGRect)canvasBounds
|
canvasBounds:(CGRect)canvasBounds
|
||||||
ctm:(CGAffineTransform)ctm;
|
ctm:(CGAffineTransform)ctm;
|
||||||
|
- (CGContext *)openContext:(CGSize)size;
|
||||||
|
- (void)endContext:(CGContext *)context;
|
||||||
|
- (CIImage *)getMaskFromRect:(CGContext *)context rect:(CGRect)rect ctm:(CGAffineTransform)ctm;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ using namespace facebook::react;
|
|||||||
[super prepareForRecycle];
|
[super prepareForRecycle];
|
||||||
_x = nil;
|
_x = nil;
|
||||||
_y = nil;
|
_y = nil;
|
||||||
_height = nil;
|
|
||||||
_width = nil;
|
_width = nil;
|
||||||
|
_height = nil;
|
||||||
_filterUnits = kRNSVGUnitsObjectBoundingBox;
|
_filterUnits = kRNSVGUnitsObjectBoundingBox;
|
||||||
_primitiveUnits = kRNSVGUnitsUserSpaceOnUse;
|
_primitiveUnits = kRNSVGUnitsUserSpaceOnUse;
|
||||||
}
|
}
|
||||||
@@ -99,14 +99,32 @@ using namespace facebook::react;
|
|||||||
[resultsMap setObject:backgroundImg forKey:@"BackgroundImage"];
|
[resultsMap setObject:backgroundImg forKey:@"BackgroundImage"];
|
||||||
[resultsMap setObject:applySourceAlphaFilter(backgroundImg) forKey:@"BackgroundAlpha"];
|
[resultsMap setObject:applySourceAlphaFilter(backgroundImg) forKey:@"BackgroundAlpha"];
|
||||||
|
|
||||||
|
// Setup crop filter
|
||||||
|
CGRect cropRect;
|
||||||
|
CIFilter *cropFilter = [CIFilter filterWithName:@"CIBlendWithMask"];
|
||||||
|
[cropFilter setDefaults];
|
||||||
|
[cropFilter setValue:nil forKey:@"inputBackgroundImage"];
|
||||||
|
CGContext *cropContext = [self openContext:canvasBounds.size];
|
||||||
|
CIImage *mask;
|
||||||
|
|
||||||
CIImage *result = img;
|
CIImage *result = img;
|
||||||
RNSVGFilterPrimitive *currentFilter;
|
RNSVGFilterPrimitive *currentFilter;
|
||||||
for (RNSVGNode *node in self.subviews) {
|
for (RNSVGNode *node in self.subviews) {
|
||||||
if ([node isKindOfClass:[RNSVGFilterPrimitive class]]) {
|
if ([node isKindOfClass:[RNSVGFilterPrimitive class]]) {
|
||||||
currentFilter = (RNSVGFilterPrimitive *)node;
|
currentFilter = (RNSVGFilterPrimitive *)node;
|
||||||
CGImageRef cgResult = [[RNSVGRenderUtils sharedCIContext] createCGImage:[currentFilter applyFilter:resultsMap
|
cropRect = [[RNSVGFilterRegion regionWithX:currentFilter.x
|
||||||
previousFilterResult:result
|
y:currentFilter.y
|
||||||
ctm:ctm]
|
width:currentFilter.width
|
||||||
|
height:currentFilter.height] getCropRect:currentFilter
|
||||||
|
units:self.primitiveUnits
|
||||||
|
renderableBounds:renderableBounds];
|
||||||
|
mask = [self getMaskFromRect:cropContext rect:cropRect ctm:ctm];
|
||||||
|
[cropFilter setValue:[currentFilter applyFilter:resultsMap previousFilterResult:result ctm:ctm]
|
||||||
|
forKey:@"inputImage"];
|
||||||
|
[cropFilter setValue:mask forKey:@"inputMaskImage"];
|
||||||
|
CGContextClearRect(cropContext, canvasBounds);
|
||||||
|
|
||||||
|
CGImageRef cgResult = [[RNSVGRenderUtils sharedCIContext] createCGImage:[cropFilter valueForKey:@"outputImage"]
|
||||||
fromRect:[result extent]];
|
fromRect:[result extent]];
|
||||||
result = [CIImage imageWithCGImage:cgResult];
|
result = [CIImage imageWithCGImage:cgResult];
|
||||||
CGImageRelease(cgResult);
|
CGImageRelease(cgResult);
|
||||||
@@ -118,8 +136,48 @@ using namespace facebook::react;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
cropRect = [[RNSVGFilterRegion regionWithX:self.x y:self.y width:self.width
|
||||||
// TODO: Crop element to filter's x, y, width, height
|
height:self.height] getCropRect:self
|
||||||
|
units:self.filterUnits
|
||||||
|
renderableBounds:renderableBounds];
|
||||||
|
mask = [self getMaskFromRect:cropContext rect:cropRect ctm:ctm];
|
||||||
|
[cropFilter setValue:result forKey:@"inputImage"];
|
||||||
|
[cropFilter setValue:mask forKey:@"inputMaskImage"];
|
||||||
|
[self endContext:cropContext];
|
||||||
|
return [cropFilter valueForKey:@"outputImage"];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (CGContext *)openContext:(CGSize)size
|
||||||
|
{
|
||||||
|
UIGraphicsBeginImageContextWithOptions(size, NO, 1.0);
|
||||||
|
CGContextRef cropContext = UIGraphicsGetCurrentContext();
|
||||||
|
#if TARGET_OS_OSX
|
||||||
|
CGFloat scale = [RNSVGRenderUtils getScreenScale];
|
||||||
|
CGContextScaleCTM(cropContext, scale, scale);
|
||||||
|
#else
|
||||||
|
CGContextTranslateCTM(cropContext, 0, size.height);
|
||||||
|
CGContextScaleCTM(cropContext, 1, -1);
|
||||||
|
#endif
|
||||||
|
return cropContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)endContext:(CGContext *)context
|
||||||
|
{
|
||||||
|
UIGraphicsEndImageContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
- (CIImage *)getMaskFromRect:(CGContext *)context rect:(CGRect)rect ctm:(CGAffineTransform)ctm
|
||||||
|
{
|
||||||
|
CGPathRef path = CGPathCreateWithRect(rect, nil);
|
||||||
|
path = CGPathCreateCopyByTransformingPath(path, &ctm);
|
||||||
|
|
||||||
|
CGContextSetRGBFillColor(context, 255, 255, 255, 255);
|
||||||
|
CGContextAddPath(context, path);
|
||||||
|
CGContextFillPath(context);
|
||||||
|
|
||||||
|
CGImageRef maskImage = CGBitmapContextCreateImage(context);
|
||||||
|
|
||||||
|
return [CIImage imageWithCGImage:maskImage];
|
||||||
}
|
}
|
||||||
|
|
||||||
static CIFilter *sourceAlphaFilter()
|
static CIFilter *sourceAlphaFilter()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
#import "RNSVGFilterRegion.h"
|
||||||
#import "RNSVGNode.h"
|
#import "RNSVGNode.h"
|
||||||
|
|
||||||
@interface RNSVGFilterPrimitive : RNSVGNode
|
@interface RNSVGFilterPrimitive : RNSVGNode
|
||||||
@@ -12,6 +13,5 @@
|
|||||||
- (CIImage *)applyFilter:(NSMutableDictionary<NSString *, CIImage *> *)results
|
- (CIImage *)applyFilter:(NSMutableDictionary<NSString *, CIImage *> *)results
|
||||||
previousFilterResult:(CIImage *)previous
|
previousFilterResult:(CIImage *)previous
|
||||||
ctm:(CGAffineTransform)ctm;
|
ctm:(CGAffineTransform)ctm;
|
||||||
- (CIImage *)cropResult:(CIImage *)result;
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
[super prepareForRecycle];
|
[super prepareForRecycle];
|
||||||
_x = nil;
|
_x = nil;
|
||||||
_y = nil;
|
_y = nil;
|
||||||
_height = nil;
|
|
||||||
_width = nil;
|
_width = nil;
|
||||||
|
_height = nil;
|
||||||
_result = nil;
|
_result = nil;
|
||||||
}
|
}
|
||||||
#endif // RCT_NEW_ARCH_ENABLED
|
#endif // RCT_NEW_ARCH_ENABLED
|
||||||
@@ -32,6 +32,12 @@
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)invalidate
|
||||||
|
{
|
||||||
|
self.dirty = false;
|
||||||
|
[super invalidate];
|
||||||
|
}
|
||||||
|
|
||||||
- (void)setX:(RNSVGLength *)x
|
- (void)setX:(RNSVGLength *)x
|
||||||
{
|
{
|
||||||
if ([x isEqualTo:_x]) {
|
if ([x isEqualTo:_x]) {
|
||||||
@@ -42,12 +48,6 @@
|
|||||||
[self invalidate];
|
[self invalidate];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)invalidate
|
|
||||||
{
|
|
||||||
self.dirty = false;
|
|
||||||
[super invalidate];
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)setY:(RNSVGLength *)y
|
- (void)setY:(RNSVGLength *)y
|
||||||
{
|
{
|
||||||
if ([y isEqualTo:_y]) {
|
if ([y isEqualTo:_y]) {
|
||||||
@@ -100,18 +100,4 @@
|
|||||||
return [self applyFilter:results previousFilterResult:previous];
|
return [self applyFilter:results previousFilterResult: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
|
@end
|
||||||
|
|||||||
19
apple/Filters/RNSVGFilterRegion.h
Normal file
19
apple/Filters/RNSVGFilterRegion.h
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#import "RNSVGNode.h"
|
||||||
|
#import "RNSVGUnits.h"
|
||||||
|
|
||||||
|
#ifndef RNSVGFilterRegion_h
|
||||||
|
#define RNSVGFilterRegion_h
|
||||||
|
|
||||||
|
@interface RNSVGFilterRegion : NSObject
|
||||||
|
|
||||||
|
@property (nonatomic, strong) RNSVGLength *x;
|
||||||
|
@property (nonatomic, strong) RNSVGLength *y;
|
||||||
|
@property (nonatomic, strong) RNSVGLength *width;
|
||||||
|
@property (nonatomic, strong) RNSVGLength *height;
|
||||||
|
|
||||||
|
+ (instancetype)regionWithX:(RNSVGLength *)x y:(RNSVGLength *)y width:(RNSVGLength *)width height:(RNSVGLength *)height;
|
||||||
|
- (CGRect)getCropRect:(RNSVGNode *)node units:(RNSVGUnits)units renderableBounds:(CGRect)renderableBounds;
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
#endif /* RNSVGFilterRegion_h */
|
||||||
65
apple/Filters/RNSVGFilterRegion.mm
Normal file
65
apple/Filters/RNSVGFilterRegion.mm
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
#import <RNSVGFilterRegion.h>
|
||||||
|
|
||||||
|
@implementation RNSVGFilterRegion
|
||||||
|
|
||||||
|
- (instancetype)init
|
||||||
|
{
|
||||||
|
self = [super init];
|
||||||
|
if (self) {
|
||||||
|
_x = [RNSVGLength lengthWithNumber:0];
|
||||||
|
_y = [RNSVGLength lengthWithNumber:0];
|
||||||
|
_width = [RNSVGLength lengthWithNumber:0];
|
||||||
|
_height = [RNSVGLength lengthWithNumber:0];
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (instancetype)regionWithX:(RNSVGLength *)x y:(RNSVGLength *)y width:(RNSVGLength *)width height:(RNSVGLength *)height
|
||||||
|
{
|
||||||
|
RNSVGFilterRegion *filterRegion = [[self alloc] init];
|
||||||
|
filterRegion.x = x;
|
||||||
|
filterRegion.y = y;
|
||||||
|
filterRegion.width = width;
|
||||||
|
filterRegion.height = height;
|
||||||
|
return filterRegion;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setX:(RNSVGLength *)x
|
||||||
|
{
|
||||||
|
_x = x;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setY:(RNSVGLength *)y
|
||||||
|
{
|
||||||
|
_y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setWidth:(RNSVGLength *)width
|
||||||
|
{
|
||||||
|
_width = width;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)setHeight:(RNSVGLength *)height
|
||||||
|
{
|
||||||
|
_height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (CGRect)getCropRect:(RNSVGNode *)node units:(RNSVGUnits)units renderableBounds:(CGRect)renderableBounds
|
||||||
|
{
|
||||||
|
CGFloat x, y, width, height;
|
||||||
|
if (units == kRNSVGUnitsObjectBoundingBox) {
|
||||||
|
x = [node relativeOnFraction:self.x relative:renderableBounds.size.width];
|
||||||
|
y = [node relativeOnFraction:self.y relative:renderableBounds.size.height];
|
||||||
|
width = [node relativeOnFraction:self.width relative:renderableBounds.size.width];
|
||||||
|
height = [node relativeOnFraction:self.height relative:renderableBounds.size.height];
|
||||||
|
return CGRectMake(renderableBounds.origin.x + x, renderableBounds.origin.y + y, width, height);
|
||||||
|
} else { // kRNSVGUnitsUserSpaceOnUse
|
||||||
|
x = [node relativeOnWidth:self.x];
|
||||||
|
y = [node relativeOnHeight:self.y];
|
||||||
|
width = [node relativeOnWidth:self.width];
|
||||||
|
height = [node relativeOnHeight:self.height];
|
||||||
|
return CGRectMake(x, y, width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -28,7 +28,7 @@ class ReferenceExample extends Component {
|
|||||||
|
|
||||||
const icon = (
|
const icon = (
|
||||||
<Svg height="30" width="30" viewBox="0 0 20 20">
|
<Svg height="30" width="30" viewBox="0 0 20 20">
|
||||||
<Filter id="iconOffset">
|
<Filter id="iconOffset" width="1.5" height="1.5">
|
||||||
<FeOffset dx="5" dy="5" />
|
<FeOffset dx="5" dy="5" />
|
||||||
</Filter>
|
</Filter>
|
||||||
<Rect x="0" y="0" width="15" height="15" fill="red" />
|
<Rect x="0" y="0" width="15" height="15" fill="red" />
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { NativeMethods } from 'react-native';
|
|||||||
import RNSVGFilter from '../../fabric/FilterNativeComponent';
|
import RNSVGFilter from '../../fabric/FilterNativeComponent';
|
||||||
import { NumberProp, Units } from '../../lib/extract/types';
|
import { NumberProp, Units } from '../../lib/extract/types';
|
||||||
import Shape from '../Shape';
|
import Shape from '../Shape';
|
||||||
import warnOnce from 'warn-once';
|
|
||||||
|
|
||||||
export interface FilterProps {
|
export interface FilterProps {
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
@@ -13,7 +12,6 @@ export interface FilterProps {
|
|||||||
width?: NumberProp;
|
width?: NumberProp;
|
||||||
height?: NumberProp;
|
height?: NumberProp;
|
||||||
filterUnits?: Units;
|
filterUnits?: Units;
|
||||||
// TODO: Implement
|
|
||||||
primitiveUnits?: Units;
|
primitiveUnits?: Units;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,22 +24,20 @@ export default class Filter extends Shape<FilterProps> {
|
|||||||
width: '120%',
|
width: '120%',
|
||||||
height: '120%',
|
height: '120%',
|
||||||
filterUnits: 'objectBoundingBox',
|
filterUnits: 'objectBoundingBox',
|
||||||
// primitiveUnits: 'userSpaceOnUse',
|
primitiveUnits: 'userSpaceOnUse',
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { id, x, y, width, height, filterUnits, primitiveUnits } = this.props;
|
const { id, x, y, width, height, filterUnits, primitiveUnits } = this.props;
|
||||||
warnOnce(
|
|
||||||
!!primitiveUnits,
|
|
||||||
"WARNING: Filter's `primitiveUnits` prop is not supported yet"
|
|
||||||
);
|
|
||||||
const filterProps = {
|
const filterProps = {
|
||||||
name: id,
|
name: id,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
filterUnits: filterUnits || 'objectBoundingBox',
|
filterUnits,
|
||||||
|
primitiveUnits,
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<RNSVGFilter
|
<RNSVGFilter
|
||||||
|
|||||||
Reference in New Issue
Block a user