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
This commit is contained in:
Wojciech Lewicki
2022-10-25 15:55:17 +02:00
committed by GitHub
parent 65f373b084
commit 6a5242f00b
32 changed files with 523 additions and 282 deletions

View File

@@ -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

View File

@@ -113,6 +113,7 @@ const names: (keyof typeof examples)[] = [
'PanResponder',
'Reusable',
'Reanimated',
'Transforms',
];
const initialState = {

View File

@@ -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,
};

View File

@@ -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 = `
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Apply a transform on the tile -->
<pattern
id="p1"
width=".25"
height=".25"
patternTransform="rotate(20)
skewX(30)
scale(1 0.5)">
<circle cx="10" cy="10" r="10" fill="black" />
</pattern>
<rect x="10" y="10" width="80" height="80" fill="url(#p1)" />
</svg>
`;
class PatternTransformExample extends Component {
static title = 'Pattern transform';
render() {
return (
<>
<Svg height="100" width="300" viewBox="0 0 200 100">
<Pattern
id="p1"
width="0.25"
height="0.25"
patternTransform={[
{rotate: '20'},
{skewX: '30'},
{scaleX: 1},
{scaleY: 0.5},
]}>
<Circle fill="black" cx="10" cy="10" r="10" />
</Pattern>
<Pattern
id="p2"
width="0.25"
height="0.25"
patternTransform="rotate(20) skewX(30) scale(1 0.5)">
<Circle fill="black" cx="10" cy="10" r="10" />
</Pattern>
<Rect x="10" y="10" width="80" height="80" fill="url(#p1)" />
<Rect
x="10"
y="10"
width="80"
height="80"
fill="url(#p2)"
transform={[{translateX: 100}]}
/>
</Svg>
{Platform.OS !== 'web' && (
<SvgXml width="100" height="100" xml={patternXml} />
)}
</>
);
}
}
class GradientTransformExample extends Component {
static title = 'Gradient transform';
render() {
return (
<Svg height="200" width="300" viewBox="0 0 420 200">
<RadialGradient
id="gradient1"
gradientUnits="userSpaceOnUse"
cx="100"
cy="100"
r="100"
fx="100"
fy="100">
<Stop offset="0%" stopColor="darkblue" />
<Stop offset="50%" stopColor="skyblue" />
<Stop offset="100%" stopColor="darkblue" />
</RadialGradient>
<RadialGradient
id="gradient2"
gradientUnits="userSpaceOnUse"
cx="100"
cy="100"
r="100"
fx="100"
fy="100"
gradientTransform="skewX(20) translate(-35, 0)">
<Stop offset="0%" stopColor="darkblue" />
<Stop offset="50%" stopColor="skyblue" />
<Stop offset="100%" stopColor="darkblue" />
</RadialGradient>
<Rect x="0" y="0" width="200" height="200" fill="url(#gradient1)" />
<Rect
x="0"
y="0"
width="200"
height="200"
fill="url(#gradient2)"
transform="translate(220)"
/>
</Svg>
);
}
}
const icon = (
<Svg height="30" width="30" viewBox="0 0 100 100">
<Pattern
id="p1"
width="0.25"
height="0.25"
patternTransform="rotate(20) skewX(30) scale(1 0.5)">
<Circle fill="black" cx="10" cy="10" r="10" />
</Pattern>
<Rect x="10" y="10" width="80" height="80" fill="url(#p1)" />
</Svg>
);
const samples = [PatternTransformExample, GradientTransformExample];
export {icon, samples};

View File

@@ -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

View File

@@ -112,6 +112,7 @@ const names = [
'TouchEvents',
'PanResponder',
'Reusable',
'Transforms',
];
const initialState = {

View File

@@ -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,
};

View File

