feat: FeFlood (#2487)

# Summary

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

## Test Plan

Example app → Filters → `FeFlood`

## Compatibility

| OS      | Implemented |
| ------- | :---------: |
| iOS     |          |
| macOS   |     _*_      |
| Android |          |
| Web     |          |

_* `canvasWidth/canvasHeight` is incorrect on macOS, so there might be
some problems_
This commit is contained in:
Jakub Grzywacz
2024-10-15 09:35:13 +02:00
committed by GitHub
parent 8fed77476b
commit ba54b15799
17 changed files with 581 additions and 6 deletions

View File

@@ -1301,6 +1301,7 @@ Filter effects are a way of processing an elements rendering before it is dis
The following filters have been implemented:
- FeColorMatrix
- FeFlood
- FeGaussianBlur
- FeMerge
- FeOffset
@@ -1314,7 +1315,6 @@ Not supported yet:
- FeDiffuseLighting
- FeDisplacementMap
- FeDropShadow
- FeFlood
- FeFuncA
- FeFuncB
- FeFuncG

View File

@@ -0,0 +1,130 @@
package com.horcrux.svg;
import android.annotation.SuppressLint;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import com.facebook.react.bridge.ColorPropConverter;
import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.JavaOnlyArray;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableType;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
@SuppressLint("ViewConstructor")
class FeFloodView extends FilterPrimitiveView {
private static final Pattern regex = Pattern.compile("[0-9.-]+");
public @Nullable ReadableArray floodColor;
public float floodOpacity = 1;
public FeFloodView(ReactContext reactContext) {
super(reactContext);
}
public void setFloodColor(@Nullable Dynamic color) {
if (color == null || color.isNull()) {
floodColor = null;
invalidate();
return;
}
ReadableType strokeType = color.getType();
if (strokeType.equals(ReadableType.Map)) {
ReadableMap colorMap = color.asMap();
setFloodColor(colorMap);
return;
}
// This code will probably never be reached with current changes
ReadableType type = color.getType();
if (type.equals(ReadableType.Number)) {
floodColor = JavaOnlyArray.of(0, color.asInt());
} else if (type.equals(ReadableType.Array)) {
floodColor = color.asArray();
} else {
JavaOnlyArray arr = new JavaOnlyArray();
arr.pushInt(0);
Matcher m = regex.matcher(color.asString());
int i = 0;
while (m.find()) {
double parsed = Double.parseDouble(m.group());
arr.pushDouble(i++ < 3 ? parsed / 255 : parsed);
}
floodColor = arr;
}
invalidate();
}
public void setFloodColor(@Nullable ReadableMap color) {
if (color == null) {
this.floodColor = null;
invalidate();
return;
}
int type = color.getInt("type");
if (type == 0) {
ReadableType payloadType = color.getType("payload");
if (payloadType.equals(ReadableType.Number)) {
this.floodColor = JavaOnlyArray.of(0, color.getInt("payload"));
} else if (payloadType.equals(ReadableType.Map)) {
this.floodColor = JavaOnlyArray.of(0, color.getMap("payload"));
}
} else if (type == 1) {
this.floodColor = JavaOnlyArray.of(1, color.getString("brushRef"));
} else {
this.floodColor = JavaOnlyArray.of(type);
}
invalidate();
}
public void setFloodOpacity(float opacity) {
this.floodOpacity = opacity;
invalidate();
}
@Override
public Bitmap applyFilter(HashMap<String, Bitmap> resultsMap, Bitmap prevResult) {
Bitmap floodBitmap =
Bitmap.createBitmap(prevResult.getWidth(), prevResult.getHeight(), Bitmap.Config.ARGB_8888);
Canvas floodCanvas = new Canvas(floodBitmap);
Paint paint = new Paint();
paint.setFlags(Paint.ANTI_ALIAS_FLAG | Paint.SUBPIXEL_TEXT_FLAG);
paint.setStyle(Paint.Style.FILL);
this.setupPaint(paint, this.floodOpacity, this.floodColor);
floodCanvas.drawPaint(paint);
return floodBitmap;
}
private void setupPaint(Paint paint, float opacity, @Nullable ReadableArray colors) {
int colorType = colors.getInt(0);
switch (colorType) {
case 0:
if (colors.size() == 2) {
int color;
if (colors.getType(1) == ReadableType.Map) {
color = ColorPropConverter.getColor(colors.getMap(1), getContext());
} else {
color = colors.getInt(1);
}
int alpha = color >>> 24;
int combined = Math.round((float) alpha * opacity);
paint.setColor(combined << 24 | (color & 0x00ffffff));
} else {
// solid color
paint.setARGB(
(int) (colors.size() > 4 ? colors.getDouble(4) * opacity * 255 : opacity * 255),
(int) (colors.getDouble(1) * 255),
(int) (colors.getDouble(2) * 255),
(int) (colors.getDouble(3) * 255));
}
break;
// TODO: handle currentColor
}
}
}

View File

@@ -105,6 +105,8 @@ 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.RNSVGFeFloodManagerDelegate;
import com.facebook.react.viewmanagers.RNSVGFeFloodManagerInterface;
import com.facebook.react.viewmanagers.RNSVGFeGaussianBlurManagerDelegate;
import com.facebook.react.viewmanagers.RNSVGFeGaussianBlurManagerInterface;
import com.facebook.react.viewmanagers.RNSVGFeMergeManagerDelegate;
@@ -590,6 +592,7 @@ class VirtualViewManager<V extends VirtualView> extends ViewGroupManager<Virtual
RNSVGMask,
RNSVGFilter,
RNSVGFeColorMatrix,
RNSVGFeFlood,
RNSVGFeGaussianBlur,
RNSVGFeMerge,
RNSVGFeOffset,
@@ -641,6 +644,8 @@ class VirtualViewManager<V extends VirtualView> extends ViewGroupManager<Virtual
return new FilterView(reactContext);
case RNSVGFeColorMatrix:
return new FeColorMatrixView(reactContext);
case RNSVGFeFlood:
return new FeFloodView(reactContext);
case RNSVGFeGaussianBlur:
return new FeGaussianBlurView(reactContext);
case RNSVGFeMerge:
@@ -1605,6 +1610,30 @@ class RenderableViewManager<T extends RenderableView> extends VirtualViewManager
}
}
static class FeFloodManager extends FilterPrimitiveManager<FeFloodView>
implements RNSVGFeFloodManagerInterface<FeFloodView> {
FeFloodManager() {
super(SVGClass.RNSVGFeFlood);
mDelegate = new RNSVGFeFloodManagerDelegate(this);
}
public static final String REACT_CLASS = "RNSVGFeFlood";
@ReactProp(name = "floodColor")
public void setFloodColor(FeFloodView node, @Nullable Dynamic strokeColors) {
node.setFloodColor(strokeColors);
}
public void setFloodColor(FeFloodView view, @Nullable ReadableMap value) {
view.setFloodColor(value);
}
@ReactProp(name = "floodOpacity", defaultFloat = 1f)
public void setFloodOpacity(FeFloodView node, float strokeOpacity) {
node.setFloodOpacity(strokeOpacity);
}
}
static class FeGaussianBlurManager extends FilterPrimitiveManager<FeGaussianBlurView>
implements RNSVGFeGaussianBlurManagerInterface<FeGaussianBlurView> {
FeGaussianBlurManager() {

View File

@@ -223,6 +223,15 @@ public class SvgPackage extends TurboReactPackage implements ViewManagerOnDemand
return new FeColorMatrixManager();
}
}));
specs.put(
FeFloodManager.REACT_CLASS,
ModuleSpec.viewManagerSpec(
new Provider<NativeModule>() {
@Override
public NativeModule get() {
return new FeFloodManager();
}
}));
specs.put(
FeGaussianBlurManager.REACT_CLASS,
ModuleSpec.viewManagerSpec(

View File

@@ -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 RNSVGFeFloodManagerDelegate<T extends View, U extends BaseViewManagerInterface<T> & RNSVGFeFloodManagerInterface<T>> extends BaseViewManagerDelegate<T, U> {
public RNSVGFeFloodManagerDelegate(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 "floodColor":
mViewManager.setFloodColor(view, new DynamicFromObject(value));
break;
case "floodOpacity":
mViewManager.setFloodOpacity(view, value == null ? 1f : ((Double) value).floatValue());
break;
default:
super.setProperty(view, propName, value);
}
}
}

View File

@@ -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 RNSVGFeFloodManagerInterface<T extends View> {
void setX(T view, Dynamic value);
void setY(T view, Dynamic value);
void setWidth(T view, Dynamic value);
void setHeight(T view, Dynamic value);
void setResult(T view, @Nullable String value);
void setFloodColor(T view, Dynamic value);
void setFloodOpacity(T view, float value);
}

View File

@@ -0,0 +1,9 @@
#import "RNSVGBrush.h"
#import "RNSVGFilterPrimitive.h"
@interface RNSVGFeFlood : RNSVGFilterPrimitive
@property (nonatomic, strong) RNSVGBrush *floodColor;
@property (nonatomic, assign) CGFloat floodOpacity;
@end

View File

@@ -0,0 +1,91 @@
#import "RNSVGFeFlood.h"
#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTConversions.h>
#import <React/RCTFabricComponentsPlugins.h>
#import <react/renderer/components/rnsvg/ComponentDescriptors.h>
#import <react/renderer/components/view/conversions.h>
#import "RNSVGConvert.h"
#import "RNSVGFabricConversions.h"
#endif // RCT_NEW_ARCH_ENABLED
@implementation RNSVGFeFlood
#ifdef RCT_NEW_ARCH_ENABLED
using namespace facebook::react;
// Needed because of this: https://github.com/facebook/react-native/pull/37274
+ (void)load
{
[super load];
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared<const RNSVGFeFloodProps>();
_props = defaultProps;
}
return self;
}
#pragma mark - RCTComponentViewProtocol
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<RNSVGFeFloodComponentDescriptor>();
}
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
{
const auto &newProps = static_cast<const RNSVGFeFloodProps &>(*props);
id floodColor = RNSVGConvertFollyDynamicToId(newProps.floodColor);
if (floodColor != nil) {
self.floodColor = [RCTConvert RNSVGBrush:floodColor];
}
self.floodOpacity = newProps.floodOpacity;
setCommonFilterProps(newProps, self);
_props = std::static_pointer_cast<RNSVGFeFloodProps const>(props);
}
- (void)prepareForRecycle
{
[super prepareForRecycle];
_floodColor = nil;
_floodOpacity = 1;
}
#endif // RCT_NEW_ARCH_ENABLED
- (void)setFloodColor:(RNSVGBrush *)floodColor
{
if (floodColor == _floodColor) {
return;
}
_floodColor = floodColor;
[self invalidate];
}
- (void)setFloodOpacity:(CGFloat)floodOpacity
{
if (floodOpacity == _floodOpacity) {
return;
}
_floodOpacity = floodOpacity;
[self invalidate];
}
- (CIImage *)applyFilter:(NSMutableDictionary<NSString *, CIImage *> *)results previousFilterResult:(CIImage *)previous
{
return [CIImage imageWithColor:[CIColor colorWithCGColor:[self.floodColor getColorWithOpacity:self.floodOpacity]]];
}
#ifdef RCT_NEW_ARCH_ENABLED
Class<RCTComponentViewProtocol> RNSVGFeFloodCls(void)
{
return RNSVGFeFlood.class;
}
#endif // RCT_NEW_ARCH_ENABLED
@end

