From 6a5242f00b38441dc29d947ffde852315e90d4fd Mon Sep 17 00:00:00 2001 From: Wojciech Lewicki Date: Tue, 25 Oct 2022 15:55:17 +0200 Subject: [PATCH] fix: proper transform prop handling (#1895) PR aligning handling of transform prop between web and native and adding proper handling of transform prop on web. origin prop is now treated as transform-origin since it seems like the current behavior of this prop on native platforms. transform prop cannot be an object with transform properties since it does not provide order of transformations which would lead to undefined behavior so this option has been removed. RN style of transform prop (array of rotation objects) can now be used font-size on web seems to be 16px by default and it is 12 in library - maybe we should align it somehow removed maskTransform prop since it does not exist in SVG standard and did nothing on native side fixed typo in Fabric Pattern updateProps method --- Example/ios/Podfile.lock | 39 +++- Example/src/App.tsx | 1 + Example/src/examples.tsx | 2 + Example/src/examples/Transforms.tsx | 134 ++++++++++++ FabricExample/ios/Podfile.lock | 4 +- FabricExample/src/App.tsx | 1 + FabricExample/src/examples.tsx | 2 + FabricExample/src/examples/Transforms.tsx | 134 ++++++++++++ .../main/java/com/horcrux/svg/MaskView.java | 31 --- .../java/com/horcrux/svg/RenderableView.java | 35 ++-- .../horcrux/svg/RenderableViewManager.java | 5 - .../RNSVGMaskManagerDelegate.java | 3 - .../RNSVGMaskManagerInterface.java | 1 - apple/Elements/RNSVGMask.h | 1 - apple/Elements/RNSVGMask.mm | 16 -- apple/Elements/RNSVGPattern.mm | 3 +- apple/ViewManagers/RNSVGMaskManager.mm | 1 - src/ReactNativeSVG.web.ts | 196 ++++++++---------- src/elements/LinearGradient.tsx | 8 +- src/elements/Mask.tsx | 22 +- src/elements/Pattern.tsx | 12 +- src/elements/RadialGradient.tsx | 8 +- src/elements/Shape.tsx | 7 +- src/elements/TSpan.tsx | 3 +- src/elements/Text.tsx | 3 +- src/elements/TextPath.tsx | 3 +- src/fabric/MaskNativeComponent.ts | 1 - src/index.js.flow | 1 - src/lib/extract/extractGradient.ts | 4 +- src/lib/extract/extractProps.ts | 7 +- src/lib/extract/extractTransform.ts | 75 +++++-- src/lib/extract/types.ts | 42 ++-- 32 files changed, 523 insertions(+), 282 deletions(-) create mode 100644 Example/src/examples/Transforms.tsx create mode 100644 FabricExample/src/examples/Transforms.tsx diff --git a/Example/ios/Podfile.lock b/Example/ios/Podfile.lock index 83c07711..9d7c06d1 100644 --- a/Example/ios/Podfile.lock +++ b/Example/ios/Podfile.lock @@ -368,7 +368,34 @@ PODS: - React-jsi (= 0.70.0) - React-logger (= 0.70.0) - React-perflogger (= 0.70.0) - - RNSVG (13.2.0): + - RNReanimated (2.10.0): + - DoubleConversion + - FBLazyVector + - FBReactNativeSpec + - glog + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-Core/DevSupport + - React-Core/RCTWebSocket + - React-CoreModules + - React-cxxreact + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-RCTActionSheet + - React-RCTAnimation + - React-RCTBlob + - React-RCTImage + - React-RCTLinking + - React-RCTNetwork + - React-RCTSettings + - React-RCTText + - ReactCommon/turbomodule/core + - Yoga + - RNSVG (13.4.0): - React-Core - SocketRocket (0.6.0) - Yoga (1.14.0) @@ -434,6 +461,7 @@ DEPENDENCIES: - React-RCTVibration (from `../node_modules/react-native/Libraries/Vibration`) - React-runtimeexecutor (from `../node_modules/react-native/ReactCommon/runtimeexecutor`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - RNReanimated (from `../node_modules/react-native-reanimated`) - RNSVG (from `../node_modules/react-native-svg`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) @@ -522,6 +550,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/runtimeexecutor" ReactCommon: :path: "../node_modules/react-native/ReactCommon" + RNReanimated: + :path: "../node_modules/react-native-reanimated" RNSVG: :path: "../node_modules/react-native-svg" Yoga: @@ -530,7 +560,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: a7c83b31436843459a1961bfd74b96033dc77234 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 - DoubleConversion: 831926d9b8bf8166fd87886c4abab286c2422662 + DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 FBLazyVector: 6c76fe46345039d5cf0549e9ddaf5aa169630a4a FBReactNativeSpec: 1a270246542f5c52248cb26a96db119cfe3cb760 Flipper: 26fc4b7382499f1281eb8cb921e5c3ad6de91fe0 @@ -543,7 +573,7 @@ SPEC CHECKSUMS: Flipper-RSocket: d9d9ade67cbecf6ac10730304bf5607266dd2541 FlipperKit: cbdee19bdd4e7f05472a66ce290f1b729ba3cb86 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 - glog: 476ee3e89abb49e07f822b48323c51c57124b572 + glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b hermes-engine: 8e84f1284180801c1a1b241f443ba64f931ff561 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c @@ -574,7 +604,8 @@ SPEC CHECKSUMS: React-RCTVibration: 5499b77c0fd57f346a5f0b36bb79fdb020d17d3e React-runtimeexecutor: 80c195ffcafb190f531fdc849dc2d19cb4bb2b34 ReactCommon: de55f940495d7bf87b5d7bf55b5b15cdd50d7d7b - RNSVG: 07037623c36f12e41312730622d5f6b3656227ca + RNReanimated: 60e291d42c77752a0f6d6f358387bdf225a87c6e + RNSVG: 07dbd870b0dcdecc99b3a202fa37c8ca163caec2 SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608 Yoga: 82c9e8f652789f67d98bed5aef9d6653f71b04a9 YogaKit: f782866e155069a2cca2517aafea43200b01fd5a diff --git a/Example/src/App.tsx b/Example/src/App.tsx index 0212df79..fa201960 100644 --- a/Example/src/App.tsx +++ b/Example/src/App.tsx @@ -113,6 +113,7 @@ const names: (keyof typeof examples)[] = [ 'PanResponder', 'Reusable', 'Reanimated', + 'Transforms', ]; const initialState = { diff --git a/Example/src/examples.tsx b/Example/src/examples.tsx index 64a80e95..8d39d354 100644 --- a/Example/src/examples.tsx +++ b/Example/src/examples.tsx @@ -16,6 +16,7 @@ import * as Reusable from './examples/Reusable'; import * as TouchEvents from './examples/TouchEvents'; import * as PanResponder from './examples/PanResponder'; import * as Reanimated from './examples/Reanimated'; +import * as Transforms from './examples/Transforms'; export { Svg, @@ -36,4 +37,5 @@ export { Reusable, PanResponder, Reanimated, + Transforms, }; diff --git a/Example/src/examples/Transforms.tsx b/Example/src/examples/Transforms.tsx new file mode 100644 index 00000000..7316a5c4 --- /dev/null +++ b/Example/src/examples/Transforms.tsx @@ -0,0 +1,134 @@ +import React, {Component} from 'react'; +import {Platform} from 'react-native'; + +import { + Svg, + Circle, + Rect, + Pattern, + RadialGradient, + Stop, + SvgXml, +} from 'react-native-svg'; + +const patternXml = ` + + + + + + + + +`; + +class PatternTransformExample extends Component { + static title = 'Pattern transform'; + render() { + return ( + <> + + + + + + + + + + + {Platform.OS !== 'web' && ( + + )} + + ); + } +} + +class GradientTransformExample extends Component { + static title = 'Gradient transform'; + render() { + return ( + + + + + + + + + + + + + + + + ); + } +} + +const icon = ( + + + + + + +); + +const samples = [PatternTransformExample, GradientTransformExample]; + +export {icon, samples}; diff --git a/FabricExample/ios/Podfile.lock b/FabricExample/ios/Podfile.lock index 42c743fd..689aca65 100644 --- a/FabricExample/ios/Podfile.lock +++ b/FabricExample/ios/Podfile.lock @@ -697,7 +697,7 @@ PODS: - React-jsi (= 0.70.0) - React-logger (= 0.70.0) - React-perflogger (= 0.70.0) - - RNSVG (13.2.0): + - RNSVG (13.4.0): - RCT-Folly - RCTRequired - RCTTypeSafety @@ -927,7 +927,7 @@ SPEC CHECKSUMS: React-rncore: 8858fe6b719170c20c197a8fd2dd53507bdae04b React-runtimeexecutor: 80c195ffcafb190f531fdc849dc2d19cb4bb2b34 ReactCommon: de55f940495d7bf87b5d7bf55b5b15cdd50d7d7b - RNSVG: fa7f6f437c90eea1fbc3d142a40365d561824ab3 + RNSVG: 8ef4c60d9378eab6996a3f006dfb5784e6dab302 SocketRocket: fccef3f9c5cedea1353a9ef6ada904fde10d6608 Yoga: 82c9e8f652789f67d98bed5aef9d6653f71b04a9 YogaKit: f782866e155069a2cca2517aafea43200b01fd5a diff --git a/FabricExample/src/App.tsx b/FabricExample/src/App.tsx index 7ed84db5..c55fe8c4 100644 --- a/FabricExample/src/App.tsx +++ b/FabricExample/src/App.tsx @@ -112,6 +112,7 @@ const names = [ 'TouchEvents', 'PanResponder', 'Reusable', + 'Transforms', ]; const initialState = { diff --git a/FabricExample/src/examples.tsx b/FabricExample/src/examples.tsx index a1d56f38..908ed89b 100644 --- a/FabricExample/src/examples.tsx +++ b/FabricExample/src/examples.tsx @@ -15,6 +15,7 @@ import * as Image from './examples/Image'; import * as Reusable from './examples/Reusable'; import * as TouchEvents from './examples/TouchEvents'; import * as PanResponder from './examples/PanResponder'; +import * as Transforms from './examples/Transforms'; export { Svg, @@ -34,4 +35,5 @@ export { TouchEvents, Reusable, PanResponder, + Transforms, }; diff --git a/FabricExample/src/examples/Transforms.tsx b/FabricExample/src/examples/Transforms.tsx new file mode 100644 index 00000000..7316a5c4 --- /dev/null +++ b/FabricExample/src/examples/Transforms.tsx @@ -0,0 +1,134 @@ +import React, {Component} from 'react'; +import {Platform} from 'react-native'; + +import { + Svg, + Circle, + Rect, + Pattern, + RadialGradient, + Stop, + SvgXml, +} from 'react-native-svg'; + +const patternXml = ` + + + + + + + + +`; + +class PatternTransformExample extends Component { + static title = 'Pattern transform'; + render() { + return ( + <> + + + + + + + + + + + {Platform.OS !== 'web' && ( + + )} + + ); + } +} + +class GradientTransformExample extends Component { + static title = 'Gradient transform'; + render() { + return ( + + + + + + + + + + + + + + + + ); + } +} + +const icon = ( + + + + + + +); + +const samples = [PatternTransformExample, GradientTransformExample]; + +export {icon, samples}; diff --git a/android/src/main/java/com/horcrux/svg/MaskView.java b/android/src/main/java/com/horcrux/svg/MaskView.java index 8b974fce..c91503aa 100644 --- a/android/src/main/java/com/horcrux/svg/MaskView.java +++ b/android/src/main/java/com/horcrux/svg/MaskView.java @@ -9,13 +9,8 @@ package com.horcrux.svg; import android.annotation.SuppressLint; -import android.graphics.Matrix; -import com.facebook.common.logging.FLog; import com.facebook.react.bridge.Dynamic; import com.facebook.react.bridge.ReactContext; -import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.common.ReactConstants; -import javax.annotation.Nullable; @SuppressLint("ViewConstructor") class MaskView extends GroupView { @@ -32,14 +27,6 @@ class MaskView extends GroupView { @SuppressWarnings({"FieldCanBeLocal", "unused"}) private Brush.BrushUnits mMaskContentUnits; - private static final float[] sRawMatrix = - new float[] { - 1, 0, 0, - 0, 1, 0, - 0, 0, 1 - }; - private Matrix mMatrix = null; - public MaskView(ReactContext reactContext) { super(reactContext); } @@ -128,24 +115,6 @@ class MaskView extends GroupView { invalidate(); } - public void setMaskTransform(@Nullable ReadableArray matrixArray) { - if (matrixArray != null) { - int matrixSize = PropHelper.toMatrixData(matrixArray, sRawMatrix, mScale); - if (matrixSize == 6) { - if (mMatrix == null) { - mMatrix = new Matrix(); - } - mMatrix.setValues(sRawMatrix); - } else if (matrixSize != -1) { - FLog.w(ReactConstants.TAG, "RNSVG: Transform matrices must be of size 6"); - } - } else { - mMatrix = null; - } - - invalidate(); - } - @Override void saveDefinition() { if (mName != null) { diff --git a/android/src/main/java/com/horcrux/svg/RenderableView.java b/android/src/main/java/com/horcrux/svg/RenderableView.java index 2aa196cd..3cb4df9c 100644 --- a/android/src/main/java/com/horcrux/svg/RenderableView.java +++ b/android/src/main/java/com/horcrux/svg/RenderableView.java @@ -27,9 +27,8 @@ 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 com.facebook.react.uimanager.PointerEvents; import com.facebook.react.touch.ReactHitSlopView; - +import com.facebook.react.uimanager.PointerEvents; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.regex.Matcher; @@ -37,7 +36,7 @@ import java.util.regex.Pattern; import javax.annotation.Nullable; @SuppressWarnings({"WeakerAccess", "RedundantSuppression"}) -abstract public class RenderableView extends VirtualView implements ReactHitSlopView { +public abstract class RenderableView extends VirtualView implements ReactHitSlopView { RenderableView(ReactContext reactContext) { super(reactContext); @@ -96,23 +95,23 @@ abstract public class RenderableView extends VirtualView implements ReactHitSlop private static final Pattern regex = Pattern.compile("[0-9.-]+"); - @Nullable - public Rect getHitSlopRect() { - /* - * In order to make the isTouchPointInView fail we need to return a very improbable Rect for the View - * This way an SVG with box_none carrying its last descendent with box_none will have the expected behavior of just having events on the actual painted area - */ - if (mPointerEvents == PointerEvents.BOX_NONE) { - return new Rect(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE); - } - return null; + @Nullable + public Rect getHitSlopRect() { + /* + * In order to make the isTouchPointInView fail we need to return a very improbable Rect for the View + * This way an SVG with box_none carrying its last descendent with box_none will have the expected behavior of just having events on the actual painted area + */ + if (mPointerEvents == PointerEvents.BOX_NONE) { + return new Rect(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE); } + return null; + } - @Override - public void setId(int id) { - super.setId(id); - RenderableViewManager.setRenderableView(id, this); - } + @Override + public void setId(int id) { + super.setId(id); + RenderableViewManager.setRenderableView(id, this); + } public void setVectorEffect(int vectorEffect) { this.vectorEffect = vectorEffect; diff --git a/android/src/main/java/com/horcrux/svg/RenderableViewManager.java b/android/src/main/java/com/horcrux/svg/RenderableViewManager.java index 72180eeb..5614f413 100644 --- a/android/src/main/java/com/horcrux/svg/RenderableViewManager.java +++ b/android/src/main/java/com/horcrux/svg/RenderableViewManager.java @@ -1714,11 +1714,6 @@ class RenderableViewManager extends VirtualViewManager public void setMaskContentUnits(MaskView node, int maskContentUnits) { node.setMaskContentUnits(maskContentUnits); } - - @ReactProp(name = "maskTransform") - public void setMaskTransform(MaskView node, @Nullable ReadableArray matrixArray) { - node.setMaskTransform(matrixArray); - } } static class ForeignObjectManager extends GroupViewManagerAbstract 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 849f8ed5..8730b0a1 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGMaskManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSVGMaskManagerDelegate.java @@ -167,9 +167,6 @@ public class RNSVGMaskManagerDelegate { void setWidth(T view, @Nullable Double value); void setMaskUnits(T view, int value); void setMaskContentUnits(T view, int value); - void setMaskTransform(T view, @Nullable ReadableArray value); } diff --git a/apple/Elements/RNSVGMask.h b/apple/Elements/RNSVGMask.h index a710c76b..bea4e4d7 100644 --- a/apple/Elements/RNSVGMask.h +++ b/apple/Elements/RNSVGMask.h @@ -10,6 +10,5 @@ @property (nonatomic, strong) RNSVGLength *maskheight; @property (nonatomic, assign) RNSVGUnits maskUnits; @property (nonatomic, assign) RNSVGUnits maskContentUnits; -@property (nonatomic, assign) CGAffineTransform maskTransform; @end diff --git a/apple/Elements/RNSVGMask.mm b/apple/Elements/RNSVGMask.mm index 7c58b13b..e8381854 100644 --- a/apple/Elements/RNSVGMask.mm +++ b/apple/Elements/RNSVGMask.mm @@ -53,15 +53,6 @@ using namespace facebook::react; } self.maskUnits = newProps.maskUnits == 0 ? kRNSVGUnitsObjectBoundingBox : kRNSVGUnitsUserSpaceOnUse; self.maskContentUnits = newProps.maskUnits == 0 ? kRNSVGUnitsObjectBoundingBox : kRNSVGUnitsUserSpaceOnUse; - if (newProps.maskTransform.size() == 6) { - self.maskTransform = CGAffineTransformMake( - newProps.maskTransform.at(0), - newProps.maskTransform.at(1), - newProps.maskTransform.at(2), - newProps.maskTransform.at(3), - newProps.maskTransform.at(4), - newProps.maskTransform.at(5)); - } setCommonGroupProps(newProps, self); _props = std::static_pointer_cast(props); @@ -76,7 +67,6 @@ using namespace facebook::react; _maskwidth = nil; _maskUnits = kRNSVGUnitsObjectBoundingBox; _maskContentUnits = kRNSVGUnitsObjectBoundingBox; - _maskTransform = CGAffineTransformIdentity; } #endif // RN_FABRIC_ENABLED @@ -151,12 +141,6 @@ using namespace facebook::react; [self invalidate]; } -- (void)setMaskTransform:(CGAffineTransform)maskTransform -{ - _maskTransform = maskTransform; - [self invalidate]; -} - @end #ifdef RN_FABRIC_ENABLED diff --git a/apple/Elements/RNSVGPattern.mm b/apple/Elements/RNSVGPattern.mm index 3bd7a053..663630e9 100644 --- a/apple/Elements/RNSVGPattern.mm +++ b/apple/Elements/RNSVGPattern.mm @@ -52,7 +52,8 @@ using namespace facebook::react; self.patternwidth = [RNSVGLength lengthWithString:RCTNSStringFromString(newProps.width)]; } self.patternUnits = newProps.patternUnits == 0 ? kRNSVGUnitsObjectBoundingBox : kRNSVGUnitsUserSpaceOnUse; - self.patternContentUnits = newProps.patternUnits == 0 ? kRNSVGUnitsObjectBoundingBox : kRNSVGUnitsUserSpaceOnUse; + self.patternContentUnits = + newProps.patternContentUnits == 0 ? kRNSVGUnitsObjectBoundingBox : kRNSVGUnitsUserSpaceOnUse; if (newProps.patternTransform.size() == 6) { self.patternTransform = CGAffineTransformMake( newProps.patternTransform.at(0), diff --git a/apple/ViewManagers/RNSVGMaskManager.mm b/apple/ViewManagers/RNSVGMaskManager.mm index bcc53257..8dbe8e7c 100644 --- a/apple/ViewManagers/RNSVGMaskManager.mm +++ b/apple/ViewManagers/RNSVGMaskManager.mm @@ -30,6 +30,5 @@ RCT_CUSTOM_VIEW_PROPERTY(width, id, RNSVGMask) } RCT_EXPORT_VIEW_PROPERTY(maskUnits, RNSVGUnits) RCT_EXPORT_VIEW_PROPERTY(maskContentUnits, RNSVGUnits) -RCT_EXPORT_VIEW_PROPERTY(maskTransform, CGAffineTransform) @end diff --git a/src/ReactNativeSVG.web.ts b/src/ReactNativeSVG.web.ts index 20af248e..b2863026 100644 --- a/src/ReactNativeSVG.web.ts +++ b/src/ReactNativeSVG.web.ts @@ -1,20 +1,15 @@ -// @ts-ignore import * as React from 'react'; import { GestureResponderEvent, // @ts-ignore unstable_createElement as ucE, - // @ts-ignore createElement as cE, + TransformsStyle, } from 'react-native'; -import { - ColumnMajorTransformMatrix, - NumberArray, - NumberProp, - TransformObject, -} from './lib/extract/types'; +import { NumberArray, NumberProp, TransformProps } from './lib/extract/types'; import SvgTouchableMixin from './lib/SvgTouchableMixin'; import { resolve } from './lib/resolve'; +import { transformsArrayToProps } from './lib/extract/extractTransform'; const createElement = cE || ucE; @@ -49,19 +44,19 @@ interface BaseProps { pressRetentionOffset?: EdgeInsetsProp; rejectResponderTermination?: boolean; - transform: ColumnMajorTransformMatrix | string | TransformObject; - translate: NumberArray; - translateX: NumberProp; - translateY: NumberProp; - scale: NumberArray; - scaleX: NumberProp; - scaleY: NumberProp; - rotation: NumberArray; - skewX: NumberProp; - skewY: NumberProp; - origin: NumberArray; - originX: NumberProp; - originY: NumberProp; + transform?: TransformProps['transform']; + translate?: NumberArray; + translateX?: NumberProp; + translateY?: NumberProp; + scale?: NumberArray; + scaleX?: NumberProp; + scaleY?: NumberProp; + rotation?: NumberProp; + skewX?: NumberProp; + skewY?: NumberProp; + origin?: NumberArray; + originX?: NumberProp; + originY?: NumberProp; fontStyle?: string; fontWeight?: NumberProp; @@ -71,6 +66,10 @@ interface BaseProps { | React.RefCallback | React.MutableRefObject; style: Iterable<{}>; + + // different tranform props + gradientTransform: TransformProps['transform']; + patternTransform: TransformProps['transform']; } const hasTouchableProperty = (props: BaseProps) => @@ -80,6 +79,63 @@ const camelCaseToDashed = (camelCase: string) => { return camelCase.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()); }; +function stringifyTransformProps(transformProps: TransformProps) { + const transformArray = []; + if (transformProps.translate != null) { + transformArray.push(`translate(${transformProps.translate})`); + } + if (transformProps.translateX != null || transformProps.translateY != null) { + transformArray.push( + `translate(${transformProps.translateX || 0}, ${ + transformProps.translateY || 0 + })`, + ); + } + if (transformProps.scale != null) { + transformArray.push(`scale(${transformProps.scale})`); + } + if (transformProps.scaleX != null || transformProps.scaleY != null) { + transformArray.push( + `scale(${transformProps.scaleX || 1}, ${transformProps.scaleY || 1})`, + ); + } + // rotation maps to rotate, not to collide with the text rotate attribute (which acts per glyph rather than block) + if (transformProps.rotation != null) { + transformArray.push(`rotate(${transformProps.rotation})`); + } + if (transformProps.skewX != null) { + transformArray.push(`skewX(${transformProps.skewX})`); + } + if (transformProps.skewY != null) { + transformArray.push(`skewY(${transformProps.skewY})`); + } + return transformArray; +} + +function parseTransformProp( + transform: TransformProps['transform'], + props?: BaseProps, +) { + const transformArray: string[] = []; + + props && transformArray.push(...stringifyTransformProps(props)); + + if (Array.isArray(transform)) { + if (typeof transform[0] === 'number') { + transformArray.push(`matrix(${transform.join(' ')})`); + } else { + const stringifiedProps = transformsArrayToProps( + transform as TransformsStyle['transform'], + ); + transformArray.push(...stringifyTransformProps(stringifiedProps)); + } + } else if (typeof transform === 'string') { + transformArray.push(transform); + } + + return transformArray.length ? transformArray.join(' ') : undefined; +} + /** * `react-native-svg` supports additional props that aren't defined in the spec. * This function replaces them in a spec conforming manner. @@ -95,15 +151,6 @@ const prepare = ( ) => { const { transform, - translate, - translateX, - translateY, - scale, - scaleX, - scaleY, - rotation, - skewX, - skewY, origin, originX, originY, @@ -113,7 +160,8 @@ const prepare = ( fontStyle, style, forwardedRef, - // @ts-ignore + gradientTransform, + patternTransform, ...rest } = props; @@ -124,7 +172,10 @@ const prepare = ( onResponderRelease?: (e: GestureResponderEvent) => void; onResponderTerminate?: (e: GestureResponderEvent) => void; onResponderTerminationRequest?: (e: GestureResponderEvent) => boolean; - transform?: ColumnMajorTransformMatrix | string | TransformObject; + transform?: string; + gradientTransform?: string; + patternTransform?: string; + 'transform-origin'?: string; style?: {}; ref?: {}; } = { @@ -143,83 +194,15 @@ const prepare = ( ...rest, }; - const transformArray = []; - - if (Array.isArray(transform) && transform.length === 6) { - transformArray.push(`matrix(${transform.join(' ')})`); - } else if (typeof transform === 'object') { - for (const key in transform) { - const value = transform[key]; - // non standard SVG transforms - if (key === 'translateX' || key === 'x') { - transformArray.push(`translate(${value} 0)`); - } else if (key === 'translateY' || key === 'y') { - transformArray.push(`translate(0 ${value})`); - } else if (key === 'originX') { - transformArray.push(`translate(${-value} 0)`); - } else if (key === 'originY') { - transformArray.push(`translate(0 ${-value})`); - } else if (key === 'origin') { - if (Array.isArray(value)) { - transformArray.push(`translate(${value.join(' ')})`); - } else { - transformArray.push(`translate(${value})`); - } - } else if (key === 'scaleX') { - transformArray.push(`scaleX(${value} 1)`); - } else if (key === 'scaleY') { - transformArray.push(`scaleX(1 ${value})`); - } else if (key === 'skew') { - if (Array.isArray(value) && value.length === 2) { - transformArray.push(`skewX(${value[0]})`); - transformArray.push(`skewY(${value[1]})`); - } else { - throw new Error('Skew prop expect array of numbers'); - } - } else { - if (Array.isArray(value)) { - transformArray.push(`${key}(${value.join(' ')})`); - } else { - transformArray.push(`${key}(${transform[key]})`); - } - } - } - } else { - transformArray.push(transform); - } - - if (translate != null) { - transformArray.push(`translate(${translate})`); - } - if (translateX != null || translateY != null) { - transformArray.push(`translate(${translateX || 0}, ${translateY || 0})`); - } - if (scale != null) { - transformArray.push(`scale(${scale})`); - } - if (scaleX != null || scaleY != null) { - transformArray.push(`scale(${scaleX || 1}, ${scaleY || 1})`); - } - // rotation maps to rotate, not to collide with the text rotate attribute (which acts per glyph rather than block) - if (rotation != null) { - transformArray.push(`rotate(${rotation})`); - } - if (skewX != null) { - transformArray.push(`skewX(${skewX})`); - } - if (skewY != null) { - transformArray.push(`skewY(${skewY})`); - } if (origin != null) { - transformArray.push(`translate(${origin})`); - } - if (originX != null || originY != null) { - transformArray.push(`translate(${-originX || 0}, ${-originY || 0})`); + clean['transform-origin'] = origin.toString().replace(',', ' '); + } else if (originX != null || originY != null) { + clean['transform-origin'] = `${originX || 0} ${originY || 0}`; } - if (transformArray.length) { - clean.transform = transformArray.join(' '); - } + clean.transform = parseTransformProp(transform, props); + clean.gradientTransform = parseTransformProp(gradientTransform); + clean.patternTransform = parseTransformProp(patternTransform); clean.ref = (el: SVGElement | null) => { self.elementRef.current = el; @@ -249,7 +232,6 @@ const prepare = ( if (fontStyle != null) { styles.fontStyle = fontStyle; } - clean.style = resolve(style, styles); return clean; diff --git a/src/elements/LinearGradient.tsx b/src/elements/LinearGradient.tsx index 88793138..b100d804 100644 --- a/src/elements/LinearGradient.tsx +++ b/src/elements/LinearGradient.tsx @@ -1,10 +1,6 @@ import React, { ReactElement } from 'react'; import extractGradient from '../lib/extract/extractGradient'; -import { - ColumnMajorTransformMatrix, - NumberProp, - Units, -} from '../lib/extract/types'; +import { NumberProp, TransformProps, Units } from '../lib/extract/types'; import Shape from './Shape'; import { RNSVGLinearGradient } from './NativeComponents'; import { stringifyPropsForFabric } from '../lib/extract/extractProps'; @@ -16,7 +12,7 @@ export interface LinearGradientProps { y1?: NumberProp; y2?: NumberProp; gradientUnits?: Units; - gradientTransform?: ColumnMajorTransformMatrix | string; + gradientTransform?: TransformProps['transform']; id?: string; } diff --git a/src/elements/Mask.tsx b/src/elements/Mask.tsx index e8108cea..d524c6af 100644 --- a/src/elements/Mask.tsx +++ b/src/elements/Mask.tsx @@ -1,14 +1,9 @@ import React, { ReactNode } from 'react'; -import extractTransform from '../lib/extract/extractTransform'; import { stringifyPropsForFabric, withoutXY, } from '../lib/extract/extractProps'; -import { - ColumnMajorTransformMatrix, - CommonPathProps, - NumberProp, -} from '../lib/extract/types'; +import { CommonPathProps, NumberProp } from '../lib/extract/types'; import units from '../lib/units'; import Shape from './Shape'; import { RNSVGMask } from './NativeComponents'; @@ -22,7 +17,6 @@ export interface MaskProps extends CommonPathProps { y?: NumberProp; width?: NumberProp; height?: NumberProp; - maskTransform?: ColumnMajorTransformMatrix | string; maskUnits?: TMaskUnits; maskContentUnits?: TMaskUnits; } @@ -39,17 +33,8 @@ export default class Mask extends Shape { render() { const { props } = this; - const { - maskTransform, - transform, - x, - y, - width, - height, - maskUnits, - maskContentUnits, - children, - } = props; + const { x, y, width, height, maskUnits, maskContentUnits, children } = + props; const strigifiedMaskProps = stringifyPropsForFabric({ x, y, @@ -57,7 +42,6 @@ export default class Mask extends Shape { height, }); const maskProps = { - maskTransform: extractTransform(maskTransform || transform || props), maskUnits: maskUnits !== undefined ? units[maskUnits] : 0, maskContentUnits: maskContentUnits !== undefined ? units[maskContentUnits] : 1, diff --git a/src/elements/Pattern.tsx b/src/elements/Pattern.tsx index 0746cd5f..35ff3b57 100644 --- a/src/elements/Pattern.tsx +++ b/src/elements/Pattern.tsx @@ -1,30 +1,24 @@ import React, { ReactNode } from 'react'; import extractTransform from '../lib/extract/extractTransform'; import extractViewBox from '../lib/extract/extractViewBox'; -import { - ColumnMajorTransformMatrix, - NumberProp, - TransformProps, - Units, -} from '../lib/extract/types'; +import { NumberProp, TransformProps, Units } from '../lib/extract/types'; import units from '../lib/units'; import Shape from './Shape'; import { RNSVGPattern } from './NativeComponents'; import { stringifyPropsForFabric } from '../lib/extract/extractProps'; -export interface PatternProps { +export interface PatternProps extends TransformProps { children?: ReactNode; id?: string; x?: NumberProp; y?: NumberProp; width?: NumberProp; height?: NumberProp; - patternTransform?: ColumnMajorTransformMatrix | string; + patternTransform?: TransformProps['transform']; patternUnits?: Units; patternContentUnits?: Units; viewBox?: string; preserveAspectRatio?: string; - transform?: ColumnMajorTransformMatrix | string | TransformProps; } export default class Pattern extends Shape { diff --git a/src/elements/RadialGradient.tsx b/src/elements/RadialGradient.tsx index 6925bc1e..e444d903 100644 --- a/src/elements/RadialGradient.tsx +++ b/src/elements/RadialGradient.tsx @@ -1,10 +1,6 @@ import React, { ReactElement } from 'react'; import extractGradient from '../lib/extract/extractGradient'; -import { - ColumnMajorTransformMatrix, - NumberProp, - Units, -} from '../lib/extract/types'; +import { NumberProp, TransformProps, Units } from '../lib/extract/types'; import Shape from './Shape'; import { RNSVGRadialGradient } from './NativeComponents'; import { stringifyPropsForFabric } from '../lib/extract/extractProps'; @@ -19,7 +15,7 @@ export interface RadialGradientProps { cy?: NumberProp; r?: NumberProp; gradientUnits?: Units; - gradientTransform?: ColumnMajorTransformMatrix | string; + gradientTransform?: TransformProps['transform']; id?: string; } diff --git a/src/elements/Shape.tsx b/src/elements/Shape.tsx index 1273360c..7b7ff2d0 100644 --- a/src/elements/Shape.tsx +++ b/src/elements/Shape.tsx @@ -1,7 +1,10 @@ import { Component } from 'react'; import SvgTouchableMixin from '../lib/SvgTouchableMixin'; import { NativeModules, findNodeHandle, NativeMethods } from 'react-native'; -import { TransformProps } from '../lib/extract/types'; +import { + ColumnMajorTransformMatrix, + TransformProps, +} from '../lib/extract/types'; const { RNSVGRenderableManager } = NativeModules; export interface SVGBoundingBoxOptions { @@ -241,7 +244,7 @@ export default class Shape

extends Component

{ }; setNativeProps = ( props: Object & { - matrix?: [number, number, number, number, number, number]; + matrix?: ColumnMajorTransformMatrix; } & TransformProps, ) => { this.root && this.root.setNativeProps(props); diff --git a/src/elements/TSpan.tsx b/src/elements/TSpan.tsx index 82fb149d..f415a486 100644 --- a/src/elements/TSpan.tsx +++ b/src/elements/TSpan.tsx @@ -5,6 +5,7 @@ import extractText, { setTSpan, TextChild } from '../lib/extract/extractText'; import { pickNotNil } from '../lib/util'; import Shape from './Shape'; import { + ColumnMajorTransformMatrix, CommonPathProps, FontProps, NumberArray, @@ -28,7 +29,7 @@ export default class TSpan extends Shape { setNativeProps = ( props: Object & { - matrix?: number[]; + matrix?: ColumnMajorTransformMatrix; style?: [] | {}; } & TransformProps, ) => { diff --git a/src/elements/Text.tsx b/src/elements/Text.tsx index fbe98728..9f8778b4 100644 --- a/src/elements/Text.tsx +++ b/src/elements/Text.tsx @@ -3,6 +3,7 @@ import extractText from '../lib/extract/extractText'; import extractProps, { propsAndStyles } from '../lib/extract/extractProps'; import extractTransform from '../lib/extract/extractTransform'; import { + ColumnMajorTransformMatrix, NumberArray, NumberProp, TextSpecificProps, @@ -29,7 +30,7 @@ export default class Text extends Shape { setNativeProps = ( props: Object & { - matrix?: number[]; + matrix?: ColumnMajorTransformMatrix; style?: [] | {}; } & TransformProps, ) => { diff --git a/src/elements/TextPath.tsx b/src/elements/TextPath.tsx index d5e9ecdf..0b68e85b 100644 --- a/src/elements/TextPath.tsx +++ b/src/elements/TextPath.tsx @@ -2,6 +2,7 @@ import React, { Component } from 'react'; import extractTransform from '../lib/extract/extractTransform'; import { withoutXY } from '../lib/extract/extractProps'; import { + ColumnMajorTransformMatrix, NumberProp, TextPathMethod, TextPathMidLine, @@ -31,7 +32,7 @@ export default class TextPath extends Shape { setNativeProps = ( props: Object & { - matrix?: number[]; + matrix?: ColumnMajorTransformMatrix; style?: [] | {}; } & TransformProps, ) => { diff --git a/src/fabric/MaskNativeComponent.ts b/src/fabric/MaskNativeComponent.ts index 9a27bc08..a32edcab 100644 --- a/src/fabric/MaskNativeComponent.ts +++ b/src/fabric/MaskNativeComponent.ts @@ -78,7 +78,6 @@ interface NativeProps width?: string; maskUnits?: Int32; maskContentUnits?: Int32; - maskTransform?: ReadonlyArray; } export default codegenNativeComponent('RNSVGMask'); diff --git a/src/index.js.flow b/src/index.js.flow index 76eead2c..dcf38f46 100644 --- a/src/index.js.flow +++ b/src/index.js.flow @@ -442,7 +442,6 @@ export type MaskProps = { y?: NumberProp, width?: NumberProp, height?: NumberProp, - maskTransform?: ColumnMajorTransformMatrix | string, maskUnits?: Units, maskContentUnits?: Units, ... diff --git a/src/lib/extract/extractGradient.ts b/src/lib/extract/extractGradient.ts index 5014b10c..49843cee 100644 --- a/src/lib/extract/extractGradient.ts +++ b/src/lib/extract/extractGradient.ts @@ -41,8 +41,8 @@ export default function extractGradient( props: { id?: string; children?: ReactElement[]; - transform?: number[] | string | TransformProps; - gradientTransform?: number[] | string | TransformProps; + transform?: TransformProps['transform']; + gradientTransform?: TransformProps['transform']; gradientUnits?: 'objectBoundingBox' | 'userSpaceOnUse'; } & TransformProps, parent: {}, diff --git a/src/lib/extract/extractProps.ts b/src/lib/extract/extractProps.ts index 41c703b8..084abfd5 100644 --- a/src/lib/extract/extractProps.ts +++ b/src/lib/extract/extractProps.ts @@ -1,6 +1,6 @@ import extractFill from './extractFill'; import extractStroke from './extractStroke'; -import { props2transform, transformToMatrix } from './extractTransform'; +import extractTransform from './extractTransform'; import extractResponder from './extractResponder'; import extractOpacity from './extractOpacity'; import { idPattern } from '../util'; @@ -49,7 +49,6 @@ export default function extractProps( display?: string; opacity?: NumberProp; onLayout?: () => void; - transform?: number[] | string | TransformProps; } & TransformProps & ResponderProps & StrokeProps & @@ -69,7 +68,6 @@ export default function extractProps( markerStart = marker, markerMid = marker, markerEnd = marker, - transform, } = props; const extracted: extractedProps = {}; @@ -82,8 +80,7 @@ export default function extractProps( extracted.propList = inherited; } - const transformProps = props2transform(props); - const matrix = transformToMatrix(transformProps, transform); + const matrix = extractTransform(props); if (matrix !== null) { extracted.matrix = matrix; } diff --git a/src/lib/extract/extractTransform.ts b/src/lib/extract/extractTransform.ts index b85ff1a1..155188a4 100644 --- a/src/lib/extract/extractTransform.ts +++ b/src/lib/extract/extractTransform.ts @@ -1,6 +1,12 @@ +import { TransformsStyle } from 'react-native'; import { append, appendTransform, identity, reset, toArray } from '../Matrix2D'; import { parse } from './transform'; -import { NumberProp, TransformedProps, TransformProps } from './types'; +import { + ColumnMajorTransformMatrix, + NumberProp, + TransformedProps, + TransformProps, +} from './types'; function appendTransformProps(props: TransformedProps) { const { x, y, originX, originY, scaleX, scaleY, rotation, skewX, skewY } = @@ -58,9 +64,33 @@ function universal2axis( return [x || defaultValue || 0, y || defaultValue || 0]; } +export function transformsArrayToProps( + transformObjectsArray: TransformsStyle['transform'], +) { + const props: TransformProps = {}; + transformObjectsArray?.forEach((transformObject) => { + const keys = Object.keys(transformObject); + if (keys.length !== 1) { + console.error( + 'You must specify exactly one property per transform object.', + ); + } + const key = keys[0] as keyof TransformProps; + const value = transformObject[key as keyof typeof transformObject]; + props[key] = value; + }); + return props; +} + export function props2transform( - props: TransformProps, + props: TransformProps | undefined, ): TransformedProps | null { + if (!props) { + return null; + } + const extractedProps = Array.isArray(props) + ? transformsArrayToProps(props) + : props; const { rotation, translate, @@ -77,7 +107,7 @@ export function props2transform( skewY, x, y, - } = props; + } = extractedProps; if ( rotation == null && translate == null && @@ -127,8 +157,8 @@ export function props2transform( export function transformToMatrix( props: TransformedProps | null, - transform: number[] | string | TransformProps | void | null | undefined, -): [number, number, number, number, number, number] | null { + transform: TransformProps['transform'], +): ColumnMajorTransformMatrix | null { if (!props && !transform) { return null; } @@ -138,16 +168,21 @@ export function transformToMatrix( if (transform) { if (Array.isArray(transform)) { if (typeof transform[0] === 'number') { + const columnMatrix = transform as ColumnMajorTransformMatrix; append( - transform[0], - transform[1], - transform[2], - transform[3], - transform[4], - transform[5], + columnMatrix[0], + columnMatrix[1], + columnMatrix[2], + columnMatrix[3], + columnMatrix[4], + columnMatrix[5], ); + } else { + const transformProps = props2transform( + transformsArrayToProps(transform as TransformsStyle['transform']), + ); + transformProps && appendTransformProps(transformProps); } - // noop for react-native transform arrays, let animated handle them } else if (typeof transform === 'string') { try { const t = parse(transform); @@ -165,10 +200,10 @@ export function transformToMatrix( } export default function extractTransform( - props: number[] | string | TransformProps, -) { - if (Array.isArray(props)) { - return props; + props: TransformProps | TransformProps['transform'], +): ColumnMajorTransformMatrix | null { + if (Array.isArray(props) && typeof props[0] === 'number') { + return props as ColumnMajorTransformMatrix; } if (typeof props === 'string') { try { @@ -179,5 +214,11 @@ export default function extractTransform( return identity; } } - return transformToMatrix(props2transform(props), props.transform); + // this type is not correct since props can be of type TransformsStyle['transform'] too + // but it satisfies TS and should not produce any type errors + const transformProps = props as TransformProps; + return transformToMatrix( + props2transform(transformProps), + transformProps?.transform, + ); } diff --git a/src/lib/extract/types.ts b/src/lib/extract/types.ts index 4e242eff..f459f414 100644 --- a/src/lib/extract/types.ts +++ b/src/lib/extract/types.ts @@ -5,6 +5,7 @@ import { LayoutChangeEvent, } from 'react-native'; import React from 'react'; +import type { TransformsStyle } from 'react-native'; export type NumberProp = string | number; export type NumberArray = NumberProp[] | NumberProp; @@ -162,24 +163,6 @@ export interface FontProps extends FontObject { font?: FontObject; } -export interface TransformObject { - translate?: NumberArray; - translateX?: NumberProp; - translateY?: NumberProp; - origin?: NumberArray; - originX?: NumberProp; - originY?: NumberProp; - scale?: NumberArray; - scaleX?: NumberProp; - scaleY?: NumberProp; - skew?: NumberArray; - skewX?: NumberProp; - skewY?: NumberProp; - rotation?: NumberProp; - x?: NumberArray; - y?: NumberArray; -} - /* ColumnMajorTransformMatrix @@ -204,9 +187,26 @@ export type ColumnMajorTransformMatrix = [ number, ]; -export interface TransformProps extends TransformObject { - transform?: ColumnMajorTransformMatrix | string | TransformObject; - // | TransformsStyle['transform']; // not used since it causes type problems +export interface TransformProps { + translate?: NumberArray; + translateX?: NumberProp; + translateY?: NumberProp; + origin?: NumberArray; + originX?: NumberProp; + originY?: NumberProp; + scale?: NumberArray; + scaleX?: NumberProp; + scaleY?: NumberProp; + skew?: NumberArray; + skewX?: NumberProp; + skewY?: NumberProp; + rotation?: NumberProp; + x?: NumberArray; + y?: NumberArray; + transform?: + | ColumnMajorTransformMatrix + | string + | TransformsStyle['transform']; } export interface TransformedProps {