@@ -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 = `
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<!-- Apply a transform on the tile -->
<pattern
id="p1"
width=".25"
height=".25"
patternTransform="rotate(20)
skewX(30)
scale(1 0.5)">
<circle cx="10" cy="10" r="10" fill="black" />
</pattern>
<rect x="10" y="10" width="80" height="80" fill="url(#p1)" />
</svg>
`;
class PatternTransformExample extends Component {
static title = 'Pattern transform';
render() {
return (
<>
<Svg height="100" width="300" viewBox="0 0 200 100">
<Pattern
id="p1"
width="0.25"
height="0.25"
patternTransform={[
{rotate: '20'},
{skewX: '30'},
{scaleX: 1},
{scaleY: 0.5},
]}>
<Circle fill="black" cx="10" cy="10" r="10" />
</Pattern>
<Pattern
id="p2"
width="0.25"
height="0.25"
patternTransform="rotate(20) skewX(30) scale(1 0.5)">
<Circle fill="black" cx="10" cy="10" r="10" />
</Pattern>
<Rect x="10" y="10" width="80" height="80" fill="url(#p1)" />
<Rect
x="10"
y="10"
width="80"
height="80"
fill="url(#p2)"
transform={[{translateX: 100}]}
/>
</Svg>
{Platform.OS !== 'web' && (
<SvgXml width="100" height="100" xml={patternXml} />
)}
</>
);
}
}
class GradientTransformExample extends Component {
static title = 'Gradient transform';
render() {
return (
<Svg height="200" width="300" viewBox="0 0 420 200">
<RadialGradient
id="gradient1"
gradientUnits="userSpaceOnUse"
cx="100"
cy="100"
r="100"
fx="100"
fy="100">
<Stop offset="0%" stopColor="darkblue" />
<Stop offset="50%" stopColor="skyblue" />
<Stop offset="100%" stopColor="darkblue" />
</RadialGradient>
<RadialGradient
id="gradient2"
gradientUnits="userSpaceOnUse"
cx="100"
cy="100"
r="100"
fx="100"
fy="100"
gradientTransform="skewX(20) translate(-35, 0)">
<Stop offset="0%" stopColor="darkblue" />
<Stop offset="50%" stopColor="skyblue" />
<Stop offset="100%" stopColor="darkblue" />
</RadialGradient>
<Rect x="0" y="0" width="200" height="200" fill="url(#gradient1)" />
<Rect
x="0"
y="0"
width="200"
height="200"
fill="url(#gradient2)"
transform="translate(220)"
/>
</Svg>
);
}
}
const icon = (
<Svg height="30" width="30" viewBox="0 0 100 100">
<Pattern
id="p1"
width="0.25"
height="0.25"
patternTransform="rotate(20) skewX(30) scale(1 0.5)">
<Circle fill="black" cx="10" cy="10" r="10" />
</Pattern>
<Rect x="10" y="10" width="80" height="80" fill="url(#p1)" />
</Svg>
);
const samples = [PatternTransformExample, GradientTransformExample];
export {icon, samples};

View File

@@ -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) {

View File

@@ -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;

View File

@@ -1714,11 +1714,6 @@ class RenderableViewManager<T extends RenderableView> 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<ForeignObjectView>

View File

@@ -167,9 +167,6 @@ public class RNSVGMaskManagerDelegate<T extends View, U extends BaseViewManagerI
case "maskContentUnits":
mViewManager.setMaskContentUnits(view, value == null ? 0 : ((Double) value).intValue());
break;
case "maskTransform":
mViewManager.setMaskTransform(view, (ReadableArray) value);
break;
default:
super.setProperty(view, propName, value);
}

View File

@@ -56,5 +56,4 @@ public interface RNSVGMaskManagerInterface<T extends View> {
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);
}

View File

@@ -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

View File

@@ -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<RNSVGMaskProps const>(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

View File

@@ -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),

View File

@@ -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

View File

@@ -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<SVGElement>
| React.MutableRefObject<SVGElement | null>;
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 = <T extends BaseProps>(
) => {
const {
transform,
translate,
translateX,
translateY,
scale,
scaleX,
scaleY,
rotation,
skewX,
skewY,
origin,
originX,
originY,
@@ -113,7 +160,8 @@ const prepare = <T extends BaseProps>(
fontStyle,
style,
forwardedRef,
// @ts-ignore
gradientTransform,
patternTransform,
...rest
} = props;
@@ -124,7 +172,10 @@ const prepare = <T extends BaseProps>(
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 = <T extends BaseProps>(
...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 = <T extends BaseProps>(
if (fontStyle != null) {
styles.fontStyle = fontStyle;
}
clean.style = resolve(style, styles);
return clean;

View File

@@ -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;
}

View File

@@ -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<MaskProps> {
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<MaskProps> {
height,
});
const maskProps = {
maskTransform: extractTransform(maskTransform || transform || props),
maskUnits: maskUnits !== undefined ? units[maskUnits] : 0,
maskContentUnits:
maskContentUnits !== undefined ? units[maskContentUnits] : 1,

View File

@@ -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<PatternProps> {

View File

@@ -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;
}

View File

@@ -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<P> extends Component<P> {
};
setNativeProps = (
props: Object & {
matrix?: [number, number, number, number, number, number];
matrix?: ColumnMajorTransformMatrix;
} & TransformProps,
) => {
this.root && this.root.setNativeProps(props);

View File

@@ -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<TSpanProps> {
setNativeProps = (
props: Object & {
matrix?: number[];
matrix?: ColumnMajorTransformMatrix;
style?: [] | {};
} & TransformProps,
) => {

View File

@@ -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<TextProps> {
setNativeProps = (
props: Object & {
matrix?: number[];
matrix?: ColumnMajorTransformMatrix;
style?: [] | {};
} & TransformProps,
) => {

View File

@@ -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<TextPathProps> {
setNativeProps = (
props: Object & {
matrix?: number[];
matrix?: ColumnMajorTransformMatrix;
style?: [] | {};
} & TransformProps,
) => {

View File

@@ -78,7 +78,6 @@ interface NativeProps
width?: string;
maskUnits?: Int32;
maskContentUnits?: Int32;
maskTransform?: ReadonlyArray<Float>;
}
export default codegenNativeComponent<NativeProps>('RNSVGMask');

View File

@@ -442,7 +442,6 @@ export type MaskProps = {
y?: NumberProp,
width?: NumberProp,
height?: NumberProp,
maskTransform?: ColumnMajorTransformMatrix | string,
maskUnits?: Units,
maskContentUnits?: Units,
...

View File

@@ -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: {},

View File

@@ -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;
}

View File

@@ -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,
);
}

View File

@@ -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 {