View File

@@ -0,0 +1,5 @@
#import "RNSVGFilterPrimitiveManager.h"
@interface RNSVGFeFloodManager : RNSVGFilterPrimitiveManager
@end

View File

@@ -0,0 +1,16 @@
#import "RNSVGFeFloodManager.h"
#import "RNSVGFeFlood.h"
@implementation RNSVGFeFloodManager
RCT_EXPORT_MODULE()
- (RNSVGFeFlood *)node
{
return [RNSVGFeFlood new];
}
RCT_EXPORT_VIEW_PROPERTY(floodColor, RNSVGBrush)
RCT_EXPORT_VIEW_PROPERTY(floodOpacity, CGFloat)
@end

View File

@@ -0,0 +1,141 @@
import React, {Component} from 'react';
import {
Circle,
FeFlood,
FeMerge,
FeMergeNode,
Filter,
G,
Line,
Rect,
Svg,
Use,
} from 'react-native-svg';
class BasicFlood extends Component {
static title = 'Basic MDN example with Use';
render() {
return (
<Svg width="200" height="200">
<Filter id="floodFilter" filterUnits="userSpaceOnUse">
<FeFlood
x="50"
y="50"
width="100"
height="100"
floodColor="green"
floodOpacity="0.5"
/>
</Filter>
<Use href="url(#useless)" filter="url(#floodFilter)" />
</Svg>
);
}
}
class TestCase1 extends Component {
static title = 'Custom Test Case 1';
render() {
return (
<Svg width="200" height="200" viewBox="0 0 200 200">
<Filter
id="floodFilter"
// x="50%"
filterUnits="userSpaceOnUse"
primitiveUnits="userSpaceOnUse">
<FeFlood
// y="-10%"
x="10%"
// width="50%"
// height="50%"
flood-color="red"
flood-opacity="0.5"
floodColor="yellow"
floodOpacity="0.5"
/>
</Filter>
<Rect x="0" y="0" width="100" height="100" fill="blue" />
<Circle cx="50" cy="50" r="40" filter="url(#floodFilter)" />
</Svg>
);
}
}
class TestCase2 extends Component {
static title = 'Custom Test Case 2';
render() {
return (
<Svg width="200" height="400">
<Filter
id="flood"
x="0"
y="0"
width="100%"
height="100%"
primitiveUnits="objectBoundingBox">
<FeFlood
x="25%"
y="25%"
width="50%"
height="50%"
floodColor="green"
floodOpacity="0.75"
/>
</Filter>
<Filter id="merge" primitiveUnits="objectBoundingBox">
<FeMerge x="25%" y="25%" width="50%" height="50%">
<FeMergeNode in="SourceGraphic" />
</FeMerge>
</Filter>
<G fill="none" stroke="blue" strokeWidth="4">
<Rect width="200" height="200" />
<Line x2="200" y2="200" />
<Line x1="200" y2="200" />
</G>
<Circle fill="green" filter="url(#flood)" cx="100" cy="100" r="90" />
<Rect x="55" y="55" width="90" height="90" />
<G transform="translate(0 200)">
<G fill="none" stroke="blue" strokeWidth="4">
<Rect width="200" height="200" />
<Line x2="200" y2="200" />
<Line x1="200" y2="200" />
</G>
<Circle
fill="green"
fillOpacity="0.5"
filter="url(#merge)"
cx="100"
cy="100"
r="90"
/>
</G>
<Rect x="55" y="255" width="90" height="90" fillOpacity=".7" />
</Svg>
);
}
}
const icon = (
<Svg height="30" width="30" viewBox="0 0 140 140">
<Filter
id="floodFilterIcon"
x="50%"
filterUnits="userSpaceOnUse"
primitiveUnits="userSpaceOnUse">
<FeFlood
y="-10%"
x="10%"
width="50%"
height="50%"
floodColor="yellow"
floodOpacity="0.5"
/>
</Filter>
<Rect x="0" y="0" width="100" height="100" fill="blue" />
<Circle cx="50" cy="50" r="40" filter="url(#floodFilterIcon)" />
</Svg>
);
const samples = [BasicFlood, TestCase1, TestCase2];
export {icon, samples};

View File

@@ -1,10 +1,12 @@
import * as FeColorMatrix from './FeColorMatrix';
import * as FeFlood from './FeFlood';
import * as FeGaussianBlur from './FeGaussianBlur';
import * as FeMerge from './FeMerge';
import * as FeOffset from './FeOffset';
import * as ReanimatedFeColorMatrix from './ReanimatedFeColorMatrix';
export {
FeColorMatrix,
FeFlood,
FeGaussianBlur,
FeMerge,
FeOffset,

View File

@@ -18,6 +18,7 @@ module.exports = {
'RNSVGClipPathComponentDescriptor',
'RNSVGDefsComponentDescriptor',
'RNSVGFeColorMatrixComponentDescriptor',
'RNSVGFeFloodComponentDescriptor',
'RNSVGFeGaussianBlurComponentDescriptor',
'RNSVGFeMergeComponentDescriptor',
'RNSVGFeOffsetComponentDescriptor',

View File

@@ -1,6 +1,7 @@
import { ColorValue } from 'react-native';
import { ColorValue, NativeMethods } from 'react-native';
import RNSVGFeFlood from '../../fabric/FeFloodNativeComponent';
import extractFeFlood, { extractFilter } from '../../lib/extract/extractFilter';
import { NumberProp } from '../../lib/extract/types';
import { warnUnimplementedFilter } from '../../lib/util';
import FilterPrimitive from './FilterPrimitive';
export interface FeFloodProps {
@@ -12,12 +13,19 @@ export interface FeFloodProps {
export default class FeFlood extends FilterPrimitive<FeFloodProps> {
static displayName = 'FeFlood';
static defaultProps = {
static defaultProps: React.ComponentProps<typeof FeFlood> = {
...this.defaultPrimitiveProps,
floodColor: 'black',
floodOpacity: 1,
};
render() {
warnUnimplementedFilter();
return null;
return (
<RNSVGFeFlood
ref={(ref) => this.refMethod(ref as (FeFlood & NativeMethods) | null)}
{...extractFilter(this.props)}
{...extractFeFlood(this.props)}
/>
);
}
}

View File

@@ -0,0 +1,31 @@
import type { ColorValue } from 'react-native';
import type {
Float,
Int32,
WithDefault,
} from 'react-native/Libraries/Types/CodegenTypes';
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
import type { NumberProp } from '../lib/extract/types';
import type { UnsafeMixed } from './codegenUtils';
import type { ViewProps } from './utils';
interface FilterPrimitiveCommonProps {
x?: UnsafeMixed<NumberProp>;
y?: UnsafeMixed<NumberProp>;
width?: UnsafeMixed<NumberProp>;
height?: UnsafeMixed<NumberProp>;
result?: string;
}
type ColorStruct = Readonly<{
type?: WithDefault<Int32, -1>;
payload?: ColorValue;
brushRef?: string;
}>;
export interface NativeProps extends ViewProps, FilterPrimitiveCommonProps {
floodColor?: UnsafeMixed<ColorValue | ColorStruct>;
floodOpacity?: WithDefault<Float, 1.0>;
}
export default codegenNativeComponent<NativeProps>('RNSVGFeFlood');

View File

@@ -22,6 +22,7 @@ import RNSVGTSpan from './TSpanNativeComponent';
import RNSVGUse from './UseNativeComponent';
import RNSVGFilter from './FilterNativeComponent';
import RNSVGFeColorMatrix from './FeColorMatrixNativeComponent';
import RNSVGFeFlood from './FeFloodNativeComponent';
import RNSVGFeGaussianBlur from './FeGaussianBlurNativeComponent';
import RNSVGFeMerge from './FeMergeNativeComponent';
import RNSVGFeOffset from './FeOffsetNativeComponent';
@@ -51,6 +52,7 @@ export {
RNSVGUse,
RNSVGFilter,
RNSVGFeColorMatrix,
RNSVGFeFlood,
RNSVGFeGaussianBlur,
RNSVGFeMerge,
RNSVGFeOffset,

View File

@@ -1,10 +1,15 @@
import React from 'react';
import { ColorValue, processColor } from 'react-native';
import { FeColorMatrixProps as FeColorMatrixComponentProps } from '../../elements/filters/FeColorMatrix';
import { FeFloodProps as FeFloodComponentProps } from '../../elements/filters/FeFlood';
import { FeGaussianBlurProps as FeGaussianBlurComponentProps } from '../../elements/filters/FeGaussianBlur';
import { FeMergeProps as FeMergeComponentProps } from '../../elements/filters/FeMerge';
import { NativeProps as FeColorMatrixNativeProps } from '../../fabric/FeColorMatrixNativeComponent';
import { NativeProps as FeFloodNativeProps } from '../../fabric/FeFloodNativeComponent';
import { NativeProps as FeGaussianBlurNativeProps } from '../../fabric/FeGaussianBlurNativeComponent';
import { NativeProps as FeMergeNativeProps } from '../../fabric/FeMergeNativeComponent';
import extractBrush from './extractBrush';
import extractOpacity from './extractOpacity';
import { NumberProp } from './types';
const spaceReg = /\s+/;
@@ -67,6 +72,28 @@ export const extractFeColorMatrix = (
return extracted;
};
const defaultFill = { type: 0, payload: processColor('black') as ColorValue };
export default function extractFeFlood(
props: FeFloodComponentProps
): FeFloodNativeProps {
const extracted: FeFloodNativeProps = {};
const { floodColor, floodOpacity } = props;
if (floodColor != null) {
extracted.floodColor =
!floodColor && typeof floodColor !== 'number'
? defaultFill
: (extractBrush(floodColor) as unknown as string);
} else {
// we want the default value of fill to be black to match the spec
extracted.floodColor = defaultFill;
}
if (floodOpacity != null) {
extracted.floodOpacity = extractOpacity(floodOpacity);
}
return extracted;
}
export const extractFeGaussianBlur = (
props: FeGaussianBlurComponentProps
): FeGaussianBlurNativeProps